processforge 0.3.19__tar.gz → 0.3.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {processforge-0.3.19/src/processforge.egg-info → processforge-0.3.21}/PKG-INFO +1 -1
- {processforge-0.3.19 → processforge-0.3.21}/pyproject.toml +1 -1
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/__init__.py +3 -1
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/__init__.py +5 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/apply.py +4 -2
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/run.py +23 -2
- processforge-0.3.21/src/processforge/cli/runs.py +96 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/result.py +28 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/state.py +35 -15
- {processforge-0.3.19 → processforge-0.3.21/src/processforge.egg-info}/PKG-INFO +1 -1
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge.egg-info/SOURCES.txt +2 -0
- processforge-0.3.21/tests/test_runs_command.py +122 -0
- {processforge-0.3.19 → processforge-0.3.21}/LICENSE +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/MANIFEST.in +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/README.md +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/setup.cfg +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/__main__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/_schema.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/common.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/compose.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/coupling.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/lock.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/output_collector.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/provenance.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/quantity.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/simulate.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/solver.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/thermo.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/types.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/base.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge.egg-info/requires.txt +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_cli.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_container_client.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_coupling.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_init.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_providers.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_state.py +0 -0
- {processforge-0.3.19 → processforge-0.3.21}/tests/test_types.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "processforge"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.21"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -18,6 +18,7 @@ from .utils.validate_flowsheet import validate_flowsheet
|
|
|
18
18
|
from .result import (
|
|
19
19
|
plot_results,
|
|
20
20
|
plot_timeseries,
|
|
21
|
+
relink_latest_results,
|
|
21
22
|
save_results_zarr,
|
|
22
23
|
)
|
|
23
24
|
from .units.pump import Pump
|
|
@@ -29,7 +30,7 @@ from .units.flash import Flash
|
|
|
29
30
|
from .units.heater import Heater
|
|
30
31
|
from .eo import EOFlowsheet, EOSolver
|
|
31
32
|
|
|
32
|
-
__version__ = "0.3.
|
|
33
|
+
__version__ = "0.3.21"
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"Flowsheet",
|
|
@@ -41,6 +42,7 @@ __all__ = [
|
|
|
41
42
|
"plot_results",
|
|
42
43
|
"plot_timeseries",
|
|
43
44
|
"save_results_zarr",
|
|
45
|
+
"relink_latest_results",
|
|
44
46
|
"build_run_info",
|
|
45
47
|
"Pump",
|
|
46
48
|
"Valve",
|
|
@@ -11,6 +11,7 @@ def register_commands(app: typer.Typer) -> None:
|
|
|
11
11
|
from .validate import validate
|
|
12
12
|
from .run import run
|
|
13
13
|
from .apply import apply
|
|
14
|
+
from .runs import runs
|
|
14
15
|
from .plan import plan
|
|
15
16
|
from .diagram import diagram
|
|
16
17
|
from .export_fmu import export_fmu
|
|
@@ -29,6 +30,10 @@ def register_commands(app: typer.Typer) -> None:
|
|
|
29
30
|
"apply",
|
|
30
31
|
help="Apply flowsheet using state-based warm start and homotopy fallback",
|
|
31
32
|
)(apply)
|
|
33
|
+
app.command(
|
|
34
|
+
"runs",
|
|
35
|
+
help="List runs for a flowsheet, or show one run's full manifest (pf runs <flowsheet> [<run_id>])",
|
|
36
|
+
)(runs)
|
|
32
37
|
app.command(
|
|
33
38
|
"plan",
|
|
34
39
|
help="Validate a flowsheet, run DOF analysis, structural diff, container health check, and generate a Mermaid diagram",
|
|
@@ -31,13 +31,15 @@ def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id)
|
|
|
31
31
|
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
32
32
|
# inside the archive, mirroring `pf run`.
|
|
33
33
|
try:
|
|
34
|
-
from ..result import save_results_zarr
|
|
34
|
+
from ..result import relink_latest_results, save_results_zarr
|
|
35
35
|
|
|
36
|
+
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
36
37
|
save_results_zarr(
|
|
37
38
|
results,
|
|
38
|
-
os.path.join(
|
|
39
|
+
os.path.join(run_results_dir, "results.zarr"),
|
|
39
40
|
run_info,
|
|
40
41
|
)
|
|
42
|
+
relink_latest_results(archive.path, run_id)
|
|
41
43
|
except Exception as e:
|
|
42
44
|
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
43
45
|
|
|
@@ -102,6 +102,7 @@ def run(
|
|
|
102
102
|
raise SystemExit(1)
|
|
103
103
|
|
|
104
104
|
run_info = build_run_info(config, x0=x0, var_names=var_names)
|
|
105
|
+
snap_x, snap_vn, snap_backend, snap_ok = x0, var_names, "dynamic", True
|
|
105
106
|
else:
|
|
106
107
|
# Pass None so EOFlowsheet resolves backend from config (with scipy default).
|
|
107
108
|
fs = EOFlowsheet(config, backend=None)
|
|
@@ -117,6 +118,9 @@ def run(
|
|
|
117
118
|
logger.warning("Steady-state simulation did NOT converge. Results may be unreliable.")
|
|
118
119
|
|
|
119
120
|
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
121
|
+
snap_x, snap_vn = fs.x_converged, fs.var_names
|
|
122
|
+
snap_backend = getattr(fs, "backend", "scipy")
|
|
123
|
+
snap_ok = bool(getattr(fs, "converged", False))
|
|
120
124
|
|
|
121
125
|
# Propagate run context to any containerized providers so their S3 uploads
|
|
122
126
|
# are keyed by the same run_id/flowsheet_hash as this archive run.
|
|
@@ -134,18 +138,35 @@ def run(
|
|
|
134
138
|
}
|
|
135
139
|
archive.save_run(manifest, stream_results=stream_results)
|
|
136
140
|
|
|
141
|
+
# Persist a StateManager snapshot so `pf plan` (and `pf apply`) can diff
|
|
142
|
+
# against this run as a baseline. Without it, `pf plan` always reports
|
|
143
|
+
# "No prior state found" and never surfaces flowsheet edits.
|
|
144
|
+
if (is_dynamic or snap_ok) and snap_x is not None:
|
|
145
|
+
try:
|
|
146
|
+
from .common import build_run_metadata, save_snapshot
|
|
147
|
+
|
|
148
|
+
meta = build_run_metadata(config, 1e-6, 50, snap_backend)
|
|
149
|
+
save_snapshot(
|
|
150
|
+
archive, config, snap_x, snap_vn,
|
|
151
|
+
metadata=meta, parent_snapshot_id=None, label="run state",
|
|
152
|
+
)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.warning(f"Failed to save state snapshot: {type(e).__name__}: {e}")
|
|
155
|
+
|
|
137
156
|
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
138
157
|
# inside the archive. Works for every provider (OpenMC, CoolProp, FESTIM,
|
|
139
158
|
# Cantera) and for both local and remote docker runs — remote artifacts
|
|
140
159
|
# reference their S3 remote_uris.
|
|
141
160
|
try:
|
|
142
|
-
from ..result import save_results_zarr
|
|
161
|
+
from ..result import relink_latest_results, save_results_zarr
|
|
143
162
|
|
|
163
|
+
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
144
164
|
save_results_zarr(
|
|
145
165
|
results,
|
|
146
|
-
os.path.join(
|
|
166
|
+
os.path.join(run_results_dir, "results.zarr"),
|
|
147
167
|
run_info,
|
|
148
168
|
)
|
|
169
|
+
relink_latest_results(archive.path, run_id)
|
|
149
170
|
except Exception as e:
|
|
150
171
|
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
151
172
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""``pf runs`` — list runs for a flowsheet, or show one run's full manifest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from .common import flowsheet_basename, output_root
|
|
12
|
+
from ..persistence.archive import ProcessStateArchive
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _summary_for(manifest) -> str:
|
|
16
|
+
"""One-line summary of a run's primary unit fields (e.g. keff)."""
|
|
17
|
+
parts = []
|
|
18
|
+
for unit_name, out in manifest.units.items():
|
|
19
|
+
for f in out.fields:
|
|
20
|
+
val = f.quantity.value
|
|
21
|
+
unit = f.quantity.unit or ""
|
|
22
|
+
parts.append(f"{unit_name}.{f.name}={val}{(' ' + unit) if unit else ''}")
|
|
23
|
+
return " ".join(parts) if parts else "-"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _zarr_status(archive_path: str, run_id: str) -> str:
|
|
27
|
+
zarr_path = os.path.join(archive_path, "results", run_id, "results.zarr")
|
|
28
|
+
return "✓" if os.path.exists(zarr_path) else "✗"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def runs(
|
|
32
|
+
flowsheet: str = typer.Argument(..., help="Path to the flowsheet JSON"),
|
|
33
|
+
run_id: str = typer.Argument(
|
|
34
|
+
None,
|
|
35
|
+
help="Show this run's full manifest instead of listing all runs",
|
|
36
|
+
),
|
|
37
|
+
):
|
|
38
|
+
"""List runs stored in a flowsheet's ``.pfarchive`` (or show one run)."""
|
|
39
|
+
base = flowsheet_basename(flowsheet)
|
|
40
|
+
archive_path = os.path.join(output_root(), f"{base}.pfarchive")
|
|
41
|
+
|
|
42
|
+
if not os.path.isdir(archive_path):
|
|
43
|
+
logger.error(
|
|
44
|
+
f"No archive found for '{flowsheet}' at {archive_path}. "
|
|
45
|
+
"Run 'pf run' first."
|
|
46
|
+
)
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
|
|
49
|
+
archive = ProcessStateArchive(archive_path)
|
|
50
|
+
latest = archive.latest_run()
|
|
51
|
+
latest_id = latest.run_id if latest is not None else None
|
|
52
|
+
|
|
53
|
+
if run_id is not None:
|
|
54
|
+
if run_id not in archive.list_runs():
|
|
55
|
+
logger.error(f"Run '{run_id}' not found in {archive_path}/runs.")
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
manifest = archive.load_run(run_id)
|
|
58
|
+
typer.echo(json.dumps(manifest.model_dump(), indent=2, default=str))
|
|
59
|
+
typer.echo("\nArtifacts on disk:")
|
|
60
|
+
for unit_name, out in manifest.units.items():
|
|
61
|
+
for art in out.artifacts:
|
|
62
|
+
present = os.path.exists(art.local_path) if art.local_path else False
|
|
63
|
+
remote = bool(art.remote_uris)
|
|
64
|
+
status = (
|
|
65
|
+
"local✓" if present else ("remote-only" if remote else "missing")
|
|
66
|
+
)
|
|
67
|
+
loc = art.local_path or "no local path"
|
|
68
|
+
typer.echo(f" [{unit_name}] {art.name}: {status} ({loc})")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
run_ids = archive.list_runs()
|
|
72
|
+
if not run_ids:
|
|
73
|
+
typer.echo(f"No runs found for '{flowsheet}'.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
header = (
|
|
77
|
+
f"{'RUN ID':<28} {'TIMESTAMP':<22} {'MODE':<8} "
|
|
78
|
+
f"{'ZARR':<5} {'LATEST':<6} SUMMARY"
|
|
79
|
+
)
|
|
80
|
+
typer.echo(header)
|
|
81
|
+
typer.echo("-" * len(header))
|
|
82
|
+
for rid in run_ids:
|
|
83
|
+
manifest = archive.load_run(rid)
|
|
84
|
+
if manifest is None:
|
|
85
|
+
continue
|
|
86
|
+
marker = "*" if rid == latest_id else " "
|
|
87
|
+
typer.echo(
|
|
88
|
+
f"{rid:<28} {manifest.timestamp:<22} {manifest.mode:<8} "
|
|
89
|
+
f"{_zarr_status(archive_path, rid):<5} {marker:<6} "
|
|
90
|
+
f"{_summary_for(manifest)}"
|
|
91
|
+
)
|
|
92
|
+
typer.echo("")
|
|
93
|
+
typer.echo(
|
|
94
|
+
"Zarr: ✓ present on disk, ✗ deleted/missing. '*' marks the latest run. "
|
|
95
|
+
"'pf runs <flowsheet> <run_id>' prints the full manifest."
|
|
96
|
+
)
|
|
@@ -272,6 +272,34 @@ def save_results_zarr(results, fname="results.zarr", run_info: RunInfo | dict |
|
|
|
272
272
|
return store_path
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def relink_latest_results(archive_path: str, run_id: str) -> None:
|
|
276
|
+
"""Point ``<archive>/results.zarr`` at the latest per-run zarr.
|
|
277
|
+
|
|
278
|
+
Each run now writes its zarr to ``<archive>/results/<run_id>/results.zarr``
|
|
279
|
+
(see `save_results_zarr`). To keep backward compatibility with tooling that
|
|
280
|
+
reads a single ``results.zarr`` at the archive root, we make that path a
|
|
281
|
+
symlink to the newest run. Falls back to a copy on platforms/filesystems
|
|
282
|
+
without symlink support (or when the existing target is a real directory).
|
|
283
|
+
"""
|
|
284
|
+
latest = os.path.join(archive_path, "results", run_id, "results.zarr")
|
|
285
|
+
if not os.path.exists(latest):
|
|
286
|
+
return
|
|
287
|
+
link_path = os.path.join(archive_path, "results.zarr")
|
|
288
|
+
|
|
289
|
+
# Remove any existing target (real dir or stale/broken symlink) without
|
|
290
|
+
# following the link.
|
|
291
|
+
if os.path.islink(link_path):
|
|
292
|
+
os.unlink(link_path)
|
|
293
|
+
elif os.path.exists(link_path):
|
|
294
|
+
shutil.rmtree(link_path)
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
os.symlink(latest, link_path)
|
|
298
|
+
except (OSError, NotImplementedError):
|
|
299
|
+
# Non-POSIX / restricted filesystem: keep a physical copy instead.
|
|
300
|
+
shutil.copytree(latest, link_path)
|
|
301
|
+
|
|
302
|
+
|
|
275
303
|
def save_results_zarr_s3(results, s3_uri: str, run_info: RunInfo | dict | None = None):
|
|
276
304
|
"""Write simulation results directly to an S3-backed Zarr store.
|
|
277
305
|
|
|
@@ -302,27 +302,47 @@ class StateManager:
|
|
|
302
302
|
if ov != nv:
|
|
303
303
|
changes.append(f"{key}: {ov!r} → {nv!r}")
|
|
304
304
|
|
|
305
|
-
# Parameter changes
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
changes.append(f"{k}: {ov} → {nv}")
|
|
312
|
-
|
|
313
|
-
# Nested "parameters" dict
|
|
314
|
-
old_params = old_cfg.get("parameters", {})
|
|
315
|
-
new_params = new_cfg.get("parameters", {})
|
|
316
|
-
for pk in set(old_params) | set(new_params):
|
|
317
|
-
ov, nv = old_params.get(pk), new_params.get(pk)
|
|
318
|
-
if ov != nv:
|
|
319
|
-
changes.append(f"parameters.{pk}: {ov} → {nv}")
|
|
305
|
+
# Parameter changes — recurse into nested dicts so leaf values
|
|
306
|
+
# (e.g. solver_config.batches) are reported individually rather
|
|
307
|
+
# than dumping the whole dict.
|
|
308
|
+
changes.extend(
|
|
309
|
+
StateManager._diff_params(old_cfg, new_cfg)
|
|
310
|
+
)
|
|
320
311
|
|
|
321
312
|
if changes:
|
|
322
313
|
modified[name] = {"type": new_cfg.get("type", "?"), "changes": changes}
|
|
323
314
|
|
|
324
315
|
return {"added": added, "removed": removed, "modified": modified}
|
|
325
316
|
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
# Structural-diff helpers
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _diff_params(
|
|
322
|
+
old_cfg: dict, new_cfg: dict, prefix: str = "", skip: set | None = None
|
|
323
|
+
) -> list[str]:
|
|
324
|
+
"""Recursively collect changed scalar/leaf params between two dicts.
|
|
325
|
+
|
|
326
|
+
Nested dicts are descended (``solver_config.batches: 25 → 999``) so the
|
|
327
|
+
structural diff reports leaf-level changes instead of whole-dict blobs.
|
|
328
|
+
"""
|
|
329
|
+
if skip is None:
|
|
330
|
+
skip = {"type", "in", "out", "out_vap", "out_liq", "provider"}
|
|
331
|
+
all_keys = set(old_cfg) | set(new_cfg)
|
|
332
|
+
if not prefix:
|
|
333
|
+
all_keys -= skip
|
|
334
|
+
changes: list[str] = []
|
|
335
|
+
for k in all_keys:
|
|
336
|
+
ov, nv = old_cfg.get(k), new_cfg.get(k)
|
|
337
|
+
label = f"{prefix}{k}"
|
|
338
|
+
if isinstance(ov, dict) and isinstance(nv, dict):
|
|
339
|
+
changes.extend(
|
|
340
|
+
StateManager._diff_params(ov, nv, prefix=f"{label}.", skip=set())
|
|
341
|
+
)
|
|
342
|
+
elif ov != nv:
|
|
343
|
+
changes.append(f"{label}: {ov} → {nv}")
|
|
344
|
+
return changes
|
|
345
|
+
|
|
326
346
|
# ------------------------------------------------------------------
|
|
327
347
|
# State → stream dict conversion (for dynamic t=0 initialisation)
|
|
328
348
|
# ------------------------------------------------------------------
|
|
@@ -47,6 +47,7 @@ src/processforge/cli/export_modelica.py
|
|
|
47
47
|
src/processforge/cli/init.py
|
|
48
48
|
src/processforge/cli/plan.py
|
|
49
49
|
src/processforge/cli/run.py
|
|
50
|
+
src/processforge/cli/runs.py
|
|
50
51
|
src/processforge/cli/validate.py
|
|
51
52
|
src/processforge/eo/__init__.py
|
|
52
53
|
src/processforge/eo/flowsheet.py
|
|
@@ -132,6 +133,7 @@ tests/test_openmc_provider.py
|
|
|
132
133
|
tests/test_provider_errors.py
|
|
133
134
|
tests/test_providers.py
|
|
134
135
|
tests/test_result_zarr.py
|
|
136
|
+
tests/test_runs_command.py
|
|
135
137
|
tests/test_s3_upload.py
|
|
136
138
|
tests/test_state.py
|
|
137
139
|
tests/test_types.py
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for per-run Zarr outputs and the ``pf runs`` listing command."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import zarr
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from processforge.cli.runs import runs
|
|
11
|
+
from processforge.result import relink_latest_results, save_results_zarr
|
|
12
|
+
from processforge.types import (
|
|
13
|
+
EngineOutput,
|
|
14
|
+
OutputArtifact,
|
|
15
|
+
OutputField,
|
|
16
|
+
Quantity,
|
|
17
|
+
RunManifest,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _eo(run_id_suffix: str, keff: float) -> EngineOutput:
|
|
22
|
+
return EngineOutput(
|
|
23
|
+
engine="openmc",
|
|
24
|
+
sim_type="eigenvalue_reactor",
|
|
25
|
+
status="completed",
|
|
26
|
+
fields=[
|
|
27
|
+
OutputField(
|
|
28
|
+
name="k_eff",
|
|
29
|
+
quantity=Quantity(value=keff, unit=""),
|
|
30
|
+
kind="scalar",
|
|
31
|
+
source="keff",
|
|
32
|
+
)
|
|
33
|
+
],
|
|
34
|
+
artifacts=[
|
|
35
|
+
OutputArtifact(
|
|
36
|
+
name="statepoint",
|
|
37
|
+
kind="h5",
|
|
38
|
+
local_path=f"/data/openmc/msre_run/{run_id_suffix}/statepoint.h5",
|
|
39
|
+
remote_uris=[],
|
|
40
|
+
source="local",
|
|
41
|
+
)
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_relink_latest_results_symlinks_to_latest(tmp_path):
|
|
47
|
+
d = str(tmp_path)
|
|
48
|
+
rid_a = "20260101T000000Z_aaaaaa"
|
|
49
|
+
rid_b = "20260102T000000Z_bbbbbb"
|
|
50
|
+
|
|
51
|
+
save_results_zarr({"openmc_solver": _eo("a", 1.04)},
|
|
52
|
+
os.path.join(d, "results", rid_a, "results.zarr"), None)
|
|
53
|
+
relink_latest_results(d, rid_a)
|
|
54
|
+
link = os.path.join(d, "results.zarr")
|
|
55
|
+
assert os.path.islink(link)
|
|
56
|
+
assert os.path.realpath(link) == os.path.realpath(
|
|
57
|
+
os.path.join(d, "results", rid_a, "results.zarr"))
|
|
58
|
+
|
|
59
|
+
# A second run re-points the symlink without clobbering run A's zarr.
|
|
60
|
+
save_results_zarr({"openmc_solver": _eo("b", 1.06)},
|
|
61
|
+
os.path.join(d, "results", rid_b, "results.zarr"), None)
|
|
62
|
+
relink_latest_results(d, rid_b)
|
|
63
|
+
assert os.path.realpath(link) == os.path.realpath(
|
|
64
|
+
os.path.join(d, "results", rid_b, "results.zarr"))
|
|
65
|
+
assert os.path.isdir(os.path.join(d, "results", rid_a, "results.zarr"))
|
|
66
|
+
|
|
67
|
+
root = zarr.open_group(link, mode="r")
|
|
68
|
+
assert abs(root["openmc_solver"]["k_eff"][0] - 1.06) < 1e-9
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_pf_runs_lists_runs_with_disk_status(tmp_path, monkeypatch):
|
|
72
|
+
monkeypatch.setenv("PROCESSFORGE_OUTPUT_DIR", str(tmp_path))
|
|
73
|
+
base = "test_flowsheet"
|
|
74
|
+
archive_path = tmp_path / f"{base}.pfarchive"
|
|
75
|
+
runs_dir = archive_path / "runs"
|
|
76
|
+
results_dir = archive_path / "results"
|
|
77
|
+
runs_dir.mkdir(parents=True)
|
|
78
|
+
|
|
79
|
+
rid_a = "20260101T000000Z_aaaaaa"
|
|
80
|
+
rid_b = "20260102T000000Z_bbbbbb"
|
|
81
|
+
|
|
82
|
+
# Run A has its zarr on disk; run B does not (deleted).
|
|
83
|
+
save_results_zarr({"openmc_solver": _eo("a", 1.04)},
|
|
84
|
+
str(results_dir / rid_a / "results.zarr"), None)
|
|
85
|
+
for rid, keff in ((rid_a, 1.04), (rid_b, 1.06)):
|
|
86
|
+
manifest = RunManifest(
|
|
87
|
+
run_id=rid, timestamp=rid.split("_")[0], mode="steady",
|
|
88
|
+
units={"openmc_solver": _eo(rid[-6:], keff)},
|
|
89
|
+
)
|
|
90
|
+
(runs_dir / f"{rid}.json").write_text(manifest.model_dump_json())
|
|
91
|
+
(archive_path / "latest_run").write_text(rid_b)
|
|
92
|
+
|
|
93
|
+
app = typer.Typer()
|
|
94
|
+
app.command()(runs)
|
|
95
|
+
result = CliRunner().invoke(app, [f"/tmp/{base}.json"])
|
|
96
|
+
assert result.exit_code == 0, result.output
|
|
97
|
+
assert rid_a in result.output
|
|
98
|
+
assert rid_b in result.output
|
|
99
|
+
assert "✓" in result.output and "✗" in result.output
|
|
100
|
+
assert "*" in result.output # latest marker on run B
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_pf_runs_shows_manifest_and_artifacts(tmp_path, monkeypatch):
|
|
104
|
+
monkeypatch.setenv("PROCESSFORGE_OUTPUT_DIR", str(tmp_path))
|
|
105
|
+
base = "test_flowsheet"
|
|
106
|
+
archive_path = tmp_path / f"{base}.pfarchive"
|
|
107
|
+
runs_dir = archive_path / "runs"
|
|
108
|
+
runs_dir.mkdir(parents=True)
|
|
109
|
+
|
|
110
|
+
rid = "20260101T000000Z_aaaaaa"
|
|
111
|
+
manifest = RunManifest(
|
|
112
|
+
run_id=rid, timestamp=rid.split("_")[0], mode="steady",
|
|
113
|
+
units={"openmc_solver": _eo(rid[-6:], 1.04)},
|
|
114
|
+
)
|
|
115
|
+
(runs_dir / f"{rid}.json").write_text(manifest.model_dump_json())
|
|
116
|
+
|
|
117
|
+
app = typer.Typer()
|
|
118
|
+
app.command()(runs)
|
|
119
|
+
result = CliRunner().invoke(app, [f"/tmp/{base}.json", rid])
|
|
120
|
+
assert result.exit_code == 0, result.output
|
|
121
|
+
assert "k_eff" in result.output
|
|
122
|
+
assert "Artifacts on disk:" in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/base_jacobian_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/jacobian_contributor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{processforge-0.3.19 → processforge-0.3.21}/src/processforge/providers/reference_state_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|