Skip to content

Feat/compartment fire physics - #15

Merged
backlundtransform merged 11 commits into
masterfrom
feat/compartment-fire-physics
Sep 11, 2026
Merged

backlundtransform merged 11 commits into
masterfrom
feat/compartment-fire-physics

Conversation

@backlundtransform

Copy link
Copy Markdown
Owner

No description provided.

backlundtransform and others added 11 commits September 2, 2026 10:38
Numerics.sln still listed CSharpNumerics.Engines and NumericTest.Engines,
which moved to their own repository in e98465b. dotnet build Numerics.sln
therefore fails on a clean checkout with MSB3202.

That went unnoticed because nothing builds the solution. publish.yml
builds and packs Numerics/Numerics/CSharpNumerics.csproj directly, and it
runs only on version tags.

The same gap has a second consequence worth naming: publish.yml has no
dotnet test step at all, so a release can be published to NuGet without a
single test having been executed. Locally the suite is blocked by a
Windows application control policy that prevents locally built test
assemblies from loading, so in practice the tests have not been running
anywhere.

Removes the two dead project entries and adds a CI workflow that builds
the solution and runs the suite on every push and pull request. It does
not publish, and publish.yml is left alone - adding a test gate there is a
change to the release path and should be a deliberate decision.
Third slice of the shipboard fire work, and the one that replaces
guesswork with published correlations. Pure functions in the physics
layer, no grid and no simulation state, following RothermelModel.

HeatReleaseRate covers t-squared growth with the four standard classes
from NFPA 72 and EN 1991-1-2, a full design curve of growth, steady
burning and linear decay, and the convective split that plume
correlations actually consume.

FirePlume is Heskestad: flame height, virtual origin, centreline
temperature and velocity, and entrainment. Centreline velocity is the one
the Marine buoyancy model needs - it is what turns 'smoke rises' into a
rate, and so what the constant-rise stand-in must be replaced by before
any timing means anything. Entrainment is why a compartment fills far
faster than the fire produces smoke: almost all of the plume mass flow is
room air dragged along with it.

FractionalEffectiveDose is Purser and ISO 13571: carbon monoxide, hydrogen
cyanide, oxygen depletion, and the hyperventilation multiplier by which
carbon dioxide makes every other gas worse. It also gives visibility
through smoke, which is usually what actually stops escape - ten metres of
visibility is lost at soot concentrations where the toxic dose is still
negligible.

This is what turns the smoke field into a decision variable. A
concentration map says where smoke is; a dose says how long someone has,
which is the only form an incident commander can act on.

Tests check the correlations against their published reduced forms rather
than against themselves: the growth coefficients against the four
published values, plume temperature and velocity against the standard-air
simplifications, and the dose model against known outcomes - 1000 ppm
carbon monoxide incapacitating in roughly half an hour, 200 ppm hydrogen
cyanide in minutes, five percent carbon dioxide roughly tripling uptake.

MQH hot gas layer temperature and a flashover criterion are deliberately
left out. Nothing consumes them yet, and building ahead of a consumer is
how an API shape ends up wrong.
Numerics.sln still listed CSharpNumerics.Engines and NumericTest.Engines,
which moved to their own repository in e98465b. dotnet build Numerics.sln
therefore fails on a clean checkout with MSB3202.

That went unnoticed because nothing builds the solution. publish.yml
builds and packs Numerics/Numerics/CSharpNumerics.csproj directly, and it
runs only on version tags.

The same gap has a second consequence worth naming: publish.yml has no
dotnet test step at all, so a release can be published to NuGet without a
single test having been executed. Locally the suite is blocked by a
Windows application control policy that prevents locally built test
assemblies from loading, so in practice the tests have not been running
anywhere.

Removes the two dead project entries and adds a CI workflow that builds
the solution and runs the suite on every push and pull request. It does
not publish, and publish.yml is left alone - adding a test gate there is a
change to the release path and should be a deliberate decision.
Two failures that the new CI surfaced. Both predate it; nothing had run
the suite before.

