processforge 0.2.3__tar.gz → 0.2.5__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.
Files changed (60) hide show
  1. {processforge-0.2.3/src/processforge.egg-info → processforge-0.2.5}/PKG-INFO +1 -1
  2. {processforge-0.2.3 → processforge-0.2.5}/pyproject.toml +1 -1
  3. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/__init__.py +2 -0
  4. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/flowsheet.py +28 -0
  5. processforge-0.2.5/src/processforge/provenance.py +123 -0
  6. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/result.py +60 -3
  7. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/simulate.py +7 -1
  8. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/pump.py +1 -1
  9. {processforge-0.2.3 → processforge-0.2.5/src/processforge.egg-info}/PKG-INFO +1 -1
  10. {processforge-0.2.3 → processforge-0.2.5}/src/processforge.egg-info/SOURCES.txt +1 -0
  11. {processforge-0.2.3 → processforge-0.2.5}/LICENSE +0 -0
  12. {processforge-0.2.3 → processforge-0.2.5}/MANIFEST.in +0 -0
  13. {processforge-0.2.3 → processforge-0.2.5}/README.md +0 -0
  14. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/archive/example_dynamic_hybrid.json +0 -0
  15. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/archive/example_dynamic_tank.json +0 -0
  16. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/archive/example_flash.json +0 -0
  17. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/archive/hydraulic_chain.json +0 -0
  18. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/closed-loop-chain.json +0 -0
  19. {processforge-0.2.3 → processforge-0.2.5}/flowsheets/hydraulic-chain.json +0 -0
  20. {processforge-0.2.3 → processforge-0.2.5}/setup.cfg +0 -0
  21. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/_schema.py +0 -0
  22. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/__init__.py +0 -0
  23. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/backends/__init__.py +0 -0
  24. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/backends/base.py +0 -0
  25. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/backends/casadi_backend.py +0 -0
  26. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  27. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/backends/scipy_backend.py +0 -0
  28. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/jacobian.py +0 -0
  29. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/mixin.py +0 -0
  30. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/solver.py +0 -0
  31. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/stream_var.py +0 -0
  32. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/__init__.py +0 -0
  33. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/flash_eo.py +0 -0
  34. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/heater_eo.py +0 -0
  35. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/pipes_eo.py +0 -0
  36. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/pump_eo.py +0 -0
  37. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/strainer_eo.py +0 -0
  38. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/eo/units/valve_eo.py +0 -0
  39. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/flowsheet.py +0 -0
  40. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/schemas/__init__.py +0 -0
  41. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/schemas/flowsheet_schema.json +0 -0
  42. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/solver.py +0 -0
  43. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/thermo.py +0 -0
  44. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/__init__.py +0 -0
  45. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/flash.py +0 -0
  46. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/heater.py +0 -0
  47. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/pipes.py +0 -0
  48. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/solver.py +0 -0
  49. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/strainer.py +0 -0
  50. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/tank.py +0 -0
  51. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/units/valve.py +0 -0
  52. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/utils/__init__.py +0 -0
  53. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/utils/flowsheet_diagram.py +0 -0
  54. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/utils/validate_flowsheet.py +0 -0
  55. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/utils/validation.py +0 -0
  56. {processforge-0.2.3 → processforge-0.2.5}/src/processforge/validate.py +0 -0
  57. {processforge-0.2.3 → processforge-0.2.5}/src/processforge.egg-info/dependency_links.txt +0 -0
  58. {processforge-0.2.3 → processforge-0.2.5}/src/processforge.egg-info/entry_points.txt +0 -0
  59. {processforge-0.2.3 → processforge-0.2.5}/src/processforge.egg-info/requires.txt +0 -0
  60. {processforge-0.2.3 → processforge-0.2.5}/src/processforge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A Python-based process simulation framework for chemical engineering applications.
5
5
  Author-email: Process Forge Team <team@processforge.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "processforge"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -11,6 +11,7 @@ Provides steady-state and dynamic process simulation capabilities including:
11
11
  """
12
12
 
13
13
  from .flowsheet import Flowsheet
14
+ from .provenance import build_run_info
14
15
  from .solver import Solver
15
16
  from .thermo import get_enthalpy_molar, get_Cp_molar, get_K_values, rachford_rice
16
17
  from .validate import validate_flowsheet
@@ -45,6 +46,7 @@ __all__ = [
45
46
  "plot_results",
46
47
  "plot_timeseries",
47
48
  "save_results_zarr",
49
+ "build_run_info",
48
50
  "Pump",
49
51
  "Valve",
50
52
  "Strainer",
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from copy import deepcopy
5
5
 
6
+ import numpy as np
6
7
  from loguru import logger
7
8
 
8
9
  from .jacobian import GlobalJacobianManager
@@ -64,12 +65,19 @@ class EOFlowsheet:
64
65
  """
