sarpyx 0.1.5__py3-none-any.whl → 0.1.6__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 (48) hide show
  1. docs/examples/advanced/batch_processing.py +1 -1
  2. docs/examples/advanced/custom_processing_chains.py +1 -1
  3. docs/examples/advanced/performance_optimization.py +1 -1
  4. docs/examples/basic/snap_integration.py +1 -1
  5. docs/examples/intermediate/quality_assessment.py +1 -1
  6. outputs/baseline/20260205-234828/__init__.py +33 -0
  7. outputs/baseline/20260205-234828/main.py +493 -0
  8. outputs/final/20260205-234851/__init__.py +33 -0
  9. outputs/final/20260205-234851/main.py +493 -0
  10. sarpyx/__init__.py +2 -2
  11. sarpyx/algorithms/__init__.py +2 -2
  12. sarpyx/cli/__init__.py +1 -1
  13. sarpyx/cli/focus.py +3 -5
  14. sarpyx/cli/main.py +106 -7
  15. sarpyx/cli/shipdet.py +1 -1
  16. sarpyx/cli/worldsar.py +549 -0
  17. sarpyx/processor/__init__.py +1 -1
  18. sarpyx/processor/core/decode.py +43 -8
  19. sarpyx/processor/core/focus.py +104 -57
  20. sarpyx/science/__init__.py +1 -1
  21. sarpyx/sla/__init__.py +8 -0
  22. sarpyx/sla/metrics.py +101 -0
  23. sarpyx/{snap → snapflow}/__init__.py +1 -1
  24. sarpyx/snapflow/engine.py +6165 -0
  25. sarpyx/{snap → snapflow}/op.py +0 -1
  26. sarpyx/utils/__init__.py +1 -1
  27. sarpyx/utils/geos.py +652 -0
  28. sarpyx/utils/grid.py +285 -0
  29. sarpyx/utils/io.py +77 -9
  30. sarpyx/utils/meta.py +55 -0
  31. sarpyx/utils/nisar_utils.py +652 -0
  32. sarpyx/utils/rfigen.py +108 -0
  33. sarpyx/utils/wkt_utils.py +109 -0
  34. sarpyx/utils/zarr_utils.py +55 -37
  35. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/METADATA +9 -5
  36. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/RECORD +41 -32
  37. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/WHEEL +1 -1
  38. sarpyx-0.1.6.dist-info/licenses/LICENSE +201 -0
  39. sarpyx-0.1.6.dist-info/top_level.txt +4 -0
  40. tests/test_zarr_compat.py +35 -0
  41. sarpyx/processor/core/decode_v0.py +0 -0
  42. sarpyx/processor/core/decode_v1.py +0 -849
  43. sarpyx/processor/core/focus_old.py +0 -1550
  44. sarpyx/processor/core/focus_v1.py +0 -1566
  45. sarpyx/processor/core/focus_v2.py +0 -1625
  46. sarpyx/snap/engine.py +0 -633
  47. sarpyx-0.1.5.dist-info/top_level.txt +0 -2
  48. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/entry_points.txt +0 -0
@@ -227,7 +227,6 @@ print(f"Totale operatori SNAP: {len(snap_operators)}")
227
227
 
228
228
 
229
229
 
230
- # ...existing code...
231
230
 
232
231
  def generate_operators_help():
