processforge 0.3.20__tar.gz → 0.3.21__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 (141) hide show
  1. {processforge-0.3.20/src/processforge.egg-info → processforge-0.3.21}/PKG-INFO +1 -1
  2. {processforge-0.3.20 → processforge-0.3.21}/pyproject.toml +1 -1
  3. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/__init__.py +3 -1
  4. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/__init__.py +5 -0
  5. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/apply.py +4 -2
  6. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/run.py +4 -2
  7. processforge-0.3.21/src/processforge/cli/runs.py +96 -0
  8. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/result.py +28 -0
  9. {processforge-0.3.20 → processforge-0.3.21/src/processforge.egg-info}/PKG-INFO +1 -1
  10. {processforge-0.3.20 → processforge-0.3.21}/src/processforge.egg-info/SOURCES.txt +2 -0
  11. processforge-0.3.21/tests/test_runs_command.py +122 -0
  12. {processforge-0.3.20 → processforge-0.3.21}/LICENSE +0 -0
  13. {processforge-0.3.20 → processforge-0.3.21}/MANIFEST.in +0 -0
  14. {processforge-0.3.20 → processforge-0.3.21}/README.md +0 -0
  15. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/closed-loop-chain.json +0 -0
  16. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/coupled/msre_coupled.json +0 -0
  17. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
  18. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
  19. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
  20. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/festim/tds_tungsten.json +0 -0
  21. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/hydraulic-chain.json +0 -0
  22. {processforge-0.3.20 → processforge-0.3.21}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  23. {processforge-0.3.20 → processforge-0.3.21}/setup.cfg +0 -0
  24. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/__main__.py +0 -0
  25. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/_schema.py +0 -0
  26. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/analysis/__init__.py +0 -0
  27. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/analysis/dof.py +0 -0
  28. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/api/__init__.py +0 -0
  29. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/api/serve.py +0 -0
  30. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/common.py +0 -0
  31. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/diagram.py +0 -0
  32. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/display.py +0 -0
  33. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/export_fmu.py +0 -0
  34. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/export_modelica.py +0 -0
  35. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/init.py +0 -0
  36. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/plan.py +0 -0
  37. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/cli/validate.py +0 -0
  38. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/compose.py +0 -0
  39. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/coupling.py +0 -0
  40. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/__init__.py +0 -0
  41. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/backends/__init__.py +0 -0
  42. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/backends/base.py +0 -0
  43. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/backends/casadi_backend.py +0 -0
  44. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  45. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/backends/scipy_backend.py +0 -0
  46. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/flowsheet.py +0 -0
  47. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/jacobian.py +0 -0
  48. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/mixin.py +0 -0
  49. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/solver.py +0 -0
  50. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/stream_var.py +0 -0
  51. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/__init__.py +0 -0
  52. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/flash_eo.py +0 -0
  53. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/heater_eo.py +0 -0
  54. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/pipes_eo.py +0 -0
  55. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/pump_eo.py +0 -0
  56. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/strainer_eo.py +0 -0
  57. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/eo/units/valve_eo.py +0 -0
  58. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/flowsheet.py +0 -0
  59. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/fmu/__init__.py +0 -0
  60. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/fmu/_fmi_vars.py +0 -0
  61. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/fmu/builder.py +0 -0
  62. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/fmu/slave_template.py +0 -0
  63. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/lock.py +0 -0
  64. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/modelica/__init__.py +0 -0
  65. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/modelica/mo_writer.py +0 -0
  66. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/modelica/omc_runner.py +0 -0
  67. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/modelica/transpiler.py +0 -0
  68. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/modelica/unit_equations.py +0 -0
  69. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/output_collector.py +0 -0
  70. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/pcl/__init__.py +0 -0
  71. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/pcl/compiler.py +0 -0
  72. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/pcl/namespace.py +0 -0
  73. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/persistence/__init__.py +0 -0
  74. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/persistence/archive.py +0 -0
  75. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/persistence/artifact_store.py +0 -0
  76. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/provenance.py +0 -0
  77. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/__init__.py +0 -0
  78. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/base.py +0 -0
  79. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  80. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/cantera_jacobian.py +0 -0
  81. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/cantera_provider.py +0 -0
  82. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/container_client.py +0 -0
  83. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/coolprop_provider.py +0 -0
  84. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/errors.py +0 -0
  85. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/festim_provider.py +0 -0
  86. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/jacobian_contributor.py +0 -0
  87. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/manager.py +0 -0
  88. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/modelica_jacobian.py +0 -0
  89. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/modelica_provider.py +0 -0
  90. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/openmc_provider.py +0 -0
  91. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/reference_state_registry.py +0 -0
  92. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/providers/registry.py +0 -0
  93. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/quantity.py +0 -0
  94. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/result_schema.py +0 -0
  95. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/schemas/__init__.py +0 -0
  96. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/schemas/festim/festim_model.py +0 -0
  97. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/schemas/flowsheet_schema.json +0 -0
  98. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  99. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/simulate.py +0 -0
  100. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/solver.py +0 -0
  101. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/state.py +0 -0
  102. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/thermo.py +0 -0
  103. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/types.py +0 -0
  104. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/__init__.py +0 -0
  105. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/base.py +0 -0
  106. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/cstr.py +0 -0
  107. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/flash.py +0 -0
  108. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/heater.py +0 -0
  109. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/pfr.py +0 -0
  110. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/pipes.py +0 -0
  111. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/pump.py +0 -0
  112. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/registry.py +0 -0
  113. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/solver_unit.py +0 -0
  114. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/strainer.py +0 -0
  115. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/tank.py +0 -0
  116. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/units/valve.py +0 -0
  117. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/__init__.py +0 -0
  118. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/flowsheet_diagram.py +0 -0
  119. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/mermaid_diagram.py +0 -0
  120. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/s3_upload.py +0 -0
  121. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/topology.py +0 -0
  122. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/unit_consistency.py +0 -0
  123. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/validate_flowsheet.py +0 -0
  124. {processforge-0.3.20 → processforge-0.3.21}/src/processforge/utils/validation.py +0 -0
  125. {processforge-0.3.20 → processforge-0.3.21}/src/processforge.egg-info/dependency_links.txt +0 -0
  126. {processforge-0.3.20 → processforge-0.3.21}/src/processforge.egg-info/entry_points.txt +0 -0
  127. {processforge-0.3.20 → processforge-0.3.21}/src/processforge.egg-info/requires.txt +0 -0
  128. {processforge-0.3.20 → processforge-0.3.21}/src/processforge.egg-info/top_level.txt +0 -0
  129. {processforge-0.3.20 → processforge-0.3.21}/tests/test_cli.py +0 -0
  130. {processforge-0.3.20 → processforge-0.3.21}/tests/test_container_client.py +0 -0
  131. {processforge-0.3.20 → processforge-0.3.21}/tests/test_coupling.py +0 -0
  132. {processforge-0.3.20 → processforge-0.3.21}/tests/test_festim_provider.py +0 -0
  133. {processforge-0.3.20 → processforge-0.3.21}/tests/test_festim_schema.py +0 -0
  134. {processforge-0.3.20 → processforge-0.3.21}/tests/test_init.py +0 -0
  135. {processforge-0.3.20 → processforge-0.3.21}/tests/test_openmc_provider.py +0 -0
  136. {processforge-0.3.20 → processforge-0.3.21}/tests/test_provider_errors.py +0 -0
  137. {processforge-0.3.20 → processforge-0.3.21}/tests/test_providers.py +0 -0
  138. {processforge-0.3.20 → processforge-0.3.21}/tests/test_result_zarr.py +0 -0
  139. {processforge-0.3.20 → processforge-0.3.21}/tests/test_s3_upload.py +0 -0
  140. {processforge-0.3.20 → processforge-0.3.21}/tests/test_state.py +0 -0
  141. {processforge-0.3.20 → processforge-0.3.21}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.20
