spatial-vtk 0.1.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.
- spatial_vtk/__init__.py +7 -0
- spatial_vtk/cli/__init__.py +1056 -0
- spatial_vtk/cli/__main__.py +9 -0
- spatial_vtk/config/__init__.py +136 -0
- spatial_vtk/config/bounds.py +105 -0
- spatial_vtk/config/default_outputs.yaml +221 -0
- spatial_vtk/config/labels.py +607 -0
- spatial_vtk/config/metric_catalog.py +159 -0
- spatial_vtk/config/metrics.py +365 -0
- spatial_vtk/config/naming.py +49 -0
- spatial_vtk/config/notebook.py +243 -0
- spatial_vtk/config/outputs.py +238 -0
- spatial_vtk/config/paths.py +154 -0
- spatial_vtk/config/runtime.py +608 -0
- spatial_vtk/io/__init__.py +185 -0
- spatial_vtk/io/artifacts.py +364 -0
- spatial_vtk/io/catalogs.py +156 -0
- spatial_vtk/io/compute_manifest.py +216 -0
- spatial_vtk/io/inventory.py +136 -0
- spatial_vtk/io/kml.py +101 -0
- spatial_vtk/io/layouts.py +67 -0
- spatial_vtk/io/master_lists.py +326 -0
- spatial_vtk/io/metadata.py +466 -0
- spatial_vtk/io/metric_inputs.py +277 -0
- spatial_vtk/io/model_aliases.py +278 -0
- spatial_vtk/io/output_paths.py +63 -0
- spatial_vtk/io/plans.py +329 -0
- spatial_vtk/io/preprocessing.py +354 -0
- spatial_vtk/io/synthetic_formats.py +882 -0
- spatial_vtk/io/tables.py +553 -0
- spatial_vtk/io/waveforms.py +944 -0
- spatial_vtk/metrics/__init__.py +118 -0
- spatial_vtk/metrics/calculate/__init__.py +198 -0
- spatial_vtk/metrics/calculate/amplitudes.py +149 -0
- spatial_vtk/metrics/calculate/arrival_picks.py +194 -0
- spatial_vtk/metrics/calculate/bands.py +142 -0
- spatial_vtk/metrics/calculate/batch.py +118 -0
- spatial_vtk/metrics/calculate/enrich.py +282 -0
- spatial_vtk/metrics/calculate/gof.py +1778 -0
- spatial_vtk/metrics/calculate/phasenet_adapter.py +464 -0
- spatial_vtk/metrics/calculate/records.py +284 -0
- spatial_vtk/metrics/calculate/spectra.py +182 -0
- spatial_vtk/metrics/calculate/summaries.py +413 -0
- spatial_vtk/metrics/calculate/transforms.py +242 -0
- spatial_vtk/metrics/calculate/waveforms.py +264 -0
- spatial_vtk/metrics/plot/__init__.py +62 -0
- spatial_vtk/metrics/plot/example_metric_plots.py +110 -0
- spatial_vtk/metrics/plot/model_comparison.py +231 -0
- spatial_vtk/metrics/plot/periods.py +164 -0
- spatial_vtk/metrics/plot/site_terms.py +56 -0
- spatial_vtk/metrics/plot/trends.py +280 -0
- spatial_vtk/metrics/workflow/__init__.py +73 -0
- spatial_vtk/metrics/workflow/execution.py +287 -0
- spatial_vtk/metrics/workflow/outputs.py +215 -0
- spatial_vtk/metrics/workflow/run.py +803 -0
- spatial_vtk/metrics/workflow/slurm.py +229 -0
- spatial_vtk/metrics/workflow/tasks.py +600 -0
- spatial_vtk/qc/__init__.py +52 -0
- spatial_vtk/qc/build/__init__.py +85 -0
- spatial_vtk/qc/build/filtering.py +355 -0
- spatial_vtk/qc/build/inventory.py +1159 -0
- spatial_vtk/qc/build/spectral.py +268 -0
- spatial_vtk/qc/build/workflow.py +1034 -0
- spatial_vtk/qc/review/__init__.py +25 -0
- spatial_vtk/qc/review/tables.py +263 -0
- spatial_vtk/qc/summary/__init__.py +25 -0
- spatial_vtk/qc/summary/rules.py +150 -0
- spatial_vtk/spatial/__init__.py +3 -0
- spatial_vtk/spatial/calculate/__init__.py +109 -0
- spatial_vtk/spatial/calculate/_common.py +247 -0
- spatial_vtk/spatial/calculate/clustering.py +499 -0
- spatial_vtk/spatial/calculate/correlation.py +615 -0
- spatial_vtk/spatial/calculate/corridors.py +1163 -0
- spatial_vtk/spatial/calculate/geojson.py +648 -0
- spatial_vtk/spatial/calculate/geology.py +796 -0
- spatial_vtk/spatial/calculate/geometry.py +44 -0
- spatial_vtk/spatial/calculate/paths.py +37 -0
- spatial_vtk/spatial/calculate/patterns.py +143 -0
- spatial_vtk/spatial/calculate/pca.py +192 -0
- spatial_vtk/spatial/calculate/polygon_edges.py +1002 -0
- spatial_vtk/spatial/calculate/prepare_stats.py +658 -0
- spatial_vtk/spatial/calculate/rotation.py +44 -0
- spatial_vtk/spatial/calculate/settings.py +143 -0
- spatial_vtk/spatial/calculate/workflow.py +78 -0
- spatial_vtk/spatial/map/__init__.py +63 -0
- spatial_vtk/spatial/map/basemaps.py +643 -0
- spatial_vtk/spatial/map/correlation.py +516 -0
- spatial_vtk/spatial/map/geojson.py +199 -0
- spatial_vtk/spatial/map/metrics.py +620 -0
- spatial_vtk/spatial/map/path/__init__.py +25 -0
- spatial_vtk/spatial/map/path/corridors.py +353 -0
- spatial_vtk/spatial/map/path/residuals.py +131 -0
- spatial_vtk/spatial/map/pca.py +452 -0
- spatial_vtk/spatial/map/region/__init__.py +3 -0
- spatial_vtk/spatial/plot/__init__.py +46 -0
- spatial_vtk/spatial/plot/correlation.py +607 -0
- spatial_vtk/spatial/plot/metrics.py +2294 -0
- spatial_vtk/spatial/plot/pca.py +127 -0
- spatial_vtk/visualize/__init__.py +92 -0
- spatial_vtk/visualize/context/__init__.py +39 -0
- spatial_vtk/visualize/context/figures.py +1510 -0
- spatial_vtk/visualize/context/maps.py +429 -0
- spatial_vtk/visualize/dashboard/__init__.py +98 -0
- spatial_vtk/visualize/dashboard/charts.py +141 -0
- spatial_vtk/visualize/dashboard/contracts.py +131 -0
- spatial_vtk/visualize/dashboard/export.py +269 -0
- spatial_vtk/visualize/dashboard/exports.py +106 -0
- spatial_vtk/visualize/dashboard/filters.py +195 -0
- spatial_vtk/visualize/dashboard/labels.py +36 -0
- spatial_vtk/visualize/dashboard/launch.py +101 -0
- spatial_vtk/visualize/dashboard/maps.py +208 -0
- spatial_vtk/visualize/dashboard/streamlit_metrics.py +261 -0
- spatial_vtk/visualize/dashboard/streamlit_qc.py +208 -0
- spatial_vtk/visualize/dashboard/tables.py +197 -0
- spatial_vtk/visualize/figure_context.py +576 -0
- spatial_vtk/visualize/figure_io.py +263 -0
- spatial_vtk/visualize/fit.py +339 -0
- spatial_vtk/visualize/qc/__init__.py +37 -0
- spatial_vtk/visualize/qc/overview.py +384 -0
- spatial_vtk/visualize/qc/retention.py +535 -0
- spatial_vtk/visualize/qc/samples.py +118 -0
- spatial_vtk/visualize/record_sections.py +576 -0
- spatial_vtk/visualize/selection.py +615 -0
- spatial_vtk/visualize/waveforms/__init__.py +23 -0
- spatial_vtk/visualize/waveforms/comparison.py +14 -0
- spatial_vtk/visualize/waveforms/overlays.py +253 -0
- spatial_vtk/visualize/waveforms/radial_sections.py +212 -0
- spatial_vtk/visualize/waveforms/record_sections.py +32 -0
- spatial_vtk/visualize/waveforms/station_event.py +252 -0
- spatial_vtk-0.1.0.dist-info/METADATA +107 -0
- spatial_vtk-0.1.0.dist-info/RECORD +135 -0
- spatial_vtk-0.1.0.dist-info/WHEEL +5 -0
- spatial_vtk-0.1.0.dist-info/entry_points.txt +2 -0
- spatial_vtk-0.1.0.dist-info/licenses/LICENSE +28 -0
- spatial_vtk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
"""Command-line entry point for Spatial-VTK.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
This module exposes the public ``svtk`` command. The curated subcommands cover
|
|
6
|
+
file-based workflows that users commonly run outside notebooks, while
|
|
7
|
+
``svtk call`` provides a generic CLI path to any importable public Python
|
|
8
|
+
function.
|
|
9
|
+
|
|
10
|
+
Usage examples
|
|
11
|
+
--------------
|
|
12
|
+
Show active config:
|
|
13
|
+
``svtk config show --config spatial-vtk.yaml``
|
|
14
|
+
|
|
15
|
+
Prepare downstream metric outputs:
|
|
16
|
+
``svtk metrics outputs --metrics metrics.csv --output-dir outputs/metrics``
|
|
17
|
+
|
|
18
|
+
Run any public function with JSON/YAML arguments:
|
|
19
|
+
``svtk call spatial_vtk.config.labels.metric_display_name --args C5``
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import importlib
|
|
26
|
+
import inspect
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass, replace
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Iterable
|
|
31
|
+
|
|
32
|
+
import pandas as pd
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class PlotCommand:
|
|
38
|
+
"""One file-backed plotting command definition.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
function
|
|
43
|
+
Importable plotting function path.
|
|
44
|
+
primary_arg
|
|
45
|
+
Function argument populated from ``--input``.
|
|
46
|
+
help
|
|
47
|
+
Short command help text.
|
|
48
|
+
table_aliases
|
|
49
|
+
Convenience table options mapped to function argument names.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
PlotCommand
|
|
54
|
+
Immutable plotting command metadata.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
function: str
|
|
58
|
+
primary_arg: str | None
|
|
59
|
+
help: str
|
|
60
|
+
table_aliases: dict[str, str] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
METRICS_PLOT_COMMANDS: dict[str, PlotCommand] = {
|
|
64
|
+
"example-metric-pairs": PlotCommand("spatial_vtk.metrics.plot.example_metric_plots.plot_example_metric_pairs", None, "Plot synthetic trace-pair examples that illustrate metric behavior."),
|
|
65
|
+
"model-metric-heatmap": PlotCommand("spatial_vtk.metrics.plot.model_comparison.plot_model_metric_heatmap", "summary_df", "Plot a model-by-metric heatmap."),
|
|
66
|
+
"winner-heatmap": PlotCommand("spatial_vtk.metrics.plot.model_comparison.plot_winner_heatmap", "summary_df", "Plot a winner/class heatmap."),
|
|
67
|
+
"band-score-distribution": PlotCommand("spatial_vtk.metrics.plot.model_comparison.plot_band_score_distribution", "df", "Plot score distributions by passband."),
|
|
68
|
+
"psa-period-curve": PlotCommand("spatial_vtk.metrics.plot.periods.plot_psa_period_curve", "df", "Plot PSA values by period."),
|
|
69
|
+
"period-spectra": PlotCommand("spatial_vtk.metrics.plot.periods.plot_period_spectra", "spectra_df", "Plot period spectra."),
|
|
70
|
+
"period-spectrogram": PlotCommand("spatial_vtk.metrics.plot.periods.plot_period_spectrogram", "spectrogram_df", "Plot a period spectrogram."),
|
|
71
|
+
"vs30-scatter": PlotCommand("spatial_vtk.metrics.plot.site_terms.plot_vs30_scatter", "df", "Plot metric values against Vs30."),
|
|
72
|
+
"geology-boxplot": PlotCommand("spatial_vtk.metrics.plot.site_terms.plot_geology_boxplot", "df", "Plot metric values by geologic class."),
|
|
73
|
+
"metric-trend": PlotCommand("spatial_vtk.metrics.plot.trends.plot_metric_trend", "df", "Plot a general metric trend."),
|
|
74
|
+
"residuals-vs-distance": PlotCommand("spatial_vtk.metrics.plot.trends.plot_residuals_vs_distance", "df", "Plot residuals against distance."),
|
|
75
|
+
"residuals-vs-depth": PlotCommand("spatial_vtk.metrics.plot.trends.plot_residuals_vs_depth", "df", "Plot residuals against event depth."),
|
|
76
|
+
"score-trends": PlotCommand("spatial_vtk.metrics.plot.trends.plot_score_trends", "df", "Plot score trends."),
|
|
77
|
+
"phase-delay-vs-distance": PlotCommand("spatial_vtk.metrics.plot.trends.plot_phase_delay_vs_distance", "df", "Plot phase delay against distance."),
|
|
78
|
+
"scatterplot": PlotCommand("spatial_vtk.spatial.plot.metrics.scatterplot", "data", "Plot any metric-table variable against another variable."),
|
|
79
|
+
"boxplot": PlotCommand("spatial_vtk.spatial.plot.metrics.boxplot", "data", "Plot metric distributions by categorical variables."),
|
|
80
|
+
"heatmap": PlotCommand("spatial_vtk.spatial.plot.metrics.heatmap", "data", "Plot categorical metric summaries as a heatmap."),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
SPATIAL_PLOT_COMMANDS: dict[str, PlotCommand] = {
|
|
85
|
+
"correlogram": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_correlogram", "distance_df", "Plot a spatial correlogram."),
|
|
86
|
+
"semivariogram": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_semivariogram", "distance_df", "Plot a semivariogram."),
|
|
87
|
+
"directional-correlogram": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_directional_correlogram", "directional_df", "Plot directional spatial correlations.", table_aliases={"fit": "fit_df"}),
|
|
88
|
+
"block-holdout-scatter": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_block_holdout_scatter", "prediction_df", "Plot observed versus held-out predictions."),
|
|
89
|
+
"cluster-solution-scores": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_cluster_solution_scores", "score_df", "Plot clustering solution scores."),
|
|
90
|
+
"cluster-feature-heatmap": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_cluster_feature_heatmap", "feature_summary_df", "Plot cluster feature summaries."),
|
|
91
|
+
"pattern-similarity": PlotCommand("spatial_vtk.spatial.plot.correlation.plot_pattern_similarity", "stations", "Plot observed/synthetic pattern similarity."),
|
|
92
|
+
"azimuthal-residuals": PlotCommand("spatial_vtk.spatial.plot.metrics.plot_azimuthal_residuals", "df", "Plot residuals by azimuth."),
|
|
93
|
+
"path-bin-summary": PlotCommand("spatial_vtk.spatial.plot.metrics.plot_path_bin_summary", "path_summary_df", "Plot path-bin summary values."),
|
|
94
|
+
"residual-correlation": PlotCommand("spatial_vtk.spatial.plot.metrics.plot_residual_correlation", "correlation_df", "Plot residual correlation values."),
|
|
95
|
+
"polar-residuals": PlotCommand("spatial_vtk.spatial.plot.metrics.plot_polar_residuals", "df", "Plot residuals in polar coordinates."),
|
|
96
|
+
"pca-explained-variance": PlotCommand("spatial_vtk.spatial.plot.pca.plot_pca_explained_variance", "explained_variance_df", "Plot PCA explained variance."),
|
|
97
|
+
"pca-feature-loadings": PlotCommand("spatial_vtk.spatial.plot.pca.plot_pca_feature_loadings", "feature_loadings_df", "Plot PCA feature loadings."),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
SPATIAL_MAP_COMMANDS: dict[str, PlotCommand] = {
|
|
102
|
+
"station-bias": PlotCommand("spatial_vtk.spatial.map.correlation.plot_station_bias_map", "station_df", "Map station bias values."),
|
|
103
|
+
"cluster": PlotCommand("spatial_vtk.spatial.map.correlation.plot_cluster_map", "assignments_df", "Map cluster assignments."),
|
|
104
|
+
"redcap-cluster": PlotCommand("spatial_vtk.spatial.map.correlation.plot_redcap_cluster_map", "redcap_df", "Map REDCAP cluster values."),
|
|
105
|
+
"block-holdout-error": PlotCommand("spatial_vtk.spatial.map.correlation.plot_block_holdout_error_map", "prediction_df", "Map block-holdout prediction errors."),
|
|
106
|
+
"pca-mode": PlotCommand("spatial_vtk.spatial.map.pca.plot_pca_mode_map", "station_scores_df", "Map one PCA spatial mode."),
|
|
107
|
+
"station-metric": PlotCommand("spatial_vtk.spatial.map.metrics.plot_station_metric_map", "df", "Map station metric values."),
|
|
108
|
+
"score": PlotCommand("spatial_vtk.spatial.map.metrics.plot_score_map", "df", "Map score values."),
|
|
109
|
+
"residual-grid": PlotCommand("spatial_vtk.spatial.map.metrics.plot_residual_grid", "grid_df", "Map residual grid values."),
|
|
110
|
+
"metric-by-model": PlotCommand("spatial_vtk.spatial.map.metrics.plot_metric_map_by_model", "df", "Map metric values by model."),
|
|
111
|
+
"model-improvement": PlotCommand("spatial_vtk.spatial.map.metrics.plot_model_improvement_map", "df", "Map model improvement values."),
|
|
112
|
+
"event-residual": PlotCommand("spatial_vtk.spatial.map.path.plot_event_residual_map", "df", "Map event residual paths."),
|
|
113
|
+
"corridor": PlotCommand("spatial_vtk.spatial.map.path.plot_corridor_map", "corridors_df", "Map corridor selections.", table_aliases={"stations": "stations_df", "events": "events_df", "records": "records_df"}),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
CONTEXT_VISUALIZE_COMMANDS: dict[str, PlotCommand] = {
|
|
118
|
+
"station-event-context": PlotCommand("spatial_vtk.visualize.context.plot_station_event_context", "stations_df", "Plot station and event context.", table_aliases={"events": "events_df"}),
|
|
119
|
+
"study-domain": PlotCommand("spatial_vtk.visualize.context.plot_study_domain_map", "stations_df", "Plot the study domain map.", table_aliases={"events": "events_df"}),
|
|
120
|
+
"station-coverage": PlotCommand("spatial_vtk.visualize.context.plot_station_coverage", "event_station_df", "Plot station record coverage."),
|
|
121
|
+
"event-coverage": PlotCommand("spatial_vtk.visualize.context.plot_event_coverage", "event_station_df", "Plot event record coverage."),
|
|
122
|
+
"record-coverage": PlotCommand("spatial_vtk.visualize.context.plot_record_coverage", "records_df", "Plot record-window coverage."),
|
|
123
|
+
"event-trace-comparison": PlotCommand("spatial_vtk.visualize.context.plot_event_trace_comparison", "records_df", "Plot event trace comparisons."),
|
|
124
|
+
"distance-amplitude-diagnostics": PlotCommand("spatial_vtk.visualize.context.plot_distance_amplitude_diagnostics", "records_df", "Plot distance/amplitude diagnostics."),
|
|
125
|
+
"event-magnitude-map": PlotCommand("spatial_vtk.visualize.context.plot_event_magnitude_map", "events_df", "Map events by magnitude."),
|
|
126
|
+
"station-event-network": PlotCommand("spatial_vtk.visualize.context.plot_station_event_network_map", "stations_df", "Map station/event network geometry.", table_aliases={"events": "events_df"}),
|
|
127
|
+
"station-event-beachball": PlotCommand("spatial_vtk.visualize.context.plot_station_event_beachball_map", "events_df", "Map station/event context with beachballs.", table_aliases={"stations": "stations_df"}),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
QC_VISUALIZE_COMMANDS: dict[str, PlotCommand] = {
|
|
132
|
+
"trace-inventory-samples": PlotCommand("spatial_vtk.visualize.qc.plot_trace_inventory_samples", "sample_df", "Plot sample QC traces."),
|
|
133
|
+
"retention-summary": PlotCommand("spatial_vtk.visualize.qc.plot_retention_summary", "qc_df", "Plot QC retention summary."),
|
|
134
|
+
"data-synthetic-availability": PlotCommand("spatial_vtk.visualize.qc.plot_data_synthetic_availability", "availability_df", "Plot observed/synthetic availability."),
|
|
135
|
+
"event-station-retention": PlotCommand("spatial_vtk.visualize.qc.plot_event_station_retention_heatmap", "retention_df", "Plot retained comparison-pair percentages by station and event."),
|
|
136
|
+
"post-qc-station-event-map": PlotCommand("spatial_vtk.visualize.qc.plot_post_qc_station_event_map", "records_df", "Map retained station/event records after QC."),
|
|
137
|
+
"drop-cause-diagnostics": PlotCommand("spatial_vtk.visualize.qc.plot_qc_drop_cause_diagnostics", "qc_df", "Plot QC drop-cause diagnostics."),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
WAVEFORM_VISUALIZE_COMMANDS: dict[str, PlotCommand] = {
|
|
142
|
+
"record-section": PlotCommand("spatial_vtk.visualize.waveforms.plot_record_section", "records", "Plot a waveform record section."),
|
|
143
|
+
"observed-synthetic-record-section": PlotCommand("spatial_vtk.visualize.waveforms.plot_observed_synthetic_record_section", "records_df", "Plot observed/synthetic record sections."),
|
|
144
|
+
"waveform-overlay-matrix": PlotCommand("spatial_vtk.visualize.waveforms.plot_waveform_overlay_matrix", "records_df", "Plot waveform overlay matrix."),
|
|
145
|
+
"event-radial-trace-section": PlotCommand("spatial_vtk.visualize.waveforms.plot_event_radial_trace_section", "records_df", "Plot event radial trace section."),
|
|
146
|
+
"station-event-waveform-map": PlotCommand("spatial_vtk.visualize.waveforms.plot_station_event_waveform_map", "records_df", "Map station/event waveforms."),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
PLOT_COMMAND_GROUPS: dict[str, dict[str, PlotCommand]] = {
|
|
151
|
+
"metrics": METRICS_PLOT_COMMANDS,
|
|
152
|
+
"spatial": SPATIAL_PLOT_COMMANDS,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
MAP_COMMAND_GROUPS: dict[str, dict[str, PlotCommand]] = {
|
|
157
|
+
"spatial": SPATIAL_MAP_COMMANDS,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
VISUALIZE_COMMAND_GROUPS: dict[str, dict[str, PlotCommand]] = {
|
|
162
|
+
"context": CONTEXT_VISUALIZE_COMMANDS,
|
|
163
|
+
"qc": QC_VISUALIZE_COMMANDS,
|
|
164
|
+
"waveforms": WAVEFORM_VISUALIZE_COMMANDS,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
AUTO_PLOT_OPTION_KEYS = frozenset({"add_basemap", "basemap_source", "bounds"})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main(argv: list[str] | None = None) -> int:
|
|
172
|
+
"""Run the public ``svtk`` command.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
argv
|
|
177
|
+
Optional command-line arguments without the program name.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
int
|
|
182
|
+
Process-style exit code.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
parser = build_parser()
|
|
186
|
+
args = parser.parse_args(argv)
|
|
187
|
+
if getattr(args, "version", False):
|
|
188
|
+
from spatial_vtk import __version__
|
|
189
|
+
|
|
190
|
+
print(__version__)
|
|
191
|
+
return 0
|
|
192
|
+
if not hasattr(args, "handler"):
|
|
193
|
+
parser.print_help()
|
|
194
|
+
return 0
|
|
195
|
+
return int(args.handler(args) or 0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
199
|
+
"""Build the top-level command parser.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
None
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
argparse.ArgumentParser
|
|
208
|
+
Configured CLI parser.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
parser = argparse.ArgumentParser(
|
|
212
|
+
prog="svtk",
|
|
213
|
+
description="Spatial validation tools for ground-motion simulations.",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument("--version", action="store_true", help="Print the package version and exit.")
|
|
216
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
217
|
+
_add_config_commands(subparsers)
|
|
218
|
+
_add_io_commands(subparsers)
|
|
219
|
+
_add_qc_commands(subparsers)
|
|
220
|
+
_add_metrics_commands(subparsers)
|
|
221
|
+
_add_plot_commands(subparsers)
|
|
222
|
+
_add_map_commands(subparsers)
|
|
223
|
+
_add_visualize_commands(subparsers)
|
|
224
|
+
_add_dashboard_commands(subparsers)
|
|
225
|
+
_add_call_command(subparsers)
|
|
226
|
+
return parser
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _add_config_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
230
|
+
"""Register configuration CLI commands.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
subparsers
|
|
235
|
+
Top-level argparse subparser collection.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
None
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
config = subparsers.add_parser("config", help="Inspect Spatial-VTK configuration.")
|
|
243
|
+
config_sub = config.add_subparsers(dest="config_command", required=True)
|
|
244
|
+
|
|
245
|
+
find = config_sub.add_parser("find", help="Print the resolved config path.")
|
|
246
|
+
find.add_argument("--config", default=None, help="Explicit config file.")
|
|
247
|
+
find.add_argument("--start-dir", default=None, help="Directory used for config discovery.")
|
|
248
|
+
find.set_defaults(handler=_cmd_config_find)
|
|
249
|
+
|
|
250
|
+
show = config_sub.add_parser("show", help="Print the active config or one section.")
|
|
251
|
+
show.add_argument("--config", default=None, help="Explicit config file.")
|
|
252
|
+
show.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay before printing.")
|
|
253
|
+
show.add_argument("--section", default=None, help="Optional dotted section key.")
|
|
254
|
+
show.add_argument("--json", action="store_true", help="Write JSON instead of YAML.")
|
|
255
|
+
show.set_defaults(handler=_cmd_config_show)
|
|
256
|
+
|
|
257
|
+
bounds = config_sub.add_parser("bounds", help="List configured named bounds presets.")
|
|
258
|
+
bounds.add_argument("--config", default=None, help="Explicit config file.")
|
|
259
|
+
bounds.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay before listing bounds.")
|
|
260
|
+
bounds.add_argument("--json", action="store_true", help="Write JSON instead of YAML.")
|
|
261
|
+
bounds.set_defaults(handler=_cmd_config_bounds)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _add_io_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
265
|
+
"""Register input/output CLI commands."""
|
|
266
|
+
|
|
267
|
+
io = subparsers.add_parser("io", help="Prepare metadata and input inventories.")
|
|
268
|
+
io_sub = io.add_subparsers(dest="io_command", required=True)
|
|
269
|
+
|
|
270
|
+
stations = io_sub.add_parser("prepare-stations", help="Normalize station metadata column names.")
|
|
271
|
+
stations.add_argument("--input", required=True, help="Station CSV/parquet path.")
|
|
272
|
+
stations.add_argument("--output", required=True, help="Output CSV/parquet path.")
|
|
273
|
+
stations.set_defaults(handler=_cmd_io_prepare_stations)
|
|
274
|
+
|
|
275
|
+
events = io_sub.add_parser("prepare-events", help="Normalize event metadata column names.")
|
|
276
|
+
events.add_argument("--input", required=True, help="Event CSV/parquet path.")
|
|
277
|
+
events.add_argument("--output", required=True, help="Output CSV/parquet path.")
|
|
278
|
+
events.set_defaults(handler=_cmd_io_prepare_events)
|
|
279
|
+
|
|
280
|
+
master_stations = io_sub.add_parser("master-stations", help="Build a master station list from one or more tables.")
|
|
281
|
+
master_stations.add_argument("--input", nargs="+", required=True, help="Station CSV/parquet paths.")
|
|
282
|
+
master_stations.add_argument("--output", required=True, help="Output CSV path.")
|
|
283
|
+
master_stations.set_defaults(handler=_cmd_io_master_stations)
|
|
284
|
+
|
|
285
|
+
master_events = io_sub.add_parser("master-events", help="Build a master event list from one or more tables.")
|
|
286
|
+
master_events.add_argument("--input", nargs="+", required=True, help="Event CSV/parquet paths.")
|
|
287
|
+
master_events.add_argument("--output", required=True, help="Output CSV path.")
|
|
288
|
+
master_events.set_defaults(handler=_cmd_io_master_events)
|
|
289
|
+
|
|
290
|
+
inventory = io_sub.add_parser("inventory", help="Build a lightweight observed/synthetic file inventory.")
|
|
291
|
+
inventory.add_argument("--observed-root", required=True, help="Observed waveform root directory.")
|
|
292
|
+
inventory.add_argument("--synthetic-root", required=True, help="Synthetic waveform root directory.")
|
|
293
|
+
inventory.add_argument("--output", required=True, help="Output CSV/parquet path.")
|
|
294
|
+
inventory.add_argument("--suffix", action="append", default=None, help="Waveform suffix to include. May be repeated.")
|
|
295
|
+
inventory.add_argument("--relative-to", default=None, help="Base path used for relative inventory paths.")
|
|
296
|
+
inventory.add_argument("--no-sha256", action="store_true", help="Skip SHA-256 hashing.")
|
|
297
|
+
inventory.set_defaults(handler=_cmd_io_inventory)
|
|
298
|
+
|
|
299
|
+
preprocess = io_sub.add_parser("preprocess-waveforms", help="Filter/resample waveform files and write reusable processed copies.")
|
|
300
|
+
preprocess.add_argument("--records", required=True, help="Event-station CSV/parquet with waveform path columns.")
|
|
301
|
+
preprocess.add_argument(
|
|
302
|
+
"--output-root",
|
|
303
|
+
default=None,
|
|
304
|
+
help="Folder where processed waveforms and metadata tables are written. Defaults to outputs.preprocessed_waveforms from config.",
|
|
305
|
+
)
|
|
306
|
+
preprocess.add_argument("--config", default=None, help="Spatial-VTK config file.")
|
|
307
|
+
preprocess.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay.")
|
|
308
|
+
preprocess.add_argument("--observed-column", default=None, help="Observed waveform path column. Auto-detected when omitted.")
|
|
309
|
+
preprocess.add_argument("--synthetic-column", default=None, help="Synthetic waveform path column. Auto-detected when omitted.")
|
|
310
|
+
preprocess.add_argument("--event-id-col", default="event_id", help="Event ID column in --records.")
|
|
311
|
+
preprocess.add_argument("--lowpass-hz", type=float, default=None, help="Optional lowpass cutoff in Hz.")
|
|
312
|
+
preprocess.add_argument("--highpass-hz", type=float, default=None, help="Optional highpass cutoff in Hz.")
|
|
313
|
+
preprocess.add_argument("--bandpass-low-hz", type=float, default=None, help="Optional bandpass low corner in Hz.")
|
|
314
|
+
preprocess.add_argument("--bandpass-high-hz", type=float, default=None, help="Optional bandpass high corner in Hz.")
|
|
315
|
+
preprocess.add_argument("--resample-hz", type=float, default=None, help="Optional target sampling rate in Hz.")
|
|
316
|
+
preprocess.add_argument("--filter-order", type=int, default=None, help="Butterworth filter order.")
|
|
317
|
+
preprocess.add_argument("--overwrite", action="store_true", help="Rewrite processed files even if they already exist.")
|
|
318
|
+
preprocess.add_argument("--continue-on-error", action="store_true", help="Record failed files in the manifest instead of stopping.")
|
|
319
|
+
preprocess.add_argument("--keep-input-columns", action="store_true", help="Keep original waveform path columns pointed at raw files.")
|
|
320
|
+
preprocess.set_defaults(handler=_cmd_io_preprocess_waveforms)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _add_qc_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
324
|
+
"""Register quality-control CLI commands."""
|
|
325
|
+
|
|
326
|
+
qc = subparsers.add_parser("qc", help="Prepare QC review outputs.")
|
|
327
|
+
qc_sub = qc.add_subparsers(dest="qc_command", required=True)
|
|
328
|
+
|
|
329
|
+
queue = qc_sub.add_parser("manual-queue", help="Export a manual-QC review queue from trace summary rows.")
|
|
330
|
+
queue.add_argument("--trace-summary", required=True, help="Trace-summary CSV/parquet path.")
|
|
331
|
+
queue.add_argument("--output", required=True, help="Output manual-review queue CSV.")
|
|
332
|
+
queue.add_argument("--event-id", default="", help="Optional event id filter.")
|
|
333
|
+
queue.add_argument("--station-family", default="all", help="Optional station-family filter.")
|
|
334
|
+
queue.add_argument("--component", default="all", help="Optional component filter.")
|
|
335
|
+
queue.add_argument("--station-contains", default="", help="Optional station substring filter.")
|
|
336
|
+
queue.add_argument("--band", default=None, help="Optional passband filter.")
|
|
337
|
+
queue.set_defaults(handler=_cmd_qc_manual_queue)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _add_metrics_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
341
|
+
"""Register metric workflow CLI commands."""
|
|
342
|
+
|
|
343
|
+
metrics = subparsers.add_parser("metrics", help="Plan, run, and post-process metric calculations.")
|
|
344
|
+
metrics_sub = metrics.add_subparsers(dest="metrics_command", required=True)
|
|
345
|
+
|
|
346
|
+
plan = metrics_sub.add_parser("plan", help="Plan metric tasks from inventories and config.")
|
|
347
|
+
plan.add_argument("--observed-inventory", default=None, help="Observed metric waveform inventory.")
|
|
348
|
+
plan.add_argument("--synthetic-inventory", default=None, help="Synthetic metric waveform inventory.")
|
|
349
|
+
plan.add_argument("--config", default=None, help="Spatial-VTK config file.")
|
|
350
|
+
plan.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay.")
|
|
351
|
+
plan.add_argument("--metric", action="append", dest="metrics", default=None, help="Metric override. Repeat or use 'all'.")
|
|
352
|
+
plan.add_argument("--metric-group", action="append", dest="metric_groups", default=None, help="Metric-group override. Repeat or use 'all'.")
|
|
353
|
+
plan.add_argument("--component", action="append", dest="components", default=None, help="Component override. Repeat for multiple components.")
|
|
354
|
+
plan.add_argument("--passband", action="append", dest="passbands", default=None, help="Period passband override, such as 1-2. Repeat for multiple bands.")
|
|
355
|
+
plan.add_argument("--model", action="append", dest="models", default=None, help="Synthetic model override. Repeat for multiple models.")
|
|
356
|
+
plan.add_argument("--transform", action="append", dest="transforms", default=None, help="Metric transform override. Repeat for multiple transforms.")
|
|
357
|
+
plan.add_argument("--output-mode", default=None, help="Metric output mode override.")
|
|
358
|
+
plan.add_argument("--output", required=True, help="Output task table or manifest path.")
|
|
359
|
+
plan.add_argument("--manifest", action="store_true", help="Write a JSON manifest instead of a task table.")
|
|
360
|
+
plan.add_argument("--batch-output-dir", default=None, help="Batch output directory when writing a manifest.")
|
|
361
|
+
plan.add_argument("--batch-size", type=int, default=100, help="Tasks per batch when writing a manifest.")
|
|
362
|
+
plan.add_argument("--qc-table", default=None, help="Optional QC inventory recorded in a manifest.")
|
|
363
|
+
plan.add_argument("--no-qc", action="store_true", help="Do not mark planned tasks as QC-filtered by default.")
|
|
364
|
+
plan.set_defaults(handler=_cmd_metrics_plan)
|
|
365
|
+
|
|
366
|
+
run = metrics_sub.add_parser("run", help="Run a task table locally.")
|
|
367
|
+
run.add_argument("--tasks", required=True, help="Task CSV/parquet path.")
|
|
368
|
+
run.add_argument("--output", required=True, help="Output metric CSV/parquet path.")
|
|
369
|
+
run.add_argument("--qc-table", default=None, help="Optional QC inventory.")
|
|
370
|
+
run.set_defaults(handler=_cmd_metrics_run)
|
|
371
|
+
|
|
372
|
+
batch = metrics_sub.add_parser("run-batch", help="Run one batch from a metric manifest.")
|
|
373
|
+
batch.add_argument("--manifest", required=True, help="Metric workflow manifest JSON.")
|
|
374
|
+
batch.add_argument("--batch-index", type=int, required=True, help="Batch index to run.")
|
|
375
|
+
batch.add_argument("--overwrite", action="store_true", help="Replace an existing batch output.")
|
|
376
|
+
batch.set_defaults(handler=_cmd_metrics_run_batch)
|
|
377
|
+
|
|
378
|
+
merge = metrics_sub.add_parser("merge-batches", help="Merge metric manifest batch outputs.")
|
|
379
|
+
merge.add_argument("--manifest", required=True, help="Metric workflow manifest JSON.")
|
|
380
|
+
merge.add_argument("--output", required=True, help="Merged output CSV/parquet path.")
|
|
381
|
+
merge.add_argument("--allow-missing", action="store_true", help="Allow missing batch outputs.")
|
|
382
|
+
merge.set_defaults(handler=_cmd_metrics_merge_batches)
|
|
383
|
+
|
|
384
|
+
outputs = metrics_sub.add_parser("outputs", help="Write standard downstream metric outputs.")
|
|
385
|
+
outputs.add_argument("--metrics", required=True, help="Metric workflow rows CSV/parquet path.")
|
|
386
|
+
outputs.add_argument("--output-dir", required=True, help="Output directory.")
|
|
387
|
+
outputs.add_argument("--events", default=None, help="Optional event metadata CSV/parquet path.")
|
|
388
|
+
outputs.add_argument("--stations", default=None, help="Optional station metadata CSV/parquet path.")
|
|
389
|
+
outputs.add_argument("--residual-column", default=None, help="Column exposed as canonical residual.")
|
|
390
|
+
outputs.add_argument("--score-column", default=None, help="Column exposed as canonical score.")
|
|
391
|
+
outputs.add_argument("--format", choices=("parquet", "csv"), default="parquet", help="Table output format.")
|
|
392
|
+
outputs.add_argument("--dashboard-partitioned", action="store_true", help="Partition dashboard metric rows.")
|
|
393
|
+
outputs.set_defaults(handler=_cmd_metrics_outputs)
|
|
394
|
+
|
|
395
|
+
slurm = metrics_sub.add_parser("slurm", help="Write a SLURM array script for a metric manifest.")
|
|
396
|
+
slurm.add_argument("--manifest", required=True, help="Metric workflow manifest JSON.")
|
|
397
|
+
slurm.add_argument("--output", required=True, help="Output SLURM script path.")
|
|
398
|
+
slurm.add_argument("--config", required=True, help="Config file containing metrics.slurm settings.")
|
|
399
|
+
slurm.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay.")
|
|
400
|
+
slurm.set_defaults(handler=_cmd_metrics_slurm)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _add_dashboard_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
404
|
+
"""Register dashboard CLI commands."""
|
|
405
|
+
|
|
406
|
+
dashboard = subparsers.add_parser("dashboard", help="Prepare and launch Streamlit dashboards.")
|
|
407
|
+
dashboard_sub = dashboard.add_subparsers(dest="dashboard_command", required=True)
|
|
408
|
+
|
|
409
|
+
metrics = dashboard_sub.add_parser("metrics", help="Launch the metrics Streamlit dashboard.")
|
|
410
|
+
metrics.add_argument("--metrics-root", required=True, help="Dashboard metric dataset root.")
|
|
411
|
+
metrics.add_argument("--summary-root", required=True, help="Dashboard summary dataset root.")
|
|
412
|
+
metrics.add_argument("--port", type=int, default=8501, help="Streamlit server port.")
|
|
413
|
+
metrics.add_argument("--address", default="127.0.0.1", help="Streamlit server address.")
|
|
414
|
+
metrics.add_argument("--show", action="store_true", help="Open Streamlit in a browser when supported.")
|
|
415
|
+
metrics.set_defaults(handler=_cmd_dashboard_metrics)
|
|
416
|
+
|
|
417
|
+
qc = dashboard_sub.add_parser("qc", help="Launch the QC Streamlit dashboard.")
|
|
418
|
+
qc.add_argument("--trace-summary", required=True, help="Trace-summary CSV/parquet path.")
|
|
419
|
+
qc.add_argument("--port", type=int, default=8502, help="Streamlit server port.")
|
|
420
|
+
qc.add_argument("--address", default="127.0.0.1", help="Streamlit server address.")
|
|
421
|
+
qc.add_argument("--show", action="store_true", help="Open Streamlit in a browser when supported.")
|
|
422
|
+
qc.set_defaults(handler=_cmd_dashboard_qc)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _add_plot_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
426
|
+
"""Register non-map plotting CLI commands."""
|
|
427
|
+
|
|
428
|
+
plot = subparsers.add_parser("plot", help="Create static metric and spatial plots.")
|
|
429
|
+
plot_sub = plot.add_subparsers(dest="plot_group", required=True)
|
|
430
|
+
_add_registered_command_group(plot_sub, "metrics", METRICS_PLOT_COMMANDS, "Metric plots.")
|
|
431
|
+
_add_registered_command_group(plot_sub, "spatial", SPATIAL_PLOT_COMMANDS, "Spatial-statistics plots.")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _add_map_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
435
|
+
"""Register map-producing CLI commands."""
|
|
436
|
+
|
|
437
|
+
map_parser = subparsers.add_parser("map", help="Create static map figures.")
|
|
438
|
+
map_sub = map_parser.add_subparsers(dest="map_group", required=True)
|
|
439
|
+
_add_registered_command_group(map_sub, "spatial", SPATIAL_MAP_COMMANDS, "Spatial map figures.", include_map_options=True)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _add_visualize_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
443
|
+
"""Register higher-level visualization CLI commands."""
|
|
444
|
+
|
|
445
|
+
visualize = subparsers.add_parser("visualize", help="Create context, QC, and waveform figures.")
|
|
446
|
+
visualize_sub = visualize.add_subparsers(dest="visualize_group", required=True)
|
|
447
|
+
_add_registered_command_group(visualize_sub, "context", CONTEXT_VISUALIZE_COMMANDS, "Context figures and maps.", include_map_options=True)
|
|
448
|
+
_add_registered_command_group(visualize_sub, "qc", QC_VISUALIZE_COMMANDS, "QC and retention figures.", include_map_options=True)
|
|
449
|
+
_add_registered_command_group(visualize_sub, "waveforms", WAVEFORM_VISUALIZE_COMMANDS, "Waveform figures and maps.", include_map_options=True)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _add_registered_command_group(
|
|
453
|
+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
454
|
+
group_name: str,
|
|
455
|
+
commands: dict[str, PlotCommand],
|
|
456
|
+
help_text: str,
|
|
457
|
+
*,
|
|
458
|
+
include_map_options: bool = False,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Register one group of registry-backed figure commands."""
|
|
461
|
+
|
|
462
|
+
group = subparsers.add_parser(group_name, help=help_text)
|
|
463
|
+
group_sub = group.add_subparsers(dest=f"{group_name}_figure", required=True)
|
|
464
|
+
list_cmd = group_sub.add_parser("list", help="List available figure commands in this group.")
|
|
465
|
+
list_cmd.set_defaults(handler=_cmd_list_registered_plots, registry=commands)
|
|
466
|
+
for command_name, spec in sorted(commands.items()):
|
|
467
|
+
command = group_sub.add_parser(command_name, help=spec.help)
|
|
468
|
+
_add_figure_io_arguments(command, spec, include_map_options=include_map_options)
|
|
469
|
+
command.set_defaults(handler=_cmd_registered_plot, plot_spec=spec)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _add_figure_io_arguments(parser: argparse.ArgumentParser, spec: PlotCommand, *, include_map_options: bool) -> None:
|
|
473
|
+
"""Add shared file-backed plotting arguments."""
|
|
474
|
+
|
|
475
|
+
if spec.primary_arg is not None:
|
|
476
|
+
parser.add_argument("--input", required=True, help=f"Input CSV/parquet table for the {spec.primary_arg} argument.")
|
|
477
|
+
parser.add_argument("--output", required=True, help="Output figure path.")
|
|
478
|
+
parser.add_argument("--table", action="append", default=(), help="Extra table as argument_name=path. May be repeated.")
|
|
479
|
+
parser.add_argument("--kwargs", nargs="*", default=(), help="Extra function keyword arguments as key=value.")
|
|
480
|
+
parser.add_argument("--kwargs-json", default=None, help="Extra function keyword arguments as a JSON/YAML mapping.")
|
|
481
|
+
for option in sorted((spec.table_aliases or {}).keys()):
|
|
482
|
+
parser.add_argument(f"--{option.replace('_', '-')}", default=None, help=f"Convenience table path for the {spec.table_aliases[option]} argument.")
|
|
483
|
+
if include_map_options:
|
|
484
|
+
parser.add_argument("--config", default=None, help="Optional Spatial-VTK config for named bounds.")
|
|
485
|
+
parser.add_argument("--run-scenario", default=None, help="Apply one named run_scenarios overlay.")
|
|
486
|
+
parser.add_argument("--bounds", default=None, help="Named bounds from config or comma-separated lon_min,lon_max,lat_min,lat_max.")
|
|
487
|
+
parser.add_argument("--no-basemap", action="store_true", help="Disable basemap rendering for map figures.")
|
|
488
|
+
parser.add_argument("--basemap-source", default=None, help="Optional contextily basemap source.")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _add_call_command(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
492
|
+
"""Register the generic Python-call CLI command."""
|
|
493
|
+
|
|
494
|
+
call = subparsers.add_parser("call", help="Call any importable Spatial-VTK Python function.")
|
|
495
|
+
call.add_argument("function", help="Import path, for example spatial_vtk.config.labels.metric_display_name.")
|
|
496
|
+
call.add_argument("--args", nargs="*", default=(), help="Positional arguments parsed as YAML scalars/sequences.")
|
|
497
|
+
call.add_argument("--args-json", default=None, help="JSON/YAML list of positional arguments.")
|
|
498
|
+
call.add_argument("--kwargs", nargs="*", default=(), help="Keyword arguments as key=value, parsed as YAML values.")
|
|
499
|
+
call.add_argument("--kwargs-json", default=None, help="JSON/YAML mapping of keyword arguments.")
|
|
500
|
+
call.add_argument("--output", default=None, help="Optional output path for DataFrame/dict/list results.")
|
|
501
|
+
call.set_defaults(handler=_cmd_call)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _cmd_config_find(args: argparse.Namespace) -> int:
|
|
505
|
+
"""Run ``svtk config find``."""
|
|
506
|
+
|
|
507
|
+
from spatial_vtk.config import find_config_file
|
|
508
|
+
|
|
509
|
+
path = find_config_file(args.config, start_dir=args.start_dir)
|
|
510
|
+
print(path or "")
|
|
511
|
+
return 0 if path is not None else 1
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _cmd_config_show(args: argparse.Namespace) -> int:
|
|
515
|
+
"""Run ``svtk config show``."""
|
|
516
|
+
|
|
517
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
518
|
+
|
|
519
|
+
config = SpatialVTKConfig.from_file(args.config, run_scenario=args.run_scenario)
|
|
520
|
+
payload = config.section(args.section) if args.section else config.data
|
|
521
|
+
_print_payload(payload, as_json=args.json)
|
|
522
|
+
return 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _cmd_config_bounds(args: argparse.Namespace) -> int:
|
|
526
|
+
"""Run ``svtk config bounds``."""
|
|
527
|
+
|
|
528
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
529
|
+
|
|
530
|
+
config = SpatialVTKConfig.from_file(args.config, run_scenario=args.run_scenario)
|
|
531
|
+
_print_payload(config.bounds_presets(), as_json=args.json)
|
|
532
|
+
return 0
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _cmd_io_prepare_stations(args: argparse.Namespace) -> int:
|
|
536
|
+
"""Run ``svtk io prepare-stations``."""
|
|
537
|
+
|
|
538
|
+
from spatial_vtk.io import prepare_station_metadata
|
|
539
|
+
|
|
540
|
+
_write_table(prepare_station_metadata(_read_table(args.input)), args.output)
|
|
541
|
+
return 0
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _cmd_io_prepare_events(args: argparse.Namespace) -> int:
|
|
545
|
+
"""Run ``svtk io prepare-events``."""
|
|
546
|
+
|
|
547
|
+
from spatial_vtk.io import prepare_event_metadata
|
|
548
|
+
|
|
549
|
+
_write_table(prepare_event_metadata(_read_table(args.input)), args.output)
|
|
550
|
+
return 0
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _cmd_io_master_stations(args: argparse.Namespace) -> int:
|
|
554
|
+
"""Run ``svtk io master-stations``."""
|
|
555
|
+
|
|
556
|
+
from spatial_vtk.io import build_master_station_list, write_master_station_list
|
|
557
|
+
|
|
558
|
+
write_master_station_list(build_master_station_list(station_tables=args.input), args.output)
|
|
559
|
+
return 0
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _cmd_io_master_events(args: argparse.Namespace) -> int:
|
|
563
|
+
"""Run ``svtk io master-events``."""
|
|
564
|
+
|
|
565
|
+
from spatial_vtk.io import build_master_event_list, write_master_event_list
|
|
566
|
+
|
|
567
|
+
write_master_event_list(build_master_event_list(event_tables=args.input), args.output)
|
|
568
|
+
return 0
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _cmd_io_inventory(args: argparse.Namespace) -> int:
|
|
572
|
+
"""Run ``svtk io inventory``."""
|
|
573
|
+
|
|
574
|
+
from spatial_vtk.io import DEFAULT_WAVEFORM_SUFFIXES, build_observed_synthetic_inventory
|
|
575
|
+
|
|
576
|
+
suffixes = args.suffix or sorted(DEFAULT_WAVEFORM_SUFFIXES)
|
|
577
|
+
df = build_observed_synthetic_inventory(
|
|
578
|
+
args.observed_root,
|
|
579
|
+
args.synthetic_root,
|
|
580
|
+
suffixes=suffixes,
|
|
581
|
+
relative_to=args.relative_to,
|
|
582
|
+
include_sha256=not args.no_sha256,
|
|
583
|
+
)
|
|
584
|
+
_write_table(df, args.output)
|
|
585
|
+
return 0
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _cmd_io_preprocess_waveforms(args: argparse.Namespace) -> int:
|
|
589
|
+
"""Run ``svtk io preprocess-waveforms``."""
|
|
590
|
+
|
|
591
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
592
|
+
from spatial_vtk.io import preprocess_waveform_files, waveform_preprocessing_from_config
|
|
593
|
+
|
|
594
|
+
config = SpatialVTKConfig.from_file(args.config, run_scenario=args.run_scenario) if args.config or args.run_scenario else None
|
|
595
|
+
settings = waveform_preprocessing_from_config(config)
|
|
596
|
+
overrides = {
|
|
597
|
+
"lowpass_hz": args.lowpass_hz,
|
|
598
|
+
"highpass_hz": args.highpass_hz,
|
|
599
|
+
"bandpass_low_hz": args.bandpass_low_hz,
|
|
600
|
+
"bandpass_high_hz": args.bandpass_high_hz,
|
|
601
|
+
"resample_hz": args.resample_hz,
|
|
602
|
+
"filter_order": args.filter_order,
|
|
603
|
+
}
|
|
604
|
+
explicit = {key: value for key, value in overrides.items() if value is not None}
|
|
605
|
+
if explicit:
|
|
606
|
+
settings = replace(settings, **explicit)
|
|
607
|
+
source_columns: dict[str, str] = {}
|
|
608
|
+
if args.observed_column:
|
|
609
|
+
source_columns["observed"] = args.observed_column
|
|
610
|
+
if args.synthetic_column:
|
|
611
|
+
source_columns["synthetic"] = args.synthetic_column
|
|
612
|
+
result = preprocess_waveform_files(
|
|
613
|
+
args.records,
|
|
614
|
+
args.output_root,
|
|
615
|
+
source_columns=source_columns or None,
|
|
616
|
+
preprocessing=settings,
|
|
617
|
+
config=config,
|
|
618
|
+
event_id_col=args.event_id_col,
|
|
619
|
+
overwrite=args.overwrite,
|
|
620
|
+
continue_on_error=args.continue_on_error,
|
|
621
|
+
replace_input_columns=not args.keep_input_columns,
|
|
622
|
+
)
|
|
623
|
+
_print_payload(
|
|
624
|
+
{
|
|
625
|
+
"event_station_records": str(result.event_station_path),
|
|
626
|
+
"manifest": str(result.manifest_path),
|
|
627
|
+
"trace_metadata": str(result.trace_metadata_path),
|
|
628
|
+
"files": int(len(result.manifest)),
|
|
629
|
+
},
|
|
630
|
+
as_json=False,
|
|
631
|
+
)
|
|
632
|
+
return 0
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _cmd_qc_manual_queue(args: argparse.Namespace) -> int:
|
|
636
|
+
"""Run ``svtk qc manual-queue``."""
|
|
637
|
+
|
|
638
|
+
from spatial_vtk.visualize.dashboard import filter_qc_dashboard_rows, write_manual_review_queue
|
|
639
|
+
from spatial_vtk.visualize.qc import load_trace_qc_summary
|
|
640
|
+
|
|
641
|
+
df = load_trace_qc_summary(args.trace_summary)
|
|
642
|
+
filtered = filter_qc_dashboard_rows(
|
|
643
|
+
df,
|
|
644
|
+
event_filter=args.event_id,
|
|
645
|
+
station_family=args.station_family,
|
|
646
|
+
component_filter=args.component,
|
|
647
|
+
station_query=args.station_contains,
|
|
648
|
+
band=args.band,
|
|
649
|
+
)
|
|
650
|
+
write_manual_review_queue(filtered, args.output)
|
|
651
|
+
return 0
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _cmd_metrics_plan(args: argparse.Namespace) -> int:
|
|
655
|
+
"""Run ``svtk metrics plan``."""
|
|
656
|
+
|
|
657
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
658
|
+
from spatial_vtk.io import metric_plan_from_config
|
|
659
|
+
from spatial_vtk.metrics.workflow import plan_metric_tasks, tasks_to_frame, write_task_manifest
|
|
660
|
+
|
|
661
|
+
config = SpatialVTKConfig.from_file(args.config, run_scenario=args.run_scenario)
|
|
662
|
+
plan = metric_plan_from_config(config, command="metrics.calculate", overrides=_metric_plan_overrides(args))
|
|
663
|
+
tasks = plan_metric_tasks(args.observed_inventory, args.synthetic_inventory, plan=plan, use_qc=not args.no_qc)
|
|
664
|
+
if args.manifest:
|
|
665
|
+
batch_dir = args.batch_output_dir or str(Path(args.output).with_suffix("")) + "_batches"
|
|
666
|
+
write_task_manifest(tasks, args.output, output_dir=batch_dir, batch_size=args.batch_size, qc_table=args.qc_table)
|
|
667
|
+
else:
|
|
668
|
+
_write_table(tasks_to_frame(tasks), args.output)
|
|
669
|
+
print(f"Planned {len(tasks)} metric tasks.")
|
|
670
|
+
return 0
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _metric_plan_overrides(args: argparse.Namespace) -> dict[str, Any]:
|
|
674
|
+
"""Build explicit metric-plan config overrides from CLI flags."""
|
|
675
|
+
|
|
676
|
+
overrides: dict[str, Any] = {}
|
|
677
|
+
if getattr(args, "metrics", None):
|
|
678
|
+
overrides["metrics"] = args.metrics
|
|
679
|
+
if getattr(args, "metric_groups", None):
|
|
680
|
+
overrides["groups"] = args.metric_groups
|
|
681
|
+
if getattr(args, "components", None):
|
|
682
|
+
overrides["components"] = args.components
|
|
683
|
+
if getattr(args, "passbands", None):
|
|
684
|
+
overrides["passbands"] = args.passbands
|
|
685
|
+
if getattr(args, "models", None):
|
|
686
|
+
overrides["models"] = args.models
|
|
687
|
+
if getattr(args, "transforms", None):
|
|
688
|
+
overrides["transforms"] = args.transforms
|
|
689
|
+
if getattr(args, "output_mode", None):
|
|
690
|
+
overrides["output_mode"] = args.output_mode
|
|
691
|
+
return overrides
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _cmd_metrics_run(args: argparse.Namespace) -> int:
|
|
695
|
+
"""Run ``svtk metrics run``."""
|
|
696
|
+
|
|
697
|
+
from spatial_vtk.metrics.workflow import run_metric_tasks, tasks_from_frame, write_metric_rows
|
|
698
|
+
|
|
699
|
+
tasks = tasks_from_frame(args.tasks)
|
|
700
|
+
rows = run_metric_tasks(tasks, qc_table=args.qc_table)
|
|
701
|
+
write_metric_rows(rows, args.output)
|
|
702
|
+
print(f"Wrote {len(rows)} metric rows.")
|
|
703
|
+
return 0
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _cmd_metrics_run_batch(args: argparse.Namespace) -> int:
|
|
707
|
+
"""Run ``svtk metrics run-batch``."""
|
|
708
|
+
|
|
709
|
+
from spatial_vtk.metrics.workflow import run_manifest_batch
|
|
710
|
+
|
|
711
|
+
path = run_manifest_batch(args.manifest, batch_index=args.batch_index, overwrite=args.overwrite)
|
|
712
|
+
print(path)
|
|
713
|
+
return 0
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _cmd_metrics_merge_batches(args: argparse.Namespace) -> int:
|
|
717
|
+
"""Run ``svtk metrics merge-batches``."""
|
|
718
|
+
|
|
719
|
+
from spatial_vtk.metrics.workflow import merge_batch_outputs
|
|
720
|
+
|
|
721
|
+
path = merge_batch_outputs(args.manifest, args.output, require_all=not args.allow_missing)
|
|
722
|
+
print(path)
|
|
723
|
+
return 0
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _cmd_metrics_outputs(args: argparse.Namespace) -> int:
|
|
727
|
+
"""Run ``svtk metrics outputs``."""
|
|
728
|
+
|
|
729
|
+
from spatial_vtk.metrics.workflow import write_metric_outputs
|
|
730
|
+
|
|
731
|
+
written = write_metric_outputs(
|
|
732
|
+
args.metrics,
|
|
733
|
+
args.output_dir,
|
|
734
|
+
events=args.events,
|
|
735
|
+
stations=args.stations,
|
|
736
|
+
residual_column=args.residual_column,
|
|
737
|
+
score_column=args.score_column,
|
|
738
|
+
table_format=args.format,
|
|
739
|
+
dashboard_partitioned=args.dashboard_partitioned,
|
|
740
|
+
)
|
|
741
|
+
_print_payload({key: str(path) for key, path in written.items()}, as_json=False)
|
|
742
|
+
return 0
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _cmd_metrics_slurm(args: argparse.Namespace) -> int:
|
|
746
|
+
"""Run ``svtk metrics slurm``."""
|
|
747
|
+
|
|
748
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
749
|
+
from spatial_vtk.metrics.workflow import slurm_settings_from_config, write_metrics_slurm_script
|
|
750
|
+
|
|
751
|
+
settings = slurm_settings_from_config(SpatialVTKConfig.from_file(args.config, run_scenario=args.run_scenario))
|
|
752
|
+
path = write_metrics_slurm_script(args.manifest, args.output, settings)
|
|
753
|
+
print(path)
|
|
754
|
+
return 0
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _cmd_dashboard_metrics(args: argparse.Namespace) -> int:
|
|
758
|
+
"""Run ``svtk dashboard metrics``."""
|
|
759
|
+
|
|
760
|
+
from spatial_vtk.visualize.dashboard import launch_metrics_dashboard
|
|
761
|
+
|
|
762
|
+
process = launch_metrics_dashboard(metrics_root=args.metrics_root, summary_root=args.summary_root, server_address=args.address, server_port=args.port, show=args.show)
|
|
763
|
+
print(f"Metrics dashboard running at http://{args.address}:{args.port} (pid {process.pid})")
|
|
764
|
+
return 0
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _cmd_dashboard_qc(args: argparse.Namespace) -> int:
|
|
768
|
+
"""Run ``svtk dashboard qc``."""
|
|
769
|
+
|
|
770
|
+
from spatial_vtk.visualize.dashboard import launch_qc_dashboard
|
|
771
|
+
|
|
772
|
+
process = launch_qc_dashboard(trace_summary=args.trace_summary, server_address=args.address, server_port=args.port, show=args.show)
|
|
773
|
+
print(f"QC dashboard running at http://{args.address}:{args.port} (pid {process.pid})")
|
|
774
|
+
return 0
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _cmd_list_registered_plots(args: argparse.Namespace) -> int:
|
|
778
|
+
"""List available registered plotting commands."""
|
|
779
|
+
|
|
780
|
+
for name, spec in sorted(args.registry.items()):
|
|
781
|
+
input_note = f" --input <table>" if spec.primary_arg is not None else ""
|
|
782
|
+
print(f"{name}{input_note} --output <path> # {spec.help}")
|
|
783
|
+
return 0
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _cmd_registered_plot(args: argparse.Namespace) -> int:
|
|
787
|
+
"""Run one registry-backed plotting command."""
|
|
788
|
+
|
|
789
|
+
spec: PlotCommand = args.plot_spec
|
|
790
|
+
function = _resolve_function(spec.function)
|
|
791
|
+
kwargs = _registered_plot_kwargs(args, spec)
|
|
792
|
+
_drop_unsupported_auto_plot_kwargs(function, kwargs)
|
|
793
|
+
result = function(**kwargs)
|
|
794
|
+
if result is not None and str(result) != str(kwargs["output_path"]):
|
|
795
|
+
print(result)
|
|
796
|
+
else:
|
|
797
|
+
print(kwargs["output_path"])
|
|
798
|
+
return 0
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _cmd_call(args: argparse.Namespace) -> int:
|
|
802
|
+
"""Run ``svtk call``."""
|
|
803
|
+
|
|
804
|
+
function = _resolve_function(args.function)
|
|
805
|
+
positional = list(_parse_sequence(args.args_json)) if args.args_json else [_parse_value(item) for item in args.args]
|
|
806
|
+
kwargs = dict(_parse_mapping(args.kwargs_json)) if args.kwargs_json else {}
|
|
807
|
+
kwargs.update(_parse_key_values(args.kwargs))
|
|
808
|
+
result = function(*positional, **kwargs)
|
|
809
|
+
if args.output:
|
|
810
|
+
_write_result(result, args.output)
|
|
811
|
+
else:
|
|
812
|
+
_print_result(result)
|
|
813
|
+
return 0
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _registered_plot_kwargs(args: argparse.Namespace, spec: PlotCommand) -> dict[str, Any]:
|
|
817
|
+
"""Build plotting keyword arguments from CLI table and scalar options."""
|
|
818
|
+
|
|
819
|
+
kwargs: dict[str, Any] = {"output_path": args.output}
|
|
820
|
+
if spec.primary_arg is not None:
|
|
821
|
+
kwargs[spec.primary_arg] = _read_table(args.input)
|
|
822
|
+
for table_arg, table_path in _parse_table_arguments(getattr(args, "table", ())):
|
|
823
|
+
kwargs[table_arg] = _read_table(table_path)
|
|
824
|
+
for option, table_arg in (spec.table_aliases or {}).items():
|
|
825
|
+
value = getattr(args, option.replace("-", "_"), None)
|
|
826
|
+
if value:
|
|
827
|
+
kwargs[table_arg] = _read_table(value)
|
|
828
|
+
if getattr(args, "kwargs_json", None):
|
|
829
|
+
kwargs.update(_parse_mapping(args.kwargs_json))
|
|
830
|
+
kwargs.update(_parse_key_values(getattr(args, "kwargs", ())))
|
|
831
|
+
if hasattr(args, "no_basemap") and args.no_basemap:
|
|
832
|
+
kwargs["add_basemap"] = False
|
|
833
|
+
if getattr(args, "basemap_source", None):
|
|
834
|
+
kwargs["basemap_source"] = args.basemap_source
|
|
835
|
+
bounds = _resolve_cli_bounds(
|
|
836
|
+
getattr(args, "bounds", None),
|
|
837
|
+
getattr(args, "config", None),
|
|
838
|
+
getattr(args, "run_scenario", None),
|
|
839
|
+
)
|
|
840
|
+
if bounds is not None:
|
|
841
|
+
kwargs["bounds"] = bounds
|
|
842
|
+
return kwargs
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _parse_table_arguments(items: Iterable[str]) -> list[tuple[str, str]]:
|
|
846
|
+
"""Parse repeated ``argument=path`` table options."""
|
|
847
|
+
|
|
848
|
+
parsed: list[tuple[str, str]] = []
|
|
849
|
+
for item in items:
|
|
850
|
+
key, separator, value = str(item).partition("=")
|
|
851
|
+
if not separator or not key or not value:
|
|
852
|
+
raise ValueError(f"Expected --table argument_name=path, got: {item!r}")
|
|
853
|
+
parsed.append((key, value))
|
|
854
|
+
return parsed
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _resolve_cli_bounds(
|
|
858
|
+
value: str | None,
|
|
859
|
+
config_path: str | None,
|
|
860
|
+
run_scenario: str | None = None,
|
|
861
|
+
) -> tuple[float, float, float, float] | None:
|
|
862
|
+
"""Resolve CLI bounds from a config keyword or explicit extent."""
|
|
863
|
+
|
|
864
|
+
if not value:
|
|
865
|
+
return None
|
|
866
|
+
raw = str(value).strip()
|
|
867
|
+
parts = [part.strip() for part in raw.split(",")]
|
|
868
|
+
if len(parts) == 4:
|
|
869
|
+
try:
|
|
870
|
+
return tuple(float(part) for part in parts) # type: ignore[return-value]
|
|
871
|
+
except ValueError:
|
|
872
|
+
pass
|
|
873
|
+
from spatial_vtk.config import SpatialVTKConfig
|
|
874
|
+
|
|
875
|
+
config = (
|
|
876
|
+
SpatialVTKConfig.from_file(config_path, run_scenario=run_scenario)
|
|
877
|
+
if config_path
|
|
878
|
+
else SpatialVTKConfig.empty(root_dir=".")
|
|
879
|
+
)
|
|
880
|
+
bounds = config.resolve_bounds(raw)
|
|
881
|
+
if bounds is None:
|
|
882
|
+
raise ValueError(f"Could not resolve bounds: {value!r}")
|
|
883
|
+
return bounds
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _drop_unsupported_auto_plot_kwargs(function: Any, kwargs: dict[str, Any]) -> None:
|
|
887
|
+
"""Remove automatic map options from functions that do not accept them."""
|
|
888
|
+
|
|
889
|
+
signature = inspect.signature(function)
|
|
890
|
+
if any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()):
|
|
891
|
+
return
|
|
892
|
+
accepted = set(signature.parameters)
|
|
893
|
+
for key in list(AUTO_PLOT_OPTION_KEYS):
|
|
894
|
+
if key in kwargs and key not in accepted:
|
|
895
|
+
kwargs.pop(key)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _resolve_function(path: str):
|
|
899
|
+
"""Resolve an importable function path.
|
|
900
|
+
|
|
901
|
+
Parameters
|
|
902
|
+
----------
|
|
903
|
+
path
|
|
904
|
+
Dotted function path.
|
|
905
|
+
|
|
906
|
+
Returns
|
|
907
|
+
-------
|
|
908
|
+
callable
|
|
909
|
+
Imported function or class.
|
|
910
|
+
"""
|
|
911
|
+
|
|
912
|
+
if not path.startswith("spatial_vtk."):
|
|
913
|
+
raise ValueError("svtk call only accepts import paths under spatial_vtk.")
|
|
914
|
+
module_name, _, attr_name = path.rpartition(".")
|
|
915
|
+
if not module_name or not attr_name:
|
|
916
|
+
raise ValueError("Function path must include a module and attribute name.")
|
|
917
|
+
module = importlib.import_module(module_name)
|
|
918
|
+
target = getattr(module, attr_name)
|
|
919
|
+
if not callable(target):
|
|
920
|
+
raise TypeError(f"Import path is not callable: {path}")
|
|
921
|
+
return target
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _read_table(path: str | Path) -> pd.DataFrame:
|
|
925
|
+
"""Read one CSV or Parquet table."""
|
|
926
|
+
|
|
927
|
+
table_path = Path(path).expanduser()
|
|
928
|
+
if table_path.suffix.lower() in {".parquet", ".pq"}:
|
|
929
|
+
return pd.read_parquet(table_path)
|
|
930
|
+
return pd.read_csv(table_path)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _write_table(df: pd.DataFrame, path: str | Path) -> Path:
|
|
934
|
+
"""Write one CSV or Parquet table."""
|
|
935
|
+
|
|
936
|
+
output = Path(path).expanduser()
|
|
937
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
938
|
+
if output.suffix.lower() in {".parquet", ".pq"}:
|
|
939
|
+
df.to_parquet(output, index=False)
|
|
940
|
+
else:
|
|
941
|
+
df.to_csv(output, index=False)
|
|
942
|
+
print(output)
|
|
943
|
+
return output
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _write_result(result: Any, output: str | Path) -> None:
|
|
947
|
+
"""Write a generic command result to disk."""
|
|
948
|
+
|
|
949
|
+
path = Path(output).expanduser()
|
|
950
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
951
|
+
if isinstance(result, pd.DataFrame):
|
|
952
|
+
_write_table(result, path)
|
|
953
|
+
elif isinstance(result, (dict, list, tuple)):
|
|
954
|
+
if path.suffix.lower() in {".yaml", ".yml"}:
|
|
955
|
+
path.write_text(yaml.safe_dump(_jsonable(result), sort_keys=False), encoding="utf-8")
|
|
956
|
+
else:
|
|
957
|
+
path.write_text(json.dumps(_jsonable(result), indent=2), encoding="utf-8")
|
|
958
|
+
print(path)
|
|
959
|
+
elif hasattr(result, "savefig"):
|
|
960
|
+
result.savefig(path, bbox_inches="tight")
|
|
961
|
+
print(path)
|
|
962
|
+
elif hasattr(result, "write_html"):
|
|
963
|
+
result.write_html(path)
|
|
964
|
+
print(path)
|
|
965
|
+
else:
|
|
966
|
+
path.write_text(str(result), encoding="utf-8")
|
|
967
|
+
print(path)
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _print_result(result: Any) -> None:
|
|
971
|
+
"""Print a generic command result."""
|
|
972
|
+
|
|
973
|
+
if isinstance(result, pd.DataFrame):
|
|
974
|
+
print(result.to_csv(index=False))
|
|
975
|
+
elif isinstance(result, (dict, list, tuple)):
|
|
976
|
+
print(json.dumps(_jsonable(result), indent=2))
|
|
977
|
+
else:
|
|
978
|
+
print(result)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _print_payload(payload: Any, *, as_json: bool) -> None:
|
|
982
|
+
"""Print a mapping/list payload as YAML or JSON."""
|
|
983
|
+
|
|
984
|
+
if as_json:
|
|
985
|
+
print(json.dumps(_jsonable(payload), indent=2))
|
|
986
|
+
else:
|
|
987
|
+
print(yaml.safe_dump(_jsonable(payload), sort_keys=False).strip())
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _parse_sequence(value: str) -> list[Any]:
|
|
991
|
+
"""Parse a JSON/YAML CLI sequence."""
|
|
992
|
+
|
|
993
|
+
parsed = yaml.safe_load(value)
|
|
994
|
+
if parsed is None:
|
|
995
|
+
return []
|
|
996
|
+
if not isinstance(parsed, list):
|
|
997
|
+
raise ValueError("--args-json must parse to a list.")
|
|
998
|
+
return parsed
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _parse_mapping(value: str) -> dict[str, Any]:
|
|
1002
|
+
"""Parse a JSON/YAML CLI mapping."""
|
|
1003
|
+
|
|
1004
|
+
parsed = yaml.safe_load(value)
|
|
1005
|
+
if parsed is None:
|
|
1006
|
+
return {}
|
|
1007
|
+
if not isinstance(parsed, dict):
|
|
1008
|
+
raise ValueError("--kwargs-json must parse to a mapping.")
|
|
1009
|
+
return dict(parsed)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _parse_key_values(items: Iterable[str]) -> dict[str, Any]:
|
|
1013
|
+
"""Parse key=value CLI arguments."""
|
|
1014
|
+
|
|
1015
|
+
parsed: dict[str, Any] = {}
|
|
1016
|
+
for item in items:
|
|
1017
|
+
key, separator, value = str(item).partition("=")
|
|
1018
|
+
if not separator or not key:
|
|
1019
|
+
raise ValueError(f"Expected key=value argument, got: {item!r}")
|
|
1020
|
+
parsed[key] = _parse_value(value)
|
|
1021
|
+
return parsed
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _parse_value(value: str) -> Any:
|
|
1025
|
+
"""Parse one YAML scalar/list/dict value from CLI text."""
|
|
1026
|
+
|
|
1027
|
+
try:
|
|
1028
|
+
return yaml.safe_load(value)
|
|
1029
|
+
except yaml.YAMLError:
|
|
1030
|
+
return value
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _jsonable(value: Any) -> Any:
|
|
1034
|
+
"""Convert common Python objects into JSON/YAML-safe values."""
|
|
1035
|
+
|
|
1036
|
+
if isinstance(value, Path):
|
|
1037
|
+
return str(value)
|
|
1038
|
+
if isinstance(value, pd.DataFrame):
|
|
1039
|
+
return value.to_dict(orient="records")
|
|
1040
|
+
if isinstance(value, pd.Series):
|
|
1041
|
+
return value.to_dict()
|
|
1042
|
+
if isinstance(value, dict):
|
|
1043
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
1044
|
+
if isinstance(value, (list, tuple, set)):
|
|
1045
|
+
return [_jsonable(item) for item in value]
|
|
1046
|
+
if inspect.isclass(value) or inspect.isfunction(value):
|
|
1047
|
+
return f"{value.__module__}.{value.__name__}"
|
|
1048
|
+
if hasattr(value, "item"):
|
|
1049
|
+
try:
|
|
1050
|
+
return value.item()
|
|
1051
|
+
except Exception:
|
|
1052
|
+
pass
|
|
1053
|
+
return value
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
__all__ = ["build_parser", "main"]
|