Skip to content

EOS table consistency

Thermodynamic identities of a regular (log10 P, log10 T) table, written as relative residuals that vanish for an exact equation of state: cp = T (ds/dT)_P, the Maxwell relation (ds/dP)_T = -alpha / rho, alpha = -(1/rho) (drho/dT)_P, (du/dT)_P = cp - P alpha / rho, nabla_ad = P alpha / (rho cp) and cp - cv = T alpha^2 / (rho kappa_T). The derivatives are central differences in ln P and ln T with a step of k grid cells; a stencil counts only when all its cells hold numbers of one phase, so a phase boundary or a missing cell never enters a derivative. The input is a table in the layout of zalmoxis.eos_export.load_paleos_all_properties.

check takes a table on a grid uniform in log10 P and log10 T (it rejects an axis whose log10 values lie more than 4e-9 off a linear fit; the coordinates of the PALEOS tables lie at most 2.1e-9 off, while the Chabrier 2021 tables, stored at fewer digits, lie about 1e-5 off and are not checked; a smooth drift of the step with a spread of 2e-6 lies 8e-7 off and would give false failures with the mean step. The floor adds the rounding of the stored coordinates in the algebraic factor of each identity, ln 10 times the departure of log10 P and log10 T from their fit, and in the mean step of the derivative axis, 2 eps over the span of the axis in ln units (5e-7 on a 9-point axis at a step of 0.001 dex, at most 2.4e-9 on the PALEOS axes), so an exact table whose stored axes lie up to 4e-9 off passes; the term decides where it is amplified, as by T alpha in the alpha identity (an exact table with T alpha = 200 and a T axis 2.1e-9 off its fit passes). The check assumes axes made by linspace or arange in log10 with the values computed at those points: if the true points depart from a uniform grid, the derivatives carry that error, and on a 1351 x 380 Murnaghan table at 9 digits with values computed at log10 P and log10 T moved by numpy.random.default_rng(seed).uniform(-1e-9, 1e-9), uniform axes stored, seeds 0 to 4 give 2,313 to 2,727 false cp, 3,620 to 4,641 maxwell, 10,835 to 15,004 dudt and 0 to 16 alpha failures) and tests each cell where 4 cells on each side along the axis of the derivative hold the differentiated column in one phase: a derivative identity fails where the Richardson residual R_x = (4 R_h - R_2h) / 3 exceeds the round-off floor of the stored values plus twice the h^4 term measured at that cell, (R_x(2h) - R_x(h)) / 15; nabla_ad is held to its floor alone. Each identity reads only its own columns (INPUTS), so a column it does not read never blocks it. floors gives that floor for a stored relative precision eps (5e-9 for 9 significant digits). signs flags cells with a non-positive density or heat capacity, cv above cp, or a density that falls with pressure, at the same floor; each condition tests the cells where its values are not NaN, and an infinite value breaks every condition but the density slope. A missing value (NaN) leaves its cells untested; a stored 0 or inf is a value. Where all values an identity reads are present but its residual or bound is not finite, the cell fails; the PALEOS H2O table stores cp = cv = alpha = 0 in 15,334 cells at 100 to 295 K, which fail in this way, and a column that is 0 or inf in every cell makes each identity whose normalization it zeroes or makes infinite fail in every present cell: with alpha = 0 everywhere, maxwell, alpha, nabla_ad and cp_cv fail, while dudt, normalized by T (cp - P alpha / rho), stays tested. An identity is skipped only when no cell holds all values it reads, on the full stencil of a derivative identity (4 cells on each side in one phase); a table where every identity is skipped raises. A tested cell whose bound is 1 or more cannot detect a missing derivative (a residual of -1 passes there); each identity counts such cells as blind, and the report tool prints them next to the present, tested and failing cells. The report tool gives the reason for a skipped identity (an input column without a value, or no full stencil), names an input column that is 0 or inf in every cell that holds a value, and names the columns without a value, if any, and what each identity needs when every identity is skipped.

Resolution at 9 stored digits, measured at one cell of an exact Murnaghan table at 150 points per decade (P = 46 MPa, T = 1,581 K): a single stored value off by 2e-8 (relative) or more fails at the cells one step from it, and by 3e-7 (entropy) or 1e-6 (density) or more also at the cells two steps from it. The exact ideal gas and Murnaghan tables give no failing cell at this precision. check raises a smaller eps to EPS_FLOOR = 5e-9, so every table, also one stored in full double precision, is checked at the 9-digit floor, and a single value off by about 1e-8 (relative) or less is out of reach on any table. cv enters only through cp - cv, whose floor at low pressure lies far above 1e-6, so a cv fault there is found only when it is large.

