4da624bfcf
Route building / optimization (tatami_masterplan.py): - fast_total_time was permutation-invariant: it applied get_courses to group *values* instead of slots and ignored the permutation, so every ordering scored identically and the annealing optimized nothing. It now maps each rotation slot to its assigned group via the permutation. - Replaced the broken next_permutation/simulated_annealing (enumerated n! orderings per iteration, fed unnormalized Boltzmann weights to np.random.choice -> ValueError, and returned the last random sample) with a standard neighbor-swap annealer that tracks and returns the best solution and handles <2 slots. - Convert the reduced Timedelta matrix to float seconds before annealing (np.exp can't operate on Timedelta). Group building (tatami_masterplan.py): - assign_courses set each group's hosts (sorted by course) before all courses were assigned, so hosts whose course was still None got mis-ordered. Assign all courses first, then wire up hosts. - get_masterplan no longer mutates the caller's participant list. classes.py: - Narrow casts on distance-matrix lookups to satisfy the mypy gate (pre-existing failures). Tests: - Add pytest suite (74 tests) covering the rotation topology, route cost and optimization, the domain model, the masterplan pipeline, and the Routes API wrapper (HTTP mocked). The cost cross-check caught the assign_courses ordering bug above.
68 lines
1.9 KiB
Python
68 lines
1.9 KiB
Python
"""Shared pytest fixtures and test setup for the tatami package.
|
|
|
|
``tatami.traveltimes`` raises at import time if ``GOOGLE_MAPS_API_KEY`` is unset,
|
|
so we set a dummy value here (conftest is imported before any test module, and
|
|
therefore before the package is imported) to make the modules importable without
|
|
a real API key. Tests never hit the live API; the HTTP layer is mocked.
|
|
"""
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("GOOGLE_MAPS_API_KEY", "test-key")
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from tatami.classes import Participant
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _seed_rng():
|
|
"""Make the randomized steps (shuffles, annealing) deterministic per test."""
|
|
np.random.seed(1234)
|
|
|
|
|
|
def make_participants(
|
|
n: int, kitchen_sizes: list[float] | None = None
|
|
) -> list[Participant]:
|
|
"""Create ``n`` participants with distinct addresses and uuids."""
|
|
if kitchen_sizes is None:
|
|
kitchen_sizes = [10.0] * n
|
|
return [
|
|
Participant(
|
|
name=f"P{i}",
|
|
address=f"address {i}",
|
|
phone=f"phone {i}",
|
|
kitchen_size=kitchen_sizes[i],
|
|
allergies="",
|
|
)
|
|
for i in range(n)
|
|
]
|
|
|
|
|
|
def make_timedelta_matrix(
|
|
participants: list[Participant], seconds: np.ndarray
|
|
) -> pd.DataFrame:
|
|
"""Build a uuid-indexed square matrix of ``pd.Timedelta`` from a seconds array.
|
|
|
|
Mirrors the shape produced by ``get_participant_distance_matrix``: object dtype
|
|
cells holding ``pd.Timedelta`` values, indexed and columned by participant uuid.
|
|
"""
|
|
uuids = [p.uuid for p in participants]
|
|
matrix = pd.DataFrame(index=pd.Index(uuids), columns=pd.Index(uuids), dtype=object)
|
|
for i, u in enumerate(uuids):
|
|
for j, v in enumerate(uuids):
|
|
matrix.at[u, v] = pd.to_timedelta(f"{int(seconds[i][j])}s")
|
|
return matrix
|
|
|
|
|
|
@pytest.fixture
|
|
def participants_factory():
|
|
return make_participants
|
|
|
|
|
|
@pytest.fixture
|
|
def matrix_factory():
|
|
return make_timedelta_matrix
|