gobbo 2.0.0__py3-none-any.whl

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.
Files changed (44) hide show
  1. gobbo/PRD.md +65 -0
  2. gobbo/__init__.py +154 -0
  3. gobbo/core/__init__.py +0 -0
  4. gobbo/core/cli.py +347 -0
  5. gobbo/core/constants.py +57 -0
  6. gobbo/core/create_stamp.py +175 -0
  7. gobbo/core/gnss.py +67 -0
  8. gobbo/core/gobbo.py +886 -0
  9. gobbo/core/gobbo_exact.py +452 -0
  10. gobbo/core/local_voxels/local_voxels.json +1 -0
  11. gobbo/core/pairwise_stamp.py +154 -0
  12. gobbo/core/stamp_canonical.py +209 -0
  13. gobbo/core/stamp_interactions.py +117 -0
  14. gobbo/core/stamp_models.py +77 -0
  15. gobbo/core/stamp_sentence.py +119 -0
  16. gobbo/core/tests.py +356 -0
  17. gobbo/core/voxel_traverse.py +220 -0
  18. gobbo/docs/formal/PRIMER.md +176 -0
  19. gobbo/docs/training/00-why-gobbo.md +83 -0
  20. gobbo/docs/training/01-nine-dimensions.md +113 -0
  21. gobbo/docs/training/02-ghn-frame.md +129 -0
  22. gobbo/docs/training/03-depth-and-voxels.md +137 -0
  23. gobbo/docs/training/04-stamp-technology.md +123 -0
  24. gobbo/docs/training/05-first-stamp-tutorial.md +147 -0
  25. gobbo/docs/training/06-angles-2td-vs-ghn.md +107 -0
  26. gobbo/docs/training/07-identity-and-rounding.md +96 -0
  27. gobbo/docs/training/08-collections-and-next.md +82 -0
  28. gobbo/docs/training/README.md +60 -0
  29. gobbo/examples/01_lla_to_voxel.py +31 -0
  30. gobbo/examples/02_create_stamp.py +56 -0
  31. gobbo/examples/03_ghn_and_2td.py +37 -0
  32. gobbo/gobbo_sdk_demo.py +38 -0
  33. gobbo/http_client.py +381 -0
  34. gobbo/nav/da3_yolo.py +97 -0
  35. gobbo/nav/init.py +1 -0
  36. gobbo/nav/testnav.py +97 -0
  37. gobbo/stamp_http_models.py +83 -0
  38. gobbo-2.0.0.dist-info/METADATA +224 -0
  39. gobbo-2.0.0.dist-info/RECORD +44 -0
  40. gobbo-2.0.0.dist-info/WHEEL +5 -0
  41. gobbo-2.0.0.dist-info/entry_points.txt +2 -0
  42. gobbo-2.0.0.dist-info/licenses/LICENSE +201 -0
  43. gobbo-2.0.0.dist-info/licenses/NOTICE +22 -0
  44. gobbo-2.0.0.dist-info/top_level.txt +1 -0
