svc-processing 0.1.2__tar.gz → 0.1.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.
- {svc_processing-0.1.2 → svc_processing-0.1.4}/PKG-INFO +30 -11
- {svc_processing-0.1.2 → svc_processing-0.1.4}/README.md +29 -10
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/cli.py +33 -4
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/run_config.py +19 -1
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/runner.py +58 -5
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pyproject.toml +1 -1
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/PKG-INFO +30 -11
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_cli.py +69 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_run_config.py +55 -0
- svc_processing-0.1.4/tests/test_runner.py +136 -0
- svc_processing-0.1.2/tests/test_runner.py +0 -53
- {svc_processing-0.1.2 → svc_processing-0.1.4}/LICENSE +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/__init__.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/processor.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/resampler.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/pipeline/sig_processor.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/setup.cfg +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/SOURCES.txt +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/dependency_links.txt +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/entry_points.txt +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/requires.txt +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/svc_processing.egg-info/top_level.txt +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_pipeline_demo.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_processor.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_resampler_parity.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_resampler_public_api.py +0 -0
- {svc_processing-0.1.2 → svc_processing-0.1.4}/tests/test_sig_processor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: svc-processing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pure-Python SVC HR-1024i hyperspectral .sig processing pipeline
|
|
5
5
|
Author: Gold Lab (GrapeSPEC project), Cornell University
|
|
6
6
|
Author-email: Cole Regnier <nr466@cornell.edu>
|
|
@@ -70,21 +70,35 @@ Generated outputs (gitignored): `pipeline_outputs/sig_processed/<run>/`, `pipeli
|
|
|
70
70
|
```bash
|
|
71
71
|
python3.11 -m venv .venv
|
|
72
72
|
source .venv/bin/activate
|
|
73
|
-
python -m pip install svc-processing
|
|
73
|
+
python -m pip install svc-processing # package name
|
|
74
74
|
|
|
75
|
-
svc-pipeline --init-config #
|
|
75
|
+
svc-pipeline --init-config # command name — different from the package name above
|
|
76
76
|
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
77
77
|
svc-pipeline
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
**Working from a clone of this repo** (config/config.json already exists)
|
|
80
|
+
**Working from a clone of this repo** (config/config.json already exists) — run the
|
|
81
|
+
setup script once, then activate the environment it created:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
./setup.sh # creates .venv, installs everything, stages the demo data
|
|
85
|
+
# ./setup.sh --dev # add the test & lint tools for development
|
|
86
|
+
|
|
87
|
+
source .venv/bin/activate # macOS / Linux
|
|
88
|
+
# .venv\Scripts\activate # Windows PowerShell
|
|
89
|
+
|
|
90
|
+
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
91
|
+
svc-pipeline config.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The script always installs into *this* repo's `.venv` no matter which folder you run
|
|
95
|
+
it from, so it avoids the "Directory '.' is not installable" and "No module named …"
|
|
96
|
+
errors that come from being in the wrong directory. Prefer to do it by hand?
|
|
81
97
|
|
|
82
98
|
```bash
|
|
83
99
|
python3.11 -m venv .venv
|
|
84
100
|
source .venv/bin/activate
|
|
85
101
|
python -m pip install -e ".[dev,demo]"
|
|
86
|
-
|
|
87
|
-
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
88
102
|
svc-pipeline config.json
|
|
89
103
|
```
|
|
90
104
|
|
|
@@ -126,8 +140,10 @@ Outputs land under `pipeline_outputs/` by default. See [`config/README.md`](conf
|
|
|
126
140
|
└────────────────────────────────────┘
|
|
127
141
|
```
|
|
128
142
|
|
|
129
|
-
The `svc-pipeline` console script
|
|
130
|
-
|
|
143
|
+
The `svc-pipeline` console script runs Stages 1 and 2 through `pipeline.cli`,
|
|
144
|
+
and Stage 3 too once a `groups_csv` is configured (or passed via
|
|
145
|
+
`--groups-csv`) — see [`config/README.md`](config/README.md#grouping--stage-3-optional).
|
|
146
|
+
Stage 3 is also available directly from notebooks against the Stage 2 output.
|
|
131
147
|
The pipeline is **single-pass and idempotent per input directory** — previous
|
|
132
148
|
processed `.sig` files in the target directory are deleted at the start of each
|
|
133
149
|
run.
|
|
@@ -146,7 +162,9 @@ svc-pipeline [config] [options]
|
|
|
146
162
|
|---|---|
|
|
147
163
|
| `config` | Run-config JSON (positional, optional; default `config/config.json`). Bare names resolve under `config/`, so `config.json`, `config`, and `config/config.json` are equivalent. See [`config/README.md`](config/README.md) for schema. |
|
|
148
164
|
| `--input-dir <path>` | Override `sig_input_dir` and process only this directory. |
|
|
149
|
-
| `--step {1,2,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1
|
|
165
|
+
| `--step {1,2,3,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1); `3` = group + average only (requires Stage 2 and `groups_csv`); `all` = every stage that's configured (default). |
|
|
166
|
+
| `--groups-csv <path>` | Override the config's `groups_csv` to group repeat scans and average them. |
|
|
167
|
+
| `--group-method {mean,median,sum,min,max}` | Override the config's `group_agg_method` for Stage 3 (default `mean`). |
|
|
150
168
|
| `--verbose` | Print INFO/DEBUG logs before and after each stage. |
|
|
151
169
|
|
|
152
170
|
### Expected output layout
|
|
@@ -157,10 +175,11 @@ pipeline_outputs/
|
|
|
157
175
|
│ ├── <truncated *.sig files>
|
|
158
176
|
│ └── <input_dir_name>_processed_sig_summary.csv
|
|
159
177
|
└── sig_resampled/<input_dir_name>/
|
|
160
|
-
|
|
178
|
+
├── <input_dir_name>_merged_spectra.csv # 2101 columns (400–2500 nm)
|
|
179
|
+
└── <input_dir_name>_grouped_spectra.csv # only if groups_csv is configured
|
|
161
180
|
```
|
|
162
181
|
|
|
163
|
-
`summary_csv_name` and `
|
|
182
|
+
`summary_csv_name`, `merged_csv_name`, and `grouped_csv_name` in the run config are suffixes; the real filenames are prefixed with the input directory name. Verify your config in [`config/README.md`](config/README.md).
|
|
164
183
|
|
|
165
184
|
---
|
|
166
185
|
|
|
@@ -37,21 +37,35 @@ Generated outputs (gitignored): `pipeline_outputs/sig_processed/<run>/`, `pipeli
|
|
|
37
37
|
```bash
|
|
38
38
|
python3.11 -m venv .venv
|
|
39
39
|
source .venv/bin/activate
|
|
40
|
-
python -m pip install svc-processing
|
|
40
|
+
python -m pip install svc-processing # package name
|
|
41
41
|
|
|
42
|
-
svc-pipeline --init-config #
|
|
42
|
+
svc-pipeline --init-config # command name — different from the package name above
|
|
43
43
|
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
44
44
|
svc-pipeline
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
**Working from a clone of this repo** (config/config.json already exists)
|
|
47
|
+
**Working from a clone of this repo** (config/config.json already exists) — run the
|
|
48
|
+
setup script once, then activate the environment it created:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./setup.sh # creates .venv, installs everything, stages the demo data
|
|
52
|
+
# ./setup.sh --dev # add the test & lint tools for development
|
|
53
|
+
|
|
54
|
+
source .venv/bin/activate # macOS / Linux
|
|
55
|
+
# .venv\Scripts\activate # Windows PowerShell
|
|
56
|
+
|
|
57
|
+
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
58
|
+
svc-pipeline config.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The script always installs into *this* repo's `.venv` no matter which folder you run
|
|
62
|
+
it from, so it avoids the "Directory '.' is not installable" and "No module named …"
|
|
63
|
+
errors that come from being in the wrong directory. Prefer to do it by hand?
|
|
48
64
|
|
|
49
65
|
```bash
|
|
50
66
|
python3.11 -m venv .venv
|
|
51
67
|
source .venv/bin/activate
|
|
52
68
|
python -m pip install -e ".[dev,demo]"
|
|
53
|
-
|
|
54
|
-
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
55
69
|
svc-pipeline config.json
|
|
56
70
|
```
|
|
57
71
|
|
|
@@ -93,8 +107,10 @@ Outputs land under `pipeline_outputs/` by default. See [`config/README.md`](conf
|
|
|
93
107
|
└────────────────────────────────────┘
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
The `svc-pipeline` console script
|
|
97
|
-
|
|
110
|
+
The `svc-pipeline` console script runs Stages 1 and 2 through `pipeline.cli`,
|
|
111
|
+
and Stage 3 too once a `groups_csv` is configured (or passed via
|
|
112
|
+
`--groups-csv`) — see [`config/README.md`](config/README.md#grouping--stage-3-optional).
|
|
113
|
+
Stage 3 is also available directly from notebooks against the Stage 2 output.
|
|
98
114
|
The pipeline is **single-pass and idempotent per input directory** — previous
|
|
99
115
|
processed `.sig` files in the target directory are deleted at the start of each
|
|
100
116
|
run.
|
|
@@ -113,7 +129,9 @@ svc-pipeline [config] [options]
|
|
|
113
129
|
|---|---|
|
|
114
130
|
| `config` | Run-config JSON (positional, optional; default `config/config.json`). Bare names resolve under `config/`, so `config.json`, `config`, and `config/config.json` are equivalent. See [`config/README.md`](config/README.md) for schema. |
|
|
115
131
|
| `--input-dir <path>` | Override `sig_input_dir` and process only this directory. |
|
|
116
|
-
| `--step {1,2,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1
|
|
132
|
+
| `--step {1,2,3,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1); `3` = group + average only (requires Stage 2 and `groups_csv`); `all` = every stage that's configured (default). |
|
|
133
|
+
| `--groups-csv <path>` | Override the config's `groups_csv` to group repeat scans and average them. |
|
|
134
|
+
| `--group-method {mean,median,sum,min,max}` | Override the config's `group_agg_method` for Stage 3 (default `mean`). |
|
|
117
135
|
| `--verbose` | Print INFO/DEBUG logs before and after each stage. |
|
|
118
136
|
|
|
119
137
|
### Expected output layout
|
|
@@ -124,10 +142,11 @@ pipeline_outputs/
|
|
|
124
142
|
│ ├── <truncated *.sig files>
|
|
125
143
|
│ └── <input_dir_name>_processed_sig_summary.csv
|
|
126
144
|
└── sig_resampled/<input_dir_name>/
|
|
127
|
-
|
|
145
|
+
├── <input_dir_name>_merged_spectra.csv # 2101 columns (400–2500 nm)
|
|
146
|
+
└── <input_dir_name>_grouped_spectra.csv # only if groups_csv is configured
|
|
128
147
|
```
|
|
129
148
|
|
|
130
|
-
`summary_csv_name` and `
|
|
149
|
+
`summary_csv_name`, `merged_csv_name`, and `grouped_csv_name` in the run config are suffixes; the real filenames are prefixed with the input directory name. Verify your config in [`config/README.md`](config/README.md).
|
|
131
150
|
|
|
132
151
|
---
|
|
133
152
|
|
|
@@ -28,7 +28,13 @@ def _configure_logging(verbose: bool) -> logging.Logger:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _parse_args() -> argparse.Namespace:
|
|
31
|
-
parser = argparse.ArgumentParser(
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
description="Run the SIG processing pipeline",
|
|
33
|
+
epilog=(
|
|
34
|
+
"Full README, TUTORIAL, and config reference: "
|
|
35
|
+
"https://github.com/regs08/SVCProcessingPipeline"
|
|
36
|
+
),
|
|
37
|
+
)
|
|
32
38
|
parser.add_argument(
|
|
33
39
|
"config",
|
|
34
40
|
nargs="?",
|
|
@@ -41,9 +47,13 @@ def _parse_args() -> argparse.Namespace:
|
|
|
41
47
|
)
|
|
42
48
|
parser.add_argument(
|
|
43
49
|
"--step",
|
|
44
|
-
choices=["1", "2", "all"],
|
|
50
|
+
choices=["1", "2", "3", "all"],
|
|
45
51
|
default="all",
|
|
46
|
-
help=
|
|
52
|
+
help=(
|
|
53
|
+
"Which step to run: 1=process+summary CSV, 2=Python resampling only, "
|
|
54
|
+
"3=group+average only (requires groups_csv; step 2 must have already run), "
|
|
55
|
+
"all=every step that's configured (step 3 only if groups_csv is set)."
|
|
56
|
+
),
|
|
47
57
|
)
|
|
48
58
|
parser.add_argument(
|
|
49
59
|
"--verbose",
|
|
@@ -55,6 +65,18 @@ def _parse_args() -> argparse.Namespace:
|
|
|
55
65
|
action="store_true",
|
|
56
66
|
help="Write a starter config/config.json in the current directory and exit.",
|
|
57
67
|
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--groups-csv",
|
|
70
|
+
help=(
|
|
71
|
+
"Override the config's groups_csv: group repeat scans of the same sample "
|
|
72
|
+
"and average them (a CSV with scans/scan_id + name columns)."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--group-method",
|
|
77
|
+
choices=["mean", "median", "sum", "min", "max"],
|
|
78
|
+
help="Override the config's group_agg_method for step 3 (default: mean).",
|
|
79
|
+
)
|
|
58
80
|
return parser.parse_args()
|
|
59
81
|
|
|
60
82
|
|
|
@@ -85,9 +107,16 @@ def main() -> None:
|
|
|
85
107
|
# warning for any non-default value before the per-directory work begins.
|
|
86
108
|
run_config.processing_params()
|
|
87
109
|
|
|
110
|
+
groups_csv_override = Path(args.groups_csv).expanduser() if args.groups_csv else None
|
|
111
|
+
|
|
88
112
|
overall_results: list[tuple[Path, dict[str, Path | None]]] = []
|
|
89
113
|
for input_dir in input_dirs:
|
|
90
|
-
settings = run_config.settings_for(
|
|
114
|
+
settings = run_config.settings_for(
|
|
115
|
+
input_dir,
|
|
116
|
+
verbose=args.verbose,
|
|
117
|
+
groups_csv_override=groups_csv_override,
|
|
118
|
+
group_method_override=args.group_method,
|
|
119
|
+
)
|
|
91
120
|
results = Pipeline(settings, logger).run(args.step)
|
|
92
121
|
overall_results.append((input_dir, results))
|
|
93
122
|
|
|
@@ -76,6 +76,9 @@ class PipelineSettings:
|
|
|
76
76
|
processing_params: dict
|
|
77
77
|
correction_types: dict[str, str]
|
|
78
78
|
instrument_numbers: dict[str, str]
|
|
79
|
+
groups_csv: Path | None
|
|
80
|
+
group_agg_method: str
|
|
81
|
+
grouped_csv_name: str
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
class RunConfig:
|
|
@@ -299,7 +302,14 @@ class RunConfig:
|
|
|
299
302
|
self._logger.info("Instrument serials from config: %s", serials)
|
|
300
303
|
return correction_types, instrument_numbers
|
|
301
304
|
|
|
302
|
-
def settings_for(
|
|
305
|
+
def settings_for(
|
|
306
|
+
self,
|
|
307
|
+
input_dir: Path,
|
|
308
|
+
*,
|
|
309
|
+
verbose: bool,
|
|
310
|
+
groups_csv_override: str | Path | None = None,
|
|
311
|
+
group_method_override: str | None = None,
|
|
312
|
+
) -> PipelineSettings:
|
|
303
313
|
"""Build the fully-resolved :class:`PipelineSettings` for one input directory."""
|
|
304
314
|
input_dir = Path(input_dir).expanduser()
|
|
305
315
|
source_name = input_dir.name or "sig_input"
|
|
@@ -325,6 +335,11 @@ class RunConfig:
|
|
|
325
335
|
if key is not None
|
|
326
336
|
}
|
|
327
337
|
|
|
338
|
+
groups_csv_value = groups_csv_override or self.data.get("groups_csv")
|
|
339
|
+
groups_csv = _resolve_under(self.base_dir, str(groups_csv_value)) if groups_csv_value else None
|
|
340
|
+
group_agg_method = str(group_method_override or self.data.get("group_agg_method", "mean"))
|
|
341
|
+
grouped_csv_name = f"{source_name}_{self.data.get('grouped_csv_name', 'grouped_spectra.csv')}"
|
|
342
|
+
|
|
328
343
|
return PipelineSettings(
|
|
329
344
|
source_name=source_name,
|
|
330
345
|
input_dir=input_dir,
|
|
@@ -337,4 +352,7 @@ class RunConfig:
|
|
|
337
352
|
processing_params=self.processing_params(),
|
|
338
353
|
correction_types=correction_types,
|
|
339
354
|
instrument_numbers=instrument_numbers,
|
|
355
|
+
groups_csv=groups_csv,
|
|
356
|
+
group_agg_method=group_agg_method,
|
|
357
|
+
grouped_csv_name=grouped_csv_name,
|
|
340
358
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Execution of the
|
|
1
|
+
"""Execution of the three SIG processing stages.
|
|
2
2
|
|
|
3
3
|
:class:`Pipeline` runs, for a single resolved
|
|
4
4
|
:class:`~pipeline.run_config.PipelineSettings`:
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
the calibration end-line and write a per-run summary CSV.
|
|
8
8
|
* Stage 2 (:meth:`Pipeline.resample`) — run the pure-Python resampler over the
|
|
9
9
|
processed files.
|
|
10
|
+
* Stage 3 (:meth:`Pipeline.group_and_average`) — optional; only runs when the
|
|
11
|
+
config supplies a ``groups_csv``. Groups repeat scans of the same sample and
|
|
12
|
+
averages them into one spectrum per group.
|
|
10
13
|
|
|
11
14
|
Config loading that produces the ``PipelineSettings`` lives in
|
|
12
15
|
``pipeline/run_config.py``; the CLI that wires them together lives in
|
|
@@ -20,6 +23,7 @@ import logging
|
|
|
20
23
|
from pathlib import Path
|
|
21
24
|
from typing import Iterable
|
|
22
25
|
|
|
26
|
+
from .processor import GroupSpec, SigSpectraAverager
|
|
23
27
|
from .resampler import resample_spectra
|
|
24
28
|
from .run_config import PipelineSettings
|
|
25
29
|
from .sig_processor import SigFileProcessor
|
|
@@ -38,12 +42,14 @@ class Pipeline:
|
|
|
38
42
|
"""Run the requested step(s) and report the produced artifacts.
|
|
39
43
|
|
|
40
44
|
``step``: ``"1"`` = Stage 1 only, ``"2"`` = Stage 2 only (Stage 1 must
|
|
41
|
-
have run previously), ``"
|
|
45
|
+
have run previously), ``"3"`` = Stage 3 only (Stage 2 must have run
|
|
46
|
+
previously; requires ``groups_csv`` to be configured), ``"all"`` = every
|
|
47
|
+
stage that's configured to run (Stage 3 only if ``groups_csv`` is set).
|
|
42
48
|
"""
|
|
43
|
-
summary_csv: Path | None
|
|
49
|
+
summary_csv: Path | None = None
|
|
44
50
|
if step in {"1", "all"}:
|
|
45
51
|
summary_csv = self.process_sig_files()
|
|
46
|
-
|
|
52
|
+
elif step == "2":
|
|
47
53
|
summary_csv = self.settings.summary_csv if self.settings.summary_csv.exists() else None
|
|
48
54
|
if summary_csv is None:
|
|
49
55
|
self.logger.error(
|
|
@@ -54,8 +60,17 @@ class Pipeline:
|
|
|
54
60
|
merged_csv: Path | None = None
|
|
55
61
|
if step in {"2", "all"}:
|
|
56
62
|
merged_csv = self.resample(summary_csv)
|
|
63
|
+
elif step == "3":
|
|
64
|
+
merged_path = self.settings.resampled_dir / self.settings.merged_csv_name
|
|
65
|
+
merged_csv = merged_path if merged_path.exists() else None
|
|
66
|
+
if merged_csv is None:
|
|
67
|
+
self.logger.error("Merged CSV not found at %s (run --step 2 first).", merged_path.resolve())
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
grouped_csv: Path | None = None
|
|
70
|
+
if step in {"3", "all"} and self.settings.groups_csv is not None:
|
|
71
|
+
grouped_csv = self.group_and_average(merged_csv)
|
|
72
|
+
|
|
73
|
+
return {"summary_csv": summary_csv, "merged_csv": merged_csv, "grouped_csv": grouped_csv}
|
|
59
74
|
|
|
60
75
|
# ── Stage 1: process raw .sig files ──────────────────────────────────────
|
|
61
76
|
|
|
@@ -194,6 +209,44 @@ class Pipeline:
|
|
|
194
209
|
logger.warning("Merged spectra file was not created at %s", merged_path)
|
|
195
210
|
return None
|
|
196
211
|
|
|
212
|
+
# ── Stage 3: group & average (optional) ──────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def group_and_average(self, merged_csv: Path | None) -> Path | None:
|
|
215
|
+
"""Average repeat scans of the same sample into one spectrum per group.
|
|
216
|
+
|
|
217
|
+
Requires ``settings.groups_csv`` (checked by the caller) and a merged
|
|
218
|
+
CSV from Stage 2. Groups are read via :meth:`GroupSpec.from_csv`
|
|
219
|
+
(``scans``/``scan_id`` + ``name`` columns) and applied with
|
|
220
|
+
:class:`SigSpectraAverager`.
|
|
221
|
+
"""
|
|
222
|
+
settings = self.settings
|
|
223
|
+
logger = self.logger
|
|
224
|
+
if merged_csv is None:
|
|
225
|
+
logger.error("Skipping grouping because no merged CSV is available.")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
groups_csv = settings.groups_csv
|
|
229
|
+
if groups_csv is None or not groups_csv.exists():
|
|
230
|
+
logger.error("Groups CSV not found: %s", groups_csv.resolve() if groups_csv else groups_csv)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
groups = GroupSpec.from_csv(groups_csv)
|
|
234
|
+
if not groups:
|
|
235
|
+
logger.warning("No groups found in %s; nothing to average.", groups_csv.resolve())
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
if settings.verbose:
|
|
239
|
+
logger.info("Grouping %s using %s (%s)", merged_csv, groups_csv.resolve(), settings.group_agg_method)
|
|
240
|
+
|
|
241
|
+
averager = SigSpectraAverager.from_csv(merged_csv)
|
|
242
|
+
grouped_df = averager.aggregate(groups, method=settings.group_agg_method)
|
|
243
|
+
|
|
244
|
+
output_path = settings.resampled_dir / settings.grouped_csv_name
|
|
245
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
grouped_df.to_csv(output_path, index=False)
|
|
247
|
+
logger.info("Grouped/averaged spectra written to %s", output_path.resolve())
|
|
248
|
+
return output_path
|
|
249
|
+
|
|
197
250
|
# ── helpers ──────────────────────────────────────────────────────────────
|
|
198
251
|
|
|
199
252
|
@staticmethod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: svc-processing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pure-Python SVC HR-1024i hyperspectral .sig processing pipeline
|
|
5
5
|
Author: Gold Lab (GrapeSPEC project), Cornell University
|
|
6
6
|
Author-email: Cole Regnier <nr466@cornell.edu>
|
|
@@ -70,21 +70,35 @@ Generated outputs (gitignored): `pipeline_outputs/sig_processed/<run>/`, `pipeli
|
|
|
70
70
|
```bash
|
|
71
71
|
python3.11 -m venv .venv
|
|
72
72
|
source .venv/bin/activate
|
|
73
|
-
python -m pip install svc-processing
|
|
73
|
+
python -m pip install svc-processing # package name
|
|
74
74
|
|
|
75
|
-
svc-pipeline --init-config #
|
|
75
|
+
svc-pipeline --init-config # command name — different from the package name above
|
|
76
76
|
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
77
77
|
svc-pipeline
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
**Working from a clone of this repo** (config/config.json already exists)
|
|
80
|
+
**Working from a clone of this repo** (config/config.json already exists) — run the
|
|
81
|
+
setup script once, then activate the environment it created:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
./setup.sh # creates .venv, installs everything, stages the demo data
|
|
85
|
+
# ./setup.sh --dev # add the test & lint tools for development
|
|
86
|
+
|
|
87
|
+
source .venv/bin/activate # macOS / Linux
|
|
88
|
+
# .venv\Scripts\activate # Windows PowerShell
|
|
89
|
+
|
|
90
|
+
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
91
|
+
svc-pipeline config.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The script always installs into *this* repo's `.venv` no matter which folder you run
|
|
95
|
+
it from, so it avoids the "Directory '.' is not installable" and "No module named …"
|
|
96
|
+
errors that come from being in the wrong directory. Prefer to do it by hand?
|
|
81
97
|
|
|
82
98
|
```bash
|
|
83
99
|
python3.11 -m venv .venv
|
|
84
100
|
source .venv/bin/activate
|
|
85
101
|
python -m pip install -e ".[dev,demo]"
|
|
86
|
-
|
|
87
|
-
# Edit config/config.json — replace "<PATH_TO_SIG_INPUT_ROOT>" with your data path.
|
|
88
102
|
svc-pipeline config.json
|
|
89
103
|
```
|
|
90
104
|
|
|
@@ -126,8 +140,10 @@ Outputs land under `pipeline_outputs/` by default. See [`config/README.md`](conf
|
|
|
126
140
|
└────────────────────────────────────┘
|
|
127
141
|
```
|
|
128
142
|
|
|
129
|
-
The `svc-pipeline` console script
|
|
130
|
-
|
|
143
|
+
The `svc-pipeline` console script runs Stages 1 and 2 through `pipeline.cli`,
|
|
144
|
+
and Stage 3 too once a `groups_csv` is configured (or passed via
|
|
145
|
+
`--groups-csv`) — see [`config/README.md`](config/README.md#grouping--stage-3-optional).
|
|
146
|
+
Stage 3 is also available directly from notebooks against the Stage 2 output.
|
|
131
147
|
The pipeline is **single-pass and idempotent per input directory** — previous
|
|
132
148
|
processed `.sig` files in the target directory are deleted at the start of each
|
|
133
149
|
run.
|
|
@@ -146,7 +162,9 @@ svc-pipeline [config] [options]
|
|
|
146
162
|
|---|---|
|
|
147
163
|
| `config` | Run-config JSON (positional, optional; default `config/config.json`). Bare names resolve under `config/`, so `config.json`, `config`, and `config/config.json` are equivalent. See [`config/README.md`](config/README.md) for schema. |
|
|
148
164
|
| `--input-dir <path>` | Override `sig_input_dir` and process only this directory. |
|
|
149
|
-
| `--step {1,2,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1
|
|
165
|
+
| `--step {1,2,3,all}` | `1` = process + summary CSV only; `2` = resample only (requires Stage 1); `3` = group + average only (requires Stage 2 and `groups_csv`); `all` = every stage that's configured (default). |
|
|
166
|
+
| `--groups-csv <path>` | Override the config's `groups_csv` to group repeat scans and average them. |
|
|
167
|
+
| `--group-method {mean,median,sum,min,max}` | Override the config's `group_agg_method` for Stage 3 (default `mean`). |
|
|
150
168
|
| `--verbose` | Print INFO/DEBUG logs before and after each stage. |
|
|
151
169
|
|
|
152
170
|
### Expected output layout
|
|
@@ -157,10 +175,11 @@ pipeline_outputs/
|
|
|
157
175
|
│ ├── <truncated *.sig files>
|
|
158
176
|
│ └── <input_dir_name>_processed_sig_summary.csv
|
|
159
177
|
└── sig_resampled/<input_dir_name>/
|
|
160
|
-
|
|
178
|
+
├── <input_dir_name>_merged_spectra.csv # 2101 columns (400–2500 nm)
|
|
179
|
+
└── <input_dir_name>_grouped_spectra.csv # only if groups_csv is configured
|
|
161
180
|
```
|
|
162
181
|
|
|
163
|
-
`summary_csv_name` and `
|
|
182
|
+
`summary_csv_name`, `merged_csv_name`, and `grouped_csv_name` in the run config are suffixes; the real filenames are prefixed with the input directory name. Verify your config in [`config/README.md`](config/README.md).
|
|
164
183
|
|
|
165
184
|
---
|
|
166
185
|
|
|
@@ -14,6 +14,18 @@ def _write_config(path: Path, data: dict) -> Path:
|
|
|
14
14
|
return path
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def test_help_links_to_github_repo(
|
|
18
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
19
|
+
capsys: pytest.CaptureFixture[str],
|
|
20
|
+
) -> None:
|
|
21
|
+
monkeypatch.setattr(sys, "argv", ["svc-pipeline", "--help"])
|
|
22
|
+
|
|
23
|
+
with pytest.raises(SystemExit):
|
|
24
|
+
_parse_args()
|
|
25
|
+
|
|
26
|
+
assert "github.com/regs08/SVCProcessingPipeline" in capsys.readouterr().out
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
def test_parse_args_step_and_input_dir_default_to_all_and_none(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
18
30
|
monkeypatch.setattr(sys, "argv", ["svc-pipeline", "config.json"])
|
|
19
31
|
|
|
@@ -39,6 +51,20 @@ def test_parse_args_accepts_step_input_dir_and_verbose(monkeypatch: pytest.Monke
|
|
|
39
51
|
assert args.verbose is True
|
|
40
52
|
|
|
41
53
|
|
|
54
|
+
def test_parse_args_accepts_step_three_and_group_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
55
|
+
monkeypatch.setattr(
|
|
56
|
+
sys,
|
|
57
|
+
"argv",
|
|
58
|
+
["svc-pipeline", "--step", "3", "--groups-csv", "naming_ids/groups.csv", "--group-method", "median"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
args = _parse_args()
|
|
62
|
+
|
|
63
|
+
assert args.step == "3"
|
|
64
|
+
assert args.groups_csv == "naming_ids/groups.csv"
|
|
65
|
+
assert args.group_method == "median"
|
|
66
|
+
|
|
67
|
+
|
|
42
68
|
def test_main_input_dir_override_bypasses_config_input_and_placeholder(
|
|
43
69
|
tmp_path: Path,
|
|
44
70
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -73,6 +99,49 @@ def test_main_input_dir_override_bypasses_config_input_and_placeholder(
|
|
|
73
99
|
assert "not produced" in captured.out
|
|
74
100
|
|
|
75
101
|
|
|
102
|
+
def test_main_step_three_groups_existing_merged_csv(
|
|
103
|
+
tmp_path: Path,
|
|
104
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
105
|
+
capsys: pytest.CaptureFixture[str],
|
|
106
|
+
) -> None:
|
|
107
|
+
groups_csv = tmp_path / "groups.csv"
|
|
108
|
+
groups_csv.write_text("scan_id,name\n1,leaf_a\n2,leaf_a\n")
|
|
109
|
+
|
|
110
|
+
config_path = _write_config(
|
|
111
|
+
tmp_path / "config.json",
|
|
112
|
+
{
|
|
113
|
+
"sig_input_dir": "<PATH_TO_SIG_INPUT_ROOT>",
|
|
114
|
+
"processed_dir": "sig_processed",
|
|
115
|
+
"resampled_dir": "sig_resampled",
|
|
116
|
+
"output_dir": str(tmp_path / "out"),
|
|
117
|
+
"summary_csv_name": "summary.csv",
|
|
118
|
+
"merged_csv_name": "merged.csv",
|
|
119
|
+
"groups_csv": str(groups_csv),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
input_dir = tmp_path / "myinput"
|
|
123
|
+
input_dir.mkdir()
|
|
124
|
+
|
|
125
|
+
resampled_dir = tmp_path / "out" / "sig_resampled" / "myinput"
|
|
126
|
+
resampled_dir.mkdir(parents=True)
|
|
127
|
+
merged_csv = resampled_dir / "myinput_merged.csv"
|
|
128
|
+
merged_csv.write_text("sample_name,400\nleaf.0001,1.0\nleaf.0002,3.0\n")
|
|
129
|
+
|
|
130
|
+
monkeypatch.setattr(
|
|
131
|
+
sys,
|
|
132
|
+
"argv",
|
|
133
|
+
["svc-pipeline", str(config_path), "--input-dir", str(input_dir), "--step", "3"],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
main()
|
|
137
|
+
|
|
138
|
+
grouped_csv = resampled_dir / "myinput_grouped_spectra.csv"
|
|
139
|
+
assert grouped_csv.exists()
|
|
140
|
+
captured = capsys.readouterr()
|
|
141
|
+
assert "grouped_csv" in captured.out
|
|
142
|
+
assert str(grouped_csv.resolve()) in captured.out
|
|
143
|
+
|
|
144
|
+
|
|
76
145
|
def test_main_init_config_writes_starter_and_exits(
|
|
77
146
|
tmp_path: Path,
|
|
78
147
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -124,3 +124,58 @@ def test_settings_for_resolves_output_paths(tmp_path: Path) -> None:
|
|
|
124
124
|
assert settings.merged_csv_name == "example_input_merged_spectra.csv"
|
|
125
125
|
assert settings.correction_types["silver"] == "2517.9"
|
|
126
126
|
assert settings.instrument_numbers["silver"] == "1202103"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_settings_for_grouping_defaults_when_unconfigured(tmp_path: Path) -> None:
|
|
130
|
+
data = {
|
|
131
|
+
"processed_dir": "sig_processed",
|
|
132
|
+
"resampled_dir": "sig_resampled",
|
|
133
|
+
"summary_csv_name": "summary.csv",
|
|
134
|
+
"merged_csv_name": "merged.csv",
|
|
135
|
+
}
|
|
136
|
+
config = RunConfig(data, path=tmp_path / "config.json", base_dir=tmp_path, logger=_logger())
|
|
137
|
+
|
|
138
|
+
settings = config.settings_for(Path("example_input"), verbose=False)
|
|
139
|
+
|
|
140
|
+
assert settings.groups_csv is None
|
|
141
|
+
assert settings.group_agg_method == "mean"
|
|
142
|
+
assert settings.grouped_csv_name == "example_input_grouped_spectra.csv"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_settings_for_resolves_groups_csv_from_config(tmp_path: Path) -> None:
|
|
146
|
+
data = {
|
|
147
|
+
"processed_dir": "sig_processed",
|
|
148
|
+
"resampled_dir": "sig_resampled",
|
|
149
|
+
"summary_csv_name": "summary.csv",
|
|
150
|
+
"merged_csv_name": "merged.csv",
|
|
151
|
+
"groups_csv": "naming_ids/groups.csv",
|
|
152
|
+
"group_agg_method": "median",
|
|
153
|
+
}
|
|
154
|
+
config = RunConfig(data, path=tmp_path / "config.json", base_dir=tmp_path, logger=_logger())
|
|
155
|
+
|
|
156
|
+
settings = config.settings_for(Path("example_input"), verbose=False)
|
|
157
|
+
|
|
158
|
+
assert settings.groups_csv == tmp_path / "naming_ids/groups.csv"
|
|
159
|
+
assert settings.group_agg_method == "median"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_settings_for_cli_overrides_take_priority_over_config(tmp_path: Path) -> None:
|
|
163
|
+
data = {
|
|
164
|
+
"processed_dir": "sig_processed",
|
|
165
|
+
"resampled_dir": "sig_resampled",
|
|
166
|
+
"summary_csv_name": "summary.csv",
|
|
167
|
+
"merged_csv_name": "merged.csv",
|
|
168
|
+
"groups_csv": "naming_ids/groups.csv",
|
|
169
|
+
"group_agg_method": "mean",
|
|
170
|
+
}
|
|
171
|
+
config = RunConfig(data, path=tmp_path / "config.json", base_dir=tmp_path, logger=_logger())
|
|
172
|
+
|
|
173
|
+
settings = config.settings_for(
|
|
174
|
+
Path("example_input"),
|
|
175
|
+
verbose=False,
|
|
176
|
+
groups_csv_override=tmp_path / "override_groups.csv",
|
|
177
|
+
group_method_override="max",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert settings.groups_csv == tmp_path / "override_groups.csv"
|
|
181
|
+
assert settings.group_agg_method == "max"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pipeline.run_config import PipelineSettings
|
|
7
|
+
from pipeline.runner import Pipeline
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _settings(
|
|
11
|
+
tmp_path: Path,
|
|
12
|
+
input_dir: Path,
|
|
13
|
+
*,
|
|
14
|
+
groups_csv: Path | None = None,
|
|
15
|
+
group_agg_method: str = "mean",
|
|
16
|
+
) -> PipelineSettings:
|
|
17
|
+
return PipelineSettings(
|
|
18
|
+
source_name=input_dir.name or "input",
|
|
19
|
+
input_dir=input_dir,
|
|
20
|
+
processed_dir=tmp_path / "processed",
|
|
21
|
+
resampled_dir=tmp_path / "resampled",
|
|
22
|
+
summary_csv=tmp_path / "processed" / "summary.csv",
|
|
23
|
+
merged_csv_name="merged.csv",
|
|
24
|
+
end_line_overrides={},
|
|
25
|
+
verbose=False,
|
|
26
|
+
processing_params={
|
|
27
|
+
"band_min": 400,
|
|
28
|
+
"band_max": 410,
|
|
29
|
+
"resample_fwhm_nm": 10.0,
|
|
30
|
+
"splice_interp_wvl": (5.0, 2.0),
|
|
31
|
+
"fixed_sensor": 2,
|
|
32
|
+
},
|
|
33
|
+
correction_types={"bronze": "2520.4", "silver": "2517.9"},
|
|
34
|
+
instrument_numbers={"bronze": "2212118", "silver": "1202103"},
|
|
35
|
+
groups_csv=groups_csv,
|
|
36
|
+
group_agg_method=group_agg_method,
|
|
37
|
+
grouped_csv_name="grouped.csv",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_process_sig_files_returns_none_for_missing_input(tmp_path: Path) -> None:
|
|
42
|
+
pipeline = Pipeline(_settings(tmp_path, tmp_path / "missing"), logging.getLogger("tests.runner"))
|
|
43
|
+
|
|
44
|
+
assert pipeline.process_sig_files() is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_process_sig_files_returns_none_for_empty_input(tmp_path: Path) -> None:
|
|
48
|
+
input_dir = tmp_path / "empty"
|
|
49
|
+
input_dir.mkdir()
|
|
50
|
+
pipeline = Pipeline(_settings(tmp_path, input_dir), logging.getLogger("tests.runner"))
|
|
51
|
+
|
|
52
|
+
assert pipeline.process_sig_files() is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_run_step_two_without_summary_does_not_resample(tmp_path: Path) -> None:
|
|
56
|
+
input_dir = tmp_path / "input"
|
|
57
|
+
input_dir.mkdir()
|
|
58
|
+
pipeline = Pipeline(_settings(tmp_path, input_dir), logging.getLogger("tests.runner"))
|
|
59
|
+
|
|
60
|
+
result = pipeline.run("2")
|
|
61
|
+
|
|
62
|
+
assert result == {"summary_csv": None, "merged_csv": None, "grouped_csv": None}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_run_all_skips_grouping_when_groups_csv_not_configured(tmp_path: Path) -> None:
|
|
66
|
+
input_dir = tmp_path / "input"
|
|
67
|
+
input_dir.mkdir()
|
|
68
|
+
pipeline = Pipeline(_settings(tmp_path, input_dir), logging.getLogger("tests.runner"))
|
|
69
|
+
|
|
70
|
+
result = pipeline.run("all")
|
|
71
|
+
|
|
72
|
+
assert result["grouped_csv"] is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_group_and_average_writes_grouped_csv(tmp_path: Path) -> None:
|
|
76
|
+
resampled_dir = tmp_path / "resampled"
|
|
77
|
+
resampled_dir.mkdir(parents=True)
|
|
78
|
+
merged_csv = resampled_dir / "merged.csv"
|
|
79
|
+
merged_csv.write_text("sample_name,400,401\nleaf.0001,1.0,2.0\nleaf.0002,3.0,4.0\n")
|
|
80
|
+
|
|
81
|
+
groups_csv = tmp_path / "groups.csv"
|
|
82
|
+
groups_csv.write_text("scan_id,name\n1,leaf_a\n2,leaf_a\n")
|
|
83
|
+
|
|
84
|
+
settings = _settings(tmp_path, tmp_path / "input", groups_csv=groups_csv)
|
|
85
|
+
pipeline = Pipeline(settings, logging.getLogger("tests.runner"))
|
|
86
|
+
|
|
87
|
+
output = pipeline.group_and_average(merged_csv)
|
|
88
|
+
|
|
89
|
+
assert output == resampled_dir / "grouped.csv"
|
|
90
|
+
written = output.read_text()
|
|
91
|
+
assert "leaf_a" in written
|
|
92
|
+
assert "2.0" in written # mean(1.0, 3.0)
|
|
93
|
+
assert "3.0" in written # mean(2.0, 4.0)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_group_and_average_missing_groups_file_returns_none(tmp_path: Path) -> None:
|
|
97
|
+
resampled_dir = tmp_path / "resampled"
|
|
98
|
+
resampled_dir.mkdir(parents=True)
|
|
99
|
+
merged_csv = resampled_dir / "merged.csv"
|
|
100
|
+
merged_csv.write_text("sample_name,400\nleaf.0001,1.0\n")
|
|
101
|
+
|
|
102
|
+
settings = _settings(tmp_path, tmp_path / "input", groups_csv=tmp_path / "missing_groups.csv")
|
|
103
|
+
pipeline = Pipeline(settings, logging.getLogger("tests.runner"))
|
|
104
|
+
|
|
105
|
+
assert pipeline.group_and_average(merged_csv) is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_run_step_three_without_merged_csv_logs_error(tmp_path: Path) -> None:
|
|
109
|
+
input_dir = tmp_path / "input"
|
|
110
|
+
input_dir.mkdir()
|
|
111
|
+
groups_csv = tmp_path / "groups.csv"
|
|
112
|
+
groups_csv.write_text("scan_id,name\n1,leaf_a\n")
|
|
113
|
+
pipeline = Pipeline(_settings(tmp_path, input_dir, groups_csv=groups_csv), logging.getLogger("tests.runner"))
|
|
114
|
+
|
|
115
|
+
result = pipeline.run("3")
|
|
116
|
+
|
|
117
|
+
assert result == {"summary_csv": None, "merged_csv": None, "grouped_csv": None}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_run_step_three_groups_existing_merged_csv(tmp_path: Path) -> None:
|
|
121
|
+
input_dir = tmp_path / "input"
|
|
122
|
+
input_dir.mkdir()
|
|
123
|
+
resampled_dir = tmp_path / "resampled"
|
|
124
|
+
resampled_dir.mkdir(parents=True)
|
|
125
|
+
merged_csv = resampled_dir / "merged.csv"
|
|
126
|
+
merged_csv.write_text("sample_name,400\nleaf.0001,1.0\nleaf.0002,3.0\n")
|
|
127
|
+
groups_csv = tmp_path / "groups.csv"
|
|
128
|
+
groups_csv.write_text("scan_id,name\n1,leaf_a\n2,leaf_a\n")
|
|
129
|
+
|
|
130
|
+
pipeline = Pipeline(_settings(tmp_path, input_dir, groups_csv=groups_csv), logging.getLogger("tests.runner"))
|
|
131
|
+
|
|
132
|
+
result = pipeline.run("3")
|
|
133
|
+
|
|
134
|
+
assert result["merged_csv"] == merged_csv
|
|
135
|
+
assert result["grouped_csv"] == resampled_dir / "grouped.csv"
|
|
136
|
+
assert result["grouped_csv"].exists()
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from pipeline.run_config import PipelineSettings
|
|
7
|
-
from pipeline.runner import Pipeline
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _settings(tmp_path: Path, input_dir: Path) -> PipelineSettings:
|
|
11
|
-
return PipelineSettings(
|
|
12
|
-
source_name=input_dir.name or "input",
|
|
13
|
-
input_dir=input_dir,
|
|
14
|
-
processed_dir=tmp_path / "processed",
|
|
15
|
-
resampled_dir=tmp_path / "resampled",
|
|
16
|
-
summary_csv=tmp_path / "processed" / "summary.csv",
|
|
17
|
-
merged_csv_name="merged.csv",
|
|
18
|
-
end_line_overrides={},
|
|
19
|
-
verbose=False,
|
|
20
|
-
processing_params={
|
|
21
|
-
"band_min": 400,
|
|
22
|
-
"band_max": 410,
|
|
23
|
-
"resample_fwhm_nm": 10.0,
|
|
24
|
-
"splice_interp_wvl": (5.0, 2.0),
|
|
25
|
-
"fixed_sensor": 2,
|
|
26
|
-
},
|
|
27
|
-
correction_types={"bronze": "2520.4", "silver": "2517.9"},
|
|
28
|
-
instrument_numbers={"bronze": "2212118", "silver": "1202103"},
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_process_sig_files_returns_none_for_missing_input(tmp_path: Path) -> None:
|
|
33
|
-
pipeline = Pipeline(_settings(tmp_path, tmp_path / "missing"), logging.getLogger("tests.runner"))
|
|
34
|
-
|
|
35
|
-
assert pipeline.process_sig_files() is None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_process_sig_files_returns_none_for_empty_input(tmp_path: Path) -> None:
|
|
39
|
-
input_dir = tmp_path / "empty"
|
|
40
|
-
input_dir.mkdir()
|
|
41
|
-
pipeline = Pipeline(_settings(tmp_path, input_dir), logging.getLogger("tests.runner"))
|
|
42
|
-
|
|
43
|
-
assert pipeline.process_sig_files() is None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_run_step_two_without_summary_does_not_resample(tmp_path: Path) -> None:
|
|
47
|
-
input_dir = tmp_path / "input"
|
|
48
|
-
input_dir.mkdir()
|
|
49
|
-
pipeline = Pipeline(_settings(tmp_path, input_dir), logging.getLogger("tests.runner"))
|
|
50
|
-
|
|
51
|
-
result = pipeline.run("2")
|
|
52
|
-
|
|
53
|
-
assert result == {"summary_csv": None, "merged_csv": None}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|