"""Shared test fixtures. Generates synthetic MME test data that mimics a realistic frontal crash test. """ from __future__ import annotations from pathlib import Path import numpy as np import pytest from impakt.channel.code import ChannelCode from impakt.channel.model import ( Channel, ChannelGroup, DummyInfo, ImpactConfig, TestData, TestMetadata, VehicleInfo, ) @pytest.fixture def sample_rate() -> float: return 20000.0 @pytest.fixture def duration() -> float: return 0.2 # 200 ms @pytest.fixture def time_array(sample_rate: float, duration: float) -> np.ndarray: """Time array: -0.01s to 0.19s (10 ms pre-trigger + 190 ms event).""" n = int(duration * sample_rate) pre = int(0.01 * sample_rate) return np.arange(n, dtype=np.float64) / sample_rate - pre / sample_rate @pytest.fixture def head_accel_x(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic head X acceleration: half-sine pulse peaking at ~40g.""" t = time_array data = np.zeros_like(t) mask = (t >= 0) & (t <= 0.1) data[mask] = 40.0 * np.sin(np.pi * t[mask] / 0.1) # Add some noise data += np.random.default_rng(42).normal(0, 0.5, len(data)) return Channel( name="11HEAD0000ACXA", code=ChannelCode.parse("11HEAD0000ACXA"), data=data, time=t, unit="g", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def head_accel_y(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic head Y acceleration: smaller lateral component.""" t = time_array data = np.zeros_like(t) mask = (t >= 0) & (t <= 0.1) data[mask] = 8.0 * np.sin(np.pi * t[mask] / 0.1) * np.cos(3 * np.pi * t[mask] / 0.1) data += np.random.default_rng(43).normal(0, 0.3, len(data)) return Channel( name="11HEAD0000ACYA", code=ChannelCode.parse("11HEAD0000ACYA"), data=data, time=t, unit="g", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def head_accel_z(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic head Z acceleration.""" t = time_array data = np.zeros_like(t) mask = (t >= 0) & (t <= 0.1) data[mask] = 15.0 * np.sin(np.pi * t[mask] / 0.1) data += np.random.default_rng(44).normal(0, 0.3, len(data)) return Channel( name="11HEAD0000ACZA", code=ChannelCode.parse("11HEAD0000ACZA"), data=data, time=t, unit="g", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def head_group(head_accel_x: Channel, head_accel_y: Channel, head_accel_z: Channel) -> ChannelGroup: return ChannelGroup( key="11HEAD0000AC_A", x=head_accel_x, y=head_accel_y, z=head_accel_z, ) @pytest.fixture def chest_deflection_channel(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic chest deflection: ramps to 35mm peak.""" t = time_array data = np.zeros_like(t) mask = (t >= 0.01) & (t <= 0.08) t_event = t[mask] - 0.01 data[mask] = 35.0 * np.sin(np.pi * t_event / 0.07) return Channel( name="11CHST0000DCXA", code=ChannelCode.parse("11CHST0000DCXA"), data=data, time=t, unit="mm", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def neck_fz_channel(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic neck Fz: tension pulse peaking at ~3000N.""" t = time_array data = np.zeros_like(t) mask = (t >= 0.005) & (t <= 0.06) t_event = t[mask] - 0.005 data[mask] = 3000.0 * np.sin(np.pi * t_event / 0.055) return Channel( name="11NECKUP00FOZA", code=ChannelCode.parse("11NECKUP00FOZA"), data=data, time=t, unit="N", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def neck_my_channel(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic neck My: flexion moment peaking at ~80 Nm.""" t = time_array data = np.zeros_like(t) mask = (t >= 0.01) & (t <= 0.07) t_event = t[mask] - 0.01 data[mask] = 80.0 * np.sin(np.pi * t_event / 0.06) return Channel( name="11NECKUP00MOYA", code=ChannelCode.parse("11NECKUP00MOYA"), data=data, time=t, unit="N·m", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def femur_left_channel(time_array: np.ndarray, sample_rate: float) -> Channel: """Synthetic left femur load: compressive pulse peaking at ~4500N.""" t = time_array data = np.zeros_like(t) mask = (t >= 0.02) & (t <= 0.09) t_event = t[mask] - 0.02 data[mask] = -4500.0 * np.sin(np.pi * t_event / 0.07) return Channel( name="11FEMRLE00FOZA", code=ChannelCode.parse("11FEMRLE00FOZA"), data=data, time=t, unit="N", sample_rate=sample_rate, source_test_id="TEST_001", ) @pytest.fixture def sample_metadata() -> TestMetadata: return TestMetadata( test_number="TEST_001", vehicle=VehicleInfo(make="Toyota", model="Camry", year=2024, mass_kg=1523.0), dummy=DummyInfo(dummy_type="H3-50M", position="Driver", mass_kg=78.0), impact=ImpactConfig( test_type="Full Frontal", speed_kmh=56.3, barrier_type="Rigid", overlap_percent=100.0, ), ) @pytest.fixture def sample_test_data( sample_metadata: TestMetadata, head_accel_x: Channel, head_accel_y: Channel, head_accel_z: Channel, chest_deflection_channel: Channel, neck_fz_channel: Channel, neck_my_channel: Channel, femur_left_channel: Channel, ) -> TestData: """Full synthetic test data with multiple channels.""" channels = { head_accel_x.name: head_accel_x, head_accel_y.name: head_accel_y, head_accel_z.name: head_accel_z, chest_deflection_channel.name: chest_deflection_channel, neck_fz_channel.name: neck_fz_channel, neck_my_channel.name: neck_my_channel, femur_left_channel.name: femur_left_channel, } return TestData( test_id="TEST_001", metadata=sample_metadata, channels=channels, ) @pytest.fixture def sample_mme_dir(tmp_path: Path, head_accel_x: Channel) -> Path: """Create a minimal MME directory structure for reader tests.""" test_dir = tmp_path / "test_001" test_dir.mkdir() # MME.ini ini = test_dir / "MME.ini" ini.write_text( "[Test]\n" "test_number = TEST_001\n" "test_type = Full Frontal\n" "test_speed = 56.3\n" "\n" "[Vehicle]\n" "vehicle_make = Toyota\n" "vehicle_model = Camry\n" "vehicle_year = 2024\n" "\n" "[Dummy]\n" "dummy_type = H3-50M\n" "dummy_position = Driver\n", encoding="utf-8", ) # Channel directory ch_dir = test_dir / "channels" ch_dir.mkdir() # .chn file chn = ch_dir / "11HEAD0000ACXA.chn" chn.write_text( "[Channel]\n" "channel_code = 11HEAD0000ACXA\n" "unit = g\n" "sample_rate = 20000\n" "num_samples = 4000\n" "pre_trigger = 200\n" "cfc = 1000\n" "data_format = ascii\n", encoding="utf-8", ) # .dat file (ASCII, one value per line) dat = ch_dir / "11HEAD0000ACXA.dat" np.savetxt(str(dat), head_accel_x.data, fmt="%.6f") return test_dir