processforge 0.2.40__tar.gz → 0.3.1__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.

Files changed (117) hide show
  1. {processforge-0.2.40 → processforge-0.3.1}/PKG-INFO +8 -4
  2. {processforge-0.2.40 → processforge-0.3.1}/README.md +6 -3
  3. {processforge-0.2.40 → processforge-0.3.1}/pyproject.toml +2 -1
  4. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/__init__.py +1 -1
  5. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/result.py +163 -57
  6. processforge-0.3.1/src/processforge/result_schema.py +26 -0
  7. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/types.py +51 -115
  8. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/PKG-INFO +8 -4
  9. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/SOURCES.txt +3 -1
  10. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/requires.txt +1 -0
  11. processforge-0.3.1/tests/test_types.py +440 -0
  12. {processforge-0.2.40 → processforge-0.3.1}/LICENSE +0 -0
  13. {processforge-0.2.40 → processforge-0.3.1}/MANIFEST.in +0 -0
  14. {processforge-0.2.40 → processforge-0.3.1}/flowsheets/closed-loop-chain.json +0 -0
  15. {processforge-0.2.40 → processforge-0.3.1}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
  16. {processforge-0.2.40 → processforge-0.3.1}/flowsheets/hydraulic-chain.json +0 -0
  17. {processforge-0.2.40 → processforge-0.3.1}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  18. {processforge-0.2.40 → processforge-0.3.1}/setup.cfg +0 -0
  19. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/__main__.py +0 -0
  20. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/_schema.py +0 -0
  21. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/analysis/__init__.py +0 -0
  22. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/analysis/dof.py +0 -0
  23. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/__init__.py +0 -0
  24. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/apply.py +0 -0
  25. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/common.py +0 -0
  26. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/diagram.py +0 -0
  27. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/display.py +0 -0
  28. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/export_fmu.py +0 -0
  29. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/export_modelica.py +0 -0
  30. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/init.py +0 -0
  31. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/plan.py +0 -0
  32. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/run.py +0 -0
  33. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/cli/validate.py +0 -0
  34. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/compose.py +0 -0
  35. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/__init__.py +0 -0
  36. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/backends/__init__.py +0 -0
  37. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/backends/base.py +0 -0
  38. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/backends/casadi_backend.py +0 -0
  39. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  40. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/backends/scipy_backend.py +0 -0
  41. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/flowsheet.py +0 -0
  42. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/jacobian.py +0 -0
  43. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/mixin.py +0 -0
  44. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/solver.py +0 -0
  45. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/stream_var.py +0 -0
  46. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/__init__.py +0 -0
  47. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/flash_eo.py +0 -0
  48. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/heater_eo.py +0 -0
  49. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/pipes_eo.py +0 -0
  50. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/pump_eo.py +0 -0
  51. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/strainer_eo.py +0 -0
  52. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/eo/units/valve_eo.py +0 -0
  53. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/flowsheet.py +0 -0
  54. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/fmu/__init__.py +0 -0
  55. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/fmu/_fmi_vars.py +0 -0
  56. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/fmu/builder.py +0 -0
  57. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/fmu/slave_template.py +0 -0
  58. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/lock.py +0 -0
  59. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/modelica/__init__.py +0 -0
  60. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/modelica/mo_writer.py +0 -0
  61. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/modelica/omc_runner.py +0 -0
  62. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/modelica/transpiler.py +0 -0
  63. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/modelica/unit_equations.py +0 -0
  64. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/pcl/__init__.py +0 -0
  65. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/pcl/compiler.py +0 -0
  66. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/pcl/namespace.py +0 -0
  67. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/provenance.py +0 -0
  68. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/__init__.py +0 -0
  69. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/base.py +0 -0
  70. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  71. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/cantera_jacobian.py +0 -0
  72. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/cantera_provider.py +0 -0
  73. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/coolprop_provider.py +0 -0
  74. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/festim_provider.py +0 -0
  75. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/jacobian_contributor.py +0 -0
  76. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/manager.py +0 -0
  77. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/modelica_jacobian.py +0 -0
  78. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/modelica_provider.py +0 -0
  79. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/openmc_provider.py +0 -0
  80. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/reference_state_registry.py +0 -0
  81. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/providers/registry.py +0 -0
  82. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/schemas/__init__.py +0 -0
  83. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/schemas/flowsheet_schema.json +0 -0
  84. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  85. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/simulate.py +0 -0
  86. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/solver.py +0 -0
  87. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/state.py +0 -0
  88. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/thermo.py +0 -0
  89. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/__init__.py +0 -0
  90. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/cstr.py +0 -0
  91. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/flash.py +0 -0
  92. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/heater.py +0 -0
  93. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/pfr.py +0 -0
  94. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/pipes.py +0 -0
  95. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/provider_mixin.py +0 -0
  96. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/pump.py +0 -0
  97. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/registry.py +0 -0
  98. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/solver.py +0 -0
  99. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/solver_unit.py +0 -0
  100. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/strainer.py +0 -0
  101. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/tank.py +0 -0
  102. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/units/valve.py +0 -0
  103. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/__init__.py +0 -0
  104. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/flowsheet_diagram.py +0 -0
  105. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/mermaid_diagram.py +0 -0
  106. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/topology.py +0 -0
  107. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/unit_consistency.py +0 -0
  108. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/validate_flowsheet.py +0 -0
  109. {processforge-0.2.40 → processforge-0.3.1}/src/processforge/utils/validation.py +0 -0
  110. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/dependency_links.txt +0 -0
  111. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/entry_points.txt +0 -0
  112. {processforge-0.2.40 → processforge-0.3.1}/src/processforge.egg-info/top_level.txt +0 -0
  113. {processforge-0.2.40 → processforge-0.3.1}/tests/test_cli.py +0 -0
  114. {processforge-0.2.40 → processforge-0.3.1}/tests/test_festim_provider.py +0 -0
  115. {processforge-0.2.40 → processforge-0.3.1}/tests/test_init.py +0 -0
  116. {processforge-0.2.40 → processforge-0.3.1}/tests/test_providers.py +0 -0
  117. {processforge-0.2.40 → processforge-0.3.1}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.2.40
