Source code for shoot.fit

#!/usr/bin/env python3
"""
Optimization and fitting routines

Functions for fitting geometric shapes to spatial data.
"""

import numpy as np
from scipy.optimize import least_squares

from . import geo as sgeo

GRAVITY = 9.81
OMEGA = 2 * np.pi / 86400

# %%
# Ellipse Mean Square fit
# -----------------------


def _residuals(params, points):
    """Compute residuals between points and ellipse

    Parameters
    ----------
    params : array-like
        Ellipse parameters [xc, yc, a, b, theta].
    points : ndarray
        Point coordinates of shape (n, 2).

    Returns
    -------
    ndarray
        Normalized distances minus 1.
    """
    xc, yc, a, b, theta = params
    R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
    shifted = points - [xc, yc]
    rotated = shifted @ R
    normed = rotated / [a, b]
    distances = np.linalg.norm(normed, axis=1)
    return distances - 1


[docs] def fit_ellipse_from_coords(lons, lats, get_fit=False): """Fit ellipse to geographic coordinates Uses least-squares optimization to fit an ellipse to a set of points given in geographic coordinates. Parameters ---------- lons : array-like Longitude coordinates in degrees. lats : array-like Latitude coordinates in degrees. get_fit : bool, default False If True, return fit error along with parameters. Returns ------- dict or tuple Dictionary with keys: - lon : Center longitude in degrees - lat : Center latitude in degrees - a : Semi-major axis in kilometers - b : Semi-minor axis in kilometers - angle : Orientation angle in degrees If get_fit=True, returns (dict, error) tuple. Example ------- >>> import numpy as np >>> from shoot.fit import fit_ellipse_from_coords >>> theta = np.linspace(0, 2 * np.pi, 50) >>> lons = 5.0 + 0.5 * np.cos(theta) >>> lats = 43.0 + 0.3 * np.sin(theta) >>> params = fit_ellipse_from_coords(lons, lats) # doctest: +SKIP >>> print(f"center: ({params['lon']:.1f}, {params['lat']:.1f})") # doctest: +SKIP """ lons = np.array(lons) lats = np.array(lats) lon0 = lons.mean() lat0 = lats.mean() x = sgeo.deg2m(lons - lon0, lat0) y = sgeo.deg2m(lats - lat0) points = np.column_stack((x, y)) # 4. Estimation initiale simple xc0, yc0 = np.mean(points, axis=0) a0, b0 = (np.ptp(points, axis=0)) / 2 theta0 = 0.0 x0 = [xc0, yc0, a0, b0, theta0] # 5. Fit avec least_squares result = least_squares(_residuals, x0, args=(points,)) xc, yc, a, b, theta = result.x error = np.mean(result.fun**2) lat = lat0 + sgeo.m2deg(yc) lon = lon0 + sgeo.m2deg(xc, lat=lat0) if b > a: a, b = b, a theta += np.pi / 2 out = dict(lon=lon, lat=lat, a=a / 1e3, b=b / 1e3, angle=np.degrees(theta)) if get_fit: out = out, error return out