92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
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
|