# tree_d_3d.simulation

*module*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L1-L1)

## tree_d_3d.simulation.DIRECT_THRESHOLD

*attribute* · *module attribute*

```python
DIRECT_THRESHOLD: int = 1000
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L24-L24)

## tree_d_3d.simulation.AbsorbedParticles

*class*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L64-L108)

### tree_d_3d.simulation.AbsorbedParticles.bodies

*attribute* · *instance attribute*

```python
bodies: list[ChargedBody] = []
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L76-L76)

### tree_d_3d.simulation.AbsorbedParticles.charge

*attribute* · *instance attribute*

```python
charge = np.zeros(0)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L82-L82)

### tree_d_3d.simulation.AbsorbedParticles.mass

*attribute* · *instance attribute*

```python
mass = np.zeros(0)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L81-L81)

### tree_d_3d.simulation.AbsorbedParticles.position

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L79-L79)

### tree_d_3d.simulation.AbsorbedParticles.time

*attribute* · *instance attribute*

```python
time = np.zeros(0)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L77-L77)

### tree_d_3d.simulation.AbsorbedParticles.velocity

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L80-L80)

### tree_d_3d.simulation.AbsorbedParticles.wall

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L78-L78)

### tree_d_3d.simulation.AbsorbedParticles.kinetic_energy

*property*

```python
kinetic_energy: float
```

Kinetic energy carried out through the walls.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L98-L100)

### tree_d_3d.simulation.AbsorbedParticles.total_charge

*property*

```python
total_charge: float
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L94-L95)

### tree_d_3d.simulation.AbsorbedParticles.counts

*method*

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

Number of particles absorbed at each wall and object.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L102-L105)

### tree_d_3d.simulation.AbsorbedParticles.record

*method*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L84-L91)

## tree_d_3d.simulation.ElectrostaticSimulation

*class*

```python
class ElectrostaticSimulation
```

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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L111-L600)

### tree_d_3d.simulation.ElectrostaticSimulation.B_z

*attribute* · *instance attribute*

```python
B_z = _finite('B_z', B_z)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L261-L261)

### tree_d_3d.simulation.ElectrostaticSimulation.absorbed

*attribute* · *instance attribute*

```python
absorbed = AbsorbedParticles()
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L272-L272)

### tree_d_3d.simulation.ElectrostaticSimulation.bodies

*attribute* · *instance attribute*

```python
bodies = list(bodies)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L224-L224)

### tree_d_3d.simulation.ElectrostaticSimulation.bounds

*attribute* · *instance attribute*

```python
bounds = bounds
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L256-L256)

### tree_d_3d.simulation.ElectrostaticSimulation.current_time

*attribute* · *instance attribute*

```python
current_time = 0.0
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L266-L266)

### tree_d_3d.simulation.ElectrostaticSimulation.direct_threshold

*attribute* · *instance attribute*

```python
direct_threshold = int(direct_threshold)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L265-L265)

### tree_d_3d.simulation.ElectrostaticSimulation.dt

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L260-L260)

### tree_d_3d.simulation.ElectrostaticSimulation.iteration

*attribute* · *instance attribute*

```python
iteration = 0
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L267-L267)

### tree_d_3d.simulation.ElectrostaticSimulation.k_e

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L258-L258)

### tree_d_3d.simulation.ElectrostaticSimulation.kernel

*attribute* · *instance attribute*

```python
kernel = kernel
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L243-L243)

### tree_d_3d.simulation.ElectrostaticSimulation.leaf_capacity

*attribute* · *instance attribute*

```python
leaf_capacity = leaf_capacity
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L262-L262)

### tree_d_3d.simulation.ElectrostaticSimulation.max_depth

*attribute* · *instance attribute*

```python
max_depth = max_depth
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L263-L263)

### tree_d_3d.simulation.ElectrostaticSimulation.method

*attribute* · *instance attribute*

```python
method = method
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L264-L264)

### tree_d_3d.simulation.ElectrostaticSimulation.order

*attribute* · *instance attribute*

```python
order = int(order)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L244-L244)

### tree_d_3d.simulation.ElectrostaticSimulation.reference_radius

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L247-L249)

### tree_d_3d.simulation.ElectrostaticSimulation.softening

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L259-L259)

### tree_d_3d.simulation.ElectrostaticSimulation.symmetric

*attribute* · *instance attribute*

```python
symmetric = bool(symmetric)
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L246-L246)

### tree_d_3d.simulation.ElectrostaticSimulation.theta

*attribute* · *instance attribute*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L257-L257)

### tree_d_3d.simulation.ElectrostaticSimulation.absorbing_objects

*property*

```python
absorbing_objects: dict[str, Shape]
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L289-L290)

### tree_d_3d.simulation.ElectrostaticSimulation.absorbing_walls

*property*

```python
absorbing_walls: dict[str, float]
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L285-L286)

### tree_d_3d.simulation.ElectrostaticSimulation.num_particles

*property*

```python
num_particles: int
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L281-L282)

### tree_d_3d.simulation.ElectrostaticSimulation.build_tree

*method*

```python
def build_tree() -> FlatQuadtree
```

Build a tree, enlarging its extent without dropping particles.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L398-L415)

### tree_d_3d.simulation.ElectrostaticSimulation.compute_fields

*method*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L448-L502)

### tree_d_3d.simulation.ElectrostaticSimulation.get_angular_momentum

*method*

```python
def get_angular_momentum() -> float
```

Mechanical angular momentum about the origin (z component).

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L596-L600)

### tree_d_3d.simulation.ElectrostaticSimulation.get_center_of_mass

*method*

```python
def get_center_of_mass() -> np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L587-L591)

### tree_d_3d.simulation.ElectrostaticSimulation.get_kinetic_energy

*method*

```python
def get_kinetic_energy() -> float
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L541-L542)

### tree_d_3d.simulation.ElectrostaticSimulation.get_momentum

*method*

```python
def get_momentum() -> np.ndarray
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L593-L594)

### tree_d_3d.simulation.ElectrostaticSimulation.get_potential_energy

*method*

```python
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).

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L544-L579)

### tree_d_3d.simulation.ElectrostaticSimulation.get_total_charge

*method*

```python
def get_total_charge() -> float
```

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L584-L585)

### tree_d_3d.simulation.ElectrostaticSimulation.get_total_energy

*method*

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

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L581-L582)

### tree_d_3d.simulation.ElectrostaticSimulation.step

*method*

```python
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.

[View source](https://github.com/max-models/tree-d-3d/blob/devel/src/tree_d_3d/simulation.py#L504-L539)
