tree_d_3d.electrodes
Conductors at prescribed potentials, by the boundary-integral (panel) method.
A conductor’s surface carries an induced line-charge density whose potential, added to
the free charges’ potential, makes the conductor an equipotential: the single-layer
formulation of :mod:tree_d_3d’s research note “10 Boundary-integral electrodes”
(obsidian-notes/notes/tree-plasma-ideas). Discretising a conductor into straight
panels and collocating at panel midpoints turns this into a small dense linear system
S Q = g: S couples panel charges to panel potentials, a panel’s own column uses
the closed-form potential of a uniformly charged straight segment at its own midpoint
(finite, unlike a point charge’s self-potential). A panel close to its target — within
near_field of its own lengths, another panel’s midpoint or a probe point — also
enters with that exact segment potential and field (:func:segment_field); farther
panels are point line charges at their midpoints, the usual flat-panel approximation,
whose error falls as (L / r)**2 and is summed fast by :func:tree_d_3d.direct_field.
Without the near-field correction the potential between collocation points is off by
about a tenth of the source scale next to a panel and the method converges at first
order; with it the boundary is an equipotential to within the piecewise-constant charge
density’s own error.
Panel counts here are the hundreds to low thousands the validation examples need, so the
system is solved directly, by an LU factorisation done once per geometry and reused by
every :meth:BoundaryIntegralElectrodes.solve; the note’s plan of an FMM-accelerated
GMRES solve, needed once panel and particle counts both grow large, is not implemented.
The log kernel is the one :class:tree_d_3d.FlatQuadtree and :func:tree_d_3d.direct_field
use with kernel="log": potential -2 q ln r and field 2 q d / r**2 per unit
charge, without k_e (applied here, as in :class:tree_d_3d.GroundedStripField).
Attributes
| Name | Description |
|---|---|
FAR_POINTS | No description. |
NEAR_FIELD | No description. |
Classes
| Name | Description |
|---|---|
BoundaryIntegralElectrodes | Conductors held at prescribed potentials, by the single-layer boundary integral. |
Conductor | A conductor's boundary: a polyline of vertices (2, n) held at one potential. |
OutlinedShape | A 2D shape with boundary loops, such as any 2D :mod:simgeometry shape. |
TabulatedField | Potential and field sampled on a regular grid, interpolated bilinearly. |
Functions
| Name | Description |
|---|---|
conductors_from_shape | Closed conductors on the boundary of a 2D :mod:simgeometry shape. |
segment_field | Potential (K,) and field (2, K) of uniformly charged straight segments. |
FAR_POINTSattributemodule attribute#
FAR_POINTS: int = 2NEAR_FIELDattributemodule attribute#
NEAR_FIELD: float = 4.0BoundaryIntegralElectrodesclass#
class BoundaryIntegralElectrodes(conductors: Sequence[Conductor], *, k_e: float = K_E, near_field: float = NEAR_FIELD, far_points: int = FAR_POINTS)Conductors held at prescribed potentials, by the single-layer boundary integral.
conductors is a sequence of :class:Conductor. Call :meth:solve once per
configuration of free charges (or with none, for a pure capacitance problem); then
:meth:probe (and :meth:potential, :meth:field) give the total field —
electrodes plus free charges — at arbitrary points, and :meth:force_on_sources
the force the electrodes and the other free charges exert back on the free charges.
Panels within near_field of their own lengths of a target use the exact field of
a uniformly charged segment (:func:segment_field); farther ones are summed fast as
far_points Gauss—Legendre point charges each, whose error falls as
(L / r) ** (2 * far_points). near_field=0, far_points=1 gives the plain
midpoint rule.
chargeattributeinstance attribute#
charge: np.ndarray | None = Noneconductorsattributeinstance attribute#
conductors = list(conductors)endsattributeinstance attribute#
ends = np.concatenate(ends, axis=1)far_pointsattributeinstance attribute#
far_points = int(far_points)groupattributeinstance attribute#
group = np.concatenate(group)k_eattributeinstance attribute#
k_e = float(k_e)lengthsattributeinstance attribute#
lengths = np.concatenate(lengths)midpointsattributeinstance attribute#
midpoints = np.concatenate(midpoints, axis=1)near_fieldattributeinstance attribute#
near_field = float(near_field)num_panelsattributeinstance attribute#
num_panels = self.midpoints.shape[1]startsattributeinstance attribute#
starts = np.concatenate(starts, axis=1)target_potentialattributeinstance attribute#
target_potential = np.concatenate(target)fieldmethod#
def field(points) -> np.ndarrayField (V/m, shape (2, M)) at points (2, M); see :meth:probe.
force_on_sourcesmethod#
def force_on_sources() -> tuple[np.ndarray, np.ndarray]Potential and field (V/m, shape (2, N)) at the free charges from everything else.
Excludes each source’s own singular self-term, so multiplying the field by a
source’s charge gives the physical force on it from the panels and the other
sources. Call :meth:solve first.
panel_fieldmethod#
def panel_field(points) -> tuple[np.ndarray, np.ndarray]Potential (V) and field (V/m) at points (2, M) of the panels alone.
The induced charge from the last :meth:solve, without the free charges passed
to it: for codes that add the free charges’ field themselves (by a tree code,
say). Near panels enter exactly, as in :meth:probe.
potentialmethod#
def potential(points) -> np.ndarrayPotential (V) at points (2, M); see :meth:probe.
probemethod#
def probe(points) -> tuple[np.ndarray, np.ndarray]Potential (V) and field (V/m, shape (2, M)) at points (2, M).
Includes the solved panel charges and the free charges passed to :meth:solve;
points are probes, not sources, so no self term applies. Call :meth:solve
first.
solvemethod#
def solve(source_positions=None, source_charges=None) -> np.ndarraySolve for the panel charges that make every conductor an equipotential.
source_positions (2, N) and source_charges (N,) are free line
charges (omit both for a pure capacitance problem). Returns and caches the
panel charges, used by :meth:probe and :meth:force_on_sources.
tabulatemethod#
def tabulate(lower, upper, shape) -> TabulatedField:meth:probe on a regular grid, for fast bilinear interpolation afterwards.
lower and upper are the corners (x, y) of the rectangle and
shape (nx, ny) the number of grid lines along each axis (at least 2).
Like :meth:probe it includes the free charges passed to :meth:solve, so
tabulate a static configuration and scale it, for example by an RF voltage.
total_chargemethod#
def total_charge(conductor_index: int | None = None)Induced charge of one conductor, or of every conductor if index is None.
Conductorclassdataclass#
class Conductor(vertices: np.ndarray, potential: float, closed: bool = True)A conductor’s boundary: a polyline of vertices (2, n) held at one potential.
Consecutive vertices are panel endpoints; closed connects the last vertex back
to the first (a loop), giving n panels instead of n - 1.
OutlinedShapeclass#
class OutlinedShape(Protocol)Bases: Protocol
A 2D shape with boundary loops, such as any 2D :mod:simgeometry shape.
outlinesmethod#
def outlines(max_spacing=None) -> list[np.ndarray]TabulatedFieldclassdataclass#
class TabulatedField(x: np.ndarray, y: np.ndarray, potential: np.ndarray, field: np.ndarray)Potential and field sampled on a regular grid, interpolated bilinearly.
x (nx,) and y (ny,) are the grid lines, potential (nx, ny)
and field (2, nx, ny) the samples, as :meth:BoundaryIntegralElectrodes.tabulate
makes them. Calling it with (2, M) points returns the interpolated potential
(M,) and field (2, M); points outside the grid take the value at the
nearest edge (see :meth:contains).
fieldattributeinstance attribute#
field: np.ndarraypotentialattributeinstance attribute#
potential: np.ndarrayxattributeinstance attribute#
x: np.ndarrayyattributeinstance attribute#
y: np.ndarraycontainsmethod#
def contains(points) -> np.ndarrayMask of the (2, M) points inside the tabulated rectangle.
conductors_from_shapefunction#
def conductors_from_shape(shape: OutlinedShape, potential: float, max_panel_length: float | Callable[[np.ndarray], np.ndarray]) -> list[Conductor]Closed conductors on the boundary of a 2D :mod:simgeometry shape.
One :class:Conductor per boundary loop of shape.outlines(max_panel_length)
(an Aperture2D gives two, one per plate), all held at potential; an open
curve such as a Polyline (a plate of zero thickness) gives an open conductor. Panels are
at most max_panel_length long; it can also be a function of (2, m) points
returning (m,) lengths, to grade the panels (short where the field varies
fastest, such as an aperture tip). Keep panels shorter than the gaps between
surfaces, a plate’s thickness included, so that the charge on one face does not
have to resolve the field of the facing one.
segment_fieldfunction#
def segment_field(points, start, end) -> tuple[np.ndarray, np.ndarray]Potential (K,) and field (2, K) of uniformly charged straight segments.
Pairs column by column: points, start and end are (2, K), and segment
k from start[:, k] to end[:, k] carries unit total charge spread evenly
along it, in the log kernel of :func:tree_d_3d.direct_field (a point charge q
has potential -2 q ln r), without k_e. With u and v the target’s
coordinates along and across the segment of length L from its start,
phi = -(2 / L) [u ln r_a - (u - L) ln r_b - L + |v| theta],
E = (2 / L) [ln(r_a / r_b) t + sign(v) theta n],
where r_a and r_b are the distances to the two ends, theta the angle the
segment subtends at the target, and t and n the unit tangent and normal. On
the segment itself the normal field (a jump of the surface charge) is taken as zero;
at an end point the potential is finite but the field diverges logarithmically.