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.BreakdownStressData

You 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 sceneCall on the resultReturns
PowerLossMonitorresult.power_loss(name)PowerLossData
PowerLossMonitor plus tissue mass_densityresult.sar(name, ...)SARResult
IncidentPowerDensityMonitorresult.incident_power_density(name)IncidentPowerDensity
BreakdownMonitorresult.breakdown(name)BreakdownStressData
ComponentStressMonitorresult.component_stress(name)ComponentStressData
Material with DielectricBreakdownresult.breakdown_dataBreakdownResultData 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#

  1. Give tissue a mass density

    Create the material with mass_density in kg/m³ and a conductivity, for example mw.Material(sigma_e=0.5, mass_density=1000.0, name="tissue").

  2. Add a PowerLossMonitor

    scene.add_monitor(mw.PowerLossMonitor("loss", position=..., size=..., channels=("conduction",))).

  3. Run FDTD with full-field DFT

    Result.power_loss and Result.sar read frequency-domain Ex, Ey, Ez. Run the FDTD simulation with full_field_dft=True.

  4. Reduce

    Call result.sar("loss", averaging=mw.SARAveraging(mass=1e-3)) and read sar.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/kg

Result.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.

massfloat | Sequence[float]required

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.

profilestrdefault 'cubical-prefix-v1'

Averaging algorithm version. Only "cubical-prefix-v1" (AVERAGING_PROFILE) is accepted; other values raise ValueError.

connectivitystrdefault 'cube'

How cube tissue is gathered. Only "cube" is implemented; other values raise NotImplementedError.

boundary_policystrdefault 'strict-interior'

Cube clipping at the monitor edge. Only "strict-interior" is implemented: clipped centers are marked invalid instead of padded.

min_tissue_fractionfloatdefault 0.1

Minimum tissue fill of a valid averaging cube, in [0, 1].

SARAveraging.payload#

SARAveraging.payload() -> dict

Returns 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) -> PowerNormalization

Scales SAR by amplitude**2, because fields scale with the source amplitude. amplitude must be positive.

PowerNormalization.none#

PowerNormalization.none() -> PowerNormalization

Same as PowerNormalization.source(amplitude=1.0): no scaling.

PowerNormalization.accepted_power#

PowerNormalization.accepted_power(port, watts) -> PowerNormalization

Scales 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.

portstrrequired

Name of a port in the result.

wattsfloatrequired

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) -> PowerNormalization

Creates 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.Tensor

Returns 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() -> dict

Returns 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.

FieldMeaning
pointMapping from each loss channel and "total" to point SAR, [F, nx, ny, nz], W/kg, NaN at invalid cells
statisticsPer-tissue mapping keyed by tissue ID: name, cell_count, mass_kg, total_absorbed_power, mean_sar, max_sar
normalizationThe PowerNormalization applied
provenanceAlgorithm and grid metadata (field convention, grid hash, averaging profile, monitor bounds, and more)
frequencies[F] frequencies, Hz
coordinatesMapping with x, y, z coordinate tensors, metres
validBoolean mask of cells where SAR is defined
occupancyTissue occupancy per cell
rho_cellMass density per cell, kg/m³
cell_volumeVolume per cell, m³
tissue_id, tissue_namesTissue ID map and ID-to-name mapping
absorbed_power_densityMapping from channel to absorbed power density, W/m³
averagedMass-averaged SAR per mass (filled when averaging was requested)
peaksSARPeak 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.Tensor

Returns the [F, nx, ny, nz] point SAR for a channel. Raises KeyError for an unknown channel.

SARResult.peak#

SARResult.peak(mass) -> SARPeak

Returns 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.Tensor

Returns 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.Tensor

Returns 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.

temperaturefloatrequired

Softmax temperature in W/kg. Must be positive.

massfloat | Nonedefault None

Averaging mass in kg. Required when several masses were computed.

SARResult.payload#

SARResult.payload() -> dict

Returns 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") -> SARResult

Class 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.

FieldMeaning
mass_targetRequested averaging mass, kg
sar[F] peak averaged SAR, W/kg, NaN where no valid center exists
frequencies[F] frequencies, Hz
indexGrid index of the peak cube center
positionPhysical coordinates of the peak center, metres, shape [F, 3]
mass_kgActual enclosed averaging mass, kg
cube_half_widthCube half-width in cells (edge 2h+1 cells)
cube_size_mPhysical 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,
) -> SARResult

Sums the complex electric fields of several same-frequency runs, then reduces once to SAR. Interference between the sources is captured.

resultsSequence[Result]required

At least two results with the same grid (checked by node-coordinate hash) and monitor frequencies.

monitorstrrequired

Name of the PowerLossMonitor in every run.

weightsSequence[complex] | Nonedefault None

Complex weight per run (relative amplitude and phase). None means all 1.0.

averagingSARAveraging | Nonedefault None

Mass-averaging request.

normalizationPowerNormalization | Nonedefault None

Power 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) -> SARResult

Adds absorbed power from several SARResult objects (power-domain sum), then re-forms point SAR and tissue statistics.

