tree_d_3d.simulation
Time integration of charged particles in a plane, with fields from the flat tree.
Attributes
| Name | Description |
|---|---|
DIRECT_THRESHOLD | No description. |
Classes
| Name | Description |
|---|---|
AbsorbedParticles | Particles removed at absorbing walls and objects, in removal order. |
ElectrostaticSimulation | Isolated, nonrelativistic electrostatic N-body dynamics in a plane. |
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: floatKinetic energy carried out through the walls.
total_chargeproperty#
total_charge: floatElectrostaticSimulationclass#
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 = 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.