motor-renders / FEMM /test_dense_waveforms.py
Lislaam's picture
Archive FEMM evidence snapshot 2026-11-02 (32/60)
706fd3a verified
Raw History Blame Contribute Delete
8.2 kB
"""Independent mathematical and geometry guards for the overnight study."""
import numpy as np
import pytest
from ans_delta import decoded_chunks, encode
from dense_waveforms import completed, same_solver_inputs, spectral_derivative
from pipeline.femm_utils.build_model import simplify_collinear_segments
@pytest.mark.parametrize("count", [96, 192, 384, 768, 1536])
def test_linkage_derivative_matches_analytic_periodic_signal(count):
period = 2 * np.pi / 7
angle = (np.arange(count) + 0.23) * period / count
values = np.column_stack(
[np.sin(7 * angle), np.cos(21 * angle), 0.1 * np.sin(84 * angle)]
)
expected = np.column_stack(
[7 * np.cos(7 * angle), -21 * np.sin(21 * angle), 8.4 * np.cos(84 * angle)]
)
np.testing.assert_allclose(
spectral_derivative(values, period), expected, atol=2e-10, rtol=1e-10
)
@pytest.mark.parametrize("junction", [False, True])
def test_compact_femm_keeps_exact_straight_geometry_and_junctions(junction):
nodes = np.array(
[[0.0, 0.0], [0.5, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]]
)
segments = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] + ([(1, 5)] if junction else [])
source = {
"nodes_mm": nodes,
"segments": segments,
"used_nodes": list(range(len(nodes))),
"boundary_edges": {"bore": [], "outer": []},
}
result = simplify_collinear_segments(source)
if junction:
assert (0, 1) in result["segments"] and (1, 2) in result["segments"]
else:
assert (0, 2) in result["segments"] and result[
"collinear_vertices_removed"
] == 1
length = lambda edges: sum(np.linalg.norm(nodes[a] - nodes[b]) for a, b in edges)
assert length(result["segments"]) == pytest.approx(length(segments), abs=1e-14)
assert source["segments"] == segments
def test_prescribed_boundary_vertices_are_never_removed():
source = {
"nodes_mm": np.array([[0.0, 0.0], [0.5, 0.0], [1.0, 0.0]]),
"segments": [(0, 1), (1, 2)],
"used_nodes": [0, 1, 2],
"boundary_edges": {"bore": [(0, 1), (1, 2)], "outer": []},
}
assert simplify_collinear_segments(source)["segments"] == source["segments"]
@pytest.mark.parametrize(
"before,after",
[
(b"", b""),
(b"x\ny\n", b"x\nz\n"),
(b"abc", b"abcd\nmore"),
(b"a\nb\nc\n", b"a\n"),
(b"", b"one\ntwo\n"),
(b"same\n", b"same\n"),
],
)
def test_answer_archive_reconstructs_exact_bytes(before, after):
from io import BytesIO
delta = BytesIO()
encode(BytesIO(before), BytesIO(after), delta)
delta.seek(0)
assert b"".join(decoded_chunks(BytesIO(before), delta)) == after
def test_femm_intermediate_readback_is_not_a_completed_dense_sample(tmp_path):
import json
directory = tmp_path / "tasks/zero-0000"
directory.mkdir(parents=True)
(directory / "result.json").write_text(
json.dumps(
{
"status": "accepted",
"execution_sha256": "execution",
"request_sha256": "request",
"outer_rotor_torque_nm": 1.0,
}
)
)
assert (
completed(
tmp_path, {"id": "zero-0000", "request_sha256": "request"}, "execution"
)
is None
)
def test_archiver_waits_for_final_driver_publication(tmp_path):
import json
from ans_delta import convert
state = tmp_path / "femm/tasks/loaded-0000"
state.mkdir(parents=True)
(state / "result.json").write_text(json.dumps({"status": "accepted"}))
(state / "base.ans.gz").write_bytes(b"not yet a finished gzip")
assert not convert(tmp_path, state)
assert (state / "base.ans.gz").exists()
def test_completed_data_renders_all_requested_comparison_charts(tmp_path):
import json
from dense_waveform_report import write_report
from dense_waveforms import task
count = 96
period = 2 * np.pi / 7
angles = (np.arange(count) + 0.5) * period / count
patterns = np.array([[1.0, -1.0, 0.0]] * count)
plan = {
"identity": "test-plan",
"max_angles": count,
"target_angles": count,
"angular_levels": [count],
"electrical_period_mechanical_rad": period,
"angles_mechanical_rad": angles.tolist(),
"currents_abc_a": patterns.tolist(),
"derivative_step_rad": period * 1e-4,
"n_theta": 1344,
"harmonics": 630,
"boundary_subdivisions": 64,
"max_area_mm2": 0.0125,
"femm_mesh_size_mm": 0.125,
"femm_precision": 1e-10,
"targets": {
"kt_relative": 0.0025,
"ke_relative": 0.0025,
"torque_extrema_absolute_nm": 0.0001,
"derivative_relative": 0.0025,
},
}
(tmp_path / "plan.json").write_text(json.dumps(plan))
for solver in ("jax", "femm"):
directory = tmp_path / solver
directory.mkdir()
(directory / "execution.json").write_text(
json.dumps({"identity": "test-execution"})
)
(directory / "status.json").write_text(
json.dumps(
{
"phase": "complete",
"current_grid": count,
"accepted_tasks": 2 * count,
}
)
)
for index, angle in enumerate(angles):
for mode in ("zero", "loaded"):
request = task(plan, mode, index)
target = directory / "tasks" / request["id"]
target.mkdir(parents=True)
data = {
**request,
"execution_sha256": "test-execution",
"status": "accepted",
"torque_nm": float(
0.002 * np.sin(84 * angle) - (0.015 if mode == "loaded" else 0)
),
"phase_linkage_wb_turn": [
float(0.001 * np.cos(7 * angle + phase))
for phase in (0, 2 * np.pi / 3, 4 * np.pi / 3)
],
"saved_artifacts": {},
}
(target / "result.json").write_text(json.dumps(data))
summary = write_report(tmp_path)
page = (tmp_path / "report.html").read_text()
for label in (
"NEW JAX and FEMM scalar values",
"NEW FEMM and JAX sample overlays",
"NEW back-EMF waveform comparison",
"Loaded torque",
"Zero-current torque",
):
assert label in page
with np.load(tmp_path / "waveforms.npz") as arrays:
assert arrays["jax/torque"].shape == (2, count)
assert arrays["femm/phase_ke"].shape == (count, 3)
assert summary["complete"]
assert not summary["resolution_checks_passed"]
def test_checkpoint_compatibility_separates_solver_physics_from_sampling_policy():
from copy import deepcopy
original = {
"reference_sha256": "reference",
"numerical_source_sha256": "source",
"n_theta": 1344,
"boundary_subdivisions": 64,
"harmonics": 630,
"max_area_mm2": 0.0125,
"controls": {"tolerance": 1e-9},
"angles_mechanical_rad": [0.1, 0.2],
"currents_abc_a": [[1, -1, 0], [1, 0, -1]],
"derivative_step_rad": 1e-4,
"femm_mesh_size_mm": 0.125,
"femm_precision": 1e-10,
"target_angles": 384,
}
changed = deepcopy(original)
changed["target_angles"] = 768
assert same_solver_inputs(original, changed, "jax") and same_solver_inputs(
original, changed, "femm"
)
changed["femm_mesh_size_mm"] = 0.0625
assert same_solver_inputs(original, changed, "jax") and not same_solver_inputs(
original, changed, "femm"
)
changed = deepcopy(original)
changed["max_area_mm2"] /= 2
assert not same_solver_inputs(original, changed, "jax") and same_solver_inputs(
original, changed, "femm"
)
changed = deepcopy(original)
changed["numerical_source_sha256"] = "different physics"
assert not same_solver_inputs(original, changed, "jax") and not same_solver_inputs(
original, changed, "femm"
)