TestMandelbrot constructs a System.Drawing Bitmap, and
System.Drawing.Common is Windows-only from .NET 6 onward, so it can never
pass on a Linux runner. It now reports inconclusive off Windows rather
than failing. Inconclusive rather than skipped because the test is not
irrelevant there - it simply cannot run - and the Mandelbrot mathematics
is already covered by the complex number tests beside it. The alternative,
moving the whole suite onto a Windows runner for one rendering test, would
be a poor trade for a library that otherwise targets netstandard2.1.

StratifiedKFoldCrossValidator is a real bug in shipped code, and this
commit does not fix it. Folds are filled class by class with each class
restarting at fold 0, so a class with fewer members than the fold count
leaves later folds short, and small classes throughout leave them empty.
An empty fold then reaches the VectorN constructor and fails with 'values
cannot be null or empty', which says nothing about the cause.

I could not reproduce the root cause by reading: the test generates 100
samples in two balanced classes across five folds, which should give
twenty per fold and no empty one. Since the suite cannot be executed
locally - a Windows application control policy blocks locally built test
assemblies - guessing at a fix for cross-validation code that users are
already running would be worse than leaving it visible.

So this adds a guard that fails with the sample count, the class count and
the smallest class size. The test still fails, deliberately, but the next
CI run reports what the fold distribution actually is, which is what is
needed to fix it properly.
Root cause of the stratified k-fold failure CI surfaced, and it is worse
than the failing test.

Series.FromCsv returned Cols as header.Skip(1). The idiom was copied from
TimeSeries.FromCsv, where column 0 is the time axis and genuinely is
excluded from Data. Series has no time axis, so every name was shifted one
step left of the column it described, and IndexOf(Cols, name) pointed at
the wrong data column.

The stratified test was the loud symptom: its y became a continuous
feature instead of the class labels, GroupBy produced a hundred singleton
classes, the per-class round-robin put every sample in fold 0, and the
empty training set for that fold crashed the VectorN constructor.

The quiet symptom matters more. RollingCrossValidator and
LeaveOneOutCrossValidator resolve the target through the same lookup, so
every cross-validation run on a Series loaded from CSV has been training
with the target leaked into the features and validating against a feature
column - producing plausible-looking scores. The regression test in the
existing suite asserted BestScore > -10, which nonsense results clear
comfortably.

Cols is now the names of exactly the columns present in Data, in order,
whatever was excluded. TimeSeries.FromCsv is untouched: its Skip(1) is
correct. The stratified test now excludes the target from X via the same
looked-up index instead of a hardcoded one, and a regression test pins the
Cols-to-Data alignment directly.
…o end

RunBootstrap_SameSeed_ShouldBeReproducible failed in CI with two seeded
runs differing by 0.014. The test has always been flaky; it was exposed,
not broken, by the recent changes.

MonteCarloClustering.Seed seeded the bootstrap sampling and nothing else.
The model clone fitted inside each iteration kept its own seed, and the
test's KMeans had none - so every centroid initialisation ran off
new Random(), which draws a fresh nondeterministic seed per instance.
Two identical seeded runs therefore agreed only as long as k-means
happened to land in the same local optima both times. On well-separated
data that is usually true, which is why the test mostly passed, and
'mostly' is the definition of a flaky test.

A seeded run now hands each model clone a seed derived from the run's own
generator, through the SetHyperParameters mechanism the models already
have. Derived per iteration rather than fixed, so bootstrap replicates
keep independent initialisations. Models that take no seed, like DBSCAN,
ignore the key. Unseeded runs are untouched.

Exact numbers from previously seeded runs will differ, since the
generator now also feeds the model seeds. Nothing can have depended on
the old values - the model initialisation was nondeterministic, which is
the bug.
Third slice of the shipboard fire work, and the one that replaces
guesswork with published correlations. Pure functions in the physics
layer, no grid and no simulation state, following RothermelModel.

HeatReleaseRate covers t-squared growth with the four standard classes
from NFPA 72 and EN 1991-1-2, a full design curve of growth, steady
burning and linear decay, and the convective split that plume
correlations actually consume.

