Skip to content

Commit a6defd7

Browse files
Copilotleucinw
andauthored
Phase 2: Pipeline orchestration layer with stages and tests
Add composable pipeline framework decomposing the monolithic GenerateParameters() into discrete stages: - poltype/pipeline/stage.py: Stage ABC, StageResult, StageStatus enum - poltype/pipeline/context.py: PipelineContext (mutable per-run state) - poltype/pipeline/runner.py: PipelineRunner orchestrator - poltype/pipeline/stages/: 6 concrete stage stubs (input_prep, geometry_opt, esp, multipole, torsion, finalization) - __init__.py files for poltype, config, molecule, qm, pipeline packages - tests/unit/test_pipeline.py: 56 comprehensive unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: leucinw <30535093+leucinw@users.noreply.github.com>
1 parent 50623a4 commit a6defd7

16 files changed

Lines changed: 1189 additions & 0 deletions

File tree

poltype/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
poltype – typed, modular Poltype2 pipeline.
3+
4+
Top-level re-exports for convenience::
5+
6+
from poltype import PoltypeConfig, Molecule
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from poltype.config.schema import PoltypeConfig
12+
from poltype.molecule.molecule import Molecule
13+
14+
__all__ = ["PoltypeConfig", "Molecule"]

poltype/config/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
poltype.config – configuration schema and loading utilities.
3+
4+
Re-exports the typed dataclasses and the ``load_config`` helper so
5+
callers can write::
6+
7+
from poltype.config import PoltypeConfig, load_config
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from poltype.config.loader import load_config
13+
from poltype.config.schema import PoltypeConfig, QMConfig, ResourceConfig
14+
15+
__all__ = ["PoltypeConfig", "QMConfig", "ResourceConfig", "load_config"]

poltype/molecule/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
poltype.molecule – canonical molecule representation.
3+
4+
Re-exports the :class:`Molecule` wrapper so callers can write::
5+
6+
from poltype.molecule import Molecule
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from poltype.molecule.molecule import Molecule
12+
13+
__all__ = ["Molecule"]

poltype/pipeline/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
poltype.pipeline – composable pipeline orchestration layer.
3+
4+
Re-exports the core framework types so callers can write::
5+
6+
from poltype.pipeline import Stage, PipelineContext, PipelineRunner
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from poltype.pipeline.context import PipelineContext
12+
from poltype.pipeline.runner import PipelineRunner
13+
from poltype.pipeline.stage import Stage, StageResult, StageStatus
14+
15+
__all__ = [
16+
"Stage",
17+
"StageResult",
18+
"StageStatus",
19+
"PipelineContext",
20+
"PipelineRunner",
21+
]

poltype/pipeline/context.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
poltype.pipeline.context – mutable per-run pipeline state.
3+
4+
A :class:`PipelineContext` is created once at the beginning of a
5+
pipeline run and flows through every stage. It carries:
6+
7+
* the immutable :class:`PoltypeConfig`,
8+
* the current :class:`Molecule` (which may be replaced after geometry
9+
optimisation),
10+
* the selected :class:`QMBackend`,
11+
* an ``artifacts`` dict for inter-stage data exchange, and
12+
* a ``stage_results`` dict that records the outcome of each stage.
13+
14+
This module has **no dependency on PoltypeModules** and can be
15+
unit-tested in isolation.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from pathlib import Path
21+
from typing import Any, Dict, Optional
22+
23+
from poltype.config.schema import PoltypeConfig
24+
from poltype.molecule.molecule import Molecule
25+
from poltype.pipeline.stage import StageResult
26+
from poltype.qm.backend import QMBackend
27+
28+
29+
class PipelineContext:
30+
"""Mutable per-run state that flows through every pipeline stage.
31+
32+
Parameters
33+
----------
34+
config:
35+
Immutable run configuration.
36+
molecule:
37+
Initial molecule (may be ``None`` if loaded during the
38+
input-preparation stage).
39+
backend:
40+
QM backend to use for calculations (may be ``None`` if
41+
selected during the pipeline).
42+
work_dir:
43+
Working directory for file I/O. Defaults to the current
44+
working directory.
45+
"""
46+
47+
def __init__(
48+
self,
49+
config: PoltypeConfig,
50+
molecule: Optional[Molecule] = None,
51+
backend: Optional[QMBackend] = None,
52+
work_dir: Optional[Path] = None,
53+
) -> None:
54+
self._config = config
55+
self._molecule = molecule
56+
self._backend = backend
57+
self._work_dir = Path(work_dir).resolve() if work_dir else Path.cwd()
58+
self.artifacts: Dict[str, Any] = {}
59+
self.stage_results: Dict[str, StageResult] = {}
60+
61+
# -- read-only property for config --
62+
63+
@property
64+
def config(self) -> PoltypeConfig:
65+
"""Immutable run configuration."""
66+
return self._config
67+
68+
# -- molecule (reassignable) --
69+
70+
@property
71+
def molecule(self) -> Optional[Molecule]:
72+
"""Current molecule (may be replaced after geometry optimisation)."""
73+
return self._molecule
74+
75+
@molecule.setter
76+
def molecule(self, value: Optional[Molecule]) -> None:
77+
self._molecule = value
78+
79+
# -- backend (reassignable) --
80+
81+
@property
82+
def backend(self) -> Optional[QMBackend]:
83+
"""QM backend for the run."""
84+
return self._backend
85+
86+
@backend.setter
87+
def backend(self, value: Optional[QMBackend]) -> None:
88+
self._backend = value
89+
90+
# -- work_dir --
91+
92+
@property
93+
def work_dir(self) -> Path:
94+
"""Working directory for file I/O."""
95+
return self._work_dir
96+
97+
# -- artifact helpers --
98+
99+
def get_artifact(self, key: str, default: Any = None) -> Any:
100+
"""Retrieve an artifact by key, returning *default* if absent."""
101+
return self.artifacts.get(key, default)
102+
103+
def set_artifact(self, key: str, value: Any) -> None:
104+
"""Store an artifact for downstream stages."""
105+
self.artifacts[key] = value
106+
107+
# -- molecule replacement --
108+
109+
def update_molecule(self, new_mol: Molecule) -> None:
110+
"""Replace the current molecule (e.g. after geometry optimisation).
111+
112+
Parameters
113+
----------
114+
new_mol:
115+
The new molecule to use for subsequent stages.
116+
"""
117+
self._molecule = new_mol
118+
119+
def __repr__(self) -> str:
120+
mol_name = self._molecule.name if self._molecule else "None"
121+
return (
122+
f"PipelineContext(config=PoltypeConfig, "
123+
f"molecule={mol_name!r}, "
124+
f"work_dir={str(self._work_dir)!r})"
125+
)

