from __future__ import annotations import pytest from boxsim import model from boxsim.model import COLS, LINK_BASE, ROWS, TOTAL_SLOTS, Orientation, PartType def test_slot_partition_is_exact(): """The three slot ranges must tile 1..375 with no gap or overlap.""" seen = {} for slot in range(1, TOTAL_SLOTS + 1): orientation, row, col = model.slot_geometry(slot) if orientation == Orientation.LINK: continue key = (orientation, row, col) assert key not in seen, f"slot {slot} collides with {seen[key]}" seen[key] = slot horizontal = sum(1 for k in seen if k[0] == Orientation.HORIZONTAL) vertical = sum(1 for k in seen if k[0] == Orientation.VERTICAL) assert horizontal == (COLS - 1) * ROWS == 180 assert vertical == COLS * (ROWS - 1) == 187 assert LINK_BASE == 368 and TOTAL_SLOTS == 375 def test_slot_geometry_bounds(): with pytest.raises(model.ModelError): model.slot_geometry(0) with pytest.raises(model.ModelError): model.slot_geometry(TOTAL_SLOTS + 1) def test_slot_endpoints_are_adjacent_nodes(): orientation, row, col = model.slot_geometry(1) assert (orientation, row, col) == (Orientation.HORIZONTAL, 0, 0) orientation, row, col = model.slot_geometry(181) assert (orientation, row, col) == (Orientation.VERTICAL, 0, 0) # The last vertical slot must sit on the bottom-but-one row. orientation, row, col = model.slot_geometry(367) assert (orientation, row, col) == (Orientation.VERTICAL, ROWS - 2, COLS - 1) def test_default_project_shape(default_project): p = default_project assert p.version == 2000 assert p.amp_count == 1 assert p.chassis_count == 3 assert [c.name for c in p.chassis] == [ "GF 200 - 8 Ohm", "B 100 - 6 Ohm", "G 20 SC - 8 Ohm", ] def test_default_netlist_is_closed(default_project): """The published Default project must have no floating terminal.""" xo = default_project.crossovers[0] netlist = xo.build_netlist() assert len(xo.components) == 22 assert netlist.net_count == 14 assert netlist.dangling() == [] def test_drivers_and_amp_present(default_project): xo = default_project.crossovers[0] drivers = [p for p in xo.components if p.typ == PartType.DRIVER] amps = [p for p in xo.components if p.typ == PartType.AMP_INPUT] assert len(amps) == 1 assert sorted(d.chassis_index for d in drivers) == [0, 1, 2] def test_bounding_box(default_project): assert default_project.crossovers[0].bounding_box() == (0, 0, 8, 9) def test_crossover_corner_frequencies(default_project): """Sanity-check the reconstruction against loudspeaker physics. A wrong slot-to-node mapping would still produce a netlist, but not one whose series/shunt pairs land on sensible crossover points. """ import math xo = default_project.crossovers[0] by_slot = {p.slot: p for p in xo.components} # Woofer low-pass: 4.7 mH series with the 15 uF shunt. fc = 1 / (2 * math.pi * math.sqrt(by_slot[5].value * by_slot[198].value)) assert 550 < fc < 650 # Tweeter high-pass: two series caps into the 0.22 mH shunt. cser = 1 / (1 / by_slot[65].value + 1 / by_slot[67].value) fc = 1 / (2 * math.pi * math.sqrt(by_slot[252].value * cser)) assert 5000 < fc < 6500