@@ -9,6 +9,13 @@ const MAX_RESOLUTION_TO_COMPARE: i32 = 9;
99const MAX_COMMAND_OUTPUT_BYTES : usize = 512 * 1024 * 1024 ;
1010const MAX_POLYGON_SAMPLES_PER_RESOLUTION : usize = 32 ;
1111const POLYGON_TOLERANCE_RADIANS : f64 = 5e-5 ;
12+ const DEEP_RANDOM_PARITY_RESOLUTION : i32 = 25 ;
13+ const DEEP_RANDOM_PARITY_BATCH_SIZE : usize = 32 ;
14+ const DEEP_RANDOM_PARITY_DURATION_NS : i128 = 10 * std .time .ns_per_s ;
15+ const DEEP_RANDOM_PARITY_SEED : u64 = 0x00d3_3e5e_0a4c_1e25 ;
16+ const DEEP_RANDOM_POINT_MIN_T : f64 = 0.000001 ;
17+ const DEEP_RANDOM_POINT_MAX_T : f64 = 0.000005 ;
18+ const DEEP_RANDOM_POINT_CONTAINMENT_EPSILON : f64 = 1e-8 ;
1219
1320const CommandOutput = struct {
1421 stdout : []const u8 ,
@@ -40,6 +47,33 @@ const PointMismatch = struct {
4047 distance_radians : f64 ,
4148};
4249
50+ fn randomCellAtResolution (
51+ random : std.Random ,
52+ resolution : i32 ,
53+ ) ! u64 {
54+ if (resolution < 0 or resolution > serialization .MAX_RESOLUTION ) {
55+ return error .InvalidResolution ;
56+ }
57+
58+ const origin_id : u8 = @intCast (random .uintLessThan (usize , 12 ));
59+ const segment : usize = if (resolution > 0 ) random .uintLessThan (usize , 5 ) else 0 ;
60+ var s : u64 = 0 ;
61+
62+ if (resolution >= serialization .FIRST_HILBERT_RESOLUTION ) {
63+ const hilbert_levels = resolution - serialization .FIRST_HILBERT_RESOLUTION + 1 ;
64+ const hilbert_bits : u6 = @intCast (2 * hilbert_levels );
65+ const max_s : u64 = @as (u64 , 1 ) << hilbert_bits ;
66+ s = random .uintLessThan (u64 , max_s );
67+ }
68+
69+ return serialization .serialize (.{
70+ .origin_id = origin_id ,
71+ .segment = segment ,
72+ .s = s ,
73+ .resolution = resolution ,
74+ });
75+ }
76+
4377fn runCommand (
4478 allocator : std.mem.Allocator ,
4579 argv : []const []const u8 ,
@@ -129,6 +163,49 @@ fn runRustExportCellBoundaries(
129163 return runCommand (allocator , argv .items , null );
130164}
131165
166+ fn runRustLonlatToCell (
167+ allocator : std.mem.Allocator ,
168+ resolution : i32 ,
169+ points : []const LonLat ,
170+ ) ! CommandOutput {
171+ var argv = try std .ArrayList ([]const u8 ).initCapacity (allocator , 11 + points .len );
172+ defer argv .deinit (allocator );
173+
174+ var encoded_points = try std .ArrayList ([]u8 ).initCapacity (allocator , points .len );
175+ defer {
176+ for (encoded_points .items ) | encoded | allocator .free (encoded );
177+ encoded_points .deinit (allocator );
178+ }
179+
180+ var resolution_buffer : [16 ]u8 = undefined ;
181+ const resolution_text = try std .fmt .bufPrint (& resolution_buffer , "{d}" , .{resolution });
182+
183+ try argv .appendSlice (allocator , &.{
184+ "cargo" ,
185+ "run" ,
186+ "--quiet" ,
187+ "--manifest-path" ,
188+ "tests/qa_rust_oracle/Cargo.toml" ,
189+ "--target-dir" ,
190+ ".zig-cache/qa_rust_oracle_target" ,
191+ "--" ,
192+ "lonlat-to-cell" ,
193+ resolution_text ,
194+ });
195+
196+ for (points ) | point | {
197+ const encoded = try std .fmt .allocPrint (
198+ allocator ,
199+ "{d:.17},{d:.17}" ,
200+ .{ point .longitude (), point .latitude () },
201+ );
202+ try encoded_points .append (allocator , encoded );
203+ try argv .append (allocator , encoded );
204+ }
205+
206+ return runCommand (allocator , argv .items , null );
207+ }
208+
132209fn parseHexCells (allocator : std.mem.Allocator , stdout : []const u8 ) ! []u64 {
133210 var ids = try std .ArrayList (u64 ).initCapacity (allocator , 0 );
134211 errdefer ids .deinit (allocator );
@@ -244,6 +321,69 @@ fn lonLatToUnitVector(lon_degrees: f64, lat_degrees: f64) UnitVec3 {
244321 };
245322}
246323
324+ fn normalizeUnitVector (vec : UnitVec3 ) UnitVec3 {
325+ const len = std .math .sqrt (vec .x * vec .x + vec .y * vec .y + vec .z * vec .z );
326+ if (len == 0.0 ) {
327+ return .{ .x = 1.0 , .y = 0.0 , .z = 0.0 };
328+ }
329+ return .{
330+ .x = vec .x / len ,
331+ .y = vec .y / len ,
332+ .z = vec .z / len ,
333+ };
334+ }
335+
336+ fn unitVectorToLonLat (vec : UnitVec3 ) LonLat {
337+ const normalized = normalizeUnitVector (vec );
338+ const lon_radians = std .math .atan2 (normalized .y , normalized .x );
339+ const lat_radians = std .math .atan2 (
340+ normalized .z ,
341+ std .math .sqrt (normalized .x * normalized .x + normalized .y * normalized .y ),
342+ );
343+ const radians_to_degrees = 180.0 / std .math .pi ;
344+ return LonLat .new (
345+ lon_radians * radians_to_degrees ,
346+ lat_radians * radians_to_degrees ,
347+ );
348+ }
349+
350+ fn pointTowardBoundary (center : LonLat , boundary_point : LonLat , t : f64 ) LonLat {
351+ const center_vec = lonLatToUnitVector (center .longitude (), center .latitude ());
352+ const boundary_vec = lonLatToUnitVector (boundary_point .longitude (), boundary_point .latitude ());
353+ const blended = UnitVec3 {
354+ .x = center_vec .x * (1.0 - t ) + boundary_vec .x * t ,
355+ .y = center_vec .y * (1.0 - t ) + boundary_vec .y * t ,
356+ .z = center_vec .z * (1.0 - t ) + boundary_vec .z * t ,
357+ };
358+ return unitVectorToLonLat (blended );
359+ }
360+
361+ fn randomInteriorPointForCell (
362+ random : std.Random ,
363+ cell_id : u64 ,
364+ boundary : []const LonLat ,
365+ ) ! LonLat {
366+ const center = try a5 .cell_to_lonlat (cell_id );
367+ const boundary_len = effectiveZigBoundaryLen (boundary );
368+ if (boundary_len == 0 ) return center ;
369+
370+ const cell_data = try serialization .deserialize (cell_id );
371+ const boundary_index = random .uintLessThan (usize , boundary_len );
372+ const target = boundary [boundary_index ];
373+ var t = DEEP_RANDOM_POINT_MIN_T +
374+ (DEEP_RANDOM_POINT_MAX_T - DEEP_RANDOM_POINT_MIN_T ) * random .float (f64 );
375+
376+ var attempt : usize = 0 ;
377+ while (attempt < 10 ) : (attempt += 1 ) {
378+ const candidate = pointTowardBoundary (center , target , t );
379+ const distance = try a5 .core .cell .a5cell_contains_point (cell_data , candidate );
380+ if (distance > DEEP_RANDOM_POINT_CONTAINMENT_EPSILON ) return candidate ;
381+ t *= 0.5 ;
382+ }
383+
384+ return center ;
385+ }
386+
247387fn angularDistanceRadians (a : UnitVec3 , b : UnitVec3 ) f64 {
248388 const dot = std .math .clamp (a .x * b .x + a .y * b .y + a .z * b .z , -1.0 , 1.0 );
249389 return std .math .acos (dot );
@@ -589,3 +729,87 @@ test "qa e2e compare a5-rs and a5-zig cell outputs up to resolution 9" {
589729 }
590730 }
591731}
732+
733+ test "qa e2e deep random cell boundary parity against rust oracle (~10s)" {
734+ var prng = std .Random .DefaultPrng .init (DEEP_RANDOM_PARITY_SEED );
735+ const random = prng .random ();
736+
737+ const start_ns = std .time .nanoTimestamp ();
738+ const deadline_ns = start_ns + DEEP_RANDOM_PARITY_DURATION_NS ;
739+
740+ var checked_cells : usize = 0 ;
741+ var batch_count : usize = 0 ;
742+
743+ while (std .time .nanoTimestamp () < deadline_ns ) : (batch_count += 1 ) {
744+ var batch = try std .ArrayList (u64 ).initCapacity (std .testing .allocator , DEEP_RANDOM_PARITY_BATCH_SIZE );
745+ defer batch .deinit (std .testing .allocator );
746+
747+ var i : usize = 0 ;
748+ while (i < DEEP_RANDOM_PARITY_BATCH_SIZE ) : (i += 1 ) {
749+ const cell_id = try randomCellAtResolution (random , DEEP_RANDOM_PARITY_RESOLUTION );
750+ try batch .append (std .testing .allocator , cell_id );
751+ }
752+
753+ const rust_output = try runRustExportCellBoundaries (std .testing .allocator , batch .items );
754+ defer std .testing .allocator .free (rust_output .stdout );
755+ defer std .testing .allocator .free (rust_output .stderr );
756+
757+ const rust_boundaries = try parseRustBoundaries (std .testing .allocator , rust_output .stdout );
758+ defer freeRustBoundaries (std .testing .allocator , rust_boundaries );
759+
760+ var sampled_points = try std .ArrayList (LonLat ).initCapacity (std .testing .allocator , rust_boundaries .len );
761+ defer sampled_points .deinit (std .testing .allocator );
762+
763+ try std .testing .expectEqual (batch .items .len , rust_boundaries .len );
764+ for (rust_boundaries , 0.. ) | rust_boundary , idx | {
765+ try std .testing .expectEqual (batch .items [idx ], rust_boundary .id );
766+ const zig_boundary = try a5 .cell_to_boundary (std .testing .allocator , rust_boundary .id , .{
767+ .closed_ring = true ,
768+ .segments = 1 ,
769+ });
770+ defer std .testing .allocator .free (zig_boundary );
771+ try expectSameBoundary (DEEP_RANDOM_PARITY_RESOLUTION , rust_boundary .id , zig_boundary , rust_boundary .points );
772+ const sampled_point = try randomInteriorPointForCell (random , rust_boundary .id , zig_boundary );
773+ try sampled_points .append (std .testing .allocator , sampled_point );
774+ }
775+
776+ const rust_lonlat_output = try runRustLonlatToCell (
777+ std .testing .allocator ,
778+ DEEP_RANDOM_PARITY_RESOLUTION ,
779+ sampled_points .items ,
780+ );
781+ defer std .testing .allocator .free (rust_lonlat_output .stdout );
782+ defer std .testing .allocator .free (rust_lonlat_output .stderr );
783+
784+ const rust_lonlat_cells = try parseHexCells (std .testing .allocator , rust_lonlat_output .stdout );
785+ defer std .testing .allocator .free (rust_lonlat_cells );
786+
787+ try std .testing .expectEqual (sampled_points .items .len , rust_lonlat_cells .len );
788+ for (sampled_points .items , 0.. ) | point , idx | {
789+ const zig_cell = try a5 .lonlat_to_cell (point , DEEP_RANDOM_PARITY_RESOLUTION );
790+ const rust_cell = rust_lonlat_cells [idx ];
791+ const source_cell = batch .items [idx ];
792+
793+ if (zig_cell != rust_cell ) {
794+ std .debug .print (
795+ "deep random lonlat parity mismatch: idx={d} lon={d:.12} lat={d:.12} source={x:0>16} zig={x:0>16} rust={x:0>16}\n " ,
796+ .{
797+ idx ,
798+ point .longitude (),
799+ point .latitude (),
800+ source_cell ,
801+ zig_cell ,
802+ rust_cell ,
803+ },
804+ );
805+ }
806+
807+ try std .testing .expectEqual (source_cell , rust_cell );
808+ try std .testing .expectEqual (rust_cell , zig_cell );
809+ }
810+
811+ checked_cells += batch .items .len ;
812+ }
813+
814+ try std .testing .expect (checked_cells > 0 );
815+ }
0 commit comments