yau-plant-assistant/cube/model/process_values.yml
Claude 34d2ccc576 Scaffold the WRPS plant operations assistant repository
Build spec and host brief carried in from C:\Claude and WRPS/02-env; the
plant model (equipment, tags, alarm bitmask, enums, unit conversions) is
derived from WRPS/04-plc/register-map.csv, WRPS/05-scada/modbus/scada-points.csv
and WRPS-CTL-003.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 13:56:32 +10:00

163 lines
6.2 KiB
YAML

# =============================================================================
# process_values.yml — sampled analogue history.
#
# SOURCE: fixture.process_value_history while USE_FIXTURES=true; the agreed imh
# process value table from Phase 4.
#
# THE THING THAT WILL BITE WHEN imh IS CONNECTED: CI Server historises with a
# deadband, so real samples are IRREGULAR. The fixtures are regular 1-minute
# samples. Any measure that averages rows rather than time-weighting them will
# look correct on fixtures and be wrong on imh - a flat period compresses to
# one row and a noisy period to hundreds, so a plain avg is weighted by how
# interesting the signal was. avg_value below is a plain average and is
# documented as an approximation; time_weighted_avg is the one to trust, and it
# must be re-verified against imh at the Phase 5 gate.
#
# SENTINELS: PS_STN_TIME_TO_SPILL_WEIR and PS_STN_TIME_TO_LSHH use 32767 to
# mean "drawing down or holding" - it is not a duration. Every measure here
# excludes it. Do not remove that filter to make a number look tidier.
#
# QUALITY: rows with quality other than GOOD are excluded from every measure.
# A BAD sample from a failed transmitter is not a low reading.
#
# UNITS: whatever the historian stores, which is not always what the PLC works
# in. Wet well level is historised as percent of the spill weir crest (raw mm
# divided by 60): 100.0 % = 6000 mm. See db/seed/tags.csv for every conversion.
# =============================================================================
cubes:
- name: process_values
sql_table: fixture.process_value_history # -> imh PV table at Phase 4
description: >
Sampled analogue history - wet well level, inflow, discharge flow, drive
speed, run hours. This is what makes an advisory question answerable with
evidence; you cannot answer a flow question from alarms.
joins:
- name: equipment
sql: "{CUBE}.equipment_id = {equipment}.equipment_id"
relationship: many_to_one
dimensions:
- name: id
sql: "{CUBE}.tag_id || '@' || {CUBE}.sample_time"
type: string
primary_key: true
- name: sample_time
sql: sample_time
type: time
description: Stored UTC, presented in SITE_TIMEZONE. Converted once, here.
- name: tag_id
sql: tag_id
type: string
- name: equipment_id
sql: equipment_id
type: string
- name: engineering_unit
sql: engineering_unit
type: string
description: >
Always report this with the number. A level of 86.7 is a percentage
of the weir crest, not a metre reading.
- name: quality
sql: quality
type: string
- name: is_fixture
sql: is_fixture
type: boolean
measures:
- name: sample_count
type: count
filters:
- sql: "{CUBE}.quality = 'GOOD' AND {CUBE}.value <> 32767"
- name: avg_value
sql: value
type: avg
filters:
- sql: "{CUBE}.quality = 'GOOD' AND {CUBE}.value <> 32767"
description: >
APPROXIMATION. Plain average of samples. Correct on the regular
fixture data; biased on deadband-compressed imh data. Prefer
time_weighted_avg for anything an engineer will check.
- name: time_weighted_avg
sql: >
SUM({CUBE}.value * EXTRACT(EPOCH FROM (
LEAD({CUBE}.sample_time) OVER (
PARTITION BY {CUBE}.tag_id ORDER BY {CUBE}.sample_time
) - {CUBE}.sample_time)))
/ NULLIF(SUM(EXTRACT(EPOCH FROM (
LEAD({CUBE}.sample_time) OVER (
PARTITION BY {CUBE}.tag_id ORDER BY {CUBE}.sample_time
) - {CUBE}.sample_time))), 0)
type: number
description: >
Time-weighted average - each sample weighted by how long it stood.
This is the honest average on deadband-compressed history. Verify it
against imh by hand at the Phase 5 gate before trusting it in prose.
- name: max_value
sql: value
type: max
filters:
- sql: "{CUBE}.quality = 'GOOD' AND {CUBE}.value <> 32767"
- name: min_value
sql: value
type: min
filters:
- sql: "{CUBE}.quality = 'GOOD' AND {CUBE}.value <> 32767"
- name: p95_value
sql: "PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {CUBE}.value)"
type: number
filters:
- sql: "{CUBE}.quality = 'GOOD' AND {CUBE}.value <> 32767"
description: >
95th percentile. More useful than max for "how high does it normally
get", because max is one sample and often a transient.
- name: seconds_above_high_level_alarm
sql: >
SUM(CASE WHEN {CUBE}.tag_id = 'PS_STN_WET_WELL_LEVEL'
AND {CUBE}.value >= 86.7 THEN 60 ELSE 0 END)
type: number
description: >
Seconds the wet well spent above the high level alarm setpoint
(86.7 % = 5200 mm, the %MW8 default). ASSUMES A 60 SECOND SAMPLE
INTERVAL, true of the fixtures and NOT true of imh. When imh is
connected this must be rewritten to sum actual sample gaps - it is on
the Phase 5 gate list for exactly that reason. If the setpoint itself
was changed during the window (PS_STN_HIGH_LEVEL_ALARM_SP), this
measure is wrong and the answer must say so.
- name: seconds_above_lshh
sql: >
SUM(CASE WHEN {CUBE}.tag_id = 'PS_STN_WET_WELL_LEVEL'
AND {CUBE}.value >= 91.7 THEN 60 ELSE 0 END)
type: number
description: >
Seconds above LSHH (91.7 % = 5500 mm). Same 60 second assumption as
above. Any non-zero value here is worth reporting explicitly.
pre_aggregations:
- name: pv_by_hour
measures: [avg_value, max_value, min_value, sample_count]
dimensions: [tag_id, equipment_id, engineering_unit]
time_dimension: sample_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()"