Skip to content

Solver3T

Solver3T

Solver3T(
    dic: Optional[dict[str, Any]] = None,
    joule: Type[PowerTerm] = PowerTerm,
    solar: Type[PowerTerm] = PowerTerm,
    convective: Type[PowerTerm] = PowerTerm,
    radiative: Type[PowerTerm] = PowerTerm,
    precipitation: Type[PowerTerm] = PowerTerm,
)

Bases: Solver

Source code in src/thermohl/solver/slv3t.py
106
107
108
109
110
111
112
113
114
115
116
def __init__(
    self,
    dic: Optional[dict[str, Any]] = None,
    joule: Type[PowerTerm] = PowerTerm,
    solar: Type[PowerTerm] = PowerTerm,
    convective: Type[PowerTerm] = PowerTerm,
    radiative: Type[PowerTerm] = PowerTerm,
    precipitation: Type[PowerTerm] = PowerTerm,
):
    super().__init__(dic, joule, solar, convective, radiative, precipitation)
    self.update()

average

average(
    surface_temperature: floatArray,
    core_temperature: floatArray,
) -> floatArrayLike

Compute average temperature given surface and core temperature.

This formula is based on analytical solution in steady-state mode. For single material, the formula reduces itself to an usual mean; for bi-material conductors, we have geometrical terms to take into account.

Parameters:

Name Type Description Default

surface_temperature

ndarray

Array of surface temperatures.

required

core_temperature

ndarray

Array of core temperatures.

required

Returns:

Type Description
floatArrayLike

float | numpy.ndarray: Array of average temperatures.

Source code in src/thermohl/solver/slv3t.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def average(
    self, surface_temperature: floatArray, core_temperature: floatArray
) -> floatArrayLike:
    """
    Compute average temperature given surface and core temperature.

    This formula is based on analytical solution in steady-state mode. For
    single material, the formula reduces itself to an usual mean; for
    bi-material conductors, we have geometrical terms to take into account.

    Args:
        surface_temperature (numpy.ndarray): Array of surface temperatures.
        core_temperature (numpy.ndarray): Array of core temperatures.

    Returns:
        float | numpy.ndarray: Array of average temperatures.
    """
    average_temperature = 0.5 * (surface_temperature + core_temperature)
    _, outer_diameter, core_diameter, positive_surface_diameter_indices = (
        self.morgan_coefficients
    )
    average_temperature[positive_surface_diameter_indices] = _profile_bim_avg(
        surface_temperature[positive_surface_diameter_indices],
        core_temperature[positive_surface_diameter_indices],
        0.5 * core_diameter[positive_surface_diameter_indices],
        0.5 * outer_diameter[positive_surface_diameter_indices],
    )
    return average_temperature

balance_3t

balance_3t(
    surface_temperature: floatArray,
    core_temperature: floatArray,
    joule_value: Optional[floatArrayLike] = None,
) -> floatArrayLike

Calculate the thermal balance.

This method computes the thermal balance by summing the joule heating, specific heat, and subtracting the contributions from the cooling components (convection, radiation, and conduction).

Parameters:

Name Type Description Default

surface_temperature

ndarray

Array of surface temperatures.

required

core_temperature

ndarray

Array of core temperatures.

required

joule_value

float | ndarray

Precomputed joule heating value. If None, it will be computed from the given temperatures.

None

Returns:

Type Description
floatArrayLike

float | numpy.ndarray: The resulting thermal balance.

Source code in src/thermohl/solver/slv3t.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def balance_3t(
    self,
    surface_temperature: floatArray,
    core_temperature: floatArray,
    joule_value: Optional[floatArrayLike] = None,
) -> floatArrayLike:
    """
    Calculate the thermal balance.

    This method computes the thermal balance by summing the joule heating,
    specific heat, and subtracting the contributions from the cooling
    components (convection, radiation, and conduction).

    Args:
        surface_temperature (numpy.ndarray): Array of surface temperatures.
        core_temperature (numpy.ndarray): Array of core temperatures.
        joule_value (float | numpy.ndarray, optional): Precomputed joule heating value.
            If None, it will be computed from the given temperatures.

    Returns:
        float | numpy.ndarray: The resulting thermal balance.
    """
    if joule_value is None:
        joule_value = self.joule(surface_temperature, core_temperature)
    return (
        joule_value
        + self.solar_heating.value(surface_temperature)
        - self.convective_cooling.value(surface_temperature)
        - self.radiative_cooling.value(surface_temperature)
        - self.precipitation_cooling.value(surface_temperature)
    )

balance_and_morgan

balance_and_morgan(
    surface_temperature: floatArray,
    core_temperature: floatArray,
) -> tuple[floatArrayLike, floatArray]

