processforge 0.3.11__tar.gz → 0.3.12__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 (132) hide show
  1. {processforge-0.3.11/src/processforge.egg-info → processforge-0.3.12}/PKG-INFO +2 -3
  2. {processforge-0.3.11 → processforge-0.3.12}/flowsheets/festim/hydrogen_transport_1d.json +24 -25
  3. processforge-0.3.12/flowsheets/festim/tds_tungsten.json +136 -0
  4. {processforge-0.3.11 → processforge-0.3.12}/pyproject.toml +2 -4
  5. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/api/serve.py +11 -22
  6. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/apply.py +24 -10
  7. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/common.py +9 -9
  8. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/run.py +64 -22
  9. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/flowsheet.py +36 -1
  10. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/jacobian.py +9 -5
  11. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/flowsheet.py +50 -1
  12. processforge-0.3.12/src/processforge/output_collector.py +97 -0
  13. processforge-0.3.12/src/processforge/persistence/__init__.py +7 -0
  14. processforge-0.3.12/src/processforge/persistence/archive.py +166 -0
  15. processforge-0.3.12/src/processforge/persistence/artifact_store.py +132 -0
  16. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/base.py +5 -4
  17. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/container_client.py +34 -16
  18. processforge-0.3.12/src/processforge/providers/festim_provider.py +891 -0
  19. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/openmc_provider.py +85 -41
  20. processforge-0.3.12/src/processforge/quantity.py +104 -0
  21. processforge-0.3.12/src/processforge/schemas/festim/festim_model.py +717 -0
  22. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/types.py +154 -23
  23. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/__init__.py +3 -0
  24. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/solver_unit.py +9 -4
  25. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/validate_flowsheet.py +31 -89
  26. {processforge-0.3.11 → processforge-0.3.12/src/processforge.egg-info}/PKG-INFO +2 -3
  27. {processforge-0.3.11 → processforge-0.3.12}/src/processforge.egg-info/SOURCES.txt +8 -0
  28. {processforge-0.3.11 → processforge-0.3.12}/src/processforge.egg-info/requires.txt +1 -3
  29. {processforge-0.3.11 → processforge-0.3.12}/tests/test_cli.py +5 -5
  30. processforge-0.3.12/tests/test_festim_provider.py +807 -0
  31. processforge-0.3.12/tests/test_festim_schema.py +341 -0
  32. {processforge-0.3.11 → processforge-0.3.12}/tests/test_types.py +40 -21
  33. processforge-0.3.11/src/processforge/providers/festim_provider.py +0 -411
  34. processforge-0.3.11/tests/test_festim_provider.py +0 -596
  35. {processforge-0.3.11 → processforge-0.3.12}/LICENSE +0 -0
  36. {processforge-0.3.11 → processforge-0.3.12}/MANIFEST.in +0 -0
  37. {processforge-0.3.11 → processforge-0.3.12}/README.md +0 -0
  38. {processforge-0.3.11 → processforge-0.3.12}/flowsheets/closed-loop-chain.json +0 -0
  39. {processforge-0.3.11 → processforge-0.3.12}/flowsheets/hydraulic-chain.json +0 -0
  40. {processforge-0.3.11 → processforge-0.3.12}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  41. {processforge-0.3.11 → processforge-0.3.12}/setup.cfg +0 -0
  42. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/__init__.py +0 -0
  43. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/__main__.py +0 -0
  44. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/_schema.py +0 -0
  45. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/analysis/__init__.py +0 -0
  46. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/analysis/dof.py +0 -0
  47. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/api/__init__.py +0 -0
  48. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/__init__.py +0 -0
  49. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/diagram.py +0 -0
  50. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/display.py +0 -0
  51. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/export_fmu.py +0 -0
  52. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/export_modelica.py +0 -0
  53. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/init.py +0 -0
  54. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/plan.py +0 -0
  55. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/cli/validate.py +0 -0
  56. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/compose.py +0 -0
  57. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/__init__.py +0 -0
  58. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/backends/__init__.py +0 -0
  59. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/backends/base.py +0 -0
  60. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/backends/casadi_backend.py +0 -0
  61. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  62. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/backends/scipy_backend.py +0 -0
  63. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/mixin.py +0 -0
  64. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/solver.py +0 -0
  65. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/stream_var.py +0 -0
  66. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/__init__.py +0 -0
  67. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/flash_eo.py +0 -0
  68. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/heater_eo.py +0 -0
  69. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/pipes_eo.py +0 -0
  70. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/pump_eo.py +0 -0
  71. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/strainer_eo.py +0 -0
  72. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/eo/units/valve_eo.py +0 -0
  73. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/fmu/__init__.py +0 -0
  74. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/fmu/_fmi_vars.py +0 -0
  75. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/fmu/builder.py +0 -0
  76. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/fmu/slave_template.py +0 -0
  77. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/lock.py +0 -0
  78. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/modelica/__init__.py +0 -0
  79. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/modelica/mo_writer.py +0 -0
  80. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/modelica/omc_runner.py +0 -0
  81. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/modelica/transpiler.py +0 -0
  82. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/modelica/unit_equations.py +0 -0
  83. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/pcl/__init__.py +0 -0
  84. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/pcl/compiler.py +0 -0
  85. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/pcl/namespace.py +0 -0
  86. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/provenance.py +0 -0
  87. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/__init__.py +0 -0
  88. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  89. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/cantera_jacobian.py +0 -0
  90. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/cantera_provider.py +0 -0
  91. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/coolprop_provider.py +0 -0
  92. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/jacobian_contributor.py +0 -0
  93. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/manager.py +0 -0
  94. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/modelica_jacobian.py +0 -0
  95. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/modelica_provider.py +0 -0
  96. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/reference_state_registry.py +0 -0
  97. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/providers/registry.py +0 -0
  98. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/result.py +0 -0
  99. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/result_schema.py +0 -0
  100. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/schemas/__init__.py +0 -0
  101. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/schemas/flowsheet_schema.json +0 -0
  102. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  103. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/simulate.py +0 -0
  104. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/solver.py +0 -0
  105. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/state.py +0 -0
  106. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/thermo.py +0 -0
  107. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/base.py +0 -0
  108. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/cstr.py +0 -0
  109. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/flash.py +0 -0
  110. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/heater.py +0 -0
  111. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/pfr.py +0 -0
  112. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/pipes.py +0 -0
  113. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/pump.py +0 -0
  114. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/registry.py +0 -0
  115. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/strainer.py +0 -0
  116. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/tank.py +0 -0
  117. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/units/valve.py +0 -0
  118. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/__init__.py +0 -0
  119. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/flowsheet_diagram.py +0 -0
  120. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/mermaid_diagram.py +0 -0
  121. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/s3_upload.py +0 -0
  122. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/topology.py +0 -0
  123. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/unit_consistency.py +0 -0
  124. {processforge-0.3.11 → processforge-0.3.12}/src/processforge/utils/validation.py +0 -0
  125. {processforge-0.3.11 → processforge-0.3.12}/src/processforge.egg-info/dependency_links.txt +0 -0
  126. {processforge-0.3.11 → processforge-0.3.12}/src/processforge.egg-info/entry_points.txt +0 -0
  127. {processforge-0.3.11 → processforge-0.3.12}/src/processforge.egg-info/top_level.txt +0 -0
  128. {processforge-0.3.11 → processforge-0.3.12}/tests/test_init.py +0 -0
  129. {processforge-0.3.11 → processforge-0.3.12}/tests/test_openmc_provider.py +0 -0
  130. {processforge-0.3.11 → processforge-0.3.12}/tests/test_providers.py +0 -0
  131. {processforge-0.3.11 → processforge-0.3.12}/tests/test_s3_upload.py +0 -0
  132. {processforge-0.3.11 → processforge-0.3.12}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.11
