processforge 0.3.25__tar.gz → 0.4.1__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.25/src/processforge.egg-info → processforge-0.4.1}/PKG-INFO +5 -1
- {processforge-0.3.25 → processforge-0.4.1}/pyproject.toml +6 -1
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/__init__.py +1 -1
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/__init__.py +1 -1
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/apply.py +21 -50
- processforge-0.4.1/src/processforge/cli/export_fmu.py +88 -0
- processforge-0.4.1/src/processforge/cli/persist.py +99 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/run.py +6 -43
- processforge-0.4.1/src/processforge/cli/runs.py +201 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/flowsheet.py +46 -12
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/solver.py +104 -65
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/fmu/_fmi_vars.py +35 -2
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/fmu/builder.py +60 -3
- processforge-0.4.1/src/processforge/fmu/interface.py +214 -0
- processforge-0.4.1/src/processforge/fmu/slave_template.py +572 -0
- processforge-0.4.1/src/processforge/pathsim/__init__.py +6 -0
- processforge-0.4.1/src/processforge/pathsim/adapter.py +184 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/result.py +135 -0
- {processforge-0.3.25 → processforge-0.4.1/src/processforge.egg-info}/PKG-INFO +5 -1
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge.egg-info/SOURCES.txt +7 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge.egg-info/requires.txt +5 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_cli.py +14 -10
- processforge-0.4.1/tests/test_fmu_solverunit.py +281 -0
- processforge-0.4.1/tests/test_pathsim_adapter.py +66 -0
- processforge-0.4.1/tests/test_persist.py +73 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_runs_command.py +27 -4
- processforge-0.3.25/src/processforge/cli/export_fmu.py +0 -41
- processforge-0.3.25/src/processforge/cli/runs.py +0 -88
- processforge-0.3.25/src/processforge/fmu/slave_template.py +0 -252
- {processforge-0.3.25 → processforge-0.4.1}/LICENSE +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/MANIFEST.in +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/README.md +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/setup.cfg +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/__main__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/common.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/compose.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/coupling.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/lock.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/output_collector.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/provenance.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/quantity.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/simulate.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/solver.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/state.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/thermo.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/types.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/base.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_container_client.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_coupling.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_display.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_init.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_providers.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_state.py +0 -0
- {processforge-0.3.25 → processforge-0.4.1}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python-based process simulation framework for chemical engineering applications.
|
|
5
5
|
Author-email: Processforge Team <team@processforge.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -43,9 +43,13 @@ Requires-Dist: pyomo>=6.7; extra == "eo-casadi"
|
|
|
43
43
|
Requires-Dist: casadi>=3.6; extra == "eo-casadi"
|
|
44
44
|
Provides-Extra: fmu
|
|
45
45
|
Requires-Dist: pythonfmu>=0.6; extra == "fmu"
|
|
46
|
+
Requires-Dist: fmpy>=0.3; extra == "fmu"
|
|
46
47
|
Provides-Extra: modelica
|
|
47
48
|
Requires-Dist: OMPython>=4.0.0; extra == "modelica"
|
|
48
49
|
Requires-Dist: fmpy>=0.3; extra == "modelica"
|
|
50
|
+
Provides-Extra: pathsim
|
|
51
|
+
Requires-Dist: pathsim>=0.9; extra == "pathsim"
|
|
52
|
+
Requires-Dist: fmpy>=0.3; extra == "pathsim"
|
|
49
53
|
Provides-Extra: cantera
|
|
50
54
|
Requires-Dist: cantera>=3.0; extra == "cantera"
|
|
51
55
|
Provides-Extra: coolprop
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "processforge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -55,11 +55,16 @@ eo-casadi = [
|
|
|
55
55
|
]
|
|
56
56
|
fmu = [
|
|
57
57
|
"pythonfmu>=0.6",
|
|
58
|
+
"fmpy>=0.3",
|
|
58
59
|
]
|
|
59
60
|
modelica = [
|
|
60
61
|
"OMPython>=4.0.0",
|
|
61
62
|
"fmpy>=0.3",
|
|
62
63
|
]
|
|
64
|
+
pathsim = [
|
|
65
|
+
"pathsim>=0.9",
|
|
66
|
+
"fmpy>=0.3",
|
|
67
|
+
]
|
|
63
68
|
cantera = [
|
|
64
69
|
"cantera>=3.0",
|
|
65
70
|
]
|
|
@@ -32,7 +32,7 @@ def register_commands(app: typer.Typer) -> None:
|
|
|
32
32
|
)(apply)
|
|
33
33
|
app.command(
|
|
34
34
|
"runs",
|
|
35
|
-
help="List runs for a flowsheet, or
|
|
35
|
+
help="List runs for a flowsheet, or inspect one run's Zarr result summary (pf runs <flowsheet> [<run_id>|latest] [--schema])",
|
|
36
36
|
)(runs)
|
|
37
37
|
app.command(
|
|
38
38
|
"plan",
|
|
@@ -6,46 +6,10 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
-
import hashlib
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
9
|
import typer
|
|
13
|
-
from datetime import datetime, timezone
|
|
14
10
|
from loguru import logger
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id):
|
|
18
|
-
"""Build a standardized RunManifest and persist it to the archive."""
|
|
19
|
-
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
|
|
20
|
-
flowsheet_hash = hashlib.sha256(
|
|
21
|
-
json.dumps(config, sort_keys=True, default=str).encode("utf-8")
|
|
22
|
-
).hexdigest()[:16]
|
|
23
|
-
manifest = fs.collect_outputs(
|
|
24
|
-
run_id, config.get("simulation", {}).get("mode", "steady"),
|
|
25
|
-
base_name, provenance=run_info.model_dump(),
|
|
26
|
-
)
|
|
27
|
-
manifest.snapshot_id = snapshot_id
|
|
28
|
-
stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
|
|
29
|
-
archive.save_run(manifest, stream_results=stream_results)
|
|
30
|
-
|
|
31
|
-
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
32
|
-
# inside the archive, mirroring `pf run`.
|
|
33
|
-
try:
|
|
34
|
-
from ..result import relink_latest_results, save_results_zarr
|
|
35
|
-
|
|
36
|
-
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
37
|
-
save_results_zarr(
|
|
38
|
-
results,
|
|
39
|
-
os.path.join(run_results_dir, "results.zarr"),
|
|
40
|
-
run_info,
|
|
41
|
-
)
|
|
42
|
-
relink_latest_results(archive.path, run_id)
|
|
43
|
-
except Exception as e:
|
|
44
|
-
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
45
|
-
|
|
46
|
-
logger.info(f"Run saved : {run_id}")
|
|
47
|
-
return run_id
|
|
48
|
-
|
|
12
|
+
from .persist import make_run_id, persist_run
|
|
49
13
|
from ..eo import EOFlowsheet
|
|
50
14
|
from ..provenance import build_run_info
|
|
51
15
|
from .common import (
|
|
@@ -158,14 +122,15 @@ def apply(
|
|
|
158
122
|
label="converged state",
|
|
159
123
|
)
|
|
160
124
|
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
161
|
-
|
|
125
|
+
run_id = make_run_id()
|
|
126
|
+
persist_run(sm, fs, run_id, results, run_info, config, base_name, snapshot_id)
|
|
162
127
|
logger.info("=== Apply Summary ===")
|
|
163
128
|
logger.info(" Status : CONVERGED")
|
|
164
129
|
logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
|
|
165
130
|
logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
|
|
166
131
|
logger.info(f" Backend : {display_backend(config, fs.backend)}")
|
|
167
132
|
logger.info(f" Snapshot ID : {snapshot_id}")
|
|
168
|
-
logger.info(f" Run ID : {
|
|
133
|
+
logger.info(f" Run ID : {run_id}")
|
|
169
134
|
logger.info(f" Elapsed (s) : {elapsed:.2f}")
|
|
170
135
|
return
|
|
171
136
|
|
|
@@ -180,25 +145,31 @@ def apply(
|
|
|
180
145
|
tmp_fs = _EO(config, backend=backend)
|
|
181
146
|
manager = tmp_fs._build()
|
|
182
147
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
148
|
+
homotopy_result = solve_with_homotopy(tmp_fs, manager, solver, state, drifted)
|
|
149
|
+
if homotopy_result.converged:
|
|
150
|
+
# Assemble the real (converged) outputs from the homotopy
|
|
151
|
+
# solution while the providers are still alive. The `results`
|
|
152
|
+
# from the earlier failed direct solve must NOT be persisted.
|
|
153
|
+
tmp_fs.assemble_from_solution(
|
|
154
|
+
manager, homotopy_result.x_solution, True, homotopy_result.stats
|
|
155
|
+
)
|
|
186
156
|
finally:
|
|
187
157
|
teardown_providers(tmp_fs._provider_map)
|
|
188
158
|
|
|
189
|
-
if
|
|
159
|
+
if homotopy_result.converged:
|
|
190
160
|
logger.info(
|
|
191
|
-
f"Homotopy converged: ||F||={
|
|
192
|
-
f"iterations={
|
|
161
|
+
f"Homotopy converged: ||F||={homotopy_result.stats.get('final_norm', '?'):.3e}, "
|
|
162
|
+
f"iterations={homotopy_result.stats.get('iterations', '?')}"
|
|
193
163
|
)
|
|
194
|
-
save_snapshot(
|
|
195
|
-
sm, config,
|
|
164
|
+
snapshot_id = save_snapshot(
|
|
165
|
+
sm, config, homotopy_result.x_solution, fs.var_names,
|
|
196
166
|
metadata=current_metadata,
|
|
197
167
|
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
198
168
|
label="homotopy solution",
|
|
199
169
|
)
|
|
200
170
|
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
201
|
-
|
|
171
|
+
run_id = make_run_id()
|
|
172
|
+
persist_run(sm, tmp_fs, run_id, tmp_fs.results, run_info, config, base_name, snapshot_id)
|
|
202
173
|
logger.info("Homotopy apply succeeded. New snapshot saved.")
|
|
203
174
|
return
|
|
204
175
|
|
|
@@ -210,8 +181,8 @@ def apply(
|
|
|
210
181
|
breakdown = log_residual_breakdown(fs)
|
|
211
182
|
divergence = build_divergence_report(
|
|
212
183
|
drifted_params=drifted,
|
|
213
|
-
solver_stats=
|
|
214
|
-
x_last=
|
|
184
|
+
solver_stats=homotopy_result.stats,
|
|
185
|
+
x_last=homotopy_result.x_solution,
|
|
215
186
|
var_names=fs.var_names,
|
|
216
187
|
breakdown=breakdown,
|
|
217
188
|
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""``pf export-fmu`` — export flowsheet as FMI 2.0 co-simulation FMU."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from .common import require_existing_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_fmu(
|
|
15
|
+
flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
|
|
16
|
+
output_dir: str = typer.Option(
|
|
17
|
+
"outputs",
|
|
18
|
+
"--output-dir",
|
|
19
|
+
"-o",
|
|
20
|
+
help="Directory for the output FMU (default: outputs/)",
|
|
21
|
+
),
|
|
22
|
+
backend: Literal["scipy", "pyomo", "casadi"] = typer.Option(
|
|
23
|
+
"scipy",
|
|
24
|
+
"--backend",
|
|
25
|
+
help="EO solver backend for steady-state mode (default: scipy)",
|
|
26
|
+
),
|
|
27
|
+
pathsim: bool = typer.Option(
|
|
28
|
+
False,
|
|
29
|
+
"--pathsim",
|
|
30
|
+
help="Also emit the fmu_interface.json manifest and a PathSim wiring example",
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Export a flowsheet as an FMI 2.0 co-simulation FMU."""
|
|
34
|
+
require_existing_file(flowsheet)
|
|
35
|
+
|
|
36
|
+
from ..fmu import build_fmu # local import — pythonfmu is optional
|
|
37
|
+
|
|
38
|
+
output_dir = output_dir or "outputs"
|
|
39
|
+
backend = backend or "scipy"
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
fmu_path = build_fmu(
|
|
43
|
+
flowsheet,
|
|
44
|
+
output_dir=output_dir,
|
|
45
|
+
backend=backend,
|
|
46
|
+
write_manifest=pathsim,
|
|
47
|
+
)
|
|
48
|
+
logger.info(f"FMU written to: {fmu_path}")
|
|
49
|
+
if pathsim:
|
|
50
|
+
base = os.path.splitext(os.path.basename(fmu_path))[0]
|
|
51
|
+
manifest_path = os.path.join(output_dir, f"{base}_fmu_interface.json")
|
|
52
|
+
example_path = os.path.join(output_dir, f"{base}_pathsim.py")
|
|
53
|
+
_write_pathsim_example(example_path, fmu_path, manifest_path)
|
|
54
|
+
logger.info(f"Manifest written to: {manifest_path}")
|
|
55
|
+
logger.info(f"PathSim example written to: {example_path}")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"FMU export failed: {type(e).__name__}: {e}")
|
|
58
|
+
logger.debug("FMU export traceback:", exc_info=True)
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _write_pathsim_example(example_path: str, fmu_path: str, manifest_path: str) -> None:
|
|
63
|
+
"""Write a minimal PathSim wiring example next to the exported FMU."""
|
|
64
|
+
content = f'''"""Auto-generated PathSim wiring example."""
|
|
65
|
+
import pathsim as ps
|
|
66
|
+
from processforge.pathsim import ProcessForgeFMU
|
|
67
|
+
|
|
68
|
+
plant = ProcessForgeFMU(
|
|
69
|
+
fmu_path={fmu_path!r},
|
|
70
|
+
manifest_path={manifest_path!r},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Inspect available ports:
|
|
74
|
+
# print(plant.inputs)
|
|
75
|
+
# print(plant.outputs)
|
|
76
|
+
|
|
77
|
+
# Example: drive an input with a constant setpoint and read an output.
|
|
78
|
+
setpoint = ps.Constant(val=1.0)
|
|
79
|
+
sim = ps.Simulation(
|
|
80
|
+
blocks=[setpoint, plant],
|
|
81
|
+
connections=[
|
|
82
|
+
# ps.Connection(setpoint, plant["<unit>.<input>"]),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
sim.run(time_span=(0, 10))
|
|
86
|
+
'''
|
|
87
|
+
with open(example_path, "w", encoding="utf-8") as f:
|
|
88
|
+
f.write(content)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Shared run-persistence helpers for the ``pf run`` / ``pf apply`` CLIs.
|
|
2
|
+
|
|
3
|
+
Both commands build a :class:`~processforge.types.RunManifest`, save it to the
|
|
4
|
+
:class:`~processforge.persistence.archive.ProcessStateArchive`, and write a Zarr
|
|
5
|
+
copy of the standardized outputs. That logic was previously duplicated (with
|
|
6
|
+
slightly different bugs in each copy); it now lives here in one place.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..persistence.archive import ProcessStateArchive
|
|
21
|
+
from ..types import RunInfo
|
|
22
|
+
|
|
23
|
+
_RUN_ID_TS_FMT = "%Y%m%dT%H%M%SZ"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_run_id() -> str:
|
|
27
|
+
"""Build a unique, sortable run id: ``<UTC timestamp>_<6 hex bytes>``."""
|
|
28
|
+
return (
|
|
29
|
+
datetime.datetime.now(datetime.timezone.utc).strftime(_RUN_ID_TS_FMT)
|
|
30
|
+
+ "_"
|
|
31
|
+
+ os.urandom(3).hex()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def flowsheet_hash(config: dict) -> str:
|
|
36
|
+
"""Short (16-hex) sha256 of a flowsheet config for run/index correlation."""
|
|
37
|
+
return hashlib.sha256(
|
|
38
|
+
json.dumps(config, sort_keys=True, default=str).encode("utf-8")
|
|
39
|
+
).hexdigest()[:16]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def persist_run(
|
|
43
|
+
archive: "ProcessStateArchive",
|
|
44
|
+
fs: Any,
|
|
45
|
+
run_id: str,
|
|
46
|
+
results: dict,
|
|
47
|
+
run_info: "RunInfo | dict",
|
|
48
|
+
config: dict,
|
|
49
|
+
base_name: str,
|
|
50
|
+
snapshot_id: str | None = None,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Persist a run's manifest + Zarr outputs into *archive*.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
archive: The :class:`ProcessStateArchive` for this flowsheet.
|
|
56
|
+
fs: The flowsheet object that produced *results* (must expose
|
|
57
|
+
``collect_outputs``). Either ``EOFlowsheet`` or ``Flowsheet``.
|
|
58
|
+
run_id: Unique run id (use :func:`make_run_id`).
|
|
59
|
+
results: The solve's stream/unit result dict (the *converged* one).
|
|
60
|
+
run_info: Provenance from
|
|
61
|
+
:func:`processforge.provenance.build_run_info`.
|
|
62
|
+
config: Validated flowsheet config (for the simulation mode).
|
|
63
|
+
base_name: Flowsheet basename used for archive paths.
|
|
64
|
+
snapshot_id: Optional snapshot id to stamp on the manifest (so a run
|
|
65
|
+
can be linked back to the state it was derived from).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The run id that was persisted.
|
|
69
|
+
"""
|
|
70
|
+
mode = config.get("simulation", {}).get("mode", "steady")
|
|
71
|
+
run_info_dump = (
|
|
72
|
+
run_info.model_dump() if hasattr(run_info, "model_dump") else run_info
|
|
73
|
+
)
|
|
74
|
+
manifest = fs.collect_outputs(
|
|
75
|
+
run_id, mode, base_name, provenance=run_info_dump
|
|
76
|
+
)
|
|
77
|
+
if snapshot_id is not None:
|
|
78
|
+
manifest.snapshot_id = snapshot_id
|
|
79
|
+
|
|
80
|
+
stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
|
|
81
|
+
archive.save_run(manifest, stream_results=stream_results)
|
|
82
|
+
|
|
83
|
+
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
84
|
+
# inside the archive, mirroring `pf run`.
|
|
85
|
+
try:
|
|
86
|
+
from ..result import relink_latest_results, save_results_zarr
|
|
87
|
+
|
|
88
|
+
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
89
|
+
save_results_zarr(
|
|
90
|
+
results,
|
|
91
|
+
os.path.join(run_results_dir, "results.zarr"),
|
|
92
|
+
run_info,
|
|
93
|
+
)
|
|
94
|
+
relink_latest_results(archive.path, run_id)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
97
|
+
|
|
98
|
+
logger.info(f"Run saved : {run_id}")
|
|
99
|
+
return run_id
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""``pf run`` — run a process simulation from a flowsheet JSON file."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import hashlib
|
|
5
4
|
import os
|
|
6
5
|
from datetime import datetime, timezone
|
|
7
6
|
|
|
@@ -21,22 +20,7 @@ from .common import (
|
|
|
21
20
|
validate_runtime_flowsheet,
|
|
22
21
|
_resolve_provider_url,
|
|
23
22
|
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _flowsheet_hash(config: dict) -> str:
|
|
27
|
-
return hashlib.sha256(
|
|
28
|
-
json_dumps(config).encode("utf-8")
|
|
29
|
-
).hexdigest()[:16]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def json_dumps(obj) -> str:
|
|
33
|
-
import json
|
|
34
|
-
|
|
35
|
-
return json.dumps(obj, sort_keys=True, default=str)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _run_id() -> str:
|
|
39
|
-
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
|
|
23
|
+
from .persist import flowsheet_hash, make_run_id, persist_run
|
|
40
24
|
|
|
41
25
|
|
|
42
26
|
def _log_active_providers(config: dict) -> None:
|
|
@@ -99,8 +83,8 @@ def run(
|
|
|
99
83
|
is_dynamic = mode == "dynamic"
|
|
100
84
|
|
|
101
85
|
# Standardized run id, shared with container-side artifact uploads.
|
|
102
|
-
run_id =
|
|
103
|
-
|
|
86
|
+
run_id = make_run_id()
|
|
87
|
+
flowsheet_hash_value = flowsheet_hash(config)
|
|
104
88
|
|
|
105
89
|
if is_dynamic:
|
|
106
90
|
# Load .pfarchive snapshot as t=0 if available.
|
|
@@ -164,17 +148,12 @@ def run(
|
|
|
164
148
|
# are keyed by the same run_id/flowsheet_hash as this archive run.
|
|
165
149
|
for provider in getattr(fs, "_provider_map", {}).values():
|
|
166
150
|
if hasattr(provider, "set_run_context"):
|
|
167
|
-
provider.set_run_context(run_id,
|
|
151
|
+
provider.set_run_context(run_id, flowsheet_hash_value)
|
|
168
152
|
|
|
169
153
|
# Build + persist the standardized run manifest.
|
|
170
154
|
archive_path = os.path.join(outputs_dir, f"{base_name}.pfarchive")
|
|
171
155
|
archive = ProcessStateArchive(archive_path)
|
|
172
|
-
|
|
173
|
-
stream_results = {
|
|
174
|
-
k: v for k, v in results.items()
|
|
175
|
-
if not hasattr(v, "fields") # exclude EngineOutput objects
|
|
176
|
-
}
|
|
177
|
-
archive.save_run(manifest, stream_results=stream_results)
|
|
156
|
+
persist_run(archive, fs, run_id, results, run_info, config, base_name)
|
|
178
157
|
|
|
179
158
|
# Persist a StateManager snapshot so `pf plan` (and `pf apply`) can diff
|
|
180
159
|
# against this run as a baseline. Without it, `pf plan` always reports
|
|
@@ -191,27 +170,11 @@ def run(
|
|
|
191
170
|
except Exception as e:
|
|
192
171
|
logger.warning(f"Failed to save state snapshot: {type(e).__name__}: {e}")
|
|
193
172
|
|
|
194
|
-
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
195
|
-
# inside the archive. Works for every provider (OpenMC, CoolProp, FESTIM,
|
|
196
|
-
# Cantera) and for both local and remote docker runs — remote artifacts
|
|
197
|
-
# reference their S3 remote_uris.
|
|
198
|
-
try:
|
|
199
|
-
from ..result import relink_latest_results, save_results_zarr
|
|
200
|
-
|
|
201
|
-
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
202
|
-
save_results_zarr(
|
|
203
|
-
results,
|
|
204
|
-
os.path.join(run_results_dir, "results.zarr"),
|
|
205
|
-
run_info,
|
|
206
|
-
)
|
|
207
|
-
relink_latest_results(archive.path, run_id)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
210
|
-
|
|
211
173
|
logger.info(f"Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
|
|
212
174
|
logger.info(f"Run saved : {os.path.join(archive_path, 'runs', run_id + '.json')}")
|
|
213
175
|
|
|
214
176
|
# Summarize standardized outputs.
|
|
177
|
+
manifest = archive.load_run(run_id)
|
|
215
178
|
for unit_name, out in manifest.units.items():
|
|
216
179
|
for f in out.fields:
|
|
217
180
|
logger.info(f" [{unit_name}] {f.name} = {f.quantity.value} {f.quantity.unit}")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""``pf runs`` — list runs and inspect their Zarr result summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import arrow
|
|
9
|
+
import typer
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from .common import flowsheet_basename, output_root
|
|
13
|
+
from ..persistence.archive import ProcessStateArchive
|
|
14
|
+
from ..result import _fmt_scientific, _one_line_summary, summarize_zarr_store
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _humanize_timestamp(ts: str) -> str:
|
|
18
|
+
"""Render a stored timestamp as a human-readable relative string.
|
|
19
|
+
|
|
20
|
+
Falls back to the raw string for values that aren't parseable ISO-8601
|
|
21
|
+
(e.g. ``20260101T000000Z`` used in some test fixtures).
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
return arrow.get(ts).humanize()
|
|
25
|
+
except (arrow.parser.ParserError, ValueError, TypeError):
|
|
26
|
+
return ts
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _zarr_path(archive_path: str, run_id: str) -> str:
|
|
30
|
+
"""Return the per-run Zarr store path."""
|
|
31
|
+
return os.path.join(archive_path, "results", run_id, "results.zarr")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _artifact_status(artifact: dict) -> str:
|
|
35
|
+
"""Return a short presence marker for an artifact dict."""
|
|
36
|
+
local_path = artifact.get("local_path")
|
|
37
|
+
remote_uris = artifact.get("remote_uris") or []
|
|
38
|
+
if local_path and os.path.exists(local_path):
|
|
39
|
+
return "local✓"
|
|
40
|
+
if remote_uris:
|
|
41
|
+
return "remote-only"
|
|
42
|
+
return "missing"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_run_details(manifest, summary: dict) -> str:
|
|
46
|
+
"""Format a detailed, human-readable result summary for a single run."""
|
|
47
|
+
lines = [
|
|
48
|
+
f"Run: {manifest.run_id}",
|
|
49
|
+
f"Timestamp: {manifest.timestamp}",
|
|
50
|
+
f"Mode: {summary.get('mode', manifest.mode)}",
|
|
51
|
+
"",
|
|
52
|
+
"Results:",
|
|
53
|
+
"────────",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
engine_outputs = summary.get("engine_outputs", {})
|
|
57
|
+
if engine_outputs:
|
|
58
|
+
for name, eo in engine_outputs.items():
|
|
59
|
+
engine = eo.get("engine", "")
|
|
60
|
+
sim_type = eo.get("sim_type", "")
|
|
61
|
+
status = eo.get("status", "")
|
|
62
|
+
lines.append(f"{name} ({engine} / {sim_type}) [{status}]")
|
|
63
|
+
for fname, fdata in eo.get("fields", {}).items():
|
|
64
|
+
val = _fmt_scientific(fdata.get("value"))
|
|
65
|
+
unit = fdata.get("unit", "")
|
|
66
|
+
std = fdata.get("std_dev")
|
|
67
|
+
if std is not None:
|
|
68
|
+
lines.append(f" {fname}: {val} ± {_fmt_scientific(std)} {unit}".strip())
|
|
69
|
+
else:
|
|
70
|
+
lines.append(f" {fname}: {val} {unit}".strip())
|
|
71
|
+
if eo.get("attrs") and not eo.get("fields"):
|
|
72
|
+
for k, v in eo["attrs"].items():
|
|
73
|
+
if not k.startswith("_") and k not in ("engine", "sim_type", "status"):
|
|
74
|
+
lines.append(f" {k}: {_fmt_scientific(v)}")
|
|
75
|
+
lines.append("")
|
|
76
|
+
else:
|
|
77
|
+
lines.append(" (no solver/engine outputs)")
|
|
78
|
+
lines.append("")
|
|
79
|
+
|
|
80
|
+
streams = summary.get("streams", {})
|
|
81
|
+
if streams:
|
|
82
|
+
lines.append("Streams:")
|
|
83
|
+
all_vars: set[str] = set()
|
|
84
|
+
for sdata in streams.values():
|
|
85
|
+
all_vars.update(sdata.get("variables", []))
|
|
86
|
+
all_vars = sorted(all_vars)
|
|
87
|
+
|
|
88
|
+
header_parts = ["stream".ljust(20)] + [v.rjust(14) for v in all_vars]
|
|
89
|
+
lines.append(" " + " ".join(header_parts))
|
|
90
|
+
lines.append(" " + "-" * len(" ".join(header_parts)))
|
|
91
|
+
for sname in sorted(streams):
|
|
92
|
+
sdata = streams[sname]
|
|
93
|
+
fields = sdata.get("fields", {})
|
|
94
|
+
row = [sname.ljust(20)]
|
|
95
|
+
for var in all_vars:
|
|
96
|
+
fdata = fields.get(var, {})
|
|
97
|
+
val = fdata.get("value")
|
|
98
|
+
row.append(_fmt_scientific(val).rjust(14))
|
|
99
|
+
lines.append(" " + " ".join(row))
|
|
100
|
+
lines.append("")
|
|
101
|
+
|
|
102
|
+
artifacts = summary.get("artifacts", [])
|
|
103
|
+
if artifacts:
|
|
104
|
+
lines.append("Artifacts:")
|
|
105
|
+
for art in artifacts:
|
|
106
|
+
name = art.get("name", "unknown")
|
|
107
|
+
kind = art.get("kind", "")
|
|
108
|
+
status = _artifact_status(art)
|
|
109
|
+
loc = art.get("local_path") or (art.get("remote_uris") or ["no location"])[0]
|
|
110
|
+
lines.append(f" [{kind}] {name}: {status} ({loc})")
|
|
111
|
+
else:
|
|
112
|
+
lines.append("Artifacts: none")
|
|
113
|
+
|
|
114
|
+
zarr_path = summary.get("_zarr_path")
|
|
115
|
+
if zarr_path:
|
|
116
|
+
lines.append("")
|
|
117
|
+
lines.append(f"Zarr store: {zarr_path}")
|
|
118
|
+
|
|
119
|
+
return "\n".join(lines)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def runs(
|
|
123
|
+
flowsheet: str = typer.Argument(..., help="Path to the flowsheet JSON"),
|
|
124
|
+
run_id: str = typer.Argument(
|
|
125
|
+
None,
|
|
126
|
+
help="Show this run's result summary. Use 'latest' for the most recent run.",
|
|
127
|
+
),
|
|
128
|
+
schema: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--schema",
|
|
131
|
+
help="Print the result schema JSON instead of the summary.",
|
|
132
|
+
),
|
|
133
|
+
):
|
|
134
|
+
"""List runs for a flowsheet, or inspect one run's Zarr result summary."""
|
|
135
|
+
base = flowsheet_basename(flowsheet)
|
|
136
|
+
archive_path = os.path.join(output_root(), f"{base}.pfarchive")
|
|
137
|
+
|
|
138
|
+
if not os.path.isdir(archive_path):
|
|
139
|
+
logger.error(
|
|
140
|
+
f"No archive found for '{flowsheet}' at {archive_path}. "
|
|
141
|
+
"Run 'pf run' first."
|
|
142
|
+
)
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
archive = ProcessStateArchive(archive_path)
|
|
146
|
+
latest = archive.latest_run()
|
|
147
|
+
latest_id = latest.run_id if latest is not None else None
|
|
148
|
+
|
|
149
|
+
if run_id is not None:
|
|
150
|
+
resolved_id = latest_id if run_id == "latest" and latest_id else run_id
|
|
151
|
+
if resolved_id != run_id and latest_id is None:
|
|
152
|
+
logger.error(f"No latest run found in {archive_path}.")
|
|
153
|
+
raise typer.Exit(code=1)
|
|
154
|
+
|
|
155
|
+
if resolved_id not in archive.list_runs():
|
|
156
|
+
logger.error(f"Run '{resolved_id}' not found in {archive_path}/runs.")
|
|
157
|
+
raise typer.Exit(code=1)
|
|
158
|
+
|
|
159
|
+
zarr_path = _zarr_path(archive_path, resolved_id)
|
|
160
|
+
|
|
161
|
+
if schema:
|
|
162
|
+
schema_path = zarr_path + ".schema.json"
|
|
163
|
+
if not os.path.isfile(schema_path):
|
|
164
|
+
logger.error(f"No schema found for run '{resolved_id}'.")
|
|
165
|
+
raise typer.Exit(code=1)
|
|
166
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
167
|
+
typer.echo(f.read())
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
manifest = archive.load_run(resolved_id)
|
|
171
|
+
summary = summarize_zarr_store(zarr_path)
|
|
172
|
+
summary["_zarr_path"] = zarr_path
|
|
173
|
+
typer.echo(_format_run_details(manifest, summary))
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# List all runs with a one-line summary.
|
|
177
|
+
run_ids = archive.list_runs()
|
|
178
|
+
if not run_ids:
|
|
179
|
+
typer.echo(f"No runs found for '{flowsheet}'.")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
header = f"{'RUN ID':<28} {'TIMESTAMP':<22} {'LATEST':<7} {'SUMMARY':<30}"
|
|
183
|
+
typer.echo(header)
|
|
184
|
+
typer.echo("-" * len(header))
|
|
185
|
+
for rid in run_ids:
|
|
186
|
+
manifest = archive.load_run(rid)
|
|
187
|
+
if manifest is None:
|
|
188
|
+
continue
|
|
189
|
+
marker = "*" if rid == latest_id else " "
|
|
190
|
+
zarr_path = _zarr_path(archive_path, rid)
|
|
191
|
+
summary = summarize_zarr_store(zarr_path)
|
|
192
|
+
one_liner = _one_line_summary(summary)
|
|
193
|
+
typer.echo(
|
|
194
|
+
f"{rid:<28} {_humanize_timestamp(manifest.timestamp):<22} {marker:<7} {one_liner:<30}"
|
|
195
|
+
)
|
|
196
|
+
typer.echo("")
|
|
197
|
+
typer.echo(
|
|
198
|
+
"'*' marks the latest run. "
|
|
199
|
+
"'pf runs <flowsheet> <run_id>' shows the result summary. "
|
|
200
|
+
"Use 'latest' as the run id for the most recent run."
|
|
201
|
+
)
|