Compute both balance and morgan efficiently by sharing computations.

This is the optimized version used by steady-state solvers to avoid redundant joule heating calculations.

Parameters:

Name Type Description Default

surface_temperature

ndarray

Array of surface temperatures.

required

core_temperature

ndarray

Array of core temperatures.

required

Returns:

Type Description
tuple[floatArrayLike, floatArray]

tuple[float | numpy.ndarray, numpy.ndarray]: The thermal balance and the Morgan function result.

Source code in src/thermohl/solver/slv3t.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def balance_and_morgan(
    self, surface_temperature: floatArray, core_temperature: floatArray
) -> tuple[floatArrayLike, floatArray]:
    """
    Compute both balance and morgan efficiently by sharing computations.

    This is the optimized version used by steady-state solvers to avoid
    redundant joule heating calculations.

    Args:
        surface_temperature (numpy.ndarray): Array of surface temperatures.
        core_temperature (numpy.ndarray): Array of core temperatures.

    Returns:
        tuple[float | numpy.ndarray, numpy.ndarray]:
            The thermal balance and the Morgan function result.
    """
    # Compute joule once and reuse for both functions
    joule_value = self.joule(surface_temperature, core_temperature)

    balance_value = self.balance_3t(
        surface_temperature, core_temperature, joule_value=joule_value
    )
    morgan_value = self.morgan_3t(
        surface_temperature, core_temperature, joule_value=joule_value
    )
    return balance_value, morgan_value

joule

joule(
    surface_temperature: floatArray,
    core_temperature: floatArray,
) -> floatArrayLike

Calculate the Joule heating effect.

Parameters:

Name Type Description Default

surface_temperature

ndarray

Array of surface temperatures.

required

core_temperature

ndarray

Array of core temperatures.

required

Returns:

Type Description
floatArrayLike

float | numpy.ndarray: The calculated Joule heating values.

Notes: - The function computes the average temperature temperature. - Returns the Joule heating values based on the adjusted temperatures.

Source code in src/thermohl/solver/slv3t.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def joule(
    self, surface_temperature: floatArray, core_temperature: floatArray
) -> floatArrayLike:
    """
    Calculate the Joule heating effect.

    Args:
        surface_temperature (numpy.ndarray): Array of surface temperatures.
        core_temperature (numpy.ndarray): Array of core temperatures.

    Returns:
        float | numpy.ndarray: The calculated Joule heating values.

    Notes:
    - The function computes the average temperature `temperature`.
    - Returns the Joule heating values based on the adjusted temperatures.
    """
    average_temperature = self.average(surface_temperature, core_temperature)
    return self.joule_heating.value(average_temperature)

morgan_3t

morgan_3t(
    surface_temperature: floatArray,
    core_temperature: floatArray,
    joule_value: Optional[floatArrayLike] = None,
) -> floatArray

Computes the Morgan function for given temperature arrays.

Parameters:

Name Type Description Default

surface_temperature

ndarray

Array of surface temperatures.

required

core_temperature

ndarray

Array of core temperatures.

required

joule_value

float | ndarray

Precomputed joule heating value. If None, it will be computed from the given temperatures.

None

Returns:

Type Description
floatArray

numpy.ndarray: Resulting array after applying the Morgan function.

Source code in src/thermohl/solver/slv3t.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def morgan_3t(
    self,
    surface_temperature: floatArray,
    core_temperature: floatArray,
    joule_value: Optional[floatArrayLike] = None,
) -> floatArray:
    """
    Computes the Morgan function for given temperature arrays.

    Args:
        surface_temperature (numpy.ndarray): Array of surface temperatures.
        core_temperature (numpy.ndarray): Array of core temperatures.
        joule_value (float | numpy.ndarray, optional): Precomputed joule heating value.
            If None, it will be computed from the given temperatures.

    Returns:
        numpy.ndarray: Resulting array after applying the Morgan function.
    """
    if joule_value is None:
        joule_value = self.joule(surface_temperature, core_temperature)
    heat_flux_coefficient = self.morgan_coefficients[0]
    thermal_resistance = heat_flux_coefficient / (
        2.0 * np.pi * self.args.radial_thermal_conductivity
    )
    return (
        core_temperature - surface_temperature
    ) - thermal_resistance * joule_value

steady_intensity

steady_intensity(
    max_conductor_temperature: floatArrayLike = np.array(
        []
    ),
    target: CableLocationListLike = None,
    cable_type: CableTypeListLike = None,
    tol: float = default.tol,
    maxiter: int = default.maxiter,
    return_err: bool = False,
    return_temp: bool = True,
    return_power: bool = True,
) -> dict[str, np.ndarray]

