(* =====================================================================
   40_prog_simulation.st - PROGRAM SIMULATION, section 8.2

   Pass 2 only.  Excluded from the field build by build.py.

   Runs on the same 100 ms task, FIRST - before IO_MUX and CONTROL - so
   that a simulated image exists on the very first scan.  See the header
   of 91_config_sim.st for why that matters: with this program last, the
   first scan latches trips on all three pumps from an all-zero field
   image.  The cost is that it acts on the run commands and speed
   CONTROL published on the previous scan, 100 ms earlier.

   It writes only g_sim_* globals; IO_MUX feeds those into the process
   image while g_SimActive is TRUE.  This program contains no located
   variable and no control logic.

   Two time bases, deliberately:

     simulated time - level integration, inflow scenarios, the sim
                      clock.  Multiplied by the time scale (%MW23).
     real time      - the per-pump start delay only.  CONTROL's timers
                      are in real seconds (FB_PUMP's no-flow TON is
                      T#20s), so a start delay scaled by 60 would be
                      invisible to it and the no-flow trip could never
                      be demonstrated.  Section 8.2 asks for the delay
                      precisely so that trip has something to detect,
                      so the delay stays real.
   ===================================================================== *)

PROGRAM SIMULATION
VAR_EXTERNAL
    g_simcmd_Inflow : INT;
    g_simcmd_Mode : INT;
    g_simcmd_Reset : INT;
    g_simcmd_TimeScale : INT;

    g_o_RunCmd : ARRAY[1..3] OF BOOL;
    g_o_Speed_x10 : INT;

    g_SimActive : BOOL;
    g_sim_ClearReset : BOOL;

    g_sim_Level_mm : INT;
    g_sim_Inflow_x10 : INT;
    g_sim_Disch_x10 : INT;
    g_sim_ManifoldP : INT;
    g_sim_PumpP : ARRAY[1..3] OF INT;
    g_sim_Vib_x10 : ARRAY[1..3] OF INT;
    g_sim_LSHH : BOOL;
    g_sim_LSLL_Wet : BOOL;
    g_sim_Spill : BOOL;
    g_sim_ThermalOK : ARRAY[1..3] OF BOOL;
    g_sim_SealLeak : ARRAY[1..3] OF BOOL;
    g_sim_MainsOK : BOOL;
END_VAR

VAR CONSTANT
    SCAN_S      : REAL := 0.1;      (* task interval, matches 91_config_sim *)
    AREA_M2     : REAL := 120.0;    (* wet well plan area *)
    SPILL_M     : REAL := 6.0;      (* spill weir *)
    LSHH_M      : REAL := 5.5;      (* high high float *)
    LSLL_M      : REAL := 0.30;     (* low low float, below = dry *)
    START_DLY_S : REAL := 3.0;      (* real seconds, command to flow *)

    HZ_LO       : REAL := 38.0;     (* static lift cutoff *)
    HZ_HI       : REAL := 50.0;
    Q_LO        : REAL := 65.0;     (* L/s at HZ_LO *)
    Q_HI        : REAL := 120.0;    (* L/s at HZ_HI *)

    P_IDLE      : REAL := 80.0;     (* kPa, not delivering *)
    P_BASE      : REAL := 220.0;    (* kPa at zero flow while delivering *)
    P_PER_LPS   : REAL := 1.4;

    VIB_IDLE    : REAL := 0.2;      (* mm/s, stopped *)
    VIB_BASE    : REAL := 1.5;      (* mm/s at zero flow *)
    VIB_PER_LPS : REAL := 0.02;     (* stays well under FB_PUMP's 7.1 alarm *)
END_VAR

VAR
    Init : BOOL := FALSE;

    Volume_m3 : REAL;
    Level_m : REAL;
    Inflow_Lps : REAL;
    SumFlow_Lps : REAL;
    SimClock_s : REAL;              (* simulated seconds since reset *)

    StartDly_s : ARRAY[1..3] OF REAL;
    Delivering : ARRAY[1..3] OF BOOL;
    Flow_Lps : ARRAY[1..3] OF REAL;
    Press_kPa : ARRAY[1..3] OF REAL;
    Vib_mms : ARRAY[1..3] OF REAL;

    TimeScale : REAL;
    dt_s : REAL;                    (* simulated seconds this scan *)
    Speed_Hz : REAL;
    UnitQ_Lps : REAL;               (* per-unit flow before derating *)
    Derate : REAL;
    nDelivering : INT;
    i : INT;

    Rnd : DINT := 12345;            (* LCG state, section 8.3 *)
    Noise : REAL;
END_VAR

(* ---------------------------------------------------------------------
   Reset / first scan.

   %MW22 = 1 restores the initial conditions of the selected mode
   (section 8.2).  The acknowledgement is a global; IO_MUX clears %MW22,
   because this program may not touch located variables.
   --------------------------------------------------------------------- *)
g_sim_ClearReset := FALSE;

IF (NOT Init) OR (g_simcmd_Reset = 1) THEN
    Init := TRUE;
    g_sim_ClearReset := TRUE;

    SimClock_s := 0.0;

    IF g_simcmd_Mode = 3 THEN
        Level_m := 4.000;           (* section 7.3 reference condition *)
    ELSE
        Level_m := 3.500;           (* below start duty, station idle *)
    END_IF;

    Volume_m3 := Level_m * AREA_M2;

    FOR i := 1 TO 3 DO
        StartDly_s[i] := 0.0;
        Delivering[i] := FALSE;
        Flow_Lps[i]   := 0.0;
        Press_kPa[i]  := P_IDLE;
        Vib_mms[i]    := VIB_IDLE;
    END_FOR;
END_IF;


(* ---------------------------------------------------------------------
   Time base.  %MW23 = 1..120, anything outside that is treated as 1.
   --------------------------------------------------------------------- *)
IF (g_simcmd_TimeScale >= 1) AND (g_simcmd_TimeScale <= 120) THEN
    TimeScale := INT_TO_REAL(g_simcmd_TimeScale);
ELSE
    TimeScale := 1.0;
END_IF;

dt_s := SCAN_S * TimeScale;
SimClock_s := SimClock_s + dt_s;


(* ---------------------------------------------------------------------
   Inflow generator, section 8.2.  %MW21 selects the mode.
   --------------------------------------------------------------------- *)
CASE g_simcmd_Mode OF
    1:  (* diurnal dry weather: 40-110 L/s over a 24 h simulated period *)
        Inflow_Lps := 75.0 + 35.0 * SIN(6.283185 * SimClock_s / 86400.0);

    2:  (* wet weather: ramp 20 min to 300, hold 40 min, decay over 90 min *)
        IF SimClock_s < 1200.0 THEN
            Inflow_Lps := 75.0 + (300.0 - 75.0) * SimClock_s / 1200.0;
        ELSIF SimClock_s < 3600.0 THEN
            Inflow_Lps := 300.0;
        ELSIF SimClock_s < 9000.0 THEN
            Inflow_Lps := 300.0 - (300.0 - 75.0) * (SimClock_s - 3600.0) / 5400.0;
        ELSE
            Inflow_Lps := 75.0;
        END_IF;

    3:  (* demo reference, section 7.3: held at exactly 165 L/s *)
        Inflow_Lps := 165.0;

    ELSE  (* 0 and anything unrecognised: manual, %MW20 in L/s x 10 *)
        Inflow_Lps := INT_TO_REAL(g_simcmd_Inflow) / 10.0;
END_CASE;

IF Inflow_Lps < 0.0 THEN
    Inflow_Lps := 0.0;
END_IF;


(* ---------------------------------------------------------------------
   Per-pump flow model, section 8.2.

   Speed comes from CONTROL's published common drive speed, which is
   this scan's value because SIMULATION runs after CONTROL.
   --------------------------------------------------------------------- *)
Speed_Hz := INT_TO_REAL(g_o_Speed_x10) / 10.0;

IF Speed_Hz < HZ_LO THEN
    UnitQ_Lps := 0.0;               (* static lift cutoff *)
ELSE
    IF Speed_Hz > HZ_HI THEN
        Speed_Hz := HZ_HI;
    END_IF;
    UnitQ_Lps := Q_LO + (Speed_Hz - HZ_LO) * (Q_HI - Q_LO) / (HZ_HI - HZ_LO);
END_IF;

(* Start delay on REAL time - see the header comment. *)
nDelivering := 0;
FOR i := 1 TO 3 DO
    IF g_o_RunCmd[i] THEN
        IF StartDly_s[i] < START_DLY_S THEN
            StartDly_s[i] := StartDly_s[i] + SCAN_S;
        END_IF;
        Delivering[i] := (StartDly_s[i] >= START_DLY_S) AND (UnitQ_Lps > 0.0);
    ELSE
        StartDly_s[i] := 0.0;
        Delivering[i] := FALSE;
    END_IF;

    IF Delivering[i] THEN
        nDelivering := nDelivering + 1;
    END_IF;
END_FOR;

(* Parallel derating: 3 units give ~360 L/s, not a naive 3 x 120. *)
CASE nDelivering OF
    1:  Derate := 1.00;
    2:  Derate := 0.94;
    3:  Derate := 0.88;
    ELSE Derate := 1.00;
END_CASE;

SumFlow_Lps := 0.0;
FOR i := 1 TO 3 DO
    IF Delivering[i] THEN
        Flow_Lps[i]  := UnitQ_Lps * Derate;
        Press_kPa[i] := P_BASE + P_PER_LPS * Flow_Lps[i];
        Vib_mms[i]   := VIB_BASE + VIB_PER_LPS * Flow_Lps[i];
        SumFlow_Lps  := SumFlow_Lps + Flow_Lps[i];
    ELSE
        Flow_Lps[i]  := 0.0;
        Press_kPa[i] := P_IDLE;
        IF g_o_RunCmd[i] THEN
            Vib_mms[i] := VIB_BASE;     (* spinning up, no flow yet *)
        ELSE
            Vib_mms[i] := VIB_IDLE;
        END_IF;
    END_IF;
END_FOR;


(* ---------------------------------------------------------------------
   Wet well integration, section 8.2.

   On reaching the weir the level holds at 6.00 m and the excess is
   discarded - the station is seen to spill rather than running the
   level off scale.
   --------------------------------------------------------------------- *)
Volume_m3 := Volume_m3 + (Inflow_Lps - SumFlow_Lps) * dt_s / 1000.0;

IF Volume_m3 < 0.0 THEN
    Volume_m3 := 0.0;
END_IF;

IF Volume_m3 > SPILL_M * AREA_M2 THEN
    Volume_m3 := SPILL_M * AREA_M2;
END_IF;

Level_m := Volume_m3 / AREA_M2;


(* ---------------------------------------------------------------------
   Publish, section 8.3: small noise so the trends are not perfectly
   smooth and FB_HEADROOM's inflow filter has something to filter.
   LCG kept small enough that the DINT multiply cannot overflow.
   --------------------------------------------------------------------- *)
Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.01;        (* +/-0.5% *)
g_sim_Level_mm := REAL_TO_INT(Level_m * 1000.0 * (1.0 + Noise));

Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.04;        (* +/-2%   *)
g_sim_Inflow_x10 := REAL_TO_INT(Inflow_Lps * 10.0 * (1.0 + Noise));

Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.04;
g_sim_Disch_x10 := REAL_TO_INT(SumFlow_Lps * 10.0 * (1.0 + Noise));

FOR i := 1 TO 3 DO
    g_sim_PumpP[i]   := REAL_TO_INT(Press_kPa[i]);
    g_sim_Vib_x10[i] := REAL_TO_INT(Vib_mms[i] * 10.0);
END_FOR;

(* Manifold: the highest delivering unit's pressure, idle if none. *)
g_sim_ManifoldP := REAL_TO_INT(P_IDLE);
IF nDelivering > 0 THEN
    g_sim_ManifoldP := REAL_TO_INT(P_BASE + P_PER_LPS * UnitQ_Lps * Derate);
END_IF;

(* --- level switches.  Sense conventions are section 2.1's. --------- *)
g_sim_LSHH     := Level_m >= LSHH_M;
g_sim_LSLL_Wet := Level_m > LSLL_M;         (* FALSE = dry *)
g_sim_Spill    := Level_m >= (SPILL_M - 0.001);

(* --- plant health.  Healthy unless a fault is injected; injection is
       not modelled yet, so these are constant. ---------------------- *)
FOR i := 1 TO 3 DO
    g_sim_ThermalOK[i] := TRUE;
    g_sim_SealLeak[i]  := FALSE;
END_FOR;
g_sim_MainsOK := TRUE;

(* Tell IO_MUX to take its process image from here, not from %IW/%IX. *)
g_SimActive := TRUE;

END_PROGRAM
