Skip to content

PALEOS-API live tabulation (producer)

Live producer side of the PALEOS-API:* registry family. Generates 10-column P-T .dat files bit-format-identical to the shipped EOS_PALEOS_* Zenodo tables, but sourced at runtime from the installed paleos Python package rather than from a flat-file checkout. This lets users opt into a different upstream PALEOS version (or a custom phase map) without re-publishing tables. The output is consumed unchanged by eos.interpolation.load_paleos_table (density + nabla_ad) and by eos_export.load_paleos_all_properties (SPIDER P-S export). Cache logic and on-disk layout live in paleos_api_cache; this module contains pure producers only.

The PALEOS-API path is tested with the PALEOS commit PALEOS_TESTED_SHA. At its first use in a process for each PALEOS SHA, in the cache resolvers and in the two generators, check_paleos_commit reads the commit of the installed PALEOS checkout: that commit runs, and a newer one that contains it runs with a warning. An older or unrelated commit, a clone that lacks the tested commit (a shallow clone is told to git fetch --unshallow), a paleos package that is not a tracked file of its checkout (a copy in a virtual environment, for example), a PALEOS installed without git, or package files that cannot be read stop with PaleosVersionError. So does package code that differs from its commit: an edit, a deleted module, older package files checked out at a newer commit, an untracked file Python imports (.py, .so, .pyd, or a .pyc outside __pycache__, ignored or not), or such a file added to the index but not committed. A link to a directory, a nested repository or a submodule under paleos/, committed or not, stops also with the opt-in below, because no key can describe the code in it; a plain untracked directory counts only through the files in it that Python imports. Setting ZALMOXIS_PALEOS_ALLOW_DIRTY=1 lets such code run with a warning, for a custom phase map; its key gets -dirty- and a hash of that code after the SHA, so each such state has its own cache. Tracked files are compared by content as git hashes them without filters, so a checkout with line-end conversion or a clean filter counts as changed, and a symlink by its target; a paleos package below the top of its repository (under src/, for example) is compared by its paths in the repository; a mode change, __pycache__ and files Python does not import do not count. git runs without the repository variables of the caller (GIT_DIR and the like). In one process the code in memory is the code of the first import, so a serial build stops when the key has moved since the first key; when paleos was imported before that first key, a warning says that a change on disk before it is not covered. The cache key also holds build_key: the RTpress install (version and commit), the md5 of the AQUA table for H2O, and the numpy, scipy and sympy versions. For an editable RTpress the key holds its HEAD only, so uncommitted edits to RTpress are not keyed; for RTpress installed from a directory without git it holds the version only. The scheduled workflow paleos-main.yml runs tools/check_paleos_main.py each day and fails when PALEOS main is at another commit or is not found, when GitHub refuses the request (401, or 403 with requests left and no Retry-After), or when the pin cannot be read or is no 40-character lowercase commit SHA; a used-up or secondary rate limit, a 5xx or a network fault only warns; a Zalmoxis change then moves PALEOS_TESTED_SHA once the PALEOS-API tests pass on it.

paleos_api

Live PALEOS tabulation — PALEOS-API:* producer side.

Generates 10-column P-T .dat files bit-format-identical to the shipped EOS_PALEOS_* Zenodo tables, but sourced at runtime from import paleos rather than from a flat-file checkout. Output is consumed unchanged by the existing readers: zalmoxis.eos.interpolation.load_paleos_table (density + nabla_ad path) and zalmoxis.eos_export.load_paleos_all_properties (SPIDER P-S export path).

Two entry points:

  • generate_paleos_api_unified_table — one file per material, stable-phase dispatch (matches EOS_PALEOS_{iron,MgSiO3_unified,H2O}).
  • generate_paleos_api_2phase_mgsio3_tables — two files (solid + liquid) with metastable extensions, matching EOS_PALEOS_MgSiO3/paleos_mgsio3_tables_pt_proteus_{solid,liquid}.dat. Aragog's P-S table build feeds these to eos_export.generate_spider_eos_tables as solid_eos_file and liquid_eos_file so that mushy-zone mixing uses phase-specific endpoints rather than interpolating across the melting curve.

