sprint-0: foundations -- data model, seed library, tests, demo
Initial commit. Delivers what the brief calls 'Sprint 0 - Foundations' (see docs/AR_Autopilot_brief.md section 12): - Complete repository structure (arautopilot package + firmware, display, installer, tools placeholders + docs). - Core data model (Pydantic v2): modes, alarms, actuator config, PID config + gain scheduling, vessel config, knob state machine, project config with YAML/JSON serialisation. - Seed library: 2 actuator profiles (hydraulic & electric DC reversible) and 2 default tunings (yacht motor planeo 30 m and 40 m). Conservative literature values, NOT the integrator's production tuning IP. - Firmware skeleton: only src/hal/pinout.h with the 21 I/O contract for the AR-NMEA-IO v1.0 board. No drivers, no main loop. - Studio stubs (real PySide6 app starts in Sprint 4). - pytest suite (80 tests, all green): modes, alarms, actuator, PID (incl. gain interpolation and the +/-50% adaptive bound from brief section 6), vessel, knob state, project config, library loader, end-to-end roundtrip. - examples/sprint0_demo.py - the acceptance demo from the brief. Acceptance criteria met: - pytest green (80/80) - demo creates, saves (YAML + JSON), reloads, and verifies a full ProjectConfig using the seed library - repository ready for tag `sprint-0-approved` See CHANGELOG.md for the detailed scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Sprint 0 acceptance demo.
|
||||
|
||||
Runs end-to-end the workflow the brief calls out in section 12:
|
||||
|
||||
1. Build a ``ProjectConfig`` programmatically using the seed library.
|
||||
2. Save it to disk (YAML and JSON).
|
||||
3. Reload it from disk.
|
||||
4. Verify the reloaded object matches the original.
|
||||
5. Print a human-readable summary.
|
||||
|
||||
Usage::
|
||||
|
||||
python examples/sprint0_demo.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from arautopilot.core import (
|
||||
ProjectConfig,
|
||||
VesselConfig,
|
||||
VesselType,
|
||||
interpolate_gains,
|
||||
)
|
||||
from arautopilot.library.loader import (
|
||||
list_actuator_profiles,
|
||||
list_default_tunings,
|
||||
load_actuator_profile,
|
||||
load_default_tuning,
|
||||
)
|
||||
from arautopilot.version import __version__
|
||||
|
||||
OUTPUT_DIR = Path(__file__).parent / "output"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 78)
|
||||
print(f" AR-Autopilot v{__version__} -- Sprint 0 acceptance demo")
|
||||
print("=" * 78)
|
||||
|
||||
# ---- 1. Inspect what the seed library carries ---------------------------
|
||||
print("\n[1] Seed library contents:")
|
||||
for profile in list_actuator_profiles():
|
||||
print(f" - actuator profile : {profile}")
|
||||
for tuning in list_default_tunings():
|
||||
print(f" - default tuning : {tuning}")
|
||||
|
||||
# ---- 2. Compose a project from seed assets ------------------------------
|
||||
print("\n[2] Building project from seed library...")
|
||||
actuator = load_actuator_profile("hydraulic_reversible")
|
||||
pid = load_default_tuning("yacht_motor_planeo_30m")
|
||||
|
||||
vessel = VesselConfig(
|
||||
name="M/Y Sprint Zero",
|
||||
type=VesselType.YACHT_MOTOR_PLANEO,
|
||||
length_m=30.0,
|
||||
displacement_t=125.0,
|
||||
max_speed_kn=28.0,
|
||||
actuator=actuator,
|
||||
pid=pid,
|
||||
)
|
||||
|
||||
project = ProjectConfig(
|
||||
client_name="Demo Shipyard S.L.",
|
||||
project_name="Sprint 0 demonstrator",
|
||||
notes=(
|
||||
"Created by examples/sprint0_demo.py. Seed-library actuator + tuning "
|
||||
"for a 30 m planing motor yacht. Conservative literature gains; not "
|
||||
"the integrator's production tuning."
|
||||
),
|
||||
vessel=vessel,
|
||||
)
|
||||
|
||||
# ---- 3. Save it to disk -------------------------------------------------
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
yaml_out = project.save_yaml(OUTPUT_DIR / "demo_project.yaml")
|
||||
json_out = project.save_json(OUTPUT_DIR / "demo_project.json")
|
||||
print(f"\n[3] Saved to:")
|
||||
print(f" - YAML : {yaml_out}")
|
||||
print(f" - JSON : {json_out}")
|
||||
|
||||
# ---- 4. Reload and verify integrity -------------------------------------
|
||||
print("\n[4] Reloading from disk and verifying integrity...")
|
||||
rebuilt_yaml = ProjectConfig.load(yaml_out)
|
||||
rebuilt_json = ProjectConfig.load(json_out)
|
||||
|
||||
assert rebuilt_yaml == project, "YAML round-trip mismatch"
|
||||
assert rebuilt_json == project, "JSON round-trip mismatch"
|
||||
print(" OK -- both YAML and JSON reload to identical objects.")
|
||||
|
||||
# ---- 5. Show a human-readable summary -----------------------------------
|
||||
print("\n[5] Project summary:")
|
||||
print(f" project_id : {project.project_id}")
|
||||
print(f" client : {project.client_name}")
|
||||
print(f" project name : {project.project_name}")
|
||||
print(f" vessel : {project.vessel.name}")
|
||||
print(f" vessel type : {project.vessel.type.value}")
|
||||
print(f" length / Vmax : {project.vessel.length_m:.1f} m / {project.vessel.max_speed_kn:.1f} kn")
|
||||
print(f" actuator type : {project.vessel.actuator.type.value}")
|
||||
print(f" rudder limit : +/-{project.vessel.actuator.max_rudder_angle_deg:.1f} deg"
|
||||
f" at max {project.vessel.actuator.max_rate_dps:.1f} deg/s")
|
||||
print(f" inner PID Kp/Ki/Kd: "
|
||||
f"{project.vessel.pid.inner_loop_base.kp} / "
|
||||
f"{project.vessel.pid.inner_loop_base.ki} / "
|
||||
f"{project.vessel.pid.inner_loop_base.kd}")
|
||||
print(f" outer PID Kp/Ki/Kd: "
|
||||
f"{project.vessel.pid.outer_loop_base.kp} / "
|
||||
f"{project.vessel.pid.outer_loop_base.ki} / "
|
||||
f"{project.vessel.pid.outer_loop_base.kd}")
|
||||
print(f" ROT feed-forward : {project.vessel.pid.rot_feedforward_gain}")
|
||||
print(f" gain schedule pts : {len(project.vessel.pid.gain_schedule)}")
|
||||
|
||||
# Show interpolation working
|
||||
print("\n Interpolated outer-loop gains by SOG:")
|
||||
for sog in (3.0, 5.0, 10.0, 15.0, 20.0, 28.0, 35.0):
|
||||
gains = interpolate_gains(project.vessel.pid.gain_schedule, sog)
|
||||
print(
|
||||
f" SOG = {sog:5.1f} kn -> "
|
||||
f"Kp={gains.kp:.3f} Ki={gains.ki:.4f} Kd={gains.kd:.3f}"
|
||||
)
|
||||
|
||||
print("\nDemo complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user