233
232
  """
sarpyx/utils/__init__.py CHANGED
@@ -16,4 +16,4 @@ __all__ = [
16
16
  ]
17
17
 
18
18
  # Version information
19
- __version__ = '0.1.0'
19
+ __version__ = '0.1.6'
sarpyx/utils/geos.py ADDED
@@ -0,0 +1,652 @@
1
+ """Point containment checker for grid points within WKT polygons."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import List, Dict, Any
6
+
7
+ from shapely import wkt
8
+ from shapely.geometry import Point, Polygon
9
+
10
+
11
+ def check_points_in_polygon(
12
+ wkt_polygon: str,
13
+ geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
14
+ ) -> List[Dict[str, Any]]:
15
+ """
16
+ Check which points from the GeoJSON file are contained within the given WKT polygon.
17
+
18
+ Args:
19
+ wkt_polygon: A WKT (Well-Known Text) string representing a polygon
20
+ geojson_path: Path to the GeoJSON file containing point features
21
+
22
+ Returns:
23
+ List of feature dictionaries containing points that fall within the polygon.
24
+ Each feature includes properties (name, row, col, row_idx, col_idx, utm_zone, epsg)
25
+ and geometry (coordinates).
26
+
27
+ Example:
28
+ >>> wkt_poly = "POLYGON((10 10, 10 20, 20 20, 20 10, 10 10))"
29
+ >>> contained_points = check_points_in_polygon(wkt_poly)
30
+ >>> print(f"Found {len(contained_points)} points")
31
+ """
32
+ # Parse the WKT polygon
33
+ polygon = wkt.loads(wkt_polygon)
34
+
35
+ # Load the GeoJSON file
36
+ with open(geojson_path, 'r') as f:
37
+ geojson_data = json.load(f)
38
+
39
+ # Use list comprehension for faster filtering of contained points
40
+ contained_features = [
41
+ feature for feature in geojson_data['features']
42
+ if polygon.contains(Point(*feature['geometry']['coordinates']))
43
+ ]
44
+
45
+ return contained_features
46
+
47
+
48
+ def get_point_names(
49
+ wkt_polygon: str,
50
+ geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
51
+ ) -> List[str]:
52
+ """
53
+ Get the names of points contained within the given WKT polygon.
54
+
55
+ Args:
56
+ wkt_polygon: A WKT (Well-Known Text) string representing a polygon
57
+ geojson_path: Path to the GeoJSON file containing point features
58
+
59
+ Returns:
60
+ List of point names (e.g., ["946D_176L", "946D_175L", ...])
61
+ """
62
+ contained_features = check_points_in_polygon(wkt_polygon, geojson_path)
63
+ return [feature['properties']['name'] for feature in contained_features]
64
+
65
+
66
+ def get_point_coordinates(
67
+ wkt_polygon: str,
68
+ geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
69
+ ) -> List[tuple]:
70
+ """
71
+ Get the coordinates of points contained within the given WKT polygon.
72
+
73
+ Args:
74
+ wkt_polygon: A WKT (Well-Known Text) string representing a polygon
75
+ geojson_path: Path to the GeoJSON file containing point features
76
+
77
+ Returns:
78
+ List of coordinate tuples [(lon, lat), ...]
79
+ """
80
+ contained_features = check_points_in_polygon(wkt_polygon, geojson_path)
81
+ return [
82
+ tuple(feature['geometry']['coordinates'])
83
+ for feature in contained_features
84
+ ]
85
+
86
+ class GridNavigator:
87
+ """
88
+ Grid navigation utilities for moving between adjacent grid points.
89
+ This module provides GridNavigator which navigates using only point names
90
+ without requiring the Grid object.
91
+
92
+ The grid naming convention:
93
+ - Rows: XD (south of equator), 0U (equator), XU (north of equator)
94
+ - Columns: XL (west of prime meridian), 0R (prime meridian), XR (east of prime meridian)
95
+
96
+ Navigation:
97
+ - North (up): Row number decreases in D hemisphere, increases in U hemisphere
98
+ - South (down): Row number increases in D hemisphere, decreases in U hemisphere
99
+ - East (right): Column number increases in R hemisphere, decreases in L hemisphere
100
+ - West (left): Column number decreases in R hemisphere, increases in L hemisphere
101
+ """
102
+
103
+ def __init__(self):
104
+ """Initialize navigator."""
105
+ pass
106
+
107
+ @staticmethod
108
+ def _parse_row(row: str) -> tuple[int, str]:
109
+ """Parse row name into number and hemisphere.
110
+
111
+ Args:
112
+ row: Row name like '298D' or '5U'
113
+
114
+ Returns:
115
+ Tuple of (number, hemisphere) e.g., (298, 'D')
116
+ """
117
+ if row.endswith('U'):
118
+ return int(row[:-1]), 'U'
119
+ elif row.endswith('D'):
120
+ return int(row[:-1]), 'D'
121
+ else:
122
+ raise ValueError(f'Invalid row format: {row}')
123
+
124
+ @staticmethod
125
+ def _parse_col(col: str) -> tuple[int, str]:
126
+ """Parse column name into number and hemisphere.
127
+
128
+ Args:
129
+ col: Column name like '323R' or '10L'
130
+
131
+ Returns:
132
+ Tuple of (number, hemisphere) e.g., (323, 'R')
133
+ """
134
+ if col.endswith('R'):
135
+ return int(col[:-1]), 'R'
136
+ elif col.endswith('L'):
137
+ return int(col[:-1]), 'L'
138
+ else:
139
+ raise ValueError(f'Invalid column format: {col}')
140
+
141
+ @staticmethod
142
+ def _make_row(num: int, hemisphere: str) -> str:
143
+ """Create row name from number and hemisphere.
144
+
145
+ Args:
146
+ num: Row number
147
+ hemisphere: 'U' or 'D'
148
+
149
+ Returns:
150
+ Row name like '298D' or '5U'
151
+ """
152
+ return f'{num}{hemisphere}'
153
+
154
+ @staticmethod
155
+ def _make_col(num: int, hemisphere: str) -> str:
156
+ """Create column name from number and hemisphere.
157
+
158
+ Args:
159
+ num: Column number
160
+ hemisphere: 'R' or 'L'
161
+
162
+ Returns:
163
+ Column name like '323R' or '10L'
164
+ """
165
+ return f'{num}{hemisphere}'
166
+
167
+ def move_up(self, row: str, col: str) -> str:
168
+ """Get the name of the point immediately above (northward).
169
+
170
+ Moving north:
171
+ - In D hemisphere: 298D → 297D → ... → 1D → 0U
172
+ - In U hemisphere: 0U → 1U → 2U → ...
173
+
174
+ Args:
175
+ row: Current row (e.g., '298D', '5U')
176
+ col: Current column (e.g., '323R', '10L')
177
+
178
+ Returns:
179
+ Name of the upper point
180
+ """
181
+ num, hemisphere = self._parse_row(row)
182
+
183
+ if hemisphere == 'D':
184
+ # Moving north in southern hemisphere
185
+ if num == 1:
186
+ # Cross to northern hemisphere
187
+ new_row = '0U'
188
+ else:
189
+ # Stay in southern hemisphere
190
+ new_row = self._make_row(num - 1, 'D')
191
+ else: # hemisphere == 'U'
192
+ # Moving north in northern hemisphere
193
+ new_row = self._make_row(num + 1, 'U')
194
+
195
+ return f'{new_row}_{col}'
196
+
197
+ def move_down(self, row: str, col: str) -> str:
198
+ """Get the name of the point immediately below (southward).
199
+
200
+ Moving south:
201
+ - In U hemisphere: 298U → 297U → ... → 1U → 0U
202
+ - In D hemisphere: 0U → 1D → 2D → ...
203
+
204
+ Args:
205
+ row: Current row (e.g., '298D', '5U')
206
+ col: Current column (e.g., '323R', '10L')
207
+
208
+ Returns:
209
+ Name of the lower point
210
+ """
211
+ num, hemisphere = self._parse_row(row)
212
+
213
+ if hemisphere == 'U':
214
+ # Moving south in northern hemisphere
215
+ if num == 0:
216
+ # Cross to southern hemisphere
217
+ new_row = '1D'
218
+ elif num == 1:
219
+ # Move to equator
220
+ new_row = '0U'
221
+ else:
222
+ # Stay in northern hemisphere
223
+ new_row = self._make_row(num - 1, 'U')
224
+ else: # hemisphere == 'D'
225
+ # Moving south in southern hemisphere
226
+ new_row = self._make_row(num + 1, 'D')
227
+
228
+ return f'{new_row}_{col}'
229
+
230
+ def move_right(self, row: str, col: str) -> str:
231
+ """Get the name of the point immediately to the right (eastward).
232
+
233
+ Moving east:
234
+ - In L hemisphere: 10L → 9L → ... → 1L → 0R
235
+ - In R hemisphere: 0R → 1R → 2R → ...
236
+
237
+ Args:
238
+ row: Current row (e.g., '298D', '5U')
239
+ col: Current column (e.g., '323R', '10L')
240
+
241
+ Returns:
242
+ Name of the right point
243
+ """
244
+ num, hemisphere = self._parse_col(col)
245
+
246
+ if hemisphere == 'L':
247
+ # Moving east in western hemisphere
248
+ if num == 1:
249
+ # Cross to eastern hemisphere
250
+ new_col = '0R'
251
+ else:
252
+ # Stay in western hemisphere
253
+ new_col = self._make_col(num - 1, 'L')
254
+ else: # hemisphere == 'R'
255
+ # Moving east in eastern hemisphere
256
+ new_col = self._make_col(num + 1, 'R')
257
+
258
+ return f'{row}_{new_col}'
259
+
260
+ def move_left(self, row: str, col: str) -> str:
261
+ """Get the name of the point immediately to the left (westward).
262
+
263
+ Moving west:
264
+ - In R hemisphere: 10R → 9R → ... → 1R → 0R
265
+ - In L hemisphere: 0R → 1L → 2L → ...
266
+
267
+ Args:
268
+ row: Current row (e.g., '298D', '5U')
269
+ col: Current column (e.g., '323R', '10L')
270
+
271
+ Returns:
272
+ Name of the left point
273
+ """
274
+ num, hemisphere = self._parse_col(col)
275
+
276
+ if hemisphere == 'R':
277
+ # Moving west in eastern hemisphere
278
+ if num == 0:
279
+ # Cross to western hemisphere
280
+ new_col = '1L'
281
+ else:
282
+ # Stay in eastern hemisphere
283
+ new_col = self._make_col(num - 1, 'R')
284
+ else: # hemisphere == 'L'
285
+ # Moving west in western hemisphere
286
+ new_col = self._make_col(num + 1, 'L')
287
+
288
+ return f'{row}_{new_col}'
289
+
290
+ def move_up_right(self, row: str, col: str) -> str:
291
+ """Get the name of the point diagonally upper-right (north-east).
292
+
293
+ Args:
294
+ row: Current row (e.g., '298D', '5U')
295
+ col: Current column (e.g., '323R', '10L')
296
+
297
+ Returns:
298
+ Name of the upper-right point
299
+ """
300
+ # Move up
301
+ upper_name = self.move_up(row, col)
302
+ upper_row, upper_col = upper_name.split('_')
303
+
304
+ # Then move right
305
+ return self.move_right(upper_row, upper_col)
306
+
307
+ def move_up_left(self, row: str, col: str) -> str:
308
+ """Get the name of the point diagonally upper-left (north-west).
309
+
310
+ Args:
311
+ row: Current row (e.g., '298D', '5U')
312
+ col: Current column (e.g., '323R', '10L')
313
+
314
+ Returns:
315
+ Name of the upper-left point
316
+ """
317
+ upper_name = self.move_up(row, col)
318
+ upper_row, upper_col = upper_name.split('_')
319
+ return self.move_left(upper_row, upper_col)
320
+
321
+ def move_down_right(self, row: str, col: str) -> str:
322
+ """Get the name of the point diagonally lower-right (south-east).
323
+
324
+ Args:
325
+ row: Current row (e.g., '298D', '5U')
326
+ col: Current column (e.g., '323R', '10L')
327
+
328
+ Returns:
329
+ Name of the lower-right point
330
+ """
331
+ lower_name = self.move_down(row, col)
332
+ lower_row, lower_col = lower_name.split('_')
333
+ return self.move_right(lower_row, lower_col)
334
+
335
+ def move_down_left(self, row: str, col: str) -> str:
336
+ """Get the name of the point diagonally lower-left (south-west).
337
+
338
+ Args:
339
+ row: Current row (e.g., '298D', '5U')
340
+ col: Current column (e.g., '323R', '10L')
341
+
342
+ Returns:
343
+ Name of the lower-left point
344
+ """
345
+ lower_name = self.move_down(row, col)
346
+ lower_row, lower_col = lower_name.split('_')
347
+ return self.move_left(lower_row, lower_col)
348
+
349
+
350
+ def is_point_in_contained(name, contained):
351
+ for feature in contained:
352
+ props = feature['properties']
353
+ if props['name'] == name:
354
+ return True
355
+ return False
356
+
357
+
358
+ def rectangle_to_wkt(rectangle: dict) -> str:
359
+ """Convert a rectangle dictionary to WKT POLYGON format.
360
+
361
+ Takes a dictionary containing four corner points (TL, TR, BR, BL) with their
362
+ coordinates and creates a closed polygon in Well-Known Text format. The polygon
363
+ is closed by repeating the first point at the end.
364
+
365
+ Args:
366
+ rectangle: Dictionary with keys 'TL', 'TR', 'BR', 'BL', each containing
367
+ a GeoJSON Feature with geometry.coordinates [lon, lat].
368
+
369
+ Returns:
370
+ WKT POLYGON string in format 'POLYGON ((lon lat, lon lat, ...))'
371
+
372
+ Example:
373
+ >>> rect = {
374
+ ... 'TL': {'geometry': {'coordinates': [32.37, -26.68]}},
375
+ ... 'TR': {'geometry': {'coordinates': [32.47, -26.68]}},
376
+ ... 'BR': {'geometry': {'coordinates': [32.49, -26.77]}},
377
+ ... 'BL': {'geometry': {'coordinates': [32.39, -26.77]}}
378
+ ... }
379
+ >>> rectangle_to_wkt(rect)
380
+ 'POLYGON ((32.37 -26.68, 32.47 -26.68, 32.49 -26.77, 32.39 -26.77, 32.37 -26.68))'
381
+ """
382
+ # Extract coordinates in clockwise order: TL -> TR -> BR -> BL -> TL (close polygon)
383
+ corners = ['TL', 'TR', 'BR', 'BL']
384
+ coords = [rectangle[corner]['geometry']['coordinates'] for corner in corners]
385
+
386
+ # Close the polygon by adding the first point at the end
387
+ coords.append(coords[0])
388
+
389
+ # Format as WKT: 'lon lat' pairs separated by commas
390
+ coord_strings = [f'{lon} {lat}' for lon, lat in coords]
391
+ wkt = f"POLYGON (({', '.join(coord_strings)}))"
392
+
393
+ return wkt
394
+
395
+
396
+ def rectanglify(contained: list) -> list | None:
397
+ """
398
+ Build rectangles for cutting based on contained points.
399
+
400
+ This function identifies rectangles formed by points in the `contained` list.
401
+ It uses the `GridNavigator` class to navigate between points and checks if
402
+ the points form a valid rectangle.
403
+
404
+ Args:
405
+ contained (list): A list of GeoJSON-like features, where each feature
406
+ contains properties (e.g., 'row', 'col', 'name') and
407
+ geometry (e.g., coordinates).
408
+
409
+ Returns:
410
+ list | None: A list of rectangles, where each rectangle is represented
411
+ as a dictionary with keys 'TL', 'TR', 'BR', 'BL' (top-left,
412
+ top-right, bottom-right, bottom-left). Returns None if no
413
+ rectangles are found.
414
+
415
+ Example:
416
+ >>> contained_points = [
417
+ ... {'properties': {'name': '1U_1R', 'row': '1U', 'col': '1R'}, 'geometry': {'coordinates': [0, 0]}},
418
+ ... {'properties': {'name': '1U_2R', 'row': '1U', 'col': '2R'}, 'geometry': {'coordinates': [1, 0]}},
419
+ ... {'properties': {'name': '2U_1R', 'row': '2U', 'col': '1R'}, 'geometry': {'coordinates': [0, 1]}},
420
+ ... {'properties': {'name': '2U_2R', 'row': '2U', 'col': '2R'}, 'geometry': {'coordinates': [1, 1]}}
421
+ ... ]
422
+ >>> rectangles = rectanglify(contained_points)
423
+ >>> print(rectangles)
424
+ """
425
+ rectangles = []
426
+ navi = GridNavigator()
427
+ for idx in range(len(contained)):
428
+ row, col = contained[idx]['properties']['row'], contained[idx]['properties']['col']
429
+
430
+ # Find the top-left corner
431
+ TL = navi.move_up(row, col)
432
+
433
+ if is_point_in_contained(TL, contained):
434
+ # Find the top-right corner
435
+ TR = navi.move_up_right(row, col)
436
+ if is_point_in_contained(TR, contained):
437
+ # Find the bottom-right corner
438
+ BR = navi.move_right(row, col)
439
+ if is_point_in_contained(BR, contained):
440
+ # Bottom-left corner is the current point
441
+ BL = f"{row}_{col}"
442
+ # Create a rectangle dictionary
443
+ rectangle = {
444
+ 'TL': [point for point in contained if point['properties']['name'] == TL][0],
445
+ 'TR': [point for point in contained if point['properties']['name'] == TR][0],
446
+ 'BR': [point for point in contained if point['properties']['name'] == BR][0],
447
+ 'BL': [point for point in contained if point['properties']['name'] == BL][0]
448
+ }
449
+ rectangles.append(rectangle)
450
+ else:
451
+ print(f'Point {TL} not in contained points.')
452
+
453
+ return rectangles if len(rectangles) > 0 else None
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+
462
+
463
+
464
+
465
+
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
475
+ # if __name__ == '__main__':
476
+ # print('='*80)
477
+ # print('GRID NAVIGATION TESTS (NAME-BASED)')
478
+ # print('='*80)
479
+ # print('\nNavigator uses only point names - no Grid object required!\n')
480
+
481
+ # # Create navigator (no Grid needed!)
482
+ # navigator = GridNavigator()
483
+
484
+ # # Test 1: Basic movements in Southern Hemisphere
485
+ # print('='*80)
486
+ # print('TEST 1: Basic movements in Southern Hemisphere')
487
+ # print('='*80)
488
+ # test_row, test_col = '100D', '50R'
489
+ # print(f'Starting point: {test_row}_{test_col}')
490
+
491
+ # up = navigator.move_up(test_row, test_col)
492
+ # print(f' ↑ Up (north): {up}')
493
+
494
+ # down = navigator.move_down(test_row, test_col)
495
+ # print(f' ↓ Down (south): {down}')
496
+
497
+ # right = navigator.move_right(test_row, test_col)
498
+ # print(f' → Right (east): {right}')
499
+
500
+ # left = navigator.move_left(test_row, test_col)
501
+ # print(f' ← Left (west): {left}')
502
+
503
+ # up_right = navigator.move_up_right(test_row, test_col)
504
+ # print(f' ↗ Up-right (NE): {up_right}')
505
+
506
+ # # Test 2: Basic movements in Northern Hemisphere
507
+ # print('\n' + '='*80)
508
+ # print('TEST 2: Basic movements in Northern Hemisphere')
509
+ # print('='*80)
510
+ # test_row, test_col = '50U', '100R'
511
+ # print(f'Starting point: {test_row}_{test_col}')
512
+
513
+ # up = navigator.move_up(test_row, test_col)
514
+ # print(f' ↑ Up (north): {up}')
515
+
516
+ # down = navigator.move_down(test_row, test_col)
517
+ # print(f' ↓ Down (south): {down}')
518
+
519
+ # right = navigator.move_right(test_row, test_col)
520
+ # print(f' → Right (east): {right}')
521
+
522
+ # left = navigator.move_left(test_row, test_col)
523
+ # print(f' ← Left (west): {left}')
524
+
525
+ # # Test 3: Crossing equator from south to north
526
+ # print('\n' + '='*80)
527
+ # print('TEST 3: Crossing the equator (South → North)')
528
+ # print('='*80)
529
+ # test_row, test_col = '1D', '100R'
530
+ # print(f'Starting point: {test_row}_{test_col}')
531
+
532
+ # up = navigator.move_up(test_row, test_col)
533
+ # print(f' ↑ Up: {test_row}_{test_col} → {up}')
534
+ # assert up == '0U_100R', f'Expected 0U_100R, got {up}'
535
+ # print(f' ✓ Successfully crossed from D to U!')
536
+
537
+ # # Test 4: Crossing equator from north to south
538
+ # print('\n' + '='*80)
539
+ # print('TEST 4: Crossing the equator (North → South)')
540
+ # print('='*80)
541
+ # test_row, test_col = '0U', '100R'
542
+ # print(f'Starting point: {test_row}_{test_col}')
543
+
544
+ # down = navigator.move_down(test_row, test_col)
545
+ # print(f' ↓ Down: {test_row}_{test_col} → {down}')
546
+ # assert down == '1D_100R', f'Expected 1D_100R, got {down}'
547
+ # print(f' ✓ Successfully crossed from U to D!')
548
+
549
+ # # Test 5: Moving through equator
550
+ # print('\n' + '='*80)
551
+ # print('TEST 5: Sequential movement through equator')
552
+ # print('='*80)
553
+ # test_row, test_col = '2D', '50R'
554
+ # print(f'Starting: {test_row}_{test_col}')
555
+
556
+ # current_row, current_col = test_row, test_col
557
+ # for i in range(5):
558
+ # next_point = navigator.move_up(current_row, current_col)
559
+ # print(f' Step {i+1}: {current_row}_{current_col} → {next_point}')
560
+ # current_row, current_col = next_point.split('_')
561
+ # print(f' ✓ Traversed: 2D → 1D → 0U → 1U → 2U → 3U')
562
+
563
+ # # Test 6: Crossing prime meridian from west to east
564
+ # print('\n' + '='*80)
565
+ # print('TEST 6: Crossing prime meridian (West → East)')
566
+ # print('='*80)
567
+ # test_row, test_col = '50D', '1L'
568
+ # print(f'Starting point: {test_row}_{test_col}')
569
+
570
+ # right = navigator.move_right(test_row, test_col)
571
+ # print(f' → Right: {test_row}_{test_col} → {right}')
572
+ # assert right == '50D_0R', f'Expected 50D_0R, got {right}'
573
+ # print(f' ✓ Successfully crossed from L to R!')
574
+
575
+ # # Test 7: Crossing prime meridian from east to west
576
+ # print('\n' + '='*80)
577
+ # print('TEST 7: Crossing prime meridian (East → West)')
578
+ # print('='*80)
579
+ # test_row, test_col = '50D', '0R'
580
+ # print(f'Starting point: {test_row}_{test_col}')
581
+
582
+ # left = navigator.move_left(test_row, test_col)
583
+ # print(f' ← Left: {test_row}_{test_col} → {left}')
584
+ # assert left == '50D_1L', f'Expected 50D_1L, got {left}'
585
+ # print(f' ✓ Successfully crossed from R to L!')
586
+
587
+ # # Test 8: Sequential movement through prime meridian
588
+ # print('\n' + '='*80)
589
+ # print('TEST 8: Sequential movement through prime meridian')
590
+ # print('='*80)
591
+ # test_row, test_col = '100U', '2L'
592
+ # print(f'Starting: {test_row}_{test_col}')
593
+
594
+ # current_row, current_col = test_row, test_col
595
+ # for i in range(5):
596
+ # next_point = navigator.move_right(current_row, current_col)
597
+ # print(f' Step {i+1}: {current_row}_{current_col} → {next_point}')
598
+ # current_row, current_col = next_point.split('_')
599
+ # print(f' ✓ Traversed: 2L → 1L → 0R → 1R → 2R → 3R')
600
+
601
+ # # Test 9: Consistency - round trip
602
+ # print('\n' + '='*80)
603
+ # print('TEST 9: Round-trip consistency')
604
+ # print('='*80)
605
+ # test_row, test_col = '200D', '150R'
606
+ # print(f'Starting point: {test_row}_{test_col}')
607
+
608
+ # # Up and down
609
+ # up = navigator.move_up(test_row, test_col)
610
+ # up_row, up_col = up.split('_')
611
+ # back_down = navigator.move_down(up_row, up_col)
612
+ # print(f' Up then down: {test_row}_{test_col} → {up} → {back_down}')
613
+ # assert back_down == f'{test_row}_{test_col}', 'Should return to original'
614
+ # print(f' ✓ Passed!')
615
+
616
+ # # Right and left
617
+ # right = navigator.move_right(test_row, test_col)
618
+ # right_row, right_col = right.split('_')
619
+ # back_left = navigator.move_left(right_row, right_col)
620
+ # print(f' Right then left: {test_row}_{test_col} → {right} → {back_left}')
621
+ # assert back_left == f'{test_row}_{test_col}', 'Should return to original'
622
+ # print(f' ✓ Passed!')
623
+
624
+ # # Test 10: Diagonal movements
625
+ # print('\n' + '='*80)
626
+ # print('TEST 10: Diagonal movements')
627
+ # print('='*80)
628
+ # test_row, test_col = '50D', '50R'
629
+ # print(f'Starting point: {test_row}_{test_col}')
630
+
631
+ # ne = navigator.move_up_right(test_row, test_col)
632
+ # print(f' ↗ NE: {ne}')
633
+
634
+ # nw = navigator.move_up_left(test_row, test_col)
635
+ # print(f' ↖ NW: {nw}')
636
+
637
+ # se = navigator.move_down_right(test_row, test_col)
638
+ # print(f' ↘ SE: {se}')
639
+
640
+ # sw = navigator.move_down_left(test_row, test_col)
641
+ # print(f' ↙ SW: {sw}')
642
+
643
+ # print('\n' + '='*80)
644
+ # print('ALL TESTS COMPLETED')
645
+ # print('='*80)
646
+ # print('\nSUMMARY:')
647
+ # print('- GridNavigator works purely by naming conventions')
648
+ # print('- No Grid object required for navigation')
649
+ # print('- Correctly handles hemisphere transitions (D↔U, L↔R)')
650
+ # print('- Round-trip navigation returns to origin')
651
+ # print('- Diagonal movements combine cardinal directions')
652
+ # print('='*80)