Compute the steady-state intensity for a given temperature profile.

Parameters:

Name Type Description Default

max_conductor_temperature

float | ndarray

Initial temperature profile. Default is an empty numpy array.

array([])

target

TargetType | list[CableLocation]

Target specification for the solver. Default is None.

None

cable_type

CableType | list[CableType]

Cable type specification for the solver. Default is None. If provided, it overrides the target specification.

None

tol

float

Tolerance for the solver. Default is DP.tol.

tol

maxiter

int

Maximum number of iterations for the solver. Default is DP.maxiter.

maxiter

return_err

bool

If True, return the error in the output DataFrame. Default is False.

False

return_temp

bool

If True, return the temperature profiles in the output DataFrame. Default is True.

True

return_power

bool

If True, return the power profiles in the output DataFrame. Default is True.

True

Returns:

Type Description
dict[str, ndarray]

dict[str, np.ndarray]: Dictionary containing the steady-state intensity and optionally the error, temperature profiles, and power profiles,

dict[str, ndarray]

along with input data.

Source code in src/thermohl/solver/slv3t.py
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
def steady_intensity(
    self,
    max_conductor_temperature: floatArrayLike = np.array([]),
    target: CableLocationListLike = None,
    cable_type: CableTypeListLike = None,
    tol: float = default.tol,
    maxiter: int = default.maxiter,
    return_err: bool = False,
    return_temp: bool = True,
    return_power: bool = True,
) -> dict[str, np.ndarray]:
    """
    Compute the steady-state intensity for a given temperature profile.

    Args:
        max_conductor_temperature (float | numpy.ndarray): Initial temperature profile. Default is an empty numpy array.
        target (TargetType | list[CableLocation]): Target specification for the solver. Default is None.
        cable_type (CableType | list[CableType]): Cable type specification for the solver. Default is None. If provided, it overrides the target specification.
        tol (float): Tolerance for the solver. Default is DP.tol.
        maxiter (int): Maximum number of iterations for the solver. Default is DP.maxiter.
        return_err (bool): If True, return the error in the output DataFrame. Default is False.
        return_temp (bool): If True, return the temperature profiles in the output DataFrame. Default is True.
        return_power (bool): If True, return the power profiles in the output DataFrame. Default is True.

    Returns:
        dict[str, np.ndarray]: Dictionary containing the steady-state intensity and optionally the error, temperature profiles, and power profiles,
        along with input data.
    """
    target = _infer_target_from_cable_type(cable_type, target)

    Tmax, newtheader = self._steady_intensity_header(
        max_conductor_temperature, target
    )

    def balance_and_morgan(
        i: floatArray, tg: floatArray
    ) -> Tuple[floatArrayLike, floatArray]:
        surface_temperature, core_temperature = newtheader(i, tg)
        return self.balance_and_morgan(surface_temperature, core_temperature)

    # solve system
    s = Solver1T(
        self.args.__dict__,
        type(self.joule_heating),
        type(self.solar_heating),
        type(self.convective_cooling),
        type(self.radiative_cooling),
        type(self.precipitation_cooling),
    )
    r = s.steady_intensity(Tmax, tol=1.0, return_power=False)
    x, y, iterations, err = quasi_newton_2d(
        balance_and_morgan,
        r[VariableType.TRANSIT.value],
        Tmax,
        relative_tolerance=tol,
        max_iterations=maxiter,
        delta_x=1.0e-03,
        delta_y=1.0e-03,
    )
    if np.max(err) > tol or iterations == maxiter:
        logger.debug(
            f"rstat_analytic max err is {np.max(err):.3E} in {iterations:d} iterations"
        )

    # format output
    result = {VariableType.TRANSIT.value: x}

    self.add_error_if_needed(err, result, return_err)

    if return_temp or return_power:
        surface_temperature, core_temperature = newtheader(x, y)
        average_temperature = self.average(surface_temperature, core_temperature)

        if return_temp:
            result[TemperatureType.SURFACE.value] = surface_temperature
            result[TemperatureType.AVERAGE.value] = average_temperature
            result[TemperatureType.CORE.value] = core_temperature

        if return_power:
            result[PowerType.JOULE.value] = self.joule_heating.value(
                average_temperature
            )
            result[PowerType.SOLAR.value] = self.solar_heating.value(
                surface_temperature
            )
            result[PowerType.CONVECTION.value] = self.convective_cooling.value(
                surface_temperature
            )
            result[PowerType.RADIATION.value] = self.radiative_cooling.value(
                surface_temperature
            )
            result[PowerType.RAIN.value] = self.precipitation_cooling.value(
                surface_temperature
            )

    result = self._add_input_data_to_result(result)

    return result

