yau-plant-assistant/cube/model/alarms.yml
Claude d6b6f4f116 Fix two Cube measures that were invalid SQL
Hand-verifying the measures against the fixtures on lin001, per the Phase 5
gate. Two of them had never executed anywhere, and both failed outright rather
than returning a wrong number - which is the good version of this, but they
failed at the point an operator asks a question, not at review.

  - time_weighted_avg put LEAD() inside SUM(). Postgres rejects that flatly:
    "aggregate function calls cannot contain window function calls". The per
    sample duration now comes from the cube's source query, which changes
    sql_table to sql, and the measure just sums value * duration over duration.
    The last sample of each tag gets a NULL duration and SUM skips it, which is
    correct - how long it stood is not yet known.

    This is the measure that matters most later. On the regular one-minute
    fixtures it agrees with avg_value to thirteen decimal places
    (42.45934027777778 against 42.45934027777775), which proves it is wired up
    and proves nothing about imh, where the deadband makes samples irregular
    and the two will not agree. Re-verify it there.

  - p95_value applied the quality filter through a Cube measure `filters:`
    block, which lands outside the aggregate and cannot work on an ordered-set
    aggregate: "column process_values.quality must appear in the GROUP BY
    clause". Folded into the CASE inside PERCENTILE_CONT, whose NULL handling
    does the exclusion.

Also: the priority dimension said only SPILL and PUMP_TRIP are priority 1,
while the data has LEVEL_SIGNAL_FAULT at priority 1 too - correctly, losing the
level signal on a well that can spill is a priority 1 condition. That comment is
the line an engineer reads when checking a priority_1_count, so it disagreeing
with the data matters more than its length suggests.

eval cases H24 and H25 record the two failures, added before the fix.

Verified against hand-written SQL on the same pinned windows: p95_value
61.104999999999976 and time_weighted_avg 42.45934027777778 both match to the
floating point tail, as do sample_count, avg_value, max_value, min_value,
seconds_above_high_level_alarm (7680 = 128 samples x 60) and every measure in
alarms and operations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:36:20 +10:00

177 lines
6 KiB
YAML

# =============================================================================
# alarms.yml — alarm and event history.
#
# SOURCE: fixture.alarm_history while USE_FIXTURES=true. When imh is live this
# becomes the agreed imh alarm table and the column names below change with it.
# Nothing else in this file should need to change; that is the point of it.
#
# THREE DEFINITIONS THAT DECIDE WHETHER THE ANSWERS ARE RIGHT. They are here in
# comments because the person checking the number needs to read them, and they
# are not obvious from the measure names.
#
# 1. AN ALARM IS A TRANSITION INTO THE ACTIVE STATE.
# state = 'ACTIVE' only. RTN is the return-to-normal of the activation that
# preceded it, and ACK is an operator acknowledging one. Counting every row
# roughly doubles every answer. "6 times last week" must mean six
# activations.
#
# 2. "LAST WEEK" IS A ROLLING 7 x 24 h WINDOW IN SITE_TIMEZONE.
# Not the previous calendar week, not 7 calendar days. Storage is UTC and
# the conversion happens here, once. If someone means the calendar week they
# have to say so, and the answer must state the window it used.
#
# 3. CHATTERING IS 3 OR MORE ACTIVATIONS OF THE SAME TAG WITHIN 60 MINUTES.
# An arbitrary threshold, chosen to match the site's alarm rationalisation
# convention. It is stated in the answer whenever chattering is reported,
# because a different threshold gives a different story.
# =============================================================================
cubes:
- name: alarms
sql_table: fixture.alarm_history # -> imh alarm table at Phase 4
description: >
Alarm and event history for the Waterloo Road Pump Station. One row per
state transition. Activations only are counted as alarms.
joins:
- name: equipment
sql: "{CUBE}.equipment_id = {equipment}.equipment_id"
relationship: many_to_one
dimensions:
- name: alarm_id
sql: alarm_id
type: number
primary_key: true
- name: event_time
sql: event_time
type: time
description: Transition time. Stored UTC, presented in SITE_TIMEZONE.
- name: tag_id
sql: tag_id
type: string
- name: equipment_id
sql: equipment_id
type: string
- name: alarm_type
sql: alarm_type
type: string
description: >
HIGH_LEVEL, HIGH_HIGH_LEVEL, LOW_LOW_LEVEL, SPILL, PUMP_TRIP,
SEAL_LEAK, HIGH_VIBRATION, LEVEL_SIGNAL_FAULT, MAINS_FAILURE,
SETPOINT_REJECTED. These correspond to the bits of the PLC alarm
bitmask %QW17 - see db/seed/tags.csv, PS_STN_ALARM_BITMASK.
- name: state
sql: state
type: string
description: ACTIVE, RTN or ACK. Only ACTIVE counts as an alarm.
- name: priority
sql: priority
type: number
description: >
1 highest, 3 lowest. Priority 1 is SPILL, PUMP_TRIP and
LEVEL_SIGNAL_FAULT - losing the level signal on a well that can spill
is a priority 1 condition, and the fixtures already treat it as one.
This comment previously named only SPILL and PUMP_TRIP and disagreed
with the data, which matters because this is the line an engineer
reads when checking a priority_1_count.
- name: value
sql: value
type: number
description: Process value at the transition, in engineering_unit.
- name: is_fixture
sql: is_fixture
type: boolean
description: >
TRUE means this row came from db/002_fixtures.sql and is generated
test data, not plant history. The API surfaces this to the operator.
measures:
- name: alarm_count
type: count
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
description: >
Number of alarm ACTIVATIONS. Definition 1 above. This is the measure
behind "how many times did X alarm come up".
- name: transition_count
type: count
description: >
Every row including RTN and ACK. Diagnostics only - do not answer an
operator question with this.
- name: distinct_tags
sql: tag_id
type: count_distinct
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
description: How many different tags alarmed in the window.
- name: first_alarm
sql: event_time
type: min
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
- name: last_alarm
sql: event_time
type: max
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
- name: priority_1_count
type: count
filters:
- sql: "{CUBE}.state = 'ACTIVE' AND {CUBE}.priority = 1"
description: Priority 1 activations - trips and spills.
pre_aggregations:
# Keeps "count alarms last week" fast without repeatedly scanning imh.
# Materialised into pg-ai schema cube_preagg. Watch its growth on
# /datadisk; the retention policy is the refresh_key plus manual pruning.
- name: alarms_by_hour
measures: [alarm_count, distinct_tags, priority_1_count]
dimensions: [alarm_type, equipment_id, tag_id]
time_dimension: event_time
granularity: hour
partition_granularity: month
refresh_key:
every: 10 minutes
build_range_start:
sql: "SELECT now() - interval '180 days'"
build_range_end:
sql: "SELECT now()"
views:
- name: alarm_activity
description: >
Alarm activations joined to equipment, so a question about "Pump 02" can
be answered without the caller knowing which tags belong to it.
cubes:
- join_path: alarms
includes:
- event_time
- alarm_type
- tag_id
- state
- priority
- value
- is_fixture
- alarm_count
- distinct_tags
- priority_1_count
- join_path: alarms.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type