Control System

Two machine settings computed from sensor readings, with every sum shown.

control-system.js · output · proof · check · try it in the playground


The question

A controller reads sensors and decides how hard to drive its actuators (the parts that act: a valve, a motor, a heater).

This example computes commands for two actuators:

What should each command be, and how was it worked out?


What we tell eyecode: the readings

fact(measurement1('input1', [6, 11]))
fact(measurement2('input2', 'true'))
fact(measurement3('disturbance1', 35766))
fact(measurement4('output2', 24))
fact(observation3('state3', 22))
fact(target2('output2', 29))
// …

A pair like [6, 11] is turned into one number by a helper rule: if the first value is smaller, use the square root of the difference (here √5); otherwise use the first value.


What we tell eyecode: actuator 1

implied_by(
  control1('actuator1', C),
  [
    measurement10('input1', M1),
    measurement2('input2', 'true'),
    measurement3('disturbance1', D1),
    gt(D1, 0),                          // the logarithm needs a positive disturbance
    is(C1, mul(M1, 19.6)),              // proportional part
    is(C2, div(log(D1), log(10))),      // compensation part: log10 of the disturbance
    is(C, sub(C1, C2)),                 // simple feedforward control
  ],
)

is means “calculate”: it binds its first argument to the value of the arithmetic term (mul, div, sub, log) in its second. The compensation is the base-10 logarithm of the disturbance, so the rule first checks with gt that the disturbance is positive: a logarithm of zero or less would stop the run with an error.


What we tell eyecode: actuator 2

implied_by(
  control1('actuator2', C),
  [
    observation3('state3', P3),
    measurement4('output2', M4),
    target2('output2', T2),
    is(E, sub(T2, M4)),                 // error: target minus output
    is(D, sub(P3, M4)),                 // differential error: observed state minus output
    is(C1, mul(5.8, E)),                // proportional part
    nonlinear_part(E, D, C2),           // nonlinear differential part
    is(C, add(C1, C2)),                 // PND feedback control
  ],
)

Guarding a division

implied_by(
  nonlinear_part(E, D, C2),
  [
    ne(E, 0),
    is(N, div(7.3, E)),                 // nonlinear factor
    is(C2, mul(N, D)),
  ],
)
implied_by(nonlinear_part(E, _, 0), eq(E, 0))

The nonlinear factor divides by the error. On target the error is zero, and the division would stop the run, so two exclusive clauses split the cases: with an error, the factor is computed; without one, the nonlinear part is left out.


What eyecode concludes

control1('actuator1', 39.27346198678276)
control1('actuator2', 26.08)

These are ordinary decimal (floating-point) numbers, printed in full.


Why: the proof in plain words

Actuator 1

Actuator 2

Every one of these values is written into the proof.


Checked, not just claimed

The checker does not take the arithmetic on faith: it recomputes each calculation itself and compares.

Verdict: checked.


Try it

eyecode examples/control-system.js            # the commands
eyecode --proof examples/control-system.js    # answers with their proof

Or open it in the playground. Raise the target to fact(target2('output2', 34)) and run again: the error becomes 10, and actuator 2’s command becomes 56.54 instead of 26.08. Actuator 1 is unchanged. Set the target to 24, the current output: the error is zero, the nonlinear part is left out, and the command is 0.0. Set the disturbance to 0: actuator 1 gets no command, since its compensation is undefined, and actuator 2 is unaffected.


Takeaway

When a machine setting comes from a chain of formulas, a proof that lists and re-checks every intermediate value turns “the controller said so” into something an engineer can audit line by line.