65
66
  Build and solve the EO system.
66
67
 
68
+ After this method returns, ``self.x0`` holds the initial-guess vector
69
+ and ``self.var_names`` holds a human-readable label for each element —
70
+ both can be passed directly to
71
+ :func:`processforge.provenance.build_run_info` for reproducibility.
72
+
67
73
  Returns:
68
74
  Stream result dict in the same format as ``Flowsheet.run()``:
69
75
  ``{stream_name: {"T": ..., "P": ..., "flowrate": ..., "z": {...}}}``
70
76
  """
71
77
  manager = self._build()
72
78
  x0 = self._warm_start(manager)
79
+ self.x0: "np.ndarray" = x0.copy()
80
+ self.var_names: list[str] = self._build_var_names(manager)
73
81
  solver = EOSolver(backend=self.backend)
74
82
  x_sol, converged, stats = solver.solve(manager, x0)
75
83
  if not converged:
@@ -172,6 +180,26 @@ class EOFlowsheet:
172
180
  comps.update(stream.get("z", {}).keys())
173
181
  return sorted(comps)
174
182
 
183
+ # ------------------------------------------------------------------
184
+ # Provenance helpers
185
+ # ------------------------------------------------------------------
186
+
187
+ def _build_var_names(self, manager: "GlobalJacobianManager") -> list[str]:
188
+ """Return a human-readable label for every scalar in the x vector.
189
+
190
+ Variable order mirrors ``GlobalJacobianManager`` layout:
191
+ ``T``, ``P``, ``flowrate``, then one entry per component ``z_<comp>``
192
+ for each registered stream in registration order.
193
+ """
194
+ suffixes = ["T", "P", "flowrate"] + [
195
+ f"z_{c}" for c in manager.components
196
+ ]
197
+ names: list[str] = []
198
+ for stream_name in manager._streams:
199
+ for suffix in suffixes:
200
+ names.append(f"{stream_name}/{suffix}")
201
+ return names
202
+
175
203
  # ------------------------------------------------------------------
176
204
  # Warm-start
177
205
  # ------------------------------------------------------------------
@@ -0,0 +1,123 @@
1
+ """Provenance and metadata utilities for ProcessForge runs."""
2
+ from __future__ import annotations
3
+
4
+ import platform
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+
9
+ import numpy as np
10
+
11
+ _KEY_PACKAGES = [
12
+ "numpy",
13
+ "scipy",
14
+ "zarr",
15
+ "coolprop",
16
+ "loguru",
17
+ "pandas",
18
+ "openpyxl",
19
+ "matplotlib",
20
+ "jsonschema",
21
+ "graphviz",
22
+ ]
23
+
24
+
25
+ def _git_hash() -> str:
26
+ """Return the current git commit hash, or 'unknown' if not in a git repo."""
27
+ try:
28
+ result = subprocess.run(
29
+ ["git", "rev-parse", "HEAD"],
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=5,
33
+ )
34
+ if result.returncode == 0:
35
+ return result.stdout.strip()
36
+ except Exception:
37
+ pass
38
+ return "unknown"
39
+
40
+
41
+ def _package_versions(packages: list[str]) -> dict[str, str]:
42
+ """Return installed versions for each package (or 'unknown')."""
43
+ from importlib.metadata import PackageNotFoundError, version
44
+
45
+ versions: dict[str, str] = {}
46
+ for pkg in packages:
47
+ try:
48
+ versions[pkg] = version(pkg)
49
+ except PackageNotFoundError:
50
+ versions[pkg] = "unknown"
51
+ return versions
52
+
53
+
54
+ def build_run_info(
55
+ config: dict,
56
+ x0: np.ndarray | None = None,
57
+ var_names: list[str] | None = None,
58
+ ) -> dict:
59
+ """Build a run_info metadata dict for provenance tracking.
60
+
61
+ Args:
62
+ config: The validated flowsheet configuration dict.
63
+ x0: Initial guess vector (EO mode) or flattened feed-stream
64
+ initial conditions (dynamic mode).
65
+ var_names: Human-readable label for each element of x0 (e.g.
66
+ ``"feed/T"``, ``"feed/P"``, ``"product/z_H2O"``).
67
+
68
+ Returns:
69
+ A dict suitable for passing to ``save_results_zarr`` as *run_info*.
70
+ Keys:
71
+ - ``git_hash`` – current HEAD commit SHA
72
+ - ``timestamp`` – ISO-8601 UTC timestamp
73
+ - ``python_version`` – full ``sys.version`` string
74
+ - ``platform`` – ``platform.platform()`` string
75
+ - ``processforge_version`` – installed processforge version
76
+ - ``mode`` – ``"steady"`` or ``"dynamic"``
77
+ - ``backend`` – EO backend name (steady-state only)
78
+ - ``pkg_versions`` – dict of {package: version}
79
+ - ``x0`` – numpy float64 array or ``None``
80
+ - ``var_names`` – list of str labels or ``None``
81
+ """
82
+ from . import __version__ as _pf_version
83
+
84
+ sim_cfg = config.get("simulation", {})
85
+ return {
86
+ "git_hash": _git_hash(),
87
+ "timestamp": datetime.now(timezone.utc).isoformat(),
88
+ "python_version": sys.version,
89
+ "platform": platform.platform(),
90
+ "processforge_version": _pf_version,
91
+ "mode": sim_cfg.get("mode", "steady"),
92
+ "backend": sim_cfg.get("backend", "scipy"),
93
+ "pkg_versions": _package_versions(_KEY_PACKAGES),
94
+ "x0": np.asarray(x0, dtype=float) if x0 is not None else None,
95
+ "var_names": list(var_names) if var_names is not None else None,
96
+ }
97
+
98
+
99
+ def build_dynamic_x0(config: dict) -> tuple[np.ndarray, list[str]]:
100
+ """Flatten feed-stream initial conditions into a reproducibility vector.
101
+
102
+ For dynamic simulations there is no single ``x0`` passed to a global
103
+ solver; instead the starting state is fully determined by the feed streams
104
+ in the config. This helper packs those values into a flat numpy array so
105
+ the same schema used for EO runs can be applied here.
106
+
107
+ Returns:
108
+ (x0_array, var_names) where ``x0_array[i]`` corresponds to
109
+ ``var_names[i]`` (e.g. ``"feed/T"``, ``"feed/z_H2O"``).
110
+ """
111
+ values: list[float] = []
112
+ names: list[str] = []
113
+
114
+ for stream_name, stream in config.get("streams", {}).items():
115
+ for prop in ("T", "P", "flowrate"):
116
+ val = stream.get(prop, 0.0)
117
+ values.append(float(val))
118
+ names.append(f"{stream_name}/{prop}")
119
+ for comp, frac in sorted(stream.get("z", {}).items()):
120
+ values.append(float(frac))
121
+ names.append(f"{stream_name}/z_{comp}")
122
+
123
+ return np.asarray(values, dtype=float), names
@@ -39,8 +39,63 @@ def _store_stream(stream_group, stream_data):
39
39
  stream_group.create_dataset(key, data=arr, shape=arr.shape)
40
40
 
41
41
 
42
- def save_results_zarr(results, fname="results.zarr"):
43
- """Persist simulation results in a Zarr directory."""
42
+ def _store_run_info(root, run_info: dict) -> None:
43
+ """Persist provenance metadata in a ``run_info`` Zarr sub-group.
44
+
45
+ Layout::
46
+
47
+ run_info/
48
+ .attrs → git_hash, timestamp, python_version, platform,
49
+ processforge_version, mode, backend
50
+ pkg_versions/
51
+ .attrs → {package: version, ...}
52
+ initial_guess/
53
+ x0 – float64 array (length = n_vars)
54
+ .attrs → var_names (JSON list of string labels)
55
+ """
56
+ ri_group = root.create_group("run_info")
57
+
58
+ scalar_keys = [
59
+ "git_hash",
60
+ "timestamp",
61
+ "python_version",
62
+ "platform",
63
+ "processforge_version",
64
+ "mode",
65
+ "backend",
66
+ ]
67
+ ri_group.attrs.update(
68
+ {k: str(run_info[k]) for k in scalar_keys if k in run_info}
69
+ )
70
+
71
+ pkg_versions = run_info.get("pkg_versions") or {}
72
+ if pkg_versions:
73
+ pkg_group = ri_group.create_group("pkg_versions")
74
+ pkg_group.attrs.update({k: str(v) for k, v in pkg_versions.items()})
75
+
76
+ x0 = run_info.get("x0")
77
+ if x0 is not None:
78
+ x0_arr = np.asarray(x0, dtype=float)
79
+ ig_group = ri_group.create_group("initial_guess")
80
+ ig_group.create_dataset("x0", data=x0_arr, shape=x0_arr.shape)
81
+ var_names = run_info.get("var_names")
82
+ if var_names:
83
+ ig_group.attrs["var_names"] = list(var_names)
84
+
85
+
86
+ def save_results_zarr(results, fname="results.zarr", run_info=None):
87
+ """Persist simulation results in a Zarr directory.
88
+
89
+ Args:
90
+ results: Stream result dict as returned by ``Flowsheet.run()``
91
+ or ``EOFlowsheet.run()``.
92
+ fname: Output path for the Zarr store directory.
93
+ run_info: Optional provenance dict from
94
+ :func:`processforge.provenance.build_run_info`. When
95
+ supplied, a ``run_info`` sub-group is written inside the
96
+ store containing the git hash, package versions, and
97
+ initial-guess vector for full reproducibility.
98
+ """
44
99
  store_path = os.path.abspath(fname)
45
100
  if os.path.exists(store_path):
46
101
  if os.path.isdir(store_path):
@@ -55,6 +110,8 @@ def save_results_zarr(results, fname="results.zarr"):
55
110
  for stream_name, stream_data in results.items():
56
111
  stream_group = root.create_group(stream_name)
57
112
  _store_stream(stream_group, stream_data)
113
+ if run_info is not None:
114
+ _store_run_info(root, run_info)
58
115
  logger.info(f"Saved Zarr results to {store_path}")
59
116
  return store_path
60
117
 
@@ -190,7 +247,7 @@ def _build_dataframe_row(group, stream_name, idx, comp_names, include_time):
190
247
  def _load_dataframe_from_zarr(store_path):
191
248
  store = zarr.storage.LocalStore(store_path)
192
249
  root = zarr.open(store=store, mode="r")
193
- streams = sorted(root.group_keys())
250
+ streams = sorted(k for k in root.group_keys() if k != "run_info")
194
251
  rows = []
195
252
  components = set()
196
253
  mode = root.attrs.get("mode", "steady")
@@ -7,6 +7,7 @@ from loguru import logger
7
7
  from .utils.validate_flowsheet import validate_flowsheet
8
8
  from .flowsheet import Flowsheet
9
9
  from .eo import EOFlowsheet
10
+ from .provenance import build_dynamic_x0, build_run_info
10
11
  from .result import (
11
12
  generate_validation_excel,
12
13
  plot_results,
@@ -40,15 +41,20 @@ def _cmd_run(args):
40
41
  if is_dynamic:
41
42
  fs = Flowsheet(config)
42
43
  logger.info("=== Dynamic Results ===")
44
+ results = fs.run()
45
+ x0, var_names = build_dynamic_x0(config)
46
+ run_info = build_run_info(config, x0=x0, var_names=var_names)
43
47
  else:
44
48
  backend = sim_cfg.get("backend", "scipy")
45
49
  fs = EOFlowsheet(config, backend=backend)
46
50
  logger.info("=== Steady-State EO Results ===")
51
+ results = fs.run()
52
+ run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
47
53
 
48
- results = fs.run()
49
54
  zarr_path = save_results_zarr(
50
55
  results,
51
56
  os.path.join("outputs", f"{base_name}_results.zarr"),
57
+ run_info=run_info,
52
58
  )
53
59
  validation_path = os.path.join("outputs", f"{base_name}_validation.xlsx")
54
60
  generate_validation_excel(
@@ -49,7 +49,7 @@ class Pump(PumpEOMixin):
49
49
 
50
50
  # Approximate temperature rise (adiabatic inefficiency)
51
51
  Cp = 4180.0 # J/kg-K for water
52
- dT = (power * (1 - self.efficiency)) / (mass_flow * Cp)
52
+ dT = (power * (1 - self.efficiency)) / (mass_flow * Cp) if mass_flow > 0 else 0.0
53
53
  outlet["T"] = inlet["T"] + dT
54
54
 
55
55
  outlet["power"] = power
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A Python-based process simulation framework for chemical engineering applications.
5
5
  Author-email: Process Forge Team <team@processforge.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -11,6 +11,7 @@ flowsheets/archive/hydraulic_chain.json
11
11
  src/processforge/__init__.py
12
12
  src/processforge/_schema.py
13
13
  src/processforge/flowsheet.py
14
+ src/processforge/provenance.py
14
15
  src/processforge/result.py
15
16
  src/processforge/simulate.py
16
17
  src/processforge/solver.py
File without changes
File without changes
File without changes
File without changes