DisclosureOS documentation · v2 Beta
Lidar point-cloud profile
Validate a point cloud, connect each return to its pulse, and inspect the original source rows.
A lidar point cloud records spatial returns from laser pulses. One pulse can have several returns. This profile preserves that relationship and distinguishes a pulse with no detected return from one whose result is unavailable.
You will reproduce a fictional local survey, validate its two source tables, inspect a point and its pulse, then diagnose three failures. A passing check establishes source integrity and consistency with the declared geometry. It does not establish survey accuracy or identify a material.
Profile status
Published in npm 2.0.0-beta.4. The profile evaluators and readers are available through @disclosureos/schema/experimental/v2. The retained source revision below is only needed to regenerate this example.
Equipment and captures explains the shared equipment model. This optional profile adds a static pulse register, discrete point returns and a local coordinate frame to an observation, acquisition context and source selection. It does not require scalar measurement bindings or claim history.
1. Open the published-package example
Requirements: Node.js 22, npm and a ZIP extractor. Create a local project and install the published API:
mkdir disclosureos-lidar-beta4
cd disclosureos-lidar-beta4
npm init -y
npm install --save-exact @disclosureos/schema@2.0.0-beta.4
curl --fail --location https://os.disclosure.org/examples/meridian.zip --output meridian.zip
unzip meridian.zip
curl --fail --location https://os.disclosure.org/examples/v2/lidar/check-lidar.mjs --output check-lidar.mjs
node check-lidar.mjs meridian/lidar-v1The download contains the full fictional example case. This guide uses only meridian/lidar-v1. Keep the terminal in this npm project for the checks below. The displayed profile and selected-value results are explained in the following sections.
What the profile describes
The optional LidarProfile describes one fixed sensor and a complete rectangular register of pulse directions. It connects that register to a table of discrete returns. It declares the coordinate axes, range bounds, return conventions, intensity scale and class labels.
Four connected documents
| Document | What it provides |
|---|---|
observation.json | Sources and the two retained point-cloud products. |
acquisition.json | Equipment, pinned configuration, return channel and raw artifacts. |
source-selection.json | CSV layouts, column names, row counts, byte lengths and digests. |
lidar-profile.json | References to those documents, product roles, pulse grid and local geometry. |
The profile selects acquisition:lidar-capture and channel returns, whose quantity is lidar_return_position with unit m. Both acquisitionProducts and products contain pulses and points roles. The first maps to acquisition products; the second maps to observation products and source layouts. Matching digests, lengths and media types bind the tables to the capture.
Both products are raw in this fixture. That identifies them as the retained inputs to the example, not as physically collected or unprocessed laser waveforms. dataOrigin: synthetic identifies their actual origin. No processing activity is declared in this packet.
Two source tables
| File | Rows, excluding header | Contents |
|---|---|---|
pulses.csv | 1,353 | Pulse ID, grid row and column, aim coordinates, status and return count. |
points.csv | 1,546 | Pulse ID, return number and count, position, normalized intensity and class code. |
Both files are UTF-8 comma-separated tables with headers. Their exact column order is:
pulse_id,scan_row,scan_column,aim_x_m,aim_y_m,aim_z_m,status,return_countpulse_id,return_number,return_count,x_m,y_m,z_m,intensity,class_codeThe pulse count must equal grid rows × columns. Pulse rows follow row-major order, and IDs are unique. Each point names a retained pulse. Its group must contain every return ordinal from 1 through the declared count, in increasing distance from the sensor. This profile allows up to 15 returns per pulse.
methods.json, model.mjs and README.md describe the authored scene. index.json is a convenience file index, not another standard contract.
2. Validate files and inspect a return
To use the public API separately from fixture reproduction, download the check script. Save it as check-lidar.mjs in your npm project so it uses the installed Beta 4 package.
From your npm project, run:
node check-lidar.mjs \
meridian/lidar-v1The runner imports evaluateLidarProfile from @disclosureos/schema/experimental/v2, loads the four documents and both local tables, then calls:
const result = await evaluateLidarProfile(
observation,
context,
selection,
profile,
{ assets },
);assets maps product:pulses and product:points to complete file bytes. It is not keyed by filename or point index. The runner locates local files through the example index and does not fetch artifact URLs.
The first report is:
{
"success": true,
"checks": {
"structure": "passed",
"sources": "passed",
"semantics": "passed"
},
"scientific": "not_checked",
"physicalCalibration": "not_checked",
"classificationAccuracy": "not_checked",
"georeferencing": "not_checked",
"issues": []
}| Check | Meaning |
|---|---|
structure | All four inputs satisfy their prerequisite contracts. |
sources | Source selections and retained bytes pass their checks, including lengths and digests. |
semantics | Document and acquisition bindings, grid layout, complete return groups, labels, intensity bounds and local ray geometry satisfy the profile. |
Read the issue as well as the stage name: a document-binding error can fail semantics before source checking. All four physical and scientific checks remain not_checked even when success is true.
Only after success, select a point and follow its pulse link:
const { pulses, points } = result.data.products;
const point = points[424];
const pulse = pulses[point.pulseIndex];The runner prints the complete decoded point, a pulse summary and the original CSV rows:
{
"point": {
"index": 424,
"pulseIndex": 459,
"pulseId": "pulse-0459",
"returnNumber": 2,
"returnCount": 3,
"x": -8.74469,
"y": 9.412687,
"z": 3.8,
"intensity": 0.329574,
"classCode": 5,
"rangeM": 19.14962871065831,
"row": {
"byteOffset": 17948,
"byteLength": 47
}
},
"pulse": {
"index": 459,
"id": "pulse-0459",
"status": "retained",
"returnCount": 3,
"pointIndices": [
423,
424,
425
],
"row": {
"byteOffset": 19717,
"byteLength": 46
}
},
"sourceRows": {
"point": "pulse-0459,2,3,-8.74469,9.412687,3.8,0.329574,5",
"pulse": "pulse-0459,11,8,-10.8,11.625,0.4625,retained,3"
}
}Point 424 is the second of three returns from pulse 459. It is not pulse 424. The pulse lists point indices [423, 424, 425], and its source row places it at grid row 11, column 8: 11 × 41 + 8 = 459.
Array indices and byte offsets start at zero. Return ordinals start at one. For these one-line CSV records, point index 424 is file line 426 when the header is line 1. Use the returned byte range for exact source access rather than assuming every CSV record occupies one text line.
The point's bytes occupy [17948, 17995) in points.csv; the pulse's bytes occupy [19717, 19763) in pulses.csv. Each range excludes the row terminator and retains the original cell formatting. rangeM is calculated from the stored coordinates and sensor origin, so it has no independent source column and does not imply a measured flight time.
The dependency-free readLidarProducts reader checks table content and geometry, but does not verify hashes or acquisition links. Prefer the decoded products returned by the successful evaluator when presenting checked data.
Pulses and returns
A point's return number states its position within one pulse's return group. First and last are ordering labels, not surface classifications. A single return is both first and last.
In the selected group, two authored vegetation points precede one terrain point. Their heights are 6.2 m, 3.8 m and 0.4625 m in the local frame. The roof example has a single return classified as structure, so its last return is not terrain.
The ASPRS LAS specification also keeps return number, number of returns, classification and intensity as distinct attributes. This profile uses its own declared class lookup; it does not claim LAS conformance or calibrated LAS intensity.
The viewer's first, last and class filters select retained rows. They do not generate a terrain surface or fill gaps. A selected point outside the current filter remains identified, so a display change does not silently substitute another source.
Inspect this exact return group in the showcase.
Coverage and unavailable results
Keep the three pulse states separate:
| Status | Return count | Meaning |
|---|---|---|
retained | An integer from 1 to 15 | The complete group of declared returns is present. |
no_return | 0 | A recorded result with zero detected returns. |
unavailable | null in the reader; an empty CSV cell | The result is not retained, so its count is unknown. |
The runner's final report compares two actual rows:
{
"availability": [
{
"pulseIndex": 126,
"status": "no_return",
"returnCount": 0,
"pointIndices": [],
"sourceRow": "pulse-0126,3,3,-15.3,4.625,0.21,no_return,0"
},
{
"pulseIndex": 972,
"status": "unavailable",
"returnCount": null,
"pointIndices": [],
"sourceRow": "pulse-0972,23,29,8.1,22.125,1.145,unavailable,"
}
]
}Both have an empty pointIndices array, but they mean different things. Preserve the trailing empty cell in the unavailable row. Converting it with Number("") would incorrectly produce zero.
A no-return result does not prove empty space. An unavailable result cannot be drawn as a zero-return measurement. The pulse register shows declared beam directions, not recovered surfaces or a physical coverage guarantee. Neither its aim coordinates nor missing cells should become interpolated points.
Coordinates and intensity
The frame is right handed and local, in metres: x points toward illustrative east, y toward illustrative north and z upward. The fixed sensor is at [0, 0, 18]. No geographic datum, map location or pulse timestamps are supplied. Row-major order is an index, not a playback clock. The document's publication date does not date the pulses.
An aim coordinate defines a ray from the sensor. It is a direction reference, not necessarily a surface hit. Each retained point must lie forward along its pulse ray, within the declared spatial bounds and between 0.1 m and 60 m from the sensor.
rayToleranceM: 0.00001 permits the small discrepancy introduced by storing decimal coordinates. It is an encoding-consistency tolerance, not ten-micrometre survey accuracy. Range is the three-dimensional Euclidean distance from the fixed sensor, not height above terrain. In the viewer, plan and elevation use equal axis scales; the orthographic oblique projection does not preserve all spatial distances on screen.
Intensity is an authored normalized return amplitude from 0 to 1. It is not calibrated reflectance, laser power, confidence or material identity. Codes 0, 2, 5 and 6 mean Unassigned, Terrain, Vegetation and Structure in this example's lookup. classifications.authority: authored_annotations declares how those labels were assigned; the evaluator checks membership in the lookup, not whether a surface was correctly identified.
The USGS processing requirements treat point classification and geometric accuracy as separate quality concerns. A structurally valid label is not a validated classification.
3. Diagnose a failed check
Each flag changes only in-memory inputs. Use one at a time. Deliberate failures exit with status 1, leave the generated files untouched and stop before selected-point output. An unsupported flag or extra argument exits with usage status 2.
Withhold the pulse register
node check-lidar.mjs \
meridian/lidar-v1 \
--missing-pulsesThe profile reports sources: failed and leaves semantics not_checked:
{
"code": "LIDAR.SOURCE_BYTES",
"pointer": "/products",
"message": "Every retained source table is required for verification."
}The additional source report has external: not_checked and an empty issue list. Missing bytes do not contradict a digest, but the point table alone cannot establish complete pulse coverage or the difference between zero and unknown results. An empty issue list alone is not a completed check.
Change a point without changing its digest
node check-lidar.mjs \
meridian/lidar-v1 \
--changed-pointsThis moves point 424 one metre along local x. File length stays unchanged, but its original digest no longer matches. Source checking fails before point geometry is inspected:
{
"code": "LIDAR.SOURCE.EXTERNAL.DIGEST",
"pointer": "product:points",
"message": "Artifact digest differs."
}The additional source report has external: failed with code EXTERNAL.DIGEST. Its pointer identifies the changed product.
Bind the changed point to matching digests
node check-lidar.mjs \
meridian/lidar-v1 \
--off-rayThis makes the same point change and updates its digest in the in-memory observation product, acquisition artifact and source layout. It deliberately creates consistent file declarations for inconsistent geometry. This is a controlled counterexample, not a repair for unexpected bytes.
structure and sources pass, but semantics fails:
{
"code": "LIDAR.CONTENT",
"pointer": "/products",
"message": "Point lies outside the declared range or pulse ray."
}The changed point is still inside the range and coordinate bounds. It fails because it no longer lies on its pulse's declared ray. A matching hash cannot prove that coordinates and pulse geometry agree. Conversely, passing this geometric check cannot prove real-world accuracy.
Rerun without a flag to restore the passing result; no file restoration is needed.
Exact sources and reproduction
This is a static discrete-return CSV profile, not a general lidar interchange format or LAS/LAZ decoder. It supplies no geographic registration, moving-platform trajectory, per-pulse time, waveform, beam footprint, atmospheric model or calibrated receiver response. Keep original provider files and any conversion process available separately rather than implying that these tables retain every instrument capability.
The downloadable showcase includes this lidar-v1 packet, its model and methods.
For exact definitions at this revision:
- Profile schema: document references, coordinate frame, bounds and availability declarations.
- Profile evaluator: acquisition bindings, source integrity and result stages.
- Source reader: CSV row spans, complete return groups and ray geometry.
The shared equipment field reference covers acquisition and measurement bindings, not this newer profile. Continue to Satellite rasters for georeferenced image cells.