midas-pipeline 0.1.0__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.
- midas_pipeline-0.1.0/PKG-INFO +94 -0
- midas_pipeline-0.1.0/README.md +50 -0
- midas_pipeline-0.1.0/midas_pipeline/__init__.py +82 -0
- midas_pipeline-0.1.0/midas_pipeline/__main__.py +11 -0
- midas_pipeline-0.1.0/midas_pipeline/_logging.py +47 -0
- midas_pipeline-0.1.0/midas_pipeline/cli.py +543 -0
- midas_pipeline-0.1.0/midas_pipeline/config.py +498 -0
- midas_pipeline-0.1.0/midas_pipeline/dispatch.py +72 -0
- midas_pipeline-0.1.0/midas_pipeline/em_refine.py +512 -0
- midas_pipeline-0.1.0/midas_pipeline/ff_shim.py +53 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/__init__.py +510 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_cluster.py +443 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_consolidation_io.py +249 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_geom.py +103 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_patches.py +128 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_sinogen.py +318 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_sinogen_indexing.py +391 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_spot_association.py +170 -0
- midas_pipeline-0.1.0/midas_pipeline/find_grains/_voxel_keys.py +143 -0
- midas_pipeline-0.1.0/midas_pipeline/fuse.py +252 -0
- midas_pipeline-0.1.0/midas_pipeline/pipeline.py +254 -0
- midas_pipeline-0.1.0/midas_pipeline/potts.py +198 -0
- midas_pipeline-0.1.0/midas_pipeline/provenance.py +185 -0
- midas_pipeline-0.1.0/midas_pipeline/py.typed +0 -0
- midas_pipeline-0.1.0/midas_pipeline/recon/__init__.py +40 -0
- midas_pipeline-0.1.0/midas_pipeline/recon/fbp.py +188 -0
- midas_pipeline-0.1.0/midas_pipeline/recon/mlem.py +521 -0
- midas_pipeline-0.1.0/midas_pipeline/recon/voxelmap.py +136 -0
- midas_pipeline-0.1.0/midas_pipeline/results.py +237 -0
- midas_pipeline-0.1.0/midas_pipeline/seeding/__init__.py +57 -0
- midas_pipeline-0.1.0/midas_pipeline/seeding/align.py +180 -0
- midas_pipeline-0.1.0/midas_pipeline/seeding/ff_index.py +181 -0
- midas_pipeline-0.1.0/midas_pipeline/seeding/handoff.py +183 -0
- midas_pipeline-0.1.0/midas_pipeline/seeding/merge_all.py +115 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/__init__.py +76 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/_base.py +95 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/_stub.py +41 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/binning.py +210 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/calc_radius.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/consolidation.py +77 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/consolidation_pf.py +354 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/cross_det_merge.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/em_refine.py +54 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/find_grains_stage.py +88 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/fuse.py +78 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/global_powder.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/hkl.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/indexing.py +169 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/merge_overlaps.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/merge_scans.py +597 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/peakfit.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/potts.py +75 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/process_grains.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/reconstruct.py +191 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/refinement.py +209 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/seeding.py +154 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/sinogen.py +68 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/transforms.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline/stages/zip_convert.py +10 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/PKG-INFO +94 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/SOURCES.txt +65 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/dependency_links.txt +1 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/entry_points.txt +2 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/requires.txt +29 -0
- midas_pipeline-0.1.0/midas_pipeline.egg-info/top_level.txt +1 -0
- midas_pipeline-0.1.0/pyproject.toml +78 -0
- midas_pipeline-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: midas-pipeline
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified MIDAS HEDM orchestrator. FF is the single-scan degeneracy of PF; one orchestrator, --scan-mode {ff,pf}.
|
|
5
|
+
Author-email: Hemant Sharma <hsharma@anl.gov>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/marinerhemant/MIDAS
|
|
8
|
+
Project-URL: Documentation, https://github.com/marinerhemant/MIDAS
|
|
9
|
+
Project-URL: Issues, https://github.com/marinerhemant/MIDAS/issues
|
|
10
|
+
Keywords: MIDAS,HEDM,FF-HEDM,PF-HEDM,scanning,diffraction,pipeline,PyTorch,multi-detector,crystallography,polycrystal,tomography
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: numpy>=1.22
|
|
19
|
+
Requires-Dist: torch>=2.0
|
|
20
|
+
Requires-Dist: zarr<3,>=2.14
|
|
21
|
+
Requires-Dist: h5py>=3.7
|
|
22
|
+
Requires-Dist: tifffile
|
|
23
|
+
Requires-Dist: pandas>=1.5
|
|
24
|
+
Requires-Dist: midas-hkls>=0.2.0
|
|
25
|
+
Requires-Dist: midas-peakfit>=0.2.0
|
|
26
|
+
Requires-Dist: midas-transforms>=0.1.1
|
|
27
|
+
Requires-Dist: midas-index>=0.3.0
|
|
28
|
+
Requires-Dist: midas-fit-grain>=0.1.0a0
|
|
29
|
+
Requires-Dist: midas-process-grains>=0.1.0
|
|
30
|
+
Requires-Dist: midas-diffract>=0.1.0
|
|
31
|
+
Requires-Dist: midas-parsl-configs>=0.1.0
|
|
32
|
+
Requires-Dist: midas-stress>=0.6.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
36
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
37
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
38
|
+
Provides-Extra: notebook
|
|
39
|
+
Requires-Dist: matplotlib; extra == "notebook"
|
|
40
|
+
Requires-Dist: ipykernel; extra == "notebook"
|
|
41
|
+
Requires-Dist: jupyter; extra == "notebook"
|
|
42
|
+
Provides-Extra: fast
|
|
43
|
+
Requires-Dist: numba>=0.56; extra == "fast"
|
|
44
|
+
|
|
45
|
+
# midas-pipeline
|
|
46
|
+
|
|
47
|
+
End-to-end MIDAS HEDM orchestrator. **FF is the single-scan degeneracy of PF.** One package, one CLI, two scan modes.
|
|
48
|
+
|
|
49
|
+
## Status
|
|
50
|
+
|
|
51
|
+
**Alpha (0.1.0a0) — end-to-end PF and FF paths live.** The scanning indexer matches the C `IndexerScanningOMP` reference on its 1-voxel C-parity gate (seed identity, solution counts, voxel-center positions exact; orientation matrices within mrad-scale, the refiner closes the gap downstream). Cross-implementation perf optimization (~130s/voxel single-threaded today) is open work — see [project_midas_index_scanning_perf](https://github.com/marinerhemant/MIDAS/blob/master/packages/midas_pipeline/dev/perf-notes.md) tracking.
|
|
52
|
+
|
|
53
|
+
Stages call in-process Python kernels via `midas-index` / `midas-fit-grain` / `midas-transforms` / `midas-stress`. FF mode shells out to `python -m midas_index` and `python -m midas_fit_grain` (same kernels, subprocess for the FF parity-preserving pattern). No CUDA C; GPU is torch-only.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install -e packages/midas_pipeline
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
midas-pipeline run --scan-mode {ff,pf,auto} --params Parameters.txt --result rundir/
|
|
65
|
+
midas-pipeline status rundir/
|
|
66
|
+
midas-pipeline resume rundir/ --from <stage>
|
|
67
|
+
midas-pipeline reprocess rundir/
|
|
68
|
+
midas-pipeline inspect rundir/LayerNr_1/
|
|
69
|
+
midas-pipeline simulate --out simdir/ --params Parameters.txt
|
|
70
|
+
midas-pipeline seed --params ... --output UniqueOrientations.csv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
When `--scan-mode` is omitted (default `auto`), the CLI sniffs the parameter file: `nScans > 1` or presence of `BeamSize` / scanning keys → `pf`, otherwise `ff`. For PF mode, `--n-scans`, `--scan-step`, `--beam-size`, and `--scan-pos-tol` default to values in the params file (CLI flags override).
|
|
74
|
+
|
|
75
|
+
## Coexistence with `midas-ff-pipeline`
|
|
76
|
+
|
|
77
|
+
The legacy `midas-ff-pipeline` console-script is preserved as an independent FF orchestrator (its own kernels, its own CLI). It is **not** deprecated by `midas-pipeline run --scan-mode ff` — both paths invoke the same `midas-index` / `midas-fit-grain` kernels under the hood, and both stay green on the FF parity gate. Pick whichever is more convenient for your workflow.
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
See [`../../.claude/plans/for-pf-we-don-t-lovely-locket.md`](../../.claude/plans/for-pf-we-don-t-lovely-locket.md) for the long-form plan. Quick summary:
|
|
82
|
+
|
|
83
|
+
- **One orchestrator** with a mode-dependent `STAGE_ORDER`.
|
|
84
|
+
- **Shared kernel packages** (`midas-index`, `midas-fit-grain`, `midas-transforms`, etc.) extended in place; FF behavior preserved by parity gates.
|
|
85
|
+
- **PF-only modules** live inside `midas_pipeline` (`find_grains/`, `sinogen`, `recon/`, `fuse`, `potts`, `em_refine`, `seeding/`).
|
|
86
|
+
- **Differentiability + multi-device** mandatory on every new compute path (CPU / CUDA / MPS via torch).
|
|
87
|
+
|
|
88
|
+
## Constraints
|
|
89
|
+
|
|
90
|
+
- No CUDA C; GPU support is torch-only.
|
|
91
|
+
- No deletions of legacy code in this effort.
|
|
92
|
+
- `midas-process-grains` is FF-only; PF consolidation is fresh pure-Python.
|
|
93
|
+
- `utils/calcMiso.py` is not imported by this package; all orientation math comes from `midas-stress`.
|
|
94
|
+
- `TOMO/midas_tomo_python.py` is imported in place, not relocated.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# midas-pipeline
|
|
2
|
+
|
|
3
|
+
End-to-end MIDAS HEDM orchestrator. **FF is the single-scan degeneracy of PF.** One package, one CLI, two scan modes.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
**Alpha (0.1.0a0) — end-to-end PF and FF paths live.** The scanning indexer matches the C `IndexerScanningOMP` reference on its 1-voxel C-parity gate (seed identity, solution counts, voxel-center positions exact; orientation matrices within mrad-scale, the refiner closes the gap downstream). Cross-implementation perf optimization (~130s/voxel single-threaded today) is open work — see [project_midas_index_scanning_perf](https://github.com/marinerhemant/MIDAS/blob/master/packages/midas_pipeline/dev/perf-notes.md) tracking.
|
|
8
|
+
|
|
9
|
+
Stages call in-process Python kernels via `midas-index` / `midas-fit-grain` / `midas-transforms` / `midas-stress`. FF mode shells out to `python -m midas_index` and `python -m midas_fit_grain` (same kernels, subprocess for the FF parity-preserving pattern). No CUDA C; GPU is torch-only.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e packages/midas_pipeline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
midas-pipeline run --scan-mode {ff,pf,auto} --params Parameters.txt --result rundir/
|
|
21
|
+
midas-pipeline status rundir/
|
|
22
|
+
midas-pipeline resume rundir/ --from <stage>
|
|
23
|
+
midas-pipeline reprocess rundir/
|
|
24
|
+
midas-pipeline inspect rundir/LayerNr_1/
|
|
25
|
+
midas-pipeline simulate --out simdir/ --params Parameters.txt
|
|
26
|
+
midas-pipeline seed --params ... --output UniqueOrientations.csv
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When `--scan-mode` is omitted (default `auto`), the CLI sniffs the parameter file: `nScans > 1` or presence of `BeamSize` / scanning keys → `pf`, otherwise `ff`. For PF mode, `--n-scans`, `--scan-step`, `--beam-size`, and `--scan-pos-tol` default to values in the params file (CLI flags override).
|
|
30
|
+
|
|
31
|
+
## Coexistence with `midas-ff-pipeline`
|
|
32
|
+
|
|
33
|
+
The legacy `midas-ff-pipeline` console-script is preserved as an independent FF orchestrator (its own kernels, its own CLI). It is **not** deprecated by `midas-pipeline run --scan-mode ff` — both paths invoke the same `midas-index` / `midas-fit-grain` kernels under the hood, and both stay green on the FF parity gate. Pick whichever is more convenient for your workflow.
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
See [`../../.claude/plans/for-pf-we-don-t-lovely-locket.md`](../../.claude/plans/for-pf-we-don-t-lovely-locket.md) for the long-form plan. Quick summary:
|
|
38
|
+
|
|
39
|
+
- **One orchestrator** with a mode-dependent `STAGE_ORDER`.
|
|
40
|
+
- **Shared kernel packages** (`midas-index`, `midas-fit-grain`, `midas-transforms`, etc.) extended in place; FF behavior preserved by parity gates.
|
|
41
|
+
- **PF-only modules** live inside `midas_pipeline` (`find_grains/`, `sinogen`, `recon/`, `fuse`, `potts`, `em_refine`, `seeding/`).
|
|
42
|
+
- **Differentiability + multi-device** mandatory on every new compute path (CPU / CUDA / MPS via torch).
|
|
43
|
+
|
|
44
|
+
## Constraints
|
|
45
|
+
|
|
46
|
+
- No CUDA C; GPU support is torch-only.
|
|
47
|
+
- No deletions of legacy code in this effort.
|
|
48
|
+
- `midas-process-grains` is FF-only; PF consolidation is fresh pure-Python.
|
|
49
|
+
- `utils/calcMiso.py` is not imported by this package; all orientation math comes from `midas-stress`.
|
|
50
|
+
- `TOMO/midas_tomo_python.py` is imported in place, not relocated.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""midas-pipeline: Unified MIDAS HEDM orchestrator (FF + PF, single source).
|
|
2
|
+
|
|
3
|
+
FF is the single-scan degeneracy of PF: ``ScanGeometry.ff()`` produces
|
|
4
|
+
``scan_mode='ff'`` with ``n_scans=1``; everything else is a regular PF
|
|
5
|
+
run with ``n_scans > 1``. One orchestrator, one CLI, two scan modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
from .config import (
|
|
13
|
+
AlignMethod,
|
|
14
|
+
Device,
|
|
15
|
+
Dtype,
|
|
16
|
+
EMConfig,
|
|
17
|
+
FusionConfig,
|
|
18
|
+
LayerSelection,
|
|
19
|
+
MachineConfig,
|
|
20
|
+
PipelineConfig,
|
|
21
|
+
ProcessGrainsMode,
|
|
22
|
+
ReconConfig,
|
|
23
|
+
ReconMethod,
|
|
24
|
+
RefineLoss,
|
|
25
|
+
RefineMode,
|
|
26
|
+
RefinePositionMode,
|
|
27
|
+
RefineSolver,
|
|
28
|
+
RefinementConfig,
|
|
29
|
+
ResumeMode,
|
|
30
|
+
ScanGeometry,
|
|
31
|
+
ScanMode,
|
|
32
|
+
SeedingConfig,
|
|
33
|
+
SeedingMode,
|
|
34
|
+
SinoSource,
|
|
35
|
+
SinoType,
|
|
36
|
+
sniff_scan_mode_from_paramfile,
|
|
37
|
+
)
|
|
38
|
+
from .pipeline import Pipeline, all_stage_names, stage_order_for
|
|
39
|
+
from .results import (
|
|
40
|
+
LayerResult,
|
|
41
|
+
StageResult,
|
|
42
|
+
BinningResult,
|
|
43
|
+
CalcRadiusResult,
|
|
44
|
+
ConsolidationResult,
|
|
45
|
+
CrossDetMergeResult,
|
|
46
|
+
EMRefineResult,
|
|
47
|
+
FindGrainsResult,
|
|
48
|
+
FuseResult,
|
|
49
|
+
HKLResult,
|
|
50
|
+
IndexResult,
|
|
51
|
+
MergeOverlapsResult,
|
|
52
|
+
MergeScansResult,
|
|
53
|
+
PeakFitResult,
|
|
54
|
+
PottsResult,
|
|
55
|
+
ProcessGrainsResult,
|
|
56
|
+
ReconResult,
|
|
57
|
+
RefineResult,
|
|
58
|
+
SinogenResult,
|
|
59
|
+
TransformsResult,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"__version__",
|
|
64
|
+
# config
|
|
65
|
+
"AlignMethod", "Device", "Dtype",
|
|
66
|
+
"EMConfig", "FusionConfig", "LayerSelection", "MachineConfig",
|
|
67
|
+
"PipelineConfig", "ProcessGrainsMode", "ReconConfig", "ReconMethod",
|
|
68
|
+
"RefineLoss", "RefineMode", "RefinePositionMode", "RefineSolver",
|
|
69
|
+
"RefinementConfig", "ResumeMode", "ScanGeometry", "ScanMode",
|
|
70
|
+
"SeedingConfig", "SeedingMode", "SinoSource", "SinoType",
|
|
71
|
+
"sniff_scan_mode_from_paramfile",
|
|
72
|
+
# pipeline
|
|
73
|
+
"Pipeline", "all_stage_names", "stage_order_for",
|
|
74
|
+
# results
|
|
75
|
+
"LayerResult", "StageResult",
|
|
76
|
+
"BinningResult", "CalcRadiusResult", "ConsolidationResult",
|
|
77
|
+
"CrossDetMergeResult", "EMRefineResult", "FindGrainsResult",
|
|
78
|
+
"FuseResult", "HKLResult", "IndexResult", "MergeOverlapsResult",
|
|
79
|
+
"MergeScansResult", "PeakFitResult", "PottsResult",
|
|
80
|
+
"ProcessGrainsResult", "ReconResult", "RefineResult",
|
|
81
|
+
"SinogenResult", "TransformsResult",
|
|
82
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Pipeline-wide logger + stage timer.
|
|
2
|
+
|
|
3
|
+
Single logger named ``midas_pipeline`` to keep child stage logs
|
|
4
|
+
co-located with the orchestrator's own.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Iterator
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger("midas_pipeline")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def configure_logging(level: int = logging.INFO) -> None:
|
|
18
|
+
"""Idempotent root-logger setup. Safe to call from CLI or notebook."""
|
|
19
|
+
root = logging.getLogger("midas_pipeline")
|
|
20
|
+
if root.handlers:
|
|
21
|
+
root.setLevel(level)
|
|
22
|
+
return
|
|
23
|
+
handler = logging.StreamHandler()
|
|
24
|
+
handler.setFormatter(logging.Formatter(
|
|
25
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
26
|
+
datefmt="%H:%M:%S",
|
|
27
|
+
))
|
|
28
|
+
root.addHandler(handler)
|
|
29
|
+
root.setLevel(level)
|
|
30
|
+
root.propagate = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def stage_timer(stage_name: str) -> Iterator[dict]:
|
|
35
|
+
"""Time a stage and yield a dict that gets populated with start/end/duration."""
|
|
36
|
+
info: dict[str, float] = {"started_at": time.time()}
|
|
37
|
+
LOG.info("→ %s", stage_name)
|
|
38
|
+
try:
|
|
39
|
+
yield info
|
|
40
|
+
except Exception:
|
|
41
|
+
info["finished_at"] = time.time()
|
|
42
|
+
info["duration_s"] = info["finished_at"] - info["started_at"]
|
|
43
|
+
LOG.exception("✗ %s failed after %.2fs", stage_name, info["duration_s"])
|
|
44
|
+
raise
|
|
45
|
+
info["finished_at"] = time.time()
|
|
46
|
+
info["duration_s"] = info["finished_at"] - info["started_at"]
|
|
47
|
+
LOG.info("✓ %s — %.2fs", stage_name, info["duration_s"])
|