Repository navigation
Skarstrom 0.4.0
Added — automatic releases
- Merging a version change into
mainnow releases it:release.ymlruns on
a push tomainthat changes the version inCMakeLists.txt, creates the
tagv<version>itself, uses the version's CHANGELOG section as the
GitHub Release notes and publishes to PyPI (TestPyPI for pre-releases). Before,
only a hand-pushed tag released, and 0.2.0 and 0.3.0 were never tagged or
published. A pushed tag still works. A final version is published only if
CHANGELOG.md has its section with nothing left under Unreleased, and
CITATION.cff carries it. - New workflow
prepare-release.ymlkeeps one "Release X.Y.Z" pull request
open (branchbot/release) whenever Unreleased has entries, rebuilt from
mainon every changelog change. The next version comes from the Unreleased
subsections: Added, Changed, Removed or Deprecated give a minor release,
Fixed or Security alone a patch release; major releases and pre-releases are
chosen by hand (Run workflow withbump/suffix). scripts/release.py(current,next,prepare,check,notes) does the
version arithmetic and the three-file release change, with tests in
scripts/test_release.py(run by CI's docs job). The version onmainis
now the last released version; it is no longer bumped for development.
RELEASING.md and CONTRIBUTING.md describe the new flow.
Added — KPIs of every cycle (KPIHistory)
- The solvers record the KPIs of every tracked component when each cycle
completes (adsim::KPIHistory,adsim/solver/kpi_history.h;
CoupledSolver::GetKPIHistory,CoupledSolverChuncked::GetKPIHistory), so a
run's KPIs cover all its cycles, not only the last
settings.max_cycles_to_keepthe logs hold.Simulation::KPIs()(Python
sim.kpis(), the CLI'skpisandkpis.csv) now lists every cycle:
cycles still in the log are computed from it as before, evicted ones come
from the history. New:Simulation::KPICycles(),
Simulation::KPIHistoryRecords(), andadsim::ComputeCycleKPIs, the
per-cycle computation both use.KPIRecordmoved fromsimulation.hto
kpi_history.h(whichsimulation.hincludes).
Added — own ramp time for pressure ramps; exponential factor
- Linear, exponential and log-linear pressure profiles (pressure boundaries
and valve streams) take an optional ramp time of their own:
use_step_time_(deck fieldUse_Step_Time, default on) keeps the ramp
over the whole step as before; off, the profile'sramp_time_/
step_time_is used and must not exceed the step time
(CheckPressureProfile, run byCycle::PerformChecks). Registry:
endpoint[i].pressure_<kind>_use_step_time, andpressure_<kind>_timewhen
it is off. Python:sk.linear(end, time=10)for a pressure. - The exponential profile's factor is a parameter:
factor_(deck field
Factor, registry<head>_exponential_factor, Python
sk.exponential(..., factor=)), v(t) = v1 + (v0 - v1) F^(-t / t_r).
The default 100 is the old fixed value (99 % at the ramp time); F must be
greater than 1 (CheckProfileFactor). - Class versions:
LinearBoundaryValue4,ExponentialBoundaryValue4,
LogLinearBoundaryValue2. Older decks load with the step time and F = 100,
so their results are unchanged.
Added — method-of-manufactured-solutions hook and order test
- Verification hook
UserData::manufactured_sources_
(adsim/equations/manufactured_sources.h): source terms, functions of z
and t, for the overall balance, each component balance, each loading's
rate law, the momentum faces (a pressure-gradient source on Darcy faces
and interior Ergun faces) and the LTE energy balance, added to the
residuals in the scaling of the neighbouring physical terms. Testing only:
not serialised and not reachable from decks, the parameter registry,
Python or the web. With no source set the results are bit-identical
(bench_solverlogs of two example decks are byte-identical to the
previous residual code) and the residual cost is unchanged.
UpdateResidualsrecords the integrator time inUserData::residual_time_
for it. core/test/validation/test_manufactured_solutions.cpp: a smooth,
time-dependent manufactured solution with dispersion, LDF kinetics on a
Langmuir isotherm, Kozeny-Carman flow and (thermal case) the energy
balance, sources derived by hand and checked by finite differences.
Observed orders on 20-160 cells: upwind 1.0 in every field; WENO and
van Leer 2 (WENO interior 2.5-2.8 before the second-order central terms
take over); with the energy balance the temperature is first order for
every scheme (the convective face temperature is always upwind). About 4 s.
Fixed — the CSS state test never passed for an inert bed
- For a bed of inert packing (an adsorbent with no isotherm) the adsorbed
loadings are round-off, 1e-26 to 1e-22 mol/kg of either sign. The state
test of the mass-balance CSS methods
(CyclicSteadyState::EndOfCycleStateChange) scales the loading change by
the RMS total loading and skipped the field only when that scale was below
1e-300, so it divided noise by noise and reported a change of 0.77 to 51
on a bed whose state repeats: a mass-balance method with the state test on
(the default) never stopped.
The loading scale is now floored at the solver's absolute tolerance on the
loading unknowns (1e-8 mol/kg); a loading below it is not resolved. Every
adsorbing deck has an RMS loading far above the floor, so its state change
is unchanged to the last bit. Test:
CyclicSteadyState.InertBedReachesCSSUnderTheStateTest(the shared PSA
cycle with inert packing stops at the minimum-cycle floor, cycle 4). Found
by the dynamic-tank CSS test of the verification programme (WP19-E2a).
Added — convergence verification of cycle results (grid, integrator tolerance, CSS)
CycleKpiGridConvergence(core/test/validation/test_grid_convergence.cpp):
purity, recovery and specific energy of the shared PSA cycle at cyclic
steady state on 10, 20, 40 and 80 cells, for each advection scheme.
Convergence is monotone; the observed order (Richardson, three finest grids)
is 1.55 for upwind (inside the[1, 2]its error expansion allows) and 2.4
to 2.5 for WENO and van Leer; Roache's GCI of the 80-cell upwind KPIs is
0.06 to 0.12 % and contains the higher-order solutions.IntegratorTolerance(test_integrator_tolerance.cpp): the error falls with
reltol at slope 0.68 (breakthrough) and 0.77 (cycle state), inside the
[0.5, 1]of BDF error-per-step control, and floors at the absolute
tolerance;max_stepchanges results by less than the tolerance error. The
KPIs converge with reltol at slope 0.42, because the boundary flows are
integrated over the logged points and the flow transient after a pressure
switch (~2e-5 s) is resolved only by the integrator's own steps.CSSConvergence(test_css_convergence.cpp): the approach to CSS is
geometric (rate 0.106 isothermal, 0.966 with the energy balance), the KPIs
follow it, the error the stopping rule leaves isg Delta rho / (1 - rho)
(measured 6.11e-5 in recovery at the default state tolerance, 6.21e-5
predicted), and Aitken acceleration stops 25 cycles earlier on the same KPIs.scripts/verification/grid_convergence.pywrites the grid study of the
example decks (observed order, GCI, extrapolated KPIs); the table and the grid
each studied deck needs are on the Numerics page ("Convergence of the
results").
Added — extended Langmuir-Freundlich isotherm (toPSAil form)
- New competitive isotherm kind
extended_langmuir_freundlich
(IsothermType::EXTENDED_LANGMUIR_FREUNDLICH,
ExtendedLangmuirFreundlichIsotherm): q_i = (k1 + k2 T) z_i / (1 + sum z_j),
z_i = k3 exp(k4 / T) (P y_i / P_ref)^{n_i}, 1/n_i = k5 + k6 / T (Yang 1987,
toPSAil'scalcIsothermExtLangFreu.m; P_ref = 1e5 Pa keeps toPSAil's
bar-based k3). No existing kind maps it unless k2 = k6 = 0. Registry,
Pythonisotherm_kinds(), IAST (an exact Sips term) and the docs know it;
the fitter does not offer it yet. Near y = 0 each term is regularised with a
step at 1e-5 (loading changed by 1e-10 / y^2: 1e-4 at y = 1e-3).
Fixed — breakthrough decks no longer stall on fine grids
Ward2022_Q50ccmran on 10-160 cells but failed on 320 with every
advection scheme ("too much work": 50 000 steps, 24 000 Newton convergence
failures, 72 000 Jacobians) as the CO2 front left the column. The
difference-quotient Jacobian perturbed the pressure by IDA's increment
floor reltol |P| + abstol (0.1 Pa at 1 bar). A pressure outlet imposes the
outlet-face velocity as the explicit Ergun root of the end-cell pressure
drop, which is linear only over A² / (4B), a pressure scale proportional to
the cell width (0.12 Pa on 60 cells, 0.02 Pa on 320): the 0.1 Pa secant
was half the true slope there, and the modified Newton iteration diverged
unless the step was tiny. The pressure columns of theBandDQ,Coloured
andColouredCheckJacobians now use sqrt(eps) max(|P|, |h P'|) floored at
the pressure's absolute tolerance (1.5e-3 Pa at 1 bar); every other column
keeps IDA's increment. The deck now runs on 320 cells in 2 169 steps with
24 convergence failures (3 s), and on 640 cells in 2 047 steps. Results of
every deck move at integrator-tolerance level, since the Jacobian's
pressure columns change in the last digits: across the paper validation
metrics, front times by at most 2e-5 relative and curve RMSEs by at most
1e-5 absolute, and every paper test still passes.
JacobianMode::IdaInternal(IDA's own Jacobian) is unchanged. Test:
FineGridBreakthrough.Ward2022Q50ccmConvergesTo320Cells.
Fixed — WENO / van Leer outlet-cell mole-fraction sum drifts during fast outflow
- With
AdvectionScheme::WENOorVanLeerthe cell at a bed end that gas
leaves through could see its mole fractions drift from summing to one
while the outflow lasted (a valve surge, a blowdown): 5.0e-4 with WENO and
4.5e-4 with van Leer inErgunValveSurgeTest(Kozeny-Carman), against
1.1e-5 with upwind. The schemes clamped every face mole fraction to [0, 1], the bed-end faces
included; those take the upwind cell's value and are not rescaled to its
sum, so an end cell holding pure nitrogen at y = 1 + 1e-9 sent out a face
that summed to one instead of the cell's sum, and the cell's departure from
one was no longer carried out of the bed. Only the reconstructed values are
clamped now; the upwind faces are left as they are, as under Upwind. The
same clamp made the logged balances disagree with the solver (the stream
logs integrate the end cell's value, the solver used the clamped face):
the DAC charge with WENO had its total mole balance off by 3.45e-2 mol
(now 2.7e-5, the upwind value) and the outlet cell's sum by 4.0e-5 (now
9e-15); the temperature purge with van Leer logged 2.4e-5 mol of a
nitrogen that is in neither the feed nor the bed (now 1.3e-8). The drift
also reached 1.6e-6 (WENO) in the steep-front breakthrough of
AdvectionSchemeConservationat reltol 1e-8; every scheme now keeps about
1e-12 there. Upwind is unchanged (its faces were never clamped). Tests:
AdvectionSchemeConservation.EveryFaceSumsToItsUpwindCellSum(the
discrete identity, face by face); the valve-surge test run with every
scheme (ErgunValveSurgeTest.*WENO,*VanLeer), which bounds the outlet
cell's sum at the end of each feed step by reltol;UserDataTest.DACand
TemperaturePurgerun with every scheme (*WENO,*VanLeer), with their
logged balances bounded at the upwind level; and the steep-front sum
check, now at reltol 1e-8 with a 1e-9 bound. Found by the WENO-everywhere
experiment of the verification programme (WP20-E1).
Fixed — the ColouredCheck Jacobian diagnostic no longer changes the run
- In
JacobianMode::ColouredCheckthe band evaluation built for the
comparison learned every coupling it found into the pattern without
recolouring, and its count was discarded: a coupling the coloured pattern
missed stayed missing from the coloured Jacobian for the rest of the run,
no audit ever counted it (NumMisses()stayed 0), and the check run took a
different path from the Coloured run it was meant to observe. The
comparison now only observes (no learning, and no residual-call,
out-of-band or pattern counts, and a failed comparison evaluation does not
fail the step), so a ColouredCheck run is the Coloured run exactly and
reports a missed coupling until an audit adds it. Test:
ColouredJacobian.ColouredCheckObservesTheColouredRun(it starts from a
pattern learned from one evaluation, the new testing switch
IDASettings::jacobian_learn_once_for_testing_). - With the WENO and van Leer schemes the pattern learned in the first cycle
missed couplings of the columns in the three cells at each end (e.g. the
first cell's mole fractions on the inlet ghost cell's other mole
fractions); the audits added them later. Those columns now also take the
interior pattern, so the coloured Jacobian equals the band DQ Jacobian entry
for entry for all three schemes (ColouredJacobian.MatchesBandDQ_*_WENO/
_VanLeer). Results of WENO / van Leer runs in the default Coloured mode
move at integrator-tolerance level (they are now bit-identical to BandDQ
runs); upwind runs are unchanged, bit for bit.
Added — constant gas heat capacity and viscosity; heat of adsorption as −ΔU + RT
fluid.gas_heat_capacity_mode:coolprop(default, unchanged) or
constant, with a molar value per component in
fluid.gas_heat_capacity[<component>][J/(mol·K)] (a new component starts
at 29.1, 7R/2). The constants replace the CoolProp values that
Fluid::Initialisecaches; the molar mixing rule is unchanged, so equal
values give that heat capacity at every composition. The Gas phase
adsorbed-phase heat capacity follows them.fluid.viscosity_mode:polynomial(default, unchanged) orconstant,
with one mixture viscosityfluid.viscosity[Pa·s] in every cell.fluid.heat_of_adsorption_convention = "isosteric_rt": the stored heat
of adsorption is −ΔU, the internal energy of adsorption, and the heat
released per mole adsorbed is −ΔU + R·T at the local adsorbed-phase
temperature (LTE bed or LTNE solid temperature). This is the source term
of Yancy-Caballero et al. (2020) / PEESEgroup PSA, −(ΔU − RT); a constant
isosteric value fixes it at one temperature.- With the three switches a deck reproduces models that use fixed gas
properties (C_p,g = 30.7 J/(mol·K), μ = 1.72e-5 Pa·s in Yancy-Caballero
2020 and Haghpanah 2013). Defaults are unchanged, so every example deck
and validation result is unchanged.Fluidis class version 4; older
decks load with the defaults. Non-positive constants fail the deck
checks. Registry, Python (sim.fluid.gas_heat_capacity_mode,
gas_heat_capacity[...],viscosity_mode,viscosity) and the script
export carry the new fields. Docs:equation-of-state.md(constant gas
properties),energy-balance.md(heat-of-adsorption conventions). Tests:
FluidPropertyModes*and theIsostericRTcases of
ComponentEnergySourceLog.
Added — valves can read the bed at the end cell
- Solver setting
settings.valve_bed_pressure(IDASettings::valve_bed_pressure_,
"Valve_Bed_Pressure";IDASettingscereal version 7):face(the default,
unchanged behaviour) evaluates the bed side of every valve stream's linear
law at the bed-end face,end_cellat the last cell, as toPSAil does. A
connected valve then reads its partner's end-cell pressure, which every
stream and pressure boundary now logs (StreamData::GetEndCellPressure).
Decks from before load withface.
Added — sensitivity studies saved with the deck
- A deck now saves its sensitivity study's set-up (
UserData::sensitivity_study,
Sensitivity_Studyin the file;UserDatacereal version 4): inputs and their
distributions, outputs, run policy, seed, and the screening and propagation
sizes. Results are not saved. Decks from before load with no study. adsim/solver/sensitivity_study.hholdsDistribution,Input,RunPolicy
(moved fromsensitivity.h, which includes it; the API is unchanged) and the
newStudy.sensitivity.haddsStudyOutputsandCheckStudy(deck, study).- Python:
Study.from_deck(sim, **overrides),Study.save_to(sim)and
Study.setup(); the study's sizes are constructor arguments, the defaults of
screen(),propagate()andScreening.important(). - CLI:
skarstrom sensitivity deck.jsonwithout--inputruns the deck's
saved study; options given override it.
Added — cross-code regression against the PEESE model and toPSAil
core/test/crosscode/: twelve single-column breakthrough and purge cases
(CO2/N2 on 13X with concentration- and pressure-form Langmuir and Henry
isotherms, 1-50 % feeds, purge, adiabatic, a 25 % Ergun pressure drop,
CO2/CH4 on LiX, a steel wall with ambient loss) run in the PEESEgroup/PSA
column model (MIT) and in toPSAil (GPL-3.0; numerical outputs only), frozen
as data with their decks and replayed byCrossCodePeese.*and
CrossCodeToPSAil.*(about 15 s). Bounds come from the measured, attributed
differences: isothermal cases agree within 0.6 s on t50 at the same grid;
the adiabatic offsets (up to 14.5 s) are the CoolProp gas heat capacity and
the constant heat of adsorption. The folder README has the attribution.
Fixed — a connection replays a bottom pressure boundary's outflow with the right sign
PressureBoundaryCondition::Logdid not record the bed end it logged at,
so a pressure boundary'sStreamDataalways said TOP. For a pressure
boundary at the BOTTOM of its bed,StreamData::GetOutflowthen returned
gas leaving the bed as a negative outflow, and a no-valve stream connected
to it (e.g. a rinse or heavy-reflux inlet fed from a blowdown or evacuation)
drew that gas out of its own bed instead of taking it in: the connection
destroyed twice the transferred moles, and with a step-change blowdown IDA
failed at the start of the receiving step. The log now records the
boundary's location, as a stream's log does. A TOP pressure boundary was not
affected, and no bundled deck, cycle template or paper case connects a
stream to a BOTTOM pressure boundary, so no published result moves. Tests:
ConnectedStreamTest.PressureBoundaryDonorLogRecordsItsBedEnd,
ConnectionSign.*.
Added — verification: extreme operating corners
core/test/validation/test_extreme_conditions.cpp: very high and very low
pressure ratios, near-zero flow, very short and very long steps and high
temperatures each converge or fail cleanly, with finite and physical states.
Added — verification: thermodynamic bound on the separation energy
core/test/validation/test_thermodynamic_bound.cpp: at cyclic steady state
the logged pump energy of a vacuum-swing and a pressure-swing cycle exceeds
the reversible pump work, the bed's exergy change and the minimum work of
separation of the products, in that order.
Added — verification: parameter limits and degenerate mixtures
core/test/validation/test_parameter_limits.cpp: the rate at which the
model reaches its limits (LDF k to infinity and to zero, heat of adsorption
to zero, wall heat capacity and bed-wall coefficient to infinity, Langmuir b
to zero, pressure ratio to one, trace feed), and two degenerate mixtures
(identical components keep their feed ratio; zero affinity is inert).
Added — verification: metamorphic relations
core/test/validation/test_metamorphic_relations.cpp: equivalent decks must
give the same solution to within the integrator's measured error, for the
component order, a packing split into identical layers, a step cut in two,
inputs written in other units, and the step a cyclic steady state is started
from (with a connection whose lag changes under the rotation).
BreakthroughCasegainsn_layers.
Added — verification: evacuation, pressure equalisation and buffer tank
core/test/validation/test_unit_operations.cppchecks constant-speed
evacuation (P = P0 exp(-S t / V a R T)), two-bed pressure equalisation
through connected valve steps at the cyclic steady state (mole balance and
theexp(-2 cv t / 1e5 V a)decay of the pressure difference; with a
Langmuir isotherm, conservation of the two beds' inventory), and a
buffer-tank fill (it delivers the set percentage of the vented moles), each
without and with Henry adsorption.
Added — verification: thermal fronts
core/test/validation/test_thermal_fronts.cppchecks the speed of an
inert-bed thermal front against the energy-balance jump condition
(including the gas heat-capacity term), and the steady exponential axial
temperature profile of a flowing bed losing heat through the wall, with
first-order convergence to it.
Added — verification: blowdown composition path
core/test/validation/test_blowdown_composition_path.cppchecks that a
binary on linear isotherms follows the equilibrium-theory (Knaebel-Hill)
composition pathy(P)during a valve blowdown, with the departure from
the path equal to the LDF lag (it halves whenkdoubles).
Added — verification: binary competitive Langmuir fronts
core/test/validation/test_competitive_langmuir_fronts.cppcompares a
binary extended-Langmuir breakthrough (constant separation factor) with
equilibrium theory: the roll-up plateau of the weaker component from the
coherence condition, and the times of both fronts from the jump conditions.
Added — verification: Langmuir desorption simple wave
core/test/validation/test_desorption_simple_wave.cppchecks that a
Langmuir desorption front spreads as the equilibrium-theory simple wave:
the outlet times of three levels inside the wave converge at first order to
the method-of-characteristics times, and their Richardson extrapolation
agrees to 0.1 %.
Added — verification: LDF-limited front against the Thomas solution
core/test/validation/test_ldf_thomas.cppcompares the whole outlet curve of
a Henry-isotherm, LDF-limited, dispersion-free front with the
Anzelius/Thomas closed form, and checks that the grid error is exactly the
upwind scheme's: the variance minusL dx R^2/u^2is the Thomas variance,
the curve error falls at first order and, after the closed-formO(dx)
correction, at second order.
Changed — CI skips the builds for docs-only pull requests
- A new
changesjob lists a pull request's files. The C++ (Linux, macOS,
Windows), Python wheel, Emscripten and subproject jobs run only if one of
them is code: anything except Markdown,docs/,mkdocs.yml,
requirements-docs.txt,.gitattributes,CITATION.cffand issue
templates.docs/python.md(its example is run bypython/tests),
python/README.md,THIRD_PARTY_NOTICES.md,LICENSEandNOTICEcount as
code. The docs and workflow-lint jobs always run, and pushes tomain,
nightly and manual runs build everything. .gitattributes:CHANGELOG.mduses git's union merge driver, so two
pull requests adding entries at the top of Unreleased no longer conflict in
local merges.
Fixed — connected streams conserve moles when the donor's flow reverses
- A connected no-valve stream (e.g. a velocity-0 equalisation consumer)
replayed the magnitude of the connected stream's logged flow. When the
donor's flow reversed (a valve outlet drawing gas back into its bed), the
backflow reached the receiving step as more inflow and the connection
created gas. The replay now keeps the sign: what left the donor's bed enters
the receiver and what flowed back into it is drawn out (StreamData::GetOutflow;
the log records the bed end it was taken at). Test:
ConnectedStreamTest.ReplayKeepsTheSignOfTheDonorFlow. - Vysyaraju 2024
PSA_Exp1was the one case affected: PER took in 29 % more
gas than PED released, so the conservation-based He recovery read 17 % (product
He / fed He was 69 %). With the fix the two recovery measures agree (63.1 %),
He purity is 99.36 %, and the tolerances are re-measured (see the paper's
README); the deck's step cap is 0.5 s. The case still fails on macOS and
Windows (it did before the fix too): its single-bed equalisation lags PER's
pressure by one cycle, and which state the cycle settles in depends on
round-off. Linux passes. - CI: one run per pull request (pushes to feature branches no longer start a
second run), superseded runs are cancelled, the paper replications run on
pull requests (full suite on Linux and macOS, fast subset on Windows), Windows
builds with Ninja + ccache (windows-ninja-releasepreset), the Emscripten
builds use ccache, and ctest's per-test timings are cached so the longest
tests start first.
Fixed — face values of logged cell variables on non-uniform meshes
EquationLog::GetFaceVariable/GetComponentFaceVariableinterpolate
with the right weights. The cell-centre-to-face interpolation weighted
each neighbour by its own width,(y_L dx_L + y_R dx_R) / (dx_L + dx_R),
which biases the face value towards the larger cell. It is now
(y_L dx_R + y_R dx_L) / (dx_L + dx_R), exact for a linear profile, so a
face between cells of different widths (between layers with different cell
sizes, or between a void cell and the bed) sits where it should. The two
boundary faces now take the plain mean of the ghost and end cells. That is
the face value the boundary conditions impose (P_face = (P_0 + P_1) / 2),
and it matters because the ghost cell copies the width of the first or last
bed cell, not of a void end cell. A uniform mesh is unchanged: every
example deck, the paper validation anddocs/validation.mdare unaffected.
Tests:EquationLogFaces.*(hand-built log, non-uniform mesh, linear
profile exact at every interior face) and
VoidRegionSolverTest.FaceReconstructedBoundaryMolesMatchTheStreamLogs
(with voids, the face-rebuilt boundary moles now equal the stream logs to
round-off; they were 1e-5 to 2e-5 off).
What moves: the feed/product KPIs (purity, recovery, productivity,
energy) do not use these face values and do not change. The total
mass-balance CSS check (css.method2), which reads the boundary-face
pressure and temperature, changes for beds with void regions. Its error had
a spurious floor. On Perez 2019 case A with both voids enabled (1e-4 m³,
void cell 0.124 m against 0.01 m bed cells), the floor was about 3.46e-3
(tolerance 5e-3) and is now 2.4e-5, the same order as without voids
(1.9e-5); CSS is still reached at cycle 75. With 3e-4 m³ voids the old
floor of 1.26e-2 never passed the tolerance (no CSS in 200 cycles); CSS is
now reached at cycle 198. Face profiles plotted by front ends also move at
layer and void interfaces.
Fixed — every cycle step starts the integrator afresh
- A step can no longer step over a short pulse at its start. Cycle steps
ran back to back on one IDA history, so the first internal step of a step
inherited the size of the previous step's last,tstop-clipped, step. The
boundary condition's algebraic jump is out of the error test
(suppress_alg_) and an implicit step samples the forcing only at its end,
so a step whose forcing is a pulse shorter than that inherited step was
integrated as if the pulse were not there. OnVysyaraju2024_PSA_exp1the
PER step replays the PED blowdown (2.8 mol/s decaying in about 0.1 s): when
BLO happened to end on a 0.2 s remainder instead of a 0.015 s one, PER's
first step jumped the whole pulse, the bed missed its equalisation gas and
the run restarted its approach to cyclic steady state (PRES uptake
0.1497 → 0.284 → −0.002 → 0.071 mol …). Which cycle tripped hung on
round-off: natively at cycle 3 (and at cycle 48 with a reltol 1e-4 away),
in the WebAssembly build at cycle 51, so the web solver disagreed with the
native one at the last cycle (relative error 0.72). The engine before
#36/#37 tripped too, but early (cycle 6 natively), and had re-converged by
cycle 60. Now
SolverData::RestartForNewStep, called by the blocking and the chunked
solver at the start of every cycle step, restarts IDA from a consistent
initial condition at the new boundary conditions with fresh history and
hin = init_step(falling back to the unchanged end-of-step point if the
consistent IC fails). Native and WebAssembly runs of the deck now agree to
3e-5 per cycle, and native runs no longer depend on the reltol's last
digits. The run's IDA statistics stay cumulative across re-initialisations
(they used to restart from zero at each retry restart). Tests:
StepBoundaryRestart.*. Costs about 6 % run time on the deck. Other
decks' endpoint totals move by at most 3.4e-4 relative (Nguyen 2023 VII);
Vysyaraju 2024's PRES uptake at cycle 60 by 1.2e-3.
Fixed — gas-mixture heat capacity mixed on the molar basis
- The gas heat capacity of a mixture is mass-consistent.
UpdateHeatCapacity
mixed the per-kilogram pure-component heat capacities thatFluidcaches
from CoolProp by mole fraction,C_p,g = Σ y_i C_p,i, which is right only
when every component has the same molar mass. It now uses
C_p,g = Σ y_i M_i C_p,i / Σ y_i M_i[J/(kg·K)] (the molar mixture heat
capacity over the mixture molar mass), soρ_g C_p,g = C Σ y_i M_i C_p,i.
For 50/50 H₂/CO₂ the gas heat capacity was about 174 J/(mol·K) and is now
about 33; for CO₂/N₂ 15/85 it was 1.3 % high. Every user of the gas heat
capacity takes this one value: the LTE and LTNE energy balances, the
convective enthalpy terms, the thermal dispersion and heat-transfer
correlations, and the logged heat capacity. The rule lives in
Fluid::MixtureHeatCapacity/Fluid::MixtureHeatCapacityFromSums, with
Fluid::MolarHeatCapacity(C_p,i M_i) and
Fluid::AdsorbedPhaseHeatCapacity(the Gas phase adsorbed-phase option,
which was already per component and is unchanged) replacing the two copies
in the equations. A single-component gas, or a mixture with all but one
mole fraction zero, is unchanged. The stream "enthalpy flows" and the pump
and compressor energy do not use the gas heat capacity (they are molar flow
× temperature and polytropic relations) and are unchanged. Tests:
GasHeatCapacityTest.*andGasHeatCapacitySolverTest.*.
Results move for decks with light components: Casas 2012 (H₂/CO₂)
breakthrough times fall by 0.2–2.2 % (Fig. 4 50/50: H₂ 87.98 → 87.40 s,
CO₂ 445.1 → 436.0 s; Fig. 6 75/25: H₂ 133.2 → 131.5 s, CO₂ 359.3 →
351.4 s), the Streb 2021 He-carrier breakthroughs move (thermocouple RMSE
down 2–18 %, the A-series outlet and front-time errors up), Nguyen 2022
front times fall by up to 1.5 %, Vysyaraju 2024 He recovery rises from
17.54 to 17.91 %; CO₂/N₂ and O₂/N₂ cycles move in the fourth digit.
Fixed — bed-variable results at the right positions and boundary values
Simulation::Series/sim.seriesandSimulation::Profile/
sim.profileread the log at the right places. They mapped the raw cell
rows, ghost cells included, onto the face positions, so every profile and
everyzquery sat half a cell downstream,bottomreturned the inlet ghost
cell andtopthe last cell. In an adiabatic CO2/N2 breakthrough (feed
313.15 K, 30 cells, t = 500 s)series("temperature", location="bottom")
read 281.3 K, the ghost's 2 T_feed − T_1 (as low as 269 K during the run),
andtoppressure read 100004 Pa against the 100000 Pa set pressure. Now:- cell variables are at the cell centres and face variables (velocity,
volumetric and molar flow rate, approach to fluidisation) at the faces;
profiles leave out the ghost cells (AxialProfile::faces/prof.faces
says which gridzis).Simulation::Positions/sim.positionsare
the cell centres, as documented; the newFacePositions/
sim.face_positionsare the faces. topandbottomare the physical boundary values. Pressure,
temperature and mole fraction are the logged values of the step's
endpoint at that end, identical tostream_series(several streams are
mixed by flow): at an inlet the value the boundary imposes (feed
composition, feed temperature after any compressor, set pressure), at an
outlet the bed-end face value. A closed end, and every other variable,
gives the face value (the mean of the ghost and the end cell; a mole
fraction's ghost, which is the face value itself; the nearest packed cell
for adsorbent-only variables). Azbetween a bed end and the first cell
centre interpolates between the two. The time base is unchanged.- The same logic is the new public header
adsim/solver/bed_series.h
(ExtractBedTimeSeries,CellCentrePositions,IsFaceRow);
ExtractBulkTimeSeries/ExtractComponentTimeSeries
(parameterisation.h) use it and take an optionalBedBoundarySources
(boundary log and steps) for the endpoint values. Front ends that fit to
or plot these series move with it.
- cell variables are at the cell centres and face variables (velocity,
stream_series(..., "pressure"): the bed-end face pressure of an
endpoint (a pressure boundary's set value) is now readable
(BoundaryVariableType::PRESSURE, appended tokBoundaryVariableTypeList),
andBoundaryLog::GetStreamDatareturns an endpoint's logged data.- Fitted or validated numbers that compared bed-end or
zseries (the
example scripts' outlet curves and thermocouples) move slightly: outlet
compositions and temperatures were already the end cell, so mainly
thermocouple positions (half a cell) and inlet-end series change.
Fixed — component mass balance with axial dispersion (Danckwerts inlets)
- Inlet faces are Danckwerts. With non-zero axial dispersion the component
balance did not close at cyclic steady state: the species residual let a
dispersive flux eb·D·C·(y₁ − y_in)/Δz cross the inlet face on top of the
convective u·C·y_in, because the inlet ghost cell holds the feed composition.
Totals still balanced (the dispersive fluxes sum to zero over components) but
each component did not, by an amount proportional to D and nearly independent
of the grid, and the stream logs, KPIs and the CSS mass-balance check never
saw the extra species (reported: CO₂ out/in 1.0035 at D = 3.1e-4 m²/s and
1.032 at 3e-3 m²/s). No dispersive flux now crosses the two boundary faces of
the bed (UpdateMoleFraction), so an inlet face carries exactly u·C·y_in and
the first cell may differ from the feed; at outlets and closed ends the ghost
mirrors the first cell, so nothing changes there. The energy balance had the
same defect: conduction across the inlet face, and an upwind value at the
inlet face equal to the mirrored ghost temperature 2·T_in − T₁ rather than
T_in. Both are fixed (UpdateTemperature,UpdateFluidTemperature). The WENO
and van Leer reconstructions take the ghost value at the boundary faces
(they averaged ghost and first cell, which at an inlet is not the feed).
GetTotalComponentMolesIn(CSS mass-balance check, mass-balance panels)
reconstructs the boundary flux without the dispersive term, matching the
residual. - Flow boundaries on Darcy layers (Kozeny–Carman, Blake–Kozeny) deliver exactly the stream flow.
FlowBoundaryCondition::Applycomputed the boundary-face flow of a Darcy
layer with the ghost cell's mobility alone, K/(μ_ghost·Δz), while the face
velocity that carries the gas into the bed obeys the face's Darcy law with
the two half-cells in series. Whenever the ghost and first-cell viscosities
differ (feed composition vs the bed gas, or the mirrored ghost temperature)
the bed took in a different flow from the one the stream logs record (≈1 %
in the new test; several per cent in the Streb 2021 decks, whose beds start
in helium). The condition now matches the stream flow against the face's own
velocity unknown, as Ergun faces (and the momentum-balance docs) already did. - New regression tests
DispersionMassBalance.*
(core/test/validation/test_dispersion_mass_balance.cpp): a dispersive
(D = 3e-3 m²/s) single-bed cycle with a flow-condition inlet at the bottom and
a pressure-condition inlet at the top. Per step and component, the moles the
stream logs record entering the bed equal the bed's inventory change (before
the fix: errors of 0.4–50 %), and at cyclic steady state out/in of every
component is 1 within 5e-4, the log quadrature error (before: O₂ 1.053, N₂ 0.983). - Verification tests adjusted:
OpenSystemEnergyBalanceConvergesUnderGridRefinement
asserted first-order convergence of an energy closure error that came from the
inlet ghost upwinding; the error is now 1.5e-3 → 6.6e-5 over the four grids
(at least first order is asserted, and < 2e-4 on the finest grid instead of
< 5e-3).EndOfCycleProfileConvergesallows an absolute 2e-4 mol/m³ wobble at
the ~1e-3 mol/m³ floor the gaps level off at. - Validation numbers that moved (example decks re-saved; tolerances
re-measured where they no longer held, each documented in the paper's
README):- Ward 2022 (species fix; the bed had taken in ~2 % more CO₂ than fed): CO₂
front time vs experiment +1.9/+1.7/+0.1 % → +4.2/+3.3/+1.5 % at 50/100/200
cm³/min, outlet-curve NRMSE 0.18/0.13/0.029 → 0.33/0.24/0.12; thermocouple
RMSE 1.19/2.06/4.01 → 1.18/1.18/2.76 K. - Streb 2021 (flow-face fix): CH₄ fronts ~3 % earlier; CH₄ front-time error
A2 7.6 → 10.5 %, A3 0.3 → 3.1 %, A5 12.1 → 14.2 %, A6 8.7 → 11.7 %,
A7 7.3 → 10.2 %, B1 0.7 → 2.1 %; A3 y_CH₄ NRMSE 0.21 → 0.50; A6 y_CO₂ NRMSE
0.048 → 0.10; A2 thermocouple RMSE 14.1 → 18.6 K. - Nguyen 2023: CO₂ recovery of IX and X no longer exceeds 100 %
(100.35/100.78 → 98.27/97.64 %); purity X 80.84 → 79.67 %, IX 84.69 →
84.28 %; VIII productivity 0.1566 → 0.1545 (−1.4 %). - Within their bounds: Perez 2019 A–F recovery down by up to 1.1 pp
(purity ≤ 0.2 pp); Arora 2021 O₂ recovery at cycle 50 4.24 → 4.53 % (PSA)
and 19.88 → 20.74 % (PVSA); Ferré 2026 CH₄ recovery up by up to 0.75 pp;
Casas 2012 H₂ breakthrough times ≤ 1.2 % earlier; Nguyen 2022 outlet-curve
NRMSE improves (Fig. 4 50 %: 0.061 → 0.042); Vysyaraju 2024 He recovery
17.32 → 17.54 %; Ward & Pini 2024 ≤ 0.2 %.docs/validation.mdand
validation_summary.jsonare regenerated.
- Ward 2022 (species fix; the bed had taken in ~2 % more CO₂ than fed): CO₂
Changed
- Cyclic steady state: the mass-balance methods no longer stop by accident.
On the way to CSS the per-cycle balance error swings through zero (a bed
alternately gaining and losing a component), so the old single-cycle test
with its 0.5 % band passed early: on the Yancy-Caballero (2020) 13X cycle at
10 cells it stopped at the fifth cycle with purity and recovery 4–13 × 10⁻³
from their converged values, and onPerez2019_A…_F26 to 166 cycles
early (KPIs up to 1.8 × 10⁻³ off). Mass Balance (Total) and Mass Balance
(Per-Component) now also require the end-of-cycle bed state to have
stopped changing: the largest relative RMS change since the previous cycle
of the component loadings (scaled by the total loading), the mole fractions
and the fluid, solid and wall temperatures
(CyclicSteadyState::EndOfCycleStateChange) must be within the new
css.state_tolerance(default 2 × 10⁻⁵; 0 switches it off). Every
method's test must now hold oncss.consecutive_cyclescycles in a row
(default 3) before CSS is declared. The balance tolerance is unchanged: a
tighter one cannot be relied on, because the balance carries a small
systematic error of its own (up to 1 × 10⁻⁴ at CSS onPerez2019_E).
With the defaults the Yancy cycle stops at cycles 59 and 100 (C1, C2,
0-based) instead of 4, within 1.4 × 10⁻⁵ of the 150-cycle KPIs, and the
example decks within 4.7 × 10⁻⁵ of their converged KPIs (Perez2019_F, which
closes only ~1.5 % of its gap per cycle, 1.3 × 10⁻⁴); the stopping cycles
and errors of 14 decks are tabulated indocs/cyclic-steady-state.md.
CyclicSteadyStateis class version 2: decks saved before it load with the
defaults, so existing decks that use a mass-balance method run longer and
report converged (different) purity and recovery, and Molar Profile decks
stop at least two cycles later.css.state_tolerance = 0with
css.consecutive_cycles = 1restores the old test. The solver's logs keep at
least the cycles the check reads (one more thanconsecutive_cycles), even
whensettings.max_cycles_to_keepis smaller.CSSResult,CSSRecordand
Python'ssim.css_history()gainstate_change,state_tolerance,
passing_cyclesandrequired_cycles; the solver log at level medium
prints them after every cycle. Every paper case that runs to CSS moved
(docs/validation.md): Perez 2019 A–F by up to 0.17 pp (A's recovery
against the experiment crossed its bound, 0.294 → 0.417 pp; the bound is now
0.5, see the Perez README), Nguyen 2023 I–X by up to 0.22 pp, Ferre 2026 by
up to 0.32 pp, Ward & Pini 2024 by 0.04 pp and Vysyaraju 2024's He recovery
by 0.64 pp (17.32 → 16.69 %); all within their bounds. Every bundled deck is
re-saved with the two new fields, and the CSS decks with their new results. - Adsorbent library: one recommended entry per material (catalogue schema 3).
library.jsongroups its entries into 22materials(id, name, family,
recommendedentry and why,aliases). A material's other entries are
variants, each naming the product, form or data range that sets it apart;
labels are the material name plus the variant ("Zeolite 13X (Zeochem Z10-02,
CO2/N2 to 1 bar)") instead of "Material (Author year)". Each isotherm records
itsdata_quality(measured with tabulated data, measured fit only, refit of
other authors' data, copied from an earlier paper, not yet checked). The
validator, the native library tests and the generated docs page (now grouped
by material) enforce it.familymoved from the entry to its material. AddLibraryAdsorbent,Simulation.add_library_adsorbentand
skarstrom.adsorbent_pathaccept a material id (e.g."zeolite-13x") and add
or return that material's recommended entry;AdsorbentLibrarygains
materials,FindMaterial,ResolveandVariants. Entry keys are unchanged.
Fixed
- WENO and van Leer advection (
advection_scheme = weno | van_leer) are fixed; Upwind (the
default, and every bundled deck) is unchanged. Compared line by line with the WENO of
Haghpanah et al. (2013) / Yancy-Caballero et al. (2020) (PEESEgroup/PSACycleSteps/WENO.m):
(1) WENO's upwind-side candidate was the face value one face upstream,(y[n-1] + y[n])/2,
instead of the extrapolation3/2 y[n] - 1/2 y[n-1], which made the scheme first order;
(2) both schemes gave the bed-end faces the average of the ghost and the first cell, but the
ghost holds the inlet composition itself, so the bed received(y_feed + y_1)/2while the
stream logs, KPIs and cycle mass balance countedy_feed(CO2 balance 0.94-0.98 on the
Yancy-Caballero 13X cycle at 10 cells with dispersion off, where upwind closes to 1.0000);
(3) built component by component, the limited faces did not sum to the cell sum, and
linearised about a steep front they weight the downwind cell, so a departure of the cell sum
of mole fractions from one grew without bound (van Leer reached a sum of 90 in a CO2/N2
breakthrough). Now the bed-end faces take the upwind value; the first interior face next to
an inlet extrapolates from the inlet value half a cell away (2 y_1 - y_0, as the reference;
van Leer doubles that slope likewise); void cells are treated as well mixed; WENO uses the
cell widths on non-uniform grids; and the interior faces are rescaled so their mole fractions
sum to the upwind cell's sum, which couples the mole fractions two cells apart, so the
Jacobian half-bandwidth for these schemes grows from2 neqto2 neq + components - 1.
On the Yancy-Caballero 13X cycle at 10 cells, WENO now gives a CO2 recovery of 90.0 % for
the minimum-energy case (PSASimulator.jl WENO reference 89.7 %; was 77.5 %), and its CO2
balance with dispersion off closes to 1e-5 at every grid. Tests:
AdvectionSchemeConservation.EachSpeciesIsConservedThroughTheBedEndsand
AdvectionSchemeConservation.CellMoleFractionsKeepSummingToOneAtASteepFront. CreateBoundaryValueandCreateInletTemperatureProfileknow the double-exponential
profile. The web document layer builds profiles through the first factory, so a deck
with a double-exponential pressure boundary (the Perez 2019 decks) lost it to a null
profile, i.e. a constant 1 bar, in the browser. A unit test now checks every listed
kind has a factory entry.
Added
- A Blake–Kozeny momentum model (
MomentumModel::BlakeKozeny= 2, builder kind and
registry choiceblake_kozeny, Pythonlayer.set_momentum("blake_kozeny")/
add_layer(..., momentum="blake_kozeny"), display name "Darcy (Blake-Kozeny)"):
Darcy's law with K = ε³ d_p² / (150 (1 − ε)²), i.e. Ergun's viscous term without the
inertial one. It is the pressure-drop law of Haghpanah et al. (2013) and Estupiñan
Perez et al. (2019, SI Eq. S5). It takes the Darcy face path everywhere, like
Kozeny–Carman. The stored values 0 (Kozeny–Carman) and 1 (Ergun) are unchanged, so
existing decks load as before; a deck that uses the new value needs this engine. - An optional constant molecular diffusivity for the macropore LDF coefficients
(macropore,macropore_capacity,macropore_knudsen): registry leaf
adsorbent[...].kinetics[<component>].molecular_diffusion_coefficient[m2/s], Python
set_rate_model(..., molecular_diffusion_coefficient=1.6e-5)/kinetics[c].molecular_diffusion_coefficient.
Above 0 it replaces the binary-diffusion D_m computed from T and P (which scales as
1/P, so at 3-10 kPa evacuation pressures the coefficient was 10-33 times the value
of the papers that use a constant D_m, e.g. Haghpanah et al. 2013 and Estupiñan Perez
et al. 2019, Table 1). The default 0 keeps the previous behaviour bit for bit. Class
versions bumped (macropore and macropore + Knudsen to 3, capacity factor to 2); older
decks load with 0. Haynes is unchanged: its film term uses D_m with the Sherwood
correlation, so a constant there is not a drop-in. - Ideal Adsorbed Solution Theory (IAST) as a per-adsorbent mixing rule
(AdsorptionSource::mixing_rule_,MixingRule::None|IAST; registry
adsorbent[<id>].mixing_rulewith choicesnone,iast; builder
SetMixingRuleand kind listMixingRuleKinds(); Pythonads.mixing_rule,
ads.set_mixing_rule(kind)). Underiastthe mixture loadings come from the
components' pure isotherms (any kinds, mixed freely), solved per cell by a
bracketed Newton iteration on the reduced spreading pressure, warm-started
from the cell's last solve. Closed-form spreading pressures for most kinds,
Gauss–Legendre quadrature for Toth, Toth 2 and Redlich–Peterson. Absent
components (y ≤ 0) take the infinite-dilution limit, so the loading is
smooth through zero mole fraction. The evaluation functions are public in
adsim/equations/iast.hand in Python asskarstrom.evaluate_iast. The
solver log warns when IAST is combined with an extended (competitive) kind.
The capacity-factor macropore coefficient (macropore_capacity), which
evaluates q* itself, takes the IAST mixture loading under the IAST rule.
AdsorptionSourceis at cereal version 1; older decks load withnoneand
run exactly as before. Docs: the isotherms page. IsCompetitiveIsothermKind(type)andPairedIsothermKind(type, out)in
adsim/equations/isotherm_kinds.h: which kinds couple components through a
shared denominator, and each one's pure counterpart with the same parameters
(and back). The IAST warning uses them and names the pure kind to switch to.- A double exponential profile (
DoubleExponentialBoundaryValue,
BoundaryValueType::DOUBLE_EXPONENTIAL, builder kinddouble_exponential, Python
sk.double_exponential(end, rate_1, rate_2, weight, start=)): a two-term decay
towards the end value, v(t) = v1 + (v0 − v1)[w e^(−r1 t) + (1 − w) e^(−r2 t)], the
form Estupiñan Perez et al. (2019) fitted to their rig's evacuation pressure. The
rates are entered in 1/s. Registry leaves<head>_double_exponential_start/_end/ _rate_1/_rate_2/_weightandpressure_double_exponential_end/_rate_1/_rate_2/_weight. - A capacity-factor macropore LDF coefficient (
MassTransferCoefficientMacroporeCapacity,
kindmacropore_capacity): k = (15 ε_p D_m / (τ r_p²)) · c / (ρ_p q*), the
Haghpanah et al. (2013) form the Rajendran-group papers use. The ratio is evaluated
from the isotherm at max(c, c_floor), with the isotherm re-evaluated at the floored
concentration, so it stays finite where c and q* both vanish; capped at 1. The plain
macroporekind is unchanged (it never applied the factor; itsratio = 1.0stub is
gone).
Changed
- Native WebAssembly exceptions by default. A new CMake option,
ADSIM_EMSCRIPTEN_EXCEPTIONS, chooses the C++
exception scheme of an Emscripten build:wasm(the new default,-fwasm-exceptions) orjs(-fexceptions,
the previous behaviour). The whole engine tree is compiled with the chosen flag, andAdsim::adsimnow passes it to
the consumer's compile and link. On the Skarstrom web solver, native exceptions ran 10 % faster on six example decks
(4–17 %) with a 12 % smaller.wasm; they need Chrome 95, Firefox 100, Safari 15.2 or Node 17. Consumers: drop
your own exception flags.-sNO_DISABLE_EXCEPTION_CATCHING=1is rejected by Emscripten together with
-fwasm-exceptions; keep the old scheme with-DADSIM_EMSCRIPTEN_EXCEPTIONS=js. The subproject consumer test now
checks that an exception thrown inside the engine reaches the consumer's catch, and CI runs it with each scheme. - CI builds the Emscripten consumer with Emscripten 6.0.13 (was 4.0.10).
- Perez 2019 example decks (
Perez2019_A/B/C1/D1/E/F) now use the paper's model
forms, after an audit of the deck against the paper and its SI
(core/test/papers/perez_2019/README.md, provenance and attribution tables): the
two-term exponential evacuation pressure of Sect. 4.3 (0.019 and 0.101 1/s; the split
of the terms is not printed and an equal split is used, chosen on A), wall ends at
ambient (SI Table S2) and the capacity-factor LDF of SI Eq. S4. The four-step
recoveries, 8-10 pp above the experiments before, are now within 0.7-1.1 pp for A and
B; the attribution shows the evacuation pressure history was the cause. The decks keep
Kozeny-Carman: the paper calls the pressure drop negligible, Ergun is worth 0.3 pp, and
it fails the initial-condition calculation of the fast-feed connected configuration in
ConfigD1_ConnectedBC(an open engine item).examples/perez_2019.pybuilds the same
decks.
Changed
- Ward 2022 example decks (
Ward2022_Q50ccm/Q100ccm/Q200ccm) now use the
paper's model forms, after an audit of the deck against the paper text
(core/test/papers/ward_2022/README.md, provenance and attribution tables):
helium as the carrier (the CO2/He experiment), the paper's dispersion
coefficient (Eq. 4 with D_m = 0.605e-5 m2/s, times the factor 4 the paper
fitted), -dU_1 + RT as the heat of adsorption (32.4 kJ/mol), the wall ends
held at ambient (Table 4) and the Ergun pressure drop. The CO2 front now
arrives 0.1-1.9 % late instead of 4-6 % early, the curve NRMSE is 3-18 %
instead of 34-38 %, and the temperature rise is within 0.9-2.9 K of the
measurement (it was 8-11 K high). The Ward test's temperature-rise
indicator follows the paper's Table 9 (against the feed temperature).
examples/ward_2022.pybuilds the same decks.
Added
- A log-linear profile (
LogLinearBoundaryValue,BoundaryValueType::LOG_LINEAR,
builder kindlog_linear, Pythonsk.log_linear(end, start=, time=)) for pressures,
flows and inlet temperatures: v(t) = v0 (v1 / v0)^(t / t_r), a geometric ramp whose
relative rate of rise d(ln v)/dt is constant. A pressurisation from vacuum with a
linear ramp puts most of the relative pressure rise, and with it the inlet pressure
gradient and approach to fluidisation, in the first seconds; the log-linear ramp
spreads it over the step. It holds its end value after the ramp, and ramps linearly
when the start or end value is zero or negative. Registry leaves
<head>_log_linear_start/_end/_timeandpressure_log_linear_end.
Changed
- Solver residual, byte-identical: the competitive concentration-based
Langmuir isotherms (ExtendedLangmuirIsotherm_Concentration,
ExtendedDualSiteLangmuirIsotherm_Concentration, the kinds most example
decks use) gain the single-passUpdateAllthe pressure-based ones had, so
their shared denominators and exponentials are computed once per cell
instead of once per component (Illustrative TSA: 17% fewer instructions for
the run, the isotherm 2.6x cheaper). Every competitive isotherm's
per-componentUpdateis now that pass for one component, which also
serves the capacity-factor MTC that re-runs the isotherm. The binary
diffusion model memoises the Neufield collision integral andT^1.5per
cell on the exact temperature bits (TemperatureCache), since most residual
evaluations repeat the previous temperature in most cells (15% fewer
instructions in that model). - Solver residual, byte-identical: the Langmuir and dual-site Langmuir isotherm
functions (and their concentration-based variants) compute eachexp(q/(R T))
factor once instead of twice (GCC's default-fmath-errnodoes not merge the
duplicate calls, so Linux builds paid 4expper cell per component), and a
constant mass-transfer coefficient is evaluated once per layer instead of once
per cell (ExpressionParam::IsRunConstant()). -17% instructions per residual
call on FiveStepProductPurge; CSV output identical on six example decks. - Solver: two settings are now on by default (
IDASettings, transient):suppress_alg_withpressure_differential_: the pressure is tagged a
differential unknown (its residual carries dP/dt) and IDA's local error
test leaves out the algebraic entries, i.e. the ghost cells (mirror
relations with no time derivative) and the face velocity. Their jumps at
a boundary-condition switch used to fail the error test and force tiny
restart steps; the cyclic example decks now take 40-60% fewer steps
(1.3-2.1x wall) at the same accuracy against a reltol = 1e-8 reference.alias_lte_solid_temperature_andderived_concentration_: with local
thermal equilibrium the solid temperature is a copy of the fluid
temperature instead of an unknown, and the gas concentration is derived
from P and T (C = P / RT; no residual reads dC/dt). Two unknowns per cell
fewer, a narrower band, one colour fewer (1.1-1.25x).
The equations solved are unchanged; the integration path is not, so
results move in their last digits (the example decks are re-saved). The
per-step boundary/outlet logs have fewer points than before, so a value read
from them at an exact time is interpolated over a longer step. Set the
switches tofalsefor the previous behaviour.
Changed
- Solver: the banded Jacobian matrix is sized from the coupling reach a
difference-quotient probe measures at the initial state (12 / 16 instead of
2 neq = 20 / 20 diagonals at ten equations per cell), about 45% less LU
work; results are byte-identical. If a Jacobian audit later finds a
coupling outside the band, the solver widens it to the full reach and
restarts from the current point (logged at medium level).
IDASettings::band_from_probe_switches it off.
Added
- Transient (not serialised)
IDASettingsswitches for solver experiments,
all off by default:alias_lte_solid_temperature_and
derived_concentration_(drop Ts and C from the DAE unknowns),
suppress_alg_(IDASetSuppressAlg),delta_cj_lsetup_
(IDASetDeltaCjLSetup) andpressure_differential_(tag P as a
differential unknown).bench_solvergets--unknowns,--band,
--suppress-alg,--dcj,--p-diffand--reltol; its report adds the
band widths, out-of-band count and whether the band was widened. - Engine internals de-duplicated (no behaviour or deck-format change): the
twenty concrete isotherm classes share oneParamIsotherm<Params, kType>base and
only declare theirUpdate(archives are byte-identical: the parameter map keeps
its name and each class itscereal_class_version); the four flow boundary value
kinds keep theirprofile_and serialisation inIFlowBoundaryValue; the
per-streamGetComponentMolarFlow/GetEnthalpyFlow, which were copied into
every stream kind, live once inIBoundaryStreamwith aBedInletTemperature
hook for the pump-driven kinds' compression heating;EquationLog/BoundaryLog
getters and face interpolation, the KPI bed-accumulation integrals and the
compressor / vacuum-pump cost structs each have a single implementation. The
unusedCalculateAccumulation_Volumeis gone. Test fixtures that were copied
across files (the validation PSA cycle, the feed/hold integration deck, the
Ogata-Banks solution) are shared headers now.
What's Changed
- Ergun: boundary faces use the interior neighbour's gas state; warn once per run about constant-pressure step changes by @samaffleck in #9
- Solver log: drop the end-of-run IDA statistics line by @samaffleck in #10
- Start 0.2.0 development after the 0.1.0 release by @samaffleck in #11
- Install consumer: request the tree's own version, not a fixed 0.1 by @samaffleck in #12
- Adsorbent library: drop the legacy Zeolite 13X entry and bulk density by @samaffleck in #13
- Sensitivity analysis and KPI error bars by @samaffleck in #14
- Give the polynomial pump efficiency its P^2 term by @samaffleck in #15
- Release 0.2.0 by @samaffleck in #16
- Upgrade cibuildwheel and pip to address certificate authority issue by @samaffleck in #17
- Allow expressions in inlet and initial compositions by @samaffleck in #18
- Wall pressure design, operating-limit warnings and warning switches by @samaffleck in #19
- Release 0.3.0 by @samaffleck in #20
- Size the Jacobian band from a coupling probe; solver experiment switches by @samaffleck in #21
- Solver: suppress algebraic error-test entries and reduce the DAE unknowns by default by @samaffleck in #22
- Add a log-linear boundary value profile by @samaffleck in #23
- Residual: one exp per temperature factor in the Langmuir isotherms; constant MTC evaluated once per layer by @samaffleck in #24
- Validation: Ward 2022 on the paper's model forms, provenance audits for Ward 2022 and Streb 2021 by @samaffleck in #25
- Perez 2019 on the paper's model forms: two-term evacuation profile and capacity-factor LDF by @samaffleck in #26
- Residual: single-pass UpdateAll for the concentration-based competitive Langmuir isotherms; memoised Neufield table by @samaffleck in #27
- Profile factories: add the double-exponential kind by @samaffleck in #28
- Remove duplicated code in the engine and the test suite by @samaffleck in #30
- Solve the Ergun flow-boundary face explicitly at step switches by @samaffleck in #32
- Add Ideal Adsorbed Solution Theory as a per-adsorbent mixing rule by @samaffleck in #33
- Adsorbent library: one recommended entry per material (catalogue schema 3) by @samaffleck in #35
- Blake-Kozeny momentum model and constant D_m for the macropore LDF coefficients by @samaffleck in #34
- Close the component mass balance with axial dispersion (Danckwerts inlets) by @samaffleck in #37
- Fix the WENO and van Leer schemes; CSS requires a settled bed state by @samaffleck in #36
- Emscripten: native WebAssembly exceptions by default; CI on Emscripten 6.0.13 by @samaffleck in #38
- Molar-basis gas heat capacity; series and profiles at the right positions by @samaffleck in #39
- Start every cycle step with a fresh IDA history (WASM/native parity) by @samaffleck in #40
- Fix face interpolation weights in EquationLog::GetFaceVariable by @samaffleck in #42
- Mole-conserving connected streams; faster CI (one run per PR, cached Windows build, paper tests before merge) by @samaffleck in #41
- Union merge for CHANGELOG.md; CI skips builds for docs-only PRs by @samaffleck in #43
- WP15-E7: list every verification-suite file in the README Files table by @samaffleck in #44
- WP15-E1: LDF-limited front against the Anzelius/Thomas solution by @samaffleck in #46
- WP15-E2: Langmuir desorption simple wave against the method of characteristics by @samaffleck in #47
- WP15-E3: binary competitive Langmuir fronts against equilibrium theory by @samaffleck in #48
- WP15-E4 (part): blowdown composition path of a binary on linear isotherms by @samaffleck in #49
- WP15-E5: thermal front speed and steady wall-loss temperature profile by @samaffleck in #50
- WP15-E6: evacuation, two-bed pressure equalisation and buffer-tank fill by @samaffleck in #51
- WP17-E3: metamorphic relations (component order, layers, split steps, units, cycle phase) by @samaffleck in #52
- WP17-E1, WP17-E2: parameter limits (rates of approach) and degenerate mixtures by @samaffleck in #53
- WP17-E4: thermodynamic bound on the separation energy at CSS by @samaffleck in #54
- WP17-E5: extreme operating corners converge or fail cleanly by @samaffleck in #55
- Fix: connected streams replay a bottom pressure boundary's outflow with the wrong sign by @samaffleck in #56
- WP18-E2: cross-code regression against the PEESE model and toPSAil by @samaffleck in #57
- Save a sensitivity study's set-up with the deck by @samaffleck in #59
- Constant gas heat capacity and viscosity; heat of adsorption as −ΔU + RT by @samaffleck in #45
- Fix: the ColouredCheck Jacobian diagnostic learns the pattern without recolouring by @samaffleck in #60
- WP19-E3: valves can read the bed side of their law at the end cell by @samaffleck in #61
- WP19-E4: extended Langmuir-Freundlich isotherm (toPSAil form) by @samaffleck in #62
- WP16-E1..E4, E7: convergence verification of cycle results (grid, integrator tolerance, CSS) by @samaffleck in #58
- Fix: WENO / van Leer outlet-cell mole-fraction sum drifts during fast outflow by @samaffleck in #63
- WP16-E6: method-of-manufactured-solutions hook and order test by @samaffleck in #65
- Own ramp time for pressure ramps; user exponential factor by @samaffleck in #68
- Fix: breakthrough decks stall on fine grids (Ward2022_Q50ccm at 320 cells) by @samaffleck in #66
- Keep the KPIs of every cycle, not only the logged ones by @samaffleck in #69
- Fix: the CSS state test never passes for an inert bed by @samaffleck in #71
- Release automatically when a version change reaches main by @samaffleck in #70
- Release 0.4.0 by @github-actions[bot] in #72
New Contributors
- @github-actions[bot] made their first contribution in #72
Full Changelog: v0.1.0...v0.4.0