Thin wires
Subgrid thin-wire conductors for FDTD - ThinWire, WireConductor, WireEnd, and the WireData read back from a WireMonitor.
This page covers the thin-wire model in witwin.maxwell: ThinWire, WireConductor, WireEnd, and WireData. Use it for wires much thinner than a grid cell, such as dipole arms, feed lines, and loops. A thin wire is not voxelized. Maxwell keeps its physical radius and couples a current/charge network on the wire to the Yee grid, so you do not need to resolve the radius with the mesh.
Note
Thin wires are experimental and FDTD-only. mw.Simulation.fdfd(...) raises ValueError for a scene that contains a ThinWire.
The workflow has three steps:
- Build each wire as a
ThinWirewith a centerline, a radius, and aWireConductor. - Add the wires to the scene with
Scene(thin_wires=...)orScene.add_thin_wire(wire). Wires are kept apart from volume structures and are read back through the read-onlyScene.thin_wirestuple. Wire names must be unique in a scene. - To read wire current, charge, or loss, add a
WireMonitorthat names the wire. After the run,result.monitor(name)returns aWireData.
To drive a wire, bind a LumpedPort or TerminalPort to wire points with WirePortBinding and WireNodeRef (see Ports).
import torch
import witwin.maxwell as mw
dx = 1.25e-3
arm = torch.linspace(0.5 * dx, 0.024, 21)
negative_arm = mw.ThinWire("negative_arm", tuple((0.0, 0.0, -float(z)) for z in arm.flip(0)),
0.05 * dx, mw.WireConductor.pec(), snap="continuous")
positive_arm = mw.ThinWire("positive_arm", tuple((0.0, 0.0, float(z)) for z in arm),
0.05 * dx, mw.WireConductor.pec(), snap="continuous")
feed = mw.LumpedPort("feed", wire_binding=mw.WirePortBinding.nodes(
negative=mw.WireNodeRef("negative_arm", 20), positive=mw.WireNodeRef("positive_arm", 0)))
scene = mw.Scene(
domain=mw.Domain(bounds=((-0.05, 0.05),) * 3), grid=mw.GridSpec.uniform(dx),
boundary=mw.BoundarySpec.pml(num_layers=8),
thin_wires=(negative_arm, positive_arm), ports=(feed,),
monitors=(mw.WireMonitor("arm_state", "positive_arm", frequencies=(3.0e9,)),),
)ThinWire#
class ThinWire(name, points, radius, conductor, endpoints=None, snap='strict')Immutable centerline definition of one thin wire. Each pair of consecutive points is one wire segment. Tensor radius keeps its autograd graph, so the radius can be trained. Tensor points keep coordinate gradients only with snap="continuous"; the compiled cell stencil itself stays a fixed, discrete choice. ThinWire compares by identity, not by value.
Non-empty name, unique within the scene. Surrounding whitespace is stripped. Monitors and port bindings refer to the wire by this name.
Centerline points in metres: a sequence of 3D coordinates or a real tensor of shape [P, 3], with at least 2 points. Consecutive points must differ. To make a closed loop, repeat the first point as the last one; a loop needs at least 3 segments. No other point may repeat.
Physical wire radius in metres, positive and finite. Give one value for the whole wire, or one value per segment (a sequence of length S or a tensor of shape [S], where S is the number of segments).
Conductor law: WireConductor.pec() or WireConductor.finite(conductivity).
NoneBoundary conditions at the first and last point. None gives two open ends for an open wire and no endpoints for a closed loop. A closed loop must not set endpoints; an open wire needs exactly two WireEnd values.
'strict'How points map to the grid. 'strict' requires every point to lie on a grid node and raises ValueError otherwise. 'nearest' moves each coordinate to the nearest grid node. 'continuous' keeps the coordinates as given. Trainable tensor points (with requires_grad=True) require 'continuous'.
Raises TypeError or ValueError for any violated rule above. Further checks run when the scene is compiled: the wire must stay inside the physical domain (not in the PML), and a segment must not collapse to zero length after snapping.
ThinWire properties#
| Property | Returns |
|---|---|
segment_count | Number of segments, len(points) - 1. |
is_closed | True when the first and last points are equal. |
loop = mw.ThinWire(
"loop",
((0.0, 0.0, 0.0), (0.01, 0.0, 0.0), (0.01, 0.01, 0.0), (0.0, 0.01, 0.0), (0.0, 0.0, 0.0)),
5.0e-4,
mw.WireConductor.pec(),
)
assert loop.is_closed and loop.endpoints == ()Tip
Keep the radius small compared with the cell. The thin-wire numerical contract gives a validity band of radius / transverse cell size at most 0.2. The compiler reports validity metadata (see WireData.metadata["validity"]) and never falls back to voxelizing the wire.
WireConductor#
class WireConductor(kind='pec', *, conductivity=None, permeability=None)Frozen conductor law for a thin wire. Use the two class methods below rather than the constructor.
'pec''pec' for a lossless perfect conductor, or 'finite' for a solid round conductor.
NoneBulk conductivity in S/m. Required for 'finite', must be positive and finite. Not allowed for 'pec'.
NonePermeability in H/m for 'finite'. None means vacuum permeability. Not allowed for 'pec'.
The compiler turns a finite conductor into a per-unit-length series impedance (DC resistance plus a passive skin-effect model).
WireConductor.pec#
WireConductor.pec() -> WireConductor # classmethodReturns a lossless perfect-conductor law.
WireConductor.finite#
WireConductor.finite(conductivity, *, permeability=None) -> WireConductor # classmethodReturns a solid round conductor with bulk conductivity (S/m) and permeability (H/m, vacuum by default).
copper = mw.WireConductor.finite(5.8e7)
print(copper.kind, copper.conductivity) # finite 58000000.0Warning
Finite-conductor wires run forward only. Gradients through the field-coupled solve, checkpoint/resume, and multi-GPU runs fail closed for a finite conductor.
WireEnd#
class WireEnd(kind='open', *, structure=None, node_name=None)Frozen boundary condition for one end of an open wire. Use the class methods.
'open''open', 'grounded', or 'node'.
NoneFor 'grounded' only: name of the PEC Structure the end connects to.
NoneFor 'node' only: name of a junction shared with other wire ends. Names starting with __closed__: are reserved.
WireEnd.open#
WireEnd.open() -> WireEnd # classmethodA free end.
WireEnd.grounded#
WireEnd.grounded(*, structure) -> WireEnd # classmethodAn end connected to a PEC structure. At compile time the structure must exist in the scene, have a unique name, be enabled, use a PEC material, and contain the wire end point.
WireEnd.node#
WireEnd.node(name) -> WireEnd # classmethodA named junction. Ends with the same node name merge into one electrical node, which lets you build branched wires. At compile time every end that uses a node name must resolve to the same grid coordinate, and a name needs at least two ends. Two wires that touch at a point without one shared node name raise ValueError.
stub = mw.ThinWire(
"stub",
((0.0, 0.0, 0.0), (0.0, 0.0, 0.01)),
5.0e-4,
mw.WireConductor.pec(),
endpoints=(mw.WireEnd.grounded(structure="ground"), mw.WireEnd.node("junction")),
)WireData#
class WireData(monitor_name, wire_name, frequencies, current, charge, ohmic_loss, metadata={})Frozen frequency-domain state of one wire, returned by result.monitor(name) for a WireMonitor. Tensors stay on the run device and keep any live autograd graph. Quantities that the monitor did not request are None.
| Field | Shape | Meaning |
|---|---|---|
monitor_name | str | Name of the WireMonitor. |
wire_name | str | Name of the monitored wire. |
frequencies | [F] | Frequencies in Hz, real, finite, strictly positive. |
current | [F, S] or None | Complex segment current phasors, one per segment. |
charge | [F, N] or None | Complex node charge phasors, one per node. |
ohmic_loss | [F, S] or None | Real, non-negative dissipated power per segment. Exactly zero for PEC wires. |
metadata | dict | Run information (keys below). |
For an FDTD run, metadata holds quantities, segment_ids, node_ids, segment_positions and node_positions (midpoints and node coordinates in metres), phasor_convention (peak phasor, exp(-i*omega*t)), the time-step limits wire_cfl_limit, uncoupled_wire_cfl_limit, maxwell_cfl_limit, the flag time_step_adjusted, and validity.
At least one of current, charge, ohmic_loss must be present. current and charge must share one complex dtype that matches the precision of frequencies. ohmic_loss must match the shape of current when both are present.
Property device returns the device of frequencies. WireData has an explicit frequency axis, so result.monitor(name, freq_index=...) and result.at(freq_index=...).monitor(name) raise ValueError; index the tensors yourself. Result.save stores a detached CPU copy, and Result.load restores it as WireData.
pulse = mw.GaussianPulse(frequency=3.0e9, fwidth=1.5e9)
result = mw.Simulation.fdtd(
scene, frequencies=[3.0e9], excitations=mw.PortExcitation("feed", source_time=pulse)
).run()
state = result.monitor("arm_state")
feed_current = state.current[0, 0] # first segment, first frequency
total_charge = state.charge.sum(dim=-1)