Add atl03_signal_class photon-selection parameter (ATL03 R007 signal_class_ph) to atl03x - #651
Add atl03_signal_class photon-selection parameter (ATL03 R007 signal_class_ph) to atl03x#651dshean wants to merge 7 commits into
atl03_signal_class photon-selection parameter (ATL03 R007 signal_class_ph) to atl03x#651Conversation
…ail loudly The yapc score photon-selection threshold was silently inert: the filter comparisons in Atl03DataFrame (atl03x) and Atl03Reader (legacy) compared the photon score against the FieldElement object rather than its value, which resolved through FieldElement's implicit operator bool, so any non-zero score behaved as a threshold of 1 and the parameter had no effect. The comparisons now use the parameter value directly. Also made previously silent failure modes fail loudly: - yapc versions outside 0-3 are rejected when the parameters are parsed - yapc versions 1-3 on atl03x (not implemented there) now raise an alert instead of silently returning all-zero scores - yapc version 0 against pre-release-006 granules (no weight_ph dataset) now raises an alert on both atl03x and the legacy reader instead of silently returning zero scores (which, with the fixed comparison, would have silently filtered out every photon) Documented the score scale: scores read from release 007 granules are the granule weight_ph values on the 0-65535 saturation-normalized scale; release 006 granule scores and server-computed (version 1-3) scores are 0-255. The threshold is compared against the raw values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t in unsupported-version test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qQW14tuisiZycmpdCigeP
The selftest asserted that parms03() returns false for yapc.version=9, but FieldMap catches the parse-time throw, logs a warning, and leaves yapc un-provided; construction succeeds. Surfaced by the first local build and selftest execution of this branch (buildenv container, make selftest). Versions 1-3 on atl03x are still rejected loudly at runtime, which the atl03_dataframe.lua selftests cover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qQW14tuisiZycmpdCigeP
Release 007 of ATL03 includes an experimental per-photon signal classification (signal_class_ph) derived from the weight_ph photon weights, labelling photons as -1:ignored, 0:likely_noise, 1:likely_signal, 2:signal_below, 3:signal_above, 4:primary_signal, 5:fitted_signal, where primary/fitted identify the highest reflecting surface. The new atl03_signal_class parameter mirrors atl08_class: a list of classification names (or numeric values) that drives photon selection in the atl03x dataframe. When requested, signal_class_ph is read from the ATL03 granule (only then; the dataset is photon rate), the selection is applied in the photon selection loop alongside the other criteria (so it composes with cnf, quality_ph, atl08_class, yapc, and the fit and phoreal algorithms), and the classification of each selected photon is returned in the atl03_signal_class column. Requests against pre-007 granules raise an alert since the dataset does not exist there, and the legacy endpoints (which do not implement the selection) reject the parameter instead of silently ignoring it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…03x dataframe The Atl03DataFrame openapi schema is generated from a representative request that enables every optional column family so that all optional columns appear in the published schema. The atl03_signal_class column is only added to the dataframe when at least one classification is enabled (an empty list enables nothing), so a classification is enabled here. The atl03_signal_class request parameter itself needs no schema edits; it is picked up automatically from the addParameter metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@dshean Is there a driving need to a server side signal_class_ph filter? Could this be accomplished via supplying it as an ancillary field in the request and then filtering on the client (web or Python)? |
|
The idea was to use these during the server-side ATL06 fits, instead of the 'ground' classified photons. Basically another way to customize which photons are going into the ATL06 processor. |
|
@dshean I see, that makes sense. Then the change I would want to see in how it is coded is for the enumeration field to default to everything being true so that when the request parameters are represented in the returned dataframe metadata, the filter is correctly captured. Then the code should check to see if anything is disabled and if anything is disabled, then it can conditionally read the signal class ph variable in the hdf5 file. |
The ATL06 Surface Fitter - Signal Classification selftest asserted that the fit on signal-class-selected photons returns no more elevations than the fit on all photons. Noise photons make segments fail the robust fit, so a cleaner selection can yield more rows: on the test granule/polygon cnf=4 alone gives 527 rows vs 415 for cnf=0, and the signal class selection gives 524. Assert instead that the selection changes the fit. Verified on a us-west-2 instance against NSIDC data: atl03_dataframe.lua selftests pass with 1111 asserts and 0 errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCwmYXCh5D3zRAdpE4X1Df
There was a problem hiding this comment.
Pull request overview
Adds a new atl03_signal_class photon-selection parameter to the ICESat-2 atl03x path, enabling server-side filtering on ATL03 R007’s experimental signal_class_ph classifications (and returning the selected class as an output column). This PR also includes the stacked fix work that makes yapc.score filtering effective and ensures unsupported YAPC configurations fail loudly instead of silently degrading results.
Changes:
- Add
atl03_signal_classparameter parsing/serialization (Lua/JSON/index) and integrate selection into the ATL03 photon subsetting loop with R007 release guards. - Add
atl03_signal_classas an opt-in output column and update OpenAPI schema generation to include it. - Add/extend offline + cloud-gated Lua selftests and live-endpoint Python tests; update user guide documentation for new selection and clarified YAPC score scaling.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/rtd/source/user_guide/icesat2.md | Documents atl03_signal_class semantics/limits and clarifies yapc_score scale by ATL03 release/source. |
| clients/python/tests/test_atl03x.py | Adds live-endpoint tests for YAPC score filtering, YAPC unsupported versions, and signal-class filtering/fit behavior. |
| apps/node/scripts/openapi.lua | Enables the optional atl03_signal_class column in the representative schema-generation request. |
| apps/node/datasets/icesat2/selftests/parameters.lua | Adds offline parameter-parse tests for atl03_signal_class (names, numeric values, invalid handling). |
| apps/node/datasets/icesat2/selftests/atl03_dataframe.lua | Adds cloud-gated behavioral tests for YAPC score filtering, unsupported YAPC versions, and signal-class filtering + fit interaction. |
| apps/node/datasets/icesat2/package/Icesat2Parameters.h | Introduces atl03_signal_class_t enum and converter declarations/encoding hook. |
| apps/node/datasets/icesat2/package/Icesat2Parameters.cpp | Implements enum converters for atl03_signal_class_t (Lua/JSON/index). |
| apps/node/datasets/icesat2/package/Atl03Parameters.h | Adds atl03SignalClass field (enumeration) to ATL03 parameters. |
| apps/node/datasets/icesat2/package/Atl03Parameters.cpp | Registers atl03_signal_class; adds parse-time validation for yapc.version. |
| apps/node/datasets/icesat2/package/Atl03DataFrame.h | Adds atl03_signal_class column, signal_class_ph dataset handle, and useSignalClass flag. |
| apps/node/datasets/icesat2/package/Atl03DataFrame.cpp | Conditionally reads signal_class_ph, guards pre-R007 usage, filters photons by class, and fixes YAPC score comparisons to use .value. |
| apps/node/datasets/icesat2/package/Atl03Reader.cpp | Rejects atl03_signal_class on legacy endpoints and fixes YAPC score comparisons to use .value (plus pre-R006 guard). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| assert 0 < len(gdf_sel) <= len(gdf_all) | ||
| assert gdf_sel.n_fit_photons.sum() < gdf_all.n_fit_photons.sum() # fit runs on selected photons only |
|
In-cloud verification of this branch on a us-west-2 Graviton instance (Amazon Linux 2023, sliderule-buildenv:latest container, instance-role auth): full selftest suite = 76 tests, 20 skipped, 4029 asserts. All icesat2 cloud selftests pass, including the new atl03_signal_class cases in atl03_dataframe.lua: on ATL03_20200304065203_10470605_007_01 gt1l over the test polygon the selection reduces 15187 photons to 14161, only classes 4/5 come back, and the pre-007 granule is rejected with zero photons. On the question above: the new ATL06 surface-fitter selftest exercises exactly that server-side use, the fitter consuming the signal-class-selected photons. On this granule/polygon it produces 524 elevations from the 14161 selected photons, versus 415 from all 15187 photons at cnf=0 and 527 from the 13317 photons at cnf=4. One assertion in that test was wrong and is fixed in 258591b: it assumed the fitter returns no more elevations from the signal-class-selected photons than from the unselected set. Fit row counts are not monotonic in photon count (noise photons make segments fail the robust fit), as the cnf=4 vs cnf=0 numbers above show for the existing filter. The test now checks that the selection changes the fit result instead of asserting a direction. |
…ph only when a subset is selected Per review on PR SlideRuleEarth#651: the enumeration now defaults to every classification enabled, so the request parameters recorded in the dataframe metadata always capture the filter. The dataframe reads signal_class_ph (and adds the atl03_signal_class column) only when anyDisabled() is true, i.e. when a subset of classifications is selected; the legacy Atl03Reader check uses the same test so the default no longer trips it. This matches the ATL24 flag fields (default on, anyDisabled() triggers the conditional read). Selecting all seven classifications explicitly is equivalent to the default and is covered by a new selftest case; the parameter description and user guide describe the default. An invalid classification leaves the enumeration empty (fromLua clears it before converting), which excludes every photon with a server-side warning, the same behavior as quality_ph. Verified on a us-west-2 instance against NSIDC data: parameters.lua (37 asserts), atl03_dataframe.lua (1114 asserts) and atl03_reader.lua (7 asserts) pass with 0 errors; full selftest suite 76 tests, 4035 asserts, 1 error (pre-existing 3DEP aspect tolerance, environmental). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCwmYXCh5D3zRAdpE4X1Df
|
Done in d624900: atl03_signal_class now defaults to every classification enabled, so the request parameters recorded in the dataframe metadata carry the full list when no filter is in effect. The reader checks anyDisabled() and only then reads signal_class_ph (and adds the atl03_signal_class column); the legacy atl03s check uses the same test, so the default no longer trips it. Selecting all seven classes explicitly is equivalent to the default (covered by a new selftest case), and the parameter description and user guide say so. One consequence worth a look: an invalid classification name now leaves the enumeration empty (fromLua clears it before converting, and FieldMap's catch keeps that state), so the filter is active and excludes every photon with only a server-side WARNING. That is the same behavior quality_ph has today, so I left it consistent rather than special-casing it; happy to make it a hard rejection instead if you prefer. Verified on the us-west-2 instance: parameters.lua, atl03_dataframe.lua and atl03_reader.lua pass in cloud context, and the full selftest suite is unchanged apart from the environmental cases noted earlier. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
apps/node/datasets/icesat2/selftests/atl03_dataframe.lua:224
- The test’s
min_score_*assertions become vacuous ifcount_rowsreturns 0 rows forscore=1000orscore=8000:min_scorestays at the sentinel value (65535) and still satisfiesmin_score >= threshold. To make this test actually validate the filtering behavior, add explicit assertions thatrows_mid > 0androws_high > 0(or computemin_scoreonly whendf:numrows() > 0and assert accordingly).
local min_score = 65535
for i = 1, math.min(df:numrows(), 1000) do
if df["yapc_score"][i] < min_score then
min_score = df["yapc_score"][i]
end
end
return df:numrows(), min_score
end
local rows_all, min_score_all = count_rows(0)
local rows_mid, min_score_mid = count_rows(1000)
local rows_high, min_score_high = count_rows(8000)
runner.assert(rows_all > 0, "no photons returned for score 0")
runner.assert(rows_mid <= rows_all, string.format("score 1000 did not filter photons: %d > %d", rows_mid, rows_all))
runner.assert(rows_high < rows_all, string.format("score 8000 did not filter photons: %d >= %d", rows_high, rows_all))
runner.assert(rows_high <= rows_mid, string.format("score threshold not monotonic: %d > %d", rows_high, rows_mid))
runner.assert(min_score_mid >= 1000, string.format("photon below score threshold returned: %d < 1000", min_score_mid))
runner.assert(min_score_high >= 8000, string.format("photon below score threshold returned: %d < 8000", min_score_high))
Summary
ATL03 R007 ships an experimental per-photon classification
/gtx/heights/signal_class_ph, derived from the YAPCweight_phweights (−1 ignored, 0 likely_noise, 1 likely_signal, 2 signal_below, 3 signal_above, 4 primary_signal, 5 fitted_signal; primary/fitted mark the highest reflecting surface found per ~5 m fit segment).atl03_ph_fieldscan already deliver the column, but nothing can select on it. This PR adds an opt-inatl03_signal_classparameter mirroring theatl08_classmachinery on atl03x — inert unless supplied, no change to any default behavior — so that e.g.{"atl03_signal_class": ["primary_signal", "fitted_signal"], "fit": {}}produces a server-side surface fit to the granule's own first-reflecting-surface photon selection, with no ATL08 dependency and no threshold to tune.
Motivation and quantitative evaluation — why a scalar
yapc.scorethreshold cannot cover brightness-varying scenes, and how this selection performed in a 383-fit-window seasonal-snow study — are in the companion issue #650.Semantics and limitations (measured; details in #650)
The selection is first-surface by construction. Over exposed ground a fit on it is simply a surface fit; over snow-free dense conifer forest the first surface is the canopy, so a ground-style fit is biased high there (+0.89 m median in the evaluation's snow-off steep-forest windows — users wanting snow-off forest terrain should use
atl08_class: ["atl08_ground"]instead), and conversely a robust fit on this selection is not a canopy-top (DSM) product.signal_class_phis new in R007 and marked experimental, so the parameter fails loudly rather than silently: requests against pre-R007 granules raise a clear alert, and the legacy reader rejects the parameter as unsupported.Implementation
"primary_signal", ...) or the numeric values −1..5;signal_class_phis read only when the parameter is supplied (it is photon-rate, so the read is conditional, like the ATL08 companion reads);cnf,quality_ph,atl08_class, andyapc, i.e. beforefit/phoreal;atl03_signal_classcolumn (int8);addParametermetadata).Files changed
apps/node/datasets/icesat2/package/Icesat2Parameters.{h,cpp}atl03_signal_class_tenum + Lua/JSON/index converters (pattern ofatl08_class_t; index offset likesignal_conf_tfor the −1 value)apps/node/datasets/icesat2/package/Atl03Parameters.{h,cpp}atl03SignalClassfield +atl03_signal_classparameter registrationapps/node/datasets/icesat2/package/Atl03DataFrame.{h,cpp}signal_class_phread; release guard; selection-loop filter;atl03_signal_classcolumnapps/node/datasets/icesat2/package/Atl03Reader.cppatl03_signal_classon legacy endpointsapps/node/scripts/openapi.luaapps/node/datasets/icesat2/selftests/parameters.luaapps/node/datasets/icesat2/selftests/atl03_dataframe.luaclients/python/tests/test_atl03x.pydocs/rtd/source/user_guide/icesat2.mdTesting
Built and tested locally in the project buildenv container (arm64, amazonlinux:2023, per the ARM dev-environment setup): release build compiles clean with no warnings or errors, and
make selftestpasses with 0 errors (76 tests, 907 asserts across the offline suite, including the new parameters.lua tests; the cloud-gatedatl03_dataframe.luatests for this feature skip locally by design and need an in-cloud run). The live-endpoint Python tests are written but require a deployed server carrying this branch. The evaluation numbers referenced in #650 were produced by emulating the selection client-side onatl03_ph_fields ["weight_ph","signal_class_ph"]pulls and applying the identical fit — i.e., they validate the selection semantics this PR exposes, on real R007 granules.Related
yapc.score; its final section records this feature ask) and fix PR Fix inert yapc.score photon filtering and fail loudly on unsupported YAPC configurations (fixes #648) #649 — this branch is stacked on that fix.atl03_signal_classphoton-selection parameter for atl03x (select on ATL03 R007signal_class_ph) #650