65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""
|
|
Group size computation from PointOfImpact real-world coordinates.
|
|
|
|
All measurements in millimetres. Origin is point-of-aim:
|
|
x > 0 = right (windage), y > 0 = up (elevation).
|
|
"""
|
|
import math
|
|
|
|
|
|
def compute_group_size(
|
|
points: list[tuple[float, float]],
|
|
distance_m: float | None = None,
|
|
) -> dict:
|
|
"""
|
|
Compute ballistic group metrics from a list of (x_mm, y_mm) coordinates.
|
|
|
|
Args:
|
|
points: list of (x_mm, y_mm) tuples — minimum 2 required.
|
|
distance_m: shooting distance in metres, used for MOA conversion.
|
|
Pass None to leave MOA fields as None.
|
|
|
|
Returns:
|
|
dict with keys matching GroupPhotoAnalysis fields.
|
|
"""
|
|
if len(points) < 2:
|
|
raise ValueError("At least 2 points of impact are required.")
|
|
|
|
xs = [p[0] for p in points]
|
|
ys = [p[1] for p in points]
|
|
n = len(points)
|
|
|
|
# Extreme spread: maximum pairwise distance
|
|
group_size_mm = 0.0
|
|
for i in range(n):
|
|
for j in range(i + 1, n):
|
|
d = math.sqrt((xs[i] - xs[j]) ** 2 + (ys[i] - ys[j]) ** 2)
|
|
if d > group_size_mm:
|
|
group_size_mm = d
|
|
|
|
# Centroid
|
|
cx = sum(xs) / n
|
|
cy = sum(ys) / n
|
|
|
|
# Mean radius: average distance from centroid
|
|
mean_radius_mm = sum(
|
|
math.sqrt((x - cx) ** 2 + (y - cy) ** 2) for x, y in points
|
|
) / n
|
|
|
|
def to_moa(mm: float) -> float | None:
|
|
"""Convert mm at distance_m to MOA. 1 MOA ≈ 0.29089 mm/m at that distance."""
|
|
if distance_m is None or distance_m <= 0:
|
|
return None
|
|
return round(mm / (distance_m * 0.29089), 3)
|
|
|
|
return {
|
|
'group_size_mm': round(group_size_mm, 2),
|
|
'group_size_moa': to_moa(group_size_mm),
|
|
'mean_radius_mm': round(mean_radius_mm, 2),
|
|
'mean_radius_moa': to_moa(mean_radius_mm),
|
|
'windage_offset_mm': round(cx, 2),
|
|
'windage_offset_moa': to_moa(cx),
|
|
'elevation_offset_mm': round(cy, 2),
|
|
'elevation_offset_moa': to_moa(cy),
|
|
}
|