Detection of a 1e-6 fault depends on the grid: on the Aragog P-T layout (check_pt), on the 200 x 200 grid that PROTEUS writes (rows at 70, 176 and 281 GPa) a planted 1e-6 entropy was found at all four cells one and two steps away from about 2000 K up, at 1 to 3 of them at 730 K and at none at 443 K, where the linear temperature step is large against T; a planted 1e-6 density was found at all four from 730 K up and at 3 of 4 at 443 K; two such faults were missed on a 60 x 60 grid, where the measured h^4 term is larger. check_pt takes axes that are one-dimensional, finite, increasing (T positive, P not negative), with at least 2 points, and uniform or uniform in log within PT_GRID_FIT = 1e-6 of the step plus the stored precision; its floor adds the rounding of the stored P and T coordinates, taken as the departure of each coordinate from the fit of its axis, so an exact table with 9-digit axes passes and a planted 1e-6 entropy or density value is still found one and two steps away; heat capacity and thermal expansion enter only the algebraic factor: a planted 1e-6 value there is not found, and one of 1e-4 only at its own cell. On a geometric axis the measured h^4 term is larger, and a planted 1e-5 entropy value is still found. Each check_pt identity also gives fill, the number of fill cells (1e-15, written by eos_export for a cell without data) per file: a clipped block inside the valid region counts as missing, and the fill count shows its size. Tables that eos_export writes by linear interpolation of the PALEOS tables hold the identities only to the interpolation error: with the PROTEUS settings (2-phase MgSiO3, 200 x 200, P up to 350 GPa) 86 to 96 % of the tested cells fail, with residuals of about 1e-4. tools/validation/eos_table_consistency_report.py writes every failing cell of a PALEOS-format table to a CSV file.

consistency

Thermodynamic consistency residuals of a regular (log10 P, log10 T) EOS table.

Each identity is written as a relative residual that is 0 for an exact EOS. The derivatives are central differences in x = ln P and y = ln T with a step of k grid cells; a stencil counts only when all its cells hold numbers of one phase, so a phase boundary never enters a derivative.

check(table, eps)

Test each cell against the Richardson bound of each identity.

A derivative identity fails where |R_x(h)| > floor_x + FACTOR * est, with R_x(kh) = (4 R_k - R_2k) / 3, floor_x = (4 floor_1 + floor_2) / 3 and est = |R_x(2h) - R_x(h)| / 15, the measured h^4 term of R_x(h) at the cell. nabla_ad fails where |R| > floor. At 9 stored digits (eps 5e-9) the exact ideal gas and Murnaghan tables give no failing cell, and one planted value is found at the cells one and two steps from it.

The floor uses eps_eff = max(eps, EPS_FLOOR): every table is checked at the 9-digit floor at best, so a single value off by about 1e-8 (relative) or less is out of reach on any table. Each identity reads only its own columns (INPUTS). A derivative identity is tested only where 4 cells on each side along its axis hold its differentiated column in one phase; NaN marks a missing value, and a stored 0 or inf is a value. Where all values an identity reads are present but its residual or bound is not finite, the cell fails; an identity is skipped only when no cell holds all values it reads, on the full stencil of a derivative identity (4 cells on each side in one phase). On a coarse grid a cell can lie outside the range where the Richardson estimate holds (R_x(2h) / R_x(h) far from 16), and an exact table can fail there: 1 cp cell of the Murnaghan table at 10 points per decade over P 1e5 to 10^11.5 Pa and T 300 to 7.5e3 K, and 29 cp_cv cells of the 60 x 60 Murnaghan table over P 1e5 to 1e11 Pa and T 500 to 5000 K, where cp and cv are positive.

Parameters:

Name Type Description Default
table dict

As for residuals, on a grid uniform in log10 P and log10 T: each axis within GRID_FIT of a linear fit, as 9-digit coordinates are.

required
eps float

Relative precision of the stored columns (0.5 x 10^(1 - n) for n significant digits).

required

Returns:

Type Description
dict

