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>
177 lines
6 KiB
YAML
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
|