|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import json |
| 4 | +import logging |
4 | 5 | from dataclasses import dataclass, field |
5 | 6 | from typing import Annotated, Any, Dict, Tuple, Type |
6 | 7 |
|
|
13 | 14 | from openlifu.util.dict_conversion import DictMixin |
14 | 15 | from openlifu.util.units import getunitconversion, getunittype |
15 | 16 |
|
| 17 | +logger = logging.getLogger(__name__) |
| 18 | + |
16 | 19 | DEFAULT_ORIGIN = np.zeros(3) |
17 | 20 |
|
18 | 21 | PARAM_FORMATS = { |
|
45 | 48 | "power_W": [None, "0.2f", "W", "Emitted Power"], |
46 | 49 | "duty_cycle_pulse_train_pct": [None, "0.1f", "%", "Pulse Train Duty Cycle"], |
47 | 50 | "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"]} |
49 | 53 |
|
50 | 54 | @dataclass |
51 | 55 | class SolutionAnalysis(DictMixin): |
@@ -139,6 +143,8 @@ class SolutionAnalysis(DictMixin): |
139 | 143 | sequence_duration_s: Annotated[float | None, OpenLIFUFieldData("Sequence duration (s)", "Total duration of the sequence (s)")] = None |
140 | 144 | """Total duration of the sequence (s)""" |
141 | 145 |
|
| 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)""" |
142 | 148 |
|
143 | 149 | param_constraints: Annotated[Dict[str, ParameterConstraint], OpenLIFUFieldData("Parameter constraints", None)] = field(default_factory=dict) |
144 | 150 | """TODO: Add description""" |
@@ -572,3 +578,69 @@ def get_beamwidth( |
572 | 578 | negoff, posoff = get_beam_bounds(da, focus=focus, dim=dim, cutoff=float(cutoff), origin=origin, min_offset=min_offset, max_offset=max_offset) |
573 | 579 | bw = posoff - negoff |
574 | 580 | 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("Squared Voltage must be between 50 and 500 V^2 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