Safety and stress
SAR, power loss, incident power density, dielectric breakdown, and component stress result types in witwin.maxwell.
This page covers the result types for RF exposure and stress analysis: power loss, specific absorption rate (SAR), incident power density, dielectric breakdown, and component rating checks. All names are exported from witwin.maxwell:
import witwin.maxwell as mw
mw.SARResult, mw.PowerLossData, mw.IncidentPowerDensity, mw.BreakdownStressDataYou rarely construct these classes yourself. You declare a monitor (or a breakdown material) on the scene, run an FDTD simulation, and call a Result method that returns the typed object.
| Declare on the scene | Call on the result | Returns |
|---|---|---|
PowerLossMonitor | result.power_loss(name) | PowerLossData |
PowerLossMonitor plus tissue mass_density | result.sar(name, ...) | SARResult |
IncidentPowerDensityMonitor | result.incident_power_density(name) | IncidentPowerDensity |
BreakdownMonitor | result.breakdown(name) | BreakdownStressData |
ComponentStressMonitor | result.component_stress(name) | ComponentStressData |
Material with DielectricBreakdown | result.breakdown_data | BreakdownResultData or None |
The monitors are documented on Monitors. DielectricBreakdown and mass_density are documented on Materials. Result is documented on Result.
Warning
None of these quantities is certified for regulatory compliance. The SAR mass averaging ("cubical-prefix-v1") and the incident-power spatial average ("spatial-average-v1") are IEEE/IEC-inspired but differ from the standards. Breakdown stress values are indicators, not failure predictions.
SAR workflow#
Give tissue a mass density
Create the material with
mass_densityin kg/m³ and a conductivity, for examplemw.Material(sigma_e=0.5, mass_density=1000.0, name="tissue").Add a PowerLossMonitor
scene.add_monitor(mw.PowerLossMonitor("loss", position=..., size=..., channels=("conduction",))).Run FDTD with full-field DFT
Result.power_lossandResult.sarread frequency-domainEx,Ey,Ez. Run the FDTD simulation withfull_field_dft=True.Reduce
Call
result.sar("loss", averaging=mw.SARAveraging(mass=1e-3))and readsar.peak(1e-3).
sar = result.sar("loss", averaging=mw.SARAveraging(mass=(1e-3, 10e-3)))
peak = sar.peak(10e-3)
print(peak.sar, peak.position, peak.mass_kg)
point = sar.point_sar("total") # [F, nx, ny, nz], W/kgResult.sar(monitor, *, averaging=None, normalization=None, electric_fields=None, volume_channels=None) only accepts FDTD results and a declared PowerLossMonitor; other cases raise NotImplementedError or KeyError. When normalization is None, it uses PowerNormalization.none() (unit source amplitude).
SARAveraging#
@dataclass(frozen=True)
class SARAveraging(
mass,
profile="cubical-prefix-v1",
connectivity="cube",
boundary_policy="strict-interior",
min_tissue_fraction=0.1,
)Request for local mass-averaged SAR. Pass it as averaging= to Result.sar or combine_incoherent_sar.
One or more target averaging masses in kg, for example 1e-3 for 1 g or 10e-3 for 10 g. Stored as a tuple. Each value must be positive.
'cubical-prefix-v1'Averaging algorithm version. Only "cubical-prefix-v1" (AVERAGING_PROFILE) is accepted; other values raise ValueError.
'cube'How cube tissue is gathered. Only "cube" is implemented; other values raise NotImplementedError.
'strict-interior'Cube clipping at the monitor edge. Only "strict-interior" is implemented: clipped centers are marked invalid instead of padded.
0.1Minimum tissue fill of a valid averaging cube, in [0, 1].
SARAveraging.payload#
SARAveraging.payload() -> dictReturns a serializable dict with mass, profile, version, connectivity, boundary_policy, and min_tissue_fraction.
AVERAGING_PROFILE#
AVERAGING_PROFILE = "cubical-prefix-v1"Name of the only supported SAR mass-averaging profile. SARPeak.profile carries this value.
PowerNormalization#
@dataclass(frozen=True)
class PowerNormalization(kind, amplitude=None, port=None, watts=None)Power scale applied to SAR, with provenance. SAR is linear in absorbed power, so the scale multiplies SAR directly. Create instances with the class methods below; kind must be "source", "accepted_power", or "input_power".
PowerNormalization.source#
PowerNormalization.source(amplitude=1.0) -> PowerNormalizationScales SAR by amplitude**2, because fields scale with the source amplitude. amplitude must be positive.
PowerNormalization.none#
PowerNormalization.none() -> PowerNormalizationSame as PowerNormalization.source(amplitude=1.0): no scaling.
PowerNormalization.accepted_power#
PowerNormalization.accepted_power(port, watts) -> PowerNormalizationScales SAR so that the named port accepts watts W at each SAR frequency. The scale is watts / measured_accepted_power per frequency and keeps the port's autograd graph.
Name of a port in the result.
Target accepted power in W. Must be positive.
Resolution raises KeyError when the port is missing or a SAR frequency is not in the port spectrum, and ValueError when the measured accepted power is not positive or the port has several excitations.
PowerNormalization.input_power#
PowerNormalization.input_power(watts) -> PowerNormalizationCreates a total-input-power request. Resolving it raises NotImplementedError in this version, because no total injected source-power diagnostic exists. Use accepted_power or source instead.
PowerNormalization.resolve_scale#
PowerNormalization.resolve_scale(result=None, *, frequencies=None) -> float | torch.TensorReturns the multiplicative power scale. "source" returns a float. "accepted_power" needs result and frequencies and returns a [F] tensor. Result.sar calls this for you.
PowerNormalization.payload#
PowerNormalization.payload() -> dictReturns kind, amplitude, port, and watts as a dict.
SARResult#
@dataclass(frozen=True)
class SARResult(
point,
statistics,
normalization,
provenance,
frequencies,
coordinates,
valid,
occupancy,
rho_cell,
cell_volume,
tissue_id,
tissue_names,
absorbed_power_density={},
averaged={},
peaks={},
sar_unit="W/kg",
power_unit="W",
)Typed SAR reduction returned by Result.sar, combine_coherent_sar, and combine_incoherent_sar. Large tensors keep their device and autograd graph; masks and IDs do not carry gradients.
| Field | Meaning |
|---|---|
point | Mapping from each loss channel and "total" to point SAR, [F, nx, ny, nz], W/kg, NaN at invalid cells |
statistics | Per-tissue mapping keyed by tissue ID: name, cell_count, mass_kg, total_absorbed_power, mean_sar, max_sar |
normalization | The PowerNormalization applied |
provenance | Algorithm and grid metadata (field convention, grid hash, averaging profile, monitor bounds, and more) |
frequencies | [F] frequencies, Hz |
coordinates | Mapping with x, y, z coordinate tensors, metres |
valid | Boolean mask of cells where SAR is defined |
occupancy | Tissue occupancy per cell |
rho_cell | Mass density per cell, kg/m³ |
cell_volume | Volume per cell, m³ |
tissue_id, tissue_names | Tissue ID map and ID-to-name mapping |
absorbed_power_density | Mapping from channel to absorbed power density, W/m³ |
averaged | Mass-averaged SAR per mass (filled when averaging was requested) |
peaks | SARPeak per mass (filled when averaging was requested) |
Properties: device, channels (tuple of point keys), and averaging_masses (sorted tuple of averaged masses in kg).
SARResult.point_sar#
SARResult.point_sar(channel="total") -> torch.TensorReturns the [F, nx, ny, nz] point SAR for a channel. Raises KeyError for an unknown channel.
SARResult.peak#
SARResult.peak(mass) -> SARPeakReturns the mass-averaged peak SAR for mass (kg). Raises ValueError when no averaging was requested and KeyError when mass was not computed.
SARResult.averaged_sar#
SARResult.averaged_sar(mass) -> torch.TensorReturns the [F, nx, ny, nz] mass-averaged SAR field for mass, W/kg, NaN at invalid centers.
SARResult.soft_peak#
SARResult.soft_peak(temperature, *, mass=None) -> torch.TensorReturns a differentiable [F] surrogate of the peak: a softmax-weighted mean over valid mass-averaged cube centers. As temperature approaches 0 it approaches peak. Use it as an optimization objective, not as a reported value.
Softmax temperature in W/kg. Must be positive.
NoneAveraging mass in kg. Required when several masses were computed.
SARResult.payload#
SARResult.payload() -> dictReturns a detached CPU dict for serialization.
SARResult.save#
SARResult.save(path)Writes payload() with torch.save, creating parent directories.
SARResult.load#
SARResult.load(path, *, map_location="cpu") -> SARResultClass method that reads a file written by save. Raises ValueError when the file is not a SARResult payload. It calls torch.load with weights_only=False, so load only files you trust.
SARPeak#
@dataclass(frozen=True)
class SARPeak(
mass_target,
sar,
frequencies,
index,
position,
mass_kg,
cube_half_width,
cube_size_m,
profile="cubical-prefix-v1",
unit="W/kg",
)Mass-averaged peak SAR per frequency, returned by SARResult.peak.
| Field | Meaning |
|---|---|
mass_target | Requested averaging mass, kg |
sar | [F] peak averaged SAR, W/kg, NaN where no valid center exists |
frequencies | [F] frequencies, Hz |
index | Grid index of the peak cube center |
position | Physical coordinates of the peak center, metres, shape [F, 3] |
mass_kg | Actual enclosed averaging mass, kg |
cube_half_width | Cube half-width in cells (edge 2h+1 cells) |
cube_size_m | Physical cube edge length per axis, metres |
The peak location comes from argmax, so it is not differentiable; sar still carries the field's autograd graph.
Methods: payload() returns a detached CPU dict, and the class method SARPeak.from_payload(payload) rebuilds an instance.
combine_coherent_sar#
combine_coherent_sar(
results,
*,
monitor,
weights=None,
averaging=None,
normalization=None,
) -> SARResultSums the complex electric fields of several same-frequency runs, then reduces once to SAR. Interference between the sources is captured.
At least two results with the same grid (checked by node-coordinate hash) and monitor frequencies.
Name of the PowerLossMonitor in every run.
NoneComplex weight per run (relative amplitude and phase). None means all 1.0.
NoneMass-averaging request.
NonePower normalization. None means unit source amplitude.
Raises ValueError for fewer than two results, mismatched grids or frequencies, or a wrong number of weights.
combined = mw.combine_coherent_sar([run_a, run_b], monitor="loss", weights=[1.0, -1.0])combine_incoherent_sar#
combine_incoherent_sar(sar_results, *, averaging=None) -> SARResultAdds absorbed power from several SARResult objects (power-domain sum), then re-forms point SAR and tissue statistics.
At least two results with identical grid hash, field convention, normalization, frequencies, loss channels, tissue map, validity mask, and mass density.
NoneWhen given, mass-averaged SAR and peaks are recomputed on the combined power.
Raises ValueError on any metadata mismatch or fewer than two operands, and TypeError when an operand is not a SARResult.
PowerLossData#
@dataclass(frozen=True)
class PowerLossData(
frequencies,
channel_power,
volume_density,
total,
component_volumes,
global_ids,
monitor_name,
bounds,
occupancy=None,
material_ids=None,
geometry_ids=None,
surface_density={},
face_areas={},
line_density={},
line_lengths={},
source_result_fingerprint=None,
autograd_provenance="live graph is preserved from supplied torch tensors; persisted data is detached",
)Loss channels on the Yee electric edges inside a PowerLossMonitor. Returned by result.power_loss(name). All tensors stay on the monitor's device.
The built-in "conduction" channel is computed from the static electric conductivity and the peak-phasor electric field, q = 0.5 * sigma_e * abs(E)**2. Other channels named in the monitor must be supplied to Result.power_loss as densities or integrated powers.
| Field | Shape | Meaning |
|---|---|---|
frequencies | [F] | Hz |
channel_power | [F] per channel | Integrated power per channel, W |
volume_density | [F, N] per channel and component | Volume loss density, W/m³ |
total | [F] | Sum of all available channels, W |
component_volumes | [N] per component | Edge control volumes, m³ |
global_ids | [N] per component | Global Yee edge identifiers |
monitor_name, bounds | Source monitor name and bounds, metres | |
surface_density, face_areas | Surface loss density (W/m²) and face areas (m²) | |
line_density, line_lengths | Line loss density (W/m) and lengths (m) |
Unit fields: power_unit ("W"), volume_density_unit ("W/m^3"), surface_density_unit, line_density_unit, volume_unit, area_unit, length_unit. String fields collocation and normalization describe the convention.
Properties: device, channels (tuple of channel names), conduction, surface, and wire (the [F] power of that channel, or None when absent).
PowerLossData.channel#
PowerLossData.channel(name) -> torch.TensorReturns the [F] integrated power of a channel. Raises KeyError when the channel is absent.
PowerLossData.density#
PowerLossData.density(channel, component) -> torch.TensorReturns the [F, N] volume density for one channel and one component ("Ex", "Ey", or "Ez"). Raises KeyError when either is absent.
loss = result.power_loss("loss")
print(loss.total, loss.channels)
q_ex = loss.density("conduction", "Ex")IncidentPowerDensity#
@dataclass(frozen=True)
class IncidentPowerDensity(
monitor,
axis,
normal_direction,
frequencies,
coordinate_names,
coordinates,
normal_poynting,
power_density,
flux,
spatial_average,
provenance,
)Time-averaged incident power density on an IncidentPowerDensityMonitor plane. Returned by result.incident_power_density(name, *, spatial_average=...).
| Field | Meaning |
|---|---|
monitor, axis, normal_direction | Monitor name, plane normal axis, and normal sign |
frequencies | Tuple of frequencies, Hz |
coordinate_names, coordinates | Names and 1D coordinate tensors of the two in-plane axes, metres |
normal_poynting | Signed normal Poynting component 0.5 * Re((E x conj(H)) . n) per cell, W/m² |
power_density | Its magnitude, W/m² |
flux | Plane integral of normal_poynting, W; equals a FluxMonitor on the same plane |
spatial_average | Square-window moving average of power_density, W/m², or None when no window was requested |
provenance | Convention and, when averaged, the spatial-average-v1 window record |
Properties: power_density_unit ("W/m^2") and flux_unit ("W").
The window area comes from the monitor's spatial_average (m²). Pass spatial_average= to Result.incident_power_density to override it, or None to disable averaging. The window is a square of side sqrt(area) centred on each cell and truncated at the plane edge.
ipd = result.incident_power_density("exposure", spatial_average=4e-4)
print(float(ipd.flux), ipd.spatial_average.max())BreakdownStressData#
@dataclass(frozen=True)
class BreakdownStressData(
name,
peak_field,
peak_index,
peak_time,
exceedance_duration,
longest_exceedance_duration,
qualifying_cell_count,
exceedance_volume_time,
damage_volume,
max_field_map,
exceedance_time_map,
longest_exceedance_map,
damage_map,
qualifying_mask,
occupancy,
region_bounds,
provenance,
)Dielectric stress inside a BreakdownMonitor region, returned by result.breakdown(name). The monitor does not feed back into the field solve. It compares the cell-center field magnitude abs(E) against the monitor's critical_field (V/m).
| Field | Meaning |
|---|---|
peak_field, peak_index | Region maximum of the per-cell running max of abs(E) (V/m) and its cell index |
peak_time | Always None in this version |
exceedance_duration | Largest per-cell time with abs(E) at or above the critical field, s |
longest_exceedance_duration | Largest per-cell contiguous exceedance, s |
qualifying_cell_count, qualifying_mask | Cells whose longest contiguous exceedance reached minimum_duration |
exceedance_volume_time | Sum of exceedance time times occupancy times cell volume, s·m³ |
damage_volume, damage_map | Damage integral of (abs(E) / Ecrit)**k while exceeding; None without damage_exponent |
max_field_map, exceedance_time_map, longest_exceedance_map | Per-cell maps, on device |
occupancy, region_bounds, provenance | Target-material occupancy, region bounds, and model metadata |
Properties: capability_level ("stress-only").
BreakdownStressData.locations#
BreakdownStressData.locations() -> dictReturns a dict for the qualifying cells: indices (an (n, ndim) index tensor) and count (an int).
BreakdownResultData#
@dataclass(frozen=True)
class BreakdownResultData(
events,
final_state,
dissipated_energy,
total_dissipated_energy,
grid_shape,
capability_level="deterministic-breakdown, uncalibrated",
)Output of the dynamic breakdown model, available as result.breakdown_data when a material carries DielectricBreakdown; otherwise result.breakdown_data is None. The model is deterministic and uncalibrated: a cell switches from intact to conducting after its field stays at or above critical_field for minimum_duration. It is a conductive-path model, not an arc predictor.
| Field | Meaning |
|---|---|
events | Tuple of BreakdownEvent, ordered by (step, cell_index) |
final_state | Per-node int8 state on the material grid |
dissipated_energy | Per-node breakdown-dissipated energy, J |
total_dissipated_energy | Sum of dissipated_energy, J |
grid_shape | Material grid shape (Nx, Ny, Nz) |
Properties: triggered_count (number of events). result.breakdown_events returns the same event tuple, or () when breakdown is inactive.
BreakdownResultData.events_for_material#
BreakdownResultData.events_for_material(material_id) -> tuple[BreakdownEvent, ...]Returns the events whose material_id matches.
BreakdownEvent#
@dataclass(frozen=True)
class BreakdownEvent(
step,
time,
cell_index,
position,
material_id,
field_before,
state_before,
state_after,
deposited_energy_at_trigger,
)One breakdown trigger.
| Field | Meaning |
|---|---|
step, time | Time step index and time, s |
cell_index | Row-major linear node index on the material grid |
position | Cell-center coordinates, metres |
material_id | Material identifier |
field_before | Cell-center abs(E) at the trigger step, V/m |
state_before, state_after | State codes: 0 intact, 1 recovering, 2 conducting, 3 failed |
deposited_energy_at_trigger | Local electric energy 0.5 * eps * abs(E)**2 * cell_volume at the trigger, J |
Properties: state_before_name and state_after_name return the state names.
SmoothBreakdownRisk#
@dataclass(frozen=True)
class SmoothBreakdownRisk(
critical_field,
width,
reduction="sum",
temperature=None,
damage_exponent=None,
)Differentiable surrogate of field over-stress for optimization. It is not a physical breakdown model and never feeds back into the solve. You evaluate it on a recorded field magnitude series yourself.
The definition is p = sigmoid((abs(E) - critical_field) / width), soft_duration = sum over time of p * dt, and risk is the reduction of occupancy * soft_duration over cells.
Reference field in V/m at the center of the sigmoid. Positive and finite.
Sigmoid width in V/m. Smaller values approach a hard threshold. Positive and finite.
'sum'"sum" (soft over-stress dose in seconds), "mean", or "softmax" (soft worst cell).
NoneSoftmax temperature in seconds. Required and positive for reduction="softmax".
NoneWhen set, also produces soft_damage_map, the sum over time of p * (abs(E) / critical_field)**k * dt. Does not change risk.
Properties: capability_level ("differentiable-surrogate (non-physical, non-regulatory)").
SmoothBreakdownRisk.evaluate#
SmoothBreakdownRisk.evaluate(e_magnitude_series, dt, *, occupancy=None, name="", provenance_extra=None) -> SmoothBreakdownRiskDataReduces a cell-center field magnitude series to the risk scalar. Autograd flows through e_magnitude_series.
Field magnitude in V/m, shape (T, ...) with a leading time axis and at least one cell axis.
Uniform sample spacing in seconds. Positive.
NonePer-cell weights in [0, 1], matching the non-time shape.
''Label stored on the result.
NoneExtra entries merged into provenance.
SmoothBreakdownRisk.evaluate_from_components#
SmoothBreakdownRisk.evaluate_from_components(ex_series, ey_series, ez_series, dt, *, occupancy=None, name="", provenance_extra=None) -> SmoothBreakdownRiskDataColocates raw Yee Ex, Ey, Ez series of shape (T, X, Y, Z) to cell centers with the same averaging BreakdownMonitor uses, then calls evaluate.
risk = mw.SmoothBreakdownRisk(critical_field=1.0e6, width=2.0e5)
data = risk.evaluate(e_mag_series, dt=1e-10)
data.risk.backward()SmoothBreakdownRiskData#
@dataclass(frozen=True)
class SmoothBreakdownRiskData(
name,
risk,
soft_duration_map,
soft_damage_map,
peak_soft_duration,
peak_instant_risk,
critical_field,
width,
dt,
reduction,
provenance,
)Output of SmoothBreakdownRisk.evaluate. risk is the trainable scalar (seconds for "sum"). soft_duration_map is the per-cell soft dwell time; soft_damage_map is None without damage_exponent. peak_soft_duration is the largest weighted per-cell dwell, and peak_instant_risk is the largest sigmoid value. All tensors stay in the autograd graph.
Properties: capability_level.
ComponentRating#
@dataclass(frozen=True)
class ComponentRating(voltage=None, current=None, energy=None, pulse_width=None, model=None)Absolute-maximum rating envelope for a component bound to a ComponentStressMonitor. None disables a limit. At least one limit must be set; each set limit must be positive and finite.
NonePeak voltage limit, V.
NonePeak current limit, A.
NoneTotal energy limit, J.
NonePulse-width limit, s.
NoneName of the rating source or part revision, stored in provenance.
ComponentRating.as_dict#
ComponentRating.as_dict() -> dictReturns the five fields as a dict.
scene.add_monitor(
mw.ComponentStressMonitor(
"part", port="feed", rating=mw.ComponentRating(voltage=100.0),
voltage_series="vprobe", current_series="iprobe",
)
)ComponentStressData#
@dataclass(frozen=True)
class ComponentStressData(
name,
port_name,
time,
voltage,
current,
power,
cumulative_energy,
peak_voltage,
peak_current,
peak_power,
total_energy,
measured_pulse_width,
exceedance,
provenance,
)Port stress of a rated component, returned by result.component_stress(name). The result reads the voltage and current time series named on the monitor; they must share one time axis.
| Field | Meaning |
|---|---|
time, voltage, current | 1D time series, s, V, A |
power | P = V * I, W |
cumulative_energy | Trapezoidal running integral of P, J |
peak_voltage, peak_current | Maximum absolute value |
peak_power, total_energy | Maximum P (W) and final cumulative energy (J) |
measured_pulse_width | Time where P is at least half its peak, s |
exceedance | Per limit (voltage, current, energy, pulse_width): rated, measured, exceeded, margin |
Properties: capability_level ("stress-only") and any_exceeded (True when any limit is exceeded).
ComponentStressData.from_time_series#
ComponentStressData.from_time_series(time, voltage, current, rating, *, name="", port_name="", provenance_extra=None) -> ComponentStressDataClass method that builds the record from tensors. time must be strictly increasing, and all three tensors must have the same length.
Raises TypeError when the inputs are not tensors or rating is not a ComponentRating, and ValueError on length or ordering errors.