// ------------------------------------------------------------------
// A variant (very simple, mostly without component failures)
// of the "steam boiler control specification problem":
// Jean-Raymond Abrial, Egon Börger, and Hans Langmaack:
// Formal Methods for Industrial Applications: Specifying and 
// Programming the Steam Boiler Control, Springer LNCS 1165, 1996.
// ------------------------------------------------------------------

// model parameters (with sample values that are safe for 7 steps)
val C = 40;  // maximal capacity of boiler
val M1 = 3;  // minimal critical limit
val M2 = 32; // maximal critical limit
val N1 = 15; // minimal normal limit
val N2 = 20; // maximal normal limit
val W = 4;   // maximal steam output
val U1 = 1;  // maximum gradient of its increase
val U2 = 2;  // maximum gradient of its decrease
val P = 2;   // nominal capacity of each pump
val N = 4;   // number of pumps

// basic types
type Pump = ℕ[N-1];  // pump index
type System = ℕ[2];  // system mode (0:init, 1:run, 2:stop)
type Valve = Bool;   // valve state (false:closed, true:open)
type Mode = ℕ[2];    // pump mode (0:off, 1:on, 2:switch)
type Modes = Map[Pump,Mode]; // mode of pumps 
type Water = ℕ[C];   // water in boiler
type Steam = ℕ[W];   // quantity of steam
type Request = Bool; // true if pump mode is to be switched
type Requests = Map[Pump,Request]; // requests for mode switches
type Flow = ℕ[P];    // a pump flow

// the boiler state
type Boiler = Record[
 s: System,   // the system mode
 v: Valve,    // the status of the valve
 m: Modes,    // the actual current pump modes
 q: Water,    // the current quantity of water in the boiler
 w: Steam     // the quantity of steam currently produced
];

// the control decision
type Control = Record[
  s: System,  // the new system mode
  v: Valve,   // the new status of the valve
  r: Requests // the requested pump states
];

// determine next modes of pumps
fun modes(m:Modes): Modes =
  choose m0:Modes with
    ∀i:Pump. m0[i] = if m[i] = 2 then 1 else m[i];
    
// determine next modes of pumps after applying requests
fun modes(m:Modes, r:Requests): Modes =
  let m0 = modes(m) in
  choose m1:Modes with
    ∀i:Pump. 
      if ¬r[i] then
        m1[i] = m0[i]
      else if m0[i] = 1 then
        m1[i] = 0  // switch off takes immediate effect
      else 
        m1[i] = 2; // switch on takes effect after next step

// request to not switch anything
val none = Map[Pump,Request](⊥);

// compute requests from pump modes m and quantity q of water
fun requests(m:Modes, q:Water): Requests =
  // previously requests to open pumps have now taken effect
  let m0 = modes(m) in
  if q < N1 then
    // request some additional closed pump, if possible
    choose i:Pump with m0[i] = 0 in
      none with [i] = ⊤
    else
      none
  else if q > N2 then
    // close some open pump, if possible
    choose i:Pump with m[i] = 1 in
      none with [i] = ⊤
    else
      none
  else 
    // everything is fine
    none;

// analyze state b of boiler and determine a control decision
fun analyze(b:Boiler): Control 
  // not run in emergency mode
  requires b.s ≠ 2;
=
  // if quantity of water is critical, emergency stop
  if b.q < M1 ∨ b.q > M2 then
    ⟨s:2, v:b.v, r:none⟩
  // initial state with non-critical quantity of water
  else if b.s = 0 then
    // if steam sensor has failed, emergency stop
    if b.w ≠ 0 then
      ⟨s:2, v:b.v, r:none⟩
    // if there is too much water, open valve
    else if b.q > N2 then
      ⟨s:b.s, v:⊤, r:none⟩
    // if there is too little water, 
    // make sure that valve is closed and open some pump
    else if b.q < N1 then
      ⟨s:b.s, v:⊥, r:requests(b.m, b.q)⟩
    // safe state has been reached, 
    // make sure that valve is closed and go to running mode
    else
      ⟨s:1, v:⊥, r:none⟩    
  // running state with non-critical quantity of water
  else 
    ⟨s:b.s, v:b.v, r:requests(b.m, b.q)⟩;