3
+ Version: 0.3.12
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
@@ -28,6 +28,7 @@ Requires-Dist: jsonschema>=4.20
28
28
  Requires-Dist: graphviz>=0.20
29
29
  Requires-Dist: pandas>=2.2
30
30
  Requires-Dist: zarr>=3.0
31
+ Requires-Dist: pint<1.0,>=0.24
31
32
  Requires-Dist: coolprop>=7.2.0
32
33
  Requires-Dist: typer>=0.12
33
34
  Provides-Extra: api
@@ -48,8 +49,6 @@ Provides-Extra: cantera
48
49
  Requires-Dist: cantera>=3.0; extra == "cantera"
49
50
  Provides-Extra: coolprop
50
51
  Requires-Dist: coolprop>=6.6; extra == "coolprop"
51
- Provides-Extra: pcl
52
- Requires-Dist: pint>=0.24; extra == "pcl"
53
52
  Provides-Extra: dev
54
53
  Requires-Dist: pytest>=7.0; extra == "dev"
55
54
  Requires-Dist: black; extra == "dev"
@@ -25,31 +25,30 @@
25
25
  "festim_solver": {
26
26
  "type": "SolverUnit",
27
27
  "provider": "festim",
28
- "sim_type": "hydrogen_transport",
29
- "mesh": {
30
- "vertices": [0.0, 0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, 0.01]
31
- },
32
- "species": [
33
- { "name": "H" }
34
- ],
35
- "subdomains": {
36
- "volume": [
37
- { "id": 1, "borders": [0.0, 0.01], "material": "default" }
38
- ],
39
- "surface": [
40
- { "id": 2, "x": 0.0 },
41
- { "id": 3, "x": 0.01 }
42
- ]
43
- },
44
- "boundary_conditions": [
45
- { "type": "fixed_concentration", "subdomain_id": 2, "species": "H", "value": 1 },
46
- { "type": "fixed_concentration", "subdomain_id": 3, "species": "H", "value": 0 }
47
- ],
48
- "exports": [
49
- { "type": "vtx_species", "field": "H", "filename": "hydrogen_concentration.bp", "times": "milestones" },
50
- { "type": "profile_1d", "field": "H", "volume_id": 1, "times": "milestones" }
51
- ],
28
+ "sim_type": "hydrogen_transport_tds",
52
29
  "solver_config": {
30
+ "mesh": {
31
+ "vertices": [0.0, 0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, 0.01]
32
+ },
33
+ "subdomains": {
34
+ "volume": [
35
+ { "id": 1, "borders": [0.0, 0.01], "material": "default" }
36
+ ],
37
+ "surface": [
38
+ { "id": 2, "x": 0.0 },
39
+ { "id": 3, "x": 0.01 }
40
+ ]
41
+ },
42
+ "species": [
43
+ { "name": "H", "mobile": true }
44
+ ],
45
+ "boundary_conditions": [
46
+ { "type": "fixed_concentration", "subdomain": 2, "species": "H", "value": 1 },
47
+ { "type": "fixed_concentration", "subdomain": 3, "species": "H", "value": 0 }
48
+ ],
49
+ "exports": [
50
+ { "type": "profile_1d", "field": "H", "volume": 1, "times": [0.05, 0.1, 0.2, 0.5, 1] }
51
+ ],
53
52
  "atol": 1e-10,
54
53
  "rtol": 1e-10,
55
54
  "final_time": 2,
@@ -67,4 +66,4 @@
67
66
  "simulation": {
68
67
  "mode": "steady"
69
68
  }
70
- }
69
+ }
@@ -0,0 +1,136 @@
1
+ {
2
+ "metadata": {
3
+ "name": "FESTIM TDS — Tungsten Thermal Desorption",
4
+ "description": "Thermo-desorption spectra (TDS) for a tungsten sample. Gaussian implantation source (400 s), resting phase, then a temperature ramp from 300 K at 8 K/s to desorb trapped hydrogen. Two trapping sites modelled via implicit empty-trap species. Reproduces notebooks_task02.ipynb.py.",
5
+ "version": "1.0"
6
+ },
7
+ "providers": {
8
+ "festim": {
9
+ "type": "festim",
10
+ "url": "http://localhost:9002",
11
+ "output_dir": "festim"
12
+ }
13
+ },
14
+ "materials": {
15
+ "tungsten": {
16
+ "id": 1,
17
+ "extra": {
18
+ "D_0": 4.1e-7,
19
+ "E_D": 0.39
20
+ }
21
+ }
22
+ },
23
+ "streams": {},
24
+ "units": {
25
+ "tds_solver": {
26
+ "type": "SolverUnit",
27
+ "provider": "festim",
28
+ "sim_type": "hydrogen_transport_tds",
29
+ "solver_config": {
30
+ "mesh": {
31
+ "segments": [
32
+ { "start": 0.0, "stop": 30e-9, "num": 200 },
33
+ { "start": 30e-9, "stop": 3e-6, "num": 300 },
34
+ { "start": 3e-6, "stop": 20e-6, "num": 200 }
35
+ ]
36
+ },
37
+ "subdomains": {
38
+ "volume": [
39
+ { "id": 1, "borders": [0.0, 20e-6], "material": "tungsten" }
40
+ ],
41
+ "surface": [
42
+ { "id": 1, "x": 0.0 },
43
+ { "id": 2, "x": 20e-6 }
44
+ ]
45
+ },
46
+ "species": [
47
+ { "name": "H", "mobile": true },
48
+ { "name": "trapped_H1", "mobile": false },
49
+ { "name": "trapped_H2", "mobile": false }
50
+ ],
51
+ "implicit_species": [
52
+ { "name": "empty_trap1", "n": 135.99814960153805, "others": ["trapped_H1"] },
53
+ { "name": "empty_trap2", "n": 41.84558449278094, "others": ["trapped_H2"] }
54
+ ],
55
+ "reactions": [
56
+ {
57
+ "type": "arrhenius",
58
+ "reactant": ["H", "empty_trap1"],
59
+ "product": ["trapped_H1"],
60
+ "k_0": 53983071.22305305,
61
+ "E_k": 0.39,
62
+ "p_0": 1e13,
63
+ "E_p": 0.87,
64
+ "volume": 1
65
+ },
66
+ {
67
+ "type": "arrhenius",
68
+ "reactant": ["H", "empty_trap2"],
69
+ "product": ["trapped_H2"],
70
+ "k_0": 53983071.22305305,
71
+ "E_k": 0.39,
72
+ "p_0": 1e13,
73
+ "E_p": 1.0,
74
+ "volume": 1
75
+ }
76
+ ],
77
+ "sources": [
78
+ {
79
+ "type": "particle",
80
+ "species": "H",
81
+ "volume": 1,
82
+ "value": {
83
+ "type": "gaussian",
84
+ "amplitude": 4.151347667934617e-05,
85
+ "center": 4.5e-9,
86
+ "width": 2.5e-9,
87
+ "active_until": 400
88
+ }
89
+ }
90
+ ],
91
+ "boundary_conditions": [
92
+ { "type": "fixed_concentration", "subdomain": 1, "species": "H", "value": 0 },
93
+ { "type": "fixed_concentration", "subdomain": 2, "species": "H", "value": 0 }
94
+ ],
95
+ "temperature": {
96
+ "type": "ramp",
97
+ "T0": 300,
98
+ "start_t": 450,
99
+ "ramp_rate": 8
100
+ },
101
+ "exports": [
102
+ { "type": "surface_flux", "field": "H", "surface": 1, "filename": "flux_left.csv" },
103
+ { "type": "surface_flux", "field": "H", "surface": 2, "filename": "flux_right.csv" },
104
+ { "type": "total_volume", "field": "H", "volume": 1, "filename": "total_mobile_H.csv" },
105
+ { "type": "total_volume", "field": "trapped_H1", "volume": 1, "filename": "total_trapped_H1.csv" },
106
+ { "type": "total_volume", "field": "trapped_H2", "volume": 1, "filename": "total_trapped_H2.csv" },
107
+ { "type": "profile_1d", "field": "H", "volume": 1, "times": [400, 450, 500] },
108
+ { "type": "profile_1d", "field": "trapped_H1", "volume": 1, "times": [400, 450, 500] },
109
+ { "type": "profile_1d", "field": "trapped_H2", "volume": 1, "times": [400, 450, 500] }
110
+ ],
111
+ "atol": 1e-10,
112
+ "rtol": 1e-10,
113
+ "max_iterations": 30,
114
+ "transient": true,
115
+ "final_time": 500,
116
+ "element_degree": 1,
117
+ "stepsize": {
118
+ "initial_value": 0.5,
119
+ "growth_factor": 1.1,
120
+ "cutback_factor": 0.9,
121
+ "target_nb_iterations": 4,
122
+ "max_stepsize": {
123
+ "type": "after_t",
124
+ "after_t": 450,
125
+ "value": 0.5
126
+ },
127
+ "milestones": [400, 450, 500]
128
+ },
129
+ "convergence_criterion": "residual"
130
+ }
131
+ }
132
+ },
133
+ "simulation": {
134
+ "mode": "steady"
135
+ }
136
+ }
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "processforge"
7
- version = "0.3.11"
7
+ version = "0.3.12"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "graphviz>=0.20",
35
35
  "pandas>=2.2",
