processforge 0.3.21__tar.gz → 0.3.23__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.21/src/processforge.egg-info → processforge-0.3.23}/PKG-INFO +2 -1
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/hydraulic-chain.json +1 -1
- {processforge-0.3.21 → processforge-0.3.23}/pyproject.toml +2 -1
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/__init__.py +1 -1
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/apply.py +3 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/common.py +105 -34
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/run.py +38 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/runs.py +13 -21
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/output_collector.py +17 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/container_client.py +13 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/coolprop_provider.py +12 -3
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/state.py +61 -27
- {processforge-0.3.21 → processforge-0.3.23/src/processforge.egg-info}/PKG-INFO +2 -1
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge.egg-info/requires.txt +1 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_cli.py +53 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_runs_command.py +24 -1
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_state.py +95 -0
- {processforge-0.3.21 → processforge-0.3.23}/LICENSE +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/MANIFEST.in +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/README.md +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/setup.cfg +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/__main__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/compose.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/coupling.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/lock.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/provenance.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/quantity.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/result.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/simulate.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/solver.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/thermo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/types.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge.egg-info/SOURCES.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_container_client.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_coupling.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_init.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_providers.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.21 → processforge-0.3.23}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processforge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.23
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: zarr>=3.0
|
|
|
31
31
|
Requires-Dist: pint<1.0,>=0.24
|
|
32
32
|
Requires-Dist: coolprop>=7.2.0
|
|
33
33
|
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: arrow>=1.3
|
|
34
35
|
Provides-Extra: api
|
|
35
36
|
Requires-Dist: fastapi>=0.110; extra == "api"
|
|
36
37
|
Requires-Dist: uvicorn>=0.29; extra == "api"
|
|
@@ -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.23"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -37,6 +37,7 @@ dependencies = [
|
|
|
37
37
|
"pint>=0.24,<1.0",
|
|
38
38
|
"coolprop>=7.2.0",
|
|
39
39
|
"typer>=0.12",
|
|
40
|
+
"arrow>=1.3",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.optional-dependencies]
|
|
@@ -133,11 +133,14 @@ def apply(
|
|
|
133
133
|
return
|
|
134
134
|
stream_drifts = [d for d in drifted if d.startswith("streams.")]
|
|
135
135
|
unit_drifts = [d for d in drifted if d.startswith("units.")]
|
|
136
|
+
other_drifts = [d for d in drifted if not d.startswith(("streams.", "units."))]
|
|
136
137
|
logger.warning("Drift detected:")
|
|
137
138
|
if stream_drifts:
|
|
138
139
|
logger.warning(f" Stream drift : {stream_drifts}")
|
|
139
140
|
if unit_drifts:
|
|
140
141
|
logger.warning(f" Unit drift : {unit_drifts}")
|
|
142
|
+
if other_drifts:
|
|
143
|
+
logger.warning(f" Config drift : {other_drifts}")
|
|
141
144
|
|
|
142
145
|
# Build flowsheet; attach saved state for warm-start unless topology changed
|
|
143
146
|
fs = EOFlowsheet(config, backend=backend)
|
|
@@ -145,6 +145,13 @@ def load_state_manager(outputs_dir: str, base_name: str) -> tuple[ProcessStateAr
|
|
|
145
145
|
# Provider checking
|
|
146
146
|
# ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
|
+
# Retry/poll settings for containerized provider health checks. A container
|
|
149
|
+
# spun up by ``pf init`` may take a few seconds to become ready; poll rather
|
|
150
|
+
# than failing immediately so ``pf run`` waits for readiness with visible text.
|
|
151
|
+
HEALTH_MAX_ATTEMPTS = 6
|
|
152
|
+
HEALTH_RETRY_DELAY = 5 # seconds between attempts
|
|
153
|
+
|
|
154
|
+
|
|
148
155
|
def is_local_provider_url(url: str | None) -> bool:
|
|
149
156
|
"""Return True if *url* points at a locally-managed provider.
|
|
150
157
|
|
|
@@ -184,6 +191,85 @@ def _ping_provider_health(url: str, timeout: int = 5) -> tuple[bool, "dict | str
|
|
|
184
191
|
return False, str(exc)
|
|
185
192
|
|
|
186
193
|
|
|
194
|
+
def _check_container_provider(
|
|
195
|
+
name: str,
|
|
196
|
+
ptype: str,
|
|
197
|
+
url: str,
|
|
198
|
+
flowsheet_path: str,
|
|
199
|
+
*,
|
|
200
|
+
fail_fast: bool,
|
|
201
|
+
errors: list[str],
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Probe a containerized provider's ``/health`` endpoint with a retry/poll.
|
|
204
|
+
|
|
205
|
+
Polls up to ``HEALTH_MAX_ATTEMPTS`` times (``HEALTH_RETRY_DELAY`` apart) so a
|
|
206
|
+
container that is still booting after ``pf init`` gets a chance to become
|
|
207
|
+
ready before we give up. Logs a visible ``[OK]``/``[ERR]`` line (matching the
|
|
208
|
+
``pf plan`` convention) so ``pf run`` never silently waits.
|
|
209
|
+
"""
|
|
210
|
+
import time
|
|
211
|
+
|
|
212
|
+
ok, info = False, "not probed"
|
|
213
|
+
for attempt in range(1, HEALTH_MAX_ATTEMPTS + 1):
|
|
214
|
+
ok, info = _ping_provider_health(url, timeout=5)
|
|
215
|
+
if ok:
|
|
216
|
+
break
|
|
217
|
+
if attempt < HEALTH_MAX_ATTEMPTS:
|
|
218
|
+
logger.info(
|
|
219
|
+
f" Waiting for '{name}' container to become healthy at {url}… "
|
|
220
|
+
f"(attempt {attempt}/{HEALTH_MAX_ATTEMPTS})"
|
|
221
|
+
)
|
|
222
|
+
time.sleep(HEALTH_RETRY_DELAY)
|
|
223
|
+
|
|
224
|
+
if ok:
|
|
225
|
+
payload = info if isinstance(info, dict) else {}
|
|
226
|
+
status = payload.get("status", "?")
|
|
227
|
+
provider_type = payload.get("provider_type", "?")
|
|
228
|
+
logger.info(
|
|
229
|
+
f" [OK] {name} [{ptype}] {url} — status={status} provider_type={provider_type}"
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
msg = (
|
|
234
|
+
f" [ERR] {name} [{ptype}] {url} — unreachable after "
|
|
235
|
+
f"{HEALTH_MAX_ATTEMPTS} attempt(s): {info}. "
|
|
236
|
+
f"Run: pf init {flowsheet_path}"
|
|
237
|
+
)
|
|
238
|
+
logger.error(msg)
|
|
239
|
+
if fail_fast:
|
|
240
|
+
raise SystemExit(1)
|
|
241
|
+
errors.append(f"Provider '{name}' unreachable at {url}: {info}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _check_pip_provider(
|
|
245
|
+
name: str,
|
|
246
|
+
ptype: str,
|
|
247
|
+
flowsheet_path: str,
|
|
248
|
+
*,
|
|
249
|
+
fail_fast: bool,
|
|
250
|
+
errors: list[str],
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Verify a pip-installable provider's module is importable."""
|
|
253
|
+
from ..providers.registry import _PROVIDER_CATALOG
|
|
254
|
+
|
|
255
|
+
catalog = _PROVIDER_CATALOG.get(ptype, {})
|
|
256
|
+
module = catalog.get("module", "")
|
|
257
|
+
try:
|
|
258
|
+
importlib.util.find_spec(module)
|
|
259
|
+
logger.info(f" [OK] {name} [{ptype}] (pip — importable)")
|
|
260
|
+
except (ModuleNotFoundError, ValueError):
|
|
261
|
+
dep = catalog.get("optional_dep")
|
|
262
|
+
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
263
|
+
msg = (
|
|
264
|
+
f" [WARN] {name} [{ptype}] — not installed. "
|
|
265
|
+
f"Run: pf init {flowsheet_path} (install with: {hint})"
|
|
266
|
+
)
|
|
267
|
+
logger.warning(msg)
|
|
268
|
+
if fail_fast:
|
|
269
|
+
raise SystemExit(1)
|
|
270
|
+
errors.append(f"Provider '{name}' not installed ({hint})")
|
|
271
|
+
|
|
272
|
+
|
|
187
273
|
def check_providers(
|
|
188
274
|
config: dict,
|
|
189
275
|
flowsheet_path: str,
|
|
@@ -193,56 +279,41 @@ def check_providers(
|
|
|
193
279
|
) -> None:
|
|
194
280
|
"""Verify all declared providers are reachable or importable.
|
|
195
281
|
|
|
282
|
+
For containerized providers this pings the container's ``/health`` endpoint
|
|
283
|
+
(with a short retry/poll so a still-booting container can become ready) and
|
|
284
|
+
logs a visible ``[OK]``/``[ERR]`` line — so ``pf run`` shows a clear health
|
|
285
|
+
check instead of silently waiting. Pip-installable providers are checked for
|
|
286
|
+
importability and reported the same way.
|
|
287
|
+
|
|
196
288
|
Parameters
|
|
197
289
|
----------
|
|
198
290
|
fail_fast:
|
|
199
291
|
If ``True`` (default), raise ``SystemExit`` on the first failure.
|
|
200
292
|
If ``False``, accumulate errors and raise once at the end.
|
|
201
293
|
verbose:
|
|
202
|
-
|
|
294
|
+
Accepted for compatibility; the health check is always logged.
|
|
203
295
|
"""
|
|
204
|
-
from ..providers.registry import is_containerized
|
|
296
|
+
from ..providers.registry import is_containerized
|
|
205
297
|
|
|
206
298
|
providers = config.get("providers", {})
|
|
207
299
|
errors: list[str] = []
|
|
208
300
|
|
|
301
|
+
if providers:
|
|
302
|
+
logger.info("=== Provider / Container Health ===")
|
|
303
|
+
|
|
209
304
|
for name, cfg in providers.items():
|
|
210
305
|
ptype = cfg.get("type", "")
|
|
211
306
|
if is_containerized(ptype):
|
|
212
307
|
url = _resolve_provider_url(cfg, ptype)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
except (urllib.error.URLError, OSError, TimeoutError) as exc:
|
|
218
|
-
msg = (
|
|
219
|
-
f"Provider '{name}' unreachable at {url}. "
|
|
220
|
-
f"Run: pf init {flowsheet_path}"
|
|
221
|
-
)
|
|
222
|
-
if fail_fast:
|
|
223
|
-
logger.error(msg)
|
|
224
|
-
raise SystemExit(1)
|
|
225
|
-
logger.error(msg)
|
|
226
|
-
errors.append(msg)
|
|
308
|
+
_check_container_provider(
|
|
309
|
+
name, ptype, url, flowsheet_path,
|
|
310
|
+
fail_fast=fail_fast, errors=errors,
|
|
311
|
+
)
|
|
227
312
|
else:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if verbose:
|
|
233
|
-
logger.info(f"Provider '{name}' — importable (pip)")
|
|
234
|
-
except (ModuleNotFoundError, ValueError):
|
|
235
|
-
dep = catalog.get("optional_dep")
|
|
236
|
-
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
237
|
-
msg = (
|
|
238
|
-
f"Provider '{name}' not installed. "
|
|
239
|
-
f"Run: pf init {flowsheet_path} (install with: {hint})"
|
|
240
|
-
)
|
|
241
|
-
if fail_fast:
|
|
242
|
-
logger.error(msg)
|
|
243
|
-
raise SystemExit(1)
|
|
244
|
-
logger.error(msg)
|
|
245
|
-
errors.append(msg)
|
|
313
|
+
_check_pip_provider(
|
|
314
|
+
name, ptype, flowsheet_path,
|
|
315
|
+
fail_fast=fail_fast, errors=errors,
|
|
316
|
+
)
|
|
246
317
|
|
|
247
318
|
if errors:
|
|
248
319
|
raise SystemExit(1)
|
|
@@ -19,6 +19,7 @@ from .common import (
|
|
|
19
19
|
output_root,
|
|
20
20
|
require_existing_file,
|
|
21
21
|
validate_runtime_flowsheet,
|
|
22
|
+
_resolve_provider_url,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
@@ -38,6 +39,39 @@ def _run_id() -> str:
|
|
|
38
39
|
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def _log_active_providers(config: dict) -> None:
|
|
43
|
+
"""Log a one-line summary of the provider(s) a run will dispatch to.
|
|
44
|
+
|
|
45
|
+
Highlights containerized providers (which do their real work inside a Docker
|
|
46
|
+
container and can take a while) so the user sees what is running instead of
|
|
47
|
+
a silent wait. Pip-installable providers are mentioned for completeness.
|
|
48
|
+
"""
|
|
49
|
+
from ..providers.registry import is_containerized
|
|
50
|
+
|
|
51
|
+
providers = config.get("providers", {})
|
|
52
|
+
if not providers:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
containers = []
|
|
56
|
+
pips = []
|
|
57
|
+
for name, cfg in providers.items():
|
|
58
|
+
ptype = cfg.get("type", "")
|
|
59
|
+
if is_containerized(ptype):
|
|
60
|
+
url = _resolve_provider_url(cfg, ptype)
|
|
61
|
+
containers.append(f"{name} [{ptype}] @ {url}")
|
|
62
|
+
else:
|
|
63
|
+
pips.append(f"{name} [{ptype}]")
|
|
64
|
+
|
|
65
|
+
if containers:
|
|
66
|
+
logger.info("=== Active providers (containerized) ===")
|
|
67
|
+
for entry in containers:
|
|
68
|
+
logger.info(f" → {entry}")
|
|
69
|
+
if pips:
|
|
70
|
+
logger.info("=== Active providers (local) ===")
|
|
71
|
+
for entry in pips:
|
|
72
|
+
logger.info(f" → {entry}")
|
|
73
|
+
|
|
74
|
+
|
|
41
75
|
def run(
|
|
42
76
|
flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
|
|
43
77
|
export_images: bool = typer.Option(
|
|
@@ -53,6 +87,10 @@ def run(
|
|
|
53
87
|
# Check provider availability (assumes any containers are already running)
|
|
54
88
|
check_providers(config, flowsheet)
|
|
55
89
|
|
|
90
|
+
# Surface which containerized provider(s) this run will dispatch to, so the
|
|
91
|
+
# command line isn't silent while the container does the heavy compute.
|
|
92
|
+
_log_active_providers(config)
|
|
93
|
+
|
|
56
94
|
base_name = flowsheet_basename(flowsheet)
|
|
57
95
|
outputs_dir = output_root()
|
|
58
96
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
|
|
8
|
+
import arrow
|
|
8
9
|
import typer
|
|
9
10
|
from loguru import logger
|
|
10
11
|
|
|
@@ -12,20 +13,16 @@ from .common import flowsheet_basename, output_root
|
|
|
12
13
|
from ..persistence.archive import ProcessStateArchive
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def
|
|
16
|
-
"""
|
|
17
|
-
parts = []
|
|
18
|
-
for unit_name, out in manifest.units.items():
|
|
19
|
-
for f in out.fields:
|
|
20
|
-
val = f.quantity.value
|
|
21
|
-
unit = f.quantity.unit or ""
|
|
22
|
-
parts.append(f"{unit_name}.{f.name}={val}{(' ' + unit) if unit else ''}")
|
|
23
|
-
return " ".join(parts) if parts else "-"
|
|
16
|
+
def _humanize_timestamp(ts: str) -> str:
|
|
17
|
+
"""Render a stored timestamp as a human-readable relative string.
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
Falls back to the raw string for values that aren't parseable ISO-8601
|
|
20
|
+
(e.g. ``20260101T000000Z`` used in some test fixtures).
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
return arrow.get(ts).humanize()
|
|
24
|
+
except (arrow.parser.ParserError, ValueError, TypeError):
|
|
25
|
+
return ts
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def runs(
|
|
@@ -73,10 +70,7 @@ def runs(
|
|
|
73
70
|
typer.echo(f"No runs found for '{flowsheet}'.")
|
|
74
71
|
return
|
|
75
72
|
|
|
76
|
-
header =
|
|
77
|
-
f"{'RUN ID':<28} {'TIMESTAMP':<22} {'MODE':<8} "
|
|
78
|
-
f"{'ZARR':<5} {'LATEST':<6} SUMMARY"
|
|
79
|
-
)
|
|
73
|
+
header = f"{'RUN ID':<28} {'TIMESTAMP':<22} LATEST"
|
|
80
74
|
typer.echo(header)
|
|
81
75
|
typer.echo("-" * len(header))
|
|
82
76
|
for rid in run_ids:
|
|
@@ -85,12 +79,10 @@ def runs(
|
|
|
85
79
|
continue
|
|
86
80
|
marker = "*" if rid == latest_id else " "
|
|
87
81
|
typer.echo(
|
|
88
|
-
f"{rid:<28} {manifest.timestamp:<22} {
|
|
89
|
-
f"{_zarr_status(archive_path, rid):<5} {marker:<6} "
|
|
90
|
-
f"{_summary_for(manifest)}"
|
|
82
|
+
f"{rid:<28} {_humanize_timestamp(manifest.timestamp):<22} {marker}"
|
|
91
83
|
)
|
|
92
84
|
typer.echo("")
|
|
93
85
|
typer.echo(
|
|
94
|
-
"
|
|
86
|
+
"'*' marks the latest run. "
|
|
95
87
|
"'pf runs <flowsheet> <run_id>' prints the full manifest."
|
|
96
88
|
)
|
|
@@ -14,8 +14,10 @@ canonical, unit-aware record persisted by :class:`ProcessStateArchive`:
|
|
|
14
14
|
"""
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from datetime import datetime, timezone
|
|
17
18
|
from typing import Optional
|
|
18
19
|
|
|
20
|
+
import arrow
|
|
19
21
|
from loguru import logger
|
|
20
22
|
|
|
21
23
|
from .types import (
|
|
@@ -75,6 +77,20 @@ def collect_stream_outputs(provider, stream_results: dict) -> dict[str, StreamOu
|
|
|
75
77
|
return out
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
def _timestamp_from_run_id(run_id: str) -> str:
|
|
81
|
+
"""Derive an ISO-8601 timestamp from a run_id's compact UTC prefix.
|
|
82
|
+
|
|
83
|
+
Run ids are ``<YYYYMMDDTHHMMSSZ>_<suffix>`` (see ``pf run`` / ``pf apply``),
|
|
84
|
+
so the start time is recoverable from the id itself. Falls back to the
|
|
85
|
+
current UTC time for ids that don't follow that convention.
|
|
86
|
+
"""
|
|
87
|
+
prefix = run_id.split("_", 1)[0]
|
|
88
|
+
try:
|
|
89
|
+
return arrow.get(prefix).isoformat()
|
|
90
|
+
except (arrow.parser.ParserError, ValueError):
|
|
91
|
+
return datetime.now(timezone.utc).isoformat()
|
|
92
|
+
|
|
93
|
+
|
|
78
94
|
def build_run_manifest(
|
|
79
95
|
*,
|
|
80
96
|
run_id: str,
|
|
@@ -91,6 +107,7 @@ def build_run_manifest(
|
|
|
91
107
|
run_id=run_id,
|
|
92
108
|
mode=mode,
|
|
93
109
|
flowsheet=flowsheet_name,
|
|
110
|
+
timestamp=_timestamp_from_run_id(run_id),
|
|
94
111
|
units=dict(engine_outputs),
|
|
95
112
|
streams=streams,
|
|
96
113
|
provenance=provenance or {},
|
|
@@ -177,6 +177,15 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
177
177
|
else {"type": unit_config.provider or self._ptype, "url": self._url}
|
|
178
178
|
)
|
|
179
179
|
|
|
180
|
+
# Fall back to context set via set_run_context() (the CLI propagates the
|
|
181
|
+
# shared run_id/flowsheet_hash there). SolverUnit calls run_simulation()
|
|
182
|
+
# without these arguments, so without the fallback the container receives
|
|
183
|
+
# null and artifacts get uploaded under the wrong key.
|
|
184
|
+
if run_id is None:
|
|
185
|
+
run_id = self._run_id or None
|
|
186
|
+
if flowsheet_hash is None:
|
|
187
|
+
flowsheet_hash = self._flowsheet_hash or None
|
|
188
|
+
|
|
180
189
|
body = {
|
|
181
190
|
"unit_config": self._serialize_unit_config(unit_config),
|
|
182
191
|
"materials": self._serialize_materials(self._materials),
|
|
@@ -190,6 +199,10 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
190
199
|
f"ContainerProviderClient: POST {self._url}/run "
|
|
191
200
|
f"sim_type='{unit_config.sim_type}' run_id='{run_id}'"
|
|
192
201
|
)
|
|
202
|
+
logger.info(
|
|
203
|
+
f"Waiting for '{self._ptype}' container at {self._url} to finish "
|
|
204
|
+
f"simulation — this may take a while…"
|
|
205
|
+
)
|
|
193
206
|
|
|
194
207
|
payload = json.dumps(body).encode()
|
|
195
208
|
req = urllib.request.Request(
|
|
@@ -56,11 +56,20 @@ class CoolPropProvider(AbstractProvider):
|
|
|
56
56
|
logger.warning(f"Component '{comp}' not found in CoolProp. Cp contribution is 0.")
|
|
57
57
|
|
|
58
58
|
Ks = {}
|
|
59
|
+
from CoolProp.CoolProp import AbstractState
|
|
60
|
+
|
|
59
61
|
for comp in z.keys():
|
|
60
62
|
try:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
st = AbstractState("PR", comp)
|
|
64
|
+
st.specify_phase(self.CP.iphase_liquid)
|
|
65
|
+
st.update(self.CP.PT_INPUTS, P, T)
|
|
66
|
+
phi_L = st.fugacity_coefficient(0)
|
|
67
|
+
st.unspecify_phase()
|
|
68
|
+
st.specify_phase(self.CP.iphase_gas)
|
|
69
|
+
st.update(self.CP.PT_INPUTS, P, T)
|
|
70
|
+
phi_V = st.fugacity_coefficient(0)
|
|
71
|
+
st.unspecify_phase()
|
|
72
|
+
Ks[comp] = phi_L / phi_V if phi_V != 0 else 1.0
|
|
64
73
|
except Exception:
|
|
65
74
|
logger.warning(f"Could not calculate K-value for '{comp}'. Using fallback K=1.0.")
|
|
66
75
|
Ks[comp] = 1.0
|
|
@@ -226,46 +226,80 @@ class StateManager:
|
|
|
226
226
|
# Drift & structural diff
|
|
227
227
|
# ------------------------------------------------------------------
|
|
228
228
|
|
|
229
|
+
# Keys treated as structural (handled by detect_structural_diff / cold
|
|
230
|
+
# start) rather than as drifted solve parameters.
|
|
231
|
+
_STRUCTURAL_UNIT_KEYS = {"type", "in", "out", "out_vap", "out_liq", "provider"}
|
|
232
|
+
|
|
233
|
+
# Top-level sections that never affect the solve and must be ignored when
|
|
234
|
+
# computing drift (cosmetic metadata, and the runtime-injected source path
|
|
235
|
+
# which is only present on the live config).
|
|
236
|
+
_DRIFT_SKIP_TOP_LEVEL = {"metadata", "_config_path"}
|
|
237
|
+
|
|
229
238
|
def detect_drift(self, current_config: dict, state: SnapshotState | dict) -> list[str]:
|
|
230
239
|
"""Return parameter paths that differ between ``current_config`` and saved state.
|
|
231
240
|
|
|
241
|
+
Recursively diffs the entire config so any solve-affecting change is
|
|
242
|
+
detected — streams, units (incl. nested ``solver_config`` /
|
|
243
|
+
``geometry_config`` / ``parameters``), materials, material mixes,
|
|
244
|
+
providers and simulation settings. Structural unit keys (``type``,
|
|
245
|
+
``in``, ``out``, ``provider``, …) and the cosmetic ``metadata`` /
|
|
246
|
+
``_config_path`` sections are excluded so topology changes remain the
|
|
247
|
+
job of :meth:`detect_structural_diff` / cold start.
|
|
248
|
+
|
|
232
249
|
Paths look like ``"streams.feed.T"``, ``"units.pump_1.delta_p"``,
|
|
233
|
-
|
|
250
|
+
``"units.openmc_solver.solver_config.batches"`` or
|
|
251
|
+
``"materials.salt.density"``.
|
|
234
252
|
"""
|
|
235
253
|
old_config = state.config if isinstance(state, SnapshotState) else state["config"]
|
|
236
254
|
drifted: list[str] = []
|
|
237
255
|
|
|
238
|
-
for
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
for section in set(old_config) | set(current_config):
|
|
257
|
+
if section in self._DRIFT_SKIP_TOP_LEVEL:
|
|
258
|
+
continue
|
|
259
|
+
old_section = old_config.get(section)
|
|
260
|
+
new_section = current_config.get(section)
|
|
261
|
+
|
|
262
|
+
if section == "units":
|
|
263
|
+
# Diff each unit subtree, skipping structural keys.
|
|
264
|
+
old_units = old_section or {}
|
|
265
|
+
new_units = new_section or {}
|
|
266
|
+
for u_name in set(old_units) | set(new_units):
|
|
267
|
+
drifted.extend(
|
|
268
|
+
self._recursive_diff(
|
|
269
|
+
old_units.get(u_name, {}),
|
|
270
|
+
new_units.get(u_name, {}),
|
|
271
|
+
prefix=f"units.{u_name}.",
|
|
272
|
+
skip=self._STRUCTURAL_UNIT_KEYS,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
drifted.extend(
|
|
277
|
+
self._recursive_diff(old_section, new_section, prefix=f"{section}.", skip=set())
|
|
278
|
+
)
|
|
249
279
|
|
|
250
|
-
|
|
280
|
+
return drifted
|
|
251
281
|
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _recursive_diff(old: object, new: object, prefix: str, skip: set[str]) -> list[str]:
|
|
284
|
+
"""Recursively collect changed leaf paths between two (sub)configs."""
|
|
285
|
+
changes: list[str] = []
|
|
254
286
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
287
|
+
if isinstance(old, dict) and isinstance(new, dict):
|
|
288
|
+
for k in (set(old) | set(new)) - skip:
|
|
289
|
+
changes.extend(
|
|
290
|
+
StateManager._recursive_diff(old.get(k), new.get(k), f"{prefix}{k}.", skip)
|
|
291
|
+
)
|
|
292
|
+
return changes
|
|
260
293
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if cur_params.get(pk) != old_params.get(pk):
|
|
266
|
-
drifted.append(f"units.{u_name}.parameters.{pk}")
|
|
294
|
+
if isinstance(old, list) and isinstance(new, list):
|
|
295
|
+
if old != new:
|
|
296
|
+
changes.append(prefix.rstrip("."))
|
|
297
|
+
return changes
|
|
267
298
|
|
|
268
|
-
|
|
299
|
+
# Leaf / type-mismatch comparison.
|
|
300
|
+
if old != new:
|
|
301
|
+
changes.append(prefix.rstrip("."))
|
|
302
|
+
return changes
|
|
269
303
|
|
|
270
304
|
def detect_structural_diff(self, current_config: dict, state: SnapshotState | dict) -> dict:
|
|
271
305
|
"""Compare unit topology between ``current_config`` and saved state.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processforge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.23
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: zarr>=3.0
|
|
|
31
31
|
Requires-Dist: pint<1.0,>=0.24
|
|
32
32
|
Requires-Dist: coolprop>=7.2.0
|
|
33
33
|
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: arrow>=1.3
|
|
34
35
|
Provides-Extra: api
|
|
35
36
|
Requires-Dist: fastapi>=0.110; extra == "api"
|
|
36
37
|
Requires-Dist: uvicorn>=0.29; extra == "api"
|
|
@@ -487,6 +487,59 @@ class TestCmdPlan:
|
|
|
487
487
|
assert any("[ERR] openmc" in m and "unreachable" in m for m in log_output)
|
|
488
488
|
|
|
489
489
|
|
|
490
|
+
class TestCheckProviders:
|
|
491
|
+
"""Health-check + retry/poll behavior used by ``pf run``/``pf apply``."""
|
|
492
|
+
|
|
493
|
+
def _config(self, **providers):
|
|
494
|
+
return {"providers": providers}
|
|
495
|
+
|
|
496
|
+
def test_container_health_ok_logs_ok(self, log_output):
|
|
497
|
+
import processforge.cli.common as common_mod
|
|
498
|
+
from processforge.cli.common import check_providers
|
|
499
|
+
|
|
500
|
+
health = {"status": "ready", "provider_type": "openmc"}
|
|
501
|
+
with patch.object(
|
|
502
|
+
common_mod, "_ping_provider_health", return_value=(True, health)
|
|
503
|
+
), patch.object(common_mod, "_resolve_provider_url", return_value="http://localhost:9001"):
|
|
504
|
+
check_providers(self._config(openmc={"type": "openmc"}), "fs.json")
|
|
505
|
+
|
|
506
|
+
assert any("=== Provider / Container Health ===" in m for m in log_output)
|
|
507
|
+
assert any("[OK] openmc" in m and "status=ready" in m for m in log_output)
|
|
508
|
+
assert not any("Waiting for" in m for m in log_output)
|
|
509
|
+
|
|
510
|
+
def test_container_health_retries_then_fails(self, log_output):
|
|
511
|
+
import processforge.cli.common as common_mod
|
|
512
|
+
from processforge.cli.common import check_providers
|
|
513
|
+
|
|
514
|
+
with patch.object(
|
|
515
|
+
common_mod, "_ping_provider_health", return_value=(False, "Connection refused")
|
|
516
|
+
), patch.object(common_mod, "_resolve_provider_url", return_value="http://localhost:9001"), \
|
|
517
|
+
patch.object(common_mod, "HEALTH_MAX_ATTEMPTS", 3), \
|
|
518
|
+
patch.object(common_mod, "HEALTH_RETRY_DELAY", 0), \
|
|
519
|
+
patch("time.sleep", lambda *_: None), \
|
|
520
|
+
pytest.raises(SystemExit) as exc_info:
|
|
521
|
+
check_providers(self._config(openmc={"type": "openmc"}), "fs.json")
|
|
522
|
+
|
|
523
|
+
assert exc_info.value.code == 1
|
|
524
|
+
# Two "Waiting for …" poll messages (attempts 1 and 2 of 3) before failure.
|
|
525
|
+
wait_msgs = [m for m in log_output if "Waiting for" in m and "openmc" in m]
|
|
526
|
+
assert len(wait_msgs) == 2
|
|
527
|
+
assert any("[ERR] openmc" in m and "unreachable after 3" in m for m in log_output)
|
|
528
|
+
|
|
529
|
+
def test_pip_provider_logs_ok(self, log_output):
|
|
530
|
+
import processforge.providers.registry as registry_mod
|
|
531
|
+
from processforge.cli.common import check_providers
|
|
532
|
+
|
|
533
|
+
with patch.object(
|
|
534
|
+
registry_mod, "_PROVIDER_CATALOG",
|
|
535
|
+
{"coolprop": {"module": "processforge.providers.coolprop_provider",
|
|
536
|
+
"optional_dep": None}},
|
|
537
|
+
):
|
|
538
|
+
check_providers(self._config(coolprop={"type": "coolprop"}), "fs.json")
|
|
539
|
+
|
|
540
|
+
assert any("[OK] coolprop" in m and "(pip — importable)" in m for m in log_output)
|
|
541
|
+
|
|
542
|
+
|
|
490
543
|
class TestCmdApply:
|
|
491
544
|
def _containerized_config(self, flowsheet_path):
|
|
492
545
|
return {
|