|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import logging |
| 4 | + |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | from openlifu.plan.param_constraint import ParameterConstraint |
6 | | -from openlifu.plan.solution_analysis import SolutionAnalysis, SolutionAnalysisOptions |
| 8 | +from openlifu.plan.solution_analysis import ( |
| 9 | + SolutionAnalysis, |
| 10 | + SolutionAnalysisOptions, |
| 11 | + model_tx_temperature_rise, |
| 12 | +) |
7 | 13 |
|
8 | 14 | # ---- Tests for SolutionAnalysis ---- |
9 | 15 |
|
@@ -80,3 +86,148 @@ def test_to_dict_from_dict_solution_analysis_options(example_solution_analysis_o |
80 | 86 | options_dict = example_solution_analysis_options.to_dict() |
81 | 87 | new_options = SolutionAnalysisOptions.from_dict(options_dict) |
82 | 88 | assert new_options == example_solution_analysis_options |
| 89 | + |
| 90 | + |
| 91 | +# ---- Tests for model_tx_temperature_rise ---- |
| 92 | + |
| 93 | +# Valid mid-range parameters used as a baseline throughout the tests. |
| 94 | +# P = voltage^2 * duty_cycle = 14^2 * 1.0 = 196 V^2 (valid range: 50-500) |
| 95 | +_BASE_VOLTAGE = 14.0 # V |
| 96 | +_LOW_VOLTAGE = 8.0 # V → P = 64 V^2 (near low end of valid range) |
| 97 | +_HIGH_VOLTAGE = 21.0 # V → P = 441 V^2 (near high end of valid range) |
| 98 | +_BASE_T0 = 30.0 # °C (mid-range of valid 20-40 °C) |
| 99 | +_BASE_FREQ = 400.0 # kHz (centre of valid 380-420 kHz) |
| 100 | + |
| 101 | + |
| 102 | +def test_low_voltage_less_heating_than_high_voltage(): |
| 103 | + """Higher voltage (more power) should produce a greater temperature rise.""" |
| 104 | + t = 120.0 # mid-range time, well within valid 1-600 s |
| 105 | + rise_low = model_tx_temperature_rise(_LOW_VOLTAGE, t, T0_degC=_BASE_T0, frequency_kHz=_BASE_FREQ) |
| 106 | + rise_high = model_tx_temperature_rise(_HIGH_VOLTAGE, t, T0_degC=_BASE_T0, frequency_kHz=_BASE_FREQ) |
| 107 | + assert rise_low < rise_high, ( |
| 108 | + f"Expected lower voltage ({_LOW_VOLTAGE} V, rise={rise_low:.4f} °C) to produce " |
| 109 | + f"less heating than higher voltage ({_HIGH_VOLTAGE} V, rise={rise_high:.4f} °C)" |
| 110 | + ) |
| 111 | + |
| 112 | + |
| 113 | +def test_little_heating_right_after_start(): |
| 114 | + """Temperature rise at t=1 s (just started) should be much less than at t=300 s.""" |
| 115 | + rise_early = model_tx_temperature_rise(_BASE_VOLTAGE, t_sec=1.0, T0_degC=_BASE_T0) |
| 116 | + rise_later = model_tx_temperature_rise(_BASE_VOLTAGE, t_sec=300.0, T0_degC=_BASE_T0) |
| 117 | + assert rise_early < rise_later, ( |
| 118 | + f"Expected rise at t=1 s ({rise_early:.4f} °C) to be less than rise at t=300 s ({rise_later:.4f} °C)" |
| 119 | + ) |
| 120 | + # Additionally confirm the early rise is small in absolute terms |
| 121 | + assert rise_early < 5.0, ( |
| 122 | + f"Expected temperature rise at t=1 s to be < 5 °C, got {rise_early:.4f} °C" |
| 123 | + ) |
| 124 | + |
| 125 | + |
| 126 | +def test_temperature_rises_monotonically_with_time(): |
| 127 | + """Temperature rise must be strictly increasing across a span of time points.""" |
| 128 | + times = [1.0, 10.0, 60.0, 180.0, 360.0, 600.0] |
| 129 | + rises = [model_tx_temperature_rise(_BASE_VOLTAGE, t, T0_degC=_BASE_T0) for t in times] |
| 130 | + for i in range(len(rises) - 1): |
| 131 | + assert rises[i] < rises[i + 1], ( |
| 132 | + f"Temperature rise not monotonically increasing: " |
| 133 | + f"rise[{times[i]} s]={rises[i]:.4f} >= rise[{times[i+1]} s]={rises[i+1]:.4f}" |
| 134 | + ) |
| 135 | + |
| 136 | + |
| 137 | +def test_temperature_rises_monotonically_with_voltage(): |
| 138 | + """Temperature rise must increase with voltage (at fixed time and other params).""" |
| 139 | + # Voltages chosen so that P = V^2 stays within the valid 50-500 V^2 range |
| 140 | + voltages = [8.0, 11.0, 14.0, 17.0, 21.0] |
| 141 | + t = 120.0 |
| 142 | + rises = [model_tx_temperature_rise(v, t, T0_degC=_BASE_T0) for v in voltages] |
| 143 | + for i in range(len(rises) - 1): |
| 144 | + assert rises[i] < rises[i + 1], ( |
| 145 | + f"Temperature rise not monotonically increasing with voltage: " |
| 146 | + f"rise[{voltages[i]} V]={rises[i]:.4f} >= rise[{voltages[i+1]} V]={rises[i+1]:.4f}" |
| 147 | + ) |
| 148 | + |
| 149 | + |
| 150 | +def test_lower_duty_cycle_produces_less_heating(): |
| 151 | + """Reducing duty cycle reduces effective power and therefore temperature rise.""" |
| 152 | + t = 120.0 |
| 153 | + voltage = _BASE_VOLTAGE |
| 154 | + rise_full = model_tx_temperature_rise(voltage, t, duty_cycle=1.0, T0_degC=_BASE_T0) |
| 155 | + rise_half = model_tx_temperature_rise(voltage, t, duty_cycle=0.5, T0_degC=_BASE_T0) |
| 156 | + assert rise_half < rise_full, ( |
| 157 | + f"Expected 50 % duty cycle ({rise_half:.4f} °C) to produce less heating " |
| 158 | + f"than 100 % duty cycle ({rise_full:.4f} °C)" |
| 159 | + ) |
| 160 | + |
| 161 | + |
| 162 | +def test_lower_apodization_produces_less_heating(): |
| 163 | + """Partial apodization reduces effective power and therefore temperature rise.""" |
| 164 | + t = 120.0 |
| 165 | + rise_full = model_tx_temperature_rise(_BASE_VOLTAGE, t, apodization_fraction=1.0, T0_degC=_BASE_T0) |
| 166 | + rise_half = model_tx_temperature_rise(_BASE_VOLTAGE, t, apodization_fraction=0.5, T0_degC=_BASE_T0) |
| 167 | + assert rise_half < rise_full, ( |
| 168 | + f"Expected apodization=0.5 ({rise_half:.4f} °C) to produce less heating " |
| 169 | + f"than apodization=1.0 ({rise_full:.4f} °C)" |
| 170 | + ) |
| 171 | + |
| 172 | + |
| 173 | +@pytest.mark.parametrize("bad_T0", [19.9, 40.1]) |
| 174 | +def test_warning_emitted_for_T0_out_of_range(bad_T0, caplog): |
| 175 | + """A warning must be logged when T0 is outside the valid 20-40 °C range.""" |
| 176 | + with caplog.at_level(logging.WARNING, logger="openlifu.plan.solution_analysis"): |
| 177 | + model_tx_temperature_rise(_BASE_VOLTAGE, t_sec=60.0, T0_degC=bad_T0) |
| 178 | + assert any("T0" in record.message or "temperature" in record.message.lower() |
| 179 | + for record in caplog.records), ( |
| 180 | + f"Expected a warning about T0 out of range for T0={bad_T0} °C" |
| 181 | + ) |
| 182 | + |
| 183 | + |
| 184 | +@pytest.mark.parametrize(("bad_voltage","bad_duty_cycle"), [ |
| 185 | + (6.0, 1.0), # P = 36 < 50 |
| 186 | + (25.0, 1.0), # P = 625 > 500 |
| 187 | +]) |
| 188 | +def test_warning_emitted_for_power_out_of_range(bad_voltage, bad_duty_cycle, caplog): |
| 189 | + """A warning must be logged when the squared-voltage power is outside 50-500 V^2.""" |
| 190 | + with caplog.at_level(logging.WARNING, logger="openlifu.plan.solution_analysis"): |
| 191 | + model_tx_temperature_rise(bad_voltage, t_sec=60.0, duty_cycle=bad_duty_cycle, T0_degC=_BASE_T0) |
| 192 | + assert any("voltage" in record.message.lower() or "squared" in record.message.lower() or "v^2" in record.message.lower() |
| 193 | + for record in caplog.records), ( |
| 194 | + f"Expected a warning about power out of range for voltage={bad_voltage} V" |
| 195 | + ) |
| 196 | + |
| 197 | + |
| 198 | +@pytest.mark.parametrize("bad_time", [0.5, 601.0]) |
| 199 | +def test_warning_emitted_for_time_out_of_range(bad_time, caplog): |
| 200 | + """A warning must be logged when t is outside the valid 1-600 s range.""" |
| 201 | + with caplog.at_level(logging.WARNING, logger="openlifu.plan.solution_analysis"): |
| 202 | + model_tx_temperature_rise(_BASE_VOLTAGE, t_sec=bad_time, T0_degC=_BASE_T0) |
| 203 | + assert any("time" in record.message.lower() or "seconds" in record.message.lower() |
| 204 | + for record in caplog.records), ( |
| 205 | + f"Expected a warning about time out of range for t={bad_time} s" |
| 206 | + ) |
| 207 | + |
| 208 | + |
| 209 | +@pytest.mark.parametrize("bad_freq", [379.9, 420.1]) |
| 210 | +def test_warning_emitted_for_frequency_out_of_range(bad_freq, caplog): |
| 211 | + """A warning must be logged when frequency is outside the valid 380-420 kHz range.""" |
| 212 | + with caplog.at_level(logging.WARNING, logger="openlifu.plan.solution_analysis"): |
| 213 | + model_tx_temperature_rise(_BASE_VOLTAGE, t_sec=60.0, frequency_kHz=bad_freq, T0_degC=_BASE_T0) |
| 214 | + assert any("frequency" in record.message.lower() or "khz" in record.message.lower() |
| 215 | + for record in caplog.records), ( |
| 216 | + f"Expected a warning about frequency out of range for freq={bad_freq} kHz" |
| 217 | + ) |
| 218 | + |
| 219 | + |
| 220 | +def test_no_warnings_for_valid_inputs(caplog): |
| 221 | + """No warnings should be emitted when all inputs are within their valid ranges.""" |
| 222 | + with caplog.at_level(logging.WARNING, logger="openlifu.plan.solution_analysis"): |
| 223 | + model_tx_temperature_rise( |
| 224 | + voltage=_BASE_VOLTAGE, |
| 225 | + t_sec=60.0, |
| 226 | + duty_cycle=1.0, |
| 227 | + apodization_fraction=1.0, |
| 228 | + frequency_kHz=_BASE_FREQ, |
| 229 | + T0_degC=_BASE_T0, |
| 230 | + ) |
| 231 | + assert len(caplog.records) == 0, ( |
| 232 | + f"Unexpected warnings for valid inputs: {[r.message for r in caplog.records]}" |
| 233 | + ) |
0 commit comments