36
36
  "zarr>=3.0",
37
+ "pint>=0.24,<1.0",
37
38
  "coolprop>=7.2.0",
38
39
  "typer>=0.12",
39
40
  ]
@@ -64,9 +65,6 @@ cantera = [
64
65
  coolprop = [
65
66
  "coolprop>=6.6",
66
67
  ]
67
- pcl = [
68
- "pint>=0.24",
69
- ]
70
68
  dev = [
71
69
  "pytest>=7.0",
72
70
  "black",
@@ -74,11 +74,20 @@ def run(body: dict):
74
74
  # Build a minimal flowsheet config holding materials.
75
75
  flowsheet_config = type("FS", (), {"materials": materials})()
76
76
 
77
+ run_id = body.get("run_id", "")
78
+ flowsheet_hash = body.get("flowsheet_hash", "")
79
+
77
80
  try:
78
81
  provider.initialize(provider_config, flowsheet_config)
79
82
  result = provider.run_simulation(unit_cfg, body.get("inlet", {}))
80
- _maybe_upload_run_outputs(result)
81
- return result.as_dict() | {"metadata": result.metadata}
83
+ # Upload any declared artifacts to object storage (fills remote_uris).
84
+ # No-op when S3 is not configured.
85
+ from processforge.persistence.artifact_store import ArtifactStore
86
+
87
+ ArtifactStore().persist_outputs(
88
+ result, run_id=run_id, flowsheet_hash=flowsheet_hash, unit=getattr(result, "unit", "")
89
+ )
90
+ return result.model_dump()
82
91
  except HTTPException:
83
92
  raise
84
93
  except Exception as exc:
@@ -90,26 +99,6 @@ def run(body: dict):
90
99
  provider.teardown()
91
100
 
92
101
 
93
- def _maybe_upload_run_outputs(result) -> None:
94
- """Upload the provider's run_dir to S3 when configured.
95
-
96
- No-op unless ``S3_BUCKET`` is set in the container environment and the
97
- provider recorded a ``run_dir`` in its result metadata. Upload failures are
98
- logged and never fail the run.
99
- """
100
- run_dir = result.metadata.get("run_dir")
101
- if not run_dir:
102
- return
103
- try:
104
- from processforge.utils.s3_upload import upload_directory_to_s3
105
-
106
- s3_uri = upload_directory_to_s3(run_dir)
107
- if s3_uri:
108
- result.metadata["s3_uri"] = s3_uri
109
- except Exception as exc: # noqa: BLE001
110
- logger.warning(f"S3 upload of {run_dir} failed: {exc}")
111
-
112
-
113
102
  if __name__ == "__main__":
114
103
  import uvicorn
115
104
 
@@ -6,13 +6,32 @@ import os
6
6
  import time
7
7
  from typing import Literal
8
8
 
9
+ import hashlib
10
+ import json
11
+ import os
9
12
  import typer
13
+ from datetime import datetime, timezone
10
14
  from loguru import logger
11
15
 
16
+
17
+ def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id):
18
+ """Build a standardized RunManifest and persist it to the archive."""
19
+ run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
20
+ flowsheet_hash = hashlib.sha256(
21
+ json.dumps(config, sort_keys=True, default=str).encode("utf-8")
22
+ ).hexdigest()[:16]
23
+ manifest = fs.collect_outputs(
24
+ run_id, config.get("simulation", {}).get("mode", "steady"),
25
+ base_name, provenance=run_info.model_dump(),
26
+ )
27
+ manifest.snapshot_id = snapshot_id
28
+ stream_results = {k: v for k, v in results.items() if not hasattr(v, "fields")}
29
+ archive.save_run(manifest, stream_results=stream_results)
30
+ logger.info(f"Run saved : {run_id}")
31
+ return run_id
32
+
12
33
  from ..eo import EOFlowsheet
13
34
  from ..provenance import build_run_info
14
- from ..result import save_results_zarr
15
- from ..state import StateManager
16
35
  from .common import (
17
36
  build_divergence_report,
18
37
  build_run_metadata,
@@ -127,15 +146,14 @@ def apply(
127
146
  label="converged state",
128
147
  )
129
148
  run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
130
- zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
131
- save_results_zarr(results, zarr_path, run_info=run_info)
149
+ saved_run_id = _persist_run(sm, fs, results, run_info, config, base_name, snapshot_id)
132
150
  logger.info("=== Apply Summary ===")
133
151
  logger.info(" Status : CONVERGED")
134
152
  logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
135
153
  logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
136
154
  logger.info(f" Backend : {display_backend(config, fs.backend)}")
137
155
  logger.info(f" Snapshot ID : {snapshot_id}")
138
- logger.info(f" Results zarr : {zarr_path}")
156
+ logger.info(f" Run ID : {saved_run_id}")
139
157
  logger.info(f" Elapsed (s) : {elapsed:.2f}")
140
158
  return
141
159
 
@@ -168,11 +186,7 @@ def apply(
168
186
  label="homotopy solution",
169
187
  )
170
188
  run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
171
- save_results_zarr(
172
- results,
173
- os.path.join(outputs_dir, f"{base_name}_results.zarr"),
174
- run_info=run_info,
175
- )
189
+ _persist_run(sm, fs, results, run_info, config, base_name, snapshot_id)
176
190
  logger.info("Homotopy apply succeeded. New snapshot saved.")
177
191
  return
178
192
 
@@ -15,7 +15,7 @@ from loguru import logger
15
15
 
16
16
  from .. import __version__ as _pf_version
17
17
  from ..utils.validate_flowsheet import validate_flowsheet
18
- from ..state import StateManager
18
+ from ..persistence.archive import ProcessStateArchive
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from ..state import SnapshotState
@@ -133,12 +133,12 @@ def extract_providers(flowsheet_path: str) -> dict:
133
133
  # State management
134
134
  # ---------------------------------------------------------------------------
135
135
 
136
- def load_state_manager(outputs_dir: str, base_name: str) -> tuple[StateManager, "SnapshotState | None"]:
137
- """Create a StateManager and load the snapshot (if it exists)."""
138
- state_path = os.path.join(outputs_dir, f"{base_name}.pfstate")
139
- sm = StateManager(state_path)
140
- state = sm.load_state()
141
- return sm, state
136
+ def load_state_manager(outputs_dir: str, base_name: str) -> tuple[ProcessStateArchive, "SnapshotState | None"]:
137
+ """Create a ProcessStateArchive and load the snapshot (if it exists)."""
138
+ archive_path = os.path.join(outputs_dir, f"{base_name}.pfarchive")
139
+ archive = ProcessStateArchive(archive_path)
140
+ state = archive.load_snapshot()
141
+ return archive, state
142
142
 
143
143
 
144
144
  # ---------------------------------------------------------------------------
@@ -307,7 +307,7 @@ def write_divergence_report(outputs_dir: str, base_name: str, divergence: dict)
307
307
  # ---------------------------------------------------------------------------
308
308
 
309
309
  def save_snapshot(
310
- sm: StateManager,
310
+ archive: ProcessStateArchive,
311
311
  config: dict,
312
312
  x_converged: Any,
313
313
  var_names: list[str],
@@ -318,7 +318,7 @@ def save_snapshot(
318
318
  ) -> str:
319
319
  """Save a state snapshot, logging a clear message on failure."""
320
320
  try:
321
- return sm.save_state(
321
+ return archive.save_snapshot(
322
322
  config, x_converged, var_names,
323
323
  metadata=metadata,
324
324
  parent_snapshot_id=parent_snapshot_id,
@@ -1,17 +1,17 @@
1
1
  """``pf run`` — run a process simulation from a flowsheet JSON file."""
2
-
3
2
  from __future__ import annotations
4
3
 
4
+ import hashlib
5
5
  import os
6
+ from datetime import datetime, timezone
6
7
 
7
8
  import typer
8
9
  from loguru import logger
9
10
 
10
- from ..flowsheet import Flowsheet
11
11
  from ..eo import EOFlowsheet
12
+ from ..flowsheet import Flowsheet
13
+ from ..persistence.archive import ProcessStateArchive
12
14
  from ..provenance import build_dynamic_x0, build_run_info
13
- from ..result import plot_results, plot_timeseries, save_results_zarr
14
- from ..state import StateManager
15
15
  from .common import (
16
16
  check_providers,
17
17
  display_backend,
@@ -22,6 +22,22 @@ from .common import (
22
22
  )
23
23
 
24
24
 
25
+ def _flowsheet_hash(config: dict) -> str:
26
+ return hashlib.sha256(
27
+ json_dumps(config).encode("utf-8")
28
+ ).hexdigest()[:16]
29
+
30
+
31
+ def json_dumps(obj) -> str:
32
+ import json
33
+
34
+ return json.dumps(obj, sort_keys=True, default=str)
35
+
36
+
37
+ def _run_id() -> str:
38
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "_" + os.urandom(3).hex()
39
+
40
+
25
41
  def run(
26
42
  flowsheet: str = typer.Argument(help="Path to the flowsheet JSON file"),
27
43
  export_images: bool = typer.Option(
@@ -44,13 +60,17 @@ def run(
44
60
  mode = sim_cfg.get("mode", "steady")
45
61
  is_dynamic = mode == "dynamic"
46
62
 
63
+ # Standardized run id, shared with container-side artifact uploads.
64
+ run_id = _run_id()
65
+ flowsheet_hash = _flowsheet_hash(config)
66
+
47
67
  if is_dynamic:
48
- # Load .pfstate as t=0 if available
49
- state_path = os.path.join(outputs_dir, f"{base_name}.pfstate")
50
- sm = StateManager(state_path)
51
- state = sm.load_state()
68
+ # Load .pfarchive snapshot as t=0 if available.
69
+ archive_path = os.path.join(outputs_dir, f"{base_name}.pfarchive")
70
+ archive = ProcessStateArchive(archive_path)
71
+ state = archive.load_snapshot()
52
72
  if state is not None:
53
- stream_inits = sm.state_to_stream_dicts(state)
73
+ stream_inits = archive.state_to_stream_dicts(state)
54
74
  streams_cfg = config.get("streams", {})
55
75
  for s_name, s_vals in stream_inits.items():
56
76
  if s_name in streams_cfg:
@@ -59,9 +79,9 @@ def run(
59
79
  logger.debug(
60
80
  f"Stream '{s_name}' from snapshot not in current flowsheet — skipped."
61
81
  )
62
- logger.info("Using .pfstate converged state as dynamic t=0.")
82
+ logger.info("Using .pfarchive converged state as dynamic t=0.")
63
83
  else:
64
- logger.debug("No .pfstate found — starting dynamic run from flowsheet defaults.")
84
+ logger.debug("No .pfarchive found — starting dynamic run from flowsheet defaults.")
65
85
 
66
86
  fs = Flowsheet(config)
67
87
  logger.info("=== Dynamic Results ===")
@@ -94,20 +114,42 @@ def run(
94
114
 
95
115
  run_info = build_run_info(config, x0=fs.x0, var_names=fs.var_names)
96
116
 
97
- os.makedirs(outputs_dir, exist_ok=True)
98
- zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
99
- save_results_zarr(results, zarr_path, run_info=run_info)
100
- logger.info(f" Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
101
- logger.info(f"Results saved to {zarr_path}")
102
-
103
- for name, res in results.items():
104
- if isinstance(res, dict) and (s3_uri := res.get("metadata", {}).get("s3_uri")):
105
- logger.info(f" Run outputs (S3): {s3_uri}")
117
+ # Propagate run context to any containerized providers so their S3 uploads
118
+ # are keyed by the same run_id/flowsheet_hash as this archive run.
119
+ for provider in getattr(fs, "_provider_map", {}).values():
120
+ if hasattr(provider, "set_run_context"):
121
+ provider.set_run_context(run_id, flowsheet_hash)
122
+
123
+ # Build + persist the standardized run manifest.
124
+ archive_path = os.path.join(outputs_dir, f"{base_name}.pfarchive")
125
+ archive = ProcessStateArchive(archive_path)
126
+ manifest = fs.collect_outputs(run_id, mode, base_name, provenance=run_info.model_dump())
127
+ stream_results = {
128
+ k: v for k, v in results.items()
129
+ if not hasattr(v, "fields") # exclude EngineOutput objects
130
+ }
131
+ archive.save_run(manifest, stream_results=stream_results)
132
+
133
+ logger.info(f"Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
134
+ logger.info(f"Run saved : {os.path.join(archive_path, 'runs', run_id + '.json')}")
135
+
136
+ # Summarize standardized outputs.
137
+ for unit_name, out in manifest.units.items():
138
+ for f in out.fields:
139
+ logger.info(f" [{unit_name}] {f.name} = {f.quantity.value} {f.quantity.unit}")
140
+ for art in out.artifacts:
141
+ if art.remote_uris:
142
+ logger.info(f" [{unit_name}] artifact {art.name} → {art.remote_uris[0]}")
143
+ for stream_name, out in manifest.streams.items():
144
+ for f in out.fields:
145
+ logger.info(f" [{stream_name}] {f.name} = {f.quantity.value} {f.quantity.unit}")
106
146
 
107
147
  if export_images:
108
148
  try:
109
- plot_results(results, fname=f"{base_name}_results.png")
110
- plot_timeseries(results, fname=f"{base_name}_timeseries.png")
149
+ from ..result import plot_results, plot_timeseries
150
+
151
+ plot_results(stream_results, fname=f"{base_name}_results.png")
152
+ plot_timeseries(stream_results, fname=f"{base_name}_timeseries.png")
111
153
  logger.info(f"Plots saved: {base_name}_results.png, {base_name}_timeseries.png")
112
154
  except Exception as e:
113
155
  logger.warning(f"Failed to generate plots: {type(e).__name__}: {e}")
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
 
13
13
  from .jacobian import GlobalJacobianManager
14
14
  from .solver import EOSolver
15
- from ..types import SnapshotState
15
+ from ..types import EngineOutput, SnapshotState
16
16
  from ..utils.topology import get_inlets, get_outlets
17
17
 
18
18
 
@@ -70,6 +70,7 @@ class EOFlowsheet:
70
70
  """
71
71
  from processforge.providers.manager import teardown_providers
72
72
  manager = self._build()
73
+ self.engine_outputs: dict = {}
73
74
  try:
74
75
  x0 = self._warm_start(manager)
75
76
  self.x0: "np.ndarray" = x0.copy()
@@ -102,12 +103,46 @@ class EOFlowsheet:
102
103
  logger.info(f"EOFlowsheet: running standalone SolverUnit '{unit_name}'")
103
104
  unit_result = unit._run_impl({})
104
105
  results[unit_name] = unit_result
106
+ self.engine_outputs[unit_name] = unit_result
105
107
 
106
108
  logger.info("EOFlowsheet: simulation complete.")
109
+ self.results = results
107
110
  return results
108
111
  finally:
109
112
  teardown_providers(self._provider_map)
110
113
 
114
+ # ------------------------------------------------------------------
115
+ # Standardized output collection
116
+ # ------------------------------------------------------------------
117
+ def _thermo_provider(self):
118
+ """Return the first provider capable of stream thermo properties."""
119
+ provider_map = getattr(self, "_provider_map", None) or {}
120
+ for provider in provider_map.values():
121
+ name = type(provider).__name__
122
+ if "Container" in name:
123
+ continue
124
+ if callable(getattr(provider, "get_thermo_properties", None)):
125
+ return provider
126
+ return None
127
+
128
+ def collect_outputs(self, run_id: str, mode: str, flowsheet_name: str, provenance: dict | None = None):
129
+ """Assemble a :class:`RunManifest` from this run's results."""
130
+ from ..output_collector import build_run_manifest
131
+
132
+ stream_results = {
133
+ k: v for k, v in getattr(self, "results", {}).items()
134
+ if not isinstance(v, EngineOutput)
135
+ }
136
+ return build_run_manifest(
137
+ run_id=run_id,
138
+ mode=mode,
139
+ flowsheet_name=flowsheet_name,
140
+ provenance=provenance or {},
141
+ engine_outputs=getattr(self, "engine_outputs", {}),
142
+ stream_results=stream_results,
143
+ thermo_provider=self._thermo_provider(),
144
+ )
145
+
111
146
  # ------------------------------------------------------------------
112
147
  # Build phase
113
148
  # ------------------------------------------------------------------
@@ -332,12 +332,16 @@ class GlobalJacobianManager:
332
332
  return {}
333
333
  return self.scalars_from_results(self._results)
334
334
 
335
- def scalars_from_results(self, results: dict[str, dict]) -> dict[str, dict]:
336
- """Extract scalars from unit results dict."""
335
+ def scalars_from_results(self, results: dict) -> dict:
336
+ """Extract solver-unit outputs from a results dict.
337
+
338
+ Solver-unit results are :class:`~processforge.types.EngineOutput`
339
+ instances; stream entries are plain dicts and are ignored here.
340
+ """
341
+ from processforge.types import EngineOutput
342
+
337
343
  return {
338
- k: v
339
- for k, v in results.items()
340
- if isinstance(v, dict) and "status" in v
344
+ k: v for k, v in results.items() if isinstance(v, EngineOutput)
341
345
  }
342
346
 
343
347
  # ------------------------------------------------------------------