3
+ Version: 0.3.1
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
@@ -30,6 +30,7 @@ Requires-Dist: pandas>=2.2
30
30
  Requires-Dist: zarr>=3.0
31
31
  Requires-Dist: coolprop>=7.2.0
32
32
  Requires-Dist: typer>=0.12
33
+ Requires-Dist: pip>=26.1.2
33
34
  Provides-Extra: eo
34
35
  Requires-Dist: pyomo>=6.7; extra == "eo"
35
36
  Provides-Extra: eo-casadi
@@ -112,7 +113,8 @@ uv tool install "processforge[modelica]"
112
113
  4. **Look at the output**
113
114
 
114
115
  `pf apply` writes results under `outputs/`:
115
- - `*_results.zarr` — simulation results store
116
+ - `*_results.zarr` — simulation results store (per-variable arrays, composition flattened)
117
+ - `*_results.zarr.schema.json` — schema file describing streams, variables, dtypes, units, shapes, and run provenance
116
118
  - `*_validation.xlsx` — validation report
117
119
  - `*.pfstate/` — versioned snapshot store with a `latest` pointer
118
120
 
@@ -127,8 +129,10 @@ fs = EOFlowsheet(config, backend="scipy")
127
129
  results = fs.run()
128
130
  ```
129
131
 
130
- ## Usage and Docker
131
- 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.
132
+ ## Usage
133
+ See the [usage guide](docs/usage.md) for CLI commands and workflows.
134
+
135
+ Individual providers (e.g. FESTIM, OpenMC) can be run via Docker images using the provider image contract.
132
136
 
133
137
  ## Flowsheets
134
138
  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.
@@ -53,7 +53,8 @@ uv tool install "processforge[modelica]"
53
53
  4. **Look at the output**
54
54
 
55
55
  `pf apply` writes results under `outputs/`:
56
- - `*_results.zarr` — simulation results store
56
+ - `*_results.zarr` — simulation results store (per-variable arrays, composition flattened)
57
+ - `*_results.zarr.schema.json` — schema file describing streams, variables, dtypes, units, shapes, and run provenance
57
58
  - `*_validation.xlsx` — validation report
58
59
  - `*.pfstate/` — versioned snapshot store with a `latest` pointer
59
60
 
@@ -68,8 +69,10 @@ fs = EOFlowsheet(config, backend="scipy")
68
69
  results = fs.run()
69
70
  ```
70
71
 
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.
72
+ ## Usage
73
+ See the [usage guide](docs/usage.md) for CLI commands and workflows.
74
+
75
+ Individual providers (e.g. FESTIM, OpenMC) can be run via Docker images using the provider image contract.
73
76
 
74
77
  ## Flowsheets
75
78
  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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "processforge"