Cache / registry wiring is handled by paleos_api_cache.py and the PALEOS-API:* registry entries. This module contains pure producers only; no cache logic lives here.

Design notes

The 2-phase solid-side picker reaches into PALEOS internals (_phase_eos_map, the phase-boundary functions, _P_HPCEN_BRG). These are not a committed public API. The PALEOS SHA is recorded in the output header so any upstream rename is caught by a cache-key miss. Once a public get_mgsio3_solid_phase + generate_twophase_pt_tables helper is contributed upstream, the internals access here collapses to one public call.

PALEOS_TESTED_SHA = '654aca9ec6116d2ad319d984a3f68e11ddef2fb4' module-attribute

ALLOW_DIRTY_ENV = 'ZALMOXIS_PALEOS_ALLOW_DIRTY' module-attribute

GridSpec(p_lo, p_hi, n_p, t_lo, t_hi, n_t) dataclass

Log-uniform (P, T) grid specification.

Pressure axis is log-uniform in [p_lo, p_hi] Pa with n_p points. Temperature axis is log-uniform in [t_lo, t_hi] K with n_t points, matching the Zenodo shipped tables (which are log-uniform in both axes at 150 pts/decade).

Attributes:

Name Type Description
p_lo, p_hi float

Pressure bounds [Pa]. p_lo must be > 0 (log spacing).

n_p int

Number of pressure nodes.

t_lo, t_hi float

Temperature bounds [K].

n_t int

Number of temperature nodes.

axes()

Return (P_axis [Pa], T_axis [K]) as 1D numpy arrays.

Source code in src/zalmoxis/eos/paleos_api.py
105
106
107
108
109
def axes(self):
    """Return (P_axis [Pa], T_axis [K]) as 1D numpy arrays."""
    P = np.logspace(np.log10(self.p_lo), np.log10(self.p_hi), self.n_p)
    T = np.logspace(np.log10(self.t_lo), np.log10(self.t_hi), self.n_t)
    return P, T

hash_short()

SHA-1 short digest for cache keying.

Source code in src/zalmoxis/eos/paleos_api.py
100
101
102
103
def hash_short(self) -> str:
    """SHA-1 short digest for cache keying."""
    payload = _json.dumps(asdict(self), sort_keys=True).encode('utf-8')
    return _hashlib.sha1(payload).hexdigest()[:10]

PaleosVersionError

Bases: Exception

The installed PALEOS code cannot be shown to contain PALEOS_TESTED_SHA: it is older or unrelated, not installed, not a tracked package of a git checkout, or git cannot read it.

Not a RuntimeError or ValueError, so a caller that turns those into a failed structure solve does not hide it.

make_grid_at_resolution(p_lo, p_hi, t_lo, t_hi, pts_per_decade=DEFAULT_PTS_PER_DECADE)

Build a GridSpec with a uniform log-log resolution.

Parameters:

Name Type Description Default
p_lo float

Pressure bounds [Pa], both > 0.

required
p_hi float

Pressure bounds [Pa], both > 0.

required
t_lo float

Temperature bounds [K], both > 0.

required
t_hi float

Temperature bounds [K], both > 0.

required
pts_per_decade int

Nodes per decade on each axis. Default DEFAULT_PTS_PER_DECADE = 600 (4x the shipped Zenodo resolution).

