Skip to content

Commit f482bd6

Browse files
Add temperature estimation #422
add temperature estimation make estimation function independently importable
1 parent 8204714 commit f482bd6

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

src/openlifu/plan/solution.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import base64
44
import json
5+
import logging
56
import tempfile
67
from dataclasses import asdict, dataclass, field
78
from datetime import datetime
@@ -21,12 +22,14 @@
2122
find_centroid,
2223
get_beamwidth,
2324
get_mask,
25+
model_tx_temperature_rise,
2426
)
2527
from openlifu.util.annotations import OpenLIFUFieldData
2628
from openlifu.util.json import PYFUSEncoder
2729
from openlifu.util.units import getunitconversion, rescale_coords, rescale_data_arr
2830
from openlifu.xdc import Transducer
2931

32+
logger = logging.getLogger(__name__)
3033

3134
def _construct_nc_filepath_from_json_filepath(json_filepath:Path) -> Path:
3235
"""Construct a default filepath to netCDF file given filepath to associated solution json file."""
@@ -276,10 +279,45 @@ def analyze(self,
276279
solution_analysis.TIC = np.mean(TIC)
277280
solution_analysis.voltage_V = self.voltage
278281
solution_analysis.power_W = np.mean(power_W)
279-
282+
solution_analysis.estimated_tx_temperature_rise_C = self.estimate_tx_temperature_rise(
283+
t_sec=solution_analysis.sequence_duration_s,
284+
)
280285
solution_analysis.param_constraints = param_constraints
281286
return solution_analysis
282287

288+
def estimate_tx_temperature_rise(self,
289+
t_sec: float,
290+
T0_degC: float = 30.0):
291+
292+
"""
293+
Standalone temperature prediction function for thermal modeling.
294+
295+
Based on physics-based power decay gradient model fitted from experimental data.
296+
Model: dT/dt = (t + t_shift)^(-n) + C
297+
298+
Parameters:
299+
-----------
300+
self: Solution
301+
The treatment solution containing voltage and pulse information.
302+
t_sec: float
303+
Time in seconds for which to predict temperature rise.
304+
T0_degC: float
305+
Initial temperature in Celsius. Default is 30.0°C.
306+
307+
Returns:
308+
--------
309+
float
310+
Predicted temperature rise in Celsius
311+
"""
312+
return model_tx_temperature_rise(
313+
voltage=self.voltage,
314+
t_sec=t_sec,
315+
duty_cycle=self.get_sequence_dutycycle(),
316+
apodization_fraction=np.mean(self.apodizations),
317+
frequency_kHz=self.pulse.frequency / 1e3,
318+
T0_degC=T0_degC,
319+
)
320+
283321
def compute_scaling_factors(
284322
self,
285323
focal_pattern: FocalPattern,

src/openlifu/plan/solution_analysis.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import logging
45
from dataclasses import dataclass, field
56
from typing import Annotated, Any, Dict, Tuple, Type
67

@@ -13,6 +14,8 @@
1314
from openlifu.util.dict_conversion import DictMixin
1415
from openlifu.util.units import getunitconversion, getunittype
1516

17+
logger = logging.getLogger(__name__)
18+
1619
DEFAULT_ORIGIN = np.zeros(3)
1720

1821
PARAM_FORMATS = {
@@ -45,7 +48,8 @@
4548
"power_W": [None, "0.2f", "W", "Emitted Power"],
4649
"duty_cycle_pulse_train_pct": [None, "0.1f", "%", "Pulse Train Duty Cycle"],
4750
"duty_cycle_sequence_pct": [None, "0.1f", "%", "Sequence Duty Cycle"],
48-
"sequence_duration_s": [None, "0.0f", "s", "Sequence Duration"],}
51+
"sequence_duration_s": [None, "0.0f", "s", "Sequence Duration"],
52+
"estimated_tx_temperature_rise_C": [None, "0.2f", "°C", "Estimated TX Temperature Rise"]}
4953

5054
@dataclass
5155
class SolutionAnalysis(DictMixin):
@@ -139,6 +143,8 @@ class SolutionAnalysis(DictMixin):
139143
sequence_duration_s: Annotated[float | None, OpenLIFUFieldData("Sequence duration (s)", "Total duration of the sequence (s)")] = None
140144
"""Total duration of the sequence (s)"""
141145

146+
estimated_tx_temperature_rise_C: Annotated[float | None, OpenLIFUFieldData("Estimated TX temperature rise (°C)", "Estimated temperature rise (assume 30°C baseline)")] = None
147+
"""Estimated temperature rise in Celsius (assume 30°C baseline)"""
142148

143149
param_constraints: Annotated[Dict[str, ParameterConstraint], OpenLIFUFieldData("Parameter constraints", None)] = field(default_factory=dict)
144150
"""TODO: Add description"""
@@ -572,3 +578,69 @@ def get_beamwidth(
572578
negoff, posoff = get_beam_bounds(da, focus=focus, dim=dim, cutoff=float(cutoff), origin=origin, min_offset=min_offset, max_offset=max_offset)
573579
bw = posoff - negoff
574580
return bw
581+
582+
def model_tx_temperature_rise(voltage: float,
583+
t_sec: float,
584+
duty_cycle: float=1.0,
585+
apodization_fraction: float=1.0,
586+
frequency_kHz: float=400.0,
587+
T0_degC: float = 30.0):
588+
"""
589+
Temperature prediction function for thermal modeling.
590+
591+
Based on physics-based power decay gradient model fitted from experimental data.
592+
Model: dT/dt = (t + t_shift)^(-n) + C
593+
594+
Parameters:
595+
-----------
596+
voltage: float
597+
Voltage to use when running sonication.
598+
duty_cycle: float
599+
Duty cycle of the sonication.
600+
apodization_fraction: float
601+
Fraction of apodization applied.
602+
frequency_kHz: float
603+
Frequency in kHz.
604+
t_sec: float
605+
Time in seconds for which to predict temperature rise.
606+
T0_degC: float
607+
Initial temperature in Celsius. Default is 30.0°C.
608+
609+
Returns:
610+
--------
611+
float
612+
Predicted temperature rise in Celsius
613+
"""
614+
P = voltage**2 * duty_cycle
615+
P = P * apodization_fraction
616+
t = t_sec
617+
T0 = T0_degC
618+
619+
if T0 < 20 or T0 > 40:
620+
logger.warning("Initial temperature T0 must be between 20 and 40 degrees Celsius for the model to be valid.")
621+
622+
if P < 50 or P > 500:
623+
logger.warning("Power P must be between 50 and 500 Watts for the model to be valid.")
624+
625+
if t < 1 or t > 600:
626+
logger.warning("Time t must be between 1 and 600 seconds for the model to be valid.")
627+
628+
if frequency_kHz < 380 or frequency_kHz > 420:
629+
logger.warning("Frequency must be between 380 and 420 kHz for the model to be valid.")
630+
631+
# Predict power law parameters using polynomial regression (degree 2)
632+
n = (2.131832 + -0.003475*P + -0.044916*T0 +
633+
0.000002*P*P + 0.000049*P*T0 + 0.000474*T0*T0)
634+
635+
t_shift = (1.074525 + 0.101532*P + -0.097741*T0 +
636+
-0.000080*P*P + -0.001891*P*T0 + 0.005975*T0*T0)
637+
638+
C = (0.051572 + -0.000072*P + -0.001668*T0 +
639+
-0.000000*P*P + 0.000004*P*T0 + 0.000007*T0*T0)
640+
641+
# Apply integrated power decay gradient model
642+
t_adj = t + t_shift
643+
integral_term = (t_adj**(1-n) - t_shift**(1-n)) / (1 - n)
644+
temperature_rise_C = integral_term + C * t
645+
646+
return temperature_rise_C

0 commit comments

Comments
 (0)