FirePlume is Heskestad: flame height, virtual origin, centreline
temperature and velocity, and entrainment. Centreline velocity is the one
the Marine buoyancy model needs - it is what turns 'smoke rises' into a
rate, and so what the constant-rise stand-in must be replaced by before
any timing means anything. Entrainment is why a compartment fills far
faster than the fire produces smoke: almost all of the plume mass flow is
room air dragged along with it.

FractionalEffectiveDose is Purser and ISO 13571: carbon monoxide, hydrogen
cyanide, oxygen depletion, and the hyperventilation multiplier by which
carbon dioxide makes every other gas worse. It also gives visibility
through smoke, which is usually what actually stops escape - ten metres of
visibility is lost at soot concentrations where the toxic dose is still
negligible.

This is what turns the smoke field into a decision variable. A
concentration map says where smoke is; a dose says how long someone has,
which is the only form an incident commander can act on.

Tests check the correlations against their published reduced forms rather
than against themselves: the growth coefficients against the four
published values, plume temperature and velocity against the standard-air
simplifications, and the dose model against known outcomes - 1000 ppm
carbon monoxide incapacitating in roughly half an hour, 200 ppm hydrogen
cyanide in minutes, five percent carbon dioxide roughly tripling uptake.

MQH hot gas layer temperature and a flashover criterion are deliberately
left out. Nothing consumes them yet, and building ahead of a consumer is
how an API shape ends up wrong.
Fourth slice of the shipboard fire work: the mechanism by which fire
climbs a ship without any opening. A deck plate over a burning compartment
conducts until the space above it is dangerous, smoke or no smoke.

HeatSlab resolves transient conduction through the thickness of a plate
with convective exchange on both faces, explicit finite differences with
internal sub-stepping so a caller may advance by any time step. This is
the standard fire-engineering treatment of thin structure - conduction
along the plate is ignored, conduction through it is resolved on its own
fine grid - and it is deliberately not the 3-D heat solver in the engines:
a 10 mm plate has no business being resolved on a 1 m ship grid, and the
right tool costs a few nodes per plate cell instead of a volume mesh.

Step returns the energy taken from the hot-side gas, so a coupled gas
model can remove it and keep the exchange two-way rather than the plate
being an infinite sink.

Radiation is not modelled, and near flames radiation dominates structure
heating, so these results are a floor on heating rate, not a ceiling. The
XML docs say so.

Tests are against closed-form results, not self-consistency: the
steady-state surface temperatures from the series-resistance solution, the
lumped-capacitance exponential in the low-Biot limit, and absorbed energy
equal to stored energy on an insulated slab. The domain-anchored one: a
bare 10 mm steel deck over an 800 K compartment passes a 140 K
unexposed-side rise - the A-class criterion - in well under fifteen
minutes, which is why A-60 divisions carry insulation.
Purser's correlation for hot-air exposure of a lightly clothed person:
about an hour to incapacitation at 40 C, twelve minutes at 100 C. Returns
zero at or below 30 C, beneath which the correlation has no validity -
ordinary warmth is not a dose. Radiant heat is a separate pathway and is
not included.

Completes the tenability set for the shipboard work: the smoke solver
carries a temperature field, and heat is the dose pathway that field
feeds.
CI failed HeatSlab_SteadyState_MatchesSeriesResistance by 2.24 K against a
0.5 K tolerance, and the miss is a confirmation, not a defect: the
approach to steady state is exponential with tau = rho*c*L/(h1+h2), about
1170 s here, and 6000 s is 5.15 tau - leaving an analytical residual of
exp(-5.15) times the 384 K gap, which is 2.23 K. The solver tracked the
closed-form decay to three decimals; the test had not given it time to
finish converging inside its own tolerance.

Twelve thousand seconds is ten time constants, residual about 0.01 K.
The tolerance stays at 0.5 K, doing real work against the discretisation
rather than absorbing an unconverged transient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@backlundtransform
backlundtransform merged commit a929750 into master Sep 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant