# tree_d_3d.electrodes

*module*

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`).

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L1-L1)

## tree_d_3d.electrodes.FAR_POINTS

*attribute* · *module attribute*

```python
FAR_POINTS: int = 2
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L48-L48)

## tree_d_3d.electrodes.NEAR_FIELD

*attribute* · *module attribute*

```python
NEAR_FIELD: float = 4.0
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L46-L46)

## tree_d_3d.electrodes.BoundaryIntegralElectrodes

*class*

```python
class BoundaryIntegralElectrodes
```

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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L196-L477)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.charge

*attribute* · *instance attribute*

```python
charge: np.ndarray | None = None
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L273-L273)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.conductors

*attribute* · *instance attribute*

```python
conductors = list(conductors)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L229-L229)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.ends

*attribute* · *instance attribute*

```python
ends = np.concatenate(ends, axis=1)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L252-L252)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.far_points

*attribute* · *instance attribute*

```python
far_points = int(far_points)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L228-L228)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.group

*attribute* · *instance attribute*

```python
group = np.concatenate(group)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L254-L254)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.k_e

*attribute* · *instance attribute*

```python
k_e = float(k_e)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L226-L226)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.lengths

*attribute* · *instance attribute*

```python
lengths = np.concatenate(lengths)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L249-L249)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.midpoints

*attribute* · *instance attribute*

```python
midpoints = np.concatenate(midpoints, axis=1)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L247-L247)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.near_field

*attribute* · *instance attribute*

```python
near_field = float(near_field)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L227-L227)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.num_panels

*attribute* · *instance attribute*

```python
num_panels = self.midpoints.shape[1]
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L257-L257)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.starts

*attribute* · *instance attribute*

```python
starts = np.concatenate(starts, axis=1)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L251-L251)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.target_potential

*attribute* · *instance attribute*

```python
target_potential = np.concatenate(target)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L256-L256)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.field

*method*

```python
def field(points) -> np.ndarray
```

Field (V/m, shape ``(2, M)``) at ``points`` ``(2, M)``; see :meth:`probe`.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L443-L445)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.force_on_sources

*method*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L447-L467)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.panel_field

*method*

```python
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`.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L404-L415)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.potential

*method*

```python
def potential(points) -> np.ndarray
```

Potential (V) at ``points`` ``(2, M)``; see :meth:`probe`.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L439-L441)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.probe

*method*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L386-L402)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.solve

*method*

```python
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`.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L357-L384)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.tabulate

*method*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L417-L437)

### tree_d_3d.electrodes.BoundaryIntegralElectrodes.total_charge

*method*

```python
def total_charge(conductor_index: int | None = None)
```

Induced charge of one conductor, or of every conductor if ``index`` is ``None``.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L469-L477)

## tree_d_3d.electrodes.Conductor

*class* · *dataclass*

```python
class Conductor
```

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L160-L170)

### tree_d_3d.electrodes.Conductor.closed

*attribute* · *class attribute* · *instance attribute*

```python
closed: bool = True
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L170-L170)

### tree_d_3d.electrodes.Conductor.potential

*attribute* · *instance attribute*

```python
potential: float
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L169-L169)

### tree_d_3d.electrodes.Conductor.vertices

*attribute* · *instance attribute*

```python
vertices: np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L168-L168)

## tree_d_3d.electrodes.OutlinedShape

*class*

```python
class OutlinedShape(Protocol)
```

Bases: `Protocol`

A 2D shape with boundary loops, such as any 2D :mod:`simgeometry` shape.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L154-L157)

### tree_d_3d.electrodes.OutlinedShape.outlines

*method*

```python
def outlines(max_spacing=None) -> list[np.ndarray]
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L157-L157)

## tree_d_3d.electrodes.TabulatedField

*class* · *dataclass*

```python
class TabulatedField
```

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`).

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L103-L151)

### tree_d_3d.electrodes.TabulatedField.field

*attribute* · *instance attribute*

```python
field: np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L117-L117)

### tree_d_3d.electrodes.TabulatedField.potential

*attribute* · *instance attribute*

```python
potential: np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L116-L116)

### tree_d_3d.electrodes.TabulatedField.x

*attribute* · *instance attribute*

```python
x: np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L114-L114)

### tree_d_3d.electrodes.TabulatedField.y

*attribute* · *instance attribute*

```python
y: np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L115-L115)

### tree_d_3d.electrodes.TabulatedField.contains

*method*

```python
def contains(points) -> np.ndarray
```

Mask of the ``(2, M)`` points inside the tabulated rectangle.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L119-L127)

## tree_d_3d.electrodes.conductors_from_shape

*function*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L173-L193)

## tree_d_3d.electrodes.segment_field

*function*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/electrodes.py#L51-L93)