DEFAULT_PTS_PER_DECADE
Source code in src/zalmoxis/eos/paleos_api.py
134
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
def make_grid_at_resolution(
    p_lo: float,
    p_hi: float,
    t_lo: float,
    t_hi: float,
    pts_per_decade: int = DEFAULT_PTS_PER_DECADE,
) -> GridSpec:
    """Build a ``GridSpec`` with a uniform log-log resolution.

    Parameters
    ----------
    p_lo, p_hi : float
        Pressure bounds [Pa], both > 0.
    t_lo, t_hi : float
        Temperature bounds [K], both > 0.
    pts_per_decade : int
        Nodes per decade on each axis. Default
        ``DEFAULT_PTS_PER_DECADE`` = 600 (4x the shipped Zenodo resolution).
    """
    return GridSpec(
        p_lo=p_lo,
        p_hi=p_hi,
        n_p=_n_points_per_decade(p_lo, p_hi, pts_per_decade),
        t_lo=t_lo,
        t_hi=t_hi,
        n_t=_n_points_per_decade(t_lo, t_hi, pts_per_decade),
    )

make_default_grid_iron()

Default Fe grid: 1e5 to 1e14 Pa, 300 to 20000 K at 600 pts/decade.

Source code in src/zalmoxis/eos/paleos_api.py
167
168
169
def make_default_grid_iron() -> GridSpec:
    """Default Fe grid: 1e5 to 1e14 Pa, 300 to 20000 K at 600 pts/decade."""
    return make_grid_at_resolution(1e5, 1e14, 300.0, 2.0e4)

make_default_grid_mgsio3()

Default MgSiO3 grid: 1e5 to 1e14 Pa, 300 to 11500 K at 600 pts/decade.

Source code in src/zalmoxis/eos/paleos_api.py
172
173
174
def make_default_grid_mgsio3() -> GridSpec:
    """Default MgSiO3 grid: 1e5 to 1e14 Pa, 300 to 11500 K at 600 pts/decade."""
    return make_grid_at_resolution(1e5, 1e14, 300.0, 1.15e4)

make_default_grid_h2o()

Default H2O grid: 0.1 Pa to 1e14 Pa, 150 to 1e5 K at 600 pts/decade.

Wider P range than Fe/MgSiO3 to match AQUA table coverage (extends to 1 micro-bar). H2O is table-backed in PALEOS (Haldemann20) so per-point cost is low.

Source code in src/zalmoxis/eos/paleos_api.py
177
178
179
180
181
182
183
184
def make_default_grid_h2o() -> GridSpec:
    """Default H2O grid: 0.1 Pa to 1e14 Pa, 150 to 1e5 K at 600 pts/decade.

    Wider P range than Fe/MgSiO3 to match AQUA table coverage (extends to
    1 micro-bar). H2O is table-backed in PALEOS (``Haldemann20``) so per-point
    cost is low.
    """
    return make_grid_at_resolution(1e-1, 1e14, 150.0, 1.0e5)

check_paleos_commit(repo_dir=None, tested=None)

Return 'tested', 'newer' or 'dirty' for the PALEOS checkout in repo_dir.

A checkout at tested (default PALEOS_TESTED_SHA) runs; one that descends from it runs with a warning. Package code that differs from its commit stops, unless ZALMOXIS_PALEOS_ALLOW_DIRTY=1 lets it run with a warning.

Returns:

Type Description
str

'tested', 'newer', or 'dirty' for an allowed checkout with changes.

Raises:

Type Description
PaleosVersionError

If PALEOS is not installed, its package is not a tracked file of a git checkout, the checkout does not contain tested (an older or unrelated commit, or a clone that lacks it), its commit or files cannot be read, or its package code differs from the commit without the opt-in.

