xcheck 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xcheck-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: xcheck
3
+ Version: 0.1.0
4
+ Summary: A package for cross-checking radargram layers using dot product similarity.
5
+ Author-email: Your Name <your.email@example.com>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering :: Physics
11
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: numpy
15
+ Requires-Dist: matplotlib
16
+ Requires-Dist: scipy
17
+ Requires-Dist: pandas
18
+ Requires-Dist: cartopy
19
+
20
+ # XCheck — Consistency-Based Validation of Englacial Layer Annotations
21
+
22
+ A Python implementation of the radargram layer-matching framework from:
23
+
24
+ > Hassan, Muhammad Behroze; Tama, Bayu Adhi; Purushotham, Sanjay;
25
+ > Janeja, Vandana P. **XCheck: A Consistency-Based Validation Framework
26
+ > for Englacial Layers Annotations.**
27
+ > <https://par.nsf.gov/biblio/10673248> ·
28
+ > DOI: [10.1109/ICDMW69685.2025.00015](https://doi.org/10.1109/ICDMW69685.2025.00015)
29
+
30
+ XCheck validates whether annotated englacial layers are *consistent*
31
+ between two radargrams that observe the same ice — either two
32
+ **consecutive** flight segments (overlap found automatically via GPS time)
33
+ or two **intersecting** flight paths (crossing point supplied via a CSV).
34
+
35
+ ---
36
+
37
+ ## Features
38
+
39
+ - **GPS Overlap Detection**: Automatically finds temporally overlapping segments between two radargrams using log-transformed GPS timestamps.
40
+ - **Data Loading & Masking**: Loads `.mat` radargram and ground truth layer files and converts annotated layers into binary depth masks.
41
+ - **Geometric Alignment**: Adjusts radargram columns by surface elevation before comparison to correct for topographic variation.
42
+ - **Layer Continuity Matching**: Dot-product similarity algorithm that identifies corresponding layers between two radargrams at their crossing or overlap point.
43
+ - **Flexible Input**: Accepts a single radargram pair via CLI flags, a batch CSV of pairs, or a pre-computed intersections CSV.
44
+ - **Visualization**: Map plots of GPS overlap regions and side-by-side mask views with matched layer annotations.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install xcheck
52
+ ```
53
+
54
+ ### NDH_PythonTools (required)
55
+
56
+ XCheck uses [NDH_PythonTools](https://github.com/nholschuh/NDH_PythonTools)
57
+ for `.mat` file loading and radar data processing. Because that repository
58
+ is not a standard pip package, it must be installed manually:
59
+
60
+ ```bash
61
+ git clone https://github.com/nholschuh/NDH_PythonTools.git
62
+ ```
63
+
64
+ Then add its **parent directory** to `PYTHONPATH` before running xcheck:
65
+
66
+ ```bash
67
+ # Linux / macOS
68
+ export PYTHONPATH="/path/to/parent/of/NDH_PythonTools"
69
+
70
+ # Windows PowerShell
71
+ $env:PYTHONPATH = "C:\path\to\parent\of\NDH_PythonTools"
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Quick Start
77
+
78
+ ### Layer matching only (no NDH_PythonTools needed)
79
+
80
+ ```python
81
+ import numpy as np
82
+ from xcheck import match_layers_with_dot_product
83
+
84
+ m1 = np.zeros((6, 5)); m1[[1, 3, 5], :] = 1
85
+ m2 = np.zeros((6, 5)); m2[[1, 3, 5], :] = 1
86
+
87
+ matches = match_layers_with_dot_product(m1, m2, max_distance=1,
88
+ window_size=3, dp_threshold=1)
89
+ print(matches) # [(1, 1), (3, 3), (5, 5)]
90
+ ```
91
+
92
+ ### Python API (requires NDH_PythonTools + data)
93
+
94
+ ```python
95
+ from xcheck import (
96
+ load_and_process_radargram,
97
+ load_log_gps_time,
98
+ find_overlapping_intervals,
99
+ extract_and_adjust_mask_columns_for_location,
100
+ match_layers_with_dot_product,
101
+ )
102
+
103
+ # Load two consecutive radargrams
104
+ r1, m1, d1 = load_and_process_radargram("Data_20120429_01_026.mat",
105
+ layer_dir="./Nick-layer-data-mat/",
106
+ radar_dir="./Nick-raw-radargram-mat/")
107
+ r2, m2, d2 = load_and_process_radargram("Data_20120429_01_027.mat",
108
+ layer_dir="./Nick-layer-data-mat/",
109
+ radar_dir="./Nick-raw-radargram-mat/")
110
+
111
+ # Find GPS overlap and derive crossing point from midpoint
112
+ import numpy as np
113
+ log_t1 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_026.mat")
114
+ log_t2 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_027.mat")
115
+ overlaps1, overlaps2 = find_overlapping_intervals(log_t1, log_t2)
116
+
117
+ for iv1, iv2 in zip(overlaps1, overlaps2):
118
+ mid1 = (iv1[0] + iv1[1]) // 2
119
+ mid2 = (iv2[0] + iv2[1]) // 2
120
+ mc1 = extract_and_adjust_mask_columns_for_location(
121
+ r1['Longitude'][mid1], r1['Latitude'][mid1], r1, m1, d1, 20)
122
+ mc2 = extract_and_adjust_mask_columns_for_location(
123
+ r2['Longitude'][mid2], r2['Latitude'][mid2], r2, m2, d2, 20)
124
+ matches = match_layers_with_dot_product(mc1, mc2)
125
+ print(f"Matched layers: {matches}")
126
+ ```
127
+
128
+ ---
129
+
130
+ ## CLI
131
+
132
+ ### Single pair — consecutive (no crossing CSV needed)
133
+
134
+ ```bash
135
+ xcheck --r1 Data_20120429_01_026.mat --r2 Data_20120429_01_027.mat \
136
+ --layer_dir ./Nick-layer-data-mat/ \
137
+ --radar_dir ./Nick-raw-radargram-mat/
138
+ ```
139
+
140
+ ### Single pair — intersecting (crossing looked up from CSV)
141
+
142
+ ```bash
143
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
144
+ --intersections_csv ./Intersections_2012.csv \
145
+ --layer_dir ./Nick-layer-data-mat/ \
146
+ --radar_dir ./Nick-raw-radargram-mat/
147
+ ```
148
+
149
+ ### Single pair — intersecting with explicit crossing coordinates
150
+
151
+ ```bash
152
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
153
+ --lat 78.909416 --lon -61.804612 \
154
+ --layer_dir ./Nick-layer-data-mat/ \
155
+ --radar_dir ./Nick-raw-radargram-mat/
156
+ ```
157
+
158
+ ### Batch mode (all pairs in a CSV)
159
+
160
+ ```bash
161
+ xcheck --intersections_csv ./Intersections_2012.csv \
162
+ --layer_dir ./Nick-layer-data-mat/ \
163
+ --radar_dir ./Nick-raw-radargram-mat/
164
+ ```
165
+
166
+ ### All CLI arguments
167
+
168
+ | Argument | Default | Description |
169
+ |---|---|---|
170
+ | `--r1`, `--r2` | — | Single-pair mode: filenames of the two radargrams |
171
+ | `--lat`, `--lon` | — | Crossing coordinates for single-pair mode |
172
+ | `--csv_path` | — | Batch CSV with columns `Radargram 1`, `Radargram 2`, `Latitude`, `Longitude` |
173
+ | `--intersections_csv` | — | Pre-computed intersections CSV (used alone or to look up coordinates) |
174
+ | `--layer_dir` | required | Directory of layer `.mat` files |
175
+ | `--radar_dir` | required | Directory of raw radargram `.mat` files |
176
+ | `--layer_prefix` | `Layer_` | Filename prefix for layer files |
177
+ | `--cols_side` | 20 / 3 | Columns extracted each side of crossing (consecutive / non-consecutive) |
178
+ | `--max_distance` | 4 / 3 | Max depth-pixel gap between candidate layer rows |
179
+ | `--window_size` | 7 / 3 | Dot product window width (must be odd) |
180
+ | `--dp_threshold` | 4 / 1 | Minimum dot product score to confirm a match |
181
+ | `--plot_overlaps` | off | Plot GPS overlap regions on a map |
182
+
183
+ Default pairs shown as **consecutive / non-consecutive**.
184
+
185
+ ---
186
+
187
+ ## Default thresholds by radargram type
188
+
189
+ | Parameter | Consecutive | Non-consecutive |
190
+ |---|---|---|
191
+ | `cols_side` | 20 (41 columns) | 3 (7 columns) |
192
+ | `max_distance` | 4 px | 3 px |
193
+ | `window_size` | 7 | 3 |
194
+ | `dp_threshold` | 4 | 1 |
195
+
196
+ These are selected automatically. Override any of them via CLI flags.
197
+
198
+ ---
199
+
200
+ ## Methodological note
201
+
202
+ The GPS overlap detection compares **`log(GPS_time)`** between two
203
+ radargrams with an absolute tolerance of `1e-8`. Because
204
+ `d(log t) ≈ dt/t` and GPS times are ~1×10⁹ seconds, this tolerance
205
+ corresponds to matching raw timestamps within roughly 10 seconds.
206
+ Consecutive flight segments (e.g. `_026.mat` → `_027.mat`) share a
207
+ small tail/head of overlapping traces, which is where the crossing point
208
+ is derived. Non-consecutive intersecting radargrams do not share GPS
209
+ timestamps; their crossing point must be supplied via `--intersections_csv`
210
+ or `--lat`/`--lon`.
211
+
212
+ ---
213
+
214
+ ## Project structure
215
+
216
+ ```
217
+ src/xcheck/
218
+ xcheck.py Core algorithms: loading, masking, GPS overlap,
219
+ column extraction, layer matching, plotting, CLI
220
+ __init__.py Public API exports
221
+ pyproject.toml Build metadata and dependencies
222
+ README.md
223
+ ```
224
+
225
+ ---
226
+
227
+ ## License
228
+
229
+ MIT
xcheck-0.1.0/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # XCheck — Consistency-Based Validation of Englacial Layer Annotations
2
+
3
+ A Python implementation of the radargram layer-matching framework from:
4
+
5
+ > Hassan, Muhammad Behroze; Tama, Bayu Adhi; Purushotham, Sanjay;
6
+ > Janeja, Vandana P. **XCheck: A Consistency-Based Validation Framework
7
+ > for Englacial Layers Annotations.**
8
+ > <https://par.nsf.gov/biblio/10673248> ·
9
+ > DOI: [10.1109/ICDMW69685.2025.00015](https://doi.org/10.1109/ICDMW69685.2025.00015)
10
+
11
+ XCheck validates whether annotated englacial layers are *consistent*
12
+ between two radargrams that observe the same ice — either two
13
+ **consecutive** flight segments (overlap found automatically via GPS time)
14
+ or two **intersecting** flight paths (crossing point supplied via a CSV).
15
+
16
+ ---
17
+
18
+ ## Features
19
+
20
+ - **GPS Overlap Detection**: Automatically finds temporally overlapping segments between two radargrams using log-transformed GPS timestamps.
21
+ - **Data Loading & Masking**: Loads `.mat` radargram and ground truth layer files and converts annotated layers into binary depth masks.
22
+ - **Geometric Alignment**: Adjusts radargram columns by surface elevation before comparison to correct for topographic variation.
23
+ - **Layer Continuity Matching**: Dot-product similarity algorithm that identifies corresponding layers between two radargrams at their crossing or overlap point.
24
+ - **Flexible Input**: Accepts a single radargram pair via CLI flags, a batch CSV of pairs, or a pre-computed intersections CSV.
25
+ - **Visualization**: Map plots of GPS overlap regions and side-by-side mask views with matched layer annotations.
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install xcheck
33
+ ```
34
+
35
+ ### NDH_PythonTools (required)
36
+
37
+ XCheck uses [NDH_PythonTools](https://github.com/nholschuh/NDH_PythonTools)
38
+ for `.mat` file loading and radar data processing. Because that repository
39
+ is not a standard pip package, it must be installed manually:
40
+
41
+ ```bash
42
+ git clone https://github.com/nholschuh/NDH_PythonTools.git
43
+ ```
44
+
45
+ Then add its **parent directory** to `PYTHONPATH` before running xcheck:
46
+
47
+ ```bash
48
+ # Linux / macOS
49
+ export PYTHONPATH="/path/to/parent/of/NDH_PythonTools"
50
+
51
+ # Windows PowerShell
52
+ $env:PYTHONPATH = "C:\path\to\parent\of\NDH_PythonTools"
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ### Layer matching only (no NDH_PythonTools needed)
60
+
61
+ ```python
62
+ import numpy as np
63
+ from xcheck import match_layers_with_dot_product
64
+
65
+ m1 = np.zeros((6, 5)); m1[[1, 3, 5], :] = 1
66
+ m2 = np.zeros((6, 5)); m2[[1, 3, 5], :] = 1
67
+
68
+ matches = match_layers_with_dot_product(m1, m2, max_distance=1,
69
+ window_size=3, dp_threshold=1)
70
+ print(matches) # [(1, 1), (3, 3), (5, 5)]
71
+ ```
72
+
73
+ ### Python API (requires NDH_PythonTools + data)
74
+
75
+ ```python
76
+ from xcheck import (
77
+ load_and_process_radargram,
78
+ load_log_gps_time,
79
+ find_overlapping_intervals,
80
+ extract_and_adjust_mask_columns_for_location,
81
+ match_layers_with_dot_product,
82
+ )
83
+
84
+ # Load two consecutive radargrams
85
+ r1, m1, d1 = load_and_process_radargram("Data_20120429_01_026.mat",
86
+ layer_dir="./Nick-layer-data-mat/",
87
+ radar_dir="./Nick-raw-radargram-mat/")
88
+ r2, m2, d2 = load_and_process_radargram("Data_20120429_01_027.mat",
89
+ layer_dir="./Nick-layer-data-mat/",
90
+ radar_dir="./Nick-raw-radargram-mat/")
91
+
92
+ # Find GPS overlap and derive crossing point from midpoint
93
+ import numpy as np
94
+ log_t1 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_026.mat")
95
+ log_t2 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_027.mat")
96
+ overlaps1, overlaps2 = find_overlapping_intervals(log_t1, log_t2)
97
+
98
+ for iv1, iv2 in zip(overlaps1, overlaps2):
99
+ mid1 = (iv1[0] + iv1[1]) // 2
100
+ mid2 = (iv2[0] + iv2[1]) // 2
101
+ mc1 = extract_and_adjust_mask_columns_for_location(
102
+ r1['Longitude'][mid1], r1['Latitude'][mid1], r1, m1, d1, 20)
103
+ mc2 = extract_and_adjust_mask_columns_for_location(
104
+ r2['Longitude'][mid2], r2['Latitude'][mid2], r2, m2, d2, 20)
105
+ matches = match_layers_with_dot_product(mc1, mc2)
106
+ print(f"Matched layers: {matches}")
107
+ ```
108
+
109
+ ---
110
+
111
+ ## CLI
112
+
113
+ ### Single pair — consecutive (no crossing CSV needed)
114
+
115
+ ```bash
116
+ xcheck --r1 Data_20120429_01_026.mat --r2 Data_20120429_01_027.mat \
117
+ --layer_dir ./Nick-layer-data-mat/ \
118
+ --radar_dir ./Nick-raw-radargram-mat/
119
+ ```
120
+
121
+ ### Single pair — intersecting (crossing looked up from CSV)
122
+
123
+ ```bash
124
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
125
+ --intersections_csv ./Intersections_2012.csv \
126
+ --layer_dir ./Nick-layer-data-mat/ \
127
+ --radar_dir ./Nick-raw-radargram-mat/
128
+ ```
129
+
130
+ ### Single pair — intersecting with explicit crossing coordinates
131
+
132
+ ```bash
133
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
134
+ --lat 78.909416 --lon -61.804612 \
135
+ --layer_dir ./Nick-layer-data-mat/ \
136
+ --radar_dir ./Nick-raw-radargram-mat/
137
+ ```
138
+
139
+ ### Batch mode (all pairs in a CSV)
140
+
141
+ ```bash
142
+ xcheck --intersections_csv ./Intersections_2012.csv \
143
+ --layer_dir ./Nick-layer-data-mat/ \
144
+ --radar_dir ./Nick-raw-radargram-mat/
145
+ ```
146
+
147
+ ### All CLI arguments
148
+
149
+ | Argument | Default | Description |
150
+ |---|---|---|
151
+ | `--r1`, `--r2` | — | Single-pair mode: filenames of the two radargrams |
152
+ | `--lat`, `--lon` | — | Crossing coordinates for single-pair mode |
153
+ | `--csv_path` | — | Batch CSV with columns `Radargram 1`, `Radargram 2`, `Latitude`, `Longitude` |
154
+ | `--intersections_csv` | — | Pre-computed intersections CSV (used alone or to look up coordinates) |
155
+ | `--layer_dir` | required | Directory of layer `.mat` files |
156
+ | `--radar_dir` | required | Directory of raw radargram `.mat` files |
157
+ | `--layer_prefix` | `Layer_` | Filename prefix for layer files |
158
+ | `--cols_side` | 20 / 3 | Columns extracted each side of crossing (consecutive / non-consecutive) |
159
+ | `--max_distance` | 4 / 3 | Max depth-pixel gap between candidate layer rows |
160
+ | `--window_size` | 7 / 3 | Dot product window width (must be odd) |
161
+ | `--dp_threshold` | 4 / 1 | Minimum dot product score to confirm a match |
162
+ | `--plot_overlaps` | off | Plot GPS overlap regions on a map |
163
+
164
+ Default pairs shown as **consecutive / non-consecutive**.
165
+
166
+ ---
167
+
168
+ ## Default thresholds by radargram type
169
+
170
+ | Parameter | Consecutive | Non-consecutive |
171
+ |---|---|---|
172
+ | `cols_side` | 20 (41 columns) | 3 (7 columns) |
173
+ | `max_distance` | 4 px | 3 px |
174
+ | `window_size` | 7 | 3 |
175
+ | `dp_threshold` | 4 | 1 |
176
+
177
+ These are selected automatically. Override any of them via CLI flags.
178
+
179
+ ---
180
+
181
+ ## Methodological note
182
+
183
+ The GPS overlap detection compares **`log(GPS_time)`** between two
184
+ radargrams with an absolute tolerance of `1e-8`. Because
185
+ `d(log t) ≈ dt/t` and GPS times are ~1×10⁹ seconds, this tolerance
186
+ corresponds to matching raw timestamps within roughly 10 seconds.
187
+ Consecutive flight segments (e.g. `_026.mat` → `_027.mat`) share a
188
+ small tail/head of overlapping traces, which is where the crossing point
189
+ is derived. Non-consecutive intersecting radargrams do not share GPS
190
+ timestamps; their crossing point must be supplied via `--intersections_csv`
191
+ or `--lat`/`--lon`.
192
+
193
+ ---
194
+
195
+ ## Project structure
196
+
197
+ ```
198
+ src/xcheck/
199
+ xcheck.py Core algorithms: loading, masking, GPS overlap,
200
+ column extraction, layer matching, plotting, CLI
201
+ __init__.py Public API exports
202
+ pyproject.toml Build metadata and dependencies
203
+ README.md
204
+ ```
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xcheck"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Your Name", email="your.email@example.com" },
10
+ ]
11
+ description = "A package for cross-checking radargram layers using dot product similarity."
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Science/Research",
19
+ "Topic :: Scientific/Engineering :: Physics",
20
+ "Topic :: Scientific/Engineering :: Image Processing",
21
+ ]
22
+ dependencies = [
23
+ "numpy",
24
+ "matplotlib",
25
+ "scipy",
26
+ "pandas",
27
+ "cartopy",
28
+ ]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [project.scripts]
34
+ xcheck = "xcheck.xcheck:main"
xcheck-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
xcheck-0.1.0/setup.py ADDED
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+ if __name__ == "__main__":
4
+ setup()
@@ -0,0 +1,12 @@
1
+ from .xcheck import (
2
+ load_and_process_radargram,
3
+ load_log_gps_time,
4
+ find_overlapping_intervals,
5
+ load_data,
6
+ adjust_column_by_surface_elevation,
7
+ extract_and_adjust_mask_columns_for_location,
8
+ match_layers_with_dot_product,
9
+ plot_overlaps,
10
+ plot_columns_with_labels,
11
+ main
12
+ )
@@ -0,0 +1,304 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+ import sys
5
+ import types
6
+ import argparse
7
+ import scipy.io
8
+ import matplotlib.pyplot as plt
9
+ import cartopy.crs as ccrs
10
+ from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
11
+
12
+ # Register NDH_PythonTools as a package without executing its __init__.py,
13
+ # which auto-imports all submodules including ones with optional heavy deps (pyvista, etc.)
14
+ def _register_ndh_package():
15
+ for p in sys.path:
16
+ candidate = os.path.join(p, 'NDH_PythonTools')
17
+ if os.path.isdir(candidate):
18
+ if 'NDH_PythonTools' not in sys.modules:
19
+ pkg = types.ModuleType('NDH_PythonTools')
20
+ pkg.__path__ = [candidate]
21
+ pkg.__package__ = 'NDH_PythonTools'
22
+ sys.modules['NDH_PythonTools'] = pkg
23
+ return
24
+ raise ImportError(
25
+ "NDH_PythonTools not found on sys.path. "
26
+ "Clone https://github.com/nholschuh/NDH_PythonTools and add its parent directory to PYTHONPATH."
27
+ )
28
+
29
+ _register_ndh_package()
30
+
31
+ from NDH_PythonTools.loadmat import loadmat
32
+ from NDH_PythonTools.radar_load import radar_load
33
+
34
+ def load_and_process_radargram(file_name, layer_dir, radar_dir, layer_prefix="Layer_"):
35
+ """Loads radargram data and ground truth layers, then creates a binary mask."""
36
+ layer_fn = os.path.join(layer_dir, layer_prefix + file_name)
37
+ radar_fn = os.path.join(radar_dir, file_name)
38
+
39
+ groundtruth = loadmat(layer_fn)
40
+ groundtruth_dists = groundtruth['layer_info'][()][2]
41
+ groundtruth_layers = groundtruth['layer_info'][()][5].T
42
+ radar_data, depth_data = radar_load(radar_fn, 0, 1)
43
+
44
+ ground_truth_mask_nick = np.zeros_like(depth_data['new_data'])
45
+
46
+ for layer in groundtruth_layers:
47
+ for idx, depth in enumerate(layer):
48
+ if not np.isnan(depth):
49
+ depth_idx = np.argmin(np.abs(depth_data['depth_axis'] - depth))
50
+ dist_idx = np.argmin(np.abs(radar_data['distance'] - groundtruth_dists[idx]))
51
+ if 0 <= depth_idx < ground_truth_mask_nick.shape[0] and 0 <= dist_idx < ground_truth_mask_nick.shape[1]:
52
+ ground_truth_mask_nick[depth_idx, dist_idx] = 1
53
+ return radar_data, ground_truth_mask_nick, depth_data
54
+
55
+ def load_log_gps_time(file_path):
56
+ """Loads GPS time from a .mat file and returns its natural logarithm."""
57
+ radar_data = scipy.io.loadmat(file_path)
58
+ gps_time = radar_data['GPS_time'][0]
59
+ return np.log(gps_time)
60
+
61
+ def find_overlapping_intervals(log_gps_time1, log_gps_time2, tolerance=1e-8):
62
+ """Finds overlapping intervals between two logarithmic GPS time series."""
63
+ overlap_intervals1, overlap_intervals2 = [], []
64
+ i, j = 0, 0
65
+ start_i, start_j = None, None
66
+
67
+ while i < len(log_gps_time1) and j < len(log_gps_time2):
68
+ if abs(log_gps_time1[i] - log_gps_time2[j]) < tolerance:
69
+ if start_i is None:
70
+ start_i, start_j = i, j
71
+ i += 1
72
+ j += 1
73
+ else:
74
+ if start_i is not None:
75
+ overlap_intervals1.append((start_i, i - 1))
76
+ overlap_intervals2.append((start_j, j - 1))
77
+ start_i, start_j = None, None
78
+ if log_gps_time1[i] < log_gps_time2[j]: i += 1
79
+ else: j += 1
80
+ if start_i is not None:
81
+ overlap_intervals1.append((start_i, i - 1))
82
+ overlap_intervals2.append((start_j, j - 1))
83
+ return overlap_intervals1, overlap_intervals2
84
+
85
+ def load_data(file_path):
86
+ """Loads latitude and longitude data from a .mat file."""
87
+ radar_data = scipy.io.loadmat(file_path)
88
+ return radar_data['Latitude'][0], radar_data['Longitude'][0]
89
+
90
+ def adjust_column_by_surface_elevation(depth_data, idx):
91
+ """Calculates the depth index corresponding to the surface elevation."""
92
+ surface_elevation = depth_data['surface_elev'][idx]
93
+ return np.argmin(np.abs(depth_data['depth_axis'] - surface_elevation))
94
+
95
+ def extract_and_adjust_mask_columns_for_location(longitude, latitude, radar_data, mask, depth_data, columns_before_after):
96
+ """Extracts and adjusts mask columns around a specific geospatial location."""
97
+ dists = (radar_data['Longitude'] - longitude) ** 2 + (radar_data['Latitude'] - latitude) ** 2
98
+ center_idx = np.argmin(dists)
99
+ start = max(0, center_idx - columns_before_after)
100
+ end = min(mask.shape[1], center_idx + columns_before_after + 1)
101
+
102
+ reference_surface_idx = adjust_column_by_surface_elevation(depth_data, center_idx)
103
+ reference_depth_axis = depth_data['depth_axis']
104
+ adjusted_cols = []
105
+
106
+ for idx in range(start, end):
107
+ current_surface_idx = adjust_column_by_surface_elevation(depth_data, idx)
108
+ current_depth_axis = depth_data['depth_axis']
109
+ depth_diff = reference_depth_axis[reference_surface_idx] - current_depth_axis[current_surface_idx]
110
+ meters_per_pixel = np.mean(np.diff(current_depth_axis))
111
+ pixel_shift = int(round(depth_diff / meters_per_pixel))
112
+ col = mask[:, idx]
113
+ if pixel_shift > 0:
114
+ col_shifted = np.pad(col, (pixel_shift, 0), mode='constant')[:len(col)]
115
+ elif pixel_shift < 0:
116
+ col_shifted = np.pad(col, (0, -pixel_shift), mode='constant')[-pixel_shift:]
117
+ if len(col_shifted) > len(col): col_shifted = col_shifted[:len(col)]
118
+ else: col_shifted = np.pad(col_shifted, (0, len(col) - len(col_shifted)), mode='constant')
119
+ else: col_shifted = col
120
+ adjusted_cols.append(col_shifted)
121
+
122
+ max_len = max(len(col) for col in adjusted_cols)
123
+ for i, col in enumerate(adjusted_cols):
124
+ if len(col) < max_len:
125
+ adjusted_cols[i] = np.pad(col, (0, max_len - len(col)), 'constant')
126
+ return np.column_stack(adjusted_cols)
127
+
128
+ def match_layers_with_dot_product(mask1, mask2, max_distance=4, window_size=7, dp_threshold=4, verbose=True):
129
+ """Matches layers between two radargram masks using a dot product similarity metric."""
130
+ if window_size % 2 == 0: raise ValueError("window_size must be odd.")
131
+ matches, matched1, matched2 = [], set(), set()
132
+ c1, c2 = mask1.shape[1] // 2, mask2.shape[1] // 2
133
+ hw = window_size // 2
134
+ cols1 = slice(max(c1 - hw, 0), min(c1 + hw + 1, mask1.shape[1]))
135
+ cols2 = slice(max(c2 - hw, 0), min(c2 + hw + 1, mask2.shape[1]))
136
+
137
+ for i in range(mask1.shape[0]):
138
+ if i in matched1 or not np.any(mask1[i, :]): continue
139
+ best_match, best_dist, best_dp = None, max_distance + 1, -1
140
+ for j in range(mask2.shape[0]):
141
+ if j in matched2 or not np.any(mask2[j, :]): continue
142
+ dist = abs(i - j)
143
+ if dist <= max_distance:
144
+ r1, r2 = mask1[i, cols1], mask2[j, cols2]
145
+ if not (r1[hw] == 1 or np.sum(r1) >= 2): continue
146
+ if not (r2[hw] == 1 or np.sum(r2) >= 2): continue
147
+ dp = np.dot(r1, r2)
148
+ if dp >= dp_threshold and dist < best_dist:
149
+ best_match, best_dist, best_dp = j, dist, dp
150
+ if best_match is not None:
151
+ matches.append((i, best_match))
152
+ matched1.add(i); matched2.add(best_match)
153
+ if verbose: print(f"Match: Layer {i} <-> Layer {best_match}")
154
+ return matches
155
+
156
+ def plot_overlaps(lat1, lon1, lat2, lon2, overlap_intervals1, overlap_intervals2):
157
+ ax = plt.figure(figsize=(20, 10)).add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
158
+ ax.coastlines()
159
+ gl = ax.gridlines(draw_labels=True, linewidth=1, color='gray', alpha=0.5, linestyle='--')
160
+ gl.top_labels = gl.right_labels = False
161
+ ax.scatter(lon1, lat1, color='blue', s=10, label='Radargram 1')
162
+ ax.scatter(lon2, lat2, color='red', s=10, label='Radargram 2')
163
+ for (s1, e1), (s2, e2) in zip(overlap_intervals1, overlap_intervals2):
164
+ ax.plot(lon1[s1:e1+1], lat1[s1:e1+1], color='yellow', linewidth=2)
165
+ ax.plot(lon2[s2:e2+1], lat2[s2:e2+1], color='yellow', linewidth=2)
166
+ ax.legend(); plt.show()
167
+
168
+ def plot_columns_with_labels(mask1, mask2, matches, columns_before_after, title1="R1", title2="R2"):
169
+ fig, axs = plt.subplots(1, 2, figsize=(14, 6), sharey=True)
170
+ x_vals = np.arange(-columns_before_after, columns_before_after + 1)
171
+ axs[0].imshow(mask1, cmap='gray', aspect='auto', extent=(x_vals[0], x_vals[-1], mask1.shape[0], 0))
172
+ axs[1].imshow(mask2, cmap='gray', aspect='auto', extent=(x_vals[0], x_vals[-1], mask2.shape[0], 0))
173
+ for i, j in matches:
174
+ axs[0].axhline(i, color='red', linestyle='--'); axs[1].axhline(j, color='red', linestyle='--')
175
+ plt.tight_layout(); plt.show()
176
+
177
+ def _lookup_intersection(intersections_df, file1, file2):
178
+ """Returns (lon, lat) for a radargram pair from an intersections DataFrame, or None if not found."""
179
+ mask = (
180
+ ((intersections_df['Radargram 1'] == file1) & (intersections_df['Radargram 2'] == file2)) |
181
+ ((intersections_df['Radargram 2'] == file1) & (intersections_df['Radargram 1'] == file2))
182
+ )
183
+ hit = intersections_df[mask]
184
+ if hit.empty:
185
+ return None
186
+ return hit['Longitude'].iloc[0], hit['Latitude'].iloc[0]
187
+
188
+
189
+ def _process_pair(file1, file2, lon, lat, args):
190
+ """Run GPS overlap detection and layer matching.
191
+
192
+ Non-consecutive/intersecting: crossing point supplied via lon/lat.
193
+ Consecutive: no crossing point — midpoint of each GPS overlap interval is used instead.
194
+ """
195
+ path1 = os.path.join(args.radar_dir, file1)
196
+ path2 = os.path.join(args.radar_dir, file2)
197
+
198
+ # GPS-based overlap detection
199
+ log_gps1 = load_log_gps_time(path1)
200
+ log_gps2 = load_log_gps_time(path2)
201
+ overlaps1, overlaps2 = find_overlapping_intervals(log_gps1, log_gps2)
202
+ print(f"{file1} <-> {file2}: {len(overlaps1)} GPS overlap interval(s) found")
203
+ for k, ((s1, e1), (s2, e2)) in enumerate(zip(overlaps1, overlaps2)):
204
+ print(f" Interval {k+1}: R1[{s1}:{e1}] R2[{s2}:{e2}] ({e1-s1+1} samples)")
205
+
206
+ if args.plot_overlaps:
207
+ lat1_arr, lon1_arr = load_data(path1)
208
+ lat2_arr, lon2_arr = load_data(path2)
209
+ plot_overlaps(lat1_arr, lon1_arr, lat2_arr, lon2_arr, overlaps1, overlaps2)
210
+
211
+ r1, m1, d1 = load_and_process_radargram(file1, args.layer_dir, args.radar_dir, args.layer_prefix)
212
+ r2, m2, d2 = load_and_process_radargram(file2, args.layer_dir, args.radar_dir, args.layer_prefix)
213
+
214
+ if lon is not None and lat is not None:
215
+ # Non-consecutive: use provided intersection coordinates
216
+ cols = args.cols_side if args.cols_side is not None else 3
217
+ maxd = args.max_distance if args.max_distance is not None else 3
218
+ wsize = args.window_size if args.window_size is not None else 3
219
+ dpth = args.dp_threshold if args.dp_threshold is not None else 1
220
+ mc1 = extract_and_adjust_mask_columns_for_location(lon, lat, r1, m1, d1, cols)
221
+ mc2 = extract_and_adjust_mask_columns_for_location(lon, lat, r2, m2, d2, cols)
222
+ matches = match_layers_with_dot_product(mc1, mc2, max_distance=maxd, window_size=wsize, dp_threshold=dpth)
223
+ print(f"Total Matches: {len(matches)}")
224
+
225
+ elif overlaps1:
226
+ # Consecutive: derive crossing point from midpoint of each GPS overlap interval
227
+ cols = args.cols_side if args.cols_side is not None else 20
228
+ maxd = args.max_distance if args.max_distance is not None else 4
229
+ wsize = args.window_size if args.window_size is not None else 7
230
+ dpth = args.dp_threshold if args.dp_threshold is not None else 4
231
+ for k, (interval1, interval2) in enumerate(zip(overlaps1, overlaps2)):
232
+ mid1 = (interval1[0] + interval1[1]) // 2
233
+ mid2 = (interval2[0] + interval2[1]) // 2
234
+ mc1 = extract_and_adjust_mask_columns_for_location(r1['Longitude'][mid1], r1['Latitude'][mid1], r1, m1, d1, cols)
235
+ mc2 = extract_and_adjust_mask_columns_for_location(r2['Longitude'][mid2], r2['Latitude'][mid2], r2, m2, d2, cols)
236
+ matches = match_layers_with_dot_product(mc1, mc2, max_distance=maxd, window_size=wsize, dp_threshold=dpth)
237
+ print(f"Overlap interval {k+1} — Total Matches: {len(matches)}")
238
+
239
+ else:
240
+ print(f"No crossing coordinates and no GPS overlap found — skipping layer matching.")
241
+
242
+
243
+ def main():
244
+ parser = argparse.ArgumentParser(description="XCheck CLI")
245
+ parser.add_argument('--r1', type=str, default=None,
246
+ help="First radargram filename (single-pair mode).")
247
+ parser.add_argument('--r2', type=str, default=None,
248
+ help="Second radargram filename (single-pair mode).")
249
+ parser.add_argument('--lat', type=float, default=None,
250
+ help="Crossing latitude for single-pair mode (optional if --intersections_csv is given).")
251
+ parser.add_argument('--lon', type=float, default=None,
252
+ help="Crossing longitude for single-pair mode (optional if --intersections_csv is given).")
253
+ parser.add_argument('--csv_path', type=str, default=None,
254
+ help="CSV with radargram pairs. Must include Longitude/Latitude columns unless --intersections_csv is also provided.")
255
+ parser.add_argument('--intersections_csv', type=str, default=None,
256
+ help="Optional CSV with pre-computed intersection coordinates (columns: Radargram 1, Radargram 2, Latitude, Longitude). "
257
+ "When provided alone, all pairs in it are processed. When combined with --csv_path, coordinates are looked up here.")
258
+ parser.add_argument('--layer_dir', type=str, required=True)
259
+ parser.add_argument('--radar_dir', type=str, required=True)
260
+ parser.add_argument('--cols_side', type=int, default=None,
261
+ help="Columns extracted each side of crossing (default: 20 for consecutive, 3 for non-consecutive).")
262
+ parser.add_argument('--max_distance', type=int, default=None,
263
+ help="Max depth-pixel gap between candidate layer rows (default: 4 consecutive, 3 non-consecutive).")
264
+ parser.add_argument('--window_size', type=int, default=None,
265
+ help="Odd column window width for dot product (default: 7 consecutive, 3 non-consecutive).")
266
+ parser.add_argument('--dp_threshold', type=float, default=None,
267
+ help="Minimum dot product to count as a match (default: 4 consecutive, 1 non-consecutive).")
268
+ parser.add_argument('--layer_prefix', type=str, default='Layer_',
269
+ help="Filename prefix for layer files (default: 'Layer_')")
270
+ parser.add_argument('--plot_overlaps', action='store_true', help="Plot GPS overlap regions on a map")
271
+ args = parser.parse_args()
272
+
273
+ # --- Single-pair mode ---
274
+ if args.r1 and args.r2:
275
+ intersections_df = pd.read_csv(args.intersections_csv) if args.intersections_csv else None
276
+ lon, lat = args.lon, args.lat
277
+ if lon is None and intersections_df is not None:
278
+ result = _lookup_intersection(intersections_df, args.r1, args.r2)
279
+ if result is not None:
280
+ lon, lat = result
281
+ _process_pair(args.r1, args.r2, lon, lat, args)
282
+ return
283
+
284
+ # --- Batch mode ---
285
+ if args.csv_path is None and args.intersections_csv is None:
286
+ parser.error("Provide --r1/--r2 for a single pair, or --csv_path/--intersections_csv for batch mode.")
287
+
288
+ intersections_df = pd.read_csv(args.intersections_csv) if args.intersections_csv else None
289
+ df = pd.read_csv(args.csv_path) if args.csv_path else intersections_df
290
+
291
+ for _, row in df.iterrows():
292
+ file1, file2 = row['Radargram 1'], row['Radargram 2']
293
+ if intersections_df is not None:
294
+ result = _lookup_intersection(intersections_df, file1, file2)
295
+ if result is None:
296
+ print(f"Skipping {file1} <-> {file2}: no intersection found in --intersections_csv")
297
+ continue
298
+ lon, lat = result
299
+ else:
300
+ lon, lat = row['Longitude'], row['Latitude']
301
+ _process_pair(file1, file2, lon, lat, args)
302
+
303
+ if __name__ == '__main__':
304
+ main()
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: xcheck
3
+ Version: 0.1.0
4
+ Summary: A package for cross-checking radargram layers using dot product similarity.
5
+ Author-email: Your Name <your.email@example.com>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering :: Physics
11
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: numpy
15
+ Requires-Dist: matplotlib
16
+ Requires-Dist: scipy
17
+ Requires-Dist: pandas
18
+ Requires-Dist: cartopy
19
+
20
+ # XCheck — Consistency-Based Validation of Englacial Layer Annotations
21
+
22
+ A Python implementation of the radargram layer-matching framework from:
23
+
24
+ > Hassan, Muhammad Behroze; Tama, Bayu Adhi; Purushotham, Sanjay;
25
+ > Janeja, Vandana P. **XCheck: A Consistency-Based Validation Framework
26
+ > for Englacial Layers Annotations.**
27
+ > <https://par.nsf.gov/biblio/10673248> ·
28
+ > DOI: [10.1109/ICDMW69685.2025.00015](https://doi.org/10.1109/ICDMW69685.2025.00015)
29
+
30
+ XCheck validates whether annotated englacial layers are *consistent*
31
+ between two radargrams that observe the same ice — either two
32
+ **consecutive** flight segments (overlap found automatically via GPS time)
33
+ or two **intersecting** flight paths (crossing point supplied via a CSV).
34
+
35
+ ---
36
+
37
+ ## Features
38
+
39
+ - **GPS Overlap Detection**: Automatically finds temporally overlapping segments between two radargrams using log-transformed GPS timestamps.
40
+ - **Data Loading & Masking**: Loads `.mat` radargram and ground truth layer files and converts annotated layers into binary depth masks.
41
+ - **Geometric Alignment**: Adjusts radargram columns by surface elevation before comparison to correct for topographic variation.
42
+ - **Layer Continuity Matching**: Dot-product similarity algorithm that identifies corresponding layers between two radargrams at their crossing or overlap point.
43
+ - **Flexible Input**: Accepts a single radargram pair via CLI flags, a batch CSV of pairs, or a pre-computed intersections CSV.
44
+ - **Visualization**: Map plots of GPS overlap regions and side-by-side mask views with matched layer annotations.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install xcheck
52
+ ```
53
+
54
+ ### NDH_PythonTools (required)
55
+
56
+ XCheck uses [NDH_PythonTools](https://github.com/nholschuh/NDH_PythonTools)
57
+ for `.mat` file loading and radar data processing. Because that repository
58
+ is not a standard pip package, it must be installed manually:
59
+
60
+ ```bash
61
+ git clone https://github.com/nholschuh/NDH_PythonTools.git
62
+ ```
63
+
64
+ Then add its **parent directory** to `PYTHONPATH` before running xcheck:
65
+
66
+ ```bash
67
+ # Linux / macOS
68
+ export PYTHONPATH="/path/to/parent/of/NDH_PythonTools"
69
+
70
+ # Windows PowerShell
71
+ $env:PYTHONPATH = "C:\path\to\parent\of\NDH_PythonTools"
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Quick Start
77
+
78
+ ### Layer matching only (no NDH_PythonTools needed)
79
+
80
+ ```python
81
+ import numpy as np
82
+ from xcheck import match_layers_with_dot_product
83
+
84
+ m1 = np.zeros((6, 5)); m1[[1, 3, 5], :] = 1
85
+ m2 = np.zeros((6, 5)); m2[[1, 3, 5], :] = 1
86
+
87
+ matches = match_layers_with_dot_product(m1, m2, max_distance=1,
88
+ window_size=3, dp_threshold=1)
89
+ print(matches) # [(1, 1), (3, 3), (5, 5)]
90
+ ```
91
+
92
+ ### Python API (requires NDH_PythonTools + data)
93
+
94
+ ```python
95
+ from xcheck import (
96
+ load_and_process_radargram,
97
+ load_log_gps_time,
98
+ find_overlapping_intervals,
99
+ extract_and_adjust_mask_columns_for_location,
100
+ match_layers_with_dot_product,
101
+ )
102
+
103
+ # Load two consecutive radargrams
104
+ r1, m1, d1 = load_and_process_radargram("Data_20120429_01_026.mat",
105
+ layer_dir="./Nick-layer-data-mat/",
106
+ radar_dir="./Nick-raw-radargram-mat/")
107
+ r2, m2, d2 = load_and_process_radargram("Data_20120429_01_027.mat",
108
+ layer_dir="./Nick-layer-data-mat/",
109
+ radar_dir="./Nick-raw-radargram-mat/")
110
+
111
+ # Find GPS overlap and derive crossing point from midpoint
112
+ import numpy as np
113
+ log_t1 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_026.mat")
114
+ log_t2 = load_log_gps_time("./Nick-raw-radargram-mat/Data_20120429_01_027.mat")
115
+ overlaps1, overlaps2 = find_overlapping_intervals(log_t1, log_t2)
116
+
117
+ for iv1, iv2 in zip(overlaps1, overlaps2):
118
+ mid1 = (iv1[0] + iv1[1]) // 2
119
+ mid2 = (iv2[0] + iv2[1]) // 2
120
+ mc1 = extract_and_adjust_mask_columns_for_location(
121
+ r1['Longitude'][mid1], r1['Latitude'][mid1], r1, m1, d1, 20)
122
+ mc2 = extract_and_adjust_mask_columns_for_location(
123
+ r2['Longitude'][mid2], r2['Latitude'][mid2], r2, m2, d2, 20)
124
+ matches = match_layers_with_dot_product(mc1, mc2)
125
+ print(f"Matched layers: {matches}")
126
+ ```
127
+
128
+ ---
129
+
130
+ ## CLI
131
+
132
+ ### Single pair — consecutive (no crossing CSV needed)
133
+
134
+ ```bash
135
+ xcheck --r1 Data_20120429_01_026.mat --r2 Data_20120429_01_027.mat \
136
+ --layer_dir ./Nick-layer-data-mat/ \
137
+ --radar_dir ./Nick-raw-radargram-mat/
138
+ ```
139
+
140
+ ### Single pair — intersecting (crossing looked up from CSV)
141
+
142
+ ```bash
143
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
144
+ --intersections_csv ./Intersections_2012.csv \
145
+ --layer_dir ./Nick-layer-data-mat/ \
146
+ --radar_dir ./Nick-raw-radargram-mat/
147
+ ```
148
+
149
+ ### Single pair — intersecting with explicit crossing coordinates
150
+
151
+ ```bash
152
+ xcheck --r1 Data_20120330_01_004.mat --r2 Data_20120511_01_054.mat \
153
+ --lat 78.909416 --lon -61.804612 \
154
+ --layer_dir ./Nick-layer-data-mat/ \
155
+ --radar_dir ./Nick-raw-radargram-mat/
156
+ ```
157
+
158
+ ### Batch mode (all pairs in a CSV)
159
+
160
+ ```bash
161
+ xcheck --intersections_csv ./Intersections_2012.csv \
162
+ --layer_dir ./Nick-layer-data-mat/ \
163
+ --radar_dir ./Nick-raw-radargram-mat/
164
+ ```
165
+
166
+ ### All CLI arguments
167
+
168
+ | Argument | Default | Description |
169
+ |---|---|---|
170
+ | `--r1`, `--r2` | — | Single-pair mode: filenames of the two radargrams |
171
+ | `--lat`, `--lon` | — | Crossing coordinates for single-pair mode |
172
+ | `--csv_path` | — | Batch CSV with columns `Radargram 1`, `Radargram 2`, `Latitude`, `Longitude` |
173
+ | `--intersections_csv` | — | Pre-computed intersections CSV (used alone or to look up coordinates) |
174
+ | `--layer_dir` | required | Directory of layer `.mat` files |
175
+ | `--radar_dir` | required | Directory of raw radargram `.mat` files |
176
+ | `--layer_prefix` | `Layer_` | Filename prefix for layer files |
177
+ | `--cols_side` | 20 / 3 | Columns extracted each side of crossing (consecutive / non-consecutive) |
178
+ | `--max_distance` | 4 / 3 | Max depth-pixel gap between candidate layer rows |
179
+ | `--window_size` | 7 / 3 | Dot product window width (must be odd) |
180
+ | `--dp_threshold` | 4 / 1 | Minimum dot product score to confirm a match |
181
+ | `--plot_overlaps` | off | Plot GPS overlap regions on a map |
182
+
183
+ Default pairs shown as **consecutive / non-consecutive**.
184
+
185
+ ---
186
+
187
+ ## Default thresholds by radargram type
188
+
189
+ | Parameter | Consecutive | Non-consecutive |
190
+ |---|---|---|
191
+ | `cols_side` | 20 (41 columns) | 3 (7 columns) |
192
+ | `max_distance` | 4 px | 3 px |
193
+ | `window_size` | 7 | 3 |
194
+ | `dp_threshold` | 4 | 1 |
195
+
196
+ These are selected automatically. Override any of them via CLI flags.
197
+
198
+ ---
199
+
200
+ ## Methodological note
201
+
202
+ The GPS overlap detection compares **`log(GPS_time)`** between two
203
+ radargrams with an absolute tolerance of `1e-8`. Because
204
+ `d(log t) ≈ dt/t` and GPS times are ~1×10⁹ seconds, this tolerance
205
+ corresponds to matching raw timestamps within roughly 10 seconds.
206
+ Consecutive flight segments (e.g. `_026.mat` → `_027.mat`) share a
207
+ small tail/head of overlapping traces, which is where the crossing point
208
+ is derived. Non-consecutive intersecting radargrams do not share GPS
209
+ timestamps; their crossing point must be supplied via `--intersections_csv`
210
+ or `--lat`/`--lon`.
211
+
212
+ ---
213
+
214
+ ## Project structure
215
+
216
+ ```
217
+ src/xcheck/
218
+ xcheck.py Core algorithms: loading, masking, GPS overlap,
219
+ column extraction, layer matching, plotting, CLI
220
+ __init__.py Public API exports
221
+ pyproject.toml Build metadata and dependencies
222
+ README.md
223
+ ```
224
+
225
+ ---
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/xcheck/__init__.py
5
+ src/xcheck/xcheck.py
6
+ src/xcheck.egg-info/PKG-INFO
7
+ src/xcheck.egg-info/SOURCES.txt
8
+ src/xcheck.egg-info/dependency_links.txt
9
+ src/xcheck.egg-info/entry_points.txt
10
+ src/xcheck.egg-info/requires.txt
11
+ src/xcheck.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ xcheck = xcheck.xcheck:main
@@ -0,0 +1,5 @@
1
+ numpy
2
+ matplotlib
3
+ scipy
4
+ pandas
5
+ cartopy
@@ -0,0 +1 @@
1
+ xcheck