3
+ Version: 0.3.21
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.20"
7
+ version = "0.3.21"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -18,6 +18,7 @@ from .utils.validate_flowsheet import validate_flowsheet
18
18
  from .result import (
19
19
  plot_results,
20
20
  plot_timeseries,
21
+ relink_latest_results,
21
22
  save_results_zarr,
22
23
  )
23
24
  from .units.pump import Pump
@@ -29,7 +30,7 @@ from .units.flash import Flash
29
30
  from .units.heater import Heater
30
31
  from .eo import EOFlowsheet, EOSolver
31
32
 
32
- __version__ = "0.3.19"
33
+ __version__ = "0.3.21"
33
34
 
34
35
  __all__ = [
35
36
  "Flowsheet",
@@ -41,6 +42,7 @@ __all__ = [
41
42
  "plot_results",
42
43
  "plot_timeseries",
43
44
  "save_results_zarr",
45
+ "relink_latest_results",
44
46
  "build_run_info",
45
47
  "Pump",
46
48
  "Valve",
@@ -11,6 +11,7 @@ def register_commands(app: typer.Typer) -> None:
11
11
  from .validate import validate
12
12
  from .run import run
13
13
  from .apply import apply
14
+ from .runs import runs
14
15
  from .plan import plan
15
16
  from .diagram import diagram
16
17
  from .export_fmu import export_fmu
@@ -29,6 +30,10 @@ def register_commands(app: typer.Typer) -> None:
29
30
  "apply",
30
31
  help="Apply flowsheet using state-based warm start and homotopy fallback",
31
32
  )(apply)
33
+ app.command(
34
+ "runs",
35
+ help="List runs for a flowsheet, or show one run's full manifest (pf runs <flowsheet> [<run_id>])",
36
+ )(runs)
32
37
  app.command(
33
38
  "plan",
34
39
  help="Validate a flowsheet, run DOF analysis, structural diff, container health check, and generate a Mermaid diagram",
@@ -31,13 +31,15 @@ def _persist_run(archive, fs, results, run_info, config, base_name, snapshot_id)
31
31
  # Always persist a Zarr copy of the standardized outputs (fields + artifacts)
32
32
  # inside the archive, mirroring `pf run`.
33
33
  try:
34
- from ..result import save_results_zarr
34
+ from ..result import relink_latest_results, save_results_zarr
35
35
 
36
+ run_results_dir = os.path.join(archive.path, "results", run_id)
36
37
  save_results_zarr(
37
38
  results,
38
- os.path.join(archive.path, "results.zarr"),
39
+ os.path.join(run_results_dir, "results.zarr"),
39
40
  run_info,
40
41
  )
42
+ relink_latest_results(archive.path, run_id)
41
43
  except Exception as e:
42
44
  logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
43
45
 
@@ -158,13 +158,15 @@ def run(
158
158
  # Cantera) and for both local and remote docker runs — remote artifacts
159
159
  # reference their S3 remote_uris.
160
160
  try:
161
- from ..result import save_results_zarr
161
+ from ..result import relink_latest_results, save_results_zarr
162
162
 
163
+ run_results_dir = os.path.join(archive.path, "results", run_id)
163
164
  save_results_zarr(
164
165
  results,
165
- os.path.join(archive.path, "results.zarr"),
166
+ os.path.join(run_results_dir, "results.zarr"),
166
167
  run_info,
167
168
  )
169
+ relink_latest_results(archive.path, run_id)
168
170
  except Exception as e:
169
171
  logger.warning(f"Failed to write results.zarr: {type(e).__name__}: {e}")
170
172
 
@@ -0,0 +1,96 @@
1
+ """``pf runs`` — list runs for a flowsheet, or show one run's full manifest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+
8
+ import typer
9
+ from loguru import logger
10
+
11
+ from .common import flowsheet_basename, output_root
12
+ from ..persistence.archive import ProcessStateArchive
13
+
14
+
15
+ def _summary_for(manifest) -> str:
16
+ """One-line summary of a run's primary unit fields (e.g. keff)."""
17
+ parts = []
18
+ for unit_name, out in manifest.units.items():
19
+ for f in out.fields:
20
+ val = f.quantity.value
21
+ unit = f.quantity.unit or ""
22
+ parts.append(f"{unit_name}.{f.name}={val}{(' ' + unit) if unit else ''}")
23
+ return " ".join(parts) if parts else "-"
24
+
25
+
26
+ def _zarr_status(archive_path: str, run_id: str) -> str:
27
+ zarr_path = os.path.join(archive_path, "results", run_id, "results.zarr")
28
+ return "✓" if os.path.exists(zarr_path) else "✗"
29
+
30
+
31
+ def runs(
32
+ flowsheet: str = typer.Argument(..., help="Path to the flowsheet JSON"),
33
+ run_id: str = typer.Argument(
34
+ None,
35
+ help="Show this run's full manifest instead of listing all runs",
36
+ ),
37
+ ):
38
+ """List runs stored in a flowsheet's ``.pfarchive`` (or show one run)."""
39
+ base = flowsheet_basename(flowsheet)
40
+ archive_path = os.path.join(output_root(), f"{base}.pfarchive")
41
+
42
+ if not os.path.isdir(archive_path):
43
+ logger.error(
44
+ f"No archive found for '{flowsheet}' at {archive_path}. "
45
+ "Run 'pf run' first."
46
+ )
47
+ raise typer.Exit(code=1)
48
+
49
+ archive = ProcessStateArchive(archive_path)
50
+ latest = archive.latest_run()
51
+ latest_id = latest.run_id if latest is not None else None
52
+
53
+ if run_id is not None:
54
+ if run_id not in archive.list_runs():
55
+ logger.error(f"Run '{run_id}' not found in {archive_path}/runs.")
56
+ raise typer.Exit(code=1)
57
+ manifest = archive.load_run(run_id)
58
+ typer.echo(json.dumps(manifest.model_dump(), indent=2, default=str))
59
+ typer.echo("\nArtifacts on disk:")
60
+ for unit_name, out in manifest.units.items():
61
+ for art in out.artifacts:
62
+ present = os.path.exists(art.local_path) if art.local_path else False
63
+ remote = bool(art.remote_uris)
64
+ status = (
65
+ "local✓" if present else ("remote-only" if remote else "missing")
66
+ )
67
+ loc = art.local_path or "no local path"
68
+ typer.echo(f" [{unit_name}] {art.name}: {status} ({loc})")
69
+ return
70
+
71
+ run_ids = archive.list_runs()
72
+ if not run_ids:
73
+ typer.echo(f"No runs found for '{flowsheet}'.")
74
+ return
75
+
76
+ header = (
77
+ f"{'RUN ID':<28} {'TIMESTAMP':<22} {'MODE':<8} "
78
+ f"{'ZARR':<5} {'LATEST':<6} SUMMARY"
79
+ )
80
+ typer.echo(header)
81
+ typer.echo("-" * len(header))
82
+ for rid in run_ids:
83
+ manifest = archive.load_run(rid)
84
+ if manifest is None:
85
+ continue
86
+ marker = "*" if rid == latest_id else " "
87
+ typer.echo(
88
+ f"{rid:<28} {manifest.timestamp:<22} {manifest.mode:<8} "
89
+ f"{_zarr_status(archive_path, rid):<5} {marker:<6} "
90
+ f"{_summary_for(manifest)}"
91
+ )
92
+ typer.echo("")
93
+ typer.echo(
94
+ "Zarr: ✓ present on disk, ✗ deleted/missing. '*' marks the latest run. "
95
+ "'pf runs <flowsheet> <run_id>' prints the full manifest."
96
+ )
@@ -272,6 +272,34 @@ def save_results_zarr(results, fname="results.zarr", run_info: RunInfo | dict |
272
272
  return store_path
273
273
 
274
274
 
275
+ def relink_latest_results(archive_path: str, run_id: str) -> None:
276
+ """Point ``<archive>/results.zarr`` at the latest per-run zarr.
277
+
278
+ Each run now writes its zarr to ``<archive>/results/<run_id>/results.zarr``
279
+ (see `save_results_zarr`). To keep backward compatibility with tooling that
280
+ reads a single ``results.zarr`` at the archive root, we make that path a
281
+ symlink to the newest run. Falls back to a copy on platforms/filesystems
282
+ without symlink support (or when the existing target is a real directory).
283
+ """
284
+ latest = os.path.join(archive_path, "results", run_id, "results.zarr")
285
+ if not os.path.exists(latest):
286
+ return
287
+ link_path = os.path.join(archive_path, "results.zarr")
288
+
289
+ # Remove any existing target (real dir or stale/broken symlink) without
290
+ # following the link.
291
+ if os.path.islink(link_path):
292
+ os.unlink(link_path)
293
+ elif os.path.exists(link_path):
294
+ shutil.rmtree(link_path)
295
+
296
+ try:
297
+ os.symlink(latest, link_path)
298
+ except (OSError, NotImplementedError):
299
+ # Non-POSIX / restricted filesystem: keep a physical copy instead.
300
+ shutil.copytree(latest, link_path)
301
+
302
+
275
303
  def save_results_zarr_s3(results, s3_uri: str, run_info: RunInfo | dict | None = None):
276
304
  """Write simulation results directly to an S3-backed Zarr store.
277
305
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.20
3
+ Version: 0.3.21
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
@@ -47,6 +47,7 @@ src/processforge/cli/export_modelica.py
47
47
  src/processforge/cli/init.py
48
48
  src/processforge/cli/plan.py
49
49
  src/processforge/cli/run.py
50
+ src/processforge/cli/runs.py
50
51
  src/processforge/cli/validate.py
51
52
  src/processforge/eo/__init__.py
52
53
  src/processforge/eo/flowsheet.py
@@ -132,6 +133,7 @@ tests/test_openmc_provider.py
132
133
  tests/test_provider_errors.py
133
134
  tests/test_providers.py
134
135
  tests/test_result_zarr.py
136
+ tests/test_runs_command.py
135
137
  tests/test_s3_upload.py
136
138
  tests/test_state.py
137
139
  tests/test_types.py
@@ -0,0 +1,122 @@
1
+ """Tests for per-run Zarr outputs and the ``pf runs`` listing command."""
2
+ import json
3
+ import os
4
+
5
+ import zarr
6
+ from typer.testing import CliRunner
7
+
8
+ import typer
9
+
10
+ from processforge.cli.runs import runs
11
+ from processforge.result import relink_latest_results, save_results_zarr
12
+ from processforge.types import (
13
+ EngineOutput,
14
+ OutputArtifact,
15
+ OutputField,
16
+ Quantity,
17
+ RunManifest,
18
+ )
19
+
20
+
21
+ def _eo(run_id_suffix: str, keff: float) -> EngineOutput:
22
+ return EngineOutput(
23
+ engine="openmc",
24
+ sim_type="eigenvalue_reactor",
25
+ status="completed",
26
+ fields=[
27
+ OutputField(
28
+ name="k_eff",
29
+ quantity=Quantity(value=keff, unit=""),
30
+ kind="scalar",
31
+ source="keff",
32
+ )
33
+ ],
34
+ artifacts=[
35
+ OutputArtifact(
36
+ name="statepoint",
37
+ kind="h5",
38
+ local_path=f"/data/openmc/msre_run/{run_id_suffix}/statepoint.h5",
39
+ remote_uris=[],
40
+ source="local",
41
+ )
42
+ ],
43
+ )
44
+
45
+
46
+ def test_relink_latest_results_symlinks_to_latest(tmp_path):
47
+ d = str(tmp_path)
48
+ rid_a = "20260101T000000Z_aaaaaa"
49
+ rid_b = "20260102T000000Z_bbbbbb"
50
+
51
+ save_results_zarr({"openmc_solver": _eo("a", 1.04)},
52
+ os.path.join(d, "results", rid_a, "results.zarr"), None)
53
+ relink_latest_results(d, rid_a)
54
+ link = os.path.join(d, "results.zarr")
55
+ assert os.path.islink(link)
56
+ assert os.path.realpath(link) == os.path.realpath(
57
+ os.path.join(d, "results", rid_a, "results.zarr"))
58
+
59
+ # A second run re-points the symlink without clobbering run A's zarr.
60
+ save_results_zarr({"openmc_solver": _eo("b", 1.06)},
61
+ os.path.join(d, "results", rid_b, "results.zarr"), None)
62
+ relink_latest_results(d, rid_b)
63
+ assert os.path.realpath(link) == os.path.realpath(
64
+ os.path.join(d, "results", rid_b, "results.zarr"))
65
+ assert os.path.isdir(os.path.join(d, "results", rid_a, "results.zarr"))
66
+
67
+ root = zarr.open_group(link, mode="r")
68
+ assert abs(root["openmc_solver"]["k_eff"][0] - 1.06) < 1e-9
69
+
70
+
71
+ def test_pf_runs_lists_runs_with_disk_status(tmp_path, monkeypatch):
72
+ monkeypatch.setenv("PROCESSFORGE_OUTPUT_DIR", str(tmp_path))
73
+ base = "test_flowsheet"
74
+ archive_path = tmp_path / f"{base}.pfarchive"
75
+ runs_dir = archive_path / "runs"
76
+ results_dir = archive_path / "results"
77
+ runs_dir.mkdir(parents=True)
78
+
79
+ rid_a = "20260101T000000Z_aaaaaa"
80
+ rid_b = "20260102T000000Z_bbbbbb"
81
+
82
+ # Run A has its zarr on disk; run B does not (deleted).
83
+ save_results_zarr({"openmc_solver": _eo("a", 1.04)},
84
+ str(results_dir / rid_a / "results.zarr"), None)
85
+ for rid, keff in ((rid_a, 1.04), (rid_b, 1.06)):
86
+ manifest = RunManifest(
87
+ run_id=rid, timestamp=rid.split("_")[0], mode="steady",
88
+ units={"openmc_solver": _eo(rid[-6:], keff)},
89
+ )
90
+ (runs_dir / f"{rid}.json").write_text(manifest.model_dump_json())
91
+ (archive_path / "latest_run").write_text(rid_b)
92
+
93
+ app = typer.Typer()
94
+ app.command()(runs)
95
+ result = CliRunner().invoke(app, [f"/tmp/{base}.json"])
96
+ assert result.exit_code == 0, result.output
97
+ assert rid_a in result.output
98
+ assert rid_b in result.output
99
+ assert "✓" in result.output and "✗" in result.output
100
+ assert "*" in result.output # latest marker on run B
101
+
102
+
103
+ def test_pf_runs_shows_manifest_and_artifacts(tmp_path, monkeypatch):
104
+ monkeypatch.setenv("PROCESSFORGE_OUTPUT_DIR", str(tmp_path))
105
+ base = "test_flowsheet"
106
+ archive_path = tmp_path / f"{base}.pfarchive"
107
+ runs_dir = archive_path / "runs"
108
+ runs_dir.mkdir(parents=True)
109
+
110
+ rid = "20260101T000000Z_aaaaaa"
111
+ manifest = RunManifest(
112
+ run_id=rid, timestamp=rid.split("_")[0], mode="steady",
113
+ units={"openmc_solver": _eo(rid[-6:], 1.04)},
114
+ )
115
+ (runs_dir / f"{rid}.json").write_text(manifest.model_dump_json())
116
+
117
+ app = typer.Typer()
118
+ app.command()(runs)
119
+ result = CliRunner().invoke(app, [f"/tmp/{base}.json", rid])
120
+ assert result.exit_code == 0, result.output
121
+ assert "k_eff" in result.output
122
+ assert "Artifacts on disk:" in result.output
File without changes
File without changes
File without changes
File without changes