Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Pylint
name: Lint

on:
push:
Expand All @@ -7,22 +7,24 @@ on:
pull_request:

jobs:
pylint:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint pyserial flask flask-socketio flask-cors numpy opencv-python supervision
pip install trackers@git+https://github.com/roboflow/trackers.git
run: uv sync --group dev --python ${{ matrix.python-version }}
- name: Ruff Check
run: uv run --python ${{ matrix.python-version }} ruff check src/openflight/ tests/
- name: Ruff Format Check
run: uv run --python ${{ matrix.python-version }} ruff format --check src/openflight/ tests/
- name: Analysing the code with pylint
run: |
pylint src/openflight/ --fail-under=9
run: uv run --python ${{ matrix.python-version }} pylint src/openflight/ --fail-under=9
17 changes: 7 additions & 10 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ jobs:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pyserial flask flask-socketio flask-cors numpy opencv-python supervision
pip install trackers@git+https://github.com/roboflow/trackers.git
pip install -e .
run: uv sync --group dev --python ${{ matrix.python-version }}
- name: Run tests
run: |
pytest tests/ -v --tb=short
run: uv run --python ${{ matrix.python-version }} pytest tests/ -v --tb=short
29 changes: 23 additions & 6 deletions .github/workflows/ui-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,35 @@ jobs:
run: npm run format:check
working-directory: ui

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".node-version"
cache: "npm"
cache-dependency-path: ui/package-lock.json
- name: Install dependencies
run: npm ci
working-directory: ui
- name: Run unit tests
run: npm run test
working-directory: ui

