processforge 0.3.4__tar.gz → 0.3.5__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.
Potentially problematic release.
This version of processforge might be problematic. Click here for more details.
- {processforge-0.3.4/src/processforge.egg-info → processforge-0.3.5}/PKG-INFO +1 -1
- {processforge-0.3.4 → processforge-0.3.5}/pyproject.toml +1 -1
- processforge-0.3.5/src/processforge/cli/apply.py +209 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/common.py +0 -44
- processforge-0.3.5/src/processforge/cli/run.py +107 -0
- processforge-0.3.5/src/processforge/compose.py +52 -0
- {processforge-0.3.4 → processforge-0.3.5/src/processforge.egg-info}/PKG-INFO +1 -1
- {processforge-0.3.4 → processforge-0.3.5}/tests/test_cli.py +4 -146
- processforge-0.3.4/src/processforge/cli/apply.py +0 -238
- processforge-0.3.4/src/processforge/cli/run.py +0 -137
- processforge-0.3.4/src/processforge/compose.py +0 -114
- {processforge-0.3.4 → processforge-0.3.5}/LICENSE +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/MANIFEST.in +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/README.md +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/setup.cfg +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/__main__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/_schema.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/lock.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/provenance.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/result.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/simulate.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/solver.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/state.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/thermo.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/types.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/base.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge.egg-info/SOURCES.txt +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge.egg-info/requires.txt +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/tests/test_init.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/tests/test_providers.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/tests/test_state.py +0 -0
- {processforge-0.3.4 → processforge-0.3.5}/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.5"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""``pf apply`` — state-based warm start with homotopy fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from ..eo import EOFlowsheet
|
|
13
|
+
from ..provenance import build_run_info
|
|
14
|
+
from ..result import save_results_zarr
|
|
15
|
+
from ..state import StateManager
|
|
16
|
+
from .common import (
|
|
17
|
+
build_divergence_report,
|
|
18
|
+
build_run_metadata,
|
|
19
|
+
check_providers,
|
|
20
|
+
flowsheet_basename,
|
|
21
|
+
load_state_manager,
|
|
22
|
+
log_residual_breakdown,
|
|
23
|
+
output_root,
|
|
24
|
+
require_existing_file,
|
|
25
|
+
save_snapshot,
|
|
26
|
+
validate_runtime_flowsheet,
|
|
27
|
+
write_divergence_report,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .display import print_structural_diff
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def apply(
|
|
34
|
+
flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
|
|
35
|
+
backend: Literal["scipy", "pyomo", "casadi"] | None = typer.Option(
|
|
36
|
+
None,
|
|
37
|
+
"--backend",
|
|
38
|
+
help="Override the flowsheet's simulation.backend",
|
|
39
|
+
),
|
|
40
|
+
tolerance: float = typer.Option(
|
|
41
|
+
1e-6,
|
|
42
|
+
"--tolerance",
|
|
43
|
+
help="Newton solver convergence tolerance (default: 1e-6)",
|
|
44
|
+
),
|
|
45
|
+
max_iter: int = typer.Option(
|
|
46
|
+
50,
|
|
47
|
+
"--max-iter",
|
|
48
|
+
help="Max Newton iterations (default: 50)",
|
|
49
|
+
),
|
|
50
|
+
skip_homotopy: bool = typer.Option(
|
|
51
|
+
False,
|
|
52
|
+
"--skip-homotopy",
|
|
53
|
+
help="Disable homotopy fallback; cold-start only",
|
|
54
|
+
),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Apply flowsheet: drift detection, warm-start, homotopy fallback, convergence guardrails."""
|
|
57
|
+
require_existing_file(flowsheet)
|
|
58
|
+
config = validate_runtime_flowsheet(flowsheet)
|
|
59
|
+
|
|
60
|
+
# Check provider availability (assumes any containers are already running)
|
|
61
|
+
check_providers(config, flowsheet)
|
|
62
|
+
|
|
63
|
+
base_name = flowsheet_basename(flowsheet)
|
|
64
|
+
|
|
65
|
+
sim_cfg = config.get("simulation", {})
|
|
66
|
+
mode = sim_cfg.get("mode", "steady")
|
|
67
|
+
if mode != "steady":
|
|
68
|
+
logger.error("pf apply is only supported for steady-state EO flowsheets.")
|
|
69
|
+
raise SystemExit(1)
|
|
70
|
+
|
|
71
|
+
outputs_dir = output_root()
|
|
72
|
+
os.makedirs(outputs_dir, exist_ok=True)
|
|
73
|
+
sm, state = load_state_manager(outputs_dir, base_name)
|
|
74
|
+
|
|
75
|
+
# Structural diff: detect topology changes
|
|
76
|
+
topology_changed = False
|
|
77
|
+
if state is not None:
|
|
78
|
+
diff = sm.detect_structural_diff(config, state)
|
|
79
|
+
print_structural_diff(diff)
|
|
80
|
+
topology_changed = bool(diff.get("added") or diff.get("removed"))
|
|
81
|
+
if topology_changed:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Topology changed (units added/removed). "
|
|
84
|
+
"Homotopy requires identical topology — falling back to cold start."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Parameter drift (only meaningful when topology is unchanged)
|
|
88
|
+
drifted: list[str] = []
|
|
89
|
+
current_metadata = build_run_metadata(config, tolerance, max_iter, backend or "scipy")
|
|
90
|
+
if state is not None and not topology_changed:
|
|
91
|
+
mismatches = sm.validate_metadata(current_metadata, state)
|
|
92
|
+
if mismatches:
|
|
93
|
+
logger.warning(f"Metadata mismatch: {mismatches}")
|
|
94
|
+
drifted = sm.detect_drift(config, state)
|
|
95
|
+
if not drifted:
|
|
96
|
+
logger.info("No drift detected. System is already at the desired state.")
|
|
97
|
+
return
|
|
98
|
+
stream_drifts = [d for d in drifted if d.startswith("streams.")]
|
|
99
|
+
unit_drifts = [d for d in drifted if d.startswith("units.")]
|
|
100
|
+
logger.warning("Drift detected:")
|
|
101
|
+
if stream_drifts:
|
|
102
|
+
logger.warning(f" Stream drift : {stream_drifts}")
|
|
103
|
+
if unit_drifts:
|
|
104
|
+
logger.warning(f" Unit drift : {unit_drifts}")
|
|
105
|
+
|
|
106
|
+
# Build flowsheet; attach saved state for warm-start unless topology changed
|
|
107
|
+
fs = EOFlowsheet(config, backend=backend)
|
|
108
|
+
fs.saved_state = state if not topology_changed else None
|
|
109
|
+
fs.solver_tol = tolerance
|
|
110
|
+
fs.solver_max_iter = max_iter
|
|
111
|
+
|
|
112
|
+
logger.info("=== Running Apply (Steady-State EO) ===")
|
|
113
|
+
t0 = time.perf_counter()
|
|
114
|
+
results = fs.run()
|
|
115
|
+
elapsed = time.perf_counter() - t0
|
|
116
|
+
|
|
117
|
+
logger.info(
|
|
118
|
+
f"Direct solve completed in {elapsed:.2f}s (converged={fs.converged})."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if fs.converged:
|
|
122
|
+
snapshot_id = save_snapshot(
|
|
123
|
+
sm, config, fs.x_converged, fs.var_names,
|
|
124
|
+
metadata=current_metadata,
|
|
125
|
+
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
126
|
+
label="converged state",
|
|
127
|
+
)
|
|
128
|
+
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
129
|
+
zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
|
|
130
|
+
save_results_zarr(results, zarr_path, run_info=run_info)
|
|
131
|
+
logger.info("=== Apply Summary ===")
|
|
132
|
+
logger.info(" Status : CONVERGED")
|
|
133
|
+
logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
|
|
134
|
+
logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
|
|
135
|
+
logger.info(f" Backend : {fs.backend}")
|
|
136
|
+
logger.info(f" Snapshot ID : {snapshot_id}")
|
|
137
|
+
logger.info(f" Results zarr : {zarr_path}")
|
|
138
|
+
logger.info(f" Elapsed (s) : {elapsed:.2f}")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Direct solve failed — try homotopy (only when topology is same and state exists)
|
|
142
|
+
if state is not None and not topology_changed and drifted and not skip_homotopy:
|
|
143
|
+
logger.warning("Direct solve failed. Attempting homotopy continuation...")
|
|
144
|
+
from ..eo.solver import EOSolver, solve_with_homotopy
|
|
145
|
+
from ..eo.flowsheet import EOFlowsheet as _EO
|
|
146
|
+
from ..providers.manager import teardown_providers
|
|
147
|
+
|
|
148
|
+
solver = EOSolver(backend=fs.backend, tol=tolerance, max_iter=max_iter)
|
|
149
|
+
tmp_fs = _EO(config, backend=backend)
|
|
150
|
+
manager = tmp_fs._build()
|
|
151
|
+
try:
|
|
152
|
+
x_hom, hom_converged, hom_stats = solve_with_homotopy(
|
|
153
|
+
tmp_fs, manager, solver, state, drifted
|
|
154
|
+
)
|
|
155
|
+
finally:
|
|
156
|
+
teardown_providers(tmp_fs._provider_map)
|
|
157
|
+
|
|
158
|
+
if hom_converged:
|
|
159
|
+
logger.info(
|
|
160
|
+
f"Homotopy converged: ||F||={hom_stats.get('final_norm', '?'):.3e}, "
|
|
161
|
+
f"iterations={hom_stats.get('iterations', '?')}"
|
|
162
|
+
)
|
|
163
|
+
save_snapshot(
|
|
164
|
+
sm, config, x_hom, fs.var_names,
|
|
165
|
+
metadata=current_metadata,
|
|
166
|
+
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
167
|
+
label="homotopy solution",
|
|
168
|
+
)
|
|
169
|
+
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
170
|
+
save_results_zarr(
|
|
171
|
+
results,
|
|
172
|
+
os.path.join(outputs_dir, f"{base_name}_results.zarr"),
|
|
173
|
+
run_info=run_info,
|
|
174
|
+
)
|
|
175
|
+
logger.info("Homotopy apply succeeded. New snapshot saved.")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Both failed — auto-revert and write divergence report
|
|
179
|
+
logger.error("Homotopy also failed to converge.")
|
|
180
|
+
prev_id = state.snapshot_id if state is not None else "unknown"
|
|
181
|
+
sm.rollback(1)
|
|
182
|
+
logger.warning(f"Reverted .pfstate to snapshot before {prev_id}.")
|
|
183
|
+
breakdown = log_residual_breakdown(fs)
|
|
184
|
+
divergence = build_divergence_report(
|
|
185
|
+
drifted_params=drifted,
|
|
186
|
+
solver_stats=hom_stats,
|
|
187
|
+
x_last=x_hom,
|
|
188
|
+
var_names=fs.var_names,
|
|
189
|
+
breakdown=breakdown,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
# Cold start also failed; no rollback (nothing to revert to)
|
|
193
|
+
if not drifted and state is None:
|
|
194
|
+
logger.error("Cold-start solve failed to converge (no prior snapshot).")
|
|
195
|
+
elif skip_homotopy:
|
|
196
|
+
logger.error("Cold-start solve failed to converge (--skip-homotopy).")
|
|
197
|
+
else:
|
|
198
|
+
logger.error("Cold-start solve failed to converge.")
|
|
199
|
+
breakdown = log_residual_breakdown(fs)
|
|
200
|
+
divergence = build_divergence_report(
|
|
201
|
+
drifted_params=[],
|
|
202
|
+
solver_stats=fs.solver_stats,
|
|
203
|
+
x_last=getattr(fs, "x_converged", []),
|
|
204
|
+
var_names=getattr(fs, "var_names", []),
|
|
205
|
+
breakdown=breakdown,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
write_divergence_report(outputs_dir, base_name, divergence)
|
|
209
|
+
raise SystemExit(1)
|
|
@@ -203,50 +203,6 @@ def check_providers(
|
|
|
203
203
|
raise SystemExit(1)
|
|
204
204
|
|
|
205
205
|
|
|
206
|
-
def has_containerized_providers(config: dict) -> bool:
|
|
207
|
-
"""Return True if any declared provider runs in a Docker container."""
|
|
208
|
-
from ..providers.registry import is_containerized
|
|
209
|
-
|
|
210
|
-
return any(
|
|
211
|
-
is_containerized(cfg.get("type", ""))
|
|
212
|
-
for cfg in config.get("providers", {}).values()
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def wait_for_provider_health(config: dict, timeout: int = 30) -> None:
|
|
217
|
-
"""Poll ``/health`` for each localhost containerized provider until ready.
|
|
218
|
-
|
|
219
|
-
Only providers whose resolved URL host is ``localhost``/``127.0.0.1`` are
|
|
220
|
-
waited on — remote providers are assumed to be managed elsewhere.
|
|
221
|
-
"""
|
|
222
|
-
import time
|
|
223
|
-
import urllib.parse
|
|
224
|
-
|
|
225
|
-
from ..providers.registry import is_containerized
|
|
226
|
-
|
|
227
|
-
providers = config.get("providers", {})
|
|
228
|
-
deadline = time.time() + timeout
|
|
229
|
-
for name, cfg in providers.items():
|
|
230
|
-
if not is_containerized(cfg.get("type", "")):
|
|
231
|
-
continue
|
|
232
|
-
url = _resolve_provider_url(cfg, cfg.get("type", ""))
|
|
233
|
-
host = urllib.parse.urlparse(url).hostname
|
|
234
|
-
if host not in ("localhost", "127.0.0.1"):
|
|
235
|
-
continue
|
|
236
|
-
health = f"{url}/health"
|
|
237
|
-
while time.time() < deadline:
|
|
238
|
-
try:
|
|
239
|
-
urllib.request.urlopen(health, timeout=2)
|
|
240
|
-
logger.info(f"Provider '{name}' ready at {url}")
|
|
241
|
-
break
|
|
242
|
-
except (urllib.error.URLError, OSError):
|
|
243
|
-
time.sleep(1)
|
|
244
|
-
else:
|
|
245
|
-
logger.warning(
|
|
246
|
-
f"Provider '{name}' not ready at {url} after {timeout}s."
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
206
|
# ---------------------------------------------------------------------------
|
|
251
207
|
# Divergence reports
|
|
252
208
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""``pf run`` — run a process simulation from a flowsheet JSON file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from ..flowsheet import Flowsheet
|
|
11
|
+
from ..eo import EOFlowsheet
|
|
12
|
+
from ..provenance import build_dynamic_x0, build_run_info
|
|
13
|
+
from ..result import plot_results, plot_timeseries, save_results_zarr
|
|
14
|
+
from ..state import StateManager
|
|
15
|
+
from .common import (
|
|
16
|
+
check_providers,
|
|
17
|
+
flowsheet_basename,
|
|
18
|
+
output_root,
|
|
19
|
+
require_existing_file,
|
|
20
|
+
validate_runtime_flowsheet,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(
|
|
25
|
+
flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
|
|
26
|
+
export_images: bool = typer.Option(
|
|
27
|
+
False,
|
|
28
|
+
"--export-images",
|
|
29
|
+
help="Generate PNG plots for simulation outputs",
|
|
30
|
+
),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Run a process simulation from a flowsheet JSON file."""
|
|
33
|
+
require_existing_file(flowsheet)
|
|
34
|
+
config = validate_runtime_flowsheet(flowsheet)
|
|
35
|
+
|
|
36
|
+
# Check provider availability (assumes any containers are already running)
|
|
37
|
+
check_providers(config, flowsheet)
|
|
38
|
+
|
|
39
|
+
base_name = flowsheet_basename(flowsheet)
|
|
40
|
+
outputs_dir = output_root()
|
|
41
|
+
|
|
42
|
+
sim_cfg = config.get("simulation", {})
|
|
43
|
+
mode = sim_cfg.get("mode", "steady")
|
|
44
|
+
is_dynamic = mode == "dynamic"
|
|
45
|
+
|
|
46
|
+
if is_dynamic:
|
|
47
|
+
# Load .pfstate as t=0 if available
|
|
48
|
+
state_path = os.path.join(outputs_dir, f"{base_name}.pfstate")
|
|
49
|
+
sm = StateManager(state_path)
|
|
50
|
+
state = sm.load_state()
|
|
51
|
+
if state is not None:
|
|
52
|
+
stream_inits = sm.state_to_stream_dicts(state)
|
|
53
|
+
streams_cfg = config.get("streams", {})
|
|
54
|
+
for s_name, s_vals in stream_inits.items():
|
|
55
|
+
if s_name in streams_cfg:
|
|
56
|
+
streams_cfg[s_name].update(s_vals)
|
|
57
|
+
else:
|
|
58
|
+
logger.debug(
|
|
59
|
+
f"Stream '{s_name}' from snapshot not in current flowsheet — skipped."
|
|
60
|
+
)
|
|
61
|
+
logger.info("Using .pfstate converged state as dynamic t=0.")
|
|
62
|
+
else:
|
|
63
|
+
logger.debug("No .pfstate found — starting dynamic run from flowsheet defaults.")
|
|
64
|
+
|
|
65
|
+
fs = Flowsheet(config)
|
|
66
|
+
logger.info("=== Dynamic Results ===")
|
|
67
|
+
results = fs.run()
|
|
68
|
+
|
|
69
|
+
if hasattr(fs, "converged"):
|
|
70
|
+
if fs.converged:
|
|
71
|
+
logger.info("Dynamic simulation converged.")
|
|
72
|
+
else:
|
|
73
|
+
logger.warning("Dynamic simulation did NOT converge. Results may be unreliable.")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
x0, var_names = build_dynamic_x0(config)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Failed to build initial state vector: {type(e).__name__}: {e}")
|
|
79
|
+
raise SystemExit(1)
|
|
80
|
+
|
|
81
|
+
run_info = build_run_info(config, x0=x0, var_names=var_names)
|
|
82
|
+
else:
|
|
83
|
+
# Pass None so EOFlowsheet resolves backend from config (with scipy default).
|
|
84
|
+
fs = EOFlowsheet(config, backend=None)
|
|
85
|
+
logger.info("=== Steady-State EO Results ===")
|
|
86
|
+
results = fs.run()
|
|
87
|
+
|
|
88
|
+
if hasattr(fs, "converged"):
|
|
89
|
+
if fs.converged:
|
|
90
|
+
logger.info("Steady-state simulation converged.")
|
|
91
|
+
else:
|
|
92
|
+
logger.warning("Steady-state simulation did NOT converge. Results may be unreliable.")
|
|
93
|
+
|
|
94
|
+
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
95
|
+
|
|
96
|
+
os.makedirs(outputs_dir, exist_ok=True)
|
|
97
|
+
zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
|
|
98
|
+
save_results_zarr(results, zarr_path, run_info=run_info)
|
|
99
|
+
logger.info(f"Results saved to {zarr_path}")
|
|
100
|
+
|
|
101
|
+
if export_images:
|
|
102
|
+
try:
|
|
103
|
+
plot_results(results, fname=f"{base_name}_results.png")
|
|
104
|
+
plot_timeseries(results, fname=f"{base_name}_timeseries.png")
|
|
105
|
+
logger.info(f"Plots saved: {base_name}_results.png, {base_name}_timeseries.png")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(f"Failed to generate plots: {type(e).__name__}: {e}")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Generate and manage docker-compose.yml for containerized providers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
COMPOSE_FILENAME = "docker-compose.yml"
|
|
9
|
+
COMPOSE_HEADER = (
|
|
10
|
+
"# Auto-generated by pf init — do not edit manually.\n"
|
|
11
|
+
"# Re-generate: pf init <flowsheet.json>\n"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _compose_path(pf_dir: str) -> str:
|
|
16
|
+
return os.path.join(pf_dir, COMPOSE_FILENAME)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_compose(
|
|
20
|
+
pf_dir: str,
|
|
21
|
+
containerized_providers: dict[str, dict],
|
|
22
|
+
outputs_dir: str = "outputs",
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Generate docker-compose.yml for providers that need Docker.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
pf_dir: Path to .processforge/ directory.
|
|
28
|
+
containerized_providers: Dict of provider_name -> {"url": ..., "docker_image": ..., "port": ...}.
|
|
29
|
+
outputs_dir: Host directory to mount as /data in containers.
|
|
30
|
+
"""
|
|
31
|
+
if not containerized_providers:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
lines = [COMPOSE_HEADER, "services:"]
|
|
35
|
+
for name, info in containerized_providers.items():
|
|
36
|
+
image = info["docker_image"]
|
|
37
|
+
port = info.get("port") or 9000
|
|
38
|
+
lines.append(f" {name}:")
|
|
39
|
+
lines.append(f" image: {image}")
|
|
40
|
+
# Bind directly to the host network so the provider is reachable at
|
|
41
|
+
# http://localhost:<port>. Docker bridge port-publishing is unreliable
|
|
42
|
+
# on some hosts (the published port is not reachable from the host),
|
|
43
|
+
# and host networking avoids that entirely.
|
|
44
|
+
lines.append(f" network_mode: host")
|
|
45
|
+
lines.append(f" volumes:")
|
|
46
|
+
lines.append(f' - "${{OPENMC_DATA_ROOT:-{outputs_dir}}}:/data"')
|
|
47
|
+
lines.append("")
|
|
48
|
+
|
|
49
|
+
os.makedirs(pf_dir, exist_ok=True)
|
|
50
|
+
path = _compose_path(pf_dir)
|
|
51
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
52
|
+
f.write("\n".join(lines))
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
-
import subprocess
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from unittest.mock import MagicMock, patch
|
|
9
8
|
|
|
@@ -371,7 +370,7 @@ class TestCmdRun:
|
|
|
371
370
|
mock_os.path.join = real_os.path.join
|
|
372
371
|
return mock_os
|
|
373
372
|
|
|
374
|
-
def
|
|
373
|
+
def test_containerized_flowsheet_runs_without_lifecycle(self, tmp_path, log_output):
|
|
375
374
|
import processforge.cli.run as run_mod
|
|
376
375
|
from processforge.cli.run import run
|
|
377
376
|
|
|
@@ -385,9 +384,6 @@ class TestCmdRun:
|
|
|
385
384
|
mock_fs.var_names = []
|
|
386
385
|
|
|
387
386
|
with patch.object(run_mod, "os", self._patch_run_os(run_mod)), \
|
|
388
|
-
patch.object(run_mod, "start_provider_containers", return_value=True) as mock_start, \
|
|
389
|
-
patch.object(run_mod, "stop_provider_containers") as mock_stop, \
|
|
390
|
-
patch.object(run_mod, "wait_for_provider_health"), \
|
|
391
387
|
patch.object(run_mod, "validate_runtime_flowsheet",
|
|
392
388
|
return_value=self._containerized_config(flowsheet)), \
|
|
393
389
|
patch.object(run_mod, "check_providers"), \
|
|
@@ -395,36 +391,6 @@ class TestCmdRun:
|
|
|
395
391
|
patch.object(run_mod, "save_results_zarr"):
|
|
396
392
|
run(flowsheet=str(flowsheet), export_images=False)
|
|
397
393
|
|
|
398
|
-
mock_start.assert_called_once()
|
|
399
|
-
mock_stop.assert_called_once()
|
|
400
|
-
|
|
401
|
-
def test_keep_containers_skips_stop(self, tmp_path):
|
|
402
|
-
import processforge.cli.run as run_mod
|
|
403
|
-
from processforge.cli.run import run
|
|
404
|
-
|
|
405
|
-
flowsheet = tmp_path / "fs.json"
|
|
406
|
-
flowsheet.write_text(json.dumps(self._containerized_config(flowsheet)["providers"]))
|
|
407
|
-
|
|
408
|
-
mock_fs = MagicMock()
|
|
409
|
-
mock_fs.run.return_value = {}
|
|
410
|
-
mock_fs.converged = True
|
|
411
|
-
mock_fs.x0 = []
|
|
412
|
-
mock_fs.var_names = []
|
|
413
|
-
|
|
414
|
-
with patch.object(run_mod, "os", self._patch_run_os(run_mod)), \
|
|
415
|
-
patch.object(run_mod, "start_provider_containers", return_value=True) as mock_start, \
|
|
416
|
-
patch.object(run_mod, "stop_provider_containers") as mock_stop, \
|
|
417
|
-
patch.object(run_mod, "wait_for_provider_health"), \
|
|
418
|
-
patch.object(run_mod, "validate_runtime_flowsheet",
|
|
419
|
-
return_value=self._containerized_config(flowsheet)), \
|
|
420
|
-
patch.object(run_mod, "check_providers"), \
|
|
421
|
-
patch.object(run_mod, "EOFlowsheet", return_value=mock_fs), \
|
|
422
|
-
patch.object(run_mod, "save_results_zarr"):
|
|
423
|
-
run(flowsheet=str(flowsheet), export_images=False, keep_containers=True)
|
|
424
|
-
|
|
425
|
-
mock_start.assert_called_once()
|
|
426
|
-
mock_stop.assert_not_called()
|
|
427
|
-
|
|
428
394
|
def test_coolprop_flowsheet_no_lifecycle(self, tmp_path):
|
|
429
395
|
import processforge.cli.run as run_mod
|
|
430
396
|
from processforge.cli.run import run
|
|
@@ -441,8 +407,6 @@ class TestCmdRun:
|
|
|
441
407
|
# A stale docker-compose.yml is present, but the flowsheet only uses
|
|
442
408
|
# pip providers — Docker must never be touched.
|
|
443
409
|
with patch.object(run_mod, "os", self._patch_run_os(run_mod)), \
|
|
444
|
-
patch.object(run_mod, "start_provider_containers") as mock_start, \
|
|
445
|
-
patch.object(run_mod, "stop_provider_containers") as mock_stop, \
|
|
446
410
|
patch.object(run_mod, "validate_runtime_flowsheet",
|
|
447
411
|
return_value={"providers": {"coolprop": {"type": "coolprop"}},
|
|
448
412
|
"streams": {}, "units": {},
|
|
@@ -453,9 +417,6 @@ class TestCmdRun:
|
|
|
453
417
|
patch.object(run_mod, "save_results_zarr"):
|
|
454
418
|
run(flowsheet=str(flowsheet), export_images=False)
|
|
455
419
|
|
|
456
|
-
mock_start.assert_not_called()
|
|
457
|
-
mock_stop.assert_not_called()
|
|
458
|
-
|
|
459
420
|
def test_no_compose_no_lifecycle(self, tmp_path):
|
|
460
421
|
import processforge.cli.run as run_mod
|
|
461
422
|
from processforge.cli.run import run
|
|
@@ -469,10 +430,9 @@ class TestCmdRun:
|
|
|
469
430
|
mock_fs.x0 = []
|
|
470
431
|
mock_fs.var_names = []
|
|
471
432
|
|
|
472
|
-
# Containerized flowsheet but no compose file
|
|
433
|
+
# Containerized flowsheet but no compose file — run still proceeds
|
|
434
|
+
# (containers are assumed already running).
|
|
473
435
|
with patch.object(run_mod, "os") as mock_os, \
|
|
474
|
-
patch.object(run_mod, "start_provider_containers") as mock_start, \
|
|
475
|
-
patch.object(run_mod, "stop_provider_containers") as mock_stop, \
|
|
476
436
|
patch.object(run_mod, "validate_runtime_flowsheet",
|
|
477
437
|
return_value=self._containerized_config(flowsheet)), \
|
|
478
438
|
patch.object(run_mod, "check_providers"), \
|
|
@@ -481,66 +441,6 @@ class TestCmdRun:
|
|
|
481
441
|
mock_os.path.exists.return_value = False # no compose file
|
|
482
442
|
run(flowsheet=str(flowsheet), export_images=False)
|
|
483
443
|
|
|
484
|
-
mock_start.assert_not_called()
|
|
485
|
-
mock_stop.assert_not_called()
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
class TestComposeLifecycle:
|
|
489
|
-
def _write_compose(self, tmp_path):
|
|
490
|
-
pf = tmp_path / ".processforge"
|
|
491
|
-
pf.mkdir()
|
|
492
|
-
(pf / "docker-compose.yml").write_text("services: {}\n")
|
|
493
|
-
return str(pf)
|
|
494
|
-
|
|
495
|
-
def test_start_success(self, tmp_path, log_output):
|
|
496
|
-
from processforge.compose import start_provider_containers
|
|
497
|
-
|
|
498
|
-
pf = self._write_compose(tmp_path)
|
|
499
|
-
with patch("processforge.compose.subprocess.run") as mock_run:
|
|
500
|
-
mock_run.return_value = subprocess.CompletedProcess([], 0, "", "")
|
|
501
|
-
assert start_provider_containers(pf) is True
|
|
502
|
-
mock_run.assert_called_once()
|
|
503
|
-
assert any("Started provider container" in m for m in log_output)
|
|
504
|
-
|
|
505
|
-
def test_start_no_compose(self, tmp_path):
|
|
506
|
-
from processforge.compose import start_provider_containers
|
|
507
|
-
|
|
508
|
-
assert start_provider_containers(str(tmp_path / "nope")) is False
|
|
509
|
-
|
|
510
|
-
def test_start_docker_missing(self, tmp_path, log_output):
|
|
511
|
-
from processforge.compose import start_provider_containers
|
|
512
|
-
|
|
513
|
-
pf = self._write_compose(tmp_path)
|
|
514
|
-
with patch("processforge.compose.subprocess.run", side_effect=FileNotFoundError):
|
|
515
|
-
assert start_provider_containers(pf) is False
|
|
516
|
-
assert any("Docker not found" in m for m in log_output)
|
|
517
|
-
|
|
518
|
-
def test_start_compose_failure(self, tmp_path, log_output):
|
|
519
|
-
from processforge.compose import start_provider_containers
|
|
520
|
-
|
|
521
|
-
pf = self._write_compose(tmp_path)
|
|
522
|
-
with patch("processforge.compose.subprocess.run") as mock_run:
|
|
523
|
-
mock_run.return_value = subprocess.CompletedProcess([], 1, "", "boom")
|
|
524
|
-
assert start_provider_containers(pf) is False
|
|
525
|
-
assert any("docker compose up failed" in m for m in log_output)
|
|
526
|
-
|
|
527
|
-
def test_stop_success(self, tmp_path, log_output):
|
|
528
|
-
from processforge.compose import stop_provider_containers
|
|
529
|
-
|
|
530
|
-
pf = self._write_compose(tmp_path)
|
|
531
|
-
with patch("processforge.compose.subprocess.run") as mock_run:
|
|
532
|
-
mock_run.return_value = subprocess.CompletedProcess([], 0, "", "")
|
|
533
|
-
stop_provider_containers(pf)
|
|
534
|
-
mock_run.assert_called_once()
|
|
535
|
-
assert any("Stopped provider container" in m for m in log_output)
|
|
536
|
-
|
|
537
|
-
def test_stop_no_compose(self, tmp_path):
|
|
538
|
-
from processforge.compose import stop_provider_containers
|
|
539
|
-
|
|
540
|
-
with patch("processforge.compose.subprocess.run") as mock_run:
|
|
541
|
-
stop_provider_containers(str(tmp_path / "nope"))
|
|
542
|
-
mock_run.assert_not_called()
|
|
543
|
-
|
|
544
444
|
|
|
545
445
|
class TestCmdApply:
|
|
546
446
|
def _containerized_config(self, flowsheet_path):
|
|
@@ -560,7 +460,7 @@ class TestCmdApply:
|
|
|
560
460
|
mock_os.path.join = real_os.path.join
|
|
561
461
|
return mock_os
|
|
562
462
|
|
|
563
|
-
def
|
|
463
|
+
def test_containerized_flowsheet_runs_without_lifecycle(self, tmp_path, log_output):
|
|
564
464
|
import processforge.cli.apply as apply_mod
|
|
565
465
|
from processforge.cli.apply import apply
|
|
566
466
|
|
|
@@ -576,9 +476,6 @@ class TestCmdApply:
|
|
|
576
476
|
mock_fs.solver_stats = {"final_norm": 0.0}
|
|
577
477
|
|
|
578
478
|
with patch.object(apply_mod, "os", self._patch_apply_os(apply_mod)), \
|
|
579
|
-
patch.object(apply_mod, "start_provider_containers", return_value=True) as mock_start, \
|
|
580
|
-
patch.object(apply_mod, "stop_provider_containers") as mock_stop, \
|
|
581
|
-
patch.object(apply_mod, "wait_for_provider_health"), \
|
|
582
479
|
patch.object(apply_mod, "validate_runtime_flowsheet",
|
|
583
480
|
return_value=self._containerized_config(flowsheet)), \
|
|
584
481
|
patch.object(apply_mod, "check_providers"), \
|
|
@@ -587,39 +484,6 @@ class TestCmdApply:
|
|
|
587
484
|
patch.object(apply_mod, "save_results_zarr"):
|
|
588
485
|
apply(flowsheet=str(flowsheet))
|
|
589
486
|
|
|
590
|
-
mock_start.assert_called_once()
|
|
591
|
-
mock_stop.assert_called_once()
|
|
592
|
-
|
|
593
|
-
def test_keep_containers_skips_stop(self, tmp_path):
|
|
594
|
-
import processforge.cli.apply as apply_mod
|
|
595
|
-
from processforge.cli.apply import apply
|
|
596
|
-
|
|
597
|
-
flowsheet = tmp_path / "fs.json"
|
|
598
|
-
flowsheet.write_text(json.dumps(self._containerized_config(flowsheet)["providers"]))
|
|
599
|
-
|
|
600
|
-
mock_fs = MagicMock()
|
|
601
|
-
mock_fs.run.return_value = {}
|
|
602
|
-
mock_fs.converged = True
|
|
603
|
-
mock_fs.x0 = []
|
|
604
|
-
mock_fs.x_converged = []
|
|
605
|
-
mock_fs.var_names = []
|
|
606
|
-
mock_fs.solver_stats = {"final_norm": 0.0}
|
|
607
|
-
|
|
608
|
-
with patch.object(apply_mod, "os", self._patch_apply_os(apply_mod)), \
|
|
609
|
-
patch.object(apply_mod, "start_provider_containers", return_value=True) as mock_start, \
|
|
610
|
-
patch.object(apply_mod, "stop_provider_containers") as mock_stop, \
|
|
611
|
-
patch.object(apply_mod, "wait_for_provider_health"), \
|
|
612
|
-
patch.object(apply_mod, "validate_runtime_flowsheet",
|
|
613
|
-
return_value=self._containerized_config(flowsheet)), \
|
|
614
|
-
patch.object(apply_mod, "check_providers"), \
|
|
615
|
-
patch.object(apply_mod, "EOFlowsheet", return_value=mock_fs), \
|
|
616
|
-
patch.object(apply_mod, "save_snapshot", return_value="snap-1"), \
|
|
617
|
-
patch.object(apply_mod, "save_results_zarr"):
|
|
618
|
-
apply(flowsheet=str(flowsheet), keep_containers=True)
|
|
619
|
-
|
|
620
|
-
mock_start.assert_called_once()
|
|
621
|
-
mock_stop.assert_not_called()
|
|
622
|
-
|
|
623
487
|
def test_coolprop_flowsheet_no_lifecycle(self, tmp_path):
|
|
624
488
|
import processforge.cli.apply as apply_mod
|
|
625
489
|
from processforge.cli.apply import apply
|
|
@@ -638,9 +502,6 @@ class TestCmdApply:
|
|
|
638
502
|
# A stale docker-compose.yml is present, but the flowsheet only uses
|
|
639
503
|
# pip providers — Docker must never be touched.
|
|
640
504
|
with patch.object(apply_mod, "os", self._patch_apply_os(apply_mod)), \
|
|
641
|
-
patch.object(apply_mod, "start_provider_containers") as mock_start, \
|
|
642
|
-
patch.object(apply_mod, "stop_provider_containers") as mock_stop, \
|
|
643
|
-
patch.object(apply_mod, "wait_for_provider_health"), \
|
|
644
505
|
patch.object(apply_mod, "validate_runtime_flowsheet",
|
|
645
506
|
return_value={"providers": {"coolprop": {"type": "coolprop"}},
|
|
646
507
|
"streams": {}, "units": {},
|
|
@@ -652,9 +513,6 @@ class TestCmdApply:
|
|
|
652
513
|
patch.object(apply_mod, "save_results_zarr"):
|
|
653
514
|
apply(flowsheet=str(flowsheet))
|
|
654
515
|
|
|
655
|
-
mock_start.assert_not_called()
|
|
656
|
-
mock_stop.assert_not_called()
|
|
657
|
-
|
|
658
516
|
|
|
659
517
|
# ---------------------------------------------------------------------------
|
|
660
518
|
# cli/diagram.py
|