7
- version = "0.2.40"
7
+ version = "0.3.1"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -36,6 +36,7 @@ dependencies = [
36
36
  "zarr>=3.0",
37
37
  "coolprop>=7.2.0",
38
38
  "typer>=0.12",
39
+ "pip>=26.1.2",
39
40
  ]
40
41
 
41
42
  [project.optional-dependencies]
@@ -29,7 +29,7 @@ from .units.flash import Flash
29
29
  from .units.heater import Heater
30
30
  from .eo import EOFlowsheet, EOSolver
31
31
 
32
- __version__ = "0.2.40"
32
+ __version__ = "0.3.0"
33
33
 
34
34
  __all__ = [
35
35
  "Flowsheet",
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import shutil
3
+ from datetime import datetime, timezone
3
4
 
4
5
  import numpy as np
5
6
  import pandas as pd
@@ -7,8 +8,11 @@ import matplotlib.pyplot as plt
7
8
  import zarr
8
9
  from loguru import logger
9
10
 
11
+ from .result_schema import ResultSchema, SolverUnitSchema, StreamSchema
10
12
  from .types import RunInfo
11
13
 
14
+ _VARIABLE_UNITS = {"T": "K", "P": "Pa", "flowrate": "mol/s", "beta": ""}
15
+
12
16
 
13
17
  def _ensure_parent_dir(path):
14
18
  os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
@@ -32,13 +36,19 @@ def _is_dynamic(results):
32
36
  def _store_stream(stream_group, stream_data):
33
37
  for key, value in stream_data.items():
34
38
  if key == "z" and isinstance(value, dict):
35
- comp_group = stream_group.create_group("__composition__")
36
- for comp, comp_values in sorted(value.items()):
37
- comp_arr = _ensure_array(comp_values)
38
- comp_group.create_array(comp, data=comp_arr)
39
+ for comp in sorted(value):
40
+ stream_group.create_array(comp, data=_ensure_array(value[comp]))
41
+ if value:
42
+ stream_group.attrs["composition"] = sorted(value)
39
43
  continue
40
- arr = _ensure_array(value)
41
- stream_group.create_array(key, data=arr)
44
+ stream_group.create_array(key, data=_ensure_array(value))
45
+
46
+
47
+ def _store_solver_unit(group, data):
48
+ for k, v in data.items():
49
+ if isinstance(v, (np.ndarray, np.generic)):
50
+ v = v.item()
51
+ group.attrs[k] = v
42
52
 
43
53
 
44
54
  def _normalize_run_info(run_info: RunInfo | dict) -> dict:
@@ -93,6 +103,88 @@ def _store_run_info(root, run_info: RunInfo | dict) -> None:
93
103
  ig_group.attrs["var_names"] = list(var_names)
94
104
 
95
105
 
106
+ def _friendly_dtype(dt: str) -> str:
107
+ dtype = str(dt)
108
+ if dtype.startswith(("<U", "|U")) or dtype.startswith(("<S", "|S")):
109
+ return "str"
110
+ return dtype
111
+
112
+
113
+ def _build_schema(root) -> ResultSchema:
114
+ from . import __version__ as _pf_version
115
+
116
+ mode = root.attrs.get("mode", "steady")
117
+ schema = ResultSchema(
118
+ created=datetime.now(timezone.utc).isoformat(),
119
+ mode=mode,
120
+ processforge_version=_pf_version,
121
+ )
122
+
123
+ for name in sorted(root.group_keys()):
124
+ if name == "run_info":
125
+ continue
126
+ group = root[name]
127
+ arrays = sorted(group.array_keys())
128
+ if arrays:
129
+ dtypes = {k: _friendly_dtype(group[k].dtype) for k in arrays}
130
+ units = {k: _VARIABLE_UNITS.get(k, "") for k in arrays}
131
+ n_rows = max(group[k].shape[0] for k in arrays) if arrays else 0
132
+ schema.streams[name] = StreamSchema(
133
+ variables=arrays,
134
+ dtypes=dtypes,
135
+ units=units,
136
+ shape=[n_rows, len(arrays)],
137
+ has_time="time" in arrays,
138
+ has_phase="phase" in arrays,
139
+ )
140
+ else:
141
+ attrs_keys = sorted(k for k in group.attrs if not k.startswith("_"))
142
+ if attrs_keys:
143
+ dtypes = {k: type(group.attrs[k]).__name__ for k in attrs_keys}
144
+ schema.solver_units[name] = SolverUnitSchema(
145
+ variables=attrs_keys, dtypes=dtypes
146
+ )
147
+
148
+ if "run_info" in root:
149
+ ri = root["run_info"]
150
+ prov = {}
151
+ for key in ("git_hash", "timestamp", "mode", "backend",
152
+ "python_version", "platform", "processforge_version"):
153
+ if key in ri.attrs:
154
+ prov[key] = ri.attrs[key]
155
+ if key == "processforge_version":
156
+ schema.processforge_version = ri.attrs[key]
157
+ if "pkg_versions" in ri:
158
+ prov["pkg_versions"] = dict(ri["pkg_versions"].attrs)
159
+ if prov:
160
+ schema.provenance = prov
161
+
162
+ return schema
163
+
164
+
165
+ def _write_schema(store_path):
166
+ store = zarr.storage.LocalStore(store_path)
167
+ root = zarr.open_group(store=store, mode="r")
168
+ schema = _build_schema(root)
169
+ schema_path = store_path + ".schema.json"
170
+ with open(schema_path, "w", encoding="utf-8") as f:
171
+ f.write(schema.model_dump_json(indent=2))
172
+ logger.debug(f"Wrote schema to {schema_path}")
173
+
174
+
175
+ def _write_schema_s3(s3_uri, storage_options):
176
+ import s3fs
177
+
178
+ store = zarr.storage.FsspecStore.from_url(s3_uri, storage_options=storage_options)
179
+ root = zarr.open_group(store=store, mode="r")
180
+ schema = _build_schema(root)
181
+ fs = s3fs.S3FileSystem(**storage_options)
182
+ schema_uri = s3_uri + ".schema.json"
183
+ with fs.open(schema_uri, "w") as f:
184
+ f.write(schema.model_dump_json(indent=2))
185
+ logger.debug(f"Wrote schema to {schema_uri}")
186
+
187
+
96
188
  def save_results_zarr(results, fname="results.zarr", run_info: RunInfo | dict | None = None):
97
189
  """Persist simulation results in a Zarr directory.
98
190
 
@@ -117,11 +209,27 @@ def save_results_zarr(results, fname="results.zarr", run_info: RunInfo | dict |
117
209
  root = zarr.group(store=store)
118
210
  mode = "dynamic" if _is_dynamic(results) else "steady"
119
211
  root.attrs["mode"] = mode
120
- for stream_name, stream_data in results.items():
121
- stream_group = root.create_group(stream_name)
122
- _store_stream(stream_group, stream_data)
212
+
213
+ streams = {}
214
+ solver_units = {}
215
+ for name, data in results.items():
216
+ if isinstance(data, dict) and "status" in data:
217
+ solver_units[name] = data
218
+ else:
219
+ streams[name] = data
220
+
221
+ for name, data in streams.items():
222
+ group = root.create_group(name)
223
+ _store_stream(group, data)
224
+
225
+ for name, data in solver_units.items():
226
+ group = root.create_group(name)
227
+ _store_solver_unit(group, data)
228
+
123
229
  if run_info is not None:
124
230
  _store_run_info(root, run_info)
231
+
232
+ _write_schema(store_path)
125
233
  logger.info(f"Saved Zarr results to {store_path}")
126
234
  return store_path
127
235
 
@@ -140,7 +248,6 @@ def save_results_zarr_s3(results, s3_uri: str, run_info: RunInfo | dict | None =
140
248
  run_info: Optional provenance metadata (same as :func:`save_results_zarr`).
141
249
  """
142
250
  import s3fs # noqa: F401 – registers the s3:// protocol with fsspec
143
- import os
144
251
 
145
252
  storage_options = {
146
253
  "key": os.environ.get("S3_ACCESS_KEY"),
@@ -160,11 +267,27 @@ def save_results_zarr_s3(results, s3_uri: str, run_info: RunInfo | dict | None =
160
267
  root = zarr.group(store=store, overwrite=True)
161
268
  mode = "dynamic" if _is_dynamic(results) else "steady"
162
269
  root.attrs["mode"] = mode
163
- for stream_name, stream_data in results.items():
164
- stream_group = root.create_group(stream_name)
165
- _store_stream(stream_group, stream_data)
270
+
271
+ streams = {}
272
+ solver_units = {}
273
+ for name, data in results.items():
274
+ if isinstance(data, dict) and "status" in data:
275
+ solver_units[name] = data
276
+ else:
277
+ streams[name] = data
278
+
279
+ for name, data in streams.items():
280
+ group = root.create_group(name)
281
+ _store_stream(group, data)
282
+
283
+ for name, data in solver_units.items():
284
+ group = root.create_group(name)
285
+ _store_solver_unit(group, data)
286
+
166
287
  if run_info is not None:
167
288
  _store_run_info(root, run_info)
289
+
290
+ _write_schema_s3(s3_uri, storage_options)
168
291
  logger.info(f"Saved Zarr results to {s3_uri}")
169
292
 
170
293
 
@@ -270,53 +393,36 @@ def _scalar_from_sequence(value):
270
393
  return _convert_value(candidate)
271
394
 
272
395
 
273
- def _get_zarr_value(dataset, idx):
274
- if dataset is None:
275
- return ""
276
- length = dataset.shape[0]
277
- if length == 0:
278
- return ""
279
- position = min(idx, length - 1)
280
- return _convert_value(dataset[position])
281
-
282
-
283
- def _build_dataframe_row(group, stream_name, idx, comp_names, include_time):
284
- row = {"stream": stream_name}
285
- if include_time and "time" in group:
286
- row["time"] = _get_zarr_value(group.get("time"), idx)
287
- row["T [K]"] = _get_zarr_value(group.get("T"), idx)
288
- row["P [Pa]"] = _get_zarr_value(group.get("P"), idx)
289
- row["Phase"] = _get_zarr_value(group.get("phase"), idx)
290
- row["VaporFrac"] = _get_zarr_value(group.get("beta"), idx)
291
- row["flowrate"] = _get_zarr_value(group.get("flowrate"), idx)
292
- comp_group = group.get("__composition__")
293
- if comp_group is not None:
294
- for comp in comp_names:
295
- row[comp] = _get_zarr_value(comp_group.get(comp), idx)
296
- return row
297
-
298
-
299
396
  def _load_dataframe_from_zarr(store_path):
300
397
  store = zarr.storage.LocalStore(store_path)
301
398
  root = zarr.open(store=store, mode="r")
302
- streams = sorted(k for k in root.group_keys() if k != "run_info")
303
399
  rows = []
304
- components = set()
305
- mode = root.attrs.get("mode", "steady")
306
- for stream in streams:
400
+
401
+ for stream in sorted(k for k in root.group_keys() if k != "run_info"):
307
402
  group = root[stream]
308
- comp_group = group.get("__composition__")
309
- comp_names = sorted(comp_group.keys()) if comp_group is not None else []
310
- components.update(comp_names)
311
- has_time = "time" in group and mode == "dynamic"
312
- length = group["time"].shape[0] if has_time else 1
313
- for idx in range(length):
314
- rows.append(_build_dataframe_row(group, stream, idx, comp_names, has_time))
315
- df = pd.DataFrame(rows)
316
- for comp in sorted(components):
317
- if comp in df:
318
- df[comp] = df[comp].fillna(0.0)
319
- else:
320
- df[comp] = 0.0
321
- return df
403
+ arrays = list(group.array_keys())
404
+ if not arrays:
405
+ continue
406
+
407
+ has_time = "time" in group
408
+ time_arr = group["time"][:] if has_time else None
409
+ has_phase = "phase" in group
410
+ phase_arr = group["phase"][:] if has_phase else None
411
+
412
+ data_vars = [k for k in arrays if k not in ("time", "phase")]
413
+ if not data_vars:
414
+ continue
415
+ n = max(group[k].shape[0] for k in data_vars)
416
+
417
+ for idx in range(n):
418
+ row = {"stream": stream}
419
+ if has_time:
420
+ row["time"] = float(time_arr[idx])
421
+ for var in data_vars:
422
+ row[var] = _convert_value(group[var][idx])
423
+ if has_phase:
424
+ row["phase"] = str(phase_arr[idx])
425
+ rows.append(row)
426
+
427
+ return pd.DataFrame(rows)
322
428
 
@@ -0,0 +1,26 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class StreamSchema(BaseModel):
5
+ variables: list[str]
6
+ dtypes: dict[str, str]
7
+ units: dict[str, str] = {}
8
+ shape: list[int]
9
+ has_time: bool = False
10
+ has_phase: bool = False
11
+
12
+
13
+ class SolverUnitSchema(BaseModel):
14
+ variables: list[str]
15
+ dtypes: dict[str, str]
16
+
17
+
18
+ class ResultSchema(BaseModel):
19
+ version: int = 1
20
+ store_type: str = "simulation_results"
21
+ created: str
22
+ mode: str
23
+ processforge_version: str = ""
24
+ streams: dict[str, StreamSchema] = {}
25
+ solver_units: dict[str, SolverUnitSchema] = {}
26
+ provenance: dict = {}