Skip to content
Projects

tree_d_3d

Type part of a name or a dotted path

Tree Dynamics in 3D: Barnes-Hut trees and fast multipole methods for charged particles.

The particles move in a plane for now; full 3D, on an octree, is planned.

  • :class:FlatQuadtree and :func:direct_field: fields of point charges (the 3D Coulomb kernel) or line charges (kernel="log", the 2D Poisson kernel) at arbitrary targets, by tree walk, FMM or direct sum, on NumPy or CuPy arrays.
  • :class:ElectrostaticSimulation: time integration of :class:ChargedBody particles with an optional uniform magnetic field, absorbing walls and absorbing :mod:simgeometry shapes.
  • :class:ReferenceQuadtree: a readable object tree computing the same as the flat tree.
  • :class:GroundedStripField: line charges between two grounded plates, by conformal mapping and the FMM.
  • :class:BoundaryIntegralElectrodes: conductors at prescribed potentials, by the single-layer boundary-integral (panel) method, with :func:conductors_from_shape panelling any 2D :mod:simgeometry shape.
  • :mod:tree_d_3d.gravity: the classic gravitational Barnes-Hut N-body code.

The compiled kernels are in :mod:tree_d_3d.kernels.

The solvers carry scope-profiler regions, tagged "tree_d_3d". The tag is opt-in, so they record nothing unless the application enables it, for example with scope_profiler.setup(enable_tags=["tree_d_3d"]).

Attributes

NameDescription
K_ENo description.
PROFILE_TAGNo description.

Classes

NameDescription
AbsorbedParticlesParticles removed at absorbing walls and objects, in removal order.
BoundaryIntegralElectrodesConductors held at prescribed potentials, by the single-layer boundary integral.
ChargedBodyA physical particle or weighted macroparticle in consistent units.
ConductorA conductor's boundary: a polyline of vertices (2, n) held at one potential.
ElectrostaticSimulationIsolated, nonrelativistic electrostatic N-body dynamics in a plane.
FlatNodeRead-only view of one node of a :class:FlatQuadtree.
FlatQuadtreeArray quadtree for point charges in a plane or for line charges.
GroundedStripFieldField and potential of line charges between plates at x = 0 and x = L.
QuadRepresents a quadrant in the quadtree.
ReferenceNodeBucket node with signed moments about its geometric centre.
ReferenceQuadtreeBounded-depth quadtree with signed moments through quadrupole order.
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.
direct_fieldExact potential (M,) and field (2, M) at targets (2, M), without k_e.
kernels_compiledWhether the kernels run compiled.
segment_fieldPotential (K,) and field (2, K) of uniformly charged straight segments.

Modules

  • bodiesCharged particles, alone or adopted by a simulation.
  • constantsPhysical constants (SI).
  • electrodesConductors at prescribed potentials, by the boundary-integral (panel) method.
  • flat_treeThe flat quadtree and direct sums: free-space electrostatics for charges constrained to the xy plane.
  • gravityBarnes-Hut N-body solver for fast gravitational force calculations.
  • kernelsThe Barnes-Hut and FMM kernels: one folder per kernel.
  • quadThe square cells of the quadtrees.
  • referenceThe object quadtree: a readable reference implementation of the flat tree.
  • simulationTime integration of charged particles in a plane, with fields from the flat tree.
  • smoothA line-charge FMM with smooth cell membership.
  • stripLine charges between two grounded plates, by conformal mapping and the FMM.

K_Eattributemodule attribute#

K_E: float = 8987551792.3

PROFILE_TAGattributemodule attribute#

Re-exported from tree_d_3d._profilingView source
PROFILE_TAG = 'tree_d_3d'

AbsorbedParticlesclass#

class AbsorbedParticles()

Particles removed at absorbing walls and objects, in removal order.

Positions and velocities are taken just after the drift that crossed the wall or entered the object, so positions lie up to one step beyond the boundary; wall names the wall or object; time is the end of that step. position and velocity are (2, M) (component-major). The bodies themselves are kept in :attr:bodies, detached from the simulation.

bodiesattributeinstance attribute#

bodies: list[ChargedBody] = []

chargeattributeinstance attribute#

charge = np.zeros(0)

massattributeinstance attribute#

mass = np.zeros(0)

positionattributeinstance attribute#

position = np.zeros((2, 0))

timeattributeinstance attribute#

time = np.zeros(0)

velocityattributeinstance attribute#

velocity = np.zeros((2, 0))

wallattributeinstance attribute#

