|
1 | 1 | import numpy as np |
| 2 | +import pyproj |
2 | 3 | import pytest |
3 | 4 | from omfiles.grids import ( |
4 | 5 | LambertAzimuthalEqualAreaProjection, |
|
8 | 9 | StereographicProjection, |
9 | 10 | ) |
10 | 11 | from omfiles.om_domains import RegularLatLonGrid |
| 12 | +from omfiles.utils import _normalize_longitude |
11 | 13 |
|
12 | 14 | # Fixtures for grids |
13 | 15 |
|
@@ -371,3 +373,220 @@ def test_lambert_conformal_conic_projection(): |
371 | 373 | assert abs(lon - 30.711975) < 0.001 |
372 | 374 | point_idx = grid.findPointXy(lat=lat, lon=lon) |
373 | 375 | 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