processforge 0.3.16__tar.gz → 0.3.19__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.16 → processforge-0.3.19}/PKG-INFO +12 -6
- {processforge-0.3.16 → processforge-0.3.19}/README.md +12 -6
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/openmc/msre_eigenvalue.json +2 -2
- {processforge-0.3.16 → processforge-0.3.19}/pyproject.toml +1 -1
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/__init__.py +1 -1
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/api/serve.py +24 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/apply.py +14 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/run.py +48 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/persistence/artifact_store.py +51 -7
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/container_client.py +2 -12
- processforge-0.3.19/src/processforge/providers/errors.py +276 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/festim_provider.py +4 -9
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/openmc_provider.py +5 -9
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/result.py +47 -2
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/types.py +19 -0
- processforge-0.3.19/src/processforge/utils/s3_upload.py +66 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/PKG-INFO +12 -6
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/SOURCES.txt +4 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_cli.py +10 -5
- processforge-0.3.19/tests/test_container_client.py +93 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_festim_provider.py +18 -7
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_openmc_provider.py +19 -0
- processforge-0.3.19/tests/test_provider_errors.py +104 -0
- processforge-0.3.19/tests/test_result_zarr.py +73 -0
- processforge-0.3.19/tests/test_s3_upload.py +64 -0
- processforge-0.3.16/src/processforge/utils/s3_upload.py +0 -69
- processforge-0.3.16/tests/test_s3_upload.py +0 -81
- {processforge-0.3.16 → processforge-0.3.19}/LICENSE +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/MANIFEST.in +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/setup.cfg +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/__main__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/_schema.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/common.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/display.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/init.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/plan.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/compose.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/coupling.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/lock.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/output_collector.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/provenance.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/quantity.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/simulate.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/solver.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/state.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/thermo.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/base.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/requires.txt +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_coupling.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_init.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_providers.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/tests/test_state.py +0 -0
- {processforge-0.3.16 → processforge-0.3.19}/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.19
|
|
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
|
|
@@ -114,12 +114,18 @@ uv tool install "processforge[modelica]"
|
|
|
114
114
|
|
|
115
115
|
4. **Look at the output**
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
- `*_results.zarr` — simulation results store (per-variable arrays, composition flattened)
|
|
119
|
-
- `*_results.zarr.schema.json` — schema file describing streams, variables, dtypes, units, shapes, and run provenance
|
|
120
|
-
- `*.pfstate/` — versioned snapshot store with a `latest` pointer
|
|
117
|
+
`pf apply` (and `pf run`) write a single unified archive under `outputs/`:
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
- `outputs/<base>.pfarchive/` — the unified store for a flowsheet's solved state and outputs:
|
|
120
|
+
- `snapshots/` — Zarr store of converged state vectors (one group per successful `pf apply`), each with `x`/`x_delta` arrays and config/var-name/metadata attributes, plus a `latest` pointer (powers warm-start, drift detection, and homotopy).
|
|
121
|
+
- `runs/<run_id>.json` — the run manifest: every stream and unit engine output (values, units, dtypes, shapes) and run provenance (backend, version, flowsheet hash).
|
|
122
|
+
- `outputs/streams/<name>.json` — per-stream timeseries from the solve.
|
|
123
|
+
- `artifacts.json` — content-addressed registry of all output artifacts (local + remote URIs).
|
|
124
|
+
- `index.json` — `field_name → occurrences` index for fast cross-run lookups.
|
|
125
|
+
- `latest_run` — plain-text pointer to the most recent run.
|
|
126
|
+
- `outputs/<base>_divergence.json` — written only when both direct and homotopy solves fail on `pf apply`, capturing drifted params, solver stats, and top residual violators.
|
|
127
|
+
|
|
128
|
+
For dynamic flowsheets with Tank units, use `pf run` (not `pf apply`, which is steady-state EO only) to solve with the dynamic engine.
|
|
123
129
|
|
|
124
130
|
## Python API
|
|
125
131
|
|
|
@@ -52,12 +52,18 @@ uv tool install "processforge[modelica]"
|
|
|
52
52
|
|
|
53
53
|
4. **Look at the output**
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
`pf apply` (and `pf run`) write a single unified archive under `outputs/`:
|
|
56
|
+
|
|
57
|
+
- `outputs/<base>.pfarchive/` — the unified store for a flowsheet's solved state and outputs:
|
|
58
|
+
- `snapshots/` — Zarr store of converged state vectors (one group per successful `pf apply`), each with `x`/`x_delta` arrays and config/var-name/metadata attributes, plus a `latest` pointer (powers warm-start, drift detection, and homotopy).
|
|
59
|
+
- `runs/<run_id>.json` — the run manifest: every stream and unit engine output (values, units, dtypes, shapes) and run provenance (backend, version, flowsheet hash).
|
|
60
|
+
- `outputs/streams/<name>.json` — per-stream timeseries from the solve.
|
|
61
|
+
- `artifacts.json` — content-addressed registry of all output artifacts (local + remote URIs).
|
|
62
|
+
- `index.json` — `field_name → occurrences` index for fast cross-run lookups.
|
|
63
|
+
- `latest_run` — plain-text pointer to the most recent run.
|
|
64
|
+
- `outputs/<base>_divergence.json` — written only when both direct and homotopy solves fail on `pf apply`, capturing drifted params, solver stats, and top residual violators.
|
|
65
|
+
|
|
66
|
+
For dynamic flowsheets with Tank units, use `pf run` (not `pf apply`, which is steady-state EO only) to solve with the dynamic engine.
|
|
61
67
|
|
|
62
68
|
## Python API
|
|
63
69
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"providers": {
|
|
8
8
|
"openmc": {
|
|
9
9
|
"type": "openmc",
|
|
10
|
-
"url": "
|
|
10
|
+
"url": "https://processforge-openmc-production.up.railway.app",
|
|
11
11
|
"output_dir": "openmc/msre_run",
|
|
12
12
|
"cross_sections": "${OPENMC_DATA_ROOT}/cross_sections/cross_sections.xml"
|
|
13
13
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"id": 2,
|
|
27
27
|
"density": 9.0,
|
|
28
28
|
"density_units": "g/cm3",
|
|
29
|
-
"temperature":
|
|
29
|
+
"temperature": 300.0,
|
|
30
30
|
"elements": [
|
|
31
31
|
{ "element": "Ni", "percent": 0.72, "percent_type": "wo" },
|
|
32
32
|
{ "element": "Mo", "percent": 0.16, "percent_type": "wo" },
|
|
@@ -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.19"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -99,7 +99,31 @@ def run(body: dict):
|
|
|
99
99
|
provider.teardown()
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def _log_startup() -> None:
|
|
103
|
+
"""Report where runs are kept and the S3 upload status at server start."""
|
|
104
|
+
root = os.environ.get("PROCESSFORGE_OUTPUT_DIR", "outputs")
|
|
105
|
+
logger.info(
|
|
106
|
+
f"Provider run outputs (scratch) are kept locally in: {root} "
|
|
107
|
+
f"(per-provider subdirs: outputs/openmc, outputs/festim)."
|
|
108
|
+
)
|
|
109
|
+
if os.environ.get("S3_BUCKET"):
|
|
110
|
+
bucket = os.environ["S3_BUCKET"]
|
|
111
|
+
logger.info(f"Remote artifact upload enabled → S3 bucket '{bucket}'.")
|
|
112
|
+
from processforge.utils.s3_upload import validate_s3
|
|
113
|
+
|
|
114
|
+
validate_s3() # fail fast on misconfigured credentials
|
|
115
|
+
else:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"S3_BUCKET not set — remote upload disabled; runs are ephemeral "
|
|
118
|
+
"on this container."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_log_startup()
|
|
123
|
+
|
|
124
|
+
|
|
102
125
|
if __name__ == "__main__":
|
|
103
126
|
import uvicorn
|
|
104
127
|
|
|
105
128
|
uvicorn.run(app, host="0.0.0.0", port=DEFAULT_PORT)
|
|
129
|
+
|
|
@@ -27,6 +27,20 @@ def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id)
|
|
|
27
27
|
manifest.snapshot_id = snapshot_id
|
|
28
28
|
stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
|
|
29
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 save_results_zarr
|
|
35
|
+
|
|
36
|
+
save_results_zarr(
|
|
37
|
+
results,
|
|
38
|
+
os.path.join(archive.path, "results.zarr"),
|
|
39
|
+
run_info,
|
|
40
|
+
)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
43
|
+
|
|
30
44
|
logger.info(f"Run saved : {run_id}")
|
|
31
45
|
return run_id
|
|
32
46
|
|
|
@@ -87,6 +87,8 @@ def run(
|
|
|
87
87
|
logger.info("=== Dynamic Results ===")
|
|
88
88
|
results = fs.run()
|
|
89
89
|
|
|
90
|
+
_check_for_failed_units(fs)
|
|
91
|
+
|
|
90
92
|
if hasattr(fs, "converged"):
|
|
91
93
|
if fs.converged:
|
|
92
94
|
logger.info("Dynamic simulation converged.")
|
|
@@ -106,6 +108,8 @@ def run(
|
|
|
106
108
|
logger.info("=== Steady-State EO Results ===")
|
|
107
109
|
results = fs.run()
|
|
108
110
|
|
|
111
|
+
_check_for_failed_units(fs)
|
|
112
|
+
|
|
109
113
|
if hasattr(fs, "converged"):
|
|
110
114
|
if fs.converged:
|
|
111
115
|
logger.info("Steady-state simulation converged.")
|
|
@@ -130,6 +134,21 @@ def run(
|
|
|
130
134
|
}
|
|
131
135
|
archive.save_run(manifest, stream_results=stream_results)
|
|
132
136
|
|
|
137
|
+
# Always persist a Zarr copy of the standardized outputs (fields + artifacts)
|
|
138
|
+
# inside the archive. Works for every provider (OpenMC, CoolProp, FESTIM,
|
|
139
|
+
# Cantera) and for both local and remote docker runs — remote artifacts
|
|
140
|
+
# reference their S3 remote_uris.
|
|
141
|
+
try:
|
|
142
|
+
from ..result import save_results_zarr
|
|
143
|
+
|
|
144
|
+
save_results_zarr(
|
|
145
|
+
results,
|
|
146
|
+
os.path.join(archive.path, "results.zarr"),
|
|
147
|
+
run_info,
|
|
148
|
+
)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
|
|
151
|
+
|
|
133
152
|
logger.info(f"Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
|
|
134
153
|
logger.info(f"Run saved : {os.path.join(archive_path, 'runs', run_id + '.json')}")
|
|
135
154
|
|
|
@@ -153,3 +172,32 @@ def run(
|
|
|
153
172
|
logger.info(f"Plots saved: {base_name}_results.png, {base_name}_timeseries.png")
|
|
154
173
|
except Exception as e:
|
|
155
174
|
logger.warning(f"Failed to generate plots: {type(e).__name__}: {e}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _check_for_failed_units(fs):
|
|
178
|
+
"""Fail loudly if any SolverUnit run returned ``status="failed"``.
|
|
179
|
+
|
|
180
|
+
Containerized providers return HTTP 200 with a structured ``EngineOutput``
|
|
181
|
+
(status="failed") rather than raising, so the failure would otherwise be
|
|
182
|
+
swallowed and recorded as a successful run. Surface it clearly and exit
|
|
183
|
+
non-zero.
|
|
184
|
+
"""
|
|
185
|
+
from ..types import EngineOutput
|
|
186
|
+
|
|
187
|
+
failed = {
|
|
188
|
+
name: out
|
|
189
|
+
for name, out in getattr(fs, "engine_outputs", {}).items()
|
|
190
|
+
if isinstance(out, EngineOutput) and out.status == "failed"
|
|
191
|
+
}
|
|
192
|
+
if not failed:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
for name, out in failed.items():
|
|
196
|
+
err = out.error
|
|
197
|
+
category = getattr(err, "category", "unknown") if err else "unknown"
|
|
198
|
+
message = getattr(err, "message", "") if err else ""
|
|
199
|
+
logger.error(f"Unit '{name}' simulation FAILED [{category}]: {message}")
|
|
200
|
+
hint = getattr(err, "hint", "") if err else ""
|
|
201
|
+
if hint:
|
|
202
|
+
logger.error(f" hint: {hint}")
|
|
203
|
+
raise SystemExit(1)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Content-addressed artifact storage for simulation outputs.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
declare their output files as :class:`~processforge.types.OutputArtifact`
|
|
3
|
+
Providers declare their output files as :class:`~processforge.types.OutputArtifact`
|
|
5
4
|
records (with ``local_path``); the :class:`ArtifactStore` computes a content
|
|
6
5
|
hash, uploads each to object storage under a deterministic, reproducible key,
|
|
7
6
|
and fills in ``remote_uris``. The CLI and the container server both use this
|
|
@@ -120,13 +119,58 @@ class ArtifactStore:
|
|
|
120
119
|
# Container-side convenience: persist all artifacts in engine outputs
|
|
121
120
|
# ------------------------------------------------------------------
|
|
122
121
|
def persist_outputs(self, outputs, *, run_id: str = "", flowsheet_hash: str = "", unit: str = ""):
|
|
123
|
-
"""Upload every artifact referenced by *outputs
|
|
122
|
+
"""Upload every artifact referenced by *outputs* to object storage.
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
Uploads the explicitly-declared artifacts, then sweeps each output's
|
|
125
|
+
``run_dir`` for any remaining files so *all* model outputs (not just the
|
|
126
|
+
ones a provider happened to declare) reach S3. Newly swept files are
|
|
127
|
+
appended to ``out.artifacts`` with ``remote_uris`` filled. Mutates the
|
|
128
|
+
artifacts in place. ``outputs`` may be a single object or an iterable.
|
|
127
129
|
"""
|
|
130
|
+
if not self.bucket:
|
|
131
|
+
logger.debug(
|
|
132
|
+
"Remote artifact upload disabled (S3_BUCKET not set); provider "
|
|
133
|
+
"run outputs remain in the container's local scratch only."
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
|
|
128
137
|
items = outputs if isinstance(outputs, (list, tuple)) else [outputs]
|
|
138
|
+
uploaded = 0
|
|
139
|
+
local_only = 0
|
|
129
140
|
for out in items:
|
|
141
|
+
seen_paths: set[str] = set()
|
|
130
142
|
for art in getattr(out, "artifacts", []):
|
|
131
|
-
if art.local_path
|
|
132
|
-
|
|
143
|
+
if not art.local_path:
|
|
144
|
+
continue
|
|
145
|
+
seen_paths.add(os.path.abspath(art.local_path))
|
|
146
|
+
self.upload(art, run_id=run_id, flowsheet_hash=flowsheet_hash, unit=unit)
|
|
147
|
+
if art.remote_uris:
|
|
148
|
+
uploaded += 1
|
|
149
|
+
else:
|
|
150
|
+
local_only += 1
|
|
151
|
+
|
|
152
|
+
run_dir = getattr(out, "run_dir", "") or ""
|
|
153
|
+
if run_dir and os.path.isdir(run_dir):
|
|
154
|
+
for root, _dirs, files in os.walk(run_dir):
|
|
155
|
+
for name in files:
|
|
156
|
+
path = os.path.abspath(os.path.join(root, name))
|
|
157
|
+
if path in seen_paths:
|
|
158
|
+
continue
|
|
159
|
+
seen_paths.add(path)
|
|
160
|
+
art = OutputArtifact(
|
|
161
|
+
name=name,
|
|
162
|
+
kind=_artifact_kind(name),
|
|
163
|
+
local_path=path,
|
|
164
|
+
source="local",
|
|
165
|
+
)
|
|
166
|
+
self.upload(art, run_id=run_id, flowsheet_hash=flowsheet_hash, unit=unit)
|
|
167
|
+
if art.remote_uris:
|
|
168
|
+
out.artifacts.append(art)
|
|
169
|
+
uploaded += 1
|
|
170
|
+
else:
|
|
171
|
+
local_only += 1
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
f"Artifact persistence: {uploaded} uploaded to S3, {local_only} local-only."
|
|
175
|
+
)
|
|
176
|
+
|
|
@@ -10,14 +10,13 @@ as a thin HTTP wrapper around that server.
|
|
|
10
10
|
Request/response contract (matches ``processforge.api.serve``)
|
|
11
11
|
----------------------------------------------------------------
|
|
12
12
|
* ``POST /run`` with a JSON body
|
|
13
|
-
``{"unit_config", "materials", "inlet", "
|
|
13
|
+
``{"unit_config", "materials", "inlet", "provider_config"}``.
|
|
14
14
|
* JSON response ``{"status", "sim_type", <scalars…>, "metadata"}`` deserialised
|
|
15
15
|
into a :class:`~processforge.types.SimulationResult`.
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
|
-
import os
|
|
21
20
|
import urllib.error
|
|
22
21
|
import urllib.request
|
|
23
22
|
from typing import TYPE_CHECKING, Optional
|
|
@@ -54,7 +53,6 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
54
53
|
def __init__(self, provider_type: str):
|
|
55
54
|
self._ptype = provider_type
|
|
56
55
|
self._url: Optional[str] = None
|
|
57
|
-
self._provider_output_dir: str = "outputs"
|
|
58
56
|
self._provider_config: Optional["ProviderConfig"] = None
|
|
59
57
|
self._materials: dict = {}
|
|
60
58
|
self._initialized: bool = False
|
|
@@ -91,6 +89,7 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
91
89
|
"out": unit_config.out,
|
|
92
90
|
"sim_type": unit_config.sim_type,
|
|
93
91
|
"solver_config": unit_config.solver_config,
|
|
92
|
+
"geometry_config": unit_config.geometry_config,
|
|
94
93
|
**unit_config.extra,
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -122,14 +121,6 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
122
121
|
"""Store config, resolve URL, verify the Docker service is reachable."""
|
|
123
122
|
self._url = self._resolve_url(provider_config)
|
|
124
123
|
|
|
125
|
-
out_dir = os.path.expandvars(
|
|
126
|
-
getattr(provider_config, "output_dir", "outputs") or "outputs"
|
|
127
|
-
)
|
|
128
|
-
if not os.path.isabs(out_dir):
|
|
129
|
-
root = os.environ.get("PROCESSFORGE_OUTPUT_DIR", "outputs")
|
|
130
|
-
out_dir = os.path.join(root, out_dir)
|
|
131
|
-
self._provider_output_dir = out_dir
|
|
132
|
-
|
|
133
124
|
# Material registry — serialised into /run and used by validate_material().
|
|
134
125
|
self._materials = dict(flowsheet_config.materials)
|
|
135
126
|
self._provider_config = provider_config
|
|
@@ -190,7 +181,6 @@ class ContainerProviderClient(AbstractProvider):
|
|
|
190
181
|
"unit_config": self._serialize_unit_config(unit_config),
|
|
191
182
|
"materials": self._serialize_materials(self._materials),
|
|
192
183
|
"inlet": inlet,
|
|
193
|
-
"output_dir": self._provider_output_dir,
|
|
194
184
|
"provider_config": provider_config_dump,
|
|
195
185
|
"run_id": run_id,
|
|
196
186
|
"flowsheet_hash": flowsheet_hash,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Provider-side run-error classification.
|
|
2
|
+
|
|
3
|
+
When a simulation is executed *inside* a provider/engine (e.g. OpenMC or FESTIM
|
|
4
|
+
running in a Docker container) the run can fail for reasons that live entirely
|
|
5
|
+
in the engine — missing nuclear-data temperatures, an MPI process abort, a
|
|
6
|
+
geometry that excludes a source point, a tally that references an unknown
|
|
7
|
+
filter, … . These are distinct from *flowsheet/setup* errors (bad JSON, unknown
|
|
8
|
+
material, schema validation), which are caught earlier during initialization or
|
|
9
|
+
flowsheet validation.
|
|
10
|
+
|
|
11
|
+
This module gives every provider a single, structured way to capture such a
|
|
12
|
+
run-time failure:
|
|
13
|
+
|
|
14
|
+
* :class:`ProviderRunError` — a typed, engine-agnostic record of what failed,
|
|
15
|
+
attributed to ``source="provider"`` so Processforge (and the user) can tell a
|
|
16
|
+
runtime engine error apart from a flowsheet configuration error.
|
|
17
|
+
* :func:`classify_run_error` — turn an exception (plus any captured
|
|
18
|
+
stdout/stderr from the engine) into a :class:`ProviderRunError`, picking a
|
|
19
|
+
category and a
|
|
20
|
+
concrete remediation hint where one is known.
|
|
21
|
+
* :func:`make_failed_output` — build the ``EngineOutput(status="failed")`` that
|
|
22
|
+
providers return, populated with the classification.
|
|
23
|
+
|
|
24
|
+
Categories are intentionally broad so a new engine signature can be added here
|
|
25
|
+
without touching the providers.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
# Error categories. Strings (not an Enum) so they serialize cleanly in JSON
|
|
35
|
+
# through the provider HTTP API and are easy to match on in tests/docs.
|
|
36
|
+
NUCLEAR_DATA = "nuclear_data"
|
|
37
|
+
CROSS_SECTIONS = "cross_sections"
|
|
38
|
+
MPI_ABORT = "mpi_abort"
|
|
39
|
+
GEOMETRY = "geometry"
|
|
40
|
+
TALLY = "tally"
|
|
41
|
+
CONVERGENCE = "convergence"
|
|
42
|
+
INPUT_VALIDATION = "input_validation"
|
|
43
|
+
ENVIRONMENT = "environment"
|
|
44
|
+
UNKNOWN = "unknown"
|
|
45
|
+
|
|
46
|
+
# (category, compiled-regex, remediation hint). First match wins.
|
|
47
|
+
# Order matters: more specific (and more actionable) patterns come first.
|
|
48
|
+
_ERROR_SIGNATURES: list[tuple[str, re.Pattern[str], str]] = [
|
|
49
|
+
(
|
|
50
|
+
NUCLEAR_DATA,
|
|
51
|
+
re.compile(
|
|
52
|
+
r"nuclear data library does not contain cross sections",
|
|
53
|
+
re.IGNORECASE,
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"The cross-section library lacks data at the requested "
|
|
57
|
+
"temperature. "
|
|
58
|
+
"Either set the material temperature to a value present in the library "
|
|
59
|
+
"(e.g. 300 K), or enable openmc.Settings.temperature handling "
|
|
60
|
+
"(temperature_method / multipole interpolation) so intermediate "
|
|
61
|
+
"temperatures are treated."
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
CROSS_SECTIONS,
|
|
66
|
+
re.compile(
|
|
67
|
+
r"cross section[s]? (file|library|data|xml)|"
|
|
68
|
+
r"could not (find|read|open).*cross_section|"
|
|
69
|
+
r"no cross sections (available|found)|"
|
|
70
|
+
r"cross_sections\.xml",
|
|
71
|
+
re.IGNORECASE,
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"Cross-section data could not be located or parsed. Verify the "
|
|
75
|
+
"provider 'cross_sections' path points at a valid cross_sections.xml "
|
|
76
|
+
"and that the data directory is mounted into the container."
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
MPI_ABORT,
|
|
81
|
+
re.compile(r"MPI_ABORT|mpi abort|orted: \w+ rank", re.IGNORECASE),
|
|
82
|
+
(
|
|
83
|
+
"The solver process crashed (MPI_ABORT). The root cause is usually the "
|
|
84
|
+
"preceding engine error (e.g. nuclear-data or geometry failure); fix "
|
|
85
|
+
"that first. If it occurs with no preceding error, the container may "
|
|
86
|
+
"be out of memory or MPI may be misconfigured."
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
GEOMETRY,
|
|
91
|
+
re.compile(
|
|
92
|
+
r"geometry ?error|cannot find cell|no cell found|"
|
|
93
|
+
r"particle (got )?lost|surface .*? not (found|defined)|"
|
|
94
|
+
r"universe .*? not (found|defined)|geometry does not contain",
|
|
95
|
+
re.IGNORECASE,
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
"The problem geometry is invalid or the source/particles fall outside "
|
|
99
|
+
"it. Check geometry_config dimensions and the source_point location, "
|
|
100
|
+
"and ensure every referenced material region is filled."
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
TALLY,
|
|
105
|
+
re.compile(r"tally ?error|tally .*? (not|does not) (exist|have)|invalid filter", re.IGNORECASE),
|
|
106
|
+
(
|
|
107
|
+
"A tally or its filter is invalid. Check mesh_tallies / tally scores, "
|
|
108
|
+
"filter IDs, and that referenced cells/meshes exist in the geometry."
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
CONVERGENCE,
|
|
113
|
+
re.compile(
|
|
114
|
+
r"did not converge|maximum number of (iterations|resampling)|"
|
|
115
|
+
r"failed to converge|stagnat",
|
|
116
|
+
re.IGNORECASE,
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"The solve did not converge. Increase batches/iterations or relax "
|
|
120
|
+
"tolerances in solver_config; verify the model is well-posed."
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
(
|
|
124
|
+
INPUT_VALIDATION,
|
|
125
|
+
re.compile(
|
|
126
|
+
r"valueerror|keyerror|typeerror|validation ?error|"
|
|
127
|
+
r"expected .*? (got|found)|missing .*? argument",
|
|
128
|
+
re.IGNORECASE,
|
|
129
|
+
),
|
|
130
|
+
(
|
|
131
|
+
"The engine rejected the resolved configuration. This usually means a "
|
|
132
|
+
"value in solver_config/geometry_config is out of range or mistyped."
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
(
|
|
136
|
+
ENVIRONMENT,
|
|
137
|
+
re.compile(
|
|
138
|
+
r"permission denied|no such file or directory|"
|
|
139
|
+
r"cannot (write|create) .*? directory|"
|
|
140
|
+
r"shared library|lib[a-z0-9]+\.so",
|
|
141
|
+
re.IGNORECASE,
|
|
142
|
+
),
|
|
143
|
+
(
|
|
144
|
+
"An environment/container issue (file permissions, missing library, "
|
|
145
|
+
"or unwritable output dir). Check the container mounts and that the "
|
|
146
|
+
"output directory is writable."
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ProviderRunError(BaseModel):
|
|
153
|
+
"""Structured record of a run-time failure that originated in a provider/engine.
|
|
154
|
+
|
|
155
|
+
Attributes:
|
|
156
|
+
category: One of the ``*`` constants above (``nuclear_data``, ``mpi_abort``,
|
|
157
|
+
…). ``unknown`` when no signature matched.
|
|
158
|
+
source: Always ``"provider"`` — distinguishes engine run-time failures
|
|
159
|
+
from flowsheet/setup validation errors.
|
|
160
|
+
message: A concise, human-readable one-line summary of the failure.
|
|
161
|
+
type: The Python exception class name (e.g. ``RuntimeError``).
|
|
162
|
+
detail: The full captured error text (engine stderr/stdout + traceback
|
|
163
|
+
tail). May be long; this is what gets logged.
|
|
164
|
+
hint: A concrete remediation suggestion where a category is recognised,
|
|
165
|
+
else a generic pointer to check the engine log.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
category: str = UNKNOWN
|
|
169
|
+
source: str = "provider"
|
|
170
|
+
message: str = ""
|
|
171
|
+
type: str = "Exception"
|
|
172
|
+
detail: str = ""
|
|
173
|
+
hint: str = (
|
|
174
|
+
"Inspect the engine log / run directory for the root cause; this is a "
|
|
175
|
+
"run-time error raised by the provider, not a flowsheet configuration error."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_exception(
|
|
180
|
+
cls,
|
|
181
|
+
exc: BaseException,
|
|
182
|
+
captured: str = "",
|
|
183
|
+
category: Optional[str] = None,
|
|
184
|
+
) -> "ProviderRunError":
|
|
185
|
+
"""Build a :class:`ProviderRunError` from an exception (category overridable)."""
|
|
186
|
+
text = str(exc)
|
|
187
|
+
if captured:
|
|
188
|
+
text = f"{text}\n\n{captured}".strip()
|
|
189
|
+
# Prefer an explicitly supplied category (e.g. a provider already knows).
|
|
190
|
+
if category is None:
|
|
191
|
+
category = _classify_text(text)
|
|
192
|
+
return cls(
|
|
193
|
+
category=category,
|
|
194
|
+
message=_summarize(text) or f"{type(exc).__name__}: {text}",
|
|
195
|
+
type=type(exc).__name__,
|
|
196
|
+
detail=text,
|
|
197
|
+
hint=_hint_for(category),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def classify_run_error(
|
|
202
|
+
engine: str,
|
|
203
|
+
exc: BaseException,
|
|
204
|
+
captured: str = "",
|
|
205
|
+
) -> ProviderRunError:
|
|
206
|
+
"""Classify a provider run-time exception into a :class:`ProviderRunError`.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
engine: Engine name (``"openmc"``, ``"festim"``, …) — recorded for context
|
|
210
|
+
only (it does not change classification today).
|
|
211
|
+
exc: The exception raised by the engine run.
|
|
212
|
+
captured: Optional captured engine stdout/stderr, used to improve the
|
|
213
|
+
classification when the raised message is terse.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
A populated :class:`ProviderRunError` (``source="provider"``).
|
|
217
|
+
"""
|
|
218
|
+
err = ProviderRunError.from_exception(exc, captured=captured)
|
|
219
|
+
err.message = f"[{engine}] {err.message}"
|
|
220
|
+
return err
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def make_failed_output(
|
|
224
|
+
engine: str,
|
|
225
|
+
sim_type: str,
|
|
226
|
+
run_dir,
|
|
227
|
+
err: ProviderRunError,
|
|
228
|
+
unit: str = "",
|
|
229
|
+
) -> "EngineOutput": # type: ignore[name-defined] # imported lazily to avoid cycle
|
|
230
|
+
"""Build the ``EngineOutput(status="failed")`` providers return on a run error."""
|
|
231
|
+
from processforge.types import EngineOutput
|
|
232
|
+
|
|
233
|
+
return EngineOutput(
|
|
234
|
+
status="failed",
|
|
235
|
+
engine=engine,
|
|
236
|
+
sim_type=sim_type,
|
|
237
|
+
unit=unit,
|
|
238
|
+
error=err,
|
|
239
|
+
run_dir=str(getattr(run_dir, "resolve", lambda: run_dir)()),
|
|
240
|
+
diagnostics={
|
|
241
|
+
"run_dir": str(getattr(run_dir, "resolve", lambda: run_dir)()),
|
|
242
|
+
"error": err.detail,
|
|
243
|
+
"error_category": err.category,
|
|
244
|
+
"error_source": err.source,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _classify_text(text: str) -> str:
|
|
250
|
+
for category, pattern, _hint in _ERROR_SIGNATURES:
|
|
251
|
+
if pattern.search(text):
|
|
252
|
+
return category
|
|
253
|
+
return UNKNOWN
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _hint_for(category: str) -> str:
|
|
257
|
+
for cat, _pattern, hint in _ERROR_SIGNATURES:
|
|
258
|
+
if cat == category:
|
|
259
|
+
return hint
|
|
260
|
+
return ProviderRunError().hint
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _summarize(text: str, limit: int = 280) -> str:
|
|
264
|
+
"""Return the most diagnostic single line of ``text`` (engine ERROR lines win)."""
|
|
265
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
266
|
+
if not lines:
|
|
267
|
+
return ""
|
|
268
|
+
for ln in lines:
|
|
269
|
+
if ln.upper().startswith("ERROR") or "ERROR:" in ln.upper():
|
|
270
|
+
return ln[:limit]
|
|
271
|
+
# Fall back to the first line that looks like a message (not a banner/traceback).
|
|
272
|
+
for ln in lines:
|
|
273
|
+
if ln.startswith(("Traceback", "File ", "raise ", "Proc:", "NOTE:")):
|
|
274
|
+
continue
|
|
275
|
+
return ln[:limit]
|
|
276
|
+
return lines[0][:limit]
|
|
@@ -67,6 +67,7 @@ from processforge.types import (
|
|
|
67
67
|
OutputField,
|
|
68
68
|
OutputProvenance,
|
|
69
69
|
)
|
|
70
|
+
from processforge.providers.errors import classify_run_error, make_failed_output
|
|
70
71
|
from processforge.units import Quantity
|
|
71
72
|
|
|
72
73
|
if TYPE_CHECKING:
|
|
@@ -771,15 +772,8 @@ class FestimProvider(AbstractProvider):
|
|
|
771
772
|
logger.exception(
|
|
772
773
|
f"FestimProvider: '{sim_type}' failed in '{run_dir}': {exc}"
|
|
773
774
|
)
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
engine="festim",
|
|
777
|
-
sim_type=sim_type,
|
|
778
|
-
diagnostics={
|
|
779
|
-
"run_dir": str(run_dir.resolve()),
|
|
780
|
-
"error": str(exc),
|
|
781
|
-
},
|
|
782
|
-
)
|
|
775
|
+
err = classify_run_error("festim", exc)
|
|
776
|
+
return make_failed_output("festim", sim_type, run_dir, err)
|
|
783
777
|
finally:
|
|
784
778
|
os.chdir(prev_cwd)
|
|
785
779
|
|
|
@@ -794,6 +788,7 @@ class FestimProvider(AbstractProvider):
|
|
|
794
788
|
artifacts=artifacts,
|
|
795
789
|
diagnostics=diagnostics,
|
|
796
790
|
provenance=OutputProvenance(),
|
|
791
|
+
run_dir=str(run_dir),
|
|
797
792
|
)
|
|
798
793
|
|
|
799
794
|
def _resolve_run_dir(self) -> pathlib.Path:
|