CRAMM 1.0.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.
- cramm-1.0.0/CRAMM.egg-info/PKG-INFO +421 -0
- cramm-1.0.0/CRAMM.egg-info/SOURCES.txt +31 -0
- cramm-1.0.0/CRAMM.egg-info/dependency_links.txt +1 -0
- cramm-1.0.0/CRAMM.egg-info/entry_points.txt +2 -0
- cramm-1.0.0/CRAMM.egg-info/requires.txt +20 -0
- cramm-1.0.0/CRAMM.egg-info/top_level.txt +1 -0
- cramm-1.0.0/LICENSE +21 -0
- cramm-1.0.0/MANIFEST.in +7 -0
- cramm-1.0.0/PKG-INFO +421 -0
- cramm-1.0.0/README.md +380 -0
- cramm-1.0.0/benchmark.py +153 -0
- cramm-1.0.0/cramm/__init__.py +23 -0
- cramm-1.0.0/cramm/classifier.py +1495 -0
- cramm-1.0.0/cramm/data/color_table.json +387 -0
- cramm-1.0.0/cramm/data/rf.json +5876 -0
- cramm-1.0.0/cramm/data/splib06b +0 -0
- cramm-1.0.0/cramm/emit_reader.py +106 -0
- cramm-1.0.0/cramm/mica_engine.py +321 -0
- cramm-1.0.0/cramm/renderer.py +485 -0
- cramm-1.0.0/docs/muscovite_wv2200_phase_diagram.png +0 -0
- cramm-1.0.0/docs/single_spectrum_diagnostic.png +0 -0
- cramm-1.0.0/example_usage.py +174 -0
- cramm-1.0.0/pyproject.toml +61 -0
- cramm-1.0.0/regen_golden.py +40 -0
- cramm-1.0.0/setup.cfg +4 -0
- cramm-1.0.0/tests/check_compiled_path.py +108 -0
- cramm-1.0.0/tests/check_rows_logic.py +142 -0
- cramm-1.0.0/tests/golden_arrays.npz +0 -0
- cramm-1.0.0/tests/test_core.py +578 -0
- cramm-1.0.0/tests/test_custom_rules.py +299 -0
- cramm-1.0.0/tests/test_parallel.py +40 -0
- cramm-1.0.0/tests/test_parallel_isolation.py +269 -0
- cramm-1.0.0/tests/test_single_spectrum.py +224 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CRAMM
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CRAMM — general-purpose hyperspectral mineral identification toolkit (USGS MICA, extended: depth-ratio rules + quantitative muscovite mapping; sensor-agnostic, EMIT L2A built in)
|
|
5
|
+
Author: Shijie Li
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/leecugb/cramm
|
|
8
|
+
Project-URL: Repository, https://github.com/leecugb/cramm
|
|
9
|
+
Project-URL: Issues, https://github.com/leecugb/cramm/issues
|
|
10
|
+
Keywords: hyperspectral,EMIT,mineral,MICA,USGS,remote-sensing
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: pandas
|
|
26
|
+
Requires-Dist: netCDF4
|
|
27
|
+
Requires-Dist: pyproj
|
|
28
|
+
Requires-Dist: pyresample
|
|
29
|
+
Requires-Dist: threadpoolctl
|
|
30
|
+
Provides-Extra: tiff
|
|
31
|
+
Requires-Dist: gdal; extra == "tiff"
|
|
32
|
+
Provides-Extra: pdf
|
|
33
|
+
Requires-Dist: matplotlib; extra == "pdf"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: gdal; extra == "all"
|
|
36
|
+
Requires-Dist: matplotlib; extra == "all"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest; extra == "test"
|
|
39
|
+
Requires-Dist: matplotlib; extra == "test"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# CRAMM
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/cramm/)
|
|
45
|
+
[](https://pypi.org/project/cramm/)
|
|
46
|
+
[](https://doi.org/10.5281/zenodo.22024483)
|
|
47
|
+
[](https://github.com/leecugb/cramm/blob/main/LICENSE)
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
**General-purpose hyperspectral mineral identification toolkit** — built on
|
|
51
|
+
the USGS MICA (Material Identification and Characterization Algorithm)
|
|
52
|
+
decision-rule system. The classification core is **sensor-agnostic**: it works
|
|
53
|
+
on any VNIR–SWIR reflectance cube given its band configuration (center
|
|
54
|
+
wavelengths, FWHM, valid-band mask), because the bundled splib06b reference
|
|
55
|
+
spectra are resampled to the sensor's bands at runtime. EMIT L2A is simply the
|
|
56
|
+
built-in data reader — one supported input type, not the defining one.
|
|
57
|
+
|
|
58
|
+
CRAMM extends MICA in three ways:
|
|
59
|
+
|
|
60
|
+
1. **An enhanced rule schema.** CRAMM adds an optional secondary-feature
|
|
61
|
+
depth-ratio constraint (`max_depth_ratio_feat1_over_feat0`) that rejects
|
|
62
|
+
pixels whose secondary absorption is too deep relative to the primary
|
|
63
|
+
2.2 µm feature — suppressing white-mica false positives that pass the
|
|
64
|
+
original five-layer MICA filtering. Nine bundled rules (muscovite, illite,
|
|
65
|
+
kaolinite–muscovite mixtures) carry the new constraint; any custom rule
|
|
66
|
+
can opt in. See *Enhancements over USGS MICA*.
|
|
67
|
+
2. **Wavelength-arbitrated muscovite subtyping.** MICA labels a pixel
|
|
68
|
+
"muscovite_lowAl / medAl / medhighAl / Fe-rich" by best fit alone; CRAMM
|
|
69
|
+
then re-arbitrates that attribution with the pixel's fitted 2.2 µm
|
|
70
|
+
absorption center against per-rule calibrated wavelength windows
|
|
71
|
+
(`absorption_center_range`) — the spectroscopically meaningful axis along
|
|
72
|
+
which these four subtypes are actually defined. See *Enhancements over
|
|
73
|
+
USGS MICA → Wavelength-based muscovite attribution*.
|
|
74
|
+
3. **From mineral detection to mineral composition.** Beyond labeling
|
|
75
|
+
muscovite pixels, CRAMM fits the per-pixel 2.2 µm absorption-center
|
|
76
|
+
wavelength (`mus_center`) — a quantitative composition proxy whose
|
|
77
|
+
thermodynamic basis (Tschermak substitution vs. wv2200 on a
|
|
78
|
+
GEMS/MINES23.1 reaction-path phase diagram) lets each fitted pixel be
|
|
79
|
+
read as muscovite chemistry, formation temperature and fluid K⁺/H⁺
|
|
80
|
+
conditions. See *Application: reading muscovite composition from
|
|
81
|
+
mus_center*.
|
|
82
|
+
|
|
83
|
+
Everything is pure Python and GUI-free. Cross-platform:
|
|
84
|
+
**Windows / Linux / macOS** · Python **3.9 – 3.13**
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Highlights
|
|
89
|
+
|
|
90
|
+
- **Enhanced MICA pipeline** — continuum removal → closed-form 2×2 least
|
|
91
|
+
squares → fit (r²) & absorption depth → five-layer constraint filtering
|
|
92
|
+
**+ the CRAMM depth-ratio constraint**, driven by a JSON rule library
|
|
93
|
+
(77 rules covering clay, sulfate, carbonate, mica, chlorite, amphibole,
|
|
94
|
+
iron oxide, snow/ice and their mixtures).
|
|
95
|
+
- **Quantitative muscovite mapping** — per-pixel 2.2 µm absorption-center
|
|
96
|
+
wavelength as a dedicated thematic map and float array; the same center
|
|
97
|
+
also re-arbitrates the lowAl / medAl / medhighAl / Fe-rich attribution
|
|
98
|
+
against calibrated wavelength windows, with a phase-diagram
|
|
99
|
+
interpretation framework.
|
|
100
|
+
- **Whole-scene and single-spectrum modes** — batch-classify an entire scene
|
|
101
|
+
to GeoTIFF, or identify one spectrum (GUI point-click, field
|
|
102
|
+
spectrometer) with Top-N ranking and a PDF diagnostic report.
|
|
103
|
+
- **Sensor-agnostic core** — everything downstream of data loading consumes a
|
|
104
|
+
generic `(spectrum, wavelengths, FWHM, valid bands)` contract. The bundled
|
|
105
|
+
reader covers EMIT L2A NetCDF; any other sensor (airborne or spaceborne)
|
|
106
|
+
plugs in through the same seven-tuple — no rule or code changes needed.
|
|
107
|
+
- **Fast** — reference-side constants are precompiled once per band
|
|
108
|
+
configuration (two-level cache; ~16× speedup on repeated single-spectrum
|
|
109
|
+
calls), and scene classification parallelizes across rules with worker
|
|
110
|
+
processes.
|
|
111
|
+
- **Bit-exact discipline** — serial and parallel paths produce identical
|
|
112
|
+
bytes; every change is guarded by a dual-path golden regression suite.
|
|
113
|
+
- **Self-contained** — the rule library (`rf.json`), the USGS `splib06b`
|
|
114
|
+
spectral library, and the mineral color table are bundled inside the wheel.
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Hyperspectral reflectance cube (any VNIR–SWIR sensor)
|
|
120
|
+
│ built-in: load_emit (EMIT L2A NetCDF, bad-band removal)
|
|
121
|
+
│ or your own loader → (spectrum, wl, w, bp, chanels)
|
|
122
|
+
▼
|
|
123
|
+
Reference resampling ── splib06b records ──► sensor wavelengths/FWHM
|
|
124
|
+
│ (Gaussian kernel, cached)
|
|
125
|
+
▼
|
|
126
|
+
Per-rule evaluation (77 rules, parallel across rules)
|
|
127
|
+
│ diagnostic features: continuum removal → 2×2 LSQ → r² / depth
|
|
128
|
+
│ not-absorption / not-related features: exclusion filters
|
|
129
|
+
│ continuum & depth-ratio constraints
|
|
130
|
+
▼
|
|
131
|
+
Best-match selection (argmax fit×depth) + muscovite 2.2 µm center fit
|
|
132
|
+
│
|
|
133
|
+
▼
|
|
134
|
+
Wavelength-arbitrated muscovite subtyping
|
|
135
|
+
│ mus_center vs. per-rule absorption_center_range windows
|
|
136
|
+
│ (lowAl / medAl / medhighAl / Fe-rich re-attribution)
|
|
137
|
+
▼
|
|
138
|
+
Mineral map · color-enhanced map · muscovite map (+ raw float arrays)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Installation
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install cramm # core features (PyPI wheels on all three platforms)
|
|
145
|
+
pip install cramm[tiff] # + GeoTIFF output (GDAL; PyPI wheels are Windows-only)
|
|
146
|
+
pip install cramm[pdf] # + single-spectrum feature PDF diagnostics
|
|
147
|
+
pip install cramm[all] # everything
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**GDAL on Linux/macOS**: PyPI ships GDAL wheels for Windows only. Install a
|
|
151
|
+
system libgdal first (conda-forge recommended), then install without deps:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
conda install -c conda-forge gdal
|
|
155
|
+
pip install cramm --no-deps # or: pip install cramm[pdf]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Without GDAL, only `write_tiff` (GeoTIFF output) is unavailable — all
|
|
159
|
+
classification and analysis functions work (lazy import).
|
|
160
|
+
|
|
161
|
+
From source (sdist / checkout):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install . # add [all] for the optional extras
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Quick start
|
|
168
|
+
|
|
169
|
+
### Command line
|
|
170
|
+
|
|
171
|
+
The CLI uses the built-in EMIT L2A reader; for other sensors, use the Python
|
|
172
|
+
API (below) with your own loader.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
cramm -i EMIT_L2A_RFL_001_xxx.nc -o output [-n scene] [-w 4] [--raw]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Flag | Default | Meaning |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `-i`, `--input` | *(required)* | Path to the EMIT L2A NetCDF file |
|
|
181
|
+
| `-o`, `--output` | `.` | Output directory |
|
|
182
|
+
| `-n`, `--name` | input filename | Output filename prefix |
|
|
183
|
+
| `-w`, `--workers` | `min(cpu, 8)` | Parallel worker processes (`1` = serial) |
|
|
184
|
+
| `--raw` | off | Also save `mus_center` + `fd` float arrays as `.npz` |
|
|
185
|
+
|
|
186
|
+
**Output files** (written to `<output>/<name>*`):
|
|
187
|
+
|
|
188
|
+
| File | Content |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `<name>_mapping_orth.tiff` | Mineral map (orthorectified, rule-library colors) |
|
|
191
|
+
| `<name>_color_enhanced_orth.tiff` | Color-enhanced mineral map |
|
|
192
|
+
| `<name>_mus_orth.tiff` | Muscovite 2.2 µm absorption-center thematic map |
|
|
193
|
+
| `<name>_raw.npz` | *(only with `--raw`)* `mus_center` [μm] + `fd` (fit×depth), float `[r, c]` |
|
|
194
|
+
|
|
195
|
+
### Python API
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from cramm import MicaEngine
|
|
199
|
+
|
|
200
|
+
engine = MicaEngine() # all resources bundled
|
|
201
|
+
spectrum, lon, lat, w, bp, wl, chanels = engine.load_emit("EMIT_xxx.nc")
|
|
202
|
+
|
|
203
|
+
# --- whole scene → GeoTIFF -------------------------------------------------
|
|
204
|
+
orth, color, mus = engine.spectrum_analysis(spectrum, wl, w, bp, chanels,
|
|
205
|
+
n_workers=4)
|
|
206
|
+
engine.write_tiff("output/scene", lon, lat, orth, color, mus)
|
|
207
|
+
|
|
208
|
+
# --- single spectrum (one pixel, field spectrometer, ...) ------------------
|
|
209
|
+
pixel = spectrum[100, 200, :] # full-band [285] or selected [len(chanels)]
|
|
210
|
+
results = engine.classify_spectrum(pixel, wl, w, bp, chanels,
|
|
211
|
+
top_n=5, pdf_path="diag.pdf")
|
|
212
|
+
for r in results:
|
|
213
|
+
print(f"{r['name']:50s} fit={r['fit']:.4f} fd={r['fd']:.4f}")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Other sensors (non-EMIT data)
|
|
217
|
+
|
|
218
|
+
`load_emit` is only a convenience reader. For any other sensor, load the cube
|
|
219
|
+
yourself and pass the same band-configuration contract — references are
|
|
220
|
+
resampled to your wavelengths/FWHM automatically:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from cramm import MicaEngine
|
|
224
|
+
import numpy as np
|
|
225
|
+
|
|
226
|
+
engine = MicaEngine()
|
|
227
|
+
spectrum = my_loader("scene.dat") # [rows, cols, bands] reflectance
|
|
228
|
+
w = np.array([...]) # band center wavelengths [µm]
|
|
229
|
+
bp = np.array([...]) # band FWHM [µm]
|
|
230
|
+
chanels = np.arange(len(w)) # valid bands (drop bad-band indices)
|
|
231
|
+
wl = w[chanels]
|
|
232
|
+
|
|
233
|
+
orth, color, mus = engine.spectrum_analysis(spectrum, wl, w, bp, chanels,
|
|
234
|
+
n_workers=4)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Only the map rendering (`write_tiff`) needs geolocation (`lon`/`lat` grids);
|
|
238
|
+
classification itself is purely spectral and location-free.
|
|
239
|
+
|
|
240
|
+
`classify_spectrum` returns a list of `{"name", "fit", "fd"}` dicts sorted by
|
|
241
|
+
descending fit (empty list when nothing passes the filters). With
|
|
242
|
+
`pdf_path=` it also writes a multi-page PDF: one page per Top-N mineral with
|
|
243
|
+
continuum-removed feature overlays and constraint annotations (requires the
|
|
244
|
+
`[pdf]` extra).
|
|
245
|
+
|
|
246
|
+
Each PDF page dissects one candidate rule — every diagnostic / not-absorption
|
|
247
|
+
/ not-relative feature with its continuum endpoints, the reference
|
|
248
|
+
continuum-removed profile (squares) against the input (circles), and the
|
|
249
|
+
full constraint audit (k0/k1, r², raw depth, weights, thresholds):
|
|
250
|
+
|
|
251
|
+

|
|
252
|
+
|
|
253
|
+
More scenarios — float (`raw=True`) output, custom rule libraries, the
|
|
254
|
+
`invalidate_caches()` contract, component-level calls — in
|
|
255
|
+
[example_usage.py](https://github.com/leecugb/cramm/blob/main/example_usage.py): `python example_usage.py pixel`.
|
|
256
|
+
|
|
257
|
+
## API overview
|
|
258
|
+
|
|
259
|
+
| `MicaEngine` method | Purpose |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `load_emit(path)` | *(EMIT-specific convenience reader)* Read EMIT L2A NetCDF → `(spectrum, lon, lat, w, bp, wl, chanels)`; float32 cube, bad bands removed, fill values zeroed. Not needed for other sensors — supply the same tuple yourself |
|
|
262
|
+
| `spectrum_analysis(spectrum, wl, w, bp, chanels, ...)` | Classify a whole scene → 3 uint8 RGB images; `raw=True` adds `mus_center` + `fd` float arrays. Supports `progress_callback`, `log_callback`, `cancel_flag`, `n_workers` |
|
|
263
|
+
| `classify_spectrum(spectrum, wl, w, bp, chanels, top_n=10, pdf_path=None)` | Identify one spectrum → Top-N `[{"name", "fit", "fd"}]` |
|
|
264
|
+
| `write_tiff(prefix, lon, lat, orth, color, mus)` | Orthorectify (pyresample) and write the 3 GeoTIFFs; requires GDAL |
|
|
265
|
+
| `get_resample(w, bp)` | All reference spectra resampled to the sensor bands `{record_id: spectrum}` (cached) |
|
|
266
|
+
| `invalidate_caches()` | **Required** after mutating `engine.rf` in place — see below |
|
|
267
|
+
|
|
268
|
+
### Custom rule libraries
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
engine = MicaEngine(rf_path="my_rules.json") # at construction
|
|
272
|
+
# — or mutate in place —
|
|
273
|
+
engine.rf["my_mineral"] = {...}
|
|
274
|
+
engine.invalidate_caches() # mandatory!
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The compiled-rule cache is keyed on band configuration only, **not** on rule
|
|
278
|
+
content. If you modify `engine.rf` after any classification call, you must call
|
|
279
|
+
`invalidate_caches()` (or build a new engine) — otherwise results silently use
|
|
280
|
+
the old reference-side constants.
|
|
281
|
+
|
|
282
|
+
## Enhancements over USGS MICA
|
|
283
|
+
|
|
284
|
+
CRAMM extends the original USGS MICA decision rules with an optional per-rule
|
|
285
|
+
**secondary-feature depth-ratio constraint**,
|
|
286
|
+
`max_depth_ratio_feat1_over_feat0`:
|
|
287
|
+
|
|
288
|
+
> After the standard MICA filtering, a rule carrying this key rejects any pixel
|
|
289
|
+
> where `raw_depth(feat1) / raw_depth(feat0) ≥ threshold`, using the
|
|
290
|
+
> *unweighted* feature depths `(1 − min(continuum-removed)) × k0`. Pixels with
|
|
291
|
+
> an invalid primary feature (NaN depth) are conservatively kept.
|
|
292
|
+
|
|
293
|
+
For white micas the primary 2.2 µm Al-OH absorption (feat0) must dominate the
|
|
294
|
+
secondary ~2.35 µm feature (feat1); a secondary absorption that is too deep
|
|
295
|
+
relative to the primary indicates look-alike minerals rather than muscovite /
|
|
296
|
+
illite. Nine bundled rules use this constraint:
|
|
297
|
+
|
|
298
|
+
| Threshold | Rules |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `0.6` | muscovite_lowAl, muscovite_medAl, muscovite_medhighAl, muscovite_Fe-rich, illite_imt1, illite_gds4 |
|
|
301
|
+
| `0.4` | kaolinite.5+muscoviteMedAl.5, kaolinite.5+muscoviteMedhighAl.5, kaolinite+muscovite_mix_intimate |
|
|
302
|
+
|
|
303
|
+
The constraint is part of the rule schema — custom rule libraries can set
|
|
304
|
+
`"max_depth_ratio_feat1_over_feat0": <float>` on any rule with ≥2 diagnostic
|
|
305
|
+
features; omitting the key disables it (original MICA behavior).
|
|
306
|
+
|
|
307
|
+
### Wavelength-based muscovite attribution
|
|
308
|
+
|
|
309
|
+
CRAMM also adds an optional per-feature **absorption-center window**,
|
|
310
|
+
`absorption_center_range` on a rule's first diagnostic feature. After the
|
|
311
|
+
best-match selection, pixels attributed to a rule carrying this field are
|
|
312
|
+
re-arbitrated by their fitted 2.2 µm absorption center (`mus_center`):
|
|
313
|
+
|
|
314
|
+
> If the center falls inside exactly one rule's `[lo, hi)` window, differs
|
|
315
|
+
> from the current match, and that rule itself accepted the pixel, the pixel
|
|
316
|
+
> is reassigned to the matching rule (fit/depth/index follow, and the center
|
|
317
|
+
> is refitted once with the new rule's endpoints). An invalid center, a
|
|
318
|
+
> center outside every window, or a center inside several overlapping
|
|
319
|
+
> windows keeps the original match (conservative).
|
|
320
|
+
|
|
321
|
+
The four bundled pure-muscovite rules carry calibrated windows (anchored on
|
|
322
|
+
each reference spectrum's measured wv2200): medhighAl `[2.195, 2.200)`,
|
|
323
|
+
medAl `[2.200, 2.206)`, lowAl / Fe-rich `[2.206, 2.220)` — the latter two
|
|
324
|
+
share a window, so wavelength never overrides their mutual attribution.
|
|
325
|
+
This is a scene-classification feature; single-spectrum Top-N ranking is
|
|
326
|
+
unaffected. Custom rule libraries opt in by adding the field; rules without
|
|
327
|
+
it are never reassigned.
|
|
328
|
+
|
|
329
|
+
## Application: reading muscovite composition from mus_center
|
|
330
|
+
|
|
331
|
+
The muscovite thematic map's per-pixel `mus_center` (2.2 µm Al-OH absorption
|
|
332
|
+
position) is a quantitative proxy for muscovite chemistry. The phase diagram
|
|
333
|
+
below — a GEMS/MINES23.1 titration reaction-path model of the
|
|
334
|
+
K₂O–Al₂O₃–SiO₂–H₂O–HCl–FeO–MgO system — overlays the Tschermak substitution
|
|
335
|
+
degree X_Ts = X(Fe-Celadonite)+X(Celadonite) in the muscovite stability field
|
|
336
|
+
with the corresponding wv2200 position (USGS conversion chain:
|
|
337
|
+
X_Ts → Al₂O₃ wt% → λ = −3.1·Al₂O₃ + 2308):
|
|
338
|
+
|
|
339
|
+

|
|
340
|
+
|
|
341
|
+
X_Ts rises from ~0 on the high-T / low-K⁺ side to 0.35+ on the low-T / high-K⁺
|
|
342
|
+
side, and the wv2200 contours (magenta, 2190→2215 nm) run nearly parallel to
|
|
343
|
+
the X_Ts contours (dark blue). Each `mus_center` value fitted from an image
|
|
344
|
+
pixel therefore maps directly onto this diagram, inverting muscovite
|
|
345
|
+
composition — and with it formation temperature and fluid K⁺/H⁺ conditions —
|
|
346
|
+
from orbit.
|
|
347
|
+
|
|
348
|
+
## Performance notes
|
|
349
|
+
|
|
350
|
+
- **Precompiled rules**: continuum endpoints, band indices, the reference-side
|
|
351
|
+
normal-equation constant `B` and depth factors are computed once per
|
|
352
|
+
`(wavelengths, FWHM, valid-band)` configuration and reused across all pixels
|
|
353
|
+
and calls.
|
|
354
|
+
- **Parallelism**: scene classification fans out across the 77 rules with
|
|
355
|
+
`multiprocessing` (spawn context); BLAS is pinned to a single thread so the
|
|
356
|
+
parallel path stays bit-identical to the serial one.
|
|
357
|
+
- **Typical runtime**: a full scene (e.g. an EMIT granule, ≈1280×1242 pixels)
|
|
358
|
+
classifies in about a minute with a few workers on a desktop; a warm
|
|
359
|
+
single-spectrum call is ≈10 ms.
|
|
360
|
+
|
|
361
|
+
## Testing
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
python tests/test_core.py # 18 API contract / behavior tests
|
|
365
|
+
# (integration section auto-skips without the test scene)
|
|
366
|
+
python tests/test_custom_rules.py # custom rule-library verification (7 scenarios:
|
|
367
|
+
# rf_path / constraint & window edits / new rules / cache contract)
|
|
368
|
+
python tests/test_parallel_isolation.py # shared-state isolation (6 checks: worker/thread
|
|
369
|
+
# isolation, env restore, temp-file cleanup)
|
|
370
|
+
|
|
371
|
+
# The suites below need the EMIT test scene in the working directory
|
|
372
|
+
# (file name defined in each script's NC constant):
|
|
373
|
+
python tests/check_rows_logic.py # rows alive-pixel semantics (16 checks)
|
|
374
|
+
python tests/test_single_spectrum.py # single-spectrum identification (7 tests:
|
|
375
|
+
# self-ID / noise robustness / determinism / ...)
|
|
376
|
+
python tests/check_compiled_path.py # compiled vs direct path, 302 pixels × 77 rules, bit-level diff
|
|
377
|
+
python tests/test_parallel.py 4 # full-scene golden regression (parallel)
|
|
378
|
+
python tests/test_parallel.py 1 # full-scene golden regression (serial)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**The golden baseline is platform-bound.** `tests/golden_arrays.npz` encodes
|
|
382
|
+
this machine's BLAS results; ulp-level differences across BLAS builds are
|
|
383
|
+
expected. On a new platform — or after an intentional classification-semantics
|
|
384
|
+
change — regenerate the baseline locally with `python regen_golden.py` (runs
|
|
385
|
+
both paths, asserts serial ≡ parallel, then rewrites the golden) before
|
|
386
|
+
relying on `test_parallel.py`.
|
|
387
|
+
|
|
388
|
+
## Troubleshooting
|
|
389
|
+
|
|
390
|
+
- **`netCDF4` fails to open a path containing non-ASCII characters on
|
|
391
|
+
Windows** — a limitation of the netCDF C library, not of CRAMM. `cd` into the
|
|
392
|
+
data directory and use a relative path instead.
|
|
393
|
+
- **`ImportError: gdal`** — you called `write_tiff` without GDAL installed; see
|
|
394
|
+
*Installation*. Classification itself never imports GDAL.
|
|
395
|
+
|
|
396
|
+
## Package layout
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
cramm/
|
|
400
|
+
__init__.py # exports MicaEngine / ProcessResult
|
|
401
|
+
mica_engine.py # facade: resource loading + component wiring + CLI main()
|
|
402
|
+
emit_reader.py # EMIT L2A NetCDF reader + bad-band removal (float32 contract)
|
|
403
|
+
classifier.py # MICA core: resampling / compiled rules / serial & parallel classification
|
|
404
|
+
renderer.py # rendering: three maps / GeoTIFF / single-spectrum PDF diagnostics
|
|
405
|
+
data/ # rf.json + splib06b + color_table.json
|
|
406
|
+
tests/ # bit-exact verification suite + API contract tests
|
|
407
|
+
example_usage.py # five usage-scenario examples
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Requirements
|
|
411
|
+
|
|
412
|
+
- Python 3.9 – 3.13
|
|
413
|
+
- Runtime: `numpy`, `pandas`, `netCDF4`, `pyproj`, `pyresample`, `threadpoolctl`
|
|
414
|
+
- Optional: `gdal` (GeoTIFF), `matplotlib` (PDF diagnostics)
|
|
415
|
+
|
|
416
|
+
## Acknowledgments
|
|
417
|
+
|
|
418
|
+
The decision rules implement the USGS MICA system
|
|
419
|
+
(Kokaly et al., `russet`-era rule set); reference spectra come from the USGS
|
|
420
|
+
splib06b spectral library (Clark et al., 2007). The bundled test scene uses
|
|
421
|
+
EMIT L2A products, courtesy of NASA/JPL.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
benchmark.py
|
|
5
|
+
example_usage.py
|
|
6
|
+
pyproject.toml
|
|
7
|
+
regen_golden.py
|
|
8
|
+
CRAMM.egg-info/PKG-INFO
|
|
9
|
+
CRAMM.egg-info/SOURCES.txt
|
|
10
|
+
CRAMM.egg-info/dependency_links.txt
|
|
11
|
+
CRAMM.egg-info/entry_points.txt
|
|
12
|
+
CRAMM.egg-info/requires.txt
|
|
13
|
+
CRAMM.egg-info/top_level.txt
|
|
14
|
+
cramm/__init__.py
|
|
15
|
+
cramm/classifier.py
|
|
16
|
+
cramm/emit_reader.py
|
|
17
|
+
cramm/mica_engine.py
|
|
18
|
+
cramm/renderer.py
|
|
19
|
+
cramm/data/color_table.json
|
|
20
|
+
cramm/data/rf.json
|
|
21
|
+
cramm/data/splib06b
|
|
22
|
+
docs/muscovite_wv2200_phase_diagram.png
|
|
23
|
+
docs/single_spectrum_diagnostic.png
|
|
24
|
+
tests/check_compiled_path.py
|
|
25
|
+
tests/check_rows_logic.py
|
|
26
|
+
tests/golden_arrays.npz
|
|
27
|
+
tests/test_core.py
|
|
28
|
+
tests/test_custom_rules.py
|
|
29
|
+
tests/test_parallel.py
|
|
30
|
+
tests/test_parallel_isolation.py
|
|
31
|
+
tests/test_single_spectrum.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cramm
|
cramm-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shijie Li
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cramm-1.0.0/MANIFEST.in
ADDED