Skip to content

Commit 298170e

Browse files
committed
tests against proj4 implementations
1 parent bdc98ba commit 298170e

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies = [
2222
dev = [
2323
"pytest>=6.0",
2424
"psutil",
25+
"pyproj",
2526
"hidefix",
2627
"h5py",
2728
"netCDF4",

tests/test_grids.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pyproj
23
import pytest
34
from omfiles.grids import (
45
LambertAzimuthalEqualAreaProjection,
@@ -8,6 +9,7 @@
89
StereographicProjection,
910
)
1011
from omfiles.om_domains import RegularLatLonGrid
12+
from omfiles.utils import _normalize_longitude
1113

1214
# Fixtures for grids
1315

@@ -371,3 +373,220 @@ def test_lambert_conformal_conic_projection():
371373
assert abs(lon - 30.711975) < 0.001
372374
point_idx = grid.findPointXy(lat=lat, lon=lon)
373375
assert point_idx == (1642, 1573)
376+
377+
378+
def test_rotated_latlon_against_proj():
379+
# Create our custom projection
380+
lat_origin = -36.0885
381+
lon_origin = 245.305
382+
custom_proj = RotatedLatLonProjection(lat_origin=lat_origin, lon_origin=lon_origin)
383+
384+
# Create equivalent PROJ projection
385+
proj_string = (f"+proj=ob_tran +o_proj=longlat +o_lat_p={-lat_origin} "
386+
f"+o_lon_p=0.0 +lon_0={lon_origin} +datum=WGS84 +no_defs +type=crs")
387+
proj_proj = pyproj.Proj(proj_string)
388+
389+
# Test points covering different regions
390+
test_points = [
391+
(0, 0), # Origin
392+
(45, 45), # Mid-latitude point
393+
(-45, -45), # Mid-latitude point (southern hemisphere)
394+
(10, 50), # Europe
395+
(40, -100), # North America
396+
(50, -170), # Pacific
397+
(-30, 170), # South Pacific
398+
]
399+
400+
for lat, lon in test_points:
401+
# Forward transformation using our implementation
402+
custom_x, custom_y = custom_proj.forward(latitude=lat, longitude=lon)
403+
404+
# Forward transformation using PROJ
405+
# Note: PROJ expects (lon, lat) order, not (lat, lon)
406+
# proj_x, proj_y = proj_proj(np.radians(lon), np.radians(lat))
407+
proj_x, proj_y = proj_proj(lon, lat)
408+
# The following fix should be available in proj, but something is weird
409+
# with radians/degrees with ob_tran....
410+
# https://github.com/OSGeo/PROJ/issues/2804
411+
proj_x = np.degrees(proj_x)
412+
proj_y = np.degrees(proj_y)
413+
414+
# Compare results - allowing for small differences due to floating point math
415+
# Convert to radians for comparison since our implementation works in radians
416+
assert abs(custom_x - proj_x) < 1e-5, f"X mismatch for ({lat}, {lon}): custom={custom_x}, proj={proj_x}"
417+
assert abs(custom_y - proj_y) < 1e-5, f"Y mismatch for ({lat}, {lon}): custom={custom_y}, proj={proj_y}"
418+
419+
# Test inverse transformation
420+
custom_lat, custom_lon = custom_proj.inverse(x=custom_x, y=custom_y)
421+
# PROJ expects inverse=True for inverse transform
422+
proj_lon, proj_lat = proj_proj(np.radians(proj_x), np.radians(proj_y), inverse=True)
423+
424+
# Compare results
425+
assert abs(custom_lat - proj_lat) < 1e-5, f"Lat mismatch for ({custom_x}, {custom_y}): custom={custom_lat}, proj={proj_lat}"
426+
assert abs(np.mod(custom_lon - proj_lon + 180, 360) - 180) < 1e-5, f"Lon mismatch for ({custom_x}, {custom_y}): custom={custom_lon}, proj={proj_lon}"
427+
428+
429+
def test_stereographic_against_proj():
430+
# Create our custom projection
431+
latitude = 90.0 # North pole
432+
longitude = 249.0
433+
radius = 6371229.0
434+
custom_proj = StereographicProjection(latitude=latitude, longitude=longitude, radius=radius)
435+
436+
# Create equivalent PROJ projection
437+
proj_string = (f"+proj=stere +lat_0={latitude} +lon_0={longitude} +k=1 "
438+
f"+x_0=0 +y_0=0 +R={radius} +units=m +no_defs")
439+
proj_proj = pyproj.Proj(proj_string)
440+
441+
# Test points - staying away from singular points (poles)
442+
test_points = [
443+
(0, 0), # Equator
444+
(45, 45), # Mid-latitude
445+
(60, -120), # Northern regions
446+
(45, 249), # Along the central meridian
447+
(70, 249), # Along the central meridian
448+
(80, 249), # Along the central meridian
449+
]
450+
451+
for lat, lon in test_points:
452+
# Forward transformation using our implementation
453+
custom_x, custom_y = custom_proj.forward(latitude=lat, longitude=lon)
454+
455+
# Forward transformation using PROJ
456+
# PROJ uses (lon, lat) order
457+
proj_x, proj_y = proj_proj(lon, lat)
458+
459+
# Compare results (allowing some tolerance due to potential differences in algorithms)
460+
# Stereographic projections can have larger errors for points far from the center
461+
tolerance = 1 # tolerance in meters
462+
assert abs(custom_x - proj_x) < tolerance, f"X mismatch for ({lat}, {lon}): custom={custom_x}, proj={proj_x}"
463+
assert abs(custom_y - proj_y) < tolerance, f"Y mismatch for ({lat}, {lon}): custom={custom_y}, proj={proj_y}"
464+
465+
# Test inverse transformation
466+
custom_lat, custom_lon = custom_proj.inverse(x=custom_x, y=custom_y)
467+
proj_lon, proj_lat = proj_proj(proj_x, proj_y, inverse=True)
468+
469+
# Compare results
470+
assert abs(custom_lat - proj_lat) < 1e-5, f"Lat mismatch: custom={custom_lat}, proj={proj_lat}"
471+
custom_lon = _normalize_longitude(custom_lon)
472+
assert abs(custom_lon - proj_lon) < 1e-4, f"Lon mismatch: custom={custom_lon}, proj={proj_lon}"
473+
474+
def test_lambert_azimuthal_equal_area_against_proj():
475+
# Create our custom projection
476+
lambda_0 = -2.5 # Central longitude in degrees
477+
phi_1 = 54.9 # Standard parallel/latitude in degrees
478+
radius = 6371229.0 # Earth radius in meters
479+
custom_proj = LambertAzimuthalEqualAreaProjection(lambda_0=lambda_0, phi_1=phi_1, radius=radius)
480+
481+
# Create equivalent PROJ projection
482+
# For Lambert Azimuthal Equal Area, we use lat_0 for the standard parallel and lon_0 for central longitude
483+
proj_string = (f"+proj=laea +lat_0={phi_1} +lon_0={lambda_0} +x_0=0 +y_0=0 "
484+
f"+R={radius} +units=m +no_defs +type=crs")
485+
proj_proj = pyproj.Proj(proj_string)
486+
487+
# Test points covering different regions
488+
test_points = [
489+
(0, 0), # Origin
490+
(54.9, -2.5), # Projection center (should map to 0,0)
491+
(45, 45), # Mid-latitude point
492+
(-45, -45), # Mid-latitude point (southern hemisphere)
493+
(10, 50), # Europe
494+
(40, -100), # North America
495+
(50, -170), # Pacific
496+
(-30, 170), # South Pacific
497+
# Test point from the existing test
498+
(57.745566, 10.620785)
499+
]
500+
501+
for lat, lon in test_points:
502+
# Forward transformation using our implementation
503+
custom_x, custom_y = custom_proj.forward(latitude=lat, longitude=lon)
504+
505+
# Forward transformation using PROJ
506+
# Note: PROJ expects (lon, lat) order, not (lat, lon)
507+
proj_x, proj_y = proj_proj(lon, lat)
508+
509+
# Compare results - Lambert projections can have larger differences due to algorithmic differences
510+
# Use a reasonable tolerance (e.g., 0.1 meter for a 6.3 million meter radius)
511+
tolerance = 0.1
512+
assert abs(custom_x - proj_x) < tolerance, f"X mismatch for ({lat}, {lon}): custom={custom_x}, proj={proj_x}"
513+
assert abs(custom_y - proj_y) < tolerance, f"Y mismatch for ({lat}, {lon}): custom={custom_y}, proj={proj_y}"
514+
515+
# Test inverse transformation (skip points very close to the poles where inverse can be unstable)
516+
if abs(lat) < 89:
517+
custom_lat, custom_lon = custom_proj.inverse(x=custom_x, y=custom_y)
518+
# PROJ expects inverse=True for inverse transform
519+
proj_lon, proj_lat = proj_proj(proj_x, proj_y, inverse=True)
520+
521+
# Compare results with appropriate tolerance
522+
# For inverse transformations, angular differences can be larger
523+
angular_tolerance = 1e-5 # roughly 0.00001 degrees
524+
assert abs(custom_lat - proj_lat) < angular_tolerance, \
525+
f"Lat mismatch for ({custom_x}, {custom_y}): custom={custom_lat}, proj={proj_lat}"
526+
527+
# Handle longitude wraparound for comparison
528+
lon_diff = np.mod(abs(custom_lon - proj_lon), 360)
529+
assert min(lon_diff, 360 - lon_diff) < angular_tolerance, \
530+
f"Lon mismatch for ({custom_x}, {custom_y}): custom={custom_lon}, proj={proj_lon}"
531+
532+
def test_lambert_conformal_conic_against_proj():
533+
# Create our custom projection with parameters from the existing test
534+
lambda_0 = 352 # Reference longitude in degrees
535+
phi_0 = 55.5 # Reference latitude in degrees
536+
phi_1 = 55.5 # First standard parallel in degrees
537+
phi_2 = 55.5 # Second standard parallel in degrees
538+
radius = 6371229.0 # Earth radius in meters
539+
540+
custom_proj = LambertConformalConicProjection(
541+
lambda_0=lambda_0, phi_0=phi_0, phi_1=phi_1, phi_2=phi_2, radius=radius
542+
)
543+
544+
lambda_0_norm = _normalize_longitude(lambda_0)
545+
# Create equivalent PROJ projection
546+
# For Lambert Conformal Conic, we use lat_0, lon_0, lat_1, lat_2 parameters
547+
proj_string = (f"+proj=lcc +lat_0={phi_0} +lon_0={lambda_0_norm} +lat_1={phi_1} +lat_2={phi_2} "
548+
f"+x_0=0 +y_0=0 +R={radius} +units=m +no_defs +type=crs")
549+
proj_proj = pyproj.Proj(proj_string)
550+
551+
# Test points from the existing test
552+
center_lat = 39.671
553+
center_lon = -25.421997
554+
test_points = [
555+
(center_lat, center_lon), # Center point
556+
(39.675304, -25.400146), # Near the center
557+
(42.18604, -15.30127), # Point from the test (x=456, y=64)
558+
(64.943695, 30.711975), # Point from the test (x=1642, y=1573)
559+
# Additional test points for broader coverage
560+
(0, 0), # Origin
561+
(phi_0, lambda_0_norm), # Projection origin
562+
(45, 0), # Mid-latitude point
563+
(-45, -45), # Southern hemisphere
564+
(10, 50), # Europe
565+
(40, -100), # North America
566+
(50, -170), # Pacific
567+
(-30, 170), # South Pacific
568+
]
569+
570+
for lat, lon in test_points:
571+
# Forward transformation using our implementation
572+
custom_x, custom_y = custom_proj.forward(latitude=lat, longitude=lon)
573+
574+
# Forward transformation using PROJ
575+
# Note: PROJ expects (lon, lat) order, not (lat, lon)
576+
proj_x, proj_y = proj_proj(lon, lat)
577+
tolerance = 0.1 # 0.1 meters for a 6.3 million meter radius is a reasonable precision
578+
assert abs(custom_x - proj_x) < tolerance, f"X mismatch for ({lat}, {lon}): custom={custom_x}, proj={proj_x}"
579+
assert abs(custom_y - proj_y) < tolerance, f"Y mismatch for ({lat}, {lon}): custom={custom_y}, proj={proj_y}"
580+
581+
# Test inverse transformation
582+
custom_lat, custom_lon = custom_proj.inverse(x=custom_x, y=custom_y)
583+
# PROJ expects inverse=True for inverse transform
584+
proj_lon, proj_lat = proj_proj(proj_x, proj_y, inverse=True)
585+
angular_tolerance = 1e-5 # approximately 0.00001 degrees
586+
assert abs(custom_lat - proj_lat) < angular_tolerance, \
587+
f"Lat mismatch for ({custom_x}, {custom_y}): custom={custom_lat}, proj={proj_lat}"
588+
589+
# Handle longitude wraparound for comparison
590+
lon_diff = np.mod(abs(custom_lon - proj_lon), 360)
591+
assert min(lon_diff, 360 - lon_diff) < angular_tolerance, \
592+
f"Lon mismatch for ({custom_x}, {custom_y}): custom={custom_lon}, proj={proj_lon}"

0 commit comments

Comments
 (0)