Identity name -> dict with rx (the Richardson residual, defined also at untested cells), bound, present (all values the identity reads on its stencil), tested (both finite), blind (tested with a bound of 1 or more, where a missing derivative passes), fail (tested and above the bound, or present with a residual or bound that is not finite) arrays, and skipped, True when no cell is present.

Raises:

Type Description
ValueError

If eps is not a real number in [0, 1), an axis has fewer than 2 points or is not finite, increasing and uniform, a column is off the shape of the grid, or every identity is skipped.

Source code in src/zalmoxis/eos/consistency.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
def check(table, eps):
    """Test each cell against the Richardson bound of each identity.

    A derivative identity fails where |R_x(h)| > floor_x + FACTOR * est, with R_x(kh) =
    (4 R_k - R_2k) / 3, floor_x = (4 floor_1 + floor_2) / 3 and est = |R_x(2h) - R_x(h)| / 15,
    the measured h^4 term of R_x(h) at the cell. nabla_ad fails where |R| > floor. At 9 stored
    digits (eps 5e-9) the exact ideal gas and Murnaghan tables give no failing cell, and one
    planted value is found at the cells one and two steps from it.

    The floor uses eps_eff = max(eps, EPS_FLOOR): every table is checked at the 9-digit
    floor at best, so a single value off by about 1e-8 (relative) or less is out of reach on any
    table. Each identity reads only its own columns (``INPUTS``). A derivative identity is
    tested only where 4 cells on each side along its axis hold its differentiated column in one
    phase; NaN marks a missing value, and a stored 0 or inf is a value. Where all values an
    identity reads are present but its residual or bound is not finite, the cell fails; an
    identity is skipped only when no cell holds all values it reads, on the full stencil of a
    derivative identity (4 cells on each side in one phase). On a coarse grid a cell can lie
    outside the range where the Richardson estimate holds (R_x(2h) / R_x(h) far from 16), and
    an exact table can fail there: 1 cp cell of the Murnaghan table at 10 points per decade over
    P 1e5 to 10^11.5 Pa and T 300 to 7.5e3 K, and 29 cp_cv cells of the 60 x 60 Murnaghan table
    over P 1e5 to 1e11 Pa and T 500 to 5000 K, where cp and cv are positive.

    Parameters
    ----------
    table : dict
        As for ``residuals``, on a grid uniform in log10 P and log10 T: each axis within
        ``GRID_FIT`` of a linear fit, as 9-digit coordinates are.
    eps : float
        Relative precision of the stored columns (0.5 x 10^(1 - n) for n significant digits).

    Returns
    -------
    dict
        Identity name -> dict with ``rx`` (the Richardson residual, defined also at untested
        cells), ``bound``, ``present`` (all values the identity reads on its stencil),
        ``tested`` (both finite), ``blind`` (tested with a bound of 1 or more, where a missing
        derivative passes), ``fail`` (tested and above the bound, or present with a residual
        or bound that is not finite) arrays, and ``skipped``, True when no cell is present.

    Raises
    ------
    ValueError
        If eps is not a real number in [0, 1), an axis has fewer than 2 points or is not
        finite, increasing and uniform, a column is off the shape of the grid, or every
        identity is skipped.
    """
    eps = _eps(eps)
    r = {k: residuals(table, k) for k in (1, 2, 4)}
    const = {**table}
    for col, value in _PRESENT.items():
        const[col] = np.where(np.isnan(np.asarray(table[col], float)), np.nan, value)
    p = {k: residuals(const, k) for k in (1, 2, 4)}
    f1, f2 = floors(table, eps, 1), floors(table, eps, 2)
    with np.errstate(invalid='ignore'):
        return _nonempty(
            {
                n: _verdict(r[1][n], f1[n], np.isfinite(p[1][n]))
                if n == 'nabla_ad'
                else _richardson(
                    r[1][n], r[2][n], r[4][n], f1[n], f2[n], p[1][n], p[2][n], p[4][n]
                )
                for n in IDENTITIES
            }
        )

check_pt(tables, eps)

check for the Aragog P-T tables that eos_export writes (no u, cv or nabla_ad).

Parameters:

Name Type Description Default
tables dict

PT_QUANTITIES name -> dict with P (Pa) and T (K) grid vectors and values of shape (n_P, n_T); the fill value PT_FILL counts as a missing cell.

required
eps float

