feat(spatial): add Ouster point cloud upload and import API - #760
feat(spatial): add Ouster point cloud upload and import API#760bonsairobo wants to merge 13 commits into
Conversation
Legal RiskThe following dependencies were released under a license that RecommendationReach out to your security team or Semgrep admin to address this issue. In special cases, exceptions may be made for dependencies with violating licenses, however, the general recommendation is to avoid using a dependency under such a license LGPL-2.1-or-later ZPL-2.1 non-standard |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default mode and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 390fb7b. Configure here.
6be9770 to
49f90ff
Compare
|
Rebased this PR onto current Follow-up commits added here:
Feedback addressed:
Verification run locally:
|
Add a single-function API for the end-to-end workflow: Ouster PCAP dataset → CSV preprocessing → upload → Dagger spatial import. - NominalClient.upload_point_cloud(): uploads a CSV point cloud and triggers spatial import via Scout's spatial service (import-file endpoint) - nominal.thirdparty.ouster.upload_ouster_point_cloud(): orchestrates Ouster PCAP preprocessing then calls upload_point_cloud() for each sensor - nominal.thirdparty.ouster.convert_ouster_dataset(): standalone preprocessing (PCAP → CSV with nav pose correction), usable independently - Register SpatialService in ClientsBunch - Add 'ouster' optional dependency group (ouster-sdk, numpy, pyyaml) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dagger reverse proxy is mounted at `/api/dagger` in combined-service.
Previously `_dagger_base_url` assumed `_api_base_url` already ended in
`/api` and appended a literal `/dagger`. That broke for bare-host base
URLs ("https://api.gov.nominal.io") and silently produced
`/dagger` instead of `/api/dagger`.
Carry the full `/api/dagger` prefix in the constant and strip a trailing
`/api` from the base before appending, so both forms work.
The single-row classifier mis-tagged columns whose first row happens to be integer-valued but later rows are float (`stress=1` then `0.998`), which broke downstream dagger ingest when the typed column failed to accept the float values. Symmetrically, a numeric-looking first row followed by a non-numeric row needs to demote to string rather than fail on the server. Read up to 1000 non-empty data rows and pick the most permissive type that covers every sample value: any non-numeric forces string, any mixed int/float promotes to real.
Reductions are the per-partition aggregations the renderer samples at coarse LOD zoom levels. Without them, Int / Real attributes can't drive `ColorSource::Ramp` (Geometry coloring falls back to solid white) and `VolumetricFilter::ValueRange` is a no-op. Attach Min + Max + Mean to every Int / Real attribute on ingest. Min and Max satisfy ValueRange's two-sided filter; any single reduction is enough to drive Ramp coloring. String attributes have no useful scalar aggregation, so their reductions stay empty.
Sample-based inference still has blind spots: an all-integer-valued
sample for a column that is *semantically* float, or a numeric-looking
string column that the caller knows shouldn't be coerced. Give callers
an escape hatch.
`column_types={"stress": "real"}` forces the named columns to the given
type; unknown column names raise ValueError, invalid type values raise
ValueError, and geometry columns (x/y/z) are silently ignored if
listed.
`SamplerType.MEAN` is not a valid pairing with an Int-typed attribute, so Int attributes get Min + Max only. Real attributes still get Min + Max + Mean. Min/Max alone are sufficient to drive `VolumetricFilter::ValueRange` and `ColorSource::Ramp` for Int columns.
49f90ff to
8435246
Compare
bbf36ae to
5bbb80c
Compare
5bbb80c to
7d18df9
Compare

Adds an end-to-end workflow for spatial point-cloud assets: Ouster PCAP → CSV preprocessing → upload → dagger import → scout spatial resource.
Public surface
nominal.thirdparty.ouster.convert_ouster_dataset(...)— PCAP → CSV with optional nav-pose correction. Standalone, usable independently.nominal.core.upload_point_cloud(client, csv_path, ...)— uploads a point-cloud CSV and creates the corresponding scout spatial resource. Returns the spatial RID.ousteroptional dependency group (ouster-sdk,numpy,pyyaml).dagger-client>=30,<31to core deps.upload_point_cloudkeeps the metadata kwargs the original revision shipped (description,labels,properties,sensor_model,coordinate_system,resolution_mm,scan_pattern).How
upload_point_cloudworksPOST /upload/v1/sign-download(nominal-io/scout#13918) so dagger can pull the file.DaggerSpatialService.ColumnLayout)./api/daggerreverse proxy usingdagger-client:PUT /v1/object-spaces/{workspace.id}?tenant={org_uuid}(idempotent)POST /v1/imports/{model_uuid}?tenant={org_uuid}&object-space={workspace.id}(fire-and-forget, 202)POST /spatial/v1/spatialson scout withdagger_uuid = model_uuid,source_handle = Handle(s3=s3_path), and the metadata kwargs. Returns the spatial RID.tenantis the org RID's UUID locator.object_spaceisworkspace.id(the workspace locator string —dagger-client's type hint saysUUIDbut the runtime stringifies, matching scout's pre-removal Java behaviour).Prerequisites — DRAFT until all three land
This PR is not mergeable until:
importFile, makesdaggerUuidrequired onCreateSpatialRequest, addssourceHandle).POST /upload/v1/sign-downloadonUploadService.nominal-apirelease that regenerates against both.Until then,
CreateSpatialRequest(dagger_uuid=..., source_handle=...)willTypeErrorat runtime (the call carries a# type: ignore[call-arg]), and the presign call uses rawclients.upload._requestinstead of a typedclients.upload.sign_download(...). Both swap to the typed surface post-regen.Test plan
tests/core/test_spatial.py(20 tests): RID locator parsing, geometry / column inference,_classify()heuristics, archetype assembly for the Ouster CSV shape (x, y, z, time, reflectivity, signal, near_infrared), dagger base-URL construction, and the end-to-end mocked flow — assertingCreateSpatialRequestfield mapping, presign request shape, dagger client construction,ImportRequestbody, and the returned spatial RID.just verifygreen on the branch (ruff check,ruff format --check,mypyacross all 145 source files,pytestacross all 272 tests).