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:
FlatQuadtreeand :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:ChargedBodyparticles with an optional uniform magnetic field, absorbing walls and absorbing :mod:simgeometryshapes. - :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_shapepanelling any 2D :mod:simgeometryshape. - :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
| Name | Description |
|---|---|
K_E | No description. |
PROFILE_TAG | No description. |
Classes
| Name | Description |
|---|---|
AbsorbedParticles | Particles removed at absorbing walls and objects, in removal order. |
BoundaryIntegralElectrodes | Conductors held at prescribed potentials, by the single-layer boundary integral. |
ChargedBody | A physical particle or weighted macroparticle in consistent units. |
Conductor | A conductor's boundary: a polyline of vertices (2, n) held at one potential. |
ElectrostaticSimulation | Isolated, nonrelativistic electrostatic N-body dynamics in a plane. |
FlatNode | Read-only view of one node of a :class:FlatQuadtree. |
FlatQuadtree | Array quadtree for point charges in a plane or for line charges. |
GroundedStripField | Field and potential of line charges between plates at x = 0 and x = L. |
Quad | Represents a quadrant in the quadtree. |
ReferenceNode | Bucket node with signed moments about its geometric centre. |
ReferenceQuadtree | Bounded-depth quadtree with signed moments through quadrupole order. |
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. |
direct_field | Exact potential (M,) and field (2, M) at targets (2, M), without k_e. |
kernels_compiled | Whether the kernels run compiled. |
segment_field | Potential (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#
tree_d_3d.constantsView sourceK_E: float = 8987551792.3PROFILE_TAGattributemodule attribute#
tree_d_3d._profilingView sourcePROFILE_TAG = 'tree_d_3d'AbsorbedParticlesclass#
tree_d_3d.simulationView sourceclass 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: floatKinetic energy carried out through the walls.
total_chargeproperty#
total_charge: floatBoundaryIntegralElectrodesclass#
tree_d_3d.electrodesView sourceclass 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.
ChargedBodyclass#
tree_d_3d.bodiesView sourceclass 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)Conductorclassdataclass#
tree_d_3d.electrodesView sourceclass 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.
ElectrostaticSimulationclass#
tree_d_3d.simulationView sourceclass 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 = boundscurrent_timeattributeinstance attribute#
current_time = 0.0direct_thresholdattributeinstance attribute#
direct_threshold = int(direct_threshold)dtattributeinstance attribute#
dt = _finite('dt', dt, positive=True)iterationattributeinstance attribute#
iteration = 0k_eattributeinstance attribute#
k_e = _finite('k_e', k_e, minimum=0)kernelattributeinstance attribute#
kernel = kernelleaf_capacityattributeinstance attribute#
leaf_capacity = leaf_capacitymax_depthattributeinstance attribute#
max_depth = max_depthmethodattributeinstance attribute#
method = methodorderattributeinstance 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: intbuild_treemethod#
def build_tree() -> FlatQuadtreeBuild a tree, enlarging its extent without dropping particles.
compute_fieldsmethod#
def compute_fields(include_fixed: bool = True) -> FlatQuadtree | NoneElectric 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() -> floatMechanical angular momentum about the origin (z component).
get_center_of_massmethod#
def get_center_of_mass() -> np.ndarrayget_kinetic_energymethod#
def get_kinetic_energy() -> floatget_momentummethod#
def get_momentum() -> np.ndarrayget_potential_energymethod#
def get_potential_energy(exact: bool = False) -> floatPair 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() -> floatget_total_energymethod#
def get_total_energy(exact: bool = False) -> floatstepmethod#
def step(dt: float | None = None) -> FlatQuadtree | NoneElectric 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#
tree_d_3d.flat_treeView sourceclass FlatNode(tree: FlatQuadtree, index: int)Read-only view of one node of a :class:FlatQuadtree.
childrenproperty#
children: tuple[FlatNode | None, ...]countproperty#
count: intdipoleproperty#
dipole: np.ndarrayparticlesproperty#
particles: np.ndarrayIndices of the particles inside this node.
quadproperty#
quad: Quadsecond_momentproperty#
second_moment: np.ndarraytotal_abs_chargeproperty#
total_abs_charge: floattotal_chargeproperty#
total_charge: floatFlatQuadtreeclass#
tree_d_3d.flat_treeView sourceclass 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 = kernelleaf_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)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#
tree_d_3d.stripView sourceclass 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 = methodorderattributeinstance 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.ndarrayPotential (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.ndarrayZeta = exp(i pi z / L), as a complex array; positions is (2, N).
Quadclass#
tree_d_3d.quadView sourceclass Quad(x_mid: float, y_mid: float, half_size: float)Represents a quadrant in the quadtree.
halfattributeinstance attribute#
half: float = half_sizexmidattributeinstance attribute#
xmid: float = x_midymidattributeinstance attribute#
ymid: float = y_midcontainsmethod#
def contains(x: float, y: float) -> boolCheck if point is within this quadrant.
northeastmethod#
def northeast() -> QuadCreate northeast child quadrant.
northwestmethod#
def northwest() -> QuadCreate northwest child quadrant.
southeastmethod#
def southeast() -> QuadCreate southeast child quadrant.
southwestmethod#
def southwest() -> QuadCreate southwest child quadrant.
ReferenceNodeclass#
tree_d_3d.referenceView sourceclass ReferenceNode(quad: Quad)Bucket node with signed moments about its geometric centre.
NEattributeinstance attribute#
NE = NoneNWattributeinstance attribute#
NW = NoneSEattributeinstance attribute#
SE = NoneSWattributeinstance attribute#
SW = Nonebodiesattributeinstance attribute#
bodies = []bodyattributeinstance attribute#
body = Nonecenter_of_chargeattributeinstance attribute#
center_of_charge = np.zeros(2)countattributeinstance attribute#
count = 0dipoleattributeinstance attribute#
dipole = np.zeros(2)quadattributeinstance attribute#
quad = quadsecond_momentattributeinstance attribute#
second_moment = np.zeros((2, 2))total_abs_chargeattributeinstance attribute#
total_abs_charge = 0.0total_chargeattributeinstance attribute#
total_charge = 0.0childrenproperty#
childrenReferenceQuadtreeclass#
tree_d_3d.referenceView sourceclass 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) -> NoneStore 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) -> NoneTabulatedFieldclassdataclass#
tree_d_3d.electrodesView sourceclass 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#
tree_d_3d.electrodesView sourcedef 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#
tree_d_3d.flat_treeView sourcedef 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#
tree_d_3d.kernelsView sourcedef kernels_compiled() -> boolWhether 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#
tree_d_3d.electrodesView sourcedef 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.