processforge 0.3.3__tar.gz → 0.3.4__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 (119) hide show
  1. {processforge-0.3.3/src/processforge.egg-info → processforge-0.3.4}/PKG-INFO +1 -1
  2. {processforge-0.3.3 → processforge-0.3.4}/pyproject.toml +1 -1
  3. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/init.py +17 -7
  4. processforge-0.3.4/src/processforge/providers/container_client.py +218 -0
  5. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/manager.py +15 -4
  6. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/openmc_provider.py +13 -1
  7. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/registry.py +9 -2
  8. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/types.py +2 -0
  9. {processforge-0.3.3 → processforge-0.3.4/src/processforge.egg-info}/PKG-INFO +1 -1
  10. {processforge-0.3.3 → processforge-0.3.4}/src/processforge.egg-info/SOURCES.txt +1 -0
  11. {processforge-0.3.3 → processforge-0.3.4}/LICENSE +0 -0
  12. {processforge-0.3.3 → processforge-0.3.4}/MANIFEST.in +0 -0
  13. {processforge-0.3.3 → processforge-0.3.4}/README.md +0 -0
  14. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/closed-loop-chain.json +0 -0
  15. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
  16. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
  17. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
  18. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/hydraulic-chain.json +0 -0
  19. {processforge-0.3.3 → processforge-0.3.4}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  20. {processforge-0.3.3 → processforge-0.3.4}/setup.cfg +0 -0
  21. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/__init__.py +0 -0
  22. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/__main__.py +0 -0
  23. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/_schema.py +0 -0
  24. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/analysis/__init__.py +0 -0
  25. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/analysis/dof.py +0 -0
  26. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/__init__.py +0 -0
  27. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/apply.py +0 -0
  28. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/common.py +0 -0
  29. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/diagram.py +0 -0
  30. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/display.py +0 -0
  31. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/export_fmu.py +0 -0
  32. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/export_modelica.py +0 -0
  33. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/plan.py +0 -0
  34. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/run.py +0 -0
  35. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/cli/validate.py +0 -0
  36. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/compose.py +0 -0
  37. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/__init__.py +0 -0
  38. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/backends/__init__.py +0 -0
  39. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/backends/base.py +0 -0
  40. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/backends/casadi_backend.py +0 -0
  41. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  42. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/backends/scipy_backend.py +0 -0
  43. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/flowsheet.py +0 -0
  44. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/jacobian.py +0 -0
  45. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/mixin.py +0 -0
  46. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/solver.py +0 -0
  47. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/stream_var.py +0 -0
  48. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/__init__.py +0 -0
  49. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/flash_eo.py +0 -0
  50. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/heater_eo.py +0 -0
  51. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/pipes_eo.py +0 -0
  52. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/pump_eo.py +0 -0
  53. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/strainer_eo.py +0 -0
  54. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/eo/units/valve_eo.py +0 -0
  55. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/flowsheet.py +0 -0
  56. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/fmu/__init__.py +0 -0
  57. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/fmu/_fmi_vars.py +0 -0
  58. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/fmu/builder.py +0 -0
  59. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/fmu/slave_template.py +0 -0
  60. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/lock.py +0 -0
  61. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/modelica/__init__.py +0 -0
  62. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/modelica/mo_writer.py +0 -0
  63. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/modelica/omc_runner.py +0 -0
  64. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/modelica/transpiler.py +0 -0
  65. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/modelica/unit_equations.py +0 -0
  66. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/pcl/__init__.py +0 -0
  67. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/pcl/compiler.py +0 -0
  68. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/pcl/namespace.py +0 -0
  69. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/provenance.py +0 -0
  70. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/__init__.py +0 -0
  71. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/base.py +0 -0
  72. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  73. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/cantera_jacobian.py +0 -0
  74. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/cantera_provider.py +0 -0
  75. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/coolprop_provider.py +0 -0
  76. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/festim_provider.py +0 -0
  77. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/jacobian_contributor.py +0 -0
  78. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/modelica_jacobian.py +0 -0
  79. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/modelica_provider.py +0 -0
  80. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/providers/reference_state_registry.py +0 -0
  81. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/result.py +0 -0
  82. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/result_schema.py +0 -0
  83. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/schemas/__init__.py +0 -0
  84. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/schemas/flowsheet_schema.json +0 -0
  85. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  86. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/simulate.py +0 -0
  87. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/solver.py +0 -0
  88. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/state.py +0 -0
  89. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/thermo.py +0 -0
  90. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/__init__.py +0 -0
  91. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/base.py +0 -0
  92. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/cstr.py +0 -0
  93. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/flash.py +0 -0
  94. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/heater.py +0 -0
  95. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/pfr.py +0 -0
  96. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/pipes.py +0 -0
  97. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/pump.py +0 -0
  98. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/registry.py +0 -0
  99. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/solver_unit.py +0 -0
  100. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/strainer.py +0 -0
  101. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/tank.py +0 -0
  102. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/units/valve.py +0 -0
  103. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/__init__.py +0 -0
  104. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/flowsheet_diagram.py +0 -0
  105. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/mermaid_diagram.py +0 -0
  106. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/topology.py +0 -0
  107. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/unit_consistency.py +0 -0
  108. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/validate_flowsheet.py +0 -0
  109. {processforge-0.3.3 → processforge-0.3.4}/src/processforge/utils/validation.py +0 -0
  110. {processforge-0.3.3 → processforge-0.3.4}/src/processforge.egg-info/dependency_links.txt +0 -0
  111. {processforge-0.3.3 → processforge-0.3.4}/src/processforge.egg-info/entry_points.txt +0 -0
  112. {processforge-0.3.3 → processforge-0.3.4}/src/processforge.egg-info/requires.txt +0 -0
  113. {processforge-0.3.3 → processforge-0.3.4}/src/processforge.egg-info/top_level.txt +0 -0
  114. {processforge-0.3.3 → processforge-0.3.4}/tests/test_cli.py +0 -0
  115. {processforge-0.3.3 → processforge-0.3.4}/tests/test_festim_provider.py +0 -0
  116. {processforge-0.3.3 → processforge-0.3.4}/tests/test_init.py +0 -0
  117. {processforge-0.3.3 → processforge-0.3.4}/tests/test_providers.py +0 -0
  118. {processforge-0.3.3 → processforge-0.3.4}/tests/test_state.py +0 -0
  119. {processforge-0.3.3 → processforge-0.3.4}/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
