Skip to content
Projects

tree_d_3d.flat_tree

The flat quadtree and direct sums: free-space electrostatics for charges constrained to the xy plane.

Uses the three-dimensional softened Coulomb kernel, not the logarithmic Green’s function of 2D Poisson. No periodic images, conducting walls, collisions, radiation, or relativistic effects are included.

The simulation stores particles as arrays and evaluates fields with the kernels tree_d_3d.kernels.<name> (quadtree_quadrants, evaluate_quadtree, fmm_m2l, …): a flat quadtree (:class:FlatQuadtree) or, for small systems, an exact direct sum. The object tree :class:ReferenceQuadtree is the readable reference implementation of the same algorithm and is kept for inspection and tests.

Each kernel has a Pyccel, a NumPy and a CUDA version with the same arguments, and runs the one matching its arrays. :class:FlatQuadtree and :func:direct_field work on arrays of cunumpy’s active backend: on NumPy the Pyccel versions run (the NumPy versions without Pyccel), on CuPy the CUDA versions, with the launch sizes given here; their results are arrays of that backend. The simulation keeps its particles on the host (each :class:ChargedBody is a view of a column) and hands them to the field solver, which returns to the host.

Per-particle arrays with a component axis are component-major: positions, targets and the fields at them have shape (2, N) (array[0] is x of every particle); per-particle scalars (charge, phi, exclude) are (N,); the tree-node arrays (node_center (nodes, 2), node_p, moments…) are node-major.

Attributes

NameDescription
MAX_ORDERNo description.

Classes

NameDescription
FlatNodeRead-only view of one node of a :class:FlatQuadtree.
FlatQuadtreeArray quadtree for point charges in a plane or for line charges.

Functions

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

MAX_ORDERattributemodule attribute#

MAX_ORDER = 64

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.

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.