wall = np.zeros(0, dtype=str)

kinetic_energyproperty#

kinetic_energy: float

Kinetic energy carried out through the walls.

total_chargeproperty#

total_charge: float

countsmethod#

def counts() -> dict[str, int]

Number of particles absorbed at each wall and object.

recordmethod#

def record(bodies, *, time, walls, positions, velocities, masses, charges)

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.

ChargedBodyclass#

class ChargedBody(x: float, y: float, vx: float = 0.0, vy: float = 0.0, mass: float = 1.0, charge: float = 1.0, *, weight: float = 1.0, fixed: bool = False)

A physical particle or weighted macroparticle in consistent units.

Input mass and charge are per physical particle. Weight multiplies both, preserving q/m; stored mass and charge are the totals. A fixed source stays immobile. SI inputs give SI outputs.

State lives in arrays. A simulation adopts its bodies, after which pos, vel and force are views of columns of the simulation’s (2, N) arrays, and reading or assigning any attribute reads or writes them.

chargeattributeclass attributeinstance attribute#

charge = property(
  lambda self: float(self._charge[self._index]),
  lambda self,
  value: self._charge.__setitem__(self._index, value)
)

fixedattributeclass attributeinstance attribute#

fixed = property(
  lambda self: bool(self._fixed[self._index]),
  lambda self,
  value: self._fixed.__setitem__(self._index, value)
)

forceattributeclass attributeinstance attribute#

force = property(
  lambda self: self._force[:, self._index],
  lambda self,
  value: self._force.__setitem__((slice(None), self._index), value)
)

massattributeclass attributeinstance attribute#

mass = property(
  lambda self: float(self._mass[self._index]),
  lambda self,
  value: self._mass.__setitem__(self._index, value)
)

posattributeclass attributeinstance attribute#

pos = property(
  lambda self: self._pos[:, self._index],
  lambda self,
  value: self._pos.__setitem__((slice(None), self._index), value)
)

velattributeclass attributeinstance attribute#

vel = property(
  lambda self: self._vel[:, self._index],
  lambda self,
  value: self._vel.__setitem__((slice(None), self._index), value)
)

weightattributeinstance attribute#

weight = _finite('weight', weight, positive=True)

add_forcemethod#

def add_force(fx: float, fy: float) -> None

reset_forcemethod#

def reset_force() -> 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

ElectrostaticSimulationclass#

class ElectrostaticSimulation(bodies: Sequence[ChargedBody], bounds: Quad, theta: float = 0.5, k_e: float = K_E, softening: float = 0.0, dt: float = 1e-12, B_z: float = 0.0, *, leaf_capacity: int | None = None, max_depth: int = 48, method: str = 'auto', direct_threshold: int = DIRECT_THRESHOLD, absorbing_walls: Mapping[str, float] | None = None, absorbing_objects: Mapping[str, Shape] | None = None, kernel: str = 'coulomb', order: int = 18, reference_radius: float = 1.0, symmetric: bool = False)

Isolated, nonrelativistic electrostatic N-body dynamics in a plane.

Bounds are the initial tree extent, not physical walls. Without absorbing_walls all sources are retained. Symmetric electric kicks around an exact uniform-B drift give a second-order scheme, reducing to velocity Verlet when B=0. Positions and velocities are synchronized. Resolve close encounters and oscillations by reducing dt; softening changes the interaction below that length scale.

method selects the field evaluation: "tree" (Barnes-Hut at theta), "direct" (exact all-pairs sum), or "auto", which sums directly for theta == 0 or at most direct_threshold charged particles, where that is exact and faster. Fields are cached and reused while positions, charges and parameters are unchanged, so one step costs one field evaluation.

absorbing_walls maps any of x_min, x_max, y_min and y_max to a wall coordinate. A particle that ends a drift beyond a wall is removed before the end-of-step field evaluation and stops acting as a source; see :attr:absorbed. Walls only remove particles: the field stays free-space, with no image charges or wall potential.

absorbing_objects maps names to 2D :mod:simgeometry shapes (a Circle, Polygon, Aperture2D, …). A particle that ends a drift inside one is removed the same way and recorded under its name; an inverted shape absorbs everything outside it, such as a round chamber wall. Only end points are tested, so keep |v| dt below the thickness of the thinnest object. Like walls, objects do not change the field: for electrodes, pair them with :class:tree_d_3d.BoundaryIntegralElectrodes.