3
+ Version: 0.3.4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "processforge"
7
- version = "0.3.3"
7
+ version = "0.3.4"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -127,24 +127,34 @@ def init(
127
127
  generate_compose(pf_dir, docker_providers, outputs_dir)
128
128
  logger.info(f"Generated {compose_path}")
129
129
 
130
- # Attempt docker compose pull
130
+ # Attempt docker compose pull — stream progress live
131
131
  try:
132
- result = subprocess.run(
132
+ logger.info("Pulling Docker images (this may take a while)...")
133
+ process = subprocess.Popen(
133
134
  ["docker", "compose", "-f", compose_path, "pull"],
134
- capture_output=True,
135
+ stdout=subprocess.PIPE,
136
+ stderr=subprocess.STDOUT,
135
137
  text=True,
136
- timeout=120,
138
+ bufsize=1,
137
139
  )
138
- if result.returncode == 0:
140
+ assert process.stdout is not None
141
+ for line in process.stdout:
142
+ line = line.rstrip()
143
+ if line:
144
+ logger.info(line)
145
+ returncode = process.wait(timeout=600)
146
+ if returncode == 0:
139
147
  logger.info("Pulled Docker images.")
140
148
  else:
141
- logger.warning(f"docker compose pull failed: {result.stderr}")
149
+ logger.warning(
150
+ f"docker compose pull failed with exit code {returncode}."
151
+ )
142
152
  except FileNotFoundError:
143
153
  logger.warning(
144
154
  "Docker not found. Install Docker to use containerized providers."
145
155
  )
146
156
  except subprocess.TimeoutExpired:
147
- logger.warning("docker compose pull timed out after 120s.")
157
+ logger.warning("docker compose pull timed out after 600s.")
148
158
  else:
149
159
  logger.info("No containerized providers — skipping Docker setup.")
150
160
 
@@ -0,0 +1,218 @@
1
+ """CLI-side HTTP client for containerized providers (OpenMC, FESTIM, …).
2
+
3
+ A provider that runs in a Docker container does its real work inside the
4
+ container (in ``docker/provider_server.py``). The CLI side must NOT import the
5
+ backend library (OpenMC, FESTIM, …) locally — instead it speaks HTTP to the
6
+ container's ``provider_server.py`` instance. :class:`ContainerProviderClient`
7
+ implements the :class:`~processforge.providers.base.AbstractProvider` contract
8
+ as a thin HTTP wrapper around that server.
9
+
10
+ Request/response contract (matches ``docker/provider_server.py``)
11
+ ----------------------------------------------------------------
12
+ * ``POST /run`` with a JSON body
13
+ ``{"unit_config", "materials", "inlet", "output_dir", "provider_config"}``.
14
+ * JSON response ``{"status", "sim_type", <scalars…>, "metadata"}`` deserialised
15
+ into a :class:`~processforge.types.SimulationResult`.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import urllib.error
22
+ import urllib.request
23
+ from typing import TYPE_CHECKING, Optional
24
+
25
+ from loguru import logger
26
+
27
+ from .base import AbstractProvider
28
+ from .registry import get_provider_default_port
29
+
30
+ if TYPE_CHECKING:
31
+ from processforge.types import (
32
+ FlowsheetConfig,
33
+ MaterialDef,
34
+ ProviderConfig,
35
+ SimulationResult,
36
+ UnitConfig,
37
+ )
38
+
39
+
40
+ class ContainerProviderClient(AbstractProvider):
41
+ """CLI-side HTTP client for a provider running in a Docker container.
42
+
43
+ Responsibilities
44
+ ----------------
45
+ * **Health check** — ``initialize()`` verifies the container is reachable
46
+ via ``GET /health``.
47
+ * **Simulation dispatch** — ``run_simulation()`` serialises the unit config
48
+ and material registry and ``POST``s them to the container's ``/run``
49
+ endpoint, then deserialises the JSON response into a ``SimulationResult``.
50
+ * **Material validation** — delegated to the provider class via
51
+ ``validate_material()`` (it is a classmethod and needs no local backend).
52
+ """
53
+
54
+ def __init__(self, provider_type: str):
55
+ self._ptype = provider_type
56
+ self._url: Optional[str] = None
57
+ self._provider_output_dir: str = "outputs"
58
+ self._provider_config: Optional["ProviderConfig"] = None
59
+ self._materials: dict = {}
60
+ self._initialized: bool = False
61
+
62
+ # ------------------------------------------------------------------
63
+ # Helpers
64
+ # ------------------------------------------------------------------
65
+
66
+ def _resolve_url(self, provider_config: "ProviderConfig") -> str:
67
+ """Return the service URL, deriving a default from the catalog if needed."""
68
+ url = getattr(provider_config, "url", None)
69
+ if not url:
70
+ port = get_provider_default_port(self._ptype) or 9000
71
+ url = f"http://localhost:{port}"
72
+ return url.rstrip("/")
73
+
74
+ @staticmethod
75
+ def _serialize_unit_config(unit_config: "UnitConfig") -> dict:
76
+ """Serialise a ``UnitConfig`` into the ``unit_config`` block of the body."""
77
+ return {
78
+ "type": unit_config.type,
79
+ "in": unit_config.inputs,
80
+ "material": unit_config.material,
81
+ "provider": unit_config.provider,
82
+ "out": unit_config.out,
83
+ "sim_type": unit_config.sim_type,
84
+ "solver_config": unit_config.solver_config,
85
+ **unit_config.extra,
86
+ }
87
+
88
+ @staticmethod
89
+ def _serialize_materials(materials: dict[str, "MaterialDef"]) -> dict:
90
+ """Serialise the material registry into the ``materials`` block of the body."""
91
+ out: dict = {}
92
+ for name, mat in materials.items():
93
+ out[name] = {
94
+ "id": mat.id,
95
+ "density": mat.density,
96
+ "density_units": mat.density_units,
97
+ "temperature": mat.temperature,
98
+ "depletable": mat.depletable,
99
+ "nuclides": mat.nuclides,
100
+ **mat.extra,
101
+ }
102
+ return out
103
+
104
+ # ------------------------------------------------------------------
105
+ # Lifecycle
106
+ # ------------------------------------------------------------------
107
+
108
+ def initialize(
109
+ self,
110
+ provider_config: "ProviderConfig",
111
+ flowsheet_config: "FlowsheetConfig",
112
+ ) -> None:
113
+ """Store config, resolve URL, verify the Docker service is reachable."""
114
+ self._url = self._resolve_url(provider_config)
115
+
116
+ out_dir = os.path.expandvars(
117
+ getattr(provider_config, "output_dir", "outputs") or "outputs"
118
+ )
119
+ if not os.path.isabs(out_dir):
120
+ root = os.environ.get("PROCESSFORGE_OUTPUT_DIR", "outputs")
121
+ out_dir = os.path.join(root, out_dir)
122
+ self._provider_output_dir = out_dir
123
+
124
+ # Material registry — serialised into /run and used by validate_material().
125
+ self._materials = dict(flowsheet_config.materials)
126
+ self._provider_config = provider_config
127
+
128
+ try:
129
+ req = urllib.request.Request(f"{self._url}/health", method="GET")
130
+ with urllib.request.urlopen(req, timeout=10) as resp:
131
+ health = json.loads(resp.read().decode())
132
+ logger.info(
133
+ f"ContainerProviderClient connected to {self._url} — "
134
+ f"status={health.get('status')}"
135
+ )
136
+ except (urllib.error.URLError, OSError, TimeoutError) as exc:
137
+ raise RuntimeError(
138
+ f"Provider '{self._ptype}' service unreachable at {self._url}. "
139
+ f"Ensure the container is running: pf init <flowsheet>"
140
+ ) from exc
141
+
142
+ self._initialized = True
143
+
144
+ def get_thermo_properties(self, stream: dict) -> dict:
145
+ raise NotImplementedError(
146
+ "ContainerProviderClient does not support stream thermodynamics."
147
+ )
148
+
149
+ def compute_unit(self, unit_type: str, config: dict, inlet: dict):
150
+ """Return ``None`` — the container provider uses ``run_simulation``."""
151
+ return None
152
+
153
+ def teardown(self) -> None:
154
+ """Release provider state."""
155
+ self._initialized = False
156
+
157
+ def run_simulation(self, unit_config: "UnitConfig", inlet: dict) -> "SimulationResult":
158
+ """Serialise the request, POST it to the container, return the result."""
159
+ from processforge.types import SimulationResult
160
+
161
+ if not self._url:
162
+ raise RuntimeError(
163
+ "ContainerProviderClient has not been initialized — call initialize() first."
164
+ )
165
+
166
+ provider_config_dump = (
167
+ self._provider_config.model_dump()
168
+ if self._provider_config is not None
169
+ else {"type": unit_config.provider or self._ptype, "url": self._url}
170
+ )
171
+
172
+ body = {
173
+ "unit_config": self._serialize_unit_config(unit_config),
174
+ "materials": self._serialize_materials(self._materials),
175
+ "inlet": inlet,
176
+ "output_dir": self._provider_output_dir,
177
+ "provider_config": provider_config_dump,
178
+ }
179
+
180
+ logger.info(
181
+ f"ContainerProviderClient: POST {self._url}/run "
182
+ f"sim_type='{unit_config.sim_type}'"
183
+ )
184
+
185
+ payload = json.dumps(body).encode()
186
+ req = urllib.request.Request(
187
+ f"{self._url}/run",
188
+ data=payload,
189
+ headers={"Content-Type": "application/json"},
190
+ method="POST",
191
+ )
192
+
193
+ try:
194
+ with urllib.request.urlopen(req, timeout=900) as resp:
195
+ data = json.loads(resp.read().decode())
196
+ except urllib.error.HTTPError as exc:
197
+ detail = exc.read().decode() if exc.fp else str(exc)
198
+ raise RuntimeError(
199
+ f"Provider '{self._ptype}' service returned HTTP {exc.code}: {detail}"
200
+ ) from exc
201
+ except (urllib.error.URLError, OSError, TimeoutError) as exc:
202
+ raise RuntimeError(
203
+ f"Provider '{self._ptype}' service unreachable at {self._url}: {exc}"
204
+ ) from exc
205
+
206
+ scalars = {
207
+ k: v
208
+ for k, v in data.items()
209
+ if k not in ("status", "sim_type", "metadata")
210
+ }
211
+ return SimulationResult(
212
+ status=data.get("status", "completed"),
213
+ sim_type=data.get("sim_type", unit_config.sim_type or ""),
214
+ scalars=scalars,
215
+ metadata=data.get("metadata", {}),
216
+ )
217
+
218
+
@@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict
19
19
  from processforge.types import CoolPropProviderConfig, FlowsheetConfig, ProviderConfig
20
20
 
21
21
  from .coolprop_provider import CoolPropProvider
22
- from .registry import get_provider_class
22
+ from .registry import get_provider_class, is_containerized
23
23
 
24
24
  _BUILTIN_DEFAULT_KEY = "__coolprop__"
25
25
  _DEFAULT_KEY = "__default__"
@@ -136,11 +136,22 @@ def build_provider_map(
136
136
  # Step 2: instantiate every declared provider.
137
137
  for name, cfg in providers_config.items():
138
138
  ptype = cfg.type
139
- cls = get_provider_class(ptype)
140
- instance = cls()
139
+ if is_containerized(ptype):
140
+ # Containerized providers (OpenMC, FESTIM, …) run their backend
141
+ # inside a Docker container; the CLI only needs a thin HTTP client
142
+ # that talks to the container's provider_server.py.
143
+ from .container_client import ContainerProviderClient
144
+
145
+ instance = ContainerProviderClient(ptype)
146
+ logger.info(
147
+ f"Initialized provider '{name}' (type='{ptype}') via container client"
148
+ )
149
+ else:
150
+ cls = get_provider_class(ptype)
151
+ instance = cls()
152
+ logger.info(f"Initialized provider '{name}' (type='{ptype}')")
141
153
  instance.initialize(cfg, flowsheet_config)
142
154
  providers[name] = instance
143
- logger.info(f"Initialized provider '{name}' (type='{ptype}')")
144
155
 
145
156
  # Step 3: resolve the active default.
146
157
  default = None
@@ -37,6 +37,7 @@ from __future__ import annotations
37
37
  import math
38
38
  import os
39
39
  import pathlib
40
+ import tempfile
40
41
  from abc import ABC, abstractmethod
41
42
  from typing import TYPE_CHECKING, Optional
42
43
 
@@ -506,7 +507,18 @@ class OpenMCProvider(AbstractProvider):
506
507
 
507
508
  # --- Working directory management ---
508
509
  run_dir = pathlib.Path(self._provider_output_dir)
509
- run_dir.mkdir(parents=True, exist_ok=True)
510
+ try:
511
+ run_dir.mkdir(parents=True, exist_ok=True)
512
+ except (PermissionError, OSError) as exc:
513
+ # The configured output dir (often the mounted /data volume) may not
514
+ # be writable by the container user. Fall back to a temp dir so the
515
+ # simulation can still run; only the persisted XML artifacts are lost.
516
+ run_dir = pathlib.Path(tempfile.mkdtemp(prefix="processforge_openmc_"))
517
+ logger.warning(
518
+ f"OpenMCProvider: cannot use output dir "
519
+ f"'{self._provider_output_dir}' ({exc}); "
520
+ f"falling back to '{run_dir}'."
521
+ )
510
522
  original_cwd = pathlib.Path.cwd()
511
523
 
512
524
  # --- Cross-section override ---
@@ -171,8 +171,15 @@ def get_provider_default_port(provider_type: str) -> int | None:
171
171
 
172
172
 
173
173
  def is_containerized(provider_type: str) -> bool:
174
- """Return True if the provider runs in a Docker container."""
175
- return get_provider_docker_image(provider_type) is not None
174
+ """Return True if the provider runs in a Docker container.
175
+
176
+ Returns False for unknown provider types rather than raising, so callers
177
+ can cleanly fall through to the local/import path.
178
+ """
179
+ try:
180
+ return get_provider_docker_image(provider_type) is not None
181
+ except ValueError:
182
+ return False
176
183
 
177
184
 
178
185
  # Seed the registry with the always-available CoolProp provider.
@@ -194,6 +194,7 @@ class OpenMCProviderConfig(BaseModel):
194
194
  """
195
195
 
196
196
  type: str = "openmc"
197
+ url: Optional[str] = None
197
198
  output_dir: str = "outputs/openmc"
198
199
  cross_sections: Optional[str] = None
199
200
  docker_image: Optional[str] = None
@@ -201,6 +202,7 @@ class OpenMCProviderConfig(BaseModel):
201
202
  @classmethod
202
203
  def from_dict(cls, d: dict) -> "OpenMCProviderConfig":
203
204
  return cls(
205
+ url=d.get("url"),
204
206
  output_dir=d.get("output_dir", "outputs/openmc"),
205
207
  cross_sections=d.get("cross_sections"),
206
208
  docker_image=d.get("docker_image"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -76,6 +76,7 @@ src/processforge/providers/base.py
76
76
  src/processforge/providers/base_jacobian_mixin.py
77
77
  src/processforge/providers/cantera_jacobian.py
78
78
  src/processforge/providers/cantera_provider.py
79
+ src/processforge/providers/container_client.py
79
80
  src/processforge/providers/coolprop_provider.py
80
81
  src/processforge/providers/festim_provider.py
81
82
  src/processforge/providers/jacobian_contributor.py
File without changes
File without changes
File without changes
File without changes