Source code in src/zalmoxis/eos/paleos_api.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
def check_paleos_commit(repo_dir: Path | None = None, tested: str | None = None) -> str:
    """Return ``'tested'``, ``'newer'`` or ``'dirty'`` for the PALEOS checkout in ``repo_dir``.

    A checkout at ``tested`` (default ``PALEOS_TESTED_SHA``) runs; one that descends
    from it runs with a warning. Package code that differs from its commit stops, unless
    ``ZALMOXIS_PALEOS_ALLOW_DIRTY=1`` lets it run with a warning.

    Returns
    -------
    str
        ``'tested'``, ``'newer'``, or ``'dirty'`` for an allowed checkout with changes.

    Raises
    ------
    PaleosVersionError
        If PALEOS is not installed, its package is not a tracked file of a git checkout,
        the checkout does not contain ``tested`` (an older or unrelated commit, or a
        clone that lacks it), its commit or files cannot be read, or its package code
        differs from the commit without the opt-in.
    """
    tested = PALEOS_TESTED_SHA if tested is None else tested
    repo_dir = _paleos_repo_dir() if repo_dir is None else Path(repo_dir)
    if repo_dir is None:
        raise PaleosVersionError('PALEOS is not installed; the PALEOS-API path needs it')
    head = _git(repo_dir, 'rev-parse', 'HEAD')
    if head.returncode != 0 or not head.stdout.strip():
        raise PaleosVersionError(
            f'cannot tell the PALEOS commit of {repo_dir} ({head.stderr.strip()[:200]}): the '
            f'PALEOS-API path needs PALEOS installed from a git checkout at {tested} or later'
        )
    if _git(repo_dir, 'ls-files', '--error-unmatch', 'paleos/__init__.py').returncode != 0:
        raise PaleosVersionError(
            f'the paleos package in {repo_dir} is not tracked by the git checkout that holds '
            'it, so its commit cannot be told; install PALEOS with pip install -e from a clone'
        )
    head_sha = head.stdout.strip()
    if head_sha != tested:
        if _git(repo_dir, 'merge-base', '--is-ancestor', tested, head_sha).returncode != 0:
            shallow = _git(repo_dir, 'rev-parse', '--is-shallow-repository').stdout.strip()
            fix = 'fetch --unshallow' if shallow == 'true' else 'pull'
            raise PaleosVersionError(
                f'PALEOS in {repo_dir} is at {head_sha}, which does not contain the tested '
                f'commit {tested}: update it, for example git -C {repo_dir} {fix}'
            )
        logger.warning(
            'PALEOS %s is newer than the commit the PALEOS-API path is tested with (%s)',
            head_sha,
            tested,
        )
    if _changes_hash(repo_dir, head_sha) is not None:
        if _os.environ.get(ALLOW_DIRTY_ENV) != '1':
            raise PaleosVersionError(
                f'the paleos package files in {repo_dir} differ from its commit {head_sha}: '
                f'commit or remove the changes, or set {ALLOW_DIRTY_ENV}=1 to run them '
                'with their own cache; in a clean checkout, a line-end conversion such as '
                'core.autocrlf is a likely cause'
            )
        logger.warning('PALEOS in %s has uncommitted changes to its package files', repo_dir)
        return 'dirty'
    return 'tested' if head_sha == tested else 'newer'

paleos_installed_sha()

Return the git SHA of the installed PALEOS checkout, or a fallback string.

A checkout whose package code differs from its commit gets -dirty-<8 hex> after the SHA, a hash of that code, so each set of changes has its own cache. Without git the result is version-<paleos.__version__>, and without PALEOS paleos-not-installed. The first result is kept for :func:_same_code_as_import; when paleos was imported before it, a warning says that changes before this call are not covered.

Source code in src/zalmoxis/eos/paleos_api.py
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
def paleos_installed_sha() -> str:
    """Return the git SHA of the installed PALEOS checkout, or a fallback string.

    A checkout whose package code differs from its commit gets ``-dirty-<8 hex>`` after
    the SHA, a hash of that code, so each set of changes has its own cache. Without git
    the result is ``version-<paleos.__version__>``, and without PALEOS
    ``paleos-not-installed``. The first result is kept for :func:`_same_code_as_import`;
    when paleos was imported before it, a warning says that changes before this call are
    not covered.
    """
    fresh = 'paleos' not in _sys.modules
    repo_dir = _paleos_repo_dir()
    if repo_dir is None:
        return 'paleos-not-installed'
    key = None
    head = _git(repo_dir, 'rev-parse', 'HEAD')
    if head.returncode == 0 and head.stdout.strip():
        sha = head.stdout.strip()
        try:
            changes = _changes_hash(repo_dir, sha)
            key = sha if changes is None else f'{sha}-dirty-{changes}'
        except PaleosVersionError:
            pass
    if key is None:
        import paleos as _paleos

        key = f'version-{getattr(_paleos, "__version__", "unknown")}'
    if 'key' not in _KEY_AT_IMPORT:
        if not fresh:
            logger.warning(
                'paleos was imported before its code key was taken; a change on disk before '
                'this call is not covered'
            )
        _KEY_AT_IMPORT['key'] = key
    return key

