processforge 0.3.26__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.26/src/processforge.egg-info → processforge-0.4.1}/PKG-INFO +5 -1
- {processforge-0.3.26 → processforge-0.4.1}/pyproject.toml +6 -1
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/__init__.py +1 -1
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/__init__.py +1 -1
- processforge-0.4.1/src/processforge/cli/export_fmu.py +88 -0
- processforge-0.4.1/src/processforge/cli/runs.py +201 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/fmu/_fmi_vars.py +35 -2
- {processforge-0.3.26 → 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.26 → processforge-0.4.1}/src/processforge/result.py +135 -0
- {processforge-0.3.26 → processforge-0.4.1/src/processforge.egg-info}/PKG-INFO +5 -1
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge.egg-info/SOURCES.txt +5 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge.egg-info/requires.txt +5 -0
- {processforge-0.3.26 → 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.3.26 → processforge-0.4.1}/tests/test_runs_command.py +27 -4
- processforge-0.3.26/src/processforge/cli/export_fmu.py +0 -41
- processforge-0.3.26/src/processforge/cli/runs.py +0 -88
- processforge-0.3.26/src/processforge/fmu/slave_template.py +0 -252
- {processforge-0.3.26 → processforge-0.4.1}/LICENSE +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/MANIFEST.in +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/README.md +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/setup.cfg +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/__main__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/_schema.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/apply.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/common.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/persist.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/run.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/compose.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/coupling.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/lock.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/output_collector.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/provenance.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/quantity.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/simulate.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/solver.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/state.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/thermo.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/types.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/base.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_container_client.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_coupling.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_display.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_init.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_persist.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_providers.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.26 → processforge-0.4.1}/tests/test_state.py +0 -0
- {processforge-0.3.26 → 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",
|
|
@@ -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,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
|
+
)
|
|
@@ -3,12 +3,45 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
|
+
from .interface import SolverUnitPort
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def _sanitize_name(s: str) -> str:
|
|
8
10
|
"""Replace characters invalid in Python identifiers with underscores."""
|
|
9
11
|
return re.sub(r"[^a-zA-Z0-9_]", "_", s)
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
def solverunit_port_to_spec(port: SolverUnitPort) -> dict:
|
|
15
|
+
"""Convert a :class:`SolverUnitPort` to the internal spec dict format."""
|
|
16
|
+
return {
|
|
17
|
+
"attr_name": port.attr_name,
|
|
18
|
+
"initial_value": float(port.initial_value),
|
|
19
|
+
"causality": port.causality,
|
|
20
|
+
"variability": port.variability,
|
|
21
|
+
"description": port.description,
|
|
22
|
+
"unit": port.unit,
|
|
23
|
+
"logical_name": f"{port.unit_name}.{port.path}",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ensure_unique_attr_names(specs: list[dict]) -> list[dict]:
|
|
28
|
+
"""Deduplicate ``attr_name`` values by appending a counter.
|
|
29
|
+
|
|
30
|
+
Preserves order and returns a new list of specs.
|
|
31
|
+
"""
|
|
32
|
+
seen: dict[str, int] = {}
|
|
33
|
+
out: list[dict] = []
|
|
34
|
+
for spec in specs:
|
|
35
|
+
name = spec["attr_name"]
|
|
36
|
+
count = seen.get(name, 0)
|
|
37
|
+
if count:
|
|
38
|
+
new_name = f"{name}_{count}"
|
|
39
|
+
spec = {**spec, "attr_name": new_name}
|
|
40
|
+
seen[name] = count + 1
|
|
41
|
+
out.append(spec)
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
12
45
|
def get_fmi_variable_specs(
|
|
13
46
|
feed_streams: list[str],
|
|
14
47
|
output_streams: list[str],
|
|
@@ -115,8 +148,8 @@ def get_fmi_variable_specs(
|
|
|
115
148
|
"description": f"Parameter '{key}' of unit '{unit_name}'",
|
|
116
149
|
})
|
|
117
150
|
|
|
118
|
-
# --- Tank state outputs (dynamic
|
|
119
|
-
if mode
|
|
151
|
+
# --- Tank state outputs (dynamic / solverunit_dynamic modes only) ---
|
|
152
|
+
if mode in ("dynamic", "solverunit_dynamic"):
|
|
120
153
|
for unit_name, unit_cfg in config["units"].items():
|
|
121
154
|
if unit_cfg.get("type") != "Tank":
|
|
122
155
|
continue
|
|
@@ -7,9 +7,17 @@ import shutil
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import tempfile
|
|
9
9
|
|
|
10
|
+
import json
|
|
11
|
+
|
|
10
12
|
from ..utils.validate_flowsheet import validate_flowsheet
|
|
11
13
|
from ..utils.topology import TOPOLOGY_KEYS, get_outlets
|
|
12
|
-
from ._fmi_vars import
|
|
14
|
+
from ._fmi_vars import (
|
|
15
|
+
_sanitize_name,
|
|
16
|
+
ensure_unique_attr_names,
|
|
17
|
+
get_fmi_variable_specs,
|
|
18
|
+
solverunit_port_to_spec,
|
|
19
|
+
)
|
|
20
|
+
from .interface import build_solverunit_interface, build_fmu_interface_manifest
|
|
13
21
|
from .slave_template import render_slave_source
|
|
14
22
|
|
|
15
23
|
|
|
@@ -17,6 +25,7 @@ def build_fmu(
|
|
|
17
25
|
config_path: str,
|
|
18
26
|
output_dir: str = "outputs",
|
|
19
27
|
backend: str = "scipy",
|
|
28
|
+
write_manifest: bool = True,
|
|
20
29
|
) -> str:
|
|
21
30
|
"""Build an FMI 2.0 co-simulation FMU from a Processforge flowsheet JSON.
|
|
22
31
|
|
|
@@ -25,6 +34,8 @@ def build_fmu(
|
|
|
25
34
|
output_dir: Directory where the ``.fmu`` file will be written.
|
|
26
35
|
backend: EO solver backend for steady-state mode
|
|
27
36
|
(``"scipy"``, ``"pyomo"``, or ``"casadi"``).
|
|
37
|
+
write_manifest: If ``True`` (default), write ``fmu_interface.json`` next
|
|
38
|
+
to the ``.fmu`` file.
|
|
28
39
|
|
|
29
40
|
Returns:
|
|
30
41
|
Absolute path to the generated ``.fmu`` file.
|
|
@@ -42,6 +53,26 @@ def build_fmu(
|
|
|
42
53
|
interface = _analyze_config(config)
|
|
43
54
|
slave_class_name = _get_slave_class_name(config, config_path)
|
|
44
55
|
|
|
56
|
+
solverunit_ports: dict = {}
|
|
57
|
+
if interface.get("has_solverunit"):
|
|
58
|
+
solverunit_ports = build_solverunit_interface(config)
|
|
59
|
+
interface["solverunit_ports"] = solverunit_ports
|
|
60
|
+
|
|
61
|
+
stream_specs = get_fmi_variable_specs(
|
|
62
|
+
interface["feed_streams"],
|
|
63
|
+
interface["output_streams"],
|
|
64
|
+
interface["components"],
|
|
65
|
+
interface["unit_params"],
|
|
66
|
+
config,
|
|
67
|
+
interface["mode"],
|
|
68
|
+
)
|
|
69
|
+
solverunit_specs = [
|
|
70
|
+
solverunit_port_to_spec(port)
|
|
71
|
+
for ports in solverunit_ports.values()
|
|
72
|
+
for port in ports
|
|
73
|
+
]
|
|
74
|
+
specs = ensure_unique_attr_names(stream_specs + solverunit_specs)
|
|
75
|
+
|
|
45
76
|
os.makedirs(output_dir, exist_ok=True)
|
|
46
77
|
|
|
47
78
|
with tempfile.TemporaryDirectory() as staging_dir:
|
|
@@ -58,6 +89,8 @@ def build_fmu(
|
|
|
58
89
|
output_streams=interface["output_streams"],
|
|
59
90
|
components=interface["components"],
|
|
60
91
|
unit_params=interface["unit_params"],
|
|
92
|
+
solverunit_ports=solverunit_ports,
|
|
93
|
+
specs=specs,
|
|
61
94
|
config=config,
|
|
62
95
|
)
|
|
63
96
|
|
|
@@ -86,6 +119,19 @@ def build_fmu(
|
|
|
86
119
|
fmu_path = os.path.abspath(
|
|
87
120
|
os.path.join(output_dir, f"{slave_class_name}.fmu")
|
|
88
121
|
)
|
|
122
|
+
|
|
123
|
+
if write_manifest:
|
|
124
|
+
parameter_specs = [spec for spec in specs if spec["causality"] == "parameter"]
|
|
125
|
+
non_param_specs = [spec for spec in specs if spec["causality"] != "parameter"]
|
|
126
|
+
manifest = build_fmu_interface_manifest(
|
|
127
|
+
config, non_param_specs, interface.get("solverunit_ports", {}), parameter_specs
|
|
128
|
+
)
|
|
129
|
+
manifest_path = os.path.join(
|
|
130
|
+
output_dir, f"{slave_class_name}_fmu_interface.json"
|
|
131
|
+
)
|
|
132
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
133
|
+
json.dump(manifest, f, indent=2)
|
|
134
|
+
|
|
89
135
|
return fmu_path
|
|
90
136
|
|
|
91
137
|
|
|
@@ -140,12 +186,21 @@ def _analyze_config(config: dict) -> dict:
|
|
|
140
186
|
if params:
|
|
141
187
|
unit_params[unit_name] = params
|
|
142
188
|
|
|
143
|
-
# Simulation mode
|
|
189
|
+
# Simulation mode
|
|
144
190
|
mode = config.get("simulation", {}).get("mode", "steady")
|
|
145
191
|
has_tank = any(
|
|
146
192
|
cfg.get("type") == "Tank" for cfg in config["units"].values()
|
|
147
193
|
)
|
|
148
|
-
|
|
194
|
+
has_solverunit = any(
|
|
195
|
+
cfg.get("type") == "SolverUnit" for cfg in config["units"].values()
|
|
196
|
+
)
|
|
197
|
+
if has_solverunit and has_tank:
|
|
198
|
+
# Both dynamic Tank(s) and SolverUnit(s) require a mixed slave.
|
|
199
|
+
mode = "solverunit_dynamic"
|
|
200
|
+
elif has_solverunit:
|
|
201
|
+
# SolverUnit flowsheets are driven by the Flowsheet-based slave.
|
|
202
|
+
mode = "solverunit"
|
|
203
|
+
elif has_tank:
|
|
149
204
|
mode = "dynamic"
|
|
150
205
|
|
|
151
206
|
return {
|
|
@@ -154,6 +209,8 @@ def _analyze_config(config: dict) -> dict:
|
|
|
154
209
|
"components": components,
|
|
155
210
|
"unit_params": unit_params,
|
|
156
211
|
"mode": mode,
|
|
212
|
+
"has_solverunit": has_solverunit,
|
|
213
|
+
"has_tank": has_tank,
|
|
157
214
|
}
|
|
158
215
|
|
|
159
216
|
def _get_slave_class_name(config: dict, config_path: str) -> str:
|