kernel="log" makes every particle an infinite line charge per unit length along z, the interaction of 2D Cartesian PIC, instead of a point charge in the plane. Its pair energy is -2 k_e q_i q_j ln(r / r_0) with r_0 = reference_radius, which shifts the potential energy but not the forces. Line charges add method="fmm", the O(N) fast multipole method of :meth:FlatQuadtree.fmm, which "auto" selects above direct_threshold. order is the number of expansion terms for line charges; leaf_capacity defaults to 32 for the FMM and 8 otherwise.

Finite-theta forces are not pairwise symmetric: check energy and momentum convergence.

B_zattributeinstance attribute#

B_z = _finite('B_z', B_z)

absorbedattributeinstance attribute#

absorbed = AbsorbedParticles()

bodiesattributeinstance attribute#

bodies = list(bodies)

boundsattributeinstance attribute#

bounds = bounds

current_timeattributeinstance attribute#

current_time = 0.0

direct_thresholdattributeinstance attribute#

direct_threshold = int(direct_threshold)

dtattributeinstance attribute#

dt = _finite('dt', dt, positive=True)

iterationattributeinstance attribute#

iteration = 0

k_eattributeinstance attribute#

k_e = _finite('k_e', k_e, minimum=0)

kernelattributeinstance attribute#

kernel = kernel

leaf_capacityattributeinstance attribute#

leaf_capacity = leaf_capacity

max_depthattributeinstance attribute#

max_depth = max_depth

methodattributeinstance attribute#

method = method

orderattributeinstance attribute#

order = int(order)

reference_radiusattributeinstance attribute#

reference_radius = _finite('reference_radius', reference_radius, positive=True)

softeningattributeinstance attribute#

softening = _finite('softening', softening, minimum=0)

symmetricattributeinstance attribute#

symmetric = bool(symmetric)

thetaattributeinstance attribute#

theta = _finite('theta', theta, minimum=0)

absorbing_objectsproperty#

absorbing_objects: dict[str, Shape]

absorbing_wallsproperty#

absorbing_walls: dict[str, float]

num_particlesproperty#

num_particles: int

build_treemethod#

def build_tree() -> FlatQuadtree

Build a tree, enlarging its extent without dropping particles.

compute_fieldsmethod#

def compute_fields(include_fixed: bool = True) -> FlatQuadtree | None

Electric force and potential on every particle, reusing a cache.

Fixed particles never move, so stepping skips them as targets (include_fixed=False) and their force and potential stay zero until they are asked for; they always act as sources. A later call with include_fixed=True fills them in from the same tree. Returns the tree used, or None for direct summation.

get_angular_momentummethod#

def get_angular_momentum() -> float

Mechanical angular momentum about the origin (z component).

get_center_of_massmethod#

def get_center_of_mass() -> np.ndarray

get_kinetic_energymethod#

def get_kinetic_energy() -> float

get_momentummethod#

def get_momentum() -> np.ndarray

get_potential_energymethod#

def get_potential_energy(exact: bool = False) -> float

Pair energy for the force kernel, excluding self-energy.

By default this is 0.5 * sum(q_i * phi_i) from the field evaluation used for the forces, at no extra cost: exact when the fields are summed directly, and accurate to the tree error otherwise. exact=True sums all pairs, which costs O(N squared).

get_total_chargemethod#

def get_total_charge() -> float

get_total_energymethod#

def get_total_energy(exact: bool = False) -> float

stepmethod#

def step(dt: float | None = None) -> FlatQuadtree | None

Electric half-kick, exact cyclotron drift, electric half-kick.

Returns the tree used for the end-of-step forces, or None when the fields were summed directly. Forces on fixed particles are not evaluated here and read as zero; call :meth:compute_fields for them.

FlatNodeclass#

class FlatNode(tree: FlatQuadtree, index: int)

Read-only view of one node of a :class:FlatQuadtree.

indexattributeinstance attribute#

index = index

treeattributeinstance attribute#

tree = tree

childrenproperty#

children: tuple[FlatNode | None, ...]

countproperty#

count: int

dipoleproperty#

dipole: np.ndarray

particlesproperty#

particles: np.ndarray

Indices of the particles inside this node.

quadproperty#

quad: Quad

second_momentproperty#

second_moment: np.ndarray

total_abs_chargeproperty#

total_abs_charge: float

total_chargeproperty#

total_charge: float

is_externalmethod#

def is_external() -> bool

FlatQuadtreeclass#

class FlatQuadtree(positions, charges, root_quad: Quad, *, leaf_capacity: int = 8, max_depth: int = 48, compiled: bool = True, kernel: str = 'coulomb', order: int = 12, symmetric: bool = False)