build_inputs(material, h2o_table_path=None)

Return what a PALEOS-API table depends on besides the PALEOS code and the grid: the RTpress install (the Wolf18 liquid), the md5 of the AQUA table of H2O, and the numpy, scipy and sympy versions.

Source code in src/zalmoxis/eos/paleos_api.py
397
398
399
400
401
402
403
404
405
def build_inputs(material: str, h2o_table_path: str | None = None) -> dict:
    """Return what a PALEOS-API table depends on besides the PALEOS code and the grid: the
    RTpress install (the Wolf18 liquid), the md5 of the AQUA table of H2O, and the numpy,
    scipy and sympy versions."""
    aqua = None
    if material == 'h2o' and h2o_table_path is not None:
        aqua = _hashlib.md5(Path(h2o_table_path).read_bytes()).hexdigest()
    ids = {name: _dist_id(name) for name in ('RTpress', 'numpy', 'scipy', 'sympy')}
    return {**ids, 'aqua_md5': aqua}

build_key(material, h2o_table_path=None)

Return an 8-character hash of :func:build_inputs, part of the cache key.

Source code in src/zalmoxis/eos/paleos_api.py
408
409
410
411
def build_key(material: str, h2o_table_path: str | None = None) -> str:
    """Return an 8-character hash of :func:`build_inputs`, part of the cache key."""
    text = _json.dumps(build_inputs(material, h2o_table_path), sort_keys=True)
    return _hashlib.sha256(text.encode()).hexdigest()[:8]

generate_paleos_api_unified_table(material, out_path, grid, *, h2o_table_path=None, log_every=50, n_workers=1)

Generate a unified PALEOS .dat for one material.

Stable-phase dispatch: for each (P, T) the PALEOS top-level class (IronEoS / MgSiO3EoS / WaterEoS) chooses the thermodynamically stable phase and evaluates that phase's EoS. Phase label is PALEOS's intrinsic string ('solid-hpcen', 'liquid', 'hcp-Fe', etc).

Parameters:

Name Type Description Default
material ('iron', 'mgsio3', 'h2o')

Material selector.

'iron'
out_path path - like

Destination .dat path. Parent directories are created.

required
grid GridSpec

(P, T) grid. p_lo must be > 0.

required
h2o_table_path str or None

AQUA table path. Passed to WaterEoS when material == 'h2o'.

None
log_every int

Emit a progress line every log_every pressure rows.

50
n_workers int

Parallelism. 1 = serial (default, backward-compatible). -1 = os.cpu_count(). Any other positive int = that many workers. Workers parallelize over P-rows; each worker instantiates its own PALEOS EoS at init (Wolf18 sympy compile cost paid once per worker, not per task).

1

Returns:

Type Description
dict

{'n_valid', 'n_skipped', 'sha'}.

