processforge 0.2.34__tar.gz → 0.2.40__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.2.34 → processforge-0.2.40}/PKG-INFO +37 -18
- processforge-0.2.40/README.md +82 -0
- {processforge-0.2.34 → processforge-0.2.40}/pyproject.toml +2 -3
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/__init__.py +1 -1
- processforge-0.2.40/src/processforge/cli/__init__.py +44 -0
- processforge-0.2.40/src/processforge/cli/apply.py +207 -0
- processforge-0.2.40/src/processforge/cli/common.py +280 -0
- processforge-0.2.40/src/processforge/cli/diagram.py +57 -0
- processforge-0.2.40/src/processforge/cli/display.py +69 -0
- processforge-0.2.40/src/processforge/cli/export_fmu.py +41 -0
- processforge-0.2.40/src/processforge/cli/export_modelica.py +50 -0
- processforge-0.2.40/src/processforge/cli/init.py +171 -0
- processforge-0.2.40/src/processforge/cli/plan.py +132 -0
- processforge-0.2.40/src/processforge/cli/run.py +106 -0
- processforge-0.2.40/src/processforge/cli/validate.py +20 -0
- processforge-0.2.40/src/processforge/simulate.py +59 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/PKG-INFO +37 -18
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/SOURCES.txt +12 -1
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/requires.txt +1 -2
- processforge-0.2.40/tests/test_cli.py +380 -0
- {processforge-0.2.34 → processforge-0.2.40}/tests/test_init.py +9 -9
- processforge-0.2.34/README.md +0 -62
- processforge-0.2.34/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -202
- processforge-0.2.34/src/processforge/simulate.py +0 -930
- {processforge-0.2.34 → processforge-0.2.40}/LICENSE +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/MANIFEST.in +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/setup.cfg +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/__main__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/_schema.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/compose.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/solver.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/flowsheet.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/lock.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/provenance.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/base.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/manager.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/providers/registry.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/result.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/solver.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/state.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/thermo.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/types.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/cstr.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/flash.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/heater.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/pfr.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/pipes.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/provider_mixin.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/pump.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/registry.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/solver.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/strainer.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/tank.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/units/valve.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/topology.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge/utils/validation.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/tests/test_festim_provider.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/tests/test_providers.py +0 -0
- {processforge-0.2.34 → processforge-0.2.40}/tests/test_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processforge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.40
|
|
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
|
|
@@ -29,8 +29,7 @@ Requires-Dist: graphviz>=0.20
|
|
|
29
29
|
Requires-Dist: pandas>=2.2
|
|
30
30
|
Requires-Dist: zarr>=3.0
|
|
31
31
|
Requires-Dist: coolprop>=7.2.0
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist: pip>=26.1.2
|
|
32
|
+
Requires-Dist: typer>=0.12
|
|
34
33
|
Provides-Extra: eo
|
|
35
34
|
Requires-Dist: pyomo>=6.7; extra == "eo"
|
|
36
35
|
Provides-Extra: eo-casadi
|
|
@@ -66,38 +65,58 @@ A lightweight Python framework for process simulation, coupling hydraulic, therm
|
|
|
66
65
|
|
|
67
66
|
## Install
|
|
68
67
|
|
|
68
|
+
Install the `pf` command-line tool with [uv](https://docs.astral.sh/uv/):
|
|
69
|
+
|
|
69
70
|
```bash
|
|
70
|
-
uv
|
|
71
|
+
uv tool install processforge
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
For CoolProp-backed units:
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
|
-
uv
|
|
77
|
+
uv tool install "processforge[coolprop]"
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
Optional solver backends:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
uv
|
|
83
|
-
uv
|
|
84
|
-
uv
|
|
83
|
+
uv tool install "processforge[eo]"
|
|
84
|
+
uv tool install "processforge[eo-casadi]"
|
|
85
|
+
uv tool install "processforge[modelica]"
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
## Quick start
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
1. **Install the tool**
|
|
90
91
|
|
|
91
|
-
```bash
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
pf apply flowsheets/hydraulic-chain.json
|
|
95
|
-
```
|
|
96
|
-
For flowsheets with Tank units, use `run` to solve with the SM solver. For direct run mode (for dynamic flowsheets with Tank units), use `run`:
|
|
92
|
+
```bash
|
|
93
|
+
uv tool install processforge
|
|
94
|
+
```
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```
|
|
96
|
+
2. **Download an example flowsheet**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -O https://raw.githubusercontent.com/urjanova/processforge/master/flowsheets/hydraulic-chain.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
3. **Initialize, plan, and apply**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pf init
|
|
106
|
+
pf plan hydraulic-chain.json
|
|
107
|
+
pf apply hydraulic-chain.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`plan` validates the flowsheet (schema, DOF, units) without running the solver; `apply` solves it and stores a snapshot.
|
|
111
|
+
|
|
112
|
+
4. **Look at the output**
|
|
113
|
+
|
|
114
|
+
`pf apply` writes results under `outputs/`:
|
|
115
|
+
- `*_results.zarr` — simulation results store
|
|
116
|
+
- `*_validation.xlsx` — validation report
|
|
117
|
+
- `*.pfstate/` — versioned snapshot store with a `latest` pointer
|
|
118
|
+
|
|
119
|
+
For flowsheets with Tank units (dynamic), use `pf run` instead of `pf apply` to solve with the SM solver.
|
|
101
120
|
|
|
102
121
|
## Python API
|
|
103
122
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Processforge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A lightweight Python framework for process simulation, coupling hydraulic, thermal, and reactor workflows.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install the `pf` command-line tool with [uv](https://docs.astral.sh/uv/):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv tool install processforge
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For CoolProp-backed units:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install "processforge[coolprop]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional solver backends:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install "processforge[eo]"
|
|
25
|
+
uv tool install "processforge[eo-casadi]"
|
|
26
|
+
uv tool install "processforge[modelica]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
1. **Install the tool**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install processforge
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. **Download an example flowsheet**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -O https://raw.githubusercontent.com/urjanova/processforge/master/flowsheets/hydraulic-chain.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. **Initialize, plan, and apply**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pf init
|
|
47
|
+
pf plan hydraulic-chain.json
|
|
48
|
+
pf apply hydraulic-chain.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`plan` validates the flowsheet (schema, DOF, units) without running the solver; `apply` solves it and stores a snapshot.
|
|
52
|
+
|
|
53
|
+
4. **Look at the output**
|
|
54
|
+
|
|
55
|
+
`pf apply` writes results under `outputs/`:
|
|
56
|
+
- `*_results.zarr` — simulation results store
|
|
57
|
+
- `*_validation.xlsx` — validation report
|
|
58
|
+
- `*.pfstate/` — versioned snapshot store with a `latest` pointer
|
|
59
|
+
|
|
60
|
+
For flowsheets with Tank units (dynamic), use `pf run` instead of `pf apply` to solve with the SM solver.
|
|
61
|
+
|
|
62
|
+
## Python API
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from processforge import EOFlowsheet, validate_flowsheet
|
|
66
|
+
config = validate_flowsheet("flowsheets/hydraulic-chain.json")
|
|
67
|
+
fs = EOFlowsheet(config, backend="scipy")
|
|
68
|
+
results = fs.run()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage and Docker
|
|
72
|
+
Processforge is also available as Docker images, and can be run in the cloud. See the [usage guide](docs/usage.md) for CLI commands, workflows, Docker, and cloud notes.
|
|
73
|
+
|
|
74
|
+
## Flowsheets
|
|
75
|
+
The core of Processforge is the flowsheet JSON format, which defines materials, units, and recycle rules. See below for more information on the formats and example flowsheets.
|
|
76
|
+
- [docs/flowsheets.md](docs/flowsheets.md) : flowsheet JSON format, materials, units, and recycle rules
|
|
77
|
+
- [flowsheets/](flowsheets/) : example flowsheets shipped with the repo
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
BSD 3-Clause License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "processforge"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.40"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -35,8 +35,7 @@ dependencies = [
|
|
|
35
35
|
"pandas>=2.2",
|
|
36
36
|
"zarr>=3.0",
|
|
37
37
|
"coolprop>=7.2.0",
|
|
38
|
-
"
|
|
39
|
-
"pip>=26.1.2",
|
|
38
|
+
"typer>=0.12",
|
|
40
39
|
]
|
|
41
40
|
|
|
42
41
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CLI subpackage — one module per ``pf`` subcommand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_commands(app: typer.Typer) -> None:
|
|
9
|
+
"""Register all subcommands on *app*."""
|
|
10
|
+
from .init import init
|
|
11
|
+
from .validate import validate
|
|
12
|
+
from .run import run
|
|
13
|
+
from .apply import apply
|
|
14
|
+
from .plan import plan
|
|
15
|
+
from .diagram import diagram
|
|
16
|
+
from .export_fmu import export_fmu
|
|
17
|
+
from .export_modelica import export_modelica
|
|
18
|
+
|
|
19
|
+
app.command(
|
|
20
|
+
"init",
|
|
21
|
+
help="Initialise .processforge/ project directory and provider environment",
|
|
22
|
+
)(init)
|
|
23
|
+
app.command(
|
|
24
|
+
"validate",
|
|
25
|
+
help="Check providers and environment are ready for a flowsheet",
|
|
26
|
+
)(validate)
|
|
27
|
+
app.command("run", help="Run a process simulation")(run)
|
|
28
|
+
app.command(
|
|
29
|
+
"apply",
|
|
30
|
+
help="Apply flowsheet using state-based warm start and homotopy fallback",
|
|
31
|
+
)(apply)
|
|
32
|
+
app.command(
|
|
33
|
+
"plan",
|
|
34
|
+
help="Validate a flowsheet, run DOF analysis, structural diff, and generate a Mermaid diagram",
|
|
35
|
+
)(plan)
|
|
36
|
+
app.command("diagram", help="Generate a flowsheet diagram")(diagram)
|
|
37
|
+
app.command(
|
|
38
|
+
"export-modelica",
|
|
39
|
+
help="Transpile flowsheet to Modelica .mo and compile via OMPython",
|
|
40
|
+
)(export_modelica)
|
|
41
|
+
app.command(
|
|
42
|
+
"export-fmu",
|
|
43
|
+
help="Export flowsheet as FMI 2.0 co-simulation FMU",
|
|
44
|
+
)(export_fmu)
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
load_state_manager,
|
|
21
|
+
log_residual_breakdown,
|
|
22
|
+
output_root,
|
|
23
|
+
require_existing_file,
|
|
24
|
+
save_snapshot,
|
|
25
|
+
validate_runtime_flowsheet,
|
|
26
|
+
write_divergence_report,
|
|
27
|
+
)
|
|
28
|
+
from .display import print_structural_diff
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def apply(
|
|
32
|
+
flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
|
|
33
|
+
backend: Literal["scipy", "pyomo", "casadi"] | None = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--backend",
|
|
36
|
+
help="Override the flowsheet's simulation.backend",
|
|
37
|
+
),
|
|
38
|
+
tolerance: float = typer.Option(
|
|
39
|
+
1e-6,
|
|
40
|
+
"--tolerance",
|
|
41
|
+
help="Newton solver convergence tolerance (default: 1e-6)",
|
|
42
|
+
),
|
|
43
|
+
max_iter: int = typer.Option(
|
|
44
|
+
50,
|
|
45
|
+
"--max-iter",
|
|
46
|
+
help="Max Newton iterations (default: 50)",
|
|
47
|
+
),
|
|
48
|
+
skip_homotopy: bool = typer.Option(
|
|
49
|
+
False,
|
|
50
|
+
"--skip-homotopy",
|
|
51
|
+
help="Disable homotopy fallback; cold-start only",
|
|
52
|
+
),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Apply flowsheet: drift detection, warm-start, homotopy fallback, convergence guardrails."""
|
|
55
|
+
require_existing_file(flowsheet)
|
|
56
|
+
config = validate_runtime_flowsheet(flowsheet)
|
|
57
|
+
|
|
58
|
+
# Check provider availability
|
|
59
|
+
check_providers(config, flowsheet)
|
|
60
|
+
|
|
61
|
+
base_name = os.path.splitext(os.path.basename(flowsheet))[0]
|
|
62
|
+
|
|
63
|
+
sim_cfg = config.get("simulation", {})
|
|
64
|
+
mode = sim_cfg.get("mode", "steady")
|
|
65
|
+
if mode != "steady":
|
|
66
|
+
logger.error("pf apply is only supported for steady-state EO flowsheets.")
|
|
67
|
+
raise SystemExit(1)
|
|
68
|
+
|
|
69
|
+
outputs_dir = output_root()
|
|
70
|
+
os.makedirs(outputs_dir, exist_ok=True)
|
|
71
|
+
sm, state = load_state_manager(outputs_dir, base_name)
|
|
72
|
+
|
|
73
|
+
# Structural diff: detect topology changes
|
|
74
|
+
topology_changed = False
|
|
75
|
+
if state is not None:
|
|
76
|
+
diff = sm.detect_structural_diff(config, state)
|
|
77
|
+
print_structural_diff(diff)
|
|
78
|
+
topology_changed = bool(diff.get("added") or diff.get("removed"))
|
|
79
|
+
if topology_changed:
|
|
80
|
+
logger.warning(
|
|
81
|
+
"Topology changed (units added/removed). "
|
|
82
|
+
"Homotopy requires identical topology — falling back to cold start."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Parameter drift (only meaningful when topology is unchanged)
|
|
86
|
+
drifted: list[str] = []
|
|
87
|
+
current_metadata = build_run_metadata(config, tolerance, max_iter, backend or "scipy")
|
|
88
|
+
if state is not None and not topology_changed:
|
|
89
|
+
mismatches = sm.validate_metadata(current_metadata, state)
|
|
90
|
+
if mismatches:
|
|
91
|
+
logger.warning(f"Metadata mismatch: {mismatches}")
|
|
92
|
+
drifted = sm.detect_drift(config, state)
|
|
93
|
+
if not drifted:
|
|
94
|
+
logger.info("No drift detected. System is already at the desired state.")
|
|
95
|
+
return
|
|
96
|
+
stream_drifts = [d for d in drifted if d.startswith("streams.")]
|
|
97
|
+
unit_drifts = [d for d in drifted if d.startswith("units.")]
|
|
98
|
+
logger.warning("Drift detected:")
|
|
99
|
+
if stream_drifts:
|
|
100
|
+
logger.warning(f" Stream drift : {stream_drifts}")
|
|
101
|
+
if unit_drifts:
|
|
102
|
+
logger.warning(f" Unit drift : {unit_drifts}")
|
|
103
|
+
|
|
104
|
+
# Build flowsheet; attach saved state for warm-start unless topology changed
|
|
105
|
+
fs = EOFlowsheet(config, backend=backend)
|
|
106
|
+
fs.saved_state = state if not topology_changed else None
|
|
107
|
+
fs.solver_tol = tolerance
|
|
108
|
+
fs.solver_max_iter = max_iter
|
|
109
|
+
|
|
110
|
+
logger.info("=== Running Apply (Steady-State EO) ===")
|
|
111
|
+
t0 = time.perf_counter()
|
|
112
|
+
results = fs.run()
|
|
113
|
+
elapsed = time.perf_counter() - t0
|
|
114
|
+
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Direct solve completed in {elapsed:.2f}s (converged={fs.converged})."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if fs.converged:
|
|
120
|
+
snapshot_id = save_snapshot(
|
|
121
|
+
sm, config, fs.x_converged, fs.var_names,
|
|
122
|
+
metadata=current_metadata,
|
|
123
|
+
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
124
|
+
label="converged state",
|
|
125
|
+
)
|
|
126
|
+
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
127
|
+
zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
|
|
128
|
+
save_results_zarr(results, zarr_path, run_info=run_info)
|
|
129
|
+
logger.info("=== Apply Summary ===")
|
|
130
|
+
logger.info(" Status : CONVERGED")
|
|
131
|
+
logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
|
|
132
|
+
logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
|
|
133
|
+
logger.info(f" Backend : {fs.backend}")
|
|
134
|
+
logger.info(f" Snapshot ID : {snapshot_id}")
|
|
135
|
+
logger.info(f" Results zarr : {zarr_path}")
|
|
136
|
+
logger.info(f" Elapsed (s) : {elapsed:.2f}")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Direct solve failed — try homotopy (only when topology is same and state exists)
|
|
140
|
+
if state is not None and not topology_changed and drifted and not skip_homotopy:
|
|
141
|
+
logger.warning("Direct solve failed. Attempting homotopy continuation...")
|
|
142
|
+
from ..eo.solver import EOSolver, solve_with_homotopy
|
|
143
|
+
from ..eo.flowsheet import EOFlowsheet as _EO
|
|
144
|
+
from ..providers.manager import teardown_providers
|
|
145
|
+
|
|
146
|
+
solver = EOSolver(backend=fs.backend, tol=tolerance, max_iter=max_iter)
|
|
147
|
+
tmp_fs = _EO(config, backend=backend)
|
|
148
|
+
manager = tmp_fs._build()
|
|
149
|
+
try:
|
|
150
|
+
x_hom, hom_converged, hom_stats = solve_with_homotopy(
|
|
151
|
+
tmp_fs, manager, solver, state, drifted
|
|
152
|
+
)
|
|
153
|
+
finally:
|
|
154
|
+
teardown_providers(tmp_fs._provider_map)
|
|
155
|
+
|
|
156
|
+
if hom_converged:
|
|
157
|
+
logger.info(
|
|
158
|
+
f"Homotopy converged: ||F||={hom_stats.get('final_norm', '?'):.3e}, "
|
|
159
|
+
f"iterations={hom_stats.get('iterations', '?')}"
|
|
160
|
+
)
|
|
161
|
+
save_snapshot(
|
|
162
|
+
sm, config, x_hom, fs.var_names,
|
|
163
|
+
metadata=current_metadata,
|
|
164
|
+
parent_snapshot_id=state.snapshot_id if state is not None and not topology_changed else None,
|
|
165
|
+
label="homotopy solution",
|
|
166
|
+
)
|
|
167
|
+
run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
|
|
168
|
+
save_results_zarr(
|
|
169
|
+
results,
|
|
170
|
+
os.path.join(outputs_dir, f"{base_name}_results.zarr"),
|
|
171
|
+
run_info=run_info,
|
|
172
|
+
)
|
|
173
|
+
logger.info("Homotopy apply succeeded. New snapshot saved.")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Both failed — auto-revert and write divergence report
|
|
177
|
+
logger.error("Homotopy also failed to converge.")
|
|
178
|
+
prev_id = state.snapshot_id if state is not None else "unknown"
|
|
179
|
+
sm.rollback(1)
|
|
180
|
+
logger.warning(f"Reverted .pfstate to snapshot before {prev_id}.")
|
|
181
|
+
breakdown = log_residual_breakdown(fs)
|
|
182
|
+
divergence = build_divergence_report(
|
|
183
|
+
drifted_params=drifted,
|
|
184
|
+
solver_stats=hom_stats,
|
|
185
|
+
x_last=x_hom,
|
|
186
|
+
var_names=fs.var_names,
|
|
187
|
+
breakdown=breakdown,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
# Cold start also failed; no rollback (nothing to revert to)
|
|
191
|
+
if not drifted and state is None:
|
|
192
|
+
logger.error("Cold-start solve failed to converge (no prior snapshot).")
|
|
193
|
+
elif skip_homotopy:
|
|
194
|
+
logger.error("Cold-start solve failed to converge (--skip-homotopy).")
|
|
195
|
+
else:
|
|
196
|
+
logger.error("Cold-start solve failed to converge.")
|
|
197
|
+
breakdown = log_residual_breakdown(fs)
|
|
198
|
+
divergence = build_divergence_report(
|
|
199
|
+
drifted_params=[],
|
|
200
|
+
solver_stats=fs.solver_stats,
|
|
201
|
+
x_last=getattr(fs, "x_converged", []),
|
|
202
|
+
var_names=getattr(fs, "var_names", []),
|
|
203
|
+
breakdown=breakdown,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
write_divergence_report(outputs_dir, base_name, divergence)
|
|
207
|
+
raise SystemExit(1)
|