Array quadtree for point charges in a plane or for line charges.

Same tree and opening rule as :class:ReferenceQuadtree: nodes holding more than leaf_capacity particles split into four until max_depth, cells containing the target are always opened, and the opening test uses unsoftened distance. positions is (2, N) (component-major) and charges (N,); both are copied, so rebuild after moving or changing a source.

kernel="coulomb" (the default) is the 3D point-charge interaction, with signed moments through quadrupole order. kernel="log" treats each charge as an infinite line charge per unit length along z, the interaction of 2D Cartesian PIC: potential -2 k_e q ln r and field 2 k_e q r / r**2. It uses complex multipole expansions of order terms, so accuracy can be raised by raising order as well as by lowering theta.

chargeattributeinstance attribute#

charge = _device(charges, compiled=compiled).reshape(-1).copy()

kernelattributeinstance attribute#

kernel = kernel

leaf_capacityattributeinstance attribute#

leaf_capacity = int(leaf_capacity)

max_depthattributeinstance attribute#

max_depth = int(max_depth)

moment_imattributeinstance attribute#

moment_im = self._xp.empty((count, self.order + 1))

moment_reattributeinstance attribute#

moment_re = self._xp.empty((count, self.order + 1))

node_countattributeinstance attribute#

node_count = int(count)

node_pattributeinstance attribute#

node_p = self._xp.empty((count, 2))

node_qattributeinstance attribute#

node_q = self._xp.empty(count)

node_qabsattributeinstance attribute#

node_qabs = self._xp.empty(count)

node_sattributeinstance attribute#

node_s = self._xp.empty((count, 3))

orderattributeinstance attribute#

order = int(order)

posattributeinstance attribute#

pos = _points(positions, 'positions', compiled=compiled).copy()

symmetricattributeinstance attribute#

symmetric = bool(symmetric)

rootproperty#

root: FlatNode

collect_nodesmethod#

def collect_nodes() -> list[FlatNode]

Views of every node that holds at least one particle.

evaluatemethod#

def evaluate(targets, *, exclude=None, theta=0.5, softening=0.0)

Potential (M,) and field (2, M) at targets (2, M), without k_e.

exclude[i] is a source index skipped for target i (-1: none).

field_atmethod#

def field_at(positions, *, theta: float = 0.5, k_e: float = K_E, softening: float = 0.0, reference_radius: float = 1.0) -> tuple[np.ndarray, np.ndarray]

Potential, shape (n,), and electric field, shape (2, n), at probes (2, n).

For line charges the potential is only defined up to a constant; it is -2 k_e sum q ln(r / reference_radius), so the potential of the net charge vanishes at reference_radius from it.

fmmmethod#

def fmm(theta: float = 0.5, softening: float = 0.0, potential: bool = True)

Line-charge potential and field at every source, by the FMM.

Fast multipole method for kernel="log" (Greengard-Rokhlin, complex form): a dual-tree traversal pairs well-separated cells, whose multipole expansions are translated into each other’s local expansions (M2L, both ways at once); local expansions are shifted down the tree (L2L) and evaluated at the particles (L2P); adjacent leaves interact exactly (P2P). Cost is O(N) rather than the O(N log N) of :meth:evaluate with every particle as a target. Returns the raw potential (N,) and field (2, N) in input order, without k_e, excluding each particle’s own charge; softening applies to the exact near field only. potential=False skips the potential (returned as zeros), which saves a logarithm per near-field pair when only forces are needed.

GroundedStripFieldclass#

class GroundedStripField(width: float, *, order: int = 14, theta: float = 0.5, leaf_capacity: int = 32, k_e: float = K_E, method: str = 'fmm', softening: float = 0.0, cutoff: float = 6.0)

Field and potential of line charges between plates at x = 0 and x = L.

cutoffattributeinstance attribute#

cutoff = float(cutoff)

k_eattributeinstance attribute#

k_e = float(k_e)

leaf_capacityattributeinstance attribute#

leaf_capacity = int(leaf_capacity)

methodattributeinstance attribute#

method = method

orderattributeinstance attribute#

order = int(order)

softeningattributeinstance attribute#

softening = float(softening)

thetaattributeinstance attribute#

theta = float(theta)

widthattributeinstance attribute#

width = float(width)

field_at_chargesmethod#

def field_at_charges(positions, charges, *, potential: bool = True) -> tuple[np.ndarray, np.ndarray]

Potential (V) and field (V/m, shape (2, N)) acting on every charge.

