processforge 0.3.25__tar.gz → 0.3.26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {processforge-0.3.25/src/processforge.egg-info → processforge-0.3.26}/PKG-INFO +1 -1
- {processforge-0.3.25 → processforge-0.3.26}/pyproject.toml +1 -1
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/__init__.py +1 -1
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/apply.py +21 -50
- processforge-0.3.26/src/processforge/cli/persist.py +99 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/run.py +6 -43
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/flowsheet.py +46 -12
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/solver.py +104 -65
- {processforge-0.3.25 → processforge-0.3.26/src/processforge.egg-info}/PKG-INFO +1 -1
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge.egg-info/SOURCES.txt +2 -0
- processforge-0.3.26/tests/test_persist.py +73 -0
- {processforge-0.3.25 → processforge-0.3.26}/LICENSE +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/MANIFEST.in +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/README.md +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/setup.cfg +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/__main__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/common.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/runs.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/compose.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/coupling.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/lock.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/output_collector.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/provenance.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/quantity.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/result.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/simulate.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/solver.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/state.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/thermo.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/types.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/base.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge.egg-info/requires.txt +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_cli.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_container_client.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_coupling.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_display.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_init.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_providers.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_runs_command.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/tests/test_state.py +0 -0
- {processforge-0.3.25 → processforge-0.3.26}/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.26"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -6,46 +6,10 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
-
import hashlib
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
9
|
import typer
|
|
13
|
-
from datetime import datetime, timezone
|
|
14
10
|
from loguru import logger
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id):
|
|
18
|
-
"""Build a standardized RunManifest and persist it to the archive."""
|
|
19
|
-
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
|
|
20
|
-
flowsheet_hash = hashlib.sha256(
|
|
21
|
-
json.dumps(config, sort_keys=True, default=str).encode("utf-8")
|
|
22
|
-
).hexdigest()[:16]
|
|
23
|
-
manifest = fs.collect_outputs(
|
|
24
|
-
run_id, config.get("simulation", {}).get("mode", "steady"),
|
|
25
|
-
base_name, provenance=run_info.model_dump(),
|
|
26
|
-
)
|
|
27
|
-
manifest.snapshot_id = snapshot_id
|
|
28
|
-
stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
|
|
29
|
-
archive.save_run(manifest, stream_results=stream_results)
|
|
30
|
-
|
|
31
|
-
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
32
|
-
# inside the archive, mirroring `pf run`.
|
|
33
|
-
try:
|
|
34
|
-
from ..result import relink_latest_results, save_results_zarr
|
|
35
|
-
|
|
36
|
-
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
37
|
-
save_results_zarr(
|
|
38
|
-
results,
|
|
39
|
-
os.path.join(run_results_dir, "results.zarr"),
|
|
40
|
-
run_info,
|
|
41
|
-
)
|
|
42
|
-
relink_latest_results(archive.path, run_id)
|
|
43
|
-
except Exception as e:
|
|
44
|
-
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
45
|
-
|
|
46
|
-
logger.info(f"Run saved : {run_id}")
|
|
47
|
-
return run_id
|
|
48
|
-
|
|
12
|
+
from .persist import make_run_id, persist_run
|
|
49
13
|
from ..eo import EOFlowsheet
|
|
50
14
|
from ..provenance import build_run_info
|
|
51
15
|
from .common import (
|
|
@@ -158,14 +122,15 @@ def apply(
|
|
|
158
122
|
label="converged state",
|
|
159
123
|
)
|
|
160
124
|
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
161
|
-
|
|
125
|
+
run_id = make_run_id()
|
|
126
|
+
persist_run(sm, fs, run_id, results, run_info, config, base_name, snapshot_id)
|
|
162
127
|
logger.info("=== Apply Summary ===")
|
|
163
128
|
logger.info(" Status : CONVERGED")
|
|
164
129
|
logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
|
|
165
130
|
logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
|
|
166
131
|
logger.info(f" Backend : {display_backend(config, fs.backend)}")
|
|
167
132
|
logger.info(f" Snapshot ID : {snapshot_id}")
|
|
168
|
-
logger.info(f" Run ID : {
|
|
133
|
+
logger.info(f" Run ID : {run_id}")
|
|
169
134
|
logger.info(f" Elapsed (s) : {elapsed:.2f}")
|
|
170
135
|
return
|
|
171
136
|
|
|
@@ -180,25 +145,31 @@ def apply(
|
|
|
180
145
|
tmp_fs = _EO(config, backend=backend)
|
|
181
146
|
manager = tmp_fs._build()
|
|
182
147
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
148
|
+
homotopy_result = solve_with_homotopy(tmp_fs, manager, solver, state, drifted)
|
|
149
|
+
if homotopy_result.converged:
|
|
150
|
+
# Assemble the real (converged) outputs from the homotopy
|
|
151
|
+
# solution while the providers are still alive. The `results`
|
|
152
|
+
# from the earlier failed direct solve must NOT be persisted.
|
|
153
|
+
tmp_fs.assemble_from_solution(
|
|
154
|
+
manager, homotopy_result.x_solution, True, homotopy_result.stats
|
|
155
|
+
)
|
|
186
156
|
finally:
|
|
187
157
|
teardown_providers(tmp_fs._provider_map)
|
|
188
158
|
|
|
189
|
-
if
|
|
159
|
+
if homotopy_result.converged:
|
|
190
160
|
logger.info(
|
|
191
|
-
f"Homotopy converged: ||F||={
|
|
192
|
-
f"iterations={
|
|
161
|
+
f"Homotopy converged: ||F||={homotopy_result.stats.get('final_norm', '?'):.3e}, "
|
|
162
|
+
f"iterations={homotopy_result.stats.get('iterations', '?')}"
|
|
193
163
|
)
|
|
194
|
-
save_snapshot(
|
|
195
|
-
sm, config,
|
|
164
|
+
snapshot_id = save_snapshot(
|
|
165
|
+
sm, config, homotopy_result.x_solution, fs.var_names,
|
|
196
166
|
metadata=current_metadata,
|
|
197
167
|
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
198
168
|
label="homotopy solution",
|
|
199
169
|
)
|
|
200
170
|
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
201
|
-
|
|
171
|
+
run_id = make_run_id()
|
|
172
|
+
persist_run(sm, tmp_fs, run_id, tmp_fs.results, run_info, config, base_name, snapshot_id)
|
|
202
173
|
logger.info("Homotopy apply succeeded. New snapshot saved.")
|
|
203
174
|
return
|
|
204
175
|
|
|
@@ -210,8 +181,8 @@ def apply(
|
|
|
210
181
|
breakdown = log_residual_breakdown(fs)
|
|
211
182
|
divergence = build_divergence_report(
|
|
212
183
|
drifted_params=drifted,
|
|
213
|
-
solver_stats=
|
|
214
|
-
x_last=
|
|
184
|
+
solver_stats=homotopy_result.stats,
|
|
185
|
+
x_last=homotopy_result.x_solution,
|
|
215
186
|
var_names=fs.var_names,
|
|
216
187
|
breakdown=breakdown,
|
|
217
188
|
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Shared run-persistence helpers for the ``pf run`` / ``pf apply`` CLIs.
|
|
2
|
+
|
|
3
|
+
Both commands build a :class:`~processforge.types.RunManifest`, save it to the
|
|
4
|
+
:class:`~processforge.persistence.archive.ProcessStateArchive`, and write a Zarr
|
|
5
|
+
copy of the standardized outputs. That logic was previously duplicated (with
|
|
6
|
+
slightly different bugs in each copy); it now lives here in one place.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..persistence.archive import ProcessStateArchive
|
|
21
|
+
from ..types import RunInfo
|
|
22
|
+
|
|
23
|
+
_RUN_ID_TS_FMT = "%Y%m%dT%H%M%SZ"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_run_id() -> str:
|
|
27
|
+
"""Build a unique, sortable run id: ``<UTC timestamp>_<6 hex bytes>``."""
|
|
28
|
+
return (
|
|
29
|
+
datetime.datetime.now(datetime.timezone.utc).strftime(_RUN_ID_TS_FMT)
|
|
30
|
+
+ "_"
|
|
31
|
+
+ os.urandom(3).hex()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def flowsheet_hash(config: dict) -> str:
|
|
36
|
+
"""Short (16-hex) sha256 of a flowsheet config for run/index correlation."""
|
|
37
|
+
return hashlib.sha256(
|
|
38
|
+
json.dumps(config, sort_keys=True, default=str).encode("utf-8")
|
|
39
|
+
).hexdigest()[:16]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def persist_run(
|
|
43
|
+
archive: "ProcessStateArchive",
|
|
44
|
+
fs: Any,
|
|
45
|
+
run_id: str,
|
|
46
|
+
results: dict,
|
|
47
|
+
run_info: "RunInfo | dict",
|
|
48
|
+
config: dict,
|
|
49
|
+
base_name: str,
|
|
50
|
+
snapshot_id: str | None = None,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Persist a run's manifest + Zarr outputs into *archive*.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
archive: The :class:`ProcessStateArchive` for this flowsheet.
|
|
56
|
+
fs: The flowsheet object that produced *results* (must expose
|
|
57
|
+
``collect_outputs``). Either ``EOFlowsheet`` or ``Flowsheet``.
|
|
58
|
+
run_id: Unique run id (use :func:`make_run_id`).
|
|
59
|
+
results: The solve's stream/unit result dict (the *converged* one).
|
|
60
|
+
run_info: Provenance from
|
|
61
|
+
:func:`processforge.provenance.build_run_info`.
|
|
62
|
+
config: Validated flowsheet config (for the simulation mode).
|
|
63
|
+
base_name: Flowsheet basename used for archive paths.
|
|
64
|
+
snapshot_id: Optional snapshot id to stamp on the manifest (so a run
|
|
65
|
+
can be linked back to the state it was derived from).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The run id that was persisted.
|
|
69
|
+
"""
|
|
70
|
+
mode = config.get("simulation", {}).get("mode", "steady")
|
|
71
|
+
run_info_dump = (
|
|
72
|
+
run_info.model_dump() if hasattr(run_info, "model_dump") else run_info
|
|
73
|
+
)
|
|
74
|
+
manifest = fs.collect_outputs(
|
|
75
|
+
run_id, mode, base_name, provenance=run_info_dump
|
|
76
|
+
)
|
|
77
|
+
if snapshot_id is not None:
|
|
78
|
+
manifest.snapshot_id = snapshot_id
|
|
79
|
+
|
|
80
|
+
stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
|
|
81
|
+
archive.save_run(manifest, stream_results=stream_results)
|
|
82
|
+
|
|
83
|
+
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
84
|
+
# inside the archive, mirroring `pf run`.
|
|
85
|
+
try:
|
|
86
|
+
from ..result import relink_latest_results, save_results_zarr
|
|
87
|
+
|
|
88
|
+
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
89
|
+
save_results_zarr(
|
|
90
|
+
results,
|
|
91
|
+
os.path.join(run_results_dir, "results.zarr"),
|
|
92
|
+
run_info,
|
|
93
|
+
)
|
|
94
|
+
relink_latest_results(archive.path, run_id)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
97
|
+
|
|
98
|
+
logger.info(f"Run saved : {run_id}")
|
|
99
|
+
return run_id
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""``pf run`` — run a process simulation from a flowsheet JSON file."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import hashlib
|
|
5
4
|
import os
|
|
6
5
|
from datetime import datetime, timezone
|
|
7
6
|
|
|
@@ -21,22 +20,7 @@ from .common import (
|
|
|
21
20
|
validate_runtime_flowsheet,
|
|
22
21
|
_resolve_provider_url,
|
|
23
22
|
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _flowsheet_hash(config: dict) -> str:
|
|
27
|
-
return hashlib.sha256(
|
|
28
|
-
json_dumps(config).encode("utf-8")
|
|
29
|
-
).hexdigest()[:16]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def json_dumps(obj) -> str:
|
|
33
|
-
import json
|
|
34
|
-
|
|
35
|
-
return json.dumps(obj, sort_keys=True, default=str)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _run_id() -> str:
|
|
39
|
-
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
|
|
23
|
+
from .persist import flowsheet_hash, make_run_id, persist_run
|
|
40
24
|
|
|
41
25
|
|
|
42
26
|
def _log_active_providers(config: dict) -> None:
|
|
@@ -99,8 +83,8 @@ def run(
|
|
|
99
83
|
is_dynamic = mode == "dynamic"
|
|
100
84
|
|
|
101
85
|
# Standardized run id, shared with container-side artifact uploads.
|
|
102
|
-
run_id =
|
|
103
|
-
|
|
86
|
+
run_id = make_run_id()
|
|
87
|
+
flowsheet_hash_value = flowsheet_hash(config)
|
|
104
88
|
|
|
105
89
|
if is_dynamic:
|
|
106
90
|
# Load .pfarchive snapshot as t=0 if available.
|
|
@@ -164,17 +148,12 @@ def run(
|
|
|
164
148
|
# are keyed by the same run_id/flowsheet_hash as this archive run.
|
|
165
149
|
for provider in getattr(fs, "_provider_map", {}).values():
|
|
166
150
|
if hasattr(provider, "set_run_context"):
|
|
167
|
-
provider.set_run_context(run_id,
|
|
151
|
+
provider.set_run_context(run_id, flowsheet_hash_value)
|
|
168
152
|
|
|
169
153
|
# Build + persist the standardized run manifest.
|
|
170
154
|
archive_path = os.path.join(outputs_dir, f"{base_name}.pfarchive")
|
|
171
155
|
archive = ProcessStateArchive(archive_path)
|
|
172
|
-
|
|
173
|
-
stream_results = {
|
|
174
|
-
k: v for k, v in results.items()
|
|
175
|
-
if not hasattr(v, "fields") # exclude EngineOutput objects
|
|
176
|
-
}
|
|
177
|
-
archive.save_run(manifest, stream_results=stream_results)
|
|
156
|
+
persist_run(archive, fs, run_id, results, run_info, config, base_name)
|
|
178
157
|
|
|
179
158
|
# Persist a StateManager snapshot so `pf plan` (and `pf apply`) can diff
|
|
180
159
|
# against this run as a baseline. Without it, `pf plan` always reports
|
|
@@ -191,27 +170,11 @@ def run(
|
|
|
191
170
|
except Exception as e:
|
|
192
171
|
logger.warning(f"Failed to save state snapshot: {type(e).__name__}: {e}")
|
|
193
172
|
|
|
194
|
-
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
195
|
-
# inside the archive. Works for every provider (OpenMC, CoolProp, FESTIM,
|
|
196
|
-
# Cantera) and for both local and remote docker runs — remote artifacts
|
|
197
|
-
# reference their S3 remote_uris.
|
|
198
|
-
try:
|
|
199
|
-
from ..result import relink_latest_results, save_results_zarr
|
|
200
|
-
|
|
201
|
-
run_results_dir = os.path.join(archive.path, "results", run_id)
|
|
202
|
-
save_results_zarr(
|
|
203
|
-
results,
|
|
204
|
-
os.path.join(run_results_dir, "results.zarr"),
|
|
205
|
-
run_info,
|
|
206
|
-
)
|
|
207
|
-
relink_latest_results(archive.path, run_id)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
210
|
-
|
|
211
173
|
logger.info(f"Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
|
|
212
174
|
logger.info(f"Run saved : {os.path.join(archive_path, 'runs', run_id + '.json')}")
|
|
213
175
|
|
|
214
176
|
# Summarize standardized outputs.
|
|
177
|
+
manifest = archive.load_run(run_id)
|
|
215
178
|
for unit_name, out in manifest.units.items():
|
|
216
179
|
for f in out.fields:
|
|
217
180
|
logger.info(f" [{unit_name}] {f.name} = {f.quantity.value} {f.quantity.unit}")
|
|
@@ -93,24 +93,58 @@ class EOFlowsheet:
|
|
|
93
93
|
)
|
|
94
94
|
except Exception: # noqa: BLE001
|
|
95
95
|
self.residual_breakdown = []
|
|
96
|
-
results =
|
|
97
|
-
|
|
98
|
-
# Run standalone SolverUnit instances (no inlet/outlet streams —
|
|
99
|
-
# they delegate entirely to their provider's run_simulation()).
|
|
100
|
-
from processforge.units.solver_unit import SolverUnit
|
|
101
|
-
for unit_name, unit in self._unit_objects.items():
|
|
102
|
-
if isinstance(unit, SolverUnit):
|
|
103
|
-
logger.info(f"EOFlowsheet: running standalone SolverUnit '{unit_name}'")
|
|
104
|
-
unit_result = unit._run_impl({})
|
|
105
|
-
results[unit_name] = unit_result
|
|
106
|
-
self.engine_outputs[unit_name] = unit_result
|
|
96
|
+
results = self.assemble_from_solution(manager, x_sol, converged, stats)
|
|
107
97
|
|
|
108
98
|
logger.info("EOFlowsheet: simulation complete.")
|
|
109
|
-
self.results = results
|
|
110
99
|
return results
|
|
111
100
|
finally:
|
|
112
101
|
teardown_providers(self._provider_map)
|
|
113
102
|
|
|
103
|
+
def assemble_from_solution(
|
|
104
|
+
self,
|
|
105
|
+
manager: "GlobalJacobianManager",
|
|
106
|
+
x_sol: "np.ndarray",
|
|
107
|
+
converged: bool,
|
|
108
|
+
stats: dict,
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""Populate run outputs from an already-computed solution vector.
|
|
111
|
+
|
|
112
|
+
Used both by :meth:`run` (after the Newton solve) and by callers such as
|
|
113
|
+
the homotopy fallback in ``pf apply``, where a solved ``x`` is produced
|
|
114
|
+
externally and must be turned into the same standardized ``results`` that
|
|
115
|
+
:meth:`run` would have returned. This keeps the post-solve assembly in
|
|
116
|
+
exactly one place.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
manager: The live Jacobian manager used to build ``x_sol``.
|
|
120
|
+
x_sol: The converged (or best-effort) solution vector.
|
|
121
|
+
converged: Whether the solve was judged converged.
|
|
122
|
+
stats: Solver statistics dict (e.g. ``final_norm``, ``iterations``).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The stream/unit result dict (also stored on ``self.results``).
|
|
126
|
+
"""
|
|
127
|
+
self.x_converged = x_sol
|
|
128
|
+
self.converged = converged
|
|
129
|
+
self.solver_stats = stats
|
|
130
|
+
|
|
131
|
+
results = manager.extract_results(x_sol)
|
|
132
|
+
|
|
133
|
+
# Run standalone SolverUnit instances (no inlet/outlet streams —
|
|
134
|
+
# they delegate entirely to their provider's run_simulation()).
|
|
135
|
+
from processforge.units.solver_unit import SolverUnit
|
|
136
|
+
|
|
137
|
+
self.engine_outputs = {}
|
|
138
|
+
for unit_name, unit in self._unit_objects.items():
|
|
139
|
+
if isinstance(unit, SolverUnit):
|
|
140
|
+
logger.info(f"EOFlowsheet: running standalone SolverUnit '{unit_name}'")
|
|
141
|
+
unit_result = unit._run_impl({})
|
|
142
|
+
results[unit_name] = unit_result
|
|
143
|
+
self.engine_outputs[unit_name] = unit_result
|
|
144
|
+
|
|
145
|
+
self.results = results
|
|
146
|
+
return results
|
|
147
|
+
|
|
114
148
|
# ------------------------------------------------------------------
|
|
115
149
|
# Standardized output collection
|
|
116
150
|
# ------------------------------------------------------------------
|
|
@@ -6,12 +6,14 @@ from typing import TYPE_CHECKING, Optional
|
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
from loguru import logger
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
10
|
|
|
10
11
|
from .backends import ScipyBackend, PyomoBackend, CasADiBackend
|
|
11
12
|
from .backends.base import AbstractEOBackend
|
|
12
13
|
from ..types import SnapshotState
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
16
|
+
from .flowsheet import EOFlowsheet
|
|
15
17
|
from .jacobian import GlobalJacobianManager
|
|
16
18
|
|
|
17
19
|
|
|
@@ -29,9 +31,18 @@ _BACKENDS: dict[str, type[AbstractEOBackend]] = {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
class HomotopyResult(BaseModel):
|
|
35
|
+
"""Typed outcome of :func:`solve_with_homotopy`."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
38
|
+
|
|
39
|
+
x_solution: np.ndarray
|
|
40
|
+
converged: bool
|
|
41
|
+
stats: dict
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
class EOSolver:
|
|
33
|
-
"""
|
|
34
|
-
Selects and runs the EO backend specified by ``backend`` name.
|
|
45
|
+
"""Selects and runs the EO backend specified by ``backend`` name.
|
|
35
46
|
|
|
36
47
|
Args:
|
|
37
48
|
backend: One of ``"scipy"``, ``"pyomo"``, ``"casadi"``.
|
|
@@ -117,22 +128,40 @@ def compute_residual_breakdown(
|
|
|
117
128
|
|
|
118
129
|
|
|
119
130
|
def solve_with_homotopy(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
""
|
|
131
|
+
flowsheet: "EOFlowsheet",
|
|
132
|
+
manager: "GlobalJacobianManager",
|
|
133
|
+
solver: "EOSolver",
|
|
134
|
+
state: "SnapshotState | dict",
|
|
135
|
+
drifted: list[str],
|
|
136
|
+
) -> "HomotopyResult":
|
|
137
|
+
"""Try a standard solve; if it fails, fall back to step-wise homotopy.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
flowsheet: The current-config :class:`EOFlowsheet` (used for its config
|
|
141
|
+
and backend).
|
|
142
|
+
manager: The live Jacobian manager for the current config.
|
|
143
|
+
solver: The :class:`EOSolver` instance driving each continuation step.
|
|
144
|
+
state: The prior converged snapshot (object or dict) used as the warm
|
|
145
|
+
start.
|
|
146
|
+
drifted: Dotted config paths that changed between the snapshot and the
|
|
147
|
+
current flowsheet.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A :class:`HomotopyResult` holding the best solution vector found, whether
|
|
151
|
+
it converged, and the associated solver statistics.
|
|
152
|
+
"""
|
|
123
153
|
old_config = state.config if isinstance(state, SnapshotState) else state["config"]
|
|
124
|
-
current_config =
|
|
154
|
+
current_config = flowsheet.config
|
|
125
155
|
|
|
126
|
-
def update_config_value(cfg, path, value):
|
|
127
|
-
|
|
128
|
-
parts = path.split('.')
|
|
156
|
+
def update_config_value(cfg: dict, path: str, value: float) -> None:
|
|
157
|
+
parts = path.split(".")
|
|
129
158
|
d = cfg
|
|
130
159
|
for p in parts[:-1]:
|
|
131
160
|
d = d[p]
|
|
132
161
|
d[parts[-1]] = float(value)
|
|
133
|
-
|
|
134
|
-
def get_config_value(cfg, path):
|
|
135
|
-
parts = path.split(
|
|
162
|
+
|
|
163
|
+
def get_config_value(cfg: dict, path: str):
|
|
164
|
+
parts = path.split(".")
|
|
136
165
|
d = cfg
|
|
137
166
|
for p in parts[:-1]:
|
|
138
167
|
if p not in d:
|
|
@@ -144,10 +173,14 @@ def solve_with_homotopy(
|
|
|
144
173
|
_DEFAULT_SIGNALS = {
|
|
145
174
|
"openmc": ConvergenceSignal(signal_key="k_eff", target=1.0, tolerance=0.01),
|
|
146
175
|
}
|
|
147
|
-
provider =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
provider = (
|
|
177
|
+
current_config.get("units", {})
|
|
178
|
+
.get(list(current_config.get("units", {}).keys())[0], {})
|
|
179
|
+
.get("provider", "openmc")
|
|
180
|
+
if current_config.get("units")
|
|
181
|
+
else None
|
|
182
|
+
)
|
|
183
|
+
|
|
151
184
|
active_signal = None
|
|
152
185
|
if convergence_signal:
|
|
153
186
|
provider_filter = convergence_signal.get("provider")
|
|
@@ -155,103 +188,109 @@ def solve_with_homotopy(
|
|
|
155
188
|
active_signal = ConvergenceSignal(**convergence_signal)
|
|
156
189
|
elif provider in _DEFAULT_SIGNALS:
|
|
157
190
|
active_signal = _DEFAULT_SIGNALS[provider]
|
|
158
|
-
|
|
191
|
+
|
|
159
192
|
if active_signal:
|
|
160
193
|
logger.info(
|
|
161
194
|
f"Homotopy convergence signal: {active_signal.signal_key} "
|
|
162
195
|
f"target={active_signal.target}"
|
|
163
196
|
)
|
|
164
197
|
|
|
165
|
-
import numpy as np
|
|
166
|
-
|
|
167
198
|
if not isinstance(state, SnapshotState):
|
|
168
|
-
|
|
199
|
+
warm_start = np.array(state["x"])
|
|
169
200
|
else:
|
|
170
|
-
|
|
201
|
+
warm_start = np.array(state.x)
|
|
171
202
|
|
|
172
|
-
|
|
203
|
+
x_solution, converged, stats = solver.solve(manager, warm_start)
|
|
173
204
|
if converged:
|
|
174
205
|
logger.info("Standard solver converged with previous state's warm guess.")
|
|
175
|
-
return
|
|
176
|
-
|
|
206
|
+
return HomotopyResult(x_solution=x_solution, converged=converged, stats=stats)
|
|
207
|
+
|
|
177
208
|
logger.warning("Standard solve failed. Invoking Homotopy 'step-wise apply' solver...")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for
|
|
181
|
-
old_val = get_config_value(old_config,
|
|
182
|
-
new_val = get_config_value(current_config,
|
|
209
|
+
|
|
210
|
+
interpolated_drifts: list[tuple[str, float, float]] = []
|
|
211
|
+
for drift in drifted:
|
|
212
|
+
old_val = get_config_value(old_config, drift)
|
|
213
|
+
new_val = get_config_value(current_config, drift)
|
|
183
214
|
if isinstance(old_val, (int, float)) and isinstance(new_val, (int, float)):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if not
|
|
215
|
+
interpolated_drifts.append((drift, float(old_val), float(new_val)))
|
|
216
|
+
|
|
217
|
+
if not interpolated_drifts:
|
|
187
218
|
logger.error("No continuous numerical parameters to interpolate. Homotopy fails.")
|
|
188
|
-
return
|
|
219
|
+
return HomotopyResult(x_solution=x_solution, converged=False, stats=stats)
|
|
189
220
|
|
|
190
|
-
x_current =
|
|
221
|
+
x_current = warm_start.copy()
|
|
191
222
|
best_x = x_current.copy()
|
|
192
|
-
best_residual = float(
|
|
223
|
+
best_residual = float("inf")
|
|
193
224
|
best_step = 0
|
|
194
|
-
|
|
225
|
+
|
|
195
226
|
from copy import deepcopy
|
|
227
|
+
|
|
196
228
|
interpolated_config = deepcopy(old_config)
|
|
197
229
|
steps = 10
|
|
198
|
-
|
|
230
|
+
|
|
199
231
|
from .flowsheet import EOFlowsheet
|
|
200
232
|
from processforge.providers.manager import teardown_providers
|
|
201
|
-
|
|
233
|
+
|
|
202
234
|
for step in range(1, steps + 1):
|
|
203
235
|
alpha = step / steps
|
|
204
236
|
logger.info(f"--- Homotopy Step {step}/{steps} (alpha={alpha:.1f}) ---")
|
|
205
|
-
|
|
206
|
-
for
|
|
207
|
-
|
|
208
|
-
update_config_value(interpolated_config,
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
|
|
238
|
+
for path, old_val, new_val in interpolated_drifts:
|
|
239
|
+
value = old_val + alpha * (new_val - old_val)
|
|
240
|
+
update_config_value(interpolated_config, path, value)
|
|
241
|
+
|
|
242
|
+
step_flowsheet = EOFlowsheet(interpolated_config, backend=flowsheet.backend)
|
|
211
243
|
try:
|
|
212
|
-
step_manager =
|
|
244
|
+
step_manager = step_flowsheet._build()
|
|
213
245
|
step_x0 = x_current.copy()
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
step_solver = EOSolver(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
step_flowsheet.x0 = step_x0
|
|
247
|
+
step_flowsheet.var_names = step_flowsheet._build_var_names(step_manager)
|
|
248
|
+
|
|
249
|
+
step_solver = EOSolver(
|
|
250
|
+
backend=flowsheet.backend, tol=solver.tol, max_iter=solver.max_iter
|
|
251
|
+
)
|
|
252
|
+
x_current, step_converged, step_stats = step_solver.solve(step_manager, step_x0)
|
|
253
|
+
|
|
254
|
+
current_residual = step_stats.get("final_norm", float("inf"))
|
|
255
|
+
|
|
222
256
|
if active_signal and active_signal.target is not None:
|
|
223
257
|
signal_key = active_signal.signal_key
|
|
224
258
|
signal_target = active_signal.target
|
|
225
259
|
signal_tol = active_signal.tolerance
|
|
226
|
-
all_scalars = {}
|
|
227
|
-
for
|
|
260
|
+
all_scalars: dict[str, float] = {}
|
|
261
|
+
for unit_scalars in step_manager.scalars.values():
|
|
228
262
|
all_scalars.update(unit_scalars)
|
|
229
263
|
signal_val = all_scalars.get(signal_key)
|
|
230
264
|
if signal_val is not None:
|
|
231
265
|
rel_error = abs(signal_val - signal_target) / signal_target
|
|
232
|
-
logger.info(
|
|
266
|
+
logger.info(
|
|
267
|
+
f" {signal_key}={signal_val:.4f}, target={signal_target}, "
|
|
268
|
+
f"rel_error={rel_error:.4f}"
|
|
269
|
+
)
|
|
233
270
|
if rel_error < signal_tol:
|
|
234
271
|
logger.info(f" Converged via convergence signal at step {step}")
|
|
235
272
|
best_x = x_current.copy()
|
|
236
273
|
best_residual = current_residual
|
|
237
274
|
best_step = step
|
|
238
275
|
break
|
|
239
|
-
|
|
276
|
+
|
|
240
277
|
if not step_converged:
|
|
241
|
-
logger.error(
|
|
278
|
+
logger.error(
|
|
279
|
+
f"Homotopy failed to converge at step {step} (alpha={alpha:.1f})"
|
|
280
|
+
)
|
|
242
281
|
if best_step > 0:
|
|
243
282
|
logger.info(f"Reverting to best checkpoint at step {best_step}")
|
|
244
283
|
x_current = best_x.copy()
|
|
245
|
-
return x_current, False,
|
|
246
|
-
|
|
284
|
+
return HomotopyResult(x_solution=x_current, converged=False, stats=step_stats)
|
|
285
|
+
|
|
247
286
|
if current_residual < best_residual:
|
|
248
287
|
best_residual = current_residual
|
|
249
288
|
best_x = x_current.copy()
|
|
250
289
|
best_step = step
|
|
251
|
-
|
|
290
|
+
|
|
252
291
|
finally:
|
|
253
|
-
teardown_providers(
|
|
254
|
-
|
|
292
|
+
teardown_providers(step_flowsheet._provider_map)
|
|
293
|
+
|
|
255
294
|
logger.info("Homotopy step-wise solve successfully reached target config.")
|
|
256
|
-
|
|
257
|
-
return
|
|
295
|
+
x_solution, converged, stats = solver.solve(manager, x_current)
|
|
296
|
+
return HomotopyResult(x_solution=x_solution, converged=converged, stats=stats)
|