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>
300 lines
14 KiB
SQL
300 lines
14 KiB
SQL
-- =============================================================================
|
|
-- 002_fixtures.sql
|
|
--
|
|
-- ############################################################
|
|
-- ## FIXTURE DATA. NOT REAL PLANT HISTORY. DO NOT QUOTE. ##
|
|
-- ############################################################
|
|
--
|
|
-- Interim stand-in for `imh` (yau-sls-poc-imh), which is not built yet.
|
|
-- Everything in the `fixture` schema is generated. A number produced from these
|
|
-- tables is a test result about the pipeline, never a fact about the station.
|
|
--
|
|
-- WHY THIS EXISTS: Phase 4 is the only true blocker in the build. Cube, the
|
|
-- contracts, the agent and the UI can all be built and tested against fixtures;
|
|
-- swapping to imh then becomes a connection-string change plus a re-verify of
|
|
-- the Phase 5 gate.
|
|
--
|
|
-- THE COLUMN NAMES BELOW ARE ASSUMED, NOT AGREED. Section 10 of
|
|
-- BUILD-AI-CONTAINERS.md must be updated with the real imh schema as soon as
|
|
-- the owner confirms it, and this file changed to match — the point of the
|
|
-- fixtures is that the shape is right, so a wrong shape is worse than useless.
|
|
--
|
|
-- Still to agree with whoever builds imh (do this now, not at Phase 4):
|
|
-- * the read-only login name, and how the password reaches you
|
|
-- * the alarm / process value / operation table names and key columns
|
|
-- * whether timestamps are UTC or local, and DST behaviour
|
|
-- * whether an `operations` concept exists at all, or must be derived
|
|
-- * an NSG rule allowing lin001 -> imh on 1433 only
|
|
--
|
|
-- HOW IT IS SWITCHED OFF: USE_FIXTURES=true in ~/ai/api.env points Cube here.
|
|
-- Set it false and repoint CUBEJS_DB_* at imh. Every Cube model file carries a
|
|
-- fixture/real note at the top; check them all when you flip it.
|
|
--
|
|
-- Every fixture table carries an is_fixture column, defaulted TRUE, so a row
|
|
-- that reaches the UI can be traced back to here. The API surfaces it as a
|
|
-- banner. Do not remove it as tidying-up.
|
|
-- =============================================================================
|
|
|
|
DROP SCHEMA IF EXISTS fixture CASCADE;
|
|
CREATE SCHEMA fixture;
|
|
COMMENT ON SCHEMA fixture IS
|
|
'GENERATED FIXTURE DATA standing in for imh. Not real plant history. See db/002_fixtures.sql.';
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- fixture.alarm_history — one row per alarm state transition.
|
|
--
|
|
-- Definition used here, and it must match whatever imh turns out to do:
|
|
-- an ALARM is a transition INTO the active state. A row with state 'RTN' is the
|
|
-- return to normal for the preceding activation, not a second alarm. Counting
|
|
-- every row doubles every answer, which is the single most likely way the
|
|
-- "how many times last week" question returns a wrong number confidently.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE fixture.alarm_history (
|
|
alarm_id BIGSERIAL PRIMARY KEY,
|
|
event_time TIMESTAMPTZ NOT NULL, -- UTC. Cube converts, once.
|
|
tag_id TEXT NOT NULL,
|
|
equipment_id TEXT,
|
|
alarm_type TEXT NOT NULL, -- HIGH_LEVEL|HIGH_HIGH_LEVEL|SPILL|
|
|
-- PUMP_TRIP|SEAL_LEAK|HIGH_VIBRATION|
|
|
-- LEVEL_SIGNAL_FAULT|MAINS_FAILURE|
|
|
-- SETPOINT_REJECTED|LOW_LOW_LEVEL
|
|
priority INT, -- 1 highest .. 3 lowest
|
|
state TEXT NOT NULL, -- ACTIVE | RTN | ACK
|
|
value DOUBLE PRECISION, -- process value at transition
|
|
engineering_unit TEXT,
|
|
description TEXT,
|
|
is_fixture BOOLEAN NOT NULL DEFAULT TRUE
|
|
);
|
|
CREATE INDEX ON fixture.alarm_history (event_time);
|
|
CREATE INDEX ON fixture.alarm_history (tag_id, event_time);
|
|
CREATE INDEX ON fixture.alarm_history (equipment_id, event_time);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- fixture.process_value_history — sampled analogue history.
|
|
--
|
|
-- 1-minute samples. Real CI Server historisation is deadband-compressed, so the
|
|
-- real table will be irregular; anything in Cube that assumes an even sample
|
|
-- interval will be wrong against imh. Time-weight the averages, do not mean the
|
|
-- rows. This fixture is deliberately regular so that the difference shows up as
|
|
-- a behaviour change when imh is connected, rather than hiding.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE fixture.process_value_history (
|
|
sample_time TIMESTAMPTZ NOT NULL, -- UTC
|
|
tag_id TEXT NOT NULL,
|
|
equipment_id TEXT,
|
|
value DOUBLE PRECISION,
|
|
engineering_unit TEXT,
|
|
quality TEXT DEFAULT 'GOOD', -- GOOD | BAD | UNCERTAIN
|
|
is_fixture BOOLEAN NOT NULL DEFAULT TRUE,
|
|
PRIMARY KEY (tag_id, sample_time)
|
|
);
|
|
CREATE INDEX ON fixture.process_value_history (sample_time);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- fixture.operation_history — pump-down operations ("fills" inverted).
|
|
--
|
|
-- WRPS is a pump station, not a tank farm: the operation of interest is a
|
|
-- PUMP-DOWN — the well fills on inflow, pumps start at the duty level, the
|
|
-- level is drawn back to the stop level. One row per pump-down.
|
|
--
|
|
-- If imh has no equivalent, derive it in Cube from a monotonic level fall while
|
|
-- pumps_running > 0, and document the heuristic in cube/model/operations.yml.
|
|
-- Keep the heuristic simple; a clever one nobody can explain is not evidence.
|
|
-- -----------------------------------------------------------------------------
|
|
CREATE TABLE fixture.operation_history (
|
|
operation_id BIGSERIAL PRIMARY KEY,
|
|
equipment_id TEXT NOT NULL, -- STN-001
|
|
operation_type TEXT NOT NULL DEFAULT 'PUMP_DOWN',
|
|
start_time TIMESTAMPTZ NOT NULL, -- UTC
|
|
end_time TIMESTAMPTZ,
|
|
start_level_pct DOUBLE PRECISION,
|
|
end_level_pct DOUBLE PRECISION,
|
|
max_level_pct DOUBLE PRECISION,
|
|
avg_inflow_m3h DOUBLE PRECISION,
|
|
avg_discharge_m3h DOUBLE PRECISION,
|
|
peak_pumps_running INT,
|
|
duty_pump TEXT, -- PU-301 | PU-302 | PU-303
|
|
high_level_alarm BOOLEAN DEFAULT FALSE, -- did it reach the HLA setpoint
|
|
spill BOOLEAN DEFAULT FALSE, -- did it go over the weir
|
|
is_fixture BOOLEAN NOT NULL DEFAULT TRUE
|
|
);
|
|
CREATE INDEX ON fixture.operation_history (start_time);
|
|
CREATE INDEX ON fixture.operation_history (equipment_id, start_time);
|
|
|
|
-- =============================================================================
|
|
-- Generated rows. 30 days ending at the load time, UTC.
|
|
--
|
|
-- The pattern is deliberately boring and explainable: a diurnal inflow with a
|
|
-- morning and evening peak, a wet-weather week in the middle, pump-downs every
|
|
-- couple of hours, and a small number of alarms clustered in the wet week. It
|
|
-- is enough to exercise every question class and no more. Do not add realism
|
|
-- here — realism in fixtures is how a fixture number ends up in a slide.
|
|
-- =============================================================================
|
|
|
|
-- --- process values: 1-minute wet well level and flows, 30 days -------------
|
|
INSERT INTO fixture.process_value_history
|
|
(sample_time, tag_id, equipment_id, value, engineering_unit)
|
|
SELECT
|
|
ts,
|
|
'PS_STN_WET_WELL_LEVEL',
|
|
'WW-101',
|
|
-- sawtooth between the stop-all level and roughly the duty start level,
|
|
-- riding on the diurnal inflow, pushed higher during the wet week.
|
|
round((
|
|
30.0
|
|
+ 25.0 * abs(((extract(epoch FROM ts)::int / 60) % 140)::numeric / 140.0 - 0.5) * 2
|
|
+ 12.0 * sin(extract(epoch FROM ts) / 13750.0)
|
|
+ CASE WHEN ts BETWEEN now() - interval '18 days'
|
|
AND now() - interval '11 days' THEN 22.0 ELSE 0.0 END
|
|
)::numeric, 1)::double precision,
|
|
'%'
|
|
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;
|
|
|
|
INSERT INTO fixture.process_value_history
|
|
(sample_time, tag_id, equipment_id, value, engineering_unit)
|
|
SELECT
|
|
ts,
|
|
'PS_STN_INFLOW',
|
|
'STN-001',
|
|
round((
|
|
180.0
|
|
+ 90.0 * sin((extract(epoch FROM ts) - 21600) * 2 * pi() / 86400.0)
|
|
+ 40.0 * sin((extract(epoch FROM ts)) * 4 * pi() / 86400.0)
|
|
+ CASE WHEN ts BETWEEN now() - interval '18 days'
|
|
AND now() - interval '11 days' THEN 420.0 ELSE 0.0 END
|
|
)::numeric, 1)::double precision,
|
|
'm3/h'
|
|
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;
|
|
|
|
INSERT INTO fixture.process_value_history
|
|
(sample_time, tag_id, equipment_id, value, engineering_unit)
|
|
SELECT
|
|
ts,
|
|
'PS_STN_TOTAL_DISCHARGE_FLOW',
|
|
'STN-001',
|
|
CASE WHEN ((extract(epoch FROM ts)::int / 60) % 140) < 55
|
|
THEN 0.0
|
|
ELSE round((432.0 + 30.0 * sin(extract(epoch FROM ts) / 900.0))::numeric, 1)::double precision
|
|
END,
|
|
'm3/h'
|
|
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;
|
|
|
|
-- --- pump-down operations: roughly every 140 minutes for 30 days ------------
|
|
INSERT INTO fixture.operation_history
|
|
(equipment_id, operation_type, start_time, end_time,
|
|
start_level_pct, end_level_pct, max_level_pct,
|
|
avg_inflow_m3h, avg_discharge_m3h, peak_pumps_running, duty_pump,
|
|
high_level_alarm, spill)
|
|
SELECT
|
|
'STN-001',
|
|
'PUMP_DOWN',
|
|
st,
|
|
st + make_interval(mins => 35 + (n % 12)),
|
|
66.7,
|
|
16.7,
|
|
max_lvl,
|
|
inflow,
|
|
CASE WHEN max_lvl > 83.3 THEN 1296.0 WHEN max_lvl > 75.0 THEN 864.0 ELSE 432.0 END,
|
|
CASE WHEN max_lvl > 83.3 THEN 3 WHEN max_lvl > 75.0 THEN 2 ELSE 1 END,
|
|
-- duty rotates on lowest run hours; over a long window that is round-robin
|
|
'PU-30' || (1 + (n % 3))::text,
|
|
max_lvl >= 86.7,
|
|
max_lvl >= 100.0
|
|
FROM (
|
|
SELECT
|
|
n,
|
|
now() - interval '30 days' + make_interval(mins => n * 140) AS st,
|
|
CASE WHEN now() - interval '30 days' + make_interval(mins => n * 140)
|
|
BETWEEN now() - interval '18 days' AND now() - interval '11 days'
|
|
THEN 84.0 + (n % 7) * 2.9 -- wet week: reaches HLA, sometimes spills
|
|
ELSE 68.0 + (n % 5) * 1.4 -- normal: comfortably below HLA
|
|
END AS max_lvl,
|
|
CASE WHEN now() - interval '30 days' + make_interval(mins => n * 140)
|
|
BETWEEN now() - interval '18 days' AND now() - interval '11 days'
|
|
THEN 620.0 ELSE 205.0
|
|
END AS inflow
|
|
FROM generate_series(0, (30 * 24 * 60) / 140) AS n
|
|
) s
|
|
WHERE st < now();
|
|
|
|
-- --- alarms: derived from the operations above, so they stay consistent -----
|
|
-- One ACTIVE row per alarm, one RTN row per activation. Counting must count
|
|
-- ACTIVE only.
|
|
INSERT INTO fixture.alarm_history
|
|
(event_time, tag_id, equipment_id, alarm_type, priority, state, value,
|
|
engineering_unit, description)
|
|
SELECT
|
|
o.start_time + interval '20 minutes',
|
|
'PS_STN_HIGH_LEVEL_ALARM',
|
|
'WW-101',
|
|
'HIGH_LEVEL',
|
|
2,
|
|
'ACTIVE',
|
|
o.max_level_pct,
|
|
'%',
|
|
'Wet well level above the high level alarm setpoint'
|
|
FROM fixture.operation_history o
|
|
WHERE o.high_level_alarm;
|
|
|
|
INSERT INTO fixture.alarm_history
|
|
(event_time, tag_id, equipment_id, alarm_type, priority, state, value,
|
|
engineering_unit, description)
|
|
SELECT
|
|
o.start_time + interval '32 minutes',
|
|
'PS_STN_HIGH_LEVEL_ALARM',
|
|
'WW-101',
|
|
'HIGH_LEVEL',
|
|
2,
|
|
'RTN',
|
|
70.0,
|
|
'%',
|
|
'Wet well level returned below the high level alarm setpoint'
|
|
FROM fixture.operation_history o
|
|
WHERE o.high_level_alarm;
|
|
|
|
INSERT INTO fixture.alarm_history
|
|
(event_time, tag_id, equipment_id, alarm_type, priority, state, value,
|
|
engineering_unit, description)
|
|
SELECT
|
|
o.start_time + interval '25 minutes',
|
|
'PS_STN_SPILL_ACTIVE',
|
|
'WEIR-105',
|
|
'SPILL',
|
|
1,
|
|
'ACTIVE',
|
|
o.max_level_pct,
|
|
'%',
|
|
'Spill over the weir - environmental reportable event'
|
|
FROM fixture.operation_history o
|
|
WHERE o.spill;
|
|
|
|
-- A handful of pump trips, one per week, so trip questions have something to
|
|
-- find and the duty-promotion story is visible.
|
|
INSERT INTO fixture.alarm_history
|
|
(event_time, tag_id, equipment_id, alarm_type, priority, state, value,
|
|
engineering_unit, description)
|
|
VALUES
|
|
(now() - interval '26 days 4 hours', 'PS_PU302_TRIPPED', 'PU-302', 'PUMP_TRIP', 1, 'ACTIVE', 1, NULL, 'PU-302 tripped - no flow 20 s after start (PIT-321 below 150 kPa)'),
|
|
(now() - interval '26 days 1 hour', 'PS_PU302_TRIPPED', 'PU-302', 'PUMP_TRIP', 1, 'RTN', 0, NULL, 'PU-302 trip reset by operator command'),
|
|
(now() - interval '19 days 9 hours', 'PS_PU301_TRIPPED', 'PU-301', 'PUMP_TRIP', 1, 'ACTIVE', 1, NULL, 'PU-301 tripped - motor thermal TE-312'),
|
|
(now() - interval '19 days 2 hours', 'PS_PU301_TRIPPED', 'PU-301', 'PUMP_TRIP', 1, 'RTN', 0, NULL, 'PU-301 trip reset by operator command'),
|
|
(now() - interval '14 days 6 hours', 'PS_STN_ALARM_BITMASK', 'PU-303', 'SEAL_LEAK', 3, 'ACTIVE', 1, NULL, 'PU-303 seal leak MSE-333 - alarm only, unit remains available'),
|
|
(now() - interval '12 days 3 hours', 'PS_STN_ALARM_BITMASK', 'PU-301', 'HIGH_VIBRATION', 2, 'ACTIVE', 8.4, 'mm/s', 'PU-301 bearing vibration above 7.1 mm/s alarm threshold'),
|
|
(now() - interval '12 days 1 hour', 'PS_STN_ALARM_BITMASK', 'PU-301', 'HIGH_VIBRATION', 2, 'RTN', 6.2, 'mm/s', 'PU-301 bearing vibration returned below threshold'),
|
|
(now() - interval '6 days 11 hours', 'PS_STN_ALARM_BITMASK', 'WW-101', 'LEVEL_SIGNAL_FAULT', 1, 'ACTIVE', NULL, NULL, 'LIT-101 frozen - no change greater than 1 mm for 10 minutes with a pump running'),
|
|
(now() - interval '6 days 10 hours', 'PS_STN_ALARM_BITMASK', 'WW-101', 'LEVEL_SIGNAL_FAULT', 1, 'RTN', NULL, NULL, 'LIT-101 signal restored'),
|
|
(now() - interval '3 days 5 hours', 'PS_STN_ALARM_BITMASK', 'STN-001', 'SETPOINT_REJECTED', 3, 'ACTIVE', 6500, 'mm', 'Setpoint write rejected - start duty level above the spill weir; previous value retained');
|
|
|
|
-- =============================================================================
|
|
-- Sanity check after loading. Expect roughly: 30 days of 1-minute samples on
|
|
-- three tags, ~300 pump-downs, and alarms concentrated in the wet week.
|
|
--
|
|
-- SELECT count(*) FROM fixture.process_value_history;
|
|
-- SELECT count(*) FROM fixture.operation_history;
|
|
-- SELECT alarm_type, state, count(*)
|
|
-- FROM fixture.alarm_history GROUP BY 1,2 ORDER BY 1,2;
|
|
--
|
|
-- And the check that matters: everything here answers TRUE.
|
|
--
|
|
-- SELECT bool_and(is_fixture) FROM fixture.alarm_history;
|
|
-- =============================================================================
|