Source code for ta.signals

"""Calculation of TGA, DTG, DTA, and MS signals from simulation data.

Each function takes a :class:`~ta.simulator.SimulationResult` (or the
relevant arrays directly) and returns a NumPy array suitable for
plotting.

Signal definitions
------------------
* **TGA** — sum of condensed-species mass fractions × 100.
* **DTG** — time derivative of TGA [%/min].
* **DTA** — wall heat flux normalised by current condensed mass [W/kg].
* **MS**  — mole fractions of volatile (target gas) species.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    from ta.simulator import SimulationResult


# ------------------------------------------------------------------
# TGA
# ------------------------------------------------------------------
[docs] def compute_tga( result: SimulationResult, condensed_species: list[str] | None = None, ) -> np.ndarray: r"""Mass remaining as a percentage of the condensed-species mass fraction. .. math:: \text{TGA}(t) = \left(\sum_{k \in \text{condensed}} Y_k(t)\right) \times 100 Parameters ---------- result : SimulationResult condensed_species : list[str] or None Override the condensed-species list stored in *result*. Returns ------- np.ndarray TGA signal [%]. """ species = condensed_species if condensed_species is not None else result.condensed_species n = len(result.time_s) Y_cond = np.zeros(n) for sp in species: if sp in result.mass_fractions: Y_cond += result.mass_fractions[sp] return Y_cond * 100.0
# ------------------------------------------------------------------ # DTG # ------------------------------------------------------------------
[docs] def compute_dtg(tga: np.ndarray, time_s: np.ndarray) -> np.ndarray: """Time derivative of the TGA signal. Uses :func:`numpy.gradient` with time converted to **minutes** so the result is in %/min. Parameters ---------- tga : np.ndarray TGA signal [%]. time_s : np.ndarray Time array [s]. Returns ------- np.ndarray DTG signal [%/min]. """ time_min = time_s / 60.0 return np.gradient(tga, time_min)
# ------------------------------------------------------------------ # DTA # ------------------------------------------------------------------
[docs] def compute_dta( result: SimulationResult, condensed_species: list[str] | None = None, sensitivity: float = 1.0, ) -> np.ndarray: r"""Differential-thermal-analysis signal. The DTA signal is the wall heat-transfer rate normalised by the current condensed mass: .. math:: \text{DTA}(t) = \frac{\dot{Q}_\text{wall}(t)} {m_\text{reactor}(t) \cdot \text{TGA}(t)/100} \times S where *S* is an optional calibration *sensitivity* factor that converts W/kg to µV/mg (instrument output). Sign convention: positive DTA = net heat flowing **into** the reactor (endothermic event or heating lag). Parameters ---------- result : SimulationResult condensed_species : list[str] or None Override the condensed-species list stored in *result*. sensitivity : float Calibration constant [µV·kg / (W·mg)] (default 1.0, i.e. the output is in W/kg). Returns ------- np.ndarray DTA signal. """ tga = compute_tga(result, condensed_species) condensed_fraction = tga / 100.0 condensed_mass_kg = result.reactor_mass * condensed_fraction with np.errstate(divide="ignore", invalid="ignore"): dta = np.where( condensed_mass_kg > 1e-30, result.wall_heat_flux / condensed_mass_kg, 0.0, ) return dta * sensitivity
# ------------------------------------------------------------------ # MS # ------------------------------------------------------------------
[docs] def compute_ms( result: SimulationResult, target_gases: list[str] | None = None, top_n: int | None = None, ) -> dict[str, np.ndarray]: """Gas-phase mole fractions as a proxy for MS ion intensity. Parameters ---------- result : SimulationResult target_gases : list[str] or None Explicit list of gas species to return. If *None*, all non-condensed, non-bath-gas species with any non-zero mole fraction are included. top_n : int or None If set, keep only the *top_n* species ranked by their **peak** mole fraction over the simulation. Returns ------- dict[str, np.ndarray] ``{species_name: mole_fraction_array}``. """ excluded = set(result.condensed_species) | {result.bath_gas} if target_gases is not None: candidates = [s for s in target_gases if s in result.mole_fractions] else: candidates = [s for s in result.species_names if s not in excluded] ms: dict[str, np.ndarray] = {} for sp in candidates: arr = result.mole_fractions[sp] if np.any(arr > 0): ms[sp] = arr if top_n is not None and len(ms) > top_n: ranked = sorted(ms, key=lambda s: float(np.max(ms[s])), reverse=True) ms = {s: ms[s] for s in ranked[:top_n]} return ms