Relative precision of the stored values; raised to EPS_FLOOR at least.

required

Returns:

Type Description
dict

As for check, for the identities cp = T (ds/dT)_P, (ds/dP)_T = -alpha / rho and alpha = -(dln rho/dT)_P, each with fill: quantity -> number of PT_FILL cells.

Raises:

Type Description
ValueError

If eps is not a real number in [0, 1), an axis is not 1-D, has fewer than 2 points, is not finite and increasing, or departs from a uniform or geometric grid by more than PT_GRID_FIT of its step plus its stored precision, T is not positive or P is negative, the four tables are not on one grid or off its shape, or every identity is skipped.

Notes

The floor includes the rounding of the stored P and T coordinates: a central difference over x[i-k], x[i+k] gains |D| (e[i+k] + e[i-k]) / (x[i+k] - x[i-k]), with e the departure of each coordinate from the uniform or geometric fit of its axis plus 4 ulp.

Source code in src/zalmoxis/eos/consistency.py
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
def check_pt(tables, eps):
    """``check`` for the Aragog P-T tables that eos_export writes (no u, cv or nabla_ad).

    Parameters
    ----------
    tables : dict
        ``PT_QUANTITIES`` name -> dict with ``P`` (Pa) and ``T`` (K) grid vectors and ``values``
        of shape (n_P, n_T); the fill value ``PT_FILL`` counts as a missing cell.
    eps : float
        Relative precision of the stored values; raised to ``EPS_FLOOR`` at least.

    Returns
    -------
    dict
        As for ``check``, for the identities cp = T (ds/dT)_P, (ds/dP)_T = -alpha / rho and
        alpha = -(dln rho/dT)_P, each with ``fill``: quantity -> number of ``PT_FILL`` cells.

    Raises
    ------
    ValueError
        If eps is not a real number in [0, 1), an axis is not 1-D, has fewer than 2 points, is
        not finite and increasing, or departs from a uniform or geometric grid by more than
        ``PT_GRID_FIT`` of its step plus its stored precision, T is not positive or P is
        negative, the four tables are not on one grid or off its shape, or every identity is
        skipped.

    Notes
    -----
    The floor includes the rounding of the stored P and T coordinates: a central difference
    over x[i-k], x[i+k] gains |D| (e[i+k] + e[i-k]) / (x[i+k] - x[i-k]), with e the departure
    of each coordinate from the uniform or geometric fit of its axis plus 4 ulp.
    """
    eps = _eps(eps)
    parts = {k: _pt_parts(tables, k, eps) for k in (1, 2, 4)}
    const = {}
    for q, t in tables.items():
        v = np.asarray(t['values'], float)
        const[q] = {**t, 'values': np.where(np.isnan(v) | (v == PT_FILL), PT_FILL, 1.0)}
    present = {k: _pt_parts(const, k, eps) for k in (1, 2, 4)}
    fill = {
        q: int((np.asarray(tables[q]['values'], float) == PT_FILL).sum()) for q in PT_QUANTITIES
    }
    out = {}
    with np.errstate(divide='ignore', invalid='ignore'):
        for n in PT_IDENTITIES:
            p = [present[k][n][0] / present[k][n][2] for k in (1, 2, 4)]
            r = {k: parts[k][n][0] / parts[k][n][2] - 1 for k in (1, 2, 4)}
            fl = {
                k: (parts[k][n][1] / np.abs(parts[k][n][2]) + np.abs(r[k] + 1) * parts[k][n][3])
                * eps
                + np.abs(r[k] + 1) * parts[k][n][4]
                for k in (1, 2)
            }
            out[n] = {**_richardson(r[1], r[2], r[4], fl[1], fl[2], *p), 'fill': dict(fill)}
    return _nonempty(out)

floors(table, eps, k=1)

Round-off bound of each residual at step k for columns stored to relative eps.

For R + 1 = D / Q the bound is (E / |Q| + |D / Q| q) eps + |D / Q| c, with E the error of the central difference from its two stored ends, q the relative error of Q, and c the relative error from the stored P and T coordinates: in Q (e_P and e_T, ln 10 times the departure of log10 P and log10 T from their linear fit) and in the mean step of the derivative axis (2 eps / span, span = (n - 1) h in ln units). nabla_ad, with no derivative, gets |R + 1| (4 eps + e_P) from its four stored factors and P.

