Skip to content
Projects

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

NameDescription
FAR_POINTSNo description.
NEAR_FIELDNo description.

Classes

NameDescription
BoundaryIntegralElectrodesConductors held at prescribed potentials, by the single-layer boundary integral.
ConductorA conductor's boundary: a polyline of vertices (2, n) held at one potential.
OutlinedShapeA 2D shape with boundary loops, such as any 2D :mod:simgeometry shape.
TabulatedFieldPotential and field sampled on a regular grid, interpolated bilinearly.

Functions

NameDescription
conductors_from_shapeClosed conductors on the boundary of a 2D :mod:simgeometry shape.
segment_fieldPotential (K,) and field (2, K) of uniformly charged straight segments.

FAR_POINTSattributemodule attribute#

FAR_POINTS: int = 2

NEAR_FIELDattributemodule attribute#

NEAR_FIELD: float = 4.0

BoundaryIntegralElectrodesclass#

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 = None

conductorsattributeinstance 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.ndarray

Field (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.ndarray

Potential (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.ndarray

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

closedattributeclass attributeinstance attribute#

closed: bool = True

potentialattributeinstance attribute#

potential: float

verticesattributeinstance attribute#

vertices: np.ndarray

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

potentialattributeinstance attribute#

potential: np.ndarray

xattributeinstance attribute#

x: np.ndarray

yattributeinstance attribute#

y: np.ndarray

containsmethod#

def contains(points) -> np.ndarray

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