positions has shape (2, N); charges are line charges per unit length (C/m). With potential=False only the field is computed (the potential is returned as zeros), which is cheaper.

potentialmethod#

def potential(positions, charges, points) -> np.ndarray

Potential (V) at arbitrary points strictly between the plates.

probemethod#

def probe(positions, charges, points) -> tuple[np.ndarray, np.ndarray]

Potential (V) and field (V/m, shape (2, M)) at points (2, M).

The points must lie strictly between the plates; they are probes, not charges, so no self term applies.

to_planemethod#

def to_plane(positions) -> np.ndarray

Zeta = exp(i pi z / L), as a complex array; positions is (2, N).

Quadclass#

Re-exported from tree_d_3d.quadView source
class Quad(x_mid: float, y_mid: float, half_size: float)

Represents a quadrant in the quadtree.

halfattributeinstance attribute#

half: float = half_size

xmidattributeinstance attribute#

xmid: float = x_mid

ymidattributeinstance attribute#

ymid: float = y_mid

containsmethod#

def contains(x: float, y: float) -> bool

Check if point is within this quadrant.

northeastmethod#

def northeast() -> Quad

Create northeast child quadrant.

northwestmethod#

def northwest() -> Quad

Create northwest child quadrant.

southeastmethod#

def southeast() -> Quad

Create southeast child quadrant.

southwestmethod#

def southwest() -> Quad

Create southwest child quadrant.

ReferenceNodeclass#

class ReferenceNode(quad: Quad)

Bucket node with signed moments about its geometric centre.

NEattributeinstance attribute#

NE = None

NWattributeinstance attribute#

NW = None

SEattributeinstance attribute#

SE = None

SWattributeinstance attribute#

SW = None

bodiesattributeinstance attribute#

bodies = []

bodyattributeinstance attribute#

body = None

center_of_chargeattributeinstance attribute#

center_of_charge = np.zeros(2)

countattributeinstance attribute#

count = 0

dipoleattributeinstance attribute#

dipole = np.zeros(2)

quadattributeinstance attribute#

quad = quad

second_momentattributeinstance attribute#

second_moment = np.zeros((2, 2))

total_abs_chargeattributeinstance attribute#

total_abs_charge = 0.0

total_chargeattributeinstance attribute#

total_charge = 0.0

childrenproperty#

children

is_externalmethod#

def is_external() -> bool

ReferenceQuadtreeclass#

class ReferenceQuadtree(root_quad: Quad, *, leaf_capacity: int = 8, max_depth: int = 48)

Bounded-depth quadtree with signed moments through quadrupole order.

Coincident particles remain in leaf buckets. Theta=0 evaluates all pairs. Cells containing the target are always opened. The opening criterion uses unsoftened distance so softening cannot make a nearby cell look distant.

leaf_capacityattributeinstance attribute#

leaf_capacity = int(leaf_capacity)

max_depthattributeinstance attribute#

max_depth = int(max_depth)

rootattributeinstance attribute#

root = ReferenceNode(root_quad)

collect_nodesmethod#

def collect_nodes() -> list[ReferenceNode]

compute_lorentz_force_onmethod#

def compute_lorentz_force_on(body: ChargedBody, theta: float = 0.5, k_e: float = K_E, softening: float = 0.0, B_z: float = 0.0) -> None

Store q(E + v cross B), with no self-force.

field_atmethod#

def field_at(position: Sequence[float], *, theta: float = 0.5, k_e: float = K_E, softening: float = 0.0, exclude: ChargedBody | None = None) -> tuple[float, np.ndarray]

Return (potential, electric field), excluding one source by identity.

Potential and field use the same softened multipole expansion. Rebuild the tree after moving or changing any source particle.

insertmethod#

def insert(body: ChargedBody) -> None

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.

direct_fieldfunction#

def direct_field(targets, pos, charge, *, softening=0.0, exclude=None, compiled=True, kernel='coulomb') -> tuple[np.ndarray, np.ndarray]

Exact potential (M,) and field (2, M) at targets (2, M), without k_e.

pos (2, N) and charge (N,) are the sources. exclude[i] is the index of a source skipped for target i (-1 for none). kernel is "coulomb" or "log" (line charges), as for :class:FlatQuadtree.

kernels_compiledfunction#

def kernels_compiled() -> bool

Whether the kernels run compiled.

On the CuPy backend they always do (CUDA); otherwise this checks once per process whether the Pyccel versions compile. Without them the tree kernels run as Python loops, so method="auto" sums directly.

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.