Source code in src/zalmoxis/eos/consistency.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def floors(table, eps, k=1):
    """Round-off bound of each residual at step ``k`` for columns stored to relative ``eps``.

    For R + 1 = D / Q the bound is (E / |Q| + |D / Q| q) eps + |D / Q| c, with E the error of
    the central difference from its two stored ends, q the relative error of Q, and c the
    relative error from the stored P and T coordinates: in Q (e_P and e_T, ln 10 times the
    departure of log10 P and log10 T from their linear fit) and in the mean step of the
    derivative axis (2 eps / span, span = (n - 1) h in ln units). nabla_ad, with no
    derivative, gets |R + 1| (4 eps + e_P) from its four stored factors and P.
    """
    hx, hy, P, _, rho, _, _, cp, _, alpha, nab = _grid(table)
    sP, sT = 2 * eps / (hx * (rho.shape[0] - 1)), 2 * eps / (hy * (rho.shape[1] - 1))
    eP = np.log(10.0) * _departure(np.asarray(table['unique_log_p'], float))[:, None]
    eT = np.log(10.0) * _departure(np.asarray(table['unique_log_t'], float))[None, :]
    with np.errstate(divide='ignore', invalid='ignore'):
        a = P * alpha / rho
        c = {
            'cp': sT,
            'maxwell': eP + sP,
            'alpha': eT + sT,
            'dudt': eT + sT + eP * np.abs(a / (cp - a)),
            'cp_cv': eP + eT + sP,
        }
        out = {
            n: (E / np.abs(Q) + np.abs(D / Q) * q) * eps + np.abs(D / Q) * c[n]
            for n, (D, E, Q, q) in _parts(table, k).items()
        }
        out['nabla_ad'] = np.abs(nab * rho * cp / (P * alpha)) * (4 * eps + eP)
    return {n: out[n] for n in IDENTITIES}

signs(table, eps)

Cells that break a sign condition beyond the stored precision eps.

Parameters:

Name Type Description Default
table dict

As for residuals.

required
eps float

Relative precision of the stored columns; raised to EPS_FLOOR at least.

required

Returns:

Type Description
dict

Condition name -> dict with tested and fail boolean arrays. rho, cp and cv not positive; cp_ge_cv: cv above cp by more than eps (|cp| + |cv|); kappa_T: rho falls with P at fixed T by more than the rounding of its two neighbours (central difference over one cell on each side, one phase). A condition tests the cells where its values are not NaN; an infinite value breaks the rho, cp, cv and cp_ge_cv conditions, and kappa_T does not test it.

Raises:

Type Description
ValueError

If eps is not a real number in [0, 1), or the axes or shapes fail as for check.

Source code in src/zalmoxis/eos/consistency.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def signs(table, eps):
    """Cells that break a sign condition beyond the stored precision ``eps``.

    Parameters
    ----------
    table : dict
        As for ``residuals``.
    eps : float
        Relative precision of the stored columns; raised to ``EPS_FLOOR`` at least.

    Returns
    -------
    dict
        Condition name -> dict with ``tested`` and ``fail`` boolean arrays. ``rho``, ``cp``
        and ``cv`` not positive; ``cp_ge_cv``: cv above cp by more than eps (|cp| + |cv|);
        ``kappa_T``: rho falls with P at fixed T by more than the rounding of its two
        neighbours (central difference over one cell on each side, one phase). A condition
        tests the cells where its values are not NaN; an infinite value breaks the rho, cp, cv
        and cp_ge_cv conditions, and kappa_T does not test it.

    Raises
    ------
    ValueError
        If eps is not a real number in [0, 1), or the axes or shapes fail as for ``check``.
    """
    eps = _eps(eps)
    _axes(table)
    rho, cp, cv = (np.asarray(table[c], float) for c in ('rho', 'cp', 'cv'))
    seg = segments(table, 0, ('rho',))
    rise = _stencil(rho, seg, 0, 1, 0.5, -1.0)  # rho[i+1] - rho[i-1]
    noise = _stencil(np.abs(rho), seg, 0, 1, 0.5, 1.0) * eps
    with np.errstate(invalid='ignore'):
        cond = {
            'rho': (rho, rho <= 0),
            'cp': (cp, cp <= 0),
            'cv': (cv, cv <= 0),
            'cp_ge_cv': (cp - cv, cv - cp > eps * (np.abs(cp) + np.abs(cv))),
            'kappa_T': (rise, rise < -noise),
        }
    return {
        n: {'tested': ~np.isnan(v), 'fail': ~np.isnan(v) & (f | np.isinf(v))}
        for n, (v, f) in cond.items()
    }