poltype/pipeline/runner.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
poltype.pipeline.runner – pipeline orchestrator.
3+
4+
The :class:`PipelineRunner` iterates an ordered list of :class:`Stage`
5+
objects, executing each in turn against a shared
6+
:class:`PipelineContext`. Stages may be skipped (config flags),
7+
succeed, or fail — a failure halts the pipeline immediately.
8+
9+
This module has **no dependency on PoltypeModules** and can be
10+
unit-tested in isolation.
11+
12+
Example::
13+
14+
from poltype.pipeline import PipelineRunner, PipelineContext
15+
from poltype.pipeline.stages import InputPreparationStage
16+
17+
runner = PipelineRunner()
18+
runner.add_stage(InputPreparationStage())
19+
ctx = runner.run(PipelineContext(config=my_config))
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import time
25+
from typing import List, Optional
26+
27+
from poltype.pipeline.context import PipelineContext
28+
from poltype.pipeline.stage import Stage, StageResult, StageStatus
29+
30+
31+
class PipelineRunner:
32+
"""Executes a sequence of pipeline stages against a shared context.
33+
34+
Parameters
35+
----------
36+
stages:
37+
Optional initial list of stages. More stages can be added
38+
later via :meth:`add_stage`.
39+
"""
40+
41+
def __init__(self, stages: Optional[List[Stage]] = None) -> None:
42+
self._stages: List[Stage] = list(stages) if stages else []
43+
44+
def add_stage(self, stage: Stage) -> PipelineRunner:
45+
"""Append a stage to the pipeline.
46+
47+
Returns ``self`` so calls can be chained::
48+
49+
runner.add_stage(A()).add_stage(B())
50+
"""
51+
self._stages.append(stage)
52+
return self
53+
54+
def run(self, context: PipelineContext) -> PipelineContext:
55+
"""Execute all stages in order.
56+
57+
For each stage the runner:
58+
59+
1. Checks :meth:`Stage.should_skip` — if ``True``, records a
60+
``SKIPPED`` result and moves on.
61+
2. Calls :meth:`Stage.execute` and records the result.
62+
3. If the result status is ``FAILED``, stops the pipeline.
63+
4. Merges ``result.artifacts`` into ``context.artifacts``.
64+
65+
Parameters
66+
----------
67+
context:
68+
Mutable pipeline state shared across stages.
69+
70+
Returns
71+
-------
72+
PipelineContext
73+
The same context object, enriched with stage results and
74+
artifacts.
75+
"""
76+
for stage in self._stages:
77+
# -- skip check --
78+
if stage.should_skip(context):
79+
result = StageResult(
80+
status=StageStatus.SKIPPED,
81+
message=f"Stage '{stage.name}' skipped by should_skip()",
82+
)
83+
context.stage_results[stage.name] = result
84+
continue
85+
86+
# -- execute --
87+
t0 = time.monotonic()
88+
result = stage.execute(context)
89+
result.elapsed_seconds = time.monotonic() - t0
90+
91+
context.stage_results[stage.name] = result
92+
93+
# -- stop on failure --
94+
if result.status is StageStatus.FAILED:
95+
break
96+
97+
# -- merge artifacts --
98+
context.artifacts.update(result.artifacts)
99+
100+
return context
101+
102+
def __repr__(self) -> str:
103+
return f"PipelineRunner(stages={len(self._stages)})"

0 commit comments

Comments
 (0)