yau-plant-assistant/cube/model/alarms.yml
Claude 156d2a6d43 Refresh pre-aggregations daily, not every ten minutes
lin001 is 2 vCPU with 3.8 GiB and no swap, shared with 27 other containers.
Cube was rebuilding three pre-aggregations over static fixture data on 10, 10
and 30 minute keys, and its own log shows what that cost:

    "Previous interval #19593 was not finished with 30000 interval"
    "Interval #19594 finished after 00:02:50"

A refresh taking 2m50s scheduled every 30 seconds, at interval #19594 -
overlapping and never catching up, for days. Every rebuild produced a
byte-identical result, because the fixtures do not change.

24 hours until imh makes the data genuinely live, at which point these get
tuned deliberately rather than left at a number that was never chosen. Cube
also suggests fewer partitions via rollup_lambda; that is a modelling change,
not a demo-day one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:53:51 +10:00

193 lines
7.1 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.
# DEFERRED DEFECT - these two return UTC, not SITE_TIMEZONE. Cube converts
# time DIMENSIONS to the query timezone; a min/max MEASURE over a
# timestamp comes back unconverted. In a Sydney day bucket for
# 2026-08-01, first_alarm returns 2026-07-31T20:00:35 - the right
# instant, ten hours and one calendar day out, next to a bucket label
# that IS in site time.
#
# Left unfixed on purpose until imh is connected: the fix must keep the
# conversion inside Cube, and which fix is right depends on whether imh
# stores UTC or local time (Phase 4, task 4). Until then, do NOT quote
# either of these to an operator as a clock time. See Phase 4,
# "Deferred from Phase 5", finding (b) in BUILD-AI-CONTAINERS.md.
- 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:
# 24h, not minutes: the fixtures are static, so a shorter
# interval rebuilds a byte-identical result on a 2-vCPU host
# and was a standing CPU load for no gain. Tune this back
# deliberately when imh makes the data genuinely live.
every: 24 hours
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