Source code in src/zalmoxis/eos/paleos_api.py
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
def generate_paleos_api_unified_table(
    material: str,
    out_path: str | Path,
    grid: GridSpec,
    *,
    h2o_table_path: str | None = None,
    log_every: int = 50,
    n_workers: int = 1,
) -> dict:
    """Generate a unified PALEOS ``.dat`` for one material.

    Stable-phase dispatch: for each (P, T) the PALEOS top-level class
    (``IronEoS`` / ``MgSiO3EoS`` / ``WaterEoS``) chooses the thermodynamically
    stable phase and evaluates that phase's EoS. Phase label is PALEOS's
    intrinsic string (``'solid-hpcen'``, ``'liquid'``, ``'hcp-Fe'``, etc).

    Parameters
    ----------
    material : {'iron', 'mgsio3', 'h2o'}
        Material selector.
    out_path : path-like
        Destination ``.dat`` path. Parent directories are created.
    grid : GridSpec
        (P, T) grid. ``p_lo`` must be > 0.
    h2o_table_path : str or None
        AQUA table path. Passed to ``WaterEoS`` when ``material == 'h2o'``.
    log_every : int
        Emit a progress line every ``log_every`` pressure rows.
    n_workers : int
        Parallelism. ``1`` = serial (default, backward-compatible). ``-1`` =
        ``os.cpu_count()``. Any other positive int = that many workers.
        Workers parallelize over P-rows; each worker instantiates its own
        PALEOS EoS at init (Wolf18 sympy compile cost paid once per worker,
        not per task).

    Returns
    -------
    dict
        ``{'n_valid', 'n_skipped', 'sha'}``.
    """
    if grid.p_lo <= 0:
        raise ValueError('GridSpec.p_lo must be > 0 (log spacing)')
    if material not in _HUMAN_MATERIAL:
        raise ValueError(f"material must be one of 'iron', 'mgsio3', 'h2o'; got {material!r}")

    human_material = _HUMAN_MATERIAL[material]
    n_workers = _resolve_n_workers(n_workers)
    sha = paleos_installed_sha()
    _check_paleos_once(sha)
    build = build_key(material, h2o_table_path)
    P_axis, T_axis = grid.axes()
    T_axis_list = T_axis.tolist()  # avoid repeated numpy scalar conversions in workers

    out_path = Path(out_path)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = out_path.with_suffix(out_path.suffix + '.tmp')

    tasks = [(i, float(P), T_axis_list) for i, P in enumerate(P_axis)]

    rows_by_i: dict[int, str] = {}
    n_valid = 0
    n_skipped = 0
    n_done = 0

    if n_workers == 1:
        # Serial path: no Pool, no worker init — tabulate in-process.
        _same_code_as_import(sha)
        _worker_init(material, h2o_table_path=h2o_table_path)
        iterator = (_worker_unified_row(t) for t in tasks)
    else:
        ctx = _mp.get_context('spawn')
        pool = ctx.Pool(
            processes=n_workers,
            initializer=_worker_init,
            initargs=(material, h2o_table_path),
        )
        # chunksize kept small so progress logs stay meaningful at high cost/cell.
        chunksize = max(1, len(tasks) // (n_workers * 16))
        iterator = pool.imap_unordered(_worker_unified_row, tasks, chunksize=chunksize)

    try:
        for i_P, rows_text, nv, ns in iterator:
            rows_by_i[i_P] = rows_text
            n_valid += nv
            n_skipped += ns
            n_done += 1
            if log_every and (n_done % log_every == 0):
                logger.info(
                    'paleos_api %s unified: %d / %d P rows done (valid=%d skipped=%d)',
                    human_material,
                    n_done,
                    grid.n_p,
                    n_valid,
                    n_skipped,
                )
    finally:
        if n_workers > 1:
            pool.close()
            pool.join()

    with open(tmp_path, 'w') as f:
        _write_header(
            f,
            human_material,
            'unified (stable-phase)',
            grid,
            sha,
            build=build,
            n_valid=n_valid,
            n_skipped=n_skipped,
        )
        for i in range(grid.n_p):
            f.write(rows_by_i[i])
    _os.replace(tmp_path, out_path)

    logger.info(
        'paleos_api %s unified: wrote %s (valid=%d skipped=%d sha=%s n_workers=%d)',
        human_material,
        out_path,
        n_valid,
        n_skipped,
        sha[:10],
        n_workers,
    )
    return {'n_valid': n_valid, 'n_skipped': n_skipped, 'sha': sha}

generate_paleos_api_2phase_mgsio3_tables(out_solid, out_liquid, grid, *, log_every=50, n_workers=1)

Generate solid and liquid MgSiO3 P-T tables with metastable extensions.

Replaces the shipped paleos_mgsio3_tables_pt_proteus_solid.dat and paleos_mgsio3_tables_pt_proteus_liquid.dat — bit-format-compatible, consumed unchanged by eos_export.generate_spider_eos_tables via solid_eos_file / liquid_eos_file.

Parameters:

Name Type Description Default
out_solid path - like

Destinations for the two files. Parent directories are created.

required
out_liquid path - like

Destinations for the two files. Parent directories are created.

required
grid GridSpec

(P, T) grid; p_lo must be > 0.

required
log_every int

Progress log cadence in pressure rows.

50
n_workers int

Parallelism. 1 = serial (default, backward-compatible). -1 = os.cpu_count(). Workers parallelize over P-rows; each worker instantiates MgSiO3EoS + Wolf18 once (Wolf18's sympy compile cost is paid once per worker, not per task).

1

Returns:

Type Description
dict

Summary with per-side n_valid / n_skipped and the installed PALEOS sha.

Notes

The liquid-side file evaluates paleos.mgsio3_eos.Wolf18 on every grid node, including below the solidus (metastable liquid). The solid-side file dispatches through MgSiO3EoS()._phase_eos_map using _get_mgsio3_solid_phase (melting-curve test removed) so every node gets a solid-polymorph EoS evaluated metastably above the liquidus if needed.

Source code in src/zalmoxis/eos/paleos_api.py
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
def generate_paleos_api_2phase_mgsio3_tables(
    out_solid: str | Path,
    out_liquid: str | Path,
    grid: GridSpec,
    *,
    log_every: int = 50,
    n_workers: int = 1,
) -> dict:
    """Generate solid and liquid MgSiO3 P-T tables with metastable extensions.

    Replaces the shipped ``paleos_mgsio3_tables_pt_proteus_solid.dat`` and
    ``paleos_mgsio3_tables_pt_proteus_liquid.dat`` — bit-format-compatible,
    consumed unchanged by ``eos_export.generate_spider_eos_tables`` via
    ``solid_eos_file`` / ``liquid_eos_file``.

    Parameters
    ----------
    out_solid, out_liquid : path-like
        Destinations for the two files. Parent directories are created.
    grid : GridSpec
        (P, T) grid; ``p_lo`` must be > 0.
    log_every : int
        Progress log cadence in pressure rows.
    n_workers : int
        Parallelism. ``1`` = serial (default, backward-compatible). ``-1`` =
        ``os.cpu_count()``. Workers parallelize over P-rows; each worker
        instantiates ``MgSiO3EoS`` + ``Wolf18`` once (Wolf18's sympy compile
        cost is paid once per worker, not per task).

    Returns
    -------
    dict
        Summary with per-side ``n_valid`` / ``n_skipped`` and the installed
        PALEOS ``sha``.

    Notes
    -----
    The liquid-side file evaluates ``paleos.mgsio3_eos.Wolf18`` on every
    grid node, including below the solidus (metastable liquid). The
    solid-side file dispatches through ``MgSiO3EoS()._phase_eos_map`` using
    ``_get_mgsio3_solid_phase`` (melting-curve test removed) so every node
    gets a solid-polymorph EoS evaluated metastably above the liquidus if
    needed.
    """
    if grid.p_lo <= 0:
        raise ValueError('GridSpec.p_lo must be > 0 (log spacing)')

    n_workers = _resolve_n_workers(n_workers)
    sha = paleos_installed_sha()
    _check_paleos_once(sha)
    build = build_key('mgsio3')
    P_axis, T_axis = grid.axes()
    T_axis_list = T_axis.tolist()

    out_solid = Path(out_solid)
    out_liquid = Path(out_liquid)
    out_solid.parent.mkdir(parents=True, exist_ok=True)
    out_liquid.parent.mkdir(parents=True, exist_ok=True)
    tmp_solid = out_solid.with_suffix(out_solid.suffix + '.tmp')
    tmp_liquid = out_liquid.with_suffix(out_liquid.suffix + '.tmp')

    tasks = [(i, float(P), T_axis_list) for i, P in enumerate(P_axis)]

    solid_by_i: dict[int, str] = {}
    liquid_by_i: dict[int, str] = {}
    n_valid_s = n_skip_s = 0
    n_valid_l = n_skip_l = 0
    n_done = 0

    if n_workers == 1:
        _same_code_as_import(sha)
        _worker_init('mgsio3')
        iterator = (_worker_2phase_row(t) for t in tasks)
    else:
        ctx = _mp.get_context('spawn')
        pool = ctx.Pool(
            processes=n_workers,
            initializer=_worker_init,
            initargs=('mgsio3',),
        )
        chunksize = max(1, len(tasks) // (n_workers * 16))
        iterator = pool.imap_unordered(_worker_2phase_row, tasks, chunksize=chunksize)

    try:
        for i_P, s_text, l_text, nv_s, ns_s, nv_l, ns_l in iterator:
            solid_by_i[i_P] = s_text
            liquid_by_i[i_P] = l_text
            n_valid_s += nv_s
            n_skip_s += ns_s
            n_valid_l += nv_l
            n_skip_l += ns_l
            n_done += 1
            if log_every and (n_done % log_every == 0):
                logger.info(
                    'paleos_api MgSiO3 2-phase: %d / %d P rows done '
                    '(solid valid=%d skipped=%d | liquid valid=%d skipped=%d)',
                    n_done,
                    grid.n_p,
                    n_valid_s,
                    n_skip_s,
                    n_valid_l,
                    n_skip_l,
                )
    finally:
        if n_workers > 1:
            pool.close()
            pool.join()

    with open(tmp_solid, 'w') as f:
        _write_header(
            f,
            'MgSiO3',
            'solid (metastable extension above liquidus)',
            grid,
            sha,
            build=build,
            n_valid=n_valid_s,
            n_skipped=n_skip_s,
            extra_lines=[
                'Solid polymorph picked by _get_mgsio3_solid_phase; melting-curve test suppressed.',
                'Phase column is the polymorph label even if (P, T) is above the liquidus.',
            ],
        )
        for i in range(grid.n_p):
            f.write(solid_by_i[i])
    _os.replace(tmp_solid, out_solid)

    with open(tmp_liquid, 'w') as f:
        _write_header(
            f,
            'MgSiO3',
            'liquid (Wolf18, metastable extension below solidus)',
            grid,
            sha,
            build=build,
            n_valid=n_valid_l,
            n_skipped=n_skip_l,
            extra_lines=[
                'Wolf18 RTpress evaluated everywhere; phase column is always "liquid".',
            ],
        )
        for i in range(grid.n_p):
            f.write(liquid_by_i[i])
    _os.replace(tmp_liquid, out_liquid)

    logger.info(
        'paleos_api MgSiO3 2-phase: wrote %s (valid=%d/%d) and %s (valid=%d/%d) '
        'sha=%s n_workers=%d',
        out_solid,
        n_valid_s,
        grid.n_p * grid.n_t,
        out_liquid,
        n_valid_l,
        grid.n_p * grid.n_t,
        sha[:10],
        n_workers,
    )
    return {
        'solid': {'n_valid': n_valid_s, 'n_skipped': n_skip_s, 'path': str(out_solid)},
        'liquid': {'n_valid': n_valid_l, 'n_skipped': n_skip_l, 'path': str(out_liquid)},
        'sha': sha,
    }