// b0 is a possible new state of a boiler with previous state b 
// determined by control decision c
pred control(b:Boiler, c:Control, b0:Boiler) ⇔
  // quantity of water leaving through valve
  ∃v:Flow with if ¬c.v then v = 0 else v > 0.
  // number of pumps currently open   
  let n = #i:Pump with b.m[i] = 1 in
  // quantity of water entering from pumps
  ∃p:ℕ[N*P] with n ≤ p ∧ p ≤ n*P.
  // the new quantity of water from the choices
  let q = let q = b.q-(v+b.w)+p in if q < 0 then 0 else q in
  // new amount of steam produced
  ∃w:Steam with
    if b.s = 0 then w = 0 else 1 ≤ w ∧ b.w-U2 ≤ w ∧ w ≤ b.w+U1.
  // new pump modes
  let m = modes(b.m, c.r) in
  // new boiler state 
  (b0.s = c.s ∧ b0.v = c.v ∧ b0.m = m ∧ b0.q = q ∧ b0.w = w);

// determine a possible new state of a boiler with previous state b 
// from control decision c
fun control(b:Boiler, c:Control): Boiler =
  // quantity of water leaving through valve
  choose v:Flow with if ¬c.v then v = 0 else v > 0 in
  // number of pumps currently open   
  let n = #i:Pump with b.m[i]=1 in
  // quantity of water entering from pumps
  choose p:ℕ[N*P] with n ≤ p ∧ p ≤ n*P in
  // new quantity of water from the choices
  let q = let q0 = b.q-(v+b.w)+p in if q0 < 0 then 0 else q0 in
  // new amount of steam produced
  choose w:Steam with 
    if b.s = 0 then w = 0 else 1 ≤ w ∧ b.w-U2 ≤ w ∧ w ≤ b.w+U1 in
  // new pump modes
  let m = modes(b.m, c.r) in
  // new boiler state
  b with .s = c.s with .v = c.v with .m = m with .q = q with .w = w;

// the safety condition of the boiler
pred safe(b:Boiler) ⇔
  // in initial state, no stream is produced
  (b.s = 0 ⇒ b.w = 0) ∧
  // valve may be only open in initial state
  (b.v ⇒ b.s = 0) ∧       
  // in running system, water level must not be critical
  (b.s = 1 ⇒ M1 ≤ b.q ∧ b.q ≤ M2);
  
// an arbitrary initial boiler state (may be even critical)
fun init(): Boiler = 
  choose b:Boiler with
  b.s = 0 ∧ b.v = ⊥ ∧ b.m = Map[Pump,Mode](0) ∧ b.w = 0;

// an arbitrary non-critical initial boiler state
fun init0(): Boiler = 
  choose b:Boiler with
  b.s = 0 ∧ b.v = ⊥ ∧ b.m = Map[Pump,Mode](0) ∧ b.w = 0 ∧
  M1 ≤ b.q ∧ b.q ≤ M2;

// an initial boiler state with water quantity q
fun init1(q:Water): Boiler = 
  choose b:Boiler with
  b.s = 0 ∧ b.v = ⊥ ∧ b.m = Map[Pump,Mode](0) ∧ b.w = 0 ∧
  b.q = q;
 
// number of execution steps  
val S:ℕ; // S = 7: 10^5 non-deterministic branches

// system is stopped
pred stopped(b:Boiler) ⇔ b.s = 2;

// an execution of the boiler system
proc boiler(): ()
{
  var i:ℕ[S] = 0;
  var b:Boiler = init1(N1);
  while i < S ∧ ¬stopped(b) do
  {
    if ¬safe(b) then 
    print ⟨i,b⟩;
    assert safe(b);
    val c = analyze(b);
    // b = choose b0:Boiler with control(b, c, b0);
    b ≔ control(b, c); 
    i ≔ i+1;
  }
  // print ⟨i,b⟩;
}

// ------------------------------------------------------------------
// end of file
// ------------------------------------------------------------------