e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
- name: Set up Python
run: uv python install 3.12
- name: Install backend dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
run: uv sync --python 3.12
- name: Set up Node.js
uses: actions/setup-node@v6
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ session_logs/*/
# Test artifacts
test_camera.jpg
.pytest_cache/
.pytest_temp/
.coverage
htmlcov/

Expand Down
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
.PHONY: test lint format dev build-ui start
.PHONY: test test-ui lint format dev build-ui start

## Run Python tests
test:
uv run pytest tests/ -v

## Run UI unit tests
test-ui:
cd ui && npm run test

## Run all linters (Python + UI)
lint:
uv run ruff check src/openflight/
uv run ruff check src/openflight/ tests/
uv run pylint src/openflight/ --fail-under=9
cd ui && npm run lint

## Auto-format Python code
format:
uv run ruff format src/openflight/
uv run ruff check --fix src/openflight/
uv run ruff format src/openflight/ tests/
uv run ruff check --fix src/openflight/ tests/

## Start server in mock mode (no hardware needed)
dev:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ dev = [
"pylint>=3.3.9",
"pre-commit>=4.0.0",
"pytest>=9.0.3",
"ruff>=0.1.0",
"ruff>=0.9.10",
]

[tool.ruff]
Expand Down Expand Up @@ -127,3 +127,4 @@ max-line-length = 100
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "--basetemp=.pytest_temp"
72 changes: 24 additions & 48 deletions src/openflight/ballistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from dataclasses import dataclass
from typing import Literal, Optional

from .club_data import CLUB_TYPICAL_SPIN_RPM
from .launch_monitor import SPIN_CONFIDENCE_HIGH, ClubType, Shot

MPH_TO_MPS = 0.44704
Expand All @@ -33,7 +34,7 @@
# the rules rather than by a guess at the specific ball in play.
BALL_MASS_KG = 0.04593
BALL_RADIUS_M = 0.02135
BALL_AREA_M2 = math.pi * BALL_RADIUS_M ** 2
BALL_AREA_M2 = math.pi * BALL_RADIUS_M**2
AIR_DENSITY_STD = 1.225 # kg/m³ at sea level, 15 °C ISA

# Cd = CD_BASE + CD_SPIN_COEFF * Sp
Expand Down Expand Up @@ -65,32 +66,6 @@
# keeping payload size reasonable for UI/log consumers.
SAMPLE_INTERVAL_S = 0.05

# Club-typical spin (RPM) from TrackMan PGA Tour averages.
# Used as fallback when measured spin is missing or low-confidence.
CLUB_TYPICAL_SPIN_RPM: dict[ClubType, float] = {
ClubType.DRIVER: 2700,
ClubType.WOOD_3: 3500,
ClubType.WOOD_5: 4200,
ClubType.WOOD_7: 4800,
ClubType.HYBRID_3: 4400,
ClubType.HYBRID_5: 4900,
ClubType.HYBRID_7: 5300,
ClubType.HYBRID_9: 5800,
ClubType.IRON_2: 4000,
ClubType.IRON_3: 4500,
ClubType.IRON_4: 5000,
ClubType.IRON_5: 5400,
ClubType.IRON_6: 6000,
ClubType.IRON_7: 6500,
ClubType.IRON_8: 7500,
ClubType.IRON_9: 8500,
ClubType.PW: 9000,
ClubType.GW: 9500,
ClubType.SW: 10000,
ClubType.LW: 10500,
ClubType.UNKNOWN: 5000,
}


@dataclass
class LaunchConditions:
Expand Down Expand Up @@ -156,9 +131,7 @@ def resolve_launch(shot: Shot) -> Optional[LaunchConditions]:
spin_rpm = float(shot.spin_rpm)
source: Literal["measured", "club_typical"] = "measured"
else:
spin_rpm = CLUB_TYPICAL_SPIN_RPM.get(
shot.club, CLUB_TYPICAL_SPIN_RPM[ClubType.UNKNOWN]
)
spin_rpm = CLUB_TYPICAL_SPIN_RPM.get(shot.club, CLUB_TYPICAL_SPIN_RPM[ClubType.UNKNOWN])
source = "club_typical"

return LaunchConditions(
Expand Down Expand Up @@ -244,10 +217,7 @@ def _rk4_step(
k3 = _derivatives(s3, omega, axis, air_density)
s4 = tuple(state[i] + dt * k3[i] for i in range(6))
k4 = _derivatives(s4, omega, axis, air_density)
return tuple(
state[i] + (dt / 6.0) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])
for i in range(6)
)
return tuple(state[i] + (dt / 6.0) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) for i in range(6))


def simulate(
Expand Down Expand Up @@ -311,15 +281,17 @@ def simulate(
final = tuple(state[i] + frac * (new_state[i] - state[i]) for i in range(6))
fx, fy, fz, fvx, fvy, fvz = final
v_final = math.sqrt(fvx * fvx + fvy * fvy + fvz * fvz)
landing_angle = math.degrees(
math.atan2(-fvz, math.sqrt(fvx * fvx + fvy * fvy))
landing_angle = math.degrees(math.atan2(-fvz, math.sqrt(fvx * fvx + fvy * fvy)))
points.append(
TrajectoryPoint(
t_hit,
fx * M_TO_YD,
fy * M_TO_YD,
max(fz, 0.0) * M_TO_YD,
v_final * MPS_TO_MPH,
omega * 60 / (2 * math.pi),
)
)
points.append(TrajectoryPoint(
t_hit,
fx * M_TO_YD, fy * M_TO_YD, max(fz, 0.0) * M_TO_YD,
v_final * MPS_TO_MPH,
omega * 60 / (2 * math.pi),
))
return Trajectory(
points=points,
carry_yards=fx * M_TO_YD,
Expand All @@ -335,12 +307,16 @@ def simulate(
if t - last_sample_t >= SAMPLE_INTERVAL_S:
sx_, sy_, sz_, svx, svy, svz = state
v = math.sqrt(svx * svx + svy * svy + svz * svz)
points.append(TrajectoryPoint(
t,
sx_ * M_TO_YD, sy_ * M_TO_YD, sz_ * M_TO_YD,
v * MPS_TO_MPH,
omega * 60 / (2 * math.pi),
))
points.append(
TrajectoryPoint(
t,
sx_ * M_TO_YD,
sy_ * M_TO_YD,
sz_ * M_TO_YD,
v * MPS_TO_MPH,
omega * 60 / (2 * math.pi),
)
)
last_sample_t = t

# Flight did not terminate — return current state as best-effort
Expand Down
Loading
Loading