Ports
RF port declarations, port excitations and sweeps, and lumped R, L, C elements and terminations for single-device FDTD runs.
This page covers the port classes in witwin.maxwell (import witwin.maxwell as mw): two-terminal LumpedPort and TerminalPort, the modal WavePort, the experimental ModePort, thin-wire port bindings, the PortExcitation and PortSweep run requests, and the lumped Resistor, Capacitor, Inductor, SeriesRLC, and ParallelRLC elements. You need it when you drive or measure a device through terminals or a waveguide aperture and want voltage, current, power waves, or an S-matrix.
All port data uses peak complex phasors with time dependence exp(-i*omega*t). Voltage is the line integral of E from the negative terminal to the positive terminal. Current is the right-hand circulation of H around the current surface with the same orientation. Average power is 0.5 * Re(V * conj(I)). Reversing a port flips the sign of V, I, a, and b together; impedance, power, and S-parameters do not change.
Ports in a scene#
You declare ports on a Scene, then request an excitation when you build the FDTD Simulation:
| Step | Call |
|---|---|
| Declare a port | Scene(ports=(...)) or scene.add_port(port) |
| Declare a standalone R, L, or C | Scene(lumped_elements=(...)) or scene.add_lumped_element(element) |
| Drive one or more ports | mw.Simulation.fdtd(scene, ..., excitations=mw.PortExcitation(...)) |
| Measure an N-port S-matrix | mw.Simulation.fdtd(scene, ..., excitations=mw.PortSweep()) |
| Read one port | result.port(name) returns a PortData |
| Read the S-matrix | result.network returns a NetworkData, or None |
Scene.add_port accepts ModePort, LumpedPort, TerminalPort, and WavePort. Port names must be unique in a scene. It resolves a TerminalPort against the scene structures at this point, and it rejects a LumpedPort whose terminals lie outside the scene domain. RF port excitation works with Simulation.fdtd(...) only; FDFD raises ValueError.
import witwin.maxwell as mw
feed = mw.LumpedPort(
"feed",
positive=(0.0, 0.0, 0.5e-3),
negative=(0.0, 0.0, -0.5e-3),
voltage_path=mw.AxisPath("z"),
current_surface=mw.Box(position=(0.0, 0.0, -0.25e-3), size=(1.0e-3, 1.0e-3, 0.0)),
reference_impedance=50.0,
)
scene = mw.Scene(
domain=mw.Domain(bounds=((-2e-3, 2e-3),) * 3),
grid=mw.GridSpec.uniform(0.25e-3),
boundary=mw.BoundarySpec.pml(num_layers=4),
ports=(feed,),
)
result = mw.Simulation.fdtd(
scene,
frequency=10e9,
excitations=mw.PortExcitation("feed", source_time=mw.CW(10e9)),
run_time=mw.TimeConfig(time_steps=1000),
).run()
port = result.port("feed")
print(port.voltage, port.current, port.accepted_power)See RF ports for a task-oriented walkthrough.
LumpedPort#
class LumpedPort(name, *, positive=None, negative=None, voltage_path=None,
current_surface=None, reference_impedance=50.0,
reference_plane=None, termination=None, wire_binding=None)Two-terminal port with explicit voltage path and current surface. The object is a frozen dataclass.
Port name. Must not be empty.
NonePositive terminal point in metres. Required unless wire_binding is set.
NoneNegative terminal point in metres. It must differ from positive only along voltage_path.axis.
NoneAxis of the straight voltage integration path. Required unless wire_binding is set.
NoneA witwin.core.Box (exported as mw.Box) around which H is circulated to get the current. Required unless wire_binding is set.
50.0Reference impedance Z0 in ohms for the power waves. The real part must be positive and finite. A tensor must hold exactly one value. A complex value with zero imaginary part is stored as float. PortSweep and time-domain source resistance require a real value.
NoneOptional reference-plane coordinate in metres, stored as float.
NonePassive load placed across the port when it is not driven. PortSweep rejects ports that carry a termination.
NoneBind the port to thin-wire nodes instead of field geometry. When set, positive, negative, voltage_path, and current_surface must all be None.
The instance also carries read-only kind ("lumped_port"), phasor_convention ("peak"), and power_convention ("0.5*Re(V*conj(I))").
Raises TypeError when voltage_path is not an AxisPath, current_surface is not a Box, termination has the wrong type, or wire_binding is not a WirePortBinding. ValueError when the terminals coincide, are not aligned with voltage_path.axis, the name is empty, or reference_impedance is invalid.
load = mw.LumpedPort(
"load",
positive=(1e-3, 0.0, 0.5e-3),
negative=(1e-3, 0.0, -0.5e-3),
voltage_path=mw.AxisPath("z"),
current_surface=mw.Box(position=(1e-3, 0.0, 0.0), size=(1e-3, 1e-3, 0.0)),
termination=mw.SeriesRLC(r=50.0),
)AxisPath#
class AxisPath(axis)Axis-aligned voltage integration path. It fixes only the Cartesian axis; the owning port's negative and positive terminals set the direction.
"x", "y", or "z". Case and surrounding spaces are ignored.
Raises TypeError if axis is not a string, ValueError for any other axis name.
TerminalPort#
class TerminalPort(name, positive_terminal=None, negative_terminal=None,
integration_path=None, reference_plane=None,
reference_impedance=50.0, termination=None, wire_binding=None)Two-conductor port whose geometry comes from two named PEC Box structures in the scene. When you add it to a scene, Maxwell places the voltage path through the center of the overlap of the two terminals' footprints (transverse to the path axis) and places the current contour on that overlap at reference_plane.
Port name. Must not be empty.
NoneStructure used as the positive conductor. Required unless wire_binding is set.
NoneStructure used as the negative conductor. Must reference a different structure.
NoneAxis along which the two terminals face each other. Required unless wire_binding is set.
NoneCoordinate along the path axis, in metres, where the current contour sits. Required and finite for structure terminals; it must lie strictly between the two facing terminal surfaces. Optional with wire_binding.
50.0Reference impedance in ohms, validated as for LumpedPort.
NonePassive load used when the port is not driven.
NoneBind the port to thin-wire nodes. When set, positive_terminal, negative_terminal, and integration_path must be None.
Each terminal structure must exist with a unique name, be enabled, use an unrotated Box geometry with positive finite extents inside the scene domain, and use a PEC material. The two footprints must overlap across the path, and the two boxes must not overlap along the path axis. Scene.add_port raises ValueError with the port name when any of these checks fails.
scene = mw.Scene(
domain=mw.Domain(bounds=((0.0, 1.0), (0.0, 1.0), (0.0, 1.0))),
grid=mw.GridSpec.uniform(0.1),
structures=(
mw.Structure(geometry=mw.Box(position=(0.5, 0.5, 0.8), size=(0.5, 0.5, 0.2)),
material=mw.Material.pec(), name="signal"),
mw.Structure(geometry=mw.Box(position=(0.5, 0.5, 0.2), size=(0.5, 0.5, 0.2)),
material=mw.Material.pec(), name="ground"),
),
ports=(mw.TerminalPort("feed", mw.TerminalRef("signal"), mw.TerminalRef("ground"),
mw.AxisPath("z"), 0.45),),
)TerminalPort.voltage_path#
TerminalPort.voltage_path -> AxisPath | NoneAlias of integration_path.
TerminalPort.positive, TerminalPort.negative, TerminalPort.current_surface#
TerminalPort.positive -> tuple[float, float, float]
TerminalPort.negative -> tuple[float, float, float]
TerminalPort.current_surface -> BoxResolved terminal points and current contour. They are available only on the port stored in the scene (scene.ports), because Scene.add_port stores a resolved copy.
Raises RuntimeError on a port that has not been added to a scene.
TerminalRef#
class TerminalRef(structure_name)Reference to one uniquely named conductor Structure in the scene.
Name of the structure. Surrounding spaces are stripped; it must not be empty.
WavePort#
class WavePort(name, position, size, direction, reference_plane, modes)Axis-aligned modal aperture for RF waveguide ports. Declaring it does not solve any mode; a PortExcitation or PortSweep solves, normalizes, and tracks the declared modes across frequency.
Port name. Must not be empty or contain ::.
Aperture center in metres.
Aperture size in metres. Exactly one component must be zero; it sets the normal axis. The two tangential components must be positive.
"+" or "-": the propagation direction into the device along the normal axis.
Reference-plane coordinate in metres. It must equal the aperture position along the normal axis.
At least one WaveModeSpec. Mode names must be unique and each mode polarization must be tangential to the aperture.
The instance also exposes normal_axis ("x", "y", or "z"), direction_sign (1 or -1), and kind ("wave_port").
Limits: a direct excitation of a WavePort must be the only PortExcitation in the run, must use source_impedance="matched" and source_time=None, and cannot use a trainable amplitude. A WavePort cannot connect to a NetworkBlock. In a WavePort run, lumped and terminal ports are allowed only when they carry a termination, and the scene must have no independent field sources.
wave_port = mw.WavePort(
"input",
position=(-5e-3, 0.0, 0.0),
size=(0.0, 10e-3, 5e-3),
direction="+",
reference_plane=-5e-3,
modes=(mw.WaveModeSpec("te", polarization="Ez"),),
)WavePort.mode_name#
WavePort.mode_name(mode: WaveModeSpec) -> strReturn the qualified channel name "port::mode" used for this mode in NetworkData.port_names.
Raises ValueError if the port does not declare mode.
WaveModeSpec#
class WaveModeSpec(family, mode_index=0, *, name=None, polarization='auto',
impedance_definition=None, voltage_path=None, current_contour=None)Selects one waveguide mode for a WavePort and fixes how its characteristic impedance is defined.
"tem", "te", "tm", or "hybrid".
0Mode index within the family. Must be 0 or greater.
NoneMode name. Defaults to the upper-case family plus the index, for example "TE0". Must not contain ::.
'auto'"auto", "Ex"/"Ey"/"Ez", "x"/"y"/"z", or an axis-aligned vector.
NoneHow the characteristic impedance is computed. See the table below.
None(negative_point, positive_point), each a 3-tuple in metres, forming a nonzero axis-aligned path.
NonePlanar, unrotated Box around which H is circulated (right-hand rule).
family | Allowed impedance_definition | Formula (impedance_formula) | Geometry |
|---|---|---|---|
tem | voltage_current (default) | V/I | needs voltage_path and current_contour |
te | te_wave (default) | omega*mu/beta | no V/I geometry allowed |
tm | tm_wave (default) | beta/(omega*epsilon) | no V/I geometry allowed |
hybrid | must be given: voltage_current, power_voltage, or power_current | V/I, abs(V)**2/(2*P), 2*P/abs(I)**2 | voltage_path for the first two, current_contour for voltage_current and power_current |
Raises ValueError for an unknown family, a negative index, a definition that does not match the family, or missing or forbidden V/I geometry. TypeError if current_contour is not a Box.
ModePort#
Experimental
ModePort, ModeSource, and ModeMonitor are experimental. Their behavior may change.
class ModePort(name, position=(0.0, 0.0, 0.0), size=(1.0, 1.0, 0.0), mode_index=0,
direction='+', polarization='auto', frequencies=None, source_time=None,
monitor_offset=0.0, normal_direction=None, bend_radius=None, bend_axis=None)Couples an optional ModeSource and a ModeMonitor on one aperture. The scene turns it into a source named "<name>::source" (only when source_time is set) and a mode monitor named name. See sources and monitors.
Port name; also the monitor name.
(0.0, 0.0, 0.0)Aperture center in metres.
(1.0, 1.0, 0.0)Aperture size in metres. Exactly one component must be zero; components must be 0 or greater.
0Mode index within the selected polarization family. Must be 0 or greater.
'+'"+" or "-": launch and measurement direction.
'auto'"auto" picks the larger tangential axis. Otherwise "Ex"/"Ey"/"Ez", "x"/"y"/"z", or an axis-aligned vector tangential to the aperture. Stored as a unit vector.
NoneMonitor frequencies in hertz, stored as a tuple.
NoneSource waveform. None makes a monitor-only port.
0.0Shift of the monitor plane from the source plane along the normal axis, in metres.
NoneFlux sign of the monitor, "+" or "-". Defaults to direction.
NoneSigned, finite, nonzero bend radius in metres for a curved waveguide. Requires bend_axis.
NoneCylinder axis of the bend, tangential to the aperture. Requires bend_radius.
port = mw.ModePort("port", size=(0.0, 0.5, 0.5), polarization="Ez",
source_time=mw.CW(frequency=1e9))ModePort.source_name#
ModePort.source_name -> strName of the generated source, "<name>::source".
ModePort.to_mode_source#
ModePort.to_mode_source() -> ModeSource | NoneBuild the ModeSource for this port, or return None when source_time is None.
ModePort.to_mode_monitor#
ModePort.to_mode_monitor() -> ModeMonitorBuild the ModeMonitor for this port, shifted by monitor_offset along the normal axis.
WireNodeRef#
class WireNodeRef(wire, point)Reference to one declared point of a ThinWire polyline.
Thin-wire name. Must not be empty.
Index into the wire's declared points. A bool is rejected.
WirePortBinding#
class WirePortBinding(kind, *, negative=None, positive=None)Binds a LumpedPort or TerminalPort to thin-wire nodes. Use the nodes and gap class methods instead of the constructor.
"nodes" or "gap".
Negative terminal.
Positive terminal. Must differ from negative.
Raises ValueError for another kind, equal terminals, or a gap whose terminals are on different wires. TypeError if a terminal is not a WireNodeRef.
WirePortBinding.nodes#
WirePortBinding.nodes(*, negative: WireNodeRef, positive: WireNodeRef) -> WirePortBindingBind the port across two wire nodes. The nodes may be on different wires.
WirePortBinding.gap#
WirePortBinding.gap(*, negative: WireNodeRef, positive: WireNodeRef) -> WirePortBindingBind the port to a removed feed gap between two points of one wire.
wire = mw.ThinWire(
name="dipole",
points=((-0.08, -0.08, 0.0), (-0.04, -0.04, 0.0), (0.04, 0.04, 0.0), (0.08, 0.08, 0.0)),
radius=2.0e-3,
conductor=mw.WireConductor.pec(),
endpoints=(mw.WireEnd.open(), mw.WireEnd.open()),
)
port = mw.LumpedPort(
"feed",
wire_binding=mw.WirePortBinding.gap(
negative=mw.WireNodeRef("dipole", 1),
positive=mw.WireNodeRef("dipole", 2),
),
reference_impedance=50.0,
)PortExcitation#
class PortExcitation(port_name, amplitude=1.0, source_impedance='matched',
source_time=None, mode_name=None)Drives one port in a Simulation.fdtd(..., excitations=...) run. Pass one PortExcitation or an iterable of them; each port may appear at most once.
Name of a LumpedPort, TerminalPort, or WavePort in the scene.
1.0Scalar source amplitude. A tensor must be 0-dimensional and finite.
'matched'Generator impedance in ohms. "matched" uses the port's reference_impedance. Other values need a positive real part; the time-domain source needs a real value.
NoneCW, GaussianPulse, or RickerWavelet. None uses a CW at the first simulation frequency. CustomSourceTime is not supported. Must be None for a WavePort.
NoneWavePort only: the mode to drive, as the local name ("TE0") or the qualified name ("input::TE0"). Defaults to the first declared mode.
A port bound to a Circuit or connected to a NetworkBlock cannot also be excited. Several excitations in one run are supported for LumpedPort and TerminalPort only.
PortSweep#
class PortSweep(ports=None, amplitude=1.0, source_time=None)Requests one independent single-device FDTD run per input port and assembles the results into result.network. Pass it as excitations=.
NonePorts to drive, in matrix order. None drives every RF port in the scene (every WavePort in a WavePort sweep). Names must be unique and non-empty.
1.0Scalar source amplitude. A trainable amplitude raises NotImplementedError.
NoneWaveform for lumped and terminal sweeps. None uses a GaussianPulse centered on the middle of the frequency band, with width equal to the band span or half the lowest frequency, whichever is larger. Must be None for a WavePort sweep, which uses calibrated per-frequency CW runs.
Rules for a lumped or terminal sweep: simulation frequencies must be positive and strictly increasing; every RF port needs a real scalar reference_impedance; no port may carry a termination (the sweep loads inactive ports with a matched SeriesRLC(r=Z0) internally); ports bound to a Circuit are rejected. The result holds result.network with port_names in sweep order and result.ports with a leading excitation axis.
For a WavePort sweep, result.network.port_names are qualified channel names such as "input::TE0".
result = mw.Simulation.fdtd(
scene,
frequencies=(8e9, 10e9, 12e9),
excitations=mw.PortSweep(),
run_time=mw.TimeConfig(time_steps=2000),
).run()
s = result.network.s # complex, shape [F, N, N], order [frequency, out, in]
z = result.network.to_z()SeriesRLC#
class SeriesRLC(r=None, l=None, c=None)Series R-L-C load for a port termination. Values are stored as scalar tensors, so they may be trainable.
NoneResistance in ohms. Zero is treated as absent.
NoneInductance in henries. Zero is treated as absent.
NoneCapacitance in farads. Zero is treated as absent.
At least one value must be positive. Values must be real, finite, non-negative, and on one device.
SeriesRLC.impedance#
SeriesRLC.impedance(angular_frequency) -> torch.TensorComplex impedance r + j*omega*l + 1/(j*omega*c) over the present components. angular_frequency is a real value or tensor in rad/s.
SeriesRLC.resonance_frequency#
SeriesRLC.resonance_frequency() -> torch.TensorReturn 1 / (2*pi*sqrt(l*c)) in hertz.
Raises ValueError if l or c is absent.
ParallelRLC#
class ParallelRLC(r=None, l=None, c=None)Parallel R-L-C load for a port termination. Arguments and validation match SeriesRLC.
ParallelRLC.impedance#
ParallelRLC.impedance(angular_frequency) -> torch.TensorComplex impedance 1 / (1/r + 1/(j*omega*l) + j*omega*c) over the present components.
ParallelRLC.resonance_frequency#
ParallelRLC.resonance_frequency() -> torch.TensorReturn 1 / (2*pi*sqrt(l*c)) in hertz. Raises ValueError if l or c is absent.
Resistor#
class Resistor(name, positive, negative, resistance)Two-terminal resistor. With 3D-point terminals it is a scene element (Scene.add_lumped_element); with CircuitNode terminals it is a Circuit device.
Element name. Must be unique among the scene's lumped elements.
Positive terminal: a point in metres, or a circuit node.
Negative terminal. Both terminals must be the same kind and distinct.
Resistance in ohms. Real, finite, and positive; stored as a scalar tensor.
Scene.add_lumped_element also requires both points inside the domain and differing along exactly one axis.
scene.add_lumped_element(
mw.Resistor("R1", positive=(0.0, 0.0, 0.5e-3), negative=(0.0, 0.0, -0.5e-3), resistance=50.0)
)Resistor members#
Resistor.value -> torch.Tensor # same as resistance
Resistor.terminals -> tuple # (positive, negative)
Resistor.parameters -> dict # {"resistance": tensor}
Resistor.initial_condition -> None
Resistor.impedance(angular_frequency) -> torch.Tensorimpedance returns the resistance as a complex tensor broadcast to the shape of angular_frequency.
Capacitor#
class Capacitor(name, positive, negative, capacitance)Two-terminal capacitor. Terminals and placement rules match Resistor.
Capacitance in farads. Real, finite, and positive.
Members match Resistor: value, terminals, parameters ({"capacitance": ...}), initial_condition, and impedance(angular_frequency), which returns 1/(j*omega*C).
Inductor#
class Inductor(name, positive, negative, inductance)Two-terminal inductor. Terminals and placement rules match Resistor.
Inductance in henries. Real, finite, and positive.
Members match Resistor: value, terminals, parameters ({"inductance": ...}), initial_condition, and impedance(angular_frequency), which returns j*omega*L.
Note
Differentiable runs support real-field, finite, positive series R/L/C branches on LumpedPort, TerminalPort, and standalone elements. ParallelRLC in a differentiable circuit run, trainable source or reference impedance, and a differentiable lumped PortSweep raise an error instead of returning detached values.