gobbo/PRD.md ADDED
@@ -0,0 +1,65 @@
1
+ # Gobbo Datum SDK Product Requirements Document (PRD)
2
+
3
+ ## Document Information
4
+ - **Version**: 1.0
5
+ - **Date**: November 25, 2025
6
+ - **Author**: Grok AI (Synthesized from Conversation Specifications)
7
+ - **Status**: Draft for MVP Deployment
8
+ - **Purpose**: This PRD defines the requirements for the Gobbo Datum SDK, an open-source Python tool that acts as "the one datum to rule them all." It converts coordinates from systems like ECEF, LLA, UTM, MGRS, and data formats like LAS (LiDAR), GeoJSON, SRTM GeoTIFF into Gobbo's binary octree paths for alignment, overlap, diff, and fusion. Gobbo is strictly positioning-only: A 1 million km ECEF geo-locked octree with binary subdivision (8 children per level) nested recursively to infinity, using 0-7 labels in concatenated paths (no decimals, base-8 int compression for memory/speed). No data storage, no timestamps, no payloads, no AI analysis, no uploads/downloads, no database integrations—pure mathematical positioning for deterministic, reproducible spatial operations.
9
+
10
+ ## Executive Summary
11
+ Gobbo Datum is the universal voxel translator that unifies any spatial data source into a single, precise framework. By hashing points into compact paths (e.g., "703" as base-8 int 451), it enables seamless alignment and operations across formats without reprojection pains. Built for users to say "This is better because it's in Gobbo," the SDK is a lightweight CLI/package with commands like `gobbo convert las scan.las --depth 20 --out gobbo.vox` and `gobbo-diff a.vox b.vox`. Pure Python (no dependencies), Apache-2.0 licensed OSS for GitHub launch. MVP deploys as pip-installable package, targeting geospatial/robotics devs. Monetize pro features like cloud API ($49/mo). Quick win: Virality through demos fusing drone LAS with SRTM diffs.
12
+
13
+ ## Problem Statement
14
+ Geospatial and robotics users waste hours aligning disparate data (e.g., LiDAR LAS from surveys vs. GeoJSON boundaries vs. SRTM rasters), with errors from datum mismatches (5-10m in UTM) or fixed precision (1cm in SLAM). Existing datums like ECEF are point-based (no volumes), while standards like WGS84 lack infinite scaling. Gobbo solves this by providing a universal volumetric datum: Hash any input to aligned cubes, enabling instant overlap/diff/fuse with bounded errors (cube size halves precision).
15
+
16
+ ## Target Audience and Personas
17
+ - **Primary Segments**: Geospatial analysts (surveying/mining), robotics engineers (precision tasks), AR devs (overlays), environmental scientists (change detection).
18
+ - **Persona 1**: "Jordan Lee," 32, geospatial analyst at a mid-size surveying firm ($10-50M revenue). JTBD: Fuse drone LAS with historical SRTM for volume changes; Pain: 2-4hr reprojection per project; Gain: Gobbo aligns in seconds with verifiable precision.
19
+ - **Persona 2**: "Alex Rivera," 28, robotics dev at a med-tech startup. JTBD: Calibrate robot arms to sub-mm using sensor data; Pain: Sensor drift consumes 30min/shift; Gain: Gobbo's auto-depth bounds errors, reducing rework 10-15%.
20
+
21
+ ## Key Features and Requirements
22
+ ### Core Positioning Engine
23
+ - Req 1.1: gobbo_path: Input ECEF XYZ (km), output base-8 int path, depth. Use LBF IDs (label = 4*[x>=half] + 2*[y>=half] + 1*[z>=half]), shift to positive (add 500k km/axis).
24
+ - Req 1.2: gobbo_to_ecef: Inverse path to center/min/max bounds. Round-trip for validation.
25
+ - Req 1.3: Dynamic depth: error_threshold param auto-computes depth (while size > threshold*2).
26
+ - Req 1.4: Error handling: Validate bounds/depth, log GobboError.
27
+
28
+ ### Multi-System Converters
29
+ - Req 2.1: gobbo_from_any(system, coords, depth/error_threshold): Handle 'ecef', 'lla', 'utm', 'mgrs'. Formulas as discussed (pure math; simplified ellipsoid for MVP).
30
+ - Req 2.2: Alt/hemisphere support for LLA/UTM/MGRS.
31
+
32
+ ### Format Parsers
33
+ - Req 3.1: gobbo_convert --in file --format las/geojson/tiff --depth/error_threshold --out vox.json
34
+ - Req 3.2: LAS: Parse points (loop over binary; no laspy—use struct for header/points).
35
+ - Req 3.3: GeoJSON: Parse features (points/polygons/lines to ECEF; --solid fills volumes).
36
+ - Req 3.4: SRTM GeoTIFF: Parse raster (loop over pixels; elev as alt, grid to points).
37
+ - Req 3.5: Output .vox: JSON array of {path_int, depth} per voxel (compact, loadable).
38
+
39
+ ### Spatial Ops Suite
40
+ - Req 4.1: gobbo_overlap(path1, depth1, path2, depth2): Ratio 0-1 (intersect vol / vol1).
41
+ - Req 4.2: gobbo_diff(vox1, vox2): Volume change (km³) + ratio.
42
+ - Req 4.3: gobbo_fuse(vox1, vox2, cutoff=0.7): Merge if overlap > cutoff; output combined .vox (average if conflicting).
43
+ - Req 4.4: gobbo_distance(path1, depth1, path2, depth2): Euclidean km between centers.
44
+
45
+ ### CLI Interface
46
+ - Req 5.1: argparse for convert/overlap/diff/fuse/distance.
47
+ - Req 5.2: Logging: Info for ops, errors for failures.
48
+ - Req 5.3: --out json/csv for .vox (array of paths).
49
+
50
+ ## Non-Functional Requirements
51
+ - **Performance**: O(depth) per op (<1ms/query); <5s for 100K points.
52
+ - **Memory**: Base-8 ints (8 bytes/path); grid loads in RAM only.
53
+ - **Security**: Offline-only; no network.
54
+ - **Compatibility**: Python 3.6+; pure math.
55
+ - **Testing**: Unit tests for each func (e.g., assert paths for known coords).
56
+ - **Docs**: README.md with examples, install guide, contribution notes.
57
+ - **License**: Apache-2.0 for OSS.
58
+
59
+ ## Deployment and Iteration
60
+ - **MVP Launch**: GitHub repo; pip / PyPI via `pyproject.toml` (`python -m build`).
61
+ - **Versions**: v0.1.0 (core/converters), v0.2.0 (parsers), v1.0 (ops suite).
62
+ - **Monetization**: Free OSS; pro: $49/mo cloud API, plugins.
63
+ - **Validation**: GitHub metrics (stars/downloads); user surveys in README.
64
+
65
+ This PRD is complete, self-contained—feed to AI for code, demos, or expansions.
gobbo/__init__.py ADDED
@@ -0,0 +1,154 @@
1
+ """
2
+ gobbo — the Gobbo 9D Voxel Datum with STAMP Technology.
3
+
4
+ Install: ``pip install gobbo``. Learn: ``gobbo/docs/training/README.md``
5
+ (lessons 0–8 ship inside this package) and ``gobbo/docs/formal/PRIMER.md``.
6
+
7
+ Copyright 2026 Gobbo Datum Labs. Inventor / original technical authorship: Adam Gobbo.
8
+ Licensed under Apache-2.0; see LICENSE, NOTICE, and ATTRIBUTION.md in the package root.
9
+ """
10
+
11
+ __version__ = "2.0.0"
12
+
13
+ from gobbo.core.gobbo import (
14
+ GobboError,
15
+ gobbo_center_ecef_pm,
16
+ gobbo_depth_for_precision,
17
+ gobbo_depth_for_precision_lla,
18
+ gobbo_from_ecef,
19
+ gobbo_from_ecef_pm,
20
+ gobbo_from_lla,
21
+ gobbo_ghn_basis,
22
+ gobbo_horizon_plane,
23
+ gobbo_min_max_pm,
24
+ gobbo_ned_basis,
25
+ gobbo_radial_vector,
26
+ gobbo_to_ecef,
27
+ gobbo_to_ecef_meters,
28
+ gobbo_to_lla,
29
+ gobbo_voxel_to_ned,
30
+ gobbo_voxels_to_ned,
31
+ path_int_to_path_oct,
32
+ path_oct_to_path_int,
33
+ )
34
+ from gobbo.core.create_stamp import GobboStampRecord, create_stamp
35
+ from gobbo.core.stamp_canonical import (
36
+ CanonicalStampInputV2,
37
+ StampCanonicalV2Error,
38
+ compute_stamp_id_v2,
39
+ js_number_string,
40
+ to_canonical_string_v2,
41
+ )
42
+ from gobbo.core.pairwise_stamp import (
43
+ EPSILON_M,
44
+ direction_unit_ecef_or_none,
45
+ ecef_delta_from_gobbo_az_tilt_distance,
46
+ euclidean_distance_m,
47
+ gobbo_azimuth_deg,
48
+ gobbo_azimuth_tilt_distance_m,
49
+ gobbo_tilt_deg,
50
+ linear_point_expected_along_segment,
51
+ new_pairwise_lines_on_stamp_append,
52
+ pairwise_line_count,
53
+ parent_mean_from_eight_children,
54
+ parent_partial_mean_from_known_children,
55
+ )
56
+ from gobbo.core.stamp_interactions import (
57
+ build_interaction_row,
58
+ compute_delta_t_seconds,
59
+ compute_delta_v,
60
+ compute_interaction_metrics,
61
+ )
62
+ from gobbo.core.gobbo import gobbo_to_mgrs, gobbo_to_utm
63
+ from gobbo.core.stamp_models import (
64
+ ALLOWED_FACT_KINDS,
65
+ DEFAULT_FACT_KIND,
66
+ FACT_KIND_CORRECTION,
67
+ FACT_KIND_DERIVED_MEASUREMENT,
68
+ FACT_KIND_ESTIMATE,
69
+ FACT_KIND_LABEL,
70
+ FACT_KIND_RAW_MEASUREMENT,
71
+ StampFact,
72
+ StampInteractionRow,
73
+ StampSpatial,
74
+ )
75
+ from gobbo.core.stamp_sentence import (
76
+ STAMP_SENTENCE_TEMPLATE,
77
+ build_gobbo_sentence,
78
+ build_stamp_sentence,
79
+ )
80
+ from gobbo.core.voxel_traverse import (
81
+ TraversalStep,
82
+ VoxelTraverseError,
83
+ ecef_to_gobbo,
84
+ path_int_from_grid_cell,
85
+ traverse_voxels_between_ecef,
86
+ )
87
+
88
+ __all__ = [
89
+ "__version__",
90
+ # Voxel datum core
91
+ "GobboError",
92
+ "gobbo_center_ecef_pm",
93
+ "gobbo_depth_for_precision",
94
+ "gobbo_depth_for_precision_lla",
95
+ "gobbo_from_ecef",
96
+ "gobbo_from_ecef_pm",
97
+ "gobbo_from_lla",
98
+ "gobbo_ghn_basis",
99
+ "gobbo_horizon_plane",
100
+ "gobbo_min_max_pm",
101
+ "gobbo_ned_basis",
102
+ "gobbo_radial_vector",
103
+ "gobbo_to_ecef",
104
+ "gobbo_to_ecef_meters",
105
+ "gobbo_to_lla",
106
+ "gobbo_voxel_to_ned",
107
+ "gobbo_voxels_to_ned",
108
+ "path_int_to_path_oct",
109
+ "path_oct_to_path_int",
110
+ # STAMP minting + canonical identity
111
+ "GobboStampRecord",
112
+ "create_stamp",
113
+ "CanonicalStampInputV2",
114
+ "StampCanonicalV2Error",
115
+ "compute_stamp_id_v2",
116
+ "js_number_string",
117
+ "to_canonical_string_v2",
118
+ "STAMP_SENTENCE_TEMPLATE",
119
+ "build_stamp_sentence",
120
+ "EPSILON_M",
121
+ "direction_unit_ecef_or_none",
122
+ "ecef_delta_from_gobbo_az_tilt_distance",
123
+ "euclidean_distance_m",
124
+ "gobbo_azimuth_deg",
125
+ "gobbo_azimuth_tilt_distance_m",
126
+ "gobbo_tilt_deg",
127
+ "linear_point_expected_along_segment",
128
+ "new_pairwise_lines_on_stamp_append",
129
+ "pairwise_line_count",
130
+ "parent_mean_from_eight_children",
131
+ "parent_partial_mean_from_known_children",
132
+ "gobbo_to_mgrs",
133
+ "gobbo_to_utm",
134
+ "ALLOWED_FACT_KINDS",
135
+ "DEFAULT_FACT_KIND",
136
+ "FACT_KIND_CORRECTION",
137
+ "FACT_KIND_DERIVED_MEASUREMENT",
138
+ "FACT_KIND_ESTIMATE",
139
+ "FACT_KIND_LABEL",
140
+ "FACT_KIND_RAW_MEASUREMENT",
141
+ "StampFact",
142
+ "StampInteractionRow",
143
+ "StampSpatial",
144
+ "build_gobbo_sentence",
145
+ "build_interaction_row",
146
+ "compute_delta_t_seconds",
147
+ "compute_delta_v",
148
+ "compute_interaction_metrics",
149
+ "TraversalStep",
150
+ "VoxelTraverseError",
151
+ "ecef_to_gobbo",
152
+ "path_int_from_grid_cell",
153
+ "traverse_voxels_between_ecef",
154
+ ]
gobbo/core/__init__.py ADDED
File without changes
gobbo/core/cli.py ADDED
@@ -0,0 +1,347 @@
1
+ import argparse
2
+ from dataclasses import asdict
3
+ from gobbo import __version__
4
+ from gobbo.core.gobbo import (
5
+ gobbo_from_ecef, gobbo_from_lla, gobbo_from_utm, gobbo_from_mgrs,
6
+ gobbo_from_las, gobbo_from_geojson, gobbo_from_srtm,
7
+ gobbo_overlap, gobbo_diff, gobbo_fuse, gobbo_distance, GobboError, gobbo_radial_vector,
8
+ gobbo_ned_basis, gobbo_horizon_plane, gobbo_voxel_to_ned, gobbo_voxels_to_ned, gobbo_to_nmea_ggga
9
+ )
10
+ from gobbo.core.gnss import gobbo_ppp_fuse
11
+ from gobbo.core.stamp_interactions import build_interaction_row
12
+ from gobbo.core.stamp_models import StampFact, StampSpatial
13
+ from gobbo.core.stamp_sentence import build_gobbo_sentence
14
+ import json
15
+ import sys
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser(description='Gobbo Datum CLI - Universal Positioning Converter')
19
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
20
+ subparsers = parser.add_subparsers(dest='command', required=True)
21
+
22
+ # Convert from coords
23
+ convert = subparsers.add_parser('convert', help='Convert coordinates to Gobbo path')
24
+ convert.add_argument('system', choices=['ecef', 'lla', 'utm', 'mgrs'], help='Coordinate system')
25
+ convert.add_argument('coords', nargs='+', help='Coordinates (e.g., x y z for ecef)')
26
+ convert.add_argument('--depth', type=int, default=10, help='Recursion depth')
27
+ convert.add_argument('--error_threshold', type=float, help='Error threshold for auto-depth (km)')
28
+ convert.add_argument('--alt', type=float, default=0, help='Altitude for lla/utm/mgrs')
29
+ convert.add_argument('--hemisphere', default='N', help='Hemisphere for utm (N/S)')
30
+
31
+ # Convert from file
32
+ convert_file = subparsers.add_parser('convert-file', help='Convert file to Gobbo voxels')
33
+ convert_file.add_argument('format', choices=['las', 'geojson', 'hgt'], help='Input format')
34
+ convert_file.add_argument('in_file', help='Input file path')
35
+ convert_file.add_argument('--depth', type=int, default=10, help='Recursion depth')
36
+ convert_file.add_argument('--error_threshold', type=float, help='Error threshold (km)')
37
+ convert_file.add_argument('--solid', action='store_true', help='Fill solids for GeoJSON')
38
+ convert_file.add_argument('--out', help='Output JSON file')
39
+
40
+ # Overlap
41
+ overlap = subparsers.add_parser('overlap', help='Compute voxel overlap ratio')
42
+ overlap.add_argument('path1', type=int, help='Path int 1')
43
+ overlap.add_argument('depth1', type=int, help='Depth 1')
44
+ overlap.add_argument('path2', type=int, help='Path int 2')
45
+ overlap.add_argument('depth2', type=int, help='Depth 2')
46
+
47
+ # Diff
48
+ diff = subparsers.add_parser('diff', help='Compute volume diff between voxel sets')
49
+ diff.add_argument('vox1', help='.vox file1')
50
+ diff.add_argument('vox2', help='.vox file2')
51
+
52
+ # Fuse
53
+ fuse = subparsers.add_parser('fuse', help='Fuse voxel sets')
54
+ fuse.add_argument('vox1', help='.vox file1')
55
+ fuse.add_argument('vox2', help='.vox file2')
56
+ fuse.add_argument('--cutoff', type=float, default=0.7, help='Overlap cutoff')
57
+ fuse.add_argument('--out', help='Output JSON file')
58
+
59
+ # Distance
60
+ distance = subparsers.add_parser('distance', help='Compute distance between voxel centers')
61
+ distance.add_argument('path1', type=int, help='Path int 1')
62
+ distance.add_argument('depth1', type=int, help='Depth 1')
63
+ distance.add_argument('path2', type=int, help='Path int 2')
64
+ distance.add_argument('depth2', type=int, help='Depth 2')
65
+
66
+ # Radial vector
67
+ radial = subparsers.add_parser('radial', help='Compute radial vector')
68
+ radial.add_argument('path', type=int, help='Path int')
69
+ radial.add_argument('depth', type=int, help='Depth')
70
+
71
+ # NED basis
72
+ ned = subparsers.add_parser('ned', help='Compute NED basis')
73
+ ned.add_argument('path', type=int, help='Path int')
74
+ ned.add_argument('depth', type=int, help='Depth')
75
+
76
+ # Horizon plane
77
+ horizon = subparsers.add_parser('horizon', help='Compute horizon plane')
78
+ horizon.add_argument('path', type=int, help='Path int')
79
+ horizon.add_argument('depth', type=int, help='Depth')
80
+
81
+ # Voxel to NED
82
+ voxel_ned = subparsers.add_parser('voxel-ned', help='Convert voxel to NED relative to ref')
83
+ voxel_ned.add_argument('ref_path', type=int, help='Ref path int')
84
+ voxel_ned.add_argument('ref_depth', type=int, help='Ref depth')
85
+ voxel_ned.add_argument('target_path', type=int, help='Target path int')
86
+ voxel_ned.add_argument('target_depth', type=int, help='Target depth')
87
+
88
+ # Voxels to NED
89
+ voxels_ned = subparsers.add_parser('voxels-ned', help='Batch convert voxels to NED')
90
+ voxels_ned.add_argument('ref_path', type=int, help='Ref path int')
91
+ voxels_ned.add_argument('ref_depth', type=int, help='Ref depth')
92
+ voxels_ned.add_argument('targets', nargs='+', help='Target path:depth pairs, e.g., 123:10 456:15')
93
+
94
+ # NMEA
95
+ nmea = subparsers.add_parser('nmea', help='Generate NMEA GGA from voxel')
96
+ nmea.add_argument('path', type=int, help='Path int')
97
+ nmea.add_argument('depth', type=int, help='Depth')
98
+ nmea.add_argument('--utc_time', default='123456.00', help='UTC time')
99
+ nmea.add_argument('--hdop', type=float, default=1.0, help='HDOP')
100
+ nmea.add_argument('--fix_quality', type=int, default=1, help='Fix quality')
101
+ nmea.add_argument('--num_sat', type=int, default=8, help='Num satellites')
102
+ nmea.add_argument('--age', type=float, default=1.0, help='Age')
103
+ nmea.add_argument('--station_id', type=int, default=0, help='Station ID')
104
+
105
+ # PPP-refine
106
+ ppp = subparsers.add_parser('ppp-refine', help='Refine voxels via PPP baseline')
107
+ ppp.add_argument('vox1', help='.vox file1')
108
+ ppp.add_argument('--vox2', help='.vox file2 (optional, defaults to vox1)')
109
+ ppp.add_argument('obs', help='RINEX obs file1')
110
+ ppp.add_argument('--obs2', help='RINEX obs file2 (optional, defaults to obs)')
111
+ ppp.add_argument('--out', default='refined.vox', help='Output JSON file')
112
+
113
+ # Map-scene
114
+ map_scene_parser = subparsers.add_parser('map-scene', help='Map image to labeled Gobbo voxels')
115
+ map_scene_parser.add_argument('image', help='RGB image file')
116
+ map_scene_parser.add_argument('--gps', nargs=3, type=float, help='LLA: lat lon alt')
117
+ map_scene_parser.add_argument('--depth', type=int, default=40, help='Voxel depth')
118
+ map_scene_parser.add_argument('--out', default='voxels.json', help='Output JSON')
119
+
120
+ # STAMP sentence builder
121
+ stamp_sentence = subparsers.add_parser(
122
+ 'stamp-sentence',
123
+ help='Build canonical STAMP sentence from fields',
124
+ )
125
+ stamp_sentence.add_argument('--path-int', type=int, required=True, help='STAMP spatial pathInt')
126
+ stamp_sentence.add_argument('--depth', type=int, required=True, help='STAMP depth')
127
+ stamp_sentence.add_argument('--path-oct', help='Optional canonical pathOct')
128
+ stamp_sentence.add_argument('--temporal', required=True, help='ISO timestamp for the STAMP')
129
+ stamp_sentence.add_argument('--assignment', required=True, help='Assignment field')
130
+ stamp_sentence.add_argument('--metric', required=True, help='Metric field')
131
+ stamp_sentence.add_argument('--point', type=float, help='Numeric point value')
132
+ stamp_sentence.add_argument('--point-value', help='Non-numeric point value fallback')
133
+ stamp_sentence.add_argument('--stamp-id', help='Optional stable STAMP identifier')
134
+ stamp_sentence.add_argument(
135
+ '--fact-kind',
136
+ default='raw_measurement',
137
+ help='Fact kind (B.12): raw_measurement | derived_measurement | estimate | label | correction',
138
+ )
139
+
140
+ # STAMP interaction row calculator
141
+ stamp_interaction = subparsers.add_parser(
142
+ 'stamp-interaction',
143
+ help='Compute deterministic interaction row between two STAMP facts',
144
+ )
145
+ stamp_interaction.add_argument('--anchor-path-int', type=int, required=True, help='Anchor pathInt')
146
+ stamp_interaction.add_argument('--anchor-depth', type=int, required=True, help='Anchor depth')
147
+ stamp_interaction.add_argument('--anchor-path-oct', help='Optional anchor pathOct')
148
+ stamp_interaction.add_argument('--anchor-time', required=True, help='Anchor timestamp (ISO)')
149
+ stamp_interaction.add_argument('--anchor-assignment', required=True, help='Anchor assignment')
150
+ stamp_interaction.add_argument('--anchor-metric', required=True, help='Anchor metric')
151
+ stamp_interaction.add_argument('--anchor-point', type=float, help='Anchor numeric point')
152
+ stamp_interaction.add_argument('--anchor-point-value', help='Anchor non-numeric point value')
153
+ stamp_interaction.add_argument('--anchor-stamp-id', help='Anchor stamp identifier')
154
+ stamp_interaction.add_argument(
155
+ '--anchor-ecef-m',
156
+ nargs=3,
157
+ type=float,
158
+ required=True,
159
+ metavar=('X', 'Y', 'Z'),
160
+ help='Anchor ECEF coordinates in meters',
161
+ )
162
+ stamp_interaction.add_argument('--neighbor-path-int', type=int, required=True, help='Neighbor pathInt')
163
+ stamp_interaction.add_argument('--neighbor-depth', type=int, required=True, help='Neighbor depth')
164
+ stamp_interaction.add_argument('--neighbor-path-oct', help='Optional neighbor pathOct')
165
+ stamp_interaction.add_argument('--neighbor-time', required=True, help='Neighbor timestamp (ISO)')
166
+ stamp_interaction.add_argument('--neighbor-assignment', required=True, help='Neighbor assignment')
167
+ stamp_interaction.add_argument('--neighbor-metric', required=True, help='Neighbor metric')
168
+ stamp_interaction.add_argument('--neighbor-point', type=float, help='Neighbor numeric point')
169
+ stamp_interaction.add_argument('--neighbor-point-value', help='Neighbor non-numeric point value')
170
+ stamp_interaction.add_argument('--neighbor-stamp-id', help='Neighbor stamp identifier')
171
+ stamp_interaction.add_argument(
172
+ '--neighbor-ecef-m',
173
+ nargs=3,
174
+ type=float,
175
+ required=True,
176
+ metavar=('X', 'Y', 'Z'),
177
+ help='Neighbor ECEF coordinates in meters',
178
+ )
179
+
180
+ args = parser.parse_args()
181
+
182
+ try:
183
+ if args.command == 'convert':
184
+ if args.system == 'ecef':
185
+ x, y, z = map(float, args.coords[:3])
186
+ path, depth = gobbo_from_ecef(x, y, z, args.depth, args.error_threshold)
187
+ elif args.system == 'lla':
188
+ lat, lon = map(float, args.coords[:2])
189
+ path, depth = gobbo_from_lla(lat, lon, args.alt, args.depth, args.error_threshold)
190
+ elif args.system == 'utm':
191
+ zone = int(args.coords[0])
192
+ easting, northing = map(float, args.coords[1:3])
193
+ path, depth = gobbo_from_utm(zone, easting, northing, args.hemisphere, args.alt, args.depth, args.error_threshold)
194
+ elif args.system == 'mgrs':
195
+ mgrs_str = args.coords[0]
196
+ path, depth = gobbo_from_mgrs(mgrs_str, args.alt, args.depth, args.error_threshold)
197
+ print(f"Path: {path}, Depth: {depth}")
198
+
199
+ elif args.command == 'convert-file':
200
+ if args.format == 'las':
201
+ voxels = gobbo_from_las(args.in_file, args.depth, args.error_threshold)
202
+ elif args.format == 'geojson':
203
+ voxels = gobbo_from_geojson(args.in_file, args.depth, args.error_threshold, args.solid)
204
+ elif args.format == 'hgt':
205
+ voxels = gobbo_from_srtm(args.in_file, args.depth, args.error_threshold)
206
+ if args.out:
207
+ with open(args.out, 'w') as f:
208
+ json.dump(voxels, f)
209
+ print(f"Output written to {args.out}")
210
+ else:
211
+ print(json.dumps(voxels[:10])) # Sample
212
+
213
+ elif args.command == 'overlap':
214
+ ratio = gobbo_overlap(args.path1, args.depth1, args.path2, args.depth2)
215
+ print(f"Overlap ratio: {ratio}")
216
+
217
+ elif args.command == 'diff':
218
+ with open(args.vox1, 'r') as f1, open(args.vox2, 'r') as f2:
219
+ v1 = json.load(f1)
220
+ v2 = json.load(f2)
221
+ change, ratio = gobbo_diff(v1, v2)
222
+ print(f"Volume change: {change} km³, Ratio: {ratio}")
223
+
224
+ elif args.command == 'fuse':
225
+ with open(args.vox1, 'r') as f1, open(args.vox2, 'r') as f2:
226
+ v1 = json.load(f1)
227
+ v2 = json.load(f2)
228
+ fused = gobbo_fuse(v1, v2, args.cutoff)
229
+ if args.out:
230
+ with open(args.out, 'w') as f:
231
+ json.dump(fused, f)
232
+ print(f"Fused output to {args.out}")
233
+ else:
234
+ print(json.dumps(fused[:10]))
235
+
236
+ elif args.command == 'distance':
237
+ dist = gobbo_distance(args.path1, args.depth1, args.path2, args.depth2)
238
+ print(f"Distance: {dist} km")
239
+
240
+ elif args.command == 'radial':
241
+ vec = gobbo_radial_vector(args.path, args.depth)
242
+ print(vec)
243
+
244
+ elif args.command == 'ned':
245
+ north, east, down = gobbo_ned_basis(args.path, args.depth)
246
+ print(f"North: {north}\nEast: {east}\nDown: {down}")
247
+
248
+ elif args.command == 'horizon':
249
+ normal, point = gobbo_horizon_plane(args.path, args.depth)
250
+ print(f"Normal: {normal}\nPoint: {point}")
251
+
252
+ elif args.command == 'voxel-ned':
253
+ n, e, d = gobbo_voxel_to_ned(args.ref_path, args.ref_depth, args.target_path, args.target_depth)
254
+ print(f"N: {n}, E: {e}, D: {d}")
255
+
256
+ elif args.command == 'voxels-ned':
257
+ targets = [tuple(map(int, t.split(':'))) for t in args.targets]
258
+ neds = gobbo_voxels_to_ned(args.ref_path, args.ref_depth, targets)
259
+ print(json.dumps(neds))
260
+
261
+ elif args.command == 'nmea':
262
+ sentence = gobbo_to_nmea_ggga(args.path, args.depth, args.utc_time, args.hdop, args.fix_quality, args.num_sat, args.age, args.station_id)
263
+ print(sentence)
264
+
265
+ elif args.command == 'ppp-refine':
266
+ with open(args.vox1, 'r') as f1:
267
+ v1 = json.load(f1)
268
+ v2 = json.load(open(args.vox2, 'r')) if args.vox2 else v1
269
+ obs2 = args.obs2 if args.obs2 else args.obs
270
+ fused = gobbo_ppp_fuse(v1, v2, args.obs, obs2)
271
+ with open(args.out, 'w') as f:
272
+ json.dump(fused, f)
273
+ print(f"Refined voxels written to {args.out}")
274
+
275
+ elif args.command == 'map-scene':
276
+ import cv2
277
+ from gobbo.nav.da3_yolo import scene_map
278
+
279
+ rgb = cv2.imread(args.image)
280
+ if rgb is None:
281
+ print("Error: Invalid image file")
282
+ sys.exit(1)
283
+ voxels = scene_map(rgb, gps_lla=args.gps, depth=args.depth)
284
+ with open(args.out, 'w') as f:
285
+ json.dump(voxels, f)
286
+ print(f"Mapped {len(voxels)} voxels to {args.out}")
287
+
288
+ elif args.command == 'stamp-sentence':
289
+ stamp = StampFact(
290
+ spatial=StampSpatial(path_int=args.path_int, depth=args.depth, path_oct=args.path_oct),
291
+ temporal=args.temporal,
292
+ assignment=args.assignment,
293
+ metric=args.metric,
294
+ point=args.point,
295
+ point_value=args.point_value,
296
+ stamp_id=args.stamp_id,
297
+ fact_kind=args.fact_kind,
298
+ )
299
+ print(build_gobbo_sentence(stamp))
300
+
301
+ elif args.command == 'stamp-interaction':
302
+ anchor = StampFact(
303
+ spatial=StampSpatial(
304
+ path_int=args.anchor_path_int,
305
+ depth=args.anchor_depth,
306
+ path_oct=args.anchor_path_oct,
307
+ ),
308
+ temporal=args.anchor_time,
309
+ assignment=args.anchor_assignment,
310
+ metric=args.anchor_metric,
311
+ point=args.anchor_point,
312
+ point_value=args.anchor_point_value,
313
+ stamp_id=args.anchor_stamp_id,
314
+ )
315
+ neighbor = StampFact(
316
+ spatial=StampSpatial(
317
+ path_int=args.neighbor_path_int,
318
+ depth=args.neighbor_depth,
319
+ path_oct=args.neighbor_path_oct,
320
+ ),
321
+ temporal=args.neighbor_time,
322
+ assignment=args.neighbor_assignment,
323
+ metric=args.neighbor_metric,
324
+ point=args.neighbor_point,
325
+ point_value=args.neighbor_point_value,
326
+ stamp_id=args.neighbor_stamp_id,
327
+ )
328
+ row = build_interaction_row(
329
+ anchor=anchor,
330
+ neighbor=neighbor,
331
+ anchor_ecef_m=tuple(args.anchor_ecef_m),
332
+ neighbor_ecef_m=tuple(args.neighbor_ecef_m),
333
+ )
334
+ print(json.dumps(asdict(row), ensure_ascii=True))
335
+
336
+ except GobboError as e:
337
+ print(f"Error: {e}")
338
+ sys.exit(1)
339
+ except ValueError as e:
340
+ print(f"Input error: {e}")
341
+ sys.exit(1)
342
+ except Exception as e:
343
+ print(f"Unexpected error: {e}")
344
+ sys.exit(1)
345
+
346
+ if __name__ == '__main__':
347
+ main()
@@ -0,0 +1,57 @@
1
+ """
2
+ Frozen ``gobbo:v1`` constants — single Python source (parity with ``lib/gobbo.ts`` / ``lib/datum-conformance.ts``).
3
+
4
+ Importing this module asserts SHA-256 of the pipe-delimited fingerprint matches ``DATUM_PARAMS_HASH``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+
11
+ # WGS-84 ellipsoid (meters)
12
+ WGS84_A = 6378137.0
13
+ WGS84_B = 6356752.314245
14
+
15
+ # Root cube (meters) — frozen
16
+ ROOT_METERS = 1_000_000_000.0
17
+ CENTER_METERS = ROOT_METERS / 2.0
18
+
19
+ # Legacy ``gobbo.py`` octree fold uses ECEF expressed in **kilometers** after ``/ 1000``.
20
+ ROOT_KM = ROOT_METERS / 1000.0
21
+ CENTER_KM = CENTER_METERS / 1000.0
22
+
23
+ MILLIMETER_FACTOR = 1000
24
+ MAX_HIGH_DEPTH = 60
25
+ CONTAINMENT_MARGIN_FACTOR = 0.15
26
+
27
+ DATUM_ID = "gobbo:v1"
28
+
29
+ # Must match ``lib/stamp-canonical.ts`` / ``docs/GOBBO_DATUM_V1_FROZEN_CONSTANTS.md``
30
+ DATUM_PARAMS_HASH = "4314894cc5878ba496064f57444fc74b2dcb950a59c30939c00824ddb49a5e3e"
31
+
32
+
33
+ def _gobbo_v1_datum_params_fingerprint() -> str:
34
+ return "|".join(
35
+ [
36
+ "6378137.0",
37
+ "6356752.314245",
38
+ f"{ROOT_METERS:.1f}",
39
+ f"{CENTER_METERS:.1f}",
40
+ str(MAX_HIGH_DEPTH),
41
+ str(MILLIMETER_FACTOR),
42
+ str(CONTAINMENT_MARGIN_FACTOR),
43
+ ]
44
+ )
45
+
46
+
47
+ _computed = hashlib.sha256(_gobbo_v1_datum_params_fingerprint().encode("utf-8")).hexdigest()
48
+ if _computed != DATUM_PARAMS_HASH:
49
+ raise RuntimeError(
50
+ f"gobbo:v1 frozen-constant drift: expected datum_params_hash {DATUM_PARAMS_HASH}, "
51
+ f"computed {_computed}. Fingerprint was: {_gobbo_v1_datum_params_fingerprint()}"
52
+ )
53
+
54
+
55
+ def assert_gobbo_v1_datum_params_conformant() -> None:
56
+ """No-op at runtime; module import already validated. For explicit tests."""
57
+ return None