Add OARMP routing engine, dashboard and documentation
- Complete routing engine: ingest, optimizer (CG+B&B), maintenance monitor, metrics, pipeline, quality checks - Streamlit dashboard with Input/Output tab structure, editable data editors, interactive Folium map with satellite layer and maintenance base highlights, FH stacked bar chart with TTM availability - CSV data files: AERONAVES, CHECKS, AIRPORTS, ESCALA DE VOO - README, CONTEXTO and CHANGELOG added - Remove legacy pre_process scripts and raw binary files (PDFs/xlsx) - Update .gitignore to exclude outputs/, data/, raw/*.pdf, raw/*.xlsx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
50
scripts/00_setup_project.py
Normal file
50
scripts/00_setup_project.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Script 00 – Project setup.
|
||||
|
||||
Creates all required directories and validates that the raw input files exist.
|
||||
Run once after cloning the repository.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.routing_engine.config import DEFAULT_CONFIG
|
||||
|
||||
cfg = DEFAULT_CONFIG
|
||||
|
||||
DIRS = [
|
||||
cfg.raw_index_dir,
|
||||
cfg.processed_dir,
|
||||
cfg.reference_dir,
|
||||
cfg.quality_dir,
|
||||
cfg.schedules_dir,
|
||||
cfg.figures_dir,
|
||||
cfg.exports_dir,
|
||||
ROOT / "outputs" / "dashboards",
|
||||
]
|
||||
|
||||
print("Creating project directories…")
|
||||
for d in DIRS:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
print(f" ok {d.relative_to(ROOT)}")
|
||||
|
||||
print("\nChecking raw input files…")
|
||||
REQUIRED = [cfg.flight_schedule_file, cfg.aircraft_file, cfg.checks_file, cfg.airports_file]
|
||||
all_ok = True
|
||||
for fname in REQUIRED:
|
||||
p = cfg.raw_dir / fname
|
||||
if p.exists():
|
||||
size = p.stat().st_size
|
||||
print(f" ok {fname} ({size:,} bytes)")
|
||||
else:
|
||||
print(f" ✗ {fname} NOT FOUND")
|
||||
all_ok = False
|
||||
|
||||
if all_ok:
|
||||
print("\n[OK] Setup complete. Run scripts in order: 01 -> 02 -> 03 -> 04.")
|
||||
else:
|
||||
print("\n[!] Some raw files are missing. Place them in the 'raw/' folder and re-run.")
|
||||
sys.exit(1)
|
||||
41
scripts/01_inspect_inputs.py
Normal file
41
scripts/01_inspect_inputs.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Script 01 – Inspect raw input files.
|
||||
|
||||
Prints metadata (encoding, separator, column names, sample rows) for every CSV
|
||||
in the raw/ folder and saves a JSON report to data/raw_index/.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.routing_engine.config import DEFAULT_CONFIG
|
||||
from src.routing_engine.inspect_files import inspect_all
|
||||
|
||||
cfg = DEFAULT_CONFIG
|
||||
|
||||
print(f"Inspecting files in: {cfg.raw_dir}\n")
|
||||
meta = inspect_all(cfg.raw_dir)
|
||||
|
||||
for name, info in meta.items():
|
||||
print(f"-- {name} -------------------------------------------")
|
||||
if "error" in info:
|
||||
print(f" ERROR: {info['error']}")
|
||||
continue
|
||||
print(f" Encoding : {info['encoding']}")
|
||||
print(f" Separator : {repr(info['separator'])}")
|
||||
print(f" Columns : {info['columns']}")
|
||||
if info["sample_rows"]:
|
||||
print(f" First row : {info['sample_rows'][0]}")
|
||||
print()
|
||||
|
||||
# Save report
|
||||
out = cfg.raw_index_dir / "file_inventory.json"
|
||||
cfg.raw_index_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(meta, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
print(f"Report saved -> {out}")
|
||||
64
scripts/02_build_fleet_reference.py
Normal file
64
scripts/02_build_fleet_reference.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Script 02 – Build fleet reference table.
|
||||
|
||||
Reads AERONAVES.csv + CHECKS.csv, computes TTM and the full check-cycle
|
||||
sequence for each aircraft, and saves the reference to data/reference/.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
|
||||
from src.routing_engine.config import DEFAULT_CONFIG
|
||||
from src.routing_engine.inspect_files import read_aircraft, read_checks
|
||||
from src.routing_engine.ingest import build_fleet, _check_cycles
|
||||
|
||||
cfg = DEFAULT_CONFIG
|
||||
|
||||
aircraft_df = read_aircraft(cfg.raw_dir / cfg.aircraft_file)
|
||||
checks_df = read_checks(cfg.raw_dir / cfg.checks_file)
|
||||
|
||||
print("Aircraft loaded:")
|
||||
print(aircraft_df.to_string(index=False))
|
||||
print()
|
||||
|
||||
print("Checks loaded:")
|
||||
print(checks_df.to_string(index=False))
|
||||
print()
|
||||
|
||||
# Build fleet
|
||||
planning_start = datetime(cfg.planning_year, 3, 18) # first OFRAG departure in the sample data
|
||||
fleet = build_fleet(aircraft_df, checks_df, planning_start)
|
||||
|
||||
print("Fleet reference:")
|
||||
for _, row in fleet.iterrows():
|
||||
print(f"\n {row['tail_number']} ({row['model']}) FH total = {row['fh_total']:.0f}")
|
||||
print(f" TTM before first check: {row['ttm_hours']:.1f} FH")
|
||||
for i, c in enumerate(row["checks"]):
|
||||
print(f" Cycle {i}: threshold={c['fh_threshold']:.0f} FH, "
|
||||
f"TTM={c['ttm']:.0f} FH, duration={c['duration_hours']:.0f} h")
|
||||
|
||||
# Save to reference
|
||||
cfg.reference_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = cfg.reference_dir / "fleet_reference.csv"
|
||||
|
||||
flat_rows = []
|
||||
for _, row in fleet.iterrows():
|
||||
flat_rows.append(
|
||||
{
|
||||
"tail_number": row["tail_number"],
|
||||
"model": row["model"],
|
||||
"fh_total": row["fh_total"],
|
||||
"ttm_hours_cycle0": row["ttm_hours"],
|
||||
"n_check_cycles": len(row["checks"]),
|
||||
"cycles_json": str(row["checks"]),
|
||||
}
|
||||
)
|
||||
|
||||
pd.DataFrame(flat_rows).to_csv(out_path, index=False)
|
||||
print(f"\nFleet reference saved -> {out_path}")
|
||||
44
scripts/03_run_optimization_pipeline.py
Normal file
44
scripts/03_run_optimization_pipeline.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Script 03 – Run the full optimisation pipeline.
|
||||
|
||||
Loads data, builds the network, runs Column Generation + B&B, and prints
|
||||
the solution. All outputs are saved to outputs/ automatically.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.routing_engine import RoutingPipeline, DEFAULT_CONFIG
|
||||
|
||||
cfg = DEFAULT_CONFIG
|
||||
# Adjust parameters here if needed:
|
||||
# cfg.tat_minutes = 60
|
||||
# cfg.mip_time_limit_seconds = 120
|
||||
|
||||
pipe = RoutingPipeline(cfg)
|
||||
result = pipe.run(save_outputs=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SOLUTION SUMMARY")
|
||||
print("=" * 60)
|
||||
s = result["summary"]
|
||||
for k, v in s.items():
|
||||
print(f" {k:<35} {v}")
|
||||
|
||||
print("\n-- Schedule ----------------------------------------------")
|
||||
sched = result["schedule"]
|
||||
if not sched.empty:
|
||||
print(sched[["aircraft", "ofrag_id", "departure", "arrival", "flight_hours", "maintenance_before"]].to_string(index=False))
|
||||
|
||||
print("\n-- Maintenance events -------------------------------------")
|
||||
maint = result["maintenance"]
|
||||
if maint.empty:
|
||||
print(" No forced maintenance events.")
|
||||
else:
|
||||
print(maint.to_string(index=False))
|
||||
|
||||
print("\n-- Fleet utilisation --------------------------------------")
|
||||
print(result["fleet"].to_string(index=False))
|
||||
75
scripts/04_generate_flight_strings.py
Normal file
75
scripts/04_generate_flight_strings.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Script 04 – Generate and export flight strings.
|
||||
|
||||
Reads the saved solution CSVs and generates a per-aircraft "flight string"
|
||||
report in human-readable format (console + Excel export).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from src.routing_engine.config import DEFAULT_CONFIG
|
||||
|
||||
cfg = DEFAULT_CONFIG
|
||||
|
||||
sched_path = cfg.schedules_dir / "flight_strings.csv"
|
||||
maint_path = cfg.schedules_dir / "maintenance_events.csv"
|
||||
|
||||
if not sched_path.exists():
|
||||
print("No schedule found. Run script 03 first.")
|
||||
sys.exit(1)
|
||||
|
||||
sched = pd.read_csv(sched_path, parse_dates=["departure", "arrival"])
|
||||
maint = pd.read_csv(maint_path, parse_dates=["maint_start", "maint_end"]) if maint_path.exists() else pd.DataFrame()
|
||||
|
||||
print("=" * 70)
|
||||
print("FLIGHT STRINGS PER AIRCRAFT")
|
||||
print("=" * 70)
|
||||
|
||||
for aircraft, group in sched.groupby("aircraft"):
|
||||
print(f"\n{'-'*70}")
|
||||
print(f" Aeronave: {aircraft}")
|
||||
print(f"{'-'*70}")
|
||||
|
||||
total_fh = 0.0
|
||||
for _, row in group.sort_values("departure").iterrows():
|
||||
maint_flag = " <- MANUTENCAO ANTES" if row.get("maintenance_before") else ""
|
||||
dep = pd.to_datetime(row["departure"])
|
||||
arr = pd.to_datetime(row["arrival"])
|
||||
fh = float(row.get("flight_hours", 0))
|
||||
total_fh += fh
|
||||
print(
|
||||
f" {row['ofrag_id']:12s} "
|
||||
f"{dep.strftime('%d/%m %H:%M')} -> {arr.strftime('%d/%m %H:%M')} "
|
||||
f"({fh:.2f} FH){maint_flag}"
|
||||
)
|
||||
|
||||
print(f" {'TOTAL':12s} {total_fh:.2f} FH")
|
||||
|
||||
if not maint.empty:
|
||||
ac_maint = maint[maint["aircraft"] == aircraft]
|
||||
if not ac_maint.empty:
|
||||
print(f"\n Eventos de manutencao:")
|
||||
for _, ev in ac_maint.iterrows():
|
||||
print(
|
||||
f" CHECK (ciclo {int(ev['check_cycle_index'])}) "
|
||||
f"{pd.to_datetime(ev['maint_start']).strftime('%d/%m %H:%M')} -> "
|
||||
f"{pd.to_datetime(ev['maint_end']).strftime('%d/%m %H:%M')} "
|
||||
f"TTM perdido: {ev['ttm_loss_hours']:.2f} FH"
|
||||
)
|
||||
|
||||
# Export to Excel
|
||||
out_xlsx = cfg.exports_dir / "flight_strings.xlsx"
|
||||
try:
|
||||
with pd.ExcelWriter(out_xlsx, engine="openpyxl") as writer:
|
||||
sched.to_excel(writer, sheet_name="Escala", index=False)
|
||||
if not maint.empty:
|
||||
maint.to_excel(writer, sheet_name="Manutencao", index=False)
|
||||
print(f"\n\nExportado para Excel -> {out_xlsx}")
|
||||
except ImportError:
|
||||
print("\n(openpyxl not installed – Excel export skipped)")
|
||||
5
scripts/05_run_dashboard.bat
Normal file
5
scripts/05_run_dashboard.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Iniciando dashboard OARMP...
|
||||
cd /d "%~dp0.."
|
||||
.venv\Scripts\streamlit.exe run app\dashboard.py --server.port 8501
|
||||
pause
|
||||
Reference in New Issue
Block a user