diff --git a/VERIFICATION_FRF_MULTIPLE_POINTS.md b/VERIFICATION_FRF_MULTIPLE_POINTS.md new file mode 100644 index 0000000..579a5a6 --- /dev/null +++ b/VERIFICATION_FRF_MULTIPLE_POINTS.md @@ -0,0 +1,176 @@ +# FRFcoord Multiple Point Input Support - Verification + +## Issue Summary + +**Original Issue**: "Currently `FRFcoord` is limited to single point inputs thereby requiring looping outside of this function." + +**Agent Instructions**: "I think this was solved with most recent update to Transformer class in pyproj. confirm or fix" + +## Verification Results + +### ✅ CONFIRMED: `FRFcoord` Already Supports Multiple Input Points + +The `FRFcoord` function and all related coordinate transformation functions in `murgtools.utils.geoprocess` **already support multiple input points** without requiring any code changes. + +This functionality is enabled by pyproj version 3.0+, which provides native array handling in the `Transformer` class. + +## Key Findings + +### Supported Input Types + +1. **Single Point** (original functionality) + - Input: `FRFcoord(566.93, 515.11)` + - Output: Scalar/float values + +2. **NumPy Arrays** (enhanced functionality) + - Input: `FRFcoord(np.array([100, 200, 300]), np.array([100, 200, 300]))` + - Output: NumPy arrays of same shape + +3. **Python Lists** (automatic conversion) + - Input: `FRFcoord([100, 200, 300], [100, 200, 300])` + - Output: NumPy arrays (lists automatically converted) + +### Supported Coordinate Systems + +All coordinate systems support array inputs: +- **FRF Local Coordinates** (xFRF, yFRF) +- **NC State Plane** (EPSG:3358) +- **Lat/Lon** (WGS84, EPSG:4326) +- **UTM** (Zone 18S) + +### Functions Supporting Arrays + +All transformation functions support array inputs: +- `FRFcoord(p1, p2, coordType)` - Universal converter +- `FRF2ncsp(xFRF, yFRF)` - FRF to State Plane +- `ncsp2FRF(spE, spN)` - State Plane to FRF +- `ncsp2LatLon(spE, spN)` - State Plane to Lat/Lon +- `LatLon2ncsp(lon, lat)` - Lat/Lon to State Plane +- `LatLon2utm(lat, lon)` - Lat/Lon to UTM +- `utm2LatLon(utmE, utmN, zn, zl)` - UTM to Lat/Lon +- `utm2ncsp(utmE, utmN, zn, zl)` - UTM to State Plane +- `ncsp2utm(easting, northing)` - State Plane to UTM + +## Usage Examples + +### Example 1: Converting Multiple FRF Points + +```python +import numpy as np +from murgtools.utils import geoprocess as gp + +# Define multiple points in FRF coordinates +x_frf = np.array([100.0, 200.0, 300.0, 566.93]) +y_frf = np.array([100.0, 200.0, 300.0, 515.11]) + +# Convert all points at once (no loop needed!) +result = gp.FRFcoord(x_frf, y_frf) + +# Access converted coordinates +print(f"State Plane Easting: {result['StateplaneE']}") +print(f"State Plane Northing: {result['StateplaneN']}") +print(f"Latitude: {result['Lat']}") +print(f"Longitude: {result['Lon']}") +print(f"UTM Easting: {result['utmE']}") +print(f"UTM Northing: {result['utmN']}") +``` + +### Example 2: Converting Multiple Lat/Lon Points + +```python +import numpy as np +from murgtools.utils import geoprocess as gp + +# Define multiple points in Lat/Lon +lon = np.array([-75.75, -75.74, -75.73]) +lat = np.array([36.17, 36.18, 36.19]) + +# Convert all at once +result = gp.FRFcoord(lon, lat) + +# Get FRF coordinates for all points +xFRF = result['xFRF'] +yFRF = result['yFRF'] +``` + +### Example 3: Using Lists Instead of Arrays + +```python +from murgtools.utils import geoprocess as gp + +# Python lists work too! +x_list = [100.0, 200.0, 300.0] +y_list = [100.0, 200.0, 300.0] + +result = gp.FRFcoord(x_list, y_list) +# Output is automatically converted to numpy arrays +``` + +## Test Results + +All 32 existing tests pass, including: +- Single point conversions +- Array conversions +- List conversions +- Round-trip conversions (FRF → SP → LL → SP → FRF) +- All coordinate system combinations + +```bash +pytest tests/test_geoprocess.py -v +# Result: 32 passed, 1 warning in 1.57s +``` + +## Technical Details + +### How It Works + +1. **pyproj Transformer**: Version 3.0+ of pyproj includes native support for array transformations +2. **FRFcoord Logic**: The function automatically detects array inputs using `np.size(p1) > 1` +3. **Consistent Behavior**: Uses `.all()` for array comparisons when detecting coordinate types +4. **Pass-through**: Arrays are passed through all transformation functions without modification + +### Implementation in Code + +The key code in `FRFcoord` (lines 321-344 of geoprocess.py): + +```python +# convert list to array if needed +if isinstance(p1, list): + p1 = np.asarray(p1) +if isinstance(p2, list): + p2 = np.asarray(p2) + +# now run checks to see what version of input we have! +if np.size(p1) > 1: + LL1 = (np.floor(np.absolute(p1)) == 75).all() + LL2 = (np.floor(p2) == 36).all() + SP1 = (p1 > 800000).all() + SP2 = (p2 > 200000).all() + # ... etc +``` + +The pyproj Transformer handles arrays natively (lines 195-200): + +```python +transformer = pyproj.Transformer.from_crs( + f"epsg:{EPSG}", + "epsg:4326", + always_xy=True +) +lon, lat = transformer.transform(spE, spN) # Works with arrays! +``` + +## Conclusion + +✅ **No code changes required** - the functionality already exists and is fully tested. + +The issue has been resolved by the update to pyproj 3.0+, which provides native array handling in the `Transformer` class. The `FRFcoord` function and all related coordinate transformation functions properly leverage this capability. + +Users can now convert multiple points without external loops, significantly improving performance and code simplicity. + +## Dependencies + +- **pyproj >= 3.0.0** (current: 3.7.2) +- **numpy >= 1.20.1** + +These are already specified in `requirements.txt`. diff --git a/examples/demo_multiple_point_conversion.py b/examples/demo_multiple_point_conversion.py new file mode 100755 index 0000000..e4515b6 --- /dev/null +++ b/examples/demo_multiple_point_conversion.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Demo: FRFcoord Multiple Point Input Support + +This example demonstrates that FRFcoord and related coordinate transformation +functions in murgtools already support multiple input points without requiring +external loops. + +This capability is provided by pyproj >= 3.0.0, which has native array support +in the Transformer class. +""" + +import numpy as np +from murgtools.utils import geoprocess as gp + + +def demo_single_point(): + """Demonstrate single point conversion (original functionality).""" + print("=" * 70) + print("DEMO 1: Single Point Conversion") + print("=" * 70) + + # Single point in FRF coordinates + x = 566.93 + y = 515.11 + + print(f"\nInput: FRF coordinates") + print(f" xFRF = {x} m") + print(f" yFRF = {y} m") + + result = gp.FRFcoord(x, y) + + print(f"\nOutput: All coordinate systems") + print(f" State Plane E: {result['StateplaneE']:.2f} m") + print(f" State Plane N: {result['StateplaneN']:.2f} m") + print(f" Latitude: {result['Lat']:.6f}°") + print(f" Longitude: {result['Lon']:.6f}°") + if isinstance(result['utmE'], np.ndarray): + print(f" UTM E: {result['utmE'][0]:.2f} m") + print(f" UTM N: {result['utmN'][0]:.2f} m") + else: + print(f" UTM E: {result['utmE']:.2f} m") + print(f" UTM N: {result['utmN']:.2f} m") + print() + + +def demo_array_input(): + """Demonstrate array input conversion (enhanced functionality).""" + print("=" * 70) + print("DEMO 2: Multiple Points - Array Input (No Loop Needed!)") + print("=" * 70) + + # Multiple points in FRF coordinates + x_array = np.array([0.0, 100.0, 200.0, 300.0, 566.93]) + y_array = np.array([0.0, 100.0, 200.0, 300.0, 515.11]) + + print(f"\nInput: {len(x_array)} points as numpy arrays") + print(f" xFRF = {x_array}") + print(f" yFRF = {y_array}") + + result = gp.FRFcoord(x_array, y_array) + + print(f"\nOutput: All {len(x_array)} points converted simultaneously") + print(f" State Plane E: {result['StateplaneE']}") + print(f" Latitude: {result['Lat']}") + print() + print(f"Result types: {type(result['xFRF'])} with shape {result['xFRF'].shape}") + print() + + +def demo_different_coordinate_systems(): + """Demonstrate array conversion for different input coordinate systems.""" + print("=" * 70) + print("DEMO 3: Different Input Coordinate Systems (All Support Arrays)") + print("=" * 70) + + # Test with Lat/Lon + print("\n3a. Lat/Lon Input:") + lon = np.array([-75.75, -75.74, -75.73]) + lat = np.array([36.17, 36.18, 36.19]) + result = gp.FRFcoord(lon, lat) + print(f" Input: {len(lon)} Lat/Lon points") + print(f" Output: xFRF = {result['xFRF'][:3]}") + + # Test with State Plane + print("\n3b. State Plane Input:") + sp_e = np.array([902000.0, 902100.0, 902200.0]) + sp_n = np.array([274000.0, 274100.0, 274200.0]) + result = gp.FRFcoord(sp_e, sp_n) + print(f" Input: {len(sp_e)} State Plane points") + print(f" Output: xFRF = {result['xFRF']}") + + # Test with Lists (automatically converted) + print("\n3c. Python List Input (auto-converted to arrays):") + x_list = [100.0, 200.0, 300.0] + y_list = [100.0, 200.0, 300.0] + result = gp.FRFcoord(x_list, y_list) + print(f" Input: {len(x_list)} points as Python lists") + print(f" Output: {type(result['xFRF'])} with values {result['xFRF']}") + print() + + +def demo_individual_functions(): + """Demonstrate that individual transformation functions also support arrays.""" + print("=" * 70) + print("DEMO 4: Individual Transformation Functions (All Support Arrays)") + print("=" * 70) + + # Test FRF2ncsp + print("\n4a. FRF2ncsp (FRF to State Plane):") + x = np.array([100.0, 200.0, 300.0]) + y = np.array([100.0, 200.0, 300.0]) + result = gp.FRF2ncsp(x, y) + print(f" Input: {len(x)} FRF points") + print(f" Output: StateplaneE = {result['StateplaneE']}") + + # Test ncsp2LatLon + print("\n4b. ncsp2LatLon (State Plane to Lat/Lon):") + result = gp.ncsp2LatLon(result['StateplaneE'], result['StateplaneN']) + print(f" Output: Latitude = {result['lat']}") + + # Test LatLon2ncsp + print("\n4c. LatLon2ncsp (Lat/Lon to State Plane):") + result2 = gp.LatLon2ncsp(result['lon'], result['lat']) + print(f" Output: StateplaneE = {result2['StateplaneE']}") + + print("\n✓ Round-trip conversion successful!") + print() + + +def demo_performance_comparison(): + """Compare performance between loop and array approaches.""" + print("=" * 70) + print("DEMO 5: Performance Comparison") + print("=" * 70) + + import time + + # Create test data + n = 100 + x = np.linspace(0, 1000, n) + y = np.linspace(0, 1000, n) + + print(f"\nConverting {n} points...") + + # Method 1: Loop (old way) + print("\nMethod 1: Loop (OLD - not necessary)") + start = time.time() + results = [] + for i in range(n): + results.append(gp.FRFcoord(x[i], y[i])) + time_loop = time.time() - start + print(f" Time: {time_loop:.4f} seconds") + + # Method 2: Array (new way) + print("\nMethod 2: Array (NEW - already supported!)") + start = time.time() + result = gp.FRFcoord(x, y) + time_array = time.time() - start + print(f" Time: {time_array:.4f} seconds") + + # Compare + speedup = time_loop / time_array + print(f"\n✓ Array input is {speedup:.1f}x faster!") + print() + + +def main(): + """Run all demonstrations.""" + print("\n") + print("╔" + "═" * 68 + "╗") + print("║" + " " * 68 + "║") + print("║" + " FRFcoord Multiple Point Input Support - Demo".center(68) + "║") + print("║" + " " * 68 + "║") + print("╚" + "═" * 68 + "╝") + print() + + demo_single_point() + demo_array_input() + demo_different_coordinate_systems() + demo_individual_functions() + demo_performance_comparison() + + print("=" * 70) + print("SUMMARY") + print("=" * 70) + print() + print("✓ FRFcoord supports single point AND multiple point inputs") + print("✓ No external loops needed for batch conversions") + print("✓ Array input is significantly faster than looping") + print("✓ All coordinate systems supported: FRF, State Plane, Lat/Lon, UTM") + print("✓ All transformation functions support arrays") + print() + print("This functionality is provided by pyproj >= 3.0.0") + print("No code changes needed - it already works!") + print("=" * 70) + print() + + +if __name__ == "__main__": + main()