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.
- gobbo/PRD.md +65 -0
- gobbo/__init__.py +154 -0
- gobbo/core/__init__.py +0 -0
- gobbo/core/cli.py +347 -0
- gobbo/core/constants.py +57 -0
- gobbo/core/create_stamp.py +175 -0
- gobbo/core/gnss.py +67 -0
- gobbo/core/gobbo.py +886 -0
- gobbo/core/gobbo_exact.py +452 -0
- gobbo/core/local_voxels/local_voxels.json +1 -0
- gobbo/core/pairwise_stamp.py +154 -0
- gobbo/core/stamp_canonical.py +209 -0
- gobbo/core/stamp_interactions.py +117 -0
- gobbo/core/stamp_models.py +77 -0
- gobbo/core/stamp_sentence.py +119 -0
- gobbo/core/tests.py +356 -0
- gobbo/core/voxel_traverse.py +220 -0
- gobbo/docs/formal/PRIMER.md +176 -0
- gobbo/docs/training/00-why-gobbo.md +83 -0
- gobbo/docs/training/01-nine-dimensions.md +113 -0
- gobbo/docs/training/02-ghn-frame.md +129 -0
- gobbo/docs/training/03-depth-and-voxels.md +137 -0
- gobbo/docs/training/04-stamp-technology.md +123 -0
- gobbo/docs/training/05-first-stamp-tutorial.md +147 -0
- gobbo/docs/training/06-angles-2td-vs-ghn.md +107 -0
- gobbo/docs/training/07-identity-and-rounding.md +96 -0
- gobbo/docs/training/08-collections-and-next.md +82 -0
- gobbo/docs/training/README.md +60 -0
- gobbo/examples/01_lla_to_voxel.py +31 -0
- gobbo/examples/02_create_stamp.py +56 -0
- gobbo/examples/03_ghn_and_2td.py +37 -0
- gobbo/gobbo_sdk_demo.py +38 -0
- gobbo/http_client.py +381 -0
- gobbo/nav/da3_yolo.py +97 -0
- gobbo/nav/init.py +1 -0
- gobbo/nav/testnav.py +97 -0
- gobbo/stamp_http_models.py +83 -0
- gobbo-2.0.0.dist-info/METADATA +224 -0
- gobbo-2.0.0.dist-info/RECORD +44 -0
- gobbo-2.0.0.dist-info/WHEEL +5 -0
- gobbo-2.0.0.dist-info/entry_points.txt +2 -0
- gobbo-2.0.0.dist-info/licenses/LICENSE +201 -0
- gobbo-2.0.0.dist-info/licenses/NOTICE +22 -0
- 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()
|
gobbo/core/constants.py
ADDED
|
@@ -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
|