Class FluidConservationGuard

java.lang.Object
jline.solvers.fluid.moments.FluidConservationGuard

public class FluidConservationGuard extends Object
Detects a moment-closure trajectory that has left the model.

WHY IT EXISTS. The moment-closure drift can leave the simplex: on a station where min(n,c) is not the identity the Gaussian correction to the per-class share can drive a coordinate negative, and since the drift is conservative another grows to match. In MATLAB odeset('NonNegative') projects the ACCEPTED step, so the excursion is CLAMPED rather than reported -- which injects mass, collapses the step size, and leaves the window never returning. One MATLAB suite run sat in test_CQN_Cox_CS_9 for 3h16m and the 2026-08-27 run was killed after test11_interlock_lqnx had held it for 100 minutes.

THIS PORT CANNOT HANG THE SAME WAY, and the difference is worth stating rather than papering over: the JAR never projects the clamp back into the integration (PassageTimeODE.computeDerivatives evaluates the drift at the integrator's own state, and MethodStepHandler clamps only what it RECORDS), so the same divergence surfaces as an LSODA corrector failure or as a finished window holding a state that is not a solution. The second of those is silent, and this is what makes it loud. The check is therefore applied to the window's recorded rows rather than as an integration-halting callback.

THE TEST IS AN EXACT INVARIANT, not a heuristic bound on time or magnitude. The drift conserves the population of every CLOSED CHAIN exactly, so any deviation is a divergence and nothing else. The tolerance is a generous fraction of that population rather than a numerical tolerance: the integrator's own error is ~1e-4 relative, while the documented excursion reaches 5.2e4 against a true population of 0.05. A closed model whose population has moved by TOL is no longer solving the model, whatever it is converging to.

THE CHAIN IS THE CONSERVED UNIT, NOT THE CLASS, and the difference is the whole correctness of this check. sn.njobs(k) is the population class k STARTS with; class switching then moves jobs between the classes of one chain, so only the chain total is invariant. Watching classes instead condemns every class-switching model out of hand -- measured on cqn_twoclass_hyperl (313 of 447 accepted states), on init_state_ps (286 of 310) and on every one of the 162 fluid layers an LQN builds under the srvn.cs encoding, where the chain sum never moved at all. A cache model is the same story with the hit/miss classes.

A wall-clock budget would have caught the same thing and was rejected: it makes the answer depend on how busy the host is, so the same model would fall back on one machine and not on another. This invariant is deterministic.

See Also:
  • Field Details

    • TOL

      public static final double TOL
      Relative population drift that counts as having left the model.
      See Also:
  • Constructor Details

    • FluidConservationGuard

      public FluidConservationGuard(NetworkStruct sn, Matrix phases)
      Parameters:
      sn - the network struct, for the chain membership and populations
      phases - (nstations,nclasses) phases per pair, as the analyzer builds it from mu; this is what fixes the coordinate blocks
  • Method Details

    • closureActive

      public static boolean closureActive(SolverOptions options)
      True when the closure is what is being integrated, i.e. some sigma2 is nonzero.
    • violation

      public int violation(double[] x)
      Parameters:
      x - a state vector
      Returns:
      the chain whose conserved population has drifted past TOL, or -1
    • assertConserved

      public void assertConserved(double[] x, double t)
      Raises FluidNonHyperbolicException when the state has left the model, so the fallback ladder in SolverFluid answers with 'dae' and then with 'matrix'/'closing' instead of returning a state that is not a solution.
      Parameters:
      x - the window's final state
      t - the time that window reached