electropycal 0.9.0__py3-none-any.whl
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.
- electropycal/__init__.py +7 -0
- electropycal/_demo.py +145 -0
- electropycal/_parallel.py +40 -0
- electropycal/analysis_config.py +168 -0
- electropycal/cli.py +323 -0
- electropycal/data/__init__.py +1 -0
- electropycal/data/inventory.py +479 -0
- electropycal/data/io.py +68 -0
- electropycal/data/pstrace.py +205 -0
- electropycal/data/quality.py +74 -0
- electropycal/data/schema.py +118 -0
- electropycal/data/stabilization.py +234 -0
- electropycal/data/synthetic.py +520 -0
- electropycal/deployment/__init__.py +1 -0
- electropycal/deployment/deploy.py +244 -0
- electropycal/deployment/domain.py +88 -0
- electropycal/deployment/plots.py +45 -0
- electropycal/diagnostics/__init__.py +1 -0
- electropycal/diagnostics/variance.py +217 -0
- electropycal/diagreview.py +770 -0
- electropycal/discovery/__init__.py +1 -0
- electropycal/discovery/baseline.py +55 -0
- electropycal/discovery/batch.py +91 -0
- electropycal/discovery/config.py +205 -0
- electropycal/discovery/review.py +157 -0
- electropycal/discovery/runner.py +362 -0
- electropycal/discovery/scheduler.py +243 -0
- electropycal/evaluation/__init__.py +1 -0
- electropycal/evaluation/admissibility.py +85 -0
- electropycal/evaluation/baselines.py +181 -0
- electropycal/evaluation/classify.py +88 -0
- electropycal/evaluation/cv.py +140 -0
- electropycal/evaluation/framing.py +210 -0
- electropycal/evaluation/hierarchical.py +161 -0
- electropycal/evaluation/metrics.py +173 -0
- electropycal/evaluation/multioutput.py +130 -0
- electropycal/evaluation/stratify.py +121 -0
- electropycal/evaluation/tracks.py +35 -0
- electropycal/features/__init__.py +1 -0
- electropycal/features/catalog.py +115 -0
- electropycal/features/eis.py +135 -0
- electropycal/features/extract.py +951 -0
- electropycal/features/fscv.py +448 -0
- electropycal/features/normalize.py +168 -0
- electropycal/features/pin.py +267 -0
- electropycal/features/targets.py +193 -0
- electropycal/models/__init__.py +1 -0
- electropycal/models/base.py +66 -0
- electropycal/models/plsr.py +141 -0
- electropycal/models/variants.py +217 -0
- electropycal/overview.py +144 -0
- electropycal/py.typed +0 -0
- electropycal/qcdash.py +467 -0
- electropycal/rawspectra.py +824 -0
- electropycal/selection/__init__.py +1 -0
- electropycal/selection/cars.py +102 -0
- electropycal/selection/icc.py +32 -0
- electropycal/selection/pseudo_multivariate.py +82 -0
- electropycal/selection/univariate.py +23 -0
- electropycal/stabreview.py +301 -0
- electropycal/viz.py +97 -0
- electropycal-0.9.0.dist-info/METADATA +156 -0
- electropycal-0.9.0.dist-info/RECORD +66 -0
- electropycal-0.9.0.dist-info/WHEEL +4 -0
- electropycal-0.9.0.dist-info/entry_points.txt +2 -0
- electropycal-0.9.0.dist-info/licenses/LICENSE +21 -0
electropycal/cli.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Command-line interface: ``electropycal discover`` and ``electropycal deploy``.
|
|
2
|
+
|
|
3
|
+
Designed for a checkpointed, batch-by-batch workflow: run a queue (or a single batch),
|
|
4
|
+
inspect the ``report/`` outputs, then decide whether to proceed. BLAS threads are pinned
|
|
5
|
+
before numpy-heavy work to keep joblib fold-parallelism from oversubscribing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .discovery.scheduler import pin_blas_single_threaded
|
|
14
|
+
|
|
15
|
+
pin_blas_single_threaded()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_band(s: str):
|
|
19
|
+
"""CLI ``--band``: ``'auto'`` (data-driven) or ``'lo,hi'`` in Hz."""
|
|
20
|
+
if s == "auto":
|
|
21
|
+
return "auto"
|
|
22
|
+
parts = s.split(",")
|
|
23
|
+
if len(parts) != 2:
|
|
24
|
+
raise argparse.ArgumentTypeError("band must be 'auto' or 'lo,hi' (e.g. 10,21544)")
|
|
25
|
+
return (float(parts[0]), float(parts[1]))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_seeds(s: str):
|
|
29
|
+
"""CLI ``--seeds``: a comma-separated seed set, e.g. ``'0,1,2'`` -> ``(0, 1, 2)``.
|
|
30
|
+
|
|
31
|
+
Distinct from ``--seed``, which is the single seed used to build the CV folds. This is the
|
|
32
|
+
set the *stochastic* selectors (CARS / MI) are repeated over and averaged across;
|
|
33
|
+
deterministic conditions collapse to one seed internally regardless.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
seeds = tuple(int(x) for x in s.split(",") if x.strip() != "")
|
|
37
|
+
except ValueError:
|
|
38
|
+
raise argparse.ArgumentTypeError(
|
|
39
|
+
f"seeds must be comma-separated integers (e.g. 0,1,2), got {s!r}") from None
|
|
40
|
+
if not seeds:
|
|
41
|
+
raise argparse.ArgumentTypeError("seeds must list at least one integer (e.g. 0,1,2)")
|
|
42
|
+
return seeds
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_cli_band(root, band):
|
|
46
|
+
"""Resolve the CLI's ``--band`` to a concrete ``(lo, hi)``, announcing it when derived.
|
|
47
|
+
|
|
48
|
+
The library primitive deliberately has no ``band`` default: ``"auto"`` is a percentile over
|
|
49
|
+
whatever corpus is present, so it cannot be reproduced over a staged subset. The CLI keeps
|
|
50
|
+
``--band auto`` as its default because there it is a *stated* choice — it appears in
|
|
51
|
+
``--help`` and in the docs — but it must not be a silent one, so the resolved band is
|
|
52
|
+
always printed and a concrete tuple is passed down. The value also lands in the run's
|
|
53
|
+
extraction pin, so a later run can reuse it rather than re-deriving it.
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(band, str):
|
|
56
|
+
return (float(band[0]), float(band[1]))
|
|
57
|
+
from .data.inventory import recommended_band
|
|
58
|
+
lo, hi = recommended_band(root)
|
|
59
|
+
print(f"--band auto -> ({lo:.0f}, {hi:.0f}) Hz (data-driven, from {root}); "
|
|
60
|
+
f"pass --band lo,hi to pin it", flush=True)
|
|
61
|
+
return (float(lo), float(hi))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_frame(source: str, progress: bool = False, band="auto", n_jobs: int = 1):
|
|
65
|
+
"""Resolve a data source to a per-dose featureset DataFrame."""
|
|
66
|
+
if source == "synthetic":
|
|
67
|
+
import pandas as pd
|
|
68
|
+
from .data.synthetic import make_dataset
|
|
69
|
+
ds = make_dataset(random_state=0)
|
|
70
|
+
df = pd.DataFrame(ds.X, columns=ds.feature_names)
|
|
71
|
+
df["channel"] = ds.channel; df["timepoint"] = ds.timepoint
|
|
72
|
+
df["concentration"] = ds.concentration; df["NormIpeak"] = ds.y
|
|
73
|
+
return df
|
|
74
|
+
if Path(source).is_dir():
|
|
75
|
+
from .features.extract import extract_dataset
|
|
76
|
+
return extract_dataset(source, band=_resolve_cli_band(source, band),
|
|
77
|
+
n_jobs=n_jobs, progress=progress)
|
|
78
|
+
import pandas as pd
|
|
79
|
+
return pd.read_parquet(source) if source.endswith(".parquet") else pd.read_csv(source)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _load_data(source: str, progress: bool = False, band="auto", target: str = "normipeak",
|
|
83
|
+
n_jobs: int = 1):
|
|
84
|
+
"""Resolve a data source to RunData: 'synthetic', a featureset parquet/csv, or a raw
|
|
85
|
+
PSTrace directory (auto-extracted, ``n_jobs`` worker processes). ``target="sensitivity"``
|
|
86
|
+
reframes to the per-sensor-timepoint dose-response slope
|
|
87
|
+
(:func:`~electropycal.features.targets.sensitivity_featureset`)."""
|
|
88
|
+
from .discovery.config import RunData
|
|
89
|
+
# per-sensor-timepoint calibration-curve targets all reframe via sensitivity_featureset
|
|
90
|
+
CURVE_TARGETS = {"sensitivity", "sensitivity_intercept", "sensitivity_curvature",
|
|
91
|
+
"sat_imax", "sat_kd", "sat_logkd", "hill_imax", "hill_kd", "hill_n",
|
|
92
|
+
"power_a", "power_beta"}
|
|
93
|
+
if target in CURVE_TARGETS:
|
|
94
|
+
from .features.targets import sensitivity_featureset
|
|
95
|
+
df = _load_frame(source, progress=progress, band=band, n_jobs=n_jobs)
|
|
96
|
+
return RunData.from_frame(sensitivity_featureset(df), target=target)
|
|
97
|
+
return RunData.from_frame(_load_frame(source, progress=progress, band=band, n_jobs=n_jobs))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract(args) -> None:
|
|
101
|
+
from .features.extract import extract_dataset
|
|
102
|
+
df = extract_dataset(args.raw, band=_resolve_cli_band(args.raw, args.band),
|
|
103
|
+
peak_method=args.peak_method, detrend=args.detrend,
|
|
104
|
+
acceptance=args.acceptance, min_norm_snr=args.min_norm_snr,
|
|
105
|
+
mono_tol=args.mono_tol, max_reps=args.max_reps,
|
|
106
|
+
min_dose_response_range=args.min_dose_response_range,
|
|
107
|
+
d0_normalize=not args.no_d0_normalize, n_jobs=args.n_jobs,
|
|
108
|
+
progress=args.progress)
|
|
109
|
+
dest = Path(args.out)
|
|
110
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
df.to_parquet(dest, index=False)
|
|
112
|
+
n_sensors = df.groupby(["device", "channel"]).ngroups if not df.empty else 0
|
|
113
|
+
print(f"featureset written to: {dest} ({len(df)} rows, {n_sensors} sensors, "
|
|
114
|
+
f"{df.shape[1]} columns)")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _interactive_batch_gate(batch_num, ranking, remaining):
|
|
118
|
+
"""Show the completed batch's ranking + auto-flags, then apply the user's
|
|
119
|
+
exclude/include edits to the remaining queue (terminal ``--batched`` mode).
|
|
120
|
+
|
|
121
|
+
Returns ``False`` to stop, or the (possibly edited) remaining ``list[Condition]``.
|
|
122
|
+
"""
|
|
123
|
+
import pandas as pd
|
|
124
|
+
|
|
125
|
+
from .discovery.scheduler import auto_flag_conditions
|
|
126
|
+
df = pd.DataFrame([r for r in ranking if r.get("batch") == batch_num])
|
|
127
|
+
cols = [c for c in ("condition", "track", "pooled_rmsep", "rmsep_ci_lo",
|
|
128
|
+
"rmsep_ci_hi", "pooled_q2", "n_folds") if c in df.columns]
|
|
129
|
+
print(f"\n=== Batch {batch_num} complete ===")
|
|
130
|
+
print(df[cols].sort_values("pooled_rmsep", na_position="last").to_string(index=False))
|
|
131
|
+
if not remaining:
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
flags = auto_flag_conditions(ranking, remaining)
|
|
135
|
+
print(f"\nUpcoming conditions ({len(remaining)}):")
|
|
136
|
+
for c in remaining:
|
|
137
|
+
print(f" batch {c.batch} {c.name}" + (f" [FLAGGED: {flags[c.name]}]"
|
|
138
|
+
if c.name in flags else ""))
|
|
139
|
+
print("\nActions (space-separated): <Enter>=proceed (exclude flagged) | "
|
|
140
|
+
"keep:<name>=include a flagged one | drop:<name>=exclude another | "
|
|
141
|
+
"all=keep everything | stop=end run")
|
|
142
|
+
resp = input("> ").strip()
|
|
143
|
+
if resp.lower() == "stop":
|
|
144
|
+
return False
|
|
145
|
+
exclude = set() if resp.lower() == "all" else set(flags)
|
|
146
|
+
if resp.lower() not in ("", "all"):
|
|
147
|
+
for tok in resp.replace(",", " ").split():
|
|
148
|
+
if tok.startswith("keep:"):
|
|
149
|
+
exclude.discard(tok[5:])
|
|
150
|
+
elif tok.startswith("drop:"):
|
|
151
|
+
exclude.add(tok[5:])
|
|
152
|
+
if exclude:
|
|
153
|
+
print("excluding:", ", ".join(sorted(exclude)))
|
|
154
|
+
return [c for c in remaining if c.name not in exclude]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _features(args) -> None:
|
|
158
|
+
from .features.catalog import print_feature_catalog
|
|
159
|
+
print_feature_catalog()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _discover(args) -> None:
|
|
163
|
+
from .discovery.config import FAST, Profile, baseline_queue
|
|
164
|
+
from .discovery.scheduler import run_discovery
|
|
165
|
+
data = _load_data(args.data, progress=args.progress, band=args.band, target=args.target,
|
|
166
|
+
n_jobs=args.n_jobs)
|
|
167
|
+
import dataclasses
|
|
168
|
+
# FAST hardcodes n_jobs=1, so selecting it used to silently discard --n-jobs. Apply the
|
|
169
|
+
# flag to whichever profile was chosen instead of only to the full one.
|
|
170
|
+
profile = dataclasses.replace(FAST, n_jobs=args.n_jobs) if args.profile == "fast" \
|
|
171
|
+
else Profile(n_jobs=args.n_jobs)
|
|
172
|
+
if args.seeds is not None:
|
|
173
|
+
# Without this the multi-seed path was unreachable from the CLI: runner honours
|
|
174
|
+
# profile.seeds only when the selector is stochastic AND more than one distinct seed is
|
|
175
|
+
# set, and both profiles ship seeds=(0,) -- so the three-seed sweep was notebook-only.
|
|
176
|
+
profile = dataclasses.replace(profile, seeds=args.seeds)
|
|
177
|
+
if args.min_train_times is not None:
|
|
178
|
+
profile = dataclasses.replace(profile, min_train_times=args.min_train_times)
|
|
179
|
+
batch_gate = _interactive_batch_gate if args.batched else None
|
|
180
|
+
from pathlib import Path as _Path
|
|
181
|
+
_proot = _Path(args.data)
|
|
182
|
+
_proot = _proot if _proot.is_dir() else _proot.parent # config/stats live at the data root
|
|
183
|
+
run_dir = run_discovery(data, conditions=baseline_queue(), out_root=args.out,
|
|
184
|
+
profile=profile, seed=args.seed, batch_gate=batch_gate,
|
|
185
|
+
progress=args.progress, provenance_root=_proot, target=args.target)
|
|
186
|
+
print(f"\ndiscovery run written to: {run_dir}")
|
|
187
|
+
print((run_dir / "report" / "discovery_summary.md").read_text())
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _freeze(args) -> None:
|
|
191
|
+
from .deployment.deploy import freeze_top
|
|
192
|
+
data = _load_data(args.data)
|
|
193
|
+
out = freeze_top(args.run, data, condition=args.condition,
|
|
194
|
+
stability_min=args.stability_min, out_dir=args.out)
|
|
195
|
+
print(f"frozen model written to: {out}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _deploy(args) -> None:
|
|
199
|
+
import pandas as pd
|
|
200
|
+
from .deployment.deploy import recalibrate, recalibrate_invivo
|
|
201
|
+
dest = Path(args.out)
|
|
202
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
if args.raw: # raw in-vivo directory → per-session
|
|
204
|
+
res = recalibrate_invivo(args.model, args.raw, flag_distance=args.flag_distance, out=dest)
|
|
205
|
+
print(f"in-vivo recalibration (per session) written to: {dest}")
|
|
206
|
+
print(res.to_string(index=False))
|
|
207
|
+
return
|
|
208
|
+
df = pd.read_parquet(args.data) if args.data.endswith(".parquet") else pd.read_csv(args.data)
|
|
209
|
+
from .data.schema import RESERVED_COLUMNS
|
|
210
|
+
feats = [c for c in df.columns if c not in RESERVED_COLUMNS and c != "time_index"]
|
|
211
|
+
out = recalibrate(args.model, df[feats].to_numpy(float))
|
|
212
|
+
df_out = df[[c for c in ("channel", "timepoint") if c in df.columns]].copy()
|
|
213
|
+
df_out["NormIpeak_recal"] = out["norm_ipeak"]
|
|
214
|
+
if "domain_distance" in out:
|
|
215
|
+
df_out["domain_distance"] = out["domain_distance"]
|
|
216
|
+
df_out.to_parquet(dest, index=False)
|
|
217
|
+
print(f"recalibrated predictions written to: {dest}")
|
|
218
|
+
if "domain_distance" in out:
|
|
219
|
+
print(f"CORAL domain distance vs in-vitro reference: {out['domain_distance']:.3f}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main(argv=None) -> None:
|
|
223
|
+
parser = argparse.ArgumentParser(prog="electropycal")
|
|
224
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
225
|
+
|
|
226
|
+
e = sub.add_parser("extract", help="build a featureset from a raw PSTrace export directory")
|
|
227
|
+
e.add_argument("--raw", required=True, help="root directory of <date>_<devicetype>_signal folders")
|
|
228
|
+
e.add_argument("--out", default="featureset_extracted.parquet")
|
|
229
|
+
e.add_argument("--band", type=_parse_band, default="auto",
|
|
230
|
+
help="EIS analysis band: 'auto' (default, data-driven upper bound below the "
|
|
231
|
+
"tightest inductive onset) or 'lo,hi' in Hz (e.g. 10,21544)")
|
|
232
|
+
e.add_argument("--peak-method", choices=["chord", "direct"], default="direct", dest="peak_method",
|
|
233
|
+
help="FSCV peak height: 'direct' (current at V_ox; default, robust to broad "
|
|
234
|
+
"DA peaks) or 'chord' (chord-baseline height)")
|
|
235
|
+
e.add_argument("--detrend", action="store_true",
|
|
236
|
+
help="subtract a non-Faradaic baseline slope before peak/noise (opt-in)")
|
|
237
|
+
e.add_argument("--acceptance", choices=["monotonic", "monotonic+snr", "snr", "none"],
|
|
238
|
+
default="monotonic",
|
|
239
|
+
help="FSCV channel acceptance: dose-response monotonicity (default); "
|
|
240
|
+
"'monotonic+snr' adds a lenient per-dose SNR cut; 'snr'; or 'none'")
|
|
241
|
+
e.add_argument("--min-norm-snr", type=float, default=3.0, dest="min_norm_snr",
|
|
242
|
+
help="SNR gate multiple (acceptance=snr)")
|
|
243
|
+
e.add_argument("--mono-tol", type=float, default=0.10, dest="mono_tol",
|
|
244
|
+
help="EIS |Z|-monotonicity tolerance (check B)")
|
|
245
|
+
e.add_argument("--max-reps", type=int, default=3, dest="max_reps",
|
|
246
|
+
help="keep only the first N FSCV replicate cycles per file "
|
|
247
|
+
"(the intended [0..N-1] rounds; drops erroneous extras; default 3)")
|
|
248
|
+
e.add_argument("--n-jobs", type=int, default=1, dest="n_jobs",
|
|
249
|
+
help="worker processes to feature-extract sessions in parallel "
|
|
250
|
+
"(-1 = all cores; output is identical to serial, just faster)")
|
|
251
|
+
e.add_argument("--min-dose-response-range", type=float, default=None, dest="min_dose_response_range",
|
|
252
|
+
help="amplitude floor: drop channel-timepoints whose NormIpeak dynamic range "
|
|
253
|
+
"(max-min over doses) is below this (excludes monotone-but-dead electrodes; "
|
|
254
|
+
"off by default)")
|
|
255
|
+
e.add_argument("--no-d0-normalize", action="store_true", dest="no_d0_normalize",
|
|
256
|
+
help="skip per-sensor D0-normalization; default is to D0-normalize")
|
|
257
|
+
e.add_argument("--progress", action="store_true",
|
|
258
|
+
help="print timestamped per-session progress with ETA during extraction")
|
|
259
|
+
e.set_defaults(func=_extract)
|
|
260
|
+
|
|
261
|
+
d = sub.add_parser("discover", help="run the model-discovery queue")
|
|
262
|
+
d.add_argument("--data", default="synthetic",
|
|
263
|
+
help="'synthetic', a featureset parquet/csv, or a raw PSTrace directory")
|
|
264
|
+
d.add_argument("--band", type=_parse_band, default="auto",
|
|
265
|
+
help="EIS analysis band when --data is a raw directory: 'auto' (default, "
|
|
266
|
+
"data-driven) or 'lo,hi' in Hz. Ignored for a featureset/synthetic --data")
|
|
267
|
+
d.add_argument("--target", choices=["normipeak", "sensitivity"], default="normipeak",
|
|
268
|
+
help="prediction target: 'normipeak' (per-dose, default) or 'sensitivity' "
|
|
269
|
+
"(per sensor-timepoint dose-response slope — the recalibration signal)")
|
|
270
|
+
d.add_argument("--out", default="outputs")
|
|
271
|
+
d.add_argument("--profile", choices=["fast", "full"], default="full")
|
|
272
|
+
d.add_argument("--n-jobs", type=int, default=1, dest="n_jobs",
|
|
273
|
+
help="worker processes over CV folds (default 1 = serial). -1 uses every "
|
|
274
|
+
"visible core, which is more robust than $(nproc) -- that returns 1 in "
|
|
275
|
+
"some containers and silently runs serially")
|
|
276
|
+
d.add_argument("--seed", type=int, default=0,
|
|
277
|
+
help="seed for CV fold construction (a single value)")
|
|
278
|
+
d.add_argument("--seeds", type=_parse_seeds, default=None,
|
|
279
|
+
help="seed SET for the stochastic selectors (CARS/MI), comma-separated, "
|
|
280
|
+
"e.g. '0,1,2'. Each such condition is repeated per seed and its "
|
|
281
|
+
"aggregates averaged; deterministic conditions collapse to one seed "
|
|
282
|
+
"internally, so this multiplies only the stochastic ones. Default: the "
|
|
283
|
+
"profile's own seeds=(0,)")
|
|
284
|
+
d.add_argument("--min-train-times", type=int, default=None, dest="min_train_times",
|
|
285
|
+
help="min prior timepoints before a t_test is used (nested CV needs ≥3 "
|
|
286
|
+
"timepoints; set 1 for a 2-timepoint smoke run)")
|
|
287
|
+
d.add_argument("--batched", action="store_true",
|
|
288
|
+
help="review/edit each task-queue batch's ranking before continuing")
|
|
289
|
+
d.add_argument("--progress", action="store_true",
|
|
290
|
+
help="print timestamped per-condition/per-fold progress with ETA "
|
|
291
|
+
"(and per-session lines when --data is a raw directory)")
|
|
292
|
+
d.set_defaults(func=_discover)
|
|
293
|
+
|
|
294
|
+
f = sub.add_parser("freeze", help="freeze a discovery-selected model on 100%% of in-vitro data")
|
|
295
|
+
f.add_argument("--run", required=True, help="a completed discovery run directory")
|
|
296
|
+
f.add_argument("--data", required=True,
|
|
297
|
+
help="the in-vitro featureset (parquet/csv) or raw dir used for discovery")
|
|
298
|
+
f.add_argument("--condition", default=None,
|
|
299
|
+
help="condition name to freeze (default: the top-ranked one)")
|
|
300
|
+
f.add_argument("--stability-min", type=float, default=0.5, dest="stability_min",
|
|
301
|
+
help="min fold selection-frequency for a feature to enter the frozen subset")
|
|
302
|
+
f.add_argument("--out", default="outputs/frozen_model")
|
|
303
|
+
f.set_defaults(func=_freeze)
|
|
304
|
+
|
|
305
|
+
p = sub.add_parser("deploy", help="recalibrate in-vivo data with a frozen model")
|
|
306
|
+
p.add_argument("--model", required=True, help="path to a frozen_model/ bundle")
|
|
307
|
+
p.add_argument("--data", default="", help="in-vivo featureset parquet/csv (omit if using --raw)")
|
|
308
|
+
p.add_argument("--raw", default="", help="raw in-vivo directory (extract + per-session recalibrate)")
|
|
309
|
+
p.add_argument("--out", default="outputs/deployments/recalibrated.parquet")
|
|
310
|
+
p.add_argument("--flag-distance", type=float, default=None, dest="flag_distance",
|
|
311
|
+
help="optional CORAL distance threshold to flag EXTRAPOLATING (with --raw); "
|
|
312
|
+
"calibrate from in-vitro CV distances — omit to just read raw distances")
|
|
313
|
+
p.set_defaults(func=_deploy)
|
|
314
|
+
|
|
315
|
+
c = sub.add_parser("features", help="print the feature catalog (types, definitions, units, role)")
|
|
316
|
+
c.set_defaults(func=_features)
|
|
317
|
+
|
|
318
|
+
args = parser.parse_args(argv)
|
|
319
|
+
args.func(args)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""electropycal.data subpackage. See docs/DESIGN.md §4."""
|