Convergence history¶
Convergence history recorded by the time-marching loop.
This module defines ConvergenceHistory, a 1D time series of flow
monitors that ember.solver.Solver.run() fills as it marches. It holds one
record per logged step, carrying the residuals, and the mass flow, stagnation
enthalpy and entropy at each station. The residuals are whole-field block
means; only the mass flow, enthalpy, and entropy are per-station quantities.
Throughout, record means one entry in the history, and row is reserved for a blade row of the machine.
A history is returned to you by ember.solver.Solver.run(); it is not something
you build directly. ConvergenceHistory.from_grid(),
ConvergenceHistory.record_convergence() exist for the solver to call, and
are of no use outside it.
Reading a history¶
Run a simulation, then read the monitors as plain numpy arrays:
hist = ember.solver.Solver(n_step=1000).run(grid)
hist.i_step # solver step index of each record
hist.residual[:, 4] # energy residual, drhoe
hist.err_mdot # mass flow conservation error
hist.zeta # entropy rise, outlet minus inlet station
Every property returns a single-precision float32 array of length i_log +
1, one entry per record.
The residuals, enthalpy and entropy are stored non-dimensionally on the fluid reference scales as described in Reference scales.
Monitors¶
Whole-field, one value per record:
ConvergenceHistory.residual– block-mean|residual_nd|per conserved variableConvergenceHistory.err_mdot– mass flow conservation error, inlet to outletConvergenceHistory.err_mdot_row– the same, resolved per blade rowConvergenceHistory.psi– stagnation enthalpy rise across the machineConvergenceHistory.zeta– entropy rise across the machineConvergenceHistory.tpnps– wall-clock time per node per step
Through-flow stations. Each blade row contributes an upstream and a downstream
face, ordered inlet to outlet as [row0_up, row0_dn, row1_up, row1_dn], so
station 0 is the inlet and station -1 the outlet. All three are
non-dimensional, hence the _nd suffix:
Throttle state, driven by ember.patch.OutletPatch.set_throttle() and
reading zero on a run whose outlets all hold a plain prescribed pressure.
Unlike the stations above, they are dimensional:
ConvergenceHistory.throttle,ConvergenceHistory.mdot_target,ConvergenceHistory.mdot_throttleConvergenceHistory.dP_P,ConvergenceHistory.dP_I,ConvergenceHistory.dP_D
Bookkeeping:
ConvergenceHistory.i_step– solver step index of each recordConvergenceHistory.time– elapsed wall-clock timeConvergenceHistory.i_log– index of the last written recordConvergenceHistory.diverged– True if the march blew upConvergenceHistory.now– view of the record currently being writtenConvergenceHistory.n_node– node count of the grid behind the history
Storage¶
ConvergenceHistory.from_grid() allocates n_log records up front and
fills them with NaN, one per log step; the solver passes
n_log = ceil(n_step / n_step_log) for n_step marched steps logged every
n_step_log. At each log step,
ember.solver.Solver.run() calls ConvergenceHistory.record_convergence()
to fill one record. A march that runs to completion fills every record; one that
diverges breaks from the loop early before logging an invalid value, sets
ConvergenceHistory.diverged, and trims the history to drop the
preallocated NaN for steps that were never reached. This means that we can
always assume that history arrays are finite and valid for plotting or
reduction, without having to mask out NaN values.
Reading from a file¶
A history is read back from disk with ConvergenceHistory.read_cnv(), which
unpickles a history written by ConvergenceHistory.write_cnv().
- class convergence_history.ConvergenceHistory(shape=(), n_row=1)[source]¶
Allocate the data array, sized for
n_rowblade rows.Station ordering and reference scales are documented in the module docstring. The station columns (
mdot_st<i>,ho_st<i>,s_st<i>) are sized to2 * n_roweach, one inlet/exit pair per row, rather than a fixed width – so the column-key list is built here, per instance, before delegating to the base initialiser, which reads it to size the backing array.- Parameters:
- classmethod from_grid(n_log, grid)[source]¶
Initialize an empty convergence history sized for a solver run.
- Parameters:
- Returns:
Configured instance ready to record data
- Return type:
- classmethod read_cnv(filename)[source]¶
Read convergence history from CNV binary format file.
Automatically detects gzip-compressed files.
- Parameters:
filename (str) – Input CNV file to read
- Return type:
- check_convergence(decay=0.0, slope=0.0, cfl=1.0)[source]¶
True when every enabled convergence criterion is met.
Three independent signals reduce the history to a single verdict, and the result is their logical AND. Each criterion is disabled by passing its no-op threshold, so a bare
check_convergence()checks divergence alone.Divergence reads
divergedonly; it never touches the residual. A diverged march is never converged.Decay and slope read the energy residual
drhoe(column 4 ofresidual), the strictest conserved-variable residual and the one that lags in a stalled march, over thei_log + 1written records.
- Parameters:
decay (float, optional) – Required fall of the residual from its peak over the whole march, in decades: converged needs
log10(r.max() / r[-1]) >= decay. The default0disables the check (0 decades of fall is always met).slope (float, optional) – Maximum allowed magnitude of the residual slope, in decades of residual per unit pseudo-time, where pseudo-time is
i_step * cfl. Fitted over the last 20% of records so it reflects the recent tail rather than the startup transient. Converged needsabs(d log10(r) / d(i_step * cfl)) <= slope. The default0disables the check.cfl (float, optional) – CFL number used to march, scaling the pseudo-time step so the slope is comparable across runs with different step sizes. Only affects the
slopecriterion.
- Return type:
- find_settling_record(tol=0.01)[source]¶
Record index at which the entropy rise
zetahas settled.Complements
check_convergence(): where that reduces the residual history to a converged/not verdict, this locates when the solution output stopped moving. It returns the record index at whichzetahas come within a fractiontolof its total change and stays there for the rest of the march.The asymptote is estimated as the mean of
zetaover the final fifth of records (the same last-20% windowcheck_convergence()fits its slope over), and the band istolof the total swingabs(zeta[0] - target)about it. The settling record is the one after the last record still outside that band – keying on the last exit rather than the first entry makes it robust to any overshoot that dips through the band and back out. Because the asymptote is the tail mean, the result is only physically meaningful once the march has actually levelled out (e.g. a runcheck_convergence()accepts).The return is a record index into the history arrays, not a solver step number, so both the step it settled at and the wall-clock time to get there are one indexing away:
idx = hist.find_settling_record() step = int(hist.i_step[idx]) # solver step wall = float(hist.time[idx] - hist.time[0]) # ms to settle
(
hist.time[0]is the one-iteration startup offset, not zero, so subtract it for elapsed march time.)
- record_convergence(i_step, conv, time=None)[source]¶
Append one fully populated record, holding solver step
i_step.Advances
i_logonto the next allocated record and writes every column of it: the step index, the time, and the monitors carried byconv. A record is never left half-written, so the only NaN a reader can meet is an untouched record pasti_log.- Parameters:
i_step (int) – Index of the solver step being recorded.
conv (ember.grid.ConvergenceStep) – One step’s monitors, from
ember.grid.Grid.get_convergence(). Themdot,hoandsstation vectors are unpacked into one scalar column per station; the history must have been constructed with a matchingn_rowto have enough station columns allocated.time (float, optional) – Elapsed time for this record, in seconds. Defaults to the wall-clock time since the history was created (the live-solver behaviour); a reader replaying a log passes the log’s own time instead.
- to_json(directory='.')[source]¶
Write convergence history to three JSON files in directory.
Writes err_mdot.json, work.json, and convergence_loss.json, each containing a list of {“x”: i_step, “y”: value} objects.
- Parameters:
directory (str or path-like, optional) – Output directory (default current directory).
- trim()[source]¶
Copy of the records actually written, dropping the unfilled ones.
from_grid()allocates enough records for the requested step count and leaves them NaN untilrecord_convergence()fills them, so a march that broke early on divergence leaves a NaN tail pasti_log. The result holdsi_log + 1records, the only length at whichi_logstays consistent with the number of records, and it can be plotted or reduced without masking the tail out first.The copy is independent of the original, which is also what makes the result safe to keep once the full allocation is dropped. It has no spare records, so
record_convergence()cannot be called on it: trim once the march is over.- Returns:
A new history containing only the logged steps.
- Return type:
- property diverged¶
True if the run that produced this history blew up, scalar.
Set by
ember.solver.Solver.run()whenember.grid.Grid.check_nan()raisesember.grid.DivergenceError, in which case the step loop broke early and onlyi_log + 1records were written.
- property dP_D¶
Derivative term of the throttle correction [Pa], record array.
The three terms
dP_P,dP_IanddP_Dsum to the total correction \(\Delta p_\mathrm{throttle}\) the outlet adds to its prescribed static pressure. This one is always zero: the throttle is a PI controller, and the column survives so the on-disk layout reads in both directions. Seeember.patch.OutletPatch.set_throttle().
- property dP_I¶
Integral term of the throttle correction [Pa], record array.
- property dP_P¶
Proportional term of the throttle correction [Pa], record array.
- property err_mdot¶
Mass flow conservation error \((\dot m_\mathrm{out} - \dot m_\mathrm{in}) / \bar{\dot m}\) [-], record array.
Taken between the first and last station, so it spans the whole machine. Zero for a perfectly converged march; the sign says whether the outlet passes more or less than the inlet.
- property err_mdot_row¶
Mass flow conservation error per blade row [-], shape
(n_log, n_row).As
err_mdot, but taken across each blade row separately:err[i, r] = (mdot_dn_r - mdot_up_r) / mdot_avg_rfor recordiand rowr.Returns an empty NaN array if the
n_rowmetadata is absent, as it is for histories written before that key existed.
- property ho_nd¶
Stagnation enthalpy \(h_0/u_\mathrm{ref}\) [-] at each station.
Record array of shape
(n_log, 2*n_row); station0is the inlet and-1the outlet. Carries an offset dependent on the arbitrary datum where \(u = s = 0\) at \((p_\mathrm{dtm}, T_\mathrm{dtm})\); only changes are physically meaningful, which is whypsiis a difference between stations. See Datum state.
- property i_log¶
Index of the last written record;
-1before any is written, scalar.A trimmed history has
i_log + 1records, soi_logis also its last valid index.
- property i_step¶
Solver step index \(i_\mathrm{step}\) [-], record array.
The step the march had reached when each record was written, so entries advance by
n_step_log, not by one.
- property mdot_nd¶
Mass flow \(\dot m\) [-] at each station, non-dimensional.
Record array of shape
(n_log, 2*n_row); station0is the inlet and-1the outlet. Scaled by the fluid mass-flux scale, and summed over the whole annulus. Not to be confused withmdot_targetandmdot_throttle, which are dimensional [kg/s] and count one passage, so the two differ by the blade count as well as by the scaling.
- property mdot_target¶
Throttle mass flow setpoint \(\dot m_\mathrm{target}\) [kg/s], record array.
Zero when no outlet is running a throttle.
- property mdot_throttle¶
Mass flow measured at the throttled outlet \(\dot m\) [kg/s], record array.
- property n_node¶
Node count of the grid that produced this history [-], scalar.
- property now¶
View of the record at
i_log, a single-record history.During a march this is the record being written. Once
ember.solver.Solver.run()has trimmed the history it is the last one.
- property psi¶
Stagnation enthalpy rise \(\psi\) [-] across the machine, record array.
Outlet station minus inlet station. Both terms are already non-dimensional (scaled by
u_ref), so this is the inlet-to-outlet stagnation enthalpy change on the fluid reference scale – no separate kinetic-energy normalisation.
- property residual¶
Residual of each conserved variable [-], shape
(n_log, 5).Ordered
(drho, drhoVx, drhoVr, drhorVt, drhoe). Each entry is a block mean of \(|\mathtt{residual\_nd}|\) for one conserved variable (seeember.grid.Grid.get_convergence()), so the values are non-negative by construction.In a real march they are also strictly positive, and callers may plot them on a log axis without masking. A mean of non-negative floats is zero only if every cell residual is exactly zero, since the sum is never smaller than its largest term and so cannot underflow. Float32 round-off in the flux balance and boundary conditions keeps a converged residual at a floor many orders of magnitude above the smallest normal float, rather than descending to zero: a uniform inviscid duct marched to a standstill still reports
~2.6e-6.The exception is a history rebuilt from an external solver log (e.g. by the ember-cfd-ts parser), which may recover only the density residual and leave the other four NaN.
- property s_nd¶
Specific entropy \(s/R_\mathrm{ref}\) [-] at each station.
Record array of shape
(n_log, 2*n_row); station0is the inlet and-1the outlet. Carries the same arbitrary datum offset asho_nd, so only changes between stations are meaningful. See Datum state.
- property throttle¶
Throttle state
(mdot_target, mdot_throttle, dP_throttle), shape(n_log, 3).The first two are mass flows [kg/s], through one passage rather than the whole annulus; the third is the total pressure correction \(\Delta p_\mathrm{throttle}\) [Pa], the sum of
dP_P,dP_IanddP_D.
- property time¶
Elapsed wall-clock time since the march began [ms], record array.
- property tpnps¶
Wall-clock time per node per step [\(\mu\mathrm{s}\)], scalar.
Measured over the interval between the last two records, so it is NaN until a second record exists.
- property zeta¶
Entropy rise \(\zeta\) [-] across the machine, record array.
Outlet station minus inlet station. Both terms are already non-dimensional (scaled by
Rgas_ref), so this is the inlet-to-outlet entropy generation on the fluid reference scale. It remains positive for an irreversible process (Gouy-Stodola), but is no longer normalised by a reference kinetic energy.