The three open Phase 5 findings were one defect: the stand-in was keyed on
CI Server POINT names (PS_STN_WET_WELL_LEVEL) when the historian is keyed on
CI Server ITEM names (AID.WRPS.STN.LEVEL). Modbus carries register numbers,
not names, so those two layers are free to differ - and do. Reconciling
against the register map, as planned, would only have proved the first three
namespaces agreed with each other.
Rebuilt from WRPS/05-scada/modbus, so item names, sample rates, retention and
timestamp semantics come from the machine rather than from a guess.
(a) Level tag does not join. PS_STN_WET_WELL_LEVEL becomes a tag row in its
own right; LIT-101 is marked NOT HISTORISED - a field input on %IW0 that
never reaches SCADA. It was the only seed row carrying two addresses.
public.historian_items holds the item-to-tag mapping, generated by
scripts/gen_historian_items.py and enforced non-empty at generate, at
deploy and at verify.
(b) first_alarm/last_alarm returned UTC. Converted inside the measure, so it
stays in Cube and happens once. Aggregate first, convert after - the other
order picks the wrong row across a DST fall-back. Returned as a formatted
string with a companion site_timezone measure. Storage being UTC is now
confirmed, not assumed: all 49 points carry TIME_ZONE "Date+time GMT" and
every history group CORRECT_DAYLIGHT=0. This answers Phase 4 task 4.
(c) High level alarm filed against the wrong equipment. Both sides were right
about different things; the defect was asserting equipment twice. The
history now carries no equipment column at all - faithful, since CI
Server's section tree stops at the station and three pumps. Equipment is
reached bit -> tag -> equipment via public.alarm_bits.
Alarms are derived, not stored: CI Server's ALARM_HISTORY group is empty
because every item imports with alarming off. Decomposing the alarm word needs
no configuration that does not exist.
Three things the SCADA config changed that were never filed as faults:
- retention is 7 days, not 30. The advisory path was reporting a month of
evidence drawn from a week of data
- the analogue rate is 5 s, not 60. Two measures multiplied sample counts by
a hardcoded 60 - a twelvefold overstatement that read as plausible
- the deadband warning in process_values.yml was wrong and was steering
people away from the correct measure
db/002_fixtures.sql now asserts its own counts at load and cross-checks the
alarm derivation against two independent signals. Those prove the pipeline,
not the plant.
db/README-standin-historian.md documents removal: the seam between generation
and contract, and twelve assumptions about imh that are NOT confirmed. Two of
them fail silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
6.8 KiB
YAML
185 lines
6.8 KiB
YAML
# =============================================================================
|
|
# operations.yml — pump-down operations.
|
|
#
|
|
# THIS IS THE FILE THAT ANSWERS THE ADVISORY QUESTION WITH EVIDENCE RATHER THAN
|
|
# OPINION. When an operator asks what rate to run the station at, the honest
|
|
# answer is a table of what has actually been run, what happened each time, and
|
|
# what the documented limits are - then a deferral. Everything needed for that
|
|
# is a measure here.
|
|
#
|
|
# REMOVING THE STAND-IN: db/README-standin-historian.md. The view below is
|
|
# CONTRACT - repoint it at imh, do not edit this file to absorb a difference.
|
|
#
|
|
# SOURCE: fixture.operation_history while USE_FIXTURES=true. It is a
|
|
# MATERIALIZED VIEW and it is DERIVED, not stored - confirmed now rather than
|
|
# assumed: CI Server has no operations concept, and the WRPS item list is 49
|
|
# points with nothing resembling a batch or campaign record.
|
|
#
|
|
# The heuristic, kept deliberately simple so it can be explained to the person
|
|
# checking the number, and implemented in db/002_fixtures.sql exactly as
|
|
# written here:
|
|
#
|
|
# A PUMP-DOWN starts at the sample where AID.WRPS.STN.PUMPS_RUNNING goes from
|
|
# 0 to non-zero, and ends at the next sample where it returns to 0. Its max
|
|
# level is the maximum AID.WRPS.STN.LEVEL over that span plus the 10 minutes
|
|
# before it, because the peak is usually just before the pumps catch up.
|
|
# Operations shorter than 5 minutes are discarded as start/stop noise.
|
|
#
|
|
# Do not make this cleverer. A heuristic nobody can explain is not evidence,
|
|
# and this cube's whole job is to produce evidence.
|
|
#
|
|
# THE TWO SOURCE ITEMS ARE SAMPLED AT DIFFERENT RATES - PUMPS_RUNNING every 30
|
|
# seconds, LEVEL every 5 - and the boundaries line up only because 30 is a
|
|
# multiple of 5. If either group's rate is retuned on the server, check that
|
|
# assumption before trusting start_level_pct or end_level_pct.
|
|
#
|
|
# THERE IS NO equipment_id. A pump-down belongs to the station and to nothing
|
|
# else; duty_pump names the unit that led it. The column used to exist, carried
|
|
# the literal 'STN-001', and was part of the same denormalisation that made
|
|
# alarm equipment disagree with the tag seed.
|
|
#
|
|
# ON THE WORD "FILL": the generic spec calls these fills. WRPS is a pump
|
|
# station, so the operation is a pump-down - the well fills passively on inflow
|
|
# and the station draws it back down. Same shape, opposite sign. The measures
|
|
# keep the pump-down naming because that is what an operator here would say.
|
|
# =============================================================================
|
|
|
|
cubes:
|
|
- name: operations
|
|
sql_table: fixture.operation_history # -> derived from imh at Phase 4
|
|
description: >
|
|
One row per pump-down at the Waterloo Road Pump Station: when it ran,
|
|
how high the well got, how much came in, how much was pumped, which unit
|
|
was duty, and whether it alarmed or spilled.
|
|
|
|
dimensions:
|
|
- name: operation_id
|
|
sql: operation_id
|
|
type: number
|
|
primary_key: true
|
|
|
|
- name: start_time
|
|
sql: start_time
|
|
type: time
|
|
description: Stored UTC, presented in SITE_TIMEZONE.
|
|
|
|
- name: end_time
|
|
sql: end_time
|
|
type: time
|
|
|
|
- name: operation_type
|
|
sql: operation_type
|
|
type: string
|
|
description: PUMP_DOWN. Reserved for future manual or wash-down operations.
|
|
|
|
- name: duty_pump
|
|
sql: duty_pump
|
|
type: string
|
|
description: >
|
|
The unit that led the operation. Duty rotates on lowest accumulated
|
|
run hours, service-due units ranked last, ties by ascending pump
|
|
number - so an uneven distribution over a long window is a finding,
|
|
not a rotation fault.
|
|
|
|
- name: peak_pumps_running
|
|
sql: peak_pumps_running
|
|
type: number
|
|
|
|
- name: high_level_alarm
|
|
sql: high_level_alarm
|
|
type: boolean
|
|
description: Did this pump-down reach the high level alarm setpoint.
|
|
|
|
- name: spill
|
|
sql: spill
|
|
type: boolean
|
|
description: >
|
|
Did the well go over the weir crest. A spill is an environmental
|
|
reportable event; report the count plainly and never round it.
|
|
|
|
- name: is_fixture
|
|
sql: is_fixture
|
|
type: boolean
|
|
|
|
measures:
|
|
- name: pump_down_count
|
|
type: count
|
|
description: Number of pump-down operations in the window.
|
|
|
|
- name: avg_discharge_rate
|
|
sql: avg_discharge_m3h
|
|
type: avg
|
|
description: >
|
|
Mean discharge rate across operations, m3/h. Evidence of what has
|
|
been run - NOT a recommendation of what to run.
|
|
|
|
- name: min_discharge_rate
|
|
sql: avg_discharge_m3h
|
|
type: min
|
|
|
|
- name: max_discharge_rate
|
|
sql: avg_discharge_m3h
|
|
type: max
|
|
|
|
- name: avg_inflow_rate
|
|
sql: avg_inflow_m3h
|
|
type: avg
|
|
|
|
- name: max_level_reached
|
|
sql: max_level_pct
|
|
type: max
|
|
description: Highest wet well level reached, percent of the weir crest.
|
|
|
|
- name: avg_max_level
|
|
sql: max_level_pct
|
|
type: avg
|
|
|
|
- name: high_alarm_count
|
|
type: count
|
|
filters:
|
|
- sql: "{CUBE}.high_level_alarm = TRUE"
|
|
|
|
- name: high_alarm_rate
|
|
sql: >
|
|
COUNT(*) FILTER (WHERE {CUBE}.high_level_alarm)::float
|
|
/ NULLIF(COUNT(*), 0)
|
|
type: number
|
|
description: >
|
|
Fraction of pump-downs that reached the high level alarm. Pair it
|
|
with pump_down_count in the answer - 1 in 2 and 50 in 100 are not the
|
|
same evidence, and a rate quoted without its denominator invites the
|
|
reader to treat a small sample as a trend.
|
|
|
|
- name: spill_count
|
|
type: count
|
|
filters:
|
|
- sql: "{CUBE}.spill = TRUE"
|
|
|
|
- name: avg_duration_minutes
|
|
sql: "EXTRACT(EPOCH FROM ({CUBE}.end_time - {CUBE}.start_time)) / 60"
|
|
type: avg
|
|
|
|
pre_aggregations:
|
|
- name: ops_by_day
|
|
measures:
|
|
- pump_down_count
|
|
- avg_discharge_rate
|
|
- max_level_reached
|
|
- high_alarm_count
|
|
- spill_count
|
|
dimensions: [duty_pump, operation_type, peak_pumps_running]
|
|
time_dimension: start_time
|
|
granularity: day
|
|
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:
|
|
# Eight days, not 365. The historian retains one week, so a pump-down
|
|
# older than that does not exist to be aggregated.
|
|
sql: "SELECT now() - interval '8 days'"
|
|
build_range_end:
|
|
sql: "SELECT now()"
|