steady_temperature

steady_temperature(
    surface_temperature_guess: Optional[
        floatArrayLike
    ] = None,
    core_temperature_guess: Optional[floatArrayLike] = None,
    tol: float = default.tol,
    maxiter: int = default.maxiter,
    return_err: bool = False,
    return_power: bool = True,
) -> dict[str, np.ndarray]

Compute the steady-state temperature distribution.

Parameters:

Name Type Description Default

surface_temperature_guess

float | ndarray | None

Initial guess for the surface temperature. If None, ambient temperature is used.

None

core_temperature_guess

float | ndarray | None

Initial guess for the core temperature. If None, 1.5 times the absolute value of ambient temperature is used.

None

tol

float

Tolerance for the quasi-Newton solver.

tol

maxiter

int

Maximum number of iterations for the quasi-Newton solver.

maxiter

return_err

bool

If True, the error of the solution is included in the returned dict.

False

return_power

bool

If True, power-related values are included in the returned dict.

True

Returns:

Type Description
dict[str, ndarray]

dict[str, np.ndarray]: Dictionary containing the steady-state temperatures and optionally the error and power-related values,

dict[str, ndarray]

along with input data.

Source code in src/thermohl/solver/slv3t.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def steady_temperature(
    self,
    surface_temperature_guess: Optional[floatArrayLike] = None,
    core_temperature_guess: Optional[floatArrayLike] = None,
    tol: float = default.tol,
    maxiter: int = default.maxiter,
    return_err: bool = False,
    return_power: bool = True,
) -> dict[str, np.ndarray]:
    """
    Compute the steady-state temperature distribution.

    Args:
        surface_temperature_guess (float | numpy.ndarray | None): Initial guess for the surface temperature. If None, ambient temperature is used.
        core_temperature_guess (float | numpy.ndarray | None): Initial guess for the core temperature. If None, 1.5 times the absolute value of ambient temperature is used.
        tol (float): Tolerance for the quasi-Newton solver.
        maxiter (int): Maximum number of iterations for the quasi-Newton solver.
        return_err (bool): If True, the error of the solution is included in the returned dict.
        return_power (bool): If True, power-related values are included in the returned dict.

    Returns:
        dict[str, np.ndarray]: Dictionary containing the steady-state temperatures and optionally the error and power-related values,
        along with input data.
    """

    # if no guess provided, use ambient temp
    shape = (self.args.get_number_of_computations(),)
    surface_temperature_guess = (
        surface_temperature_guess
        if surface_temperature_guess is not None
        else 1.0 * self.args.ambient_temperature
    )
    core_temperature_guess = (
        core_temperature_guess
        if core_temperature_guess is not None
        else 1.5 * np.abs(self.args.ambient_temperature)
    )
    surface_temperature_guess_ = surface_temperature_guess * np.ones(shape)
    core_temperature_guess_ = core_temperature_guess * np.ones(shape)

    # solve system
    surface_temperature, core_temperature, iterations, err = quasi_newton_2d(
        self.balance_and_morgan,
        x_init=surface_temperature_guess_,
        y_init=core_temperature_guess_,
        relative_tolerance=tol,
        max_iterations=maxiter,
        delta_x=1.0e-03,
        delta_y=1.0e-03,
    )
    if np.max(err) > tol or iterations == maxiter:
        logger.debug(
            f"rstat_analytic max err is {np.max(err):.3E} in {iterations:d} iterations"
        )

    # format output
    average_temperature = self.average(surface_temperature, core_temperature)
    result = {
        TemperatureType.SURFACE.value: surface_temperature,
        TemperatureType.AVERAGE.value: average_temperature,
        TemperatureType.CORE.value: core_temperature,
    }

    self.add_error_if_needed(err, result, return_err)
    self.add_power_if_needed(
        average_temperature, result, return_power, surface_temperature
    )

    result = self._add_input_data_to_result(result)

    return result

transient_temperature

transient_temperature(
    offset: floatArray = np.array([]),
    surface_temperature_0: Optional[floatArrayLike] = None,
    core_temperature_0: Optional[floatArrayLike] = None,
    return_power: bool = False,
) -> Dict[str, Any]

Compute transient-state temperature.

Parameters:

Name Type Description Default

offset

ndarray

A 1D array with times (in seconds) when the temperature needs to be computed. The array must contain increasing values (undefined behaviour otherwise).

array([])

surface_temperature_0

float | ndarray | None

Initial surface temperature. If None, the ambient temperature from the internal dict will be used. The default is None.

None

