Pressure regulators, modelled on what a datasheet actually tells you.
A regulator is not a restriction. A pipe's pressure drop is a function of the
flow through it; a regulator's outlet is a *setpoint* it defends, and the drop
across it is whatever the inlet happens to be minus that setpoint. Modelled as
a resistance it is wrong in the most misleading way -- it will happily report a
tank pressure that tracks COPV pressure, which is the exact behaviour a
regulator exists to prevent.
Why there is no force balance here
----------------------------------
An earlier plan for this module proposed deriving droop from a force balance
across the sensing diaphragm: dome pressure on the diaphragm against spring
preload, seat and stem areas, flow force on the poppet. That was abandoned, and
the reason is worth recording so nobody rebuilds it.
The balance needs the diaphragm's **effective area as a function of lift**, and
effective area depends on how the convolution rolls under load. No vendor
publishes it, it changes with the elastomer and with age, and it cannot be
recovered from an outline drawing. Building the model means inventing three
unmeasurable numbers and tuning them until the answer reproduces the one number
the datasheet already gave you. More machinery, less confidence.
How a dome-loaded regulator is actually set
--------------------------------------------
A dome regulator has no knob. Its outlet follows the pressure in its dome, and
the dome is fed by a second, hand-loaded regulator upstream -- the *control*
regulator. Turning that knob is how the setpoint moves, during a test, by hand.
So the dome pressure is a **signal** here, not a fixed parameter: a scenario can
ramp it, and an operator can change it mid-run, which is exactly what happens on
the stand.
Between the dome and the outlet sits a **bias**. The Aqua Environment 1092-50 in
use here delivers 50 psi *above* what its dome is loaded to, by design. Modelled
as a parameter rather than folded into the setpoint, because it is a property of
the regulator and the dome pressure is a property of the test: fold them
together and every scenario has to remember to add 50, and one day one of them
will not.
.. code-block:: text
p_set = p_dome + bias dome-loaded
p_set = setpoint hand-loaded, or a fixed bench setting
What the datasheet does give you is two more coefficients, and they cover the
two physically distinct effects:
**Supply-pressure effect.** Outlet *rises* as inlet *falls*, because inlet
pressure helps hold the poppet shut against the seat and that help goes away.
Datasheets quote it as a rate, and it is declared here in the units they quote
it in -- the Aqua Environment 1092-50 in use here is **17 psi per 1000 psi** of
inlet, written
.. code-block:: toml
supply_coefficient = { value = 17, unit = "psi/1000psi" }
It is measured from **zero inlet**, not from a full bottle: the outlet is
.. code-block:: text
p_out = p_dome + bias - S . p_in (p_in gauge)
so a 1092-50 loaded to 500 psi with 4000 psi behind it holds
``500 + 50 - 4 x 17 = 482`` psi, and it climbs from there as the bottle falls
-- from the first gram, not from some charge pressure onwards. (The team,
2026-10-07. An earlier version measured the term from an ``inlet_reference``
or the COPV charge, which zeroed it at a full bottle and put the whole effect
on the far side of that pressure.) Over a COPV decay from 4500 to 1500 psi it
moves the outlet up 51 psi -- about 6% on chamber pressure, and the wrong sign
to guess at.
The unit matters more than it looks. Outlet-pressure-per-inlet-pressure is
dimensionless by arithmetic, and an earlier version of this module took it as a
bare ``0.0147`` with unit ``-``. Nobody can check that against a datasheet:
17 psi/1000 psi, 1.7 psi/100 psi and a slipped decimal all look alike once the
units are gone, and the number that reaches the solver is three keystrokes from
being wrong by a factor of ten. It now carries its own ``pressure_ratio``
dimension, so ``-`` is rejected where one belongs and the vendor's own wording
is what gets typed in.
**Flow droop.** Outlet *falls* as flow rises, because the poppet must lift and
lifting takes a pressure difference across the sensing element. This is a
*separate* coefficient, and it turns out to be structurally load-bearing rather
than a refinement -- see below. It is tempting to think the seat's Cv already produces
it -- it does not. Cv sets where the regulator saturates and falls off a cliff;
at a tenth of rated flow the wide-open seat loss is a couple of psi while a real
unit has already drooped by ten. Both are modelled, and they are not the same
number.
The force balance is not forbidden, it is *deferred*: if a fitted droop
coefficient turns out to vary with something two coefficients cannot see, that
is the evidence that would justify building one.
Why zero flow droop is not "no droop" but "no answer"
-----------------------------------------------------
A regulator with ``flow_droop = 0`` holds its outlet at exactly the setpoint no
matter what passes through it. Put one between a COPV and a tank -- both of
whose pressures are states, hence boundary pressures at any instant -- and the
branch equation the network solver assembles is
.. code-block:: text
(p_up - p_dn) - dp(mdot) = 0 with dp(mdot) = p_up - p_set
which is satisfied for **every** value of ``mdot``. The flow is genuinely
indeterminate, the branch's Jacobian row is exactly zero, and Newton has nothing
to converge on. That is not a numerical quirk to regularise away; it is the
model saying that a perfect regulator does not determine its own flow.
Flow droop is what closes it. A non-zero coefficient gives the branch a slope of
``flow_droop / rated_flow`` and the flow becomes uniquely determined by how much
droop the downstream pressure is showing -- which is exactly the physical
mechanism. So :meth:`Regulator.check` reports a zero coefficient as an error
rather than a warning when the intent is a transient, and
:class:`IdealRegulator` exists precisely so that "I want a perfect setpoint" is a
deliberate, named choice with its consequence understood.