sar_resultsSequence[SARResult]required

At least two results with identical grid hash, field convention, normalization, frequencies, loss channels, tissue map, validity mask, and mass density.

averagingSARAveraging | Nonedefault None

When 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.

FieldShapeMeaning
frequencies[F]Hz
channel_power[F] per channelIntegrated power per channel, W
volume_density[F, N] per channel and componentVolume loss density, W/m³
total[F]Sum of all available channels, W
component_volumes[N] per componentEdge control volumes, m³
global_ids[N] per componentGlobal Yee edge identifiers
monitor_name, boundsSource monitor name and bounds, metres
surface_density, face_areasSurface loss density (W/m²) and face areas (m²)
line_density, line_lengthsLine 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.Tensor

Returns the [F] integrated power of a channel. Raises KeyError when the channel is absent.

PowerLossData.density#

PowerLossData.density(channel, component) -> torch.Tensor

Returns 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=...).

FieldMeaning
monitor, axis, normal_directionMonitor name, plane normal axis, and normal sign
frequenciesTuple of frequencies, Hz
coordinate_names, coordinatesNames and 1D coordinate tensors of the two in-plane axes, metres
normal_poyntingSigned normal Poynting component 0.5 * Re((E x conj(H)) . n) per cell, W/m²
power_densityIts magnitude, W/m²
fluxPlane integral of normal_poynting, W; equals a FluxMonitor on the same plane
spatial_averageSquare-window moving average of power_density, W/m², or None when no window was requested
provenanceConvention 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).

FieldMeaning
peak_field, peak_indexRegion maximum of the per-cell running max of abs(E) (V/m) and its cell index
peak_timeAlways None in this version
exceedance_durationLargest per-cell time with abs(E) at or above the critical field, s
longest_exceedance_durationLargest per-cell contiguous exceedance, s
qualifying_cell_count, qualifying_maskCells whose longest contiguous exceedance reached minimum_duration
exceedance_volume_timeSum of exceedance time times occupancy times cell volume, s·m³
damage_volume, damage_mapDamage integral of (abs(E) / Ecrit)**k while exceeding; None without damage_exponent
max_field_map, exceedance_time_map, longest_exceedance_mapPer-cell maps, on device
occupancy, region_bounds, provenanceTarget-material occupancy, region bounds, and model metadata

Properties: capability_level ("stress-only").

BreakdownStressData.locations#

BreakdownStressData.locations() -> dict

Returns 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.

FieldMeaning
eventsTuple of BreakdownEvent, ordered by (step, cell_index)
final_statePer-node int8 state on the material grid
dissipated_energyPer-node breakdown-dissipated energy, J
total_dissipated_energySum of dissipated_energy, J
grid_shapeMaterial 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.

FieldMeaning
step, timeTime step index and time, s
cell_indexRow-major linear node index on the material grid
positionCell-center coordinates, metres
material_idMaterial identifier
field_beforeCell-center abs(E) at the trigger step, V/m
state_before, state_afterState codes: 0 intact, 1 recovering, 2 conducting, 3 failed
deposited_energy_at_triggerLocal 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.

critical_fieldfloatrequired

Reference field in V/m at the center of the sigmoid. Positive and finite.

widthfloatrequired

Sigmoid width in V/m. Smaller values approach a hard threshold. Positive and finite.

reductionstrdefault 'sum'

"sum" (soft over-stress dose in seconds), "mean", or "softmax" (soft worst cell).

temperaturefloat | Nonedefault None

Softmax temperature in seconds. Required and positive for reduction="softmax".

damage_exponentfloat | Nonedefault None

When 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) -> SmoothBreakdownRiskData

Reduces a cell-center field magnitude series to the risk scalar. Autograd flows through e_magnitude_series.

e_magnitude_seriestorch.Tensorrequired

Field magnitude in V/m, shape (T, ...) with a leading time axis and at least one cell axis.

dtfloatrequired

Uniform sample spacing in seconds. Positive.

occupancytorch.Tensor | Nonedefault None

Per-cell weights in [0, 1], matching the non-time shape.

namestrdefault ''

Label stored on the result.

provenance_extraMapping[str, Any] | Nonedefault None

Extra 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) -> SmoothBreakdownRiskData

Colocates 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.

voltagefloat | Nonedefault None

Peak voltage limit, V.

currentfloat | Nonedefault None

Peak current limit, A.

energyfloat | Nonedefault None

Total energy limit, J.

pulse_widthfloat | Nonedefault None

Pulse-width limit, s.

modelstr | Nonedefault None

Name of the rating source or part revision, stored in provenance.

ComponentRating.as_dict#

ComponentRating.as_dict() -> dict

Returns 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.

FieldMeaning
time, voltage, current1D time series, s, V, A
powerP = V * I, W
cumulative_energyTrapezoidal running integral of P, J
peak_voltage, peak_currentMaximum absolute value
peak_power, total_energyMaximum P (W) and final cumulative energy (J)
measured_pulse_widthTime where P is at least half its peak, s
exceedancePer 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) -> ComponentStressData

Class 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.