core_temperature_0

float | ndarray | None

Initial core temperature. If None, the ambient temperature from the internal dict will be used. The default is None.

None

return_power

bool

Return power term values. The default is False.

False

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: A dictionary with temperature and other results (depending on inputs) in the keys,

Dict[str, Any]

along with input data.

Source code in src/thermohl/solver/slv3t.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
def transient_temperature(
    self,
    offset: floatArray = np.array([]),
    surface_temperature_0: Optional[floatArrayLike] = None,
    core_temperature_0: Optional[floatArrayLike] = None,
    return_power: bool = False,
) -> Dict[str, Any]:
    """
    Compute transient-state temperature.

    Args:
        offset (numpy.ndarray): A 1D array with times (in seconds) when the temperature needs to be computed. The array must contain increasing values (undefined behaviour otherwise).
        surface_temperature_0 (float | numpy.ndarray | None): Initial surface temperature. If None, the ambient temperature from the internal dict will be used. The default is None.
        core_temperature_0 (float | numpy.ndarray | None): Initial core temperature. If None, the ambient temperature from the internal dict will be used. The default is None.
        return_power (bool, optional): Return power term values. The default is False.

    Returns:
        Dict[str, Any]: A dictionary with temperature and other results (depending on inputs) in the keys,
        along with input data.

    """
    # get sizes (n for input dict entries, N for time)
    n = self.args.get_number_of_computations()
    N = len(offset)
    if N < 2:
        raise ValueError()

    # get initial temperature
    surface_temperature_0 = (
        surface_temperature_0
        if surface_temperature_0 is not None
        else self.args.ambient_temperature
    )
    core_temperature_0 = (
        core_temperature_0
        if core_temperature_0 is not None
        else 1.0 + surface_temperature_0
    )
    time_changing_parameters = get_time_changing_parameters(self.args, offset, N, n)
    c1, c2 = self._morgan_transient()
    # inverse of m*C : shortcuts for time-loop
    imc = 1.0 / (self.args.linear_mass * self.args.heat_capacity)

    # init
    surface_temperature = np.zeros((N, n))
    average_temperature = np.zeros((N, n))
    core_temperature = np.zeros((N, n))
    surface_temperature[0, :] = surface_temperature_0
    core_temperature[0, :] = core_temperature_0
    average_temperature[0, :] = self.average(
        surface_temperature[0, :], core_temperature[0, :]
    )

    # main time loop
    for i in range(1, len(offset)):
        for k in time_changing_parameters.keys():
            self.args[k] = time_changing_parameters[k][i, :]
        self.update()
        bal = self.balance_3t(
            surface_temperature[i - 1, :], core_temperature[i - 1, :]
        )
        average_temperature[i, :] = (
            average_temperature[i - 1, :] + (offset[i] - offset[i - 1]) * bal * imc
        )
        mrg = c1 * (self.joule_heating.value(average_temperature[i, :]) - bal)
        core_temperature[i, :] = average_temperature[i, :] + c2 * mrg
        surface_temperature[i, :] = core_temperature[i, :] - mrg

    result = self._transient_temperature_results(
        offset,
        surface_temperature,
        average_temperature,
        core_temperature,
        return_power,
        n,
    )
    result = self._add_input_data_to_result(result)
    return result

update

update() -> None

Updates the solver's internal state by reinitializing several components and recalculating the Morgan coefficients. This method performs the following steps: 1. Extends the arguments to their maximum length. 2. Reinitializes the joule_heating, solar_heating, convective_cooling, radiative_cooling, and precipitation_cooling components using the updated arguments. 3. Recalculates the Morgan coefficients using the updated dimensions. 4. Compresses the arguments. Returns: None

Source code in src/thermohl/solver/slv3t.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def update(self) -> None:
    """
    Updates the solver's internal state by reinitializing several components
    and recalculating the Morgan coefficients.
    This method performs the following steps:
    1. Extends the arguments to their maximum length.
    2. Reinitializes the `joule_heating`, `solar_heating`, `convective_cooling`, `radiative_cooling`, and `precipitation_cooling` components using the updated arguments.
    3. Recalculates the Morgan coefficients using the updated dimensions.
    4. Compresses the arguments.
    Returns:
        None
    """
    self.args.extend()
    self.joule_heating.__init__(**self.args.__dict__)
    self.solar_heating.__init__(**self.args.__dict__)
    self.convective_cooling.__init__(**self.args.__dict__)
    self.radiative_cooling.__init__(**self.args.__dict__)
    self.precipitation_cooling.__init__(**self.args.__dict__)
    self.morgan_coefficients = self._morgan_coefficients()
    self.args.compress()