residuals(table, k=1)

Relative residuals of the identities at stencil step k cells.

Parameters:

Name Type Description Default
table dict

unique_log_p, unique_log_t (uniform, log10 Pa and K) and the columns rho, u, s, cp, cv, alpha, nabla_ad (SI) and phase on the (P, T) grid, as from zalmoxis.eos_export.load_paleos_all_properties.

required
k int

Stencil step in grid cells.

1

Returns:

Type Description
dict

Identity name -> residual array on the grid (NaN where it cannot be formed): cp: (ds/dlnT)_P / cp - 1; maxwell: (ds/dlnP)_T / (-P alpha / rho) - 1; alpha: -(dln rho/dlnT)_P / (T alpha) - 1; dudt: (du/dlnT)_P / T / (cp - P alpha / rho) - 1; nabla_ad: nabla_ad rho cp / (P alpha) - 1 (no derivative); cp_cv: (cp - cv) rho kappa_T / (T alpha^2) - 1 with kappa_T = (dln rho/dlnP)_T / P.

Source code in src/zalmoxis/eos/consistency.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def residuals(table, k=1):
    """Relative residuals of the identities at stencil step ``k`` cells.

    Parameters
    ----------
    table : dict
        ``unique_log_p``, ``unique_log_t`` (uniform, log10 Pa and K) and the columns rho, u, s,
        cp, cv, alpha, nabla_ad (SI) and phase on the (P, T) grid, as from
        ``zalmoxis.eos_export.load_paleos_all_properties``.
    k : int
        Stencil step in grid cells.

    Returns
    -------
    dict
        Identity name -> residual array on the grid (NaN where it cannot be formed):
        ``cp``: (ds/dlnT)_P / cp - 1; ``maxwell``: (ds/dlnP)_T / (-P alpha / rho) - 1;
        ``alpha``: -(dln rho/dlnT)_P / (T alpha) - 1; ``dudt``: (du/dlnT)_P / T / (cp - P alpha
        / rho) - 1; ``nabla_ad``: nabla_ad rho cp / (P alpha) - 1 (no derivative);
        ``cp_cv``: (cp - cv) rho kappa_T / (T alpha^2) - 1 with kappa_T = (dln rho/dlnP)_T / P.
    """
    _, _, P, _, rho, _, _, cp, _, alpha, nab = _grid(table)
    parts = _parts(table, k)
    with np.errstate(divide='ignore', invalid='ignore'):
        out = {n: D / Q - 1 for n, (D, _, Q, _) in parts.items()}
        out['nabla_ad'] = nab * rho * cp / (P * alpha) - 1
    return {n: out[n] for n in IDENTITIES}

derivative(f, seg, axis, k, h)

d f / d(axis) with step k h, NaN where the stencil leaves one segment.

Source code in src/zalmoxis/eos/consistency.py
63
64
65
def derivative(f, seg, axis, k, h):
    """d f / d(axis) with step ``k h``, NaN where the stencil leaves one segment."""
    return _stencil(f, seg, axis, k, h, -1.0)

segments(table, axis, cols=COLUMNS, valid=np.isfinite, code=None)

Label runs of cells of one phase along axis; cells where valid (a function of a column) is false for one of cols get -1. code is _phase_code(table) if given.

Source code in src/zalmoxis/eos/consistency.py
34
35
36
37
38
39
40
41
42
def segments(table, axis, cols=COLUMNS, valid=np.isfinite, code=None):
    """Label runs of cells of one phase along ``axis``; cells where ``valid`` (a function of a
    column) is false for one of ``cols`` get -1. ``code`` is ``_phase_code(table)`` if given."""
    ok = np.logical_and.reduce([valid(np.asarray(table[c], float)) for c in cols])
    code = np.where(ok, _phase_code(table) if code is None else code, -1)
    start = np.ones(code.shape, bool)
    inner = tuple(slice(1, None) if a == axis else slice(None) for a in range(code.ndim))
    start[inner] = np.diff(code, axis=axis) != 0
    return np.where(ok, np.cumsum(start, axis=axis), -1)