Skip to content
Projects

tree_d_3d.simulation

Time integration of charged particles in a plane, with fields from the flat tree.

Attributes

NameDescription
DIRECT_THRESHOLDNo description.

Classes

NameDescription
AbsorbedParticlesParticles removed at absorbing walls and objects, in removal order.
ElectrostaticSimulationIsolated, nonrelativistic electrostatic N-body dynamics in a plane.

DIRECT_THRESHOLDattributemodule attribute#

DIRECT_THRESHOLD: int = 1000

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)

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.