Add your plant

A plant is one Julia file. Adding a model to the app means writing that file: nothing else in the app has to change, and the plant's form in the interface is generated from what the file declares.

The contract

Commandu between -1 and +1: +1 is filtration, -1 is backwash, held constant over each sampling period
Dynamicsyou write two vector fields, F₊ for filtration and F₋ for backwash; the app integrates dx/dt = ½(1+u)·F₊(x) + ½(1-u)·F₋(x)
Statex[1] is the cost, x[2] is x1 (hidden, typically the fouling), x[3] is x2; any number of further states may follow
Measuredthe cost and x2, with Gaussian noise
Shown livethe first three states only
Timein the unit you declare; the interface can display another one

What a plant file declares

  • Parameters and initial conditions. Each has a name, a default value, a label, a unit and a group. You choose which ones appear in the interface and which ones stay fixed in the file.
  • The dynamics: the two vector fields.
  • The initial state, built from the initial conditions and the parameters.
  • The solver: the integration algorithm and its tolerances. It is never shown in the interface.
  • Suggested controller settings (optional): warm-up and refit durations in your time unit, and a starting point for the controllers' internal model.

Parameters are declared like this:

const PARAMS = [
    PlantParam(:r0,      1.0, label = "Clean-membrane resistance", unit = "-",   group = "Membrane"),
    PlantParam(:k_foul,  0.8, label = "Fouling rate",              unit = "1/d", group = "Membrane"),
    PlantParam(:k_clean, 6.0, label = "Cleaning rate",             unit = "1/d", group = "Backwash"),
    PlantParam(:rho,     0.5, label = "Backwash water cost",       group = "Backwash", ui = false),
]

and the dynamics as two functions returning the derivative of the state, here for a toy model whose state is (cost, x1, x2):

Q(x1, p) = 1 / (p.r0 + x1)

make_fields(p) = (
    # filtration: the membrane produces and fouls
    (x, mem) -> [Q(x[2], p), p.k_foul * Q(x[2], p), 1.0],
    # backwash: it uses water and cleans the membrane
    (x, mem) -> [-p.rho * Q(x[2], p), -p.k_clean * x[2], 1.0],
)

Steps

  1. Copy the template plant shipped with the package: a complete, commented, working example.
  2. Rename it and replace its model with yours.
  3. Check it. check_plant runs the plant exactly the way a live run does and reports what is wrong:
    using FiltrationSimulation
    check_plant(:my_plant)
  4. Restart the server. The plant is in the list.

A plant that fails its check at start-up is reported and left out of the list; the server and the other plants are not affected.

Large or stiff models

A model with dozens of states is fine: put the cost, x1 and x2 first and everything else after. If your second state is not the quantity you want to compare with a controller's fouling estimate, declare how to compute the displayed one; the simulated physics is not changed.

For a stiff model, declare an implicit solver and tight tolerances. check_plant reports the time per step.