# tree_d_3d.smooth

*module*

A line-charge FMM with smooth cell membership.

Forces are the exact gradient of a smooth potential energy, and mutual pair forces
are exactly antisymmetric.

This is a NumPy prototype for method research, not a fast code: everything is
complex arithmetic on small arrays, and it is meant for a few thousand particles.
It separates two things the production tree ties together:

* a :class:`BoxTree`: a static set of square cells (split until a cell holds at most
  ``leaf_capacity`` of the positions it was built from). The cells can be kept across
  time steps, so the interaction lists depend on nothing that moves;
* a :class:`SmoothFMM` over that tree for given positions and charges.

**Membership.** A particle belongs to every leaf whose box, grown by ``band``, contains
it, with a weight ``w_L(z)`` that is the tensor product of quintic smoothsteps across
the leaf's edges. The weights form a partition of unity on any quadtree leaf
partition when ``band`` is at most half the smallest leaf width. The interaction of
particles ``i`` and ``j`` is split over all their membership pairs (routes)
``(L, L')`` with weight ``w_L(z_i) w_L'(z_j)``; each route uses the exact kernel if
``(L, L')`` is a near-field pair and the symmetric multipole-to-local expansion of the
accepted ancestor pair otherwise. ``band = 0`` is the ordinary tree (hard membership).
Softening is compact (a smooth charge cloud of radius ``softening``), so the far field of
a softened particle is exactly the point charge's and the tree solves the same model as the direct sum.

**Energy and forces.** In the units of :mod:`tree_d_3d.flat_tree` (potential of a line
charge ``-2 q ln r``, field ``2 q d / r^2``, no ``k_e``),

    U = -2 sum_routes w_m w_m' q_i q_j Phi(z_i - z_j),
    F_i = -dU/dx_i = 2 q_i sum_{m: i in m} [ w_m G_m + Phi_m grad w_m ],

where ``Phi_m`` and ``G_m`` are the potential and its gradient at ``z_i`` seen through
membership ``m`` (local expansion of its leaf plus its near-field partners). The
second term, potential times the gradient of the weight, is what makes ``F`` the
exact gradient; without it (or with ``band = 0``) the energy is not conserved when a
particle changes leaf.

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

## tree_d_3d.smooth.BoxTree

*class*

```python
class BoxTree
```

Square cells of a quadtree over ``root``, split to hold ``leaf_capacity`` points.

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

### tree_d_3d.smooth.BoxTree.center

*attribute* · *instance attribute*

```python
center = np.array(center)
```

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

### tree_d_3d.smooth.BoxTree.child

*attribute* · *instance attribute*

```python
child = np.array(child)
```

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

### tree_d_3d.smooth.BoxTree.depth

*attribute* · *instance attribute*

```python
depth = np.array(depth)
```

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

### tree_d_3d.smooth.BoxTree.half

*attribute* · *instance attribute*

```python
half = np.array(half)
```

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

### tree_d_3d.smooth.BoxTree.leaves

*attribute* · *instance attribute*

```python
leaves = np.flatnonzero(self.child < 0)
```

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

### tree_d_3d.smooth.BoxTree.min_leaf_half

*attribute* · *instance attribute*

```python
min_leaf_half = float(self.half[self.leaves].min())
```

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

### tree_d_3d.smooth.BoxTree.parent

*attribute* · *instance attribute*

```python
parent = np.array(parent)
```

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

### tree_d_3d.smooth.BoxTree.root

*attribute* · *instance attribute*

```python
root = root
```

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

### tree_d_3d.smooth.BoxTree.memberships

*method*

```python
def memberships(z: np.ndarray, band: float)
```

(particle, leaf, weight, dw/dx, dw/dy) for every particle-leaf pair with w > 0.

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

## tree_d_3d.smooth.SmoothFMM

*class*

```python
class SmoothFMM
```

Potential energy and exact-gradient forces of line charges on a :class:`BoxTree`.

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

### tree_d_3d.smooth.SmoothFMM.band

*attribute* · *instance attribute*

```python
band = float(band)
```

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

### tree_d_3d.smooth.SmoothFMM.eps2

*attribute* · *instance attribute*

```python
eps2 = float(softening) ** 2
```

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

### tree_d_3d.smooth.SmoothFMM.order

*attribute* · *instance attribute*

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

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

### tree_d_3d.smooth.SmoothFMM.q

*attribute* · *instance attribute*

```python
q = np.asarray(charge, dtype=float)
```

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

### tree_d_3d.smooth.SmoothFMM.symmetric

*attribute* · *instance attribute*

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

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

### tree_d_3d.smooth.SmoothFMM.theta

*attribute* · *instance attribute*

```python
theta = float(theta)
```

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

### tree_d_3d.smooth.SmoothFMM.tree

*attribute* · *instance attribute*

```python
tree = tree
```

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

### tree_d_3d.smooth.SmoothFMM.z

*attribute* · *instance attribute*

```python
z = np.asarray(pos[0], dtype=float) + 1j * np.asarray(pos[1], dtype=float)
```

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

### tree_d_3d.smooth.SmoothFMM.energy

*property*

```python
energy: float
```

U = -2 sum over unordered routes, in the units of ``flat_tree``.

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

### tree_d_3d.smooth.SmoothFMM.fields

*method*

```python
def fields() -> tuple[np.ndarray, np.ndarray]
```

Potential ``(N,)`` and field ``(2, N)`` such that ``F_i = q_i E_i = -dU/dx_i``.

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

### tree_d_3d.smooth.SmoothFMM.potential_at

*method*

```python
def potential_at(points: np.ndarray) -> np.ndarray
```

Direct-sum potential at external points, for checks (no tree).

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

## tree_d_3d.smooth.comoving_fields

*function*

```python
def comoving_fields(tree: BoxTree, pos: np.ndarray, charge: np.ndarray, mass: np.ndarray, center: np.ndarray, **options) -> tuple[float, np.ndarray, np.ndarray]
```

Energy, potential and field with the boxes attached to the centre of mass.

The box set of ``tree`` is used in a frame whose origin follows the centre of
mass ``c(x)``, i.e. ``U(x) = U_tree(x - (c(x) - center))``. ``U`` is then
translation invariant, so its exact gradient has zero net force: the
gradient of the shift subtracts the mass-weighted net force of the
smooth-membership terms. With this, both energy (smooth ``U``) and momentum
(Noether) are conserved by a symplectic integrator. ``center`` is where the
centre of mass was when the boxes were built.

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

## tree_d_3d.smooth.direct_energy

*function*

```python
def direct_energy(pos, charge, softening=0.0)
```

Exact pair energy, same units as :attr:`SmoothFMM.energy`.

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

## tree_d_3d.smooth.direct_fields

*function*

```python
def direct_fields(pos, charge, softening=0.0)
```

Exact potential and field, same units as :meth:`SmoothFMM.fields`.

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