Module vorts.vortons

Vorton/Tracer classes and Vortons/Tracers container classes.

The module name is "vortons" because most of the focus is on the vorton collection.

Vortons and Tracers can combined using +. Currently the result has the class of the one on the left in the addition.

>>> import vorts
>>> ts = vorts.Tracers(0, 0)
>>> ts + ts
Tracers(
  Tracer(x=0.0, y=0.0)
  Tracer(x=0.0, y=0.0)
)
>>> vorts.Vortons.regular_polygon(3) + ts  # doctest: +ELLIPSIS
Vortons(
  Vorton(G=1.0, x=0.0, y=1.0)
  Vorton(G=1.0, x=-0.86602540..., y=-0.49999999...)
  Vorton(G=1.0, x=0.86602540..., y=-0.50000000...)
  Vorton(G=0.0, x=0.0, y=0.0)
)

Vortons/Tracers can also be transformed (creating a new object).

>>> ts = vorts.Tracers(2, 1)
>>> ts
Tracers(
  Tracer(x=2.0, y=1.0)
)
>>> ts + (1, 1)  # translate
Tracers(
  Tracer(x=3.0, y=2.0)
)
>>> 2 * ts  # scale
Tracers(
  Tracer(x=4.0, y=2.0)
)
>>> ts.rotate(90)  # rotate  # doctest: +ELLIPSIS
Tracers(
  Tracer(x=-0.99999999..., y=2.0)
)

Functions

def points_asterisk(n_limbs=5, n_per_limb=3, *, rmax=1)

Asterisk with n_limbs number of limbs and n_per_limb points per limb.

Parameters

n_limbs : int
For example, 5 to get a 5-pointed asterisk
n_per_limb : int
Number of evenly-spaced points in the limb, not including the center!
rmax : float
Limb length (maximum radius for on-limb points)

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def points_circles(ns=(10, 20, 34, 50), rs=(0.5, 1, 1.5, 2))

Concentric circles.

Parameters

ns : array_like
Number of points in each circle.
rs : array_like
Radii of each circle (one for each value of ns).

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def points_grid(nx, ny, *, xbounds=(-2, 2), ybounds=(-2, 2), dxy=None)

Points on a grid.

Parameters

nx, ny : int
Number of points in the grid in each direction.
xbounds, ybounds : array_like
Inclusive bounds in each direction (lower, upper).
dxy : float, optional
Overrides xbounds and ybounds, setting both to (-dxy, dxy); more convenient if finer-grained control is not needed.

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def points_randn(n, *, mu_x=0, mu_y=0, sig_x=1, sig_y=1)

Sample from normal distribution.

Parameters

n : int
Number of points.
mu_x, mu_y : float
Mean/center of the distribution in each direction.
sig_x, sig_y : float
Standard deviation of the distribution in each direction.

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def points_randu(n, *, dx=2, dy=2)

Sample from 2-d uniform.

Parameters

n : int
Number of points.
dx, dy : float
$x$ positions will be sampled from $[$-dx, dx$)$, and $y$ similarly.

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def points_spiral(n, *, rmin=0, rmax=2, revs=3, kind='Archimedean', spacing='linear')

Create spiral of points.

Parameters

n : int
Number of points.
rmin : float
Minimum radius (distance from the center for the innermost point). Normally should be 0 (not really a spiral without the 0 point).
rmax : float
Maximum radius (distance from the center for the outermost point).
revs : float
Total number of revolutions in the spiral.
kind : str, {'Archimedean', "Fermat's", 'logarithmic'}
Type of spiral.
spacing : str, {'linear', 'log', 'inv-exp', '1/x'}
Method for spacing $\theta$ values on the spiral.

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def rotate_2d(x, *, ang_deg=None, rotmat=None)

Rotate vector x by ang_deg degrees.

Important

Either ang_deg or rotmat can be provided to specify the degree of rotation, but not both.

If ang_deg is used, the rotation matrix will be computed with rotmat_2d(), so you can pass rotmat instead to avoid computing it multiple times.

Parameters

x : array_like
The vector to be rotated.
ang_deg : int, float

Degrees by which to rotate x about the origin.

positive $\to$ counter-clockwise rotation

rotmat : array_like

shape: (2, 2)

Rotation matrix – left-multiplies a column position vector to give rotated position.

def rotmat_2d(ang_deg)

Return rotation matrix for rotation ang_deg in degrees. For left-multiplication of a column position vector.

Note

scipy.spatial.transform.Rotation can be used for 3-d rotations.

def vertices_isos_triangle(*, theta_deg=None, Lambda=None)

Isosceles triangle vertices. With fixed top point $(0, 1)$ and fixed left & right $y=-0.5$.

Important

Either theta_deg or Lambda can be used to specify the angle, but not both.

Parameters

theta_deg : float

Value of the two angles $\theta$ between the horizontal base and connections to the top point at $(0,1)$ in degrees.

$\theta = 72^{\circ} \to \Lambda_c$ (equal to $1/\sqrt{2}$)

$\theta = 60^{\circ} \to$ equilateral triangle (can also create with vertices_regular_polygon(), which gives control over size and location)

Lambda : float

$\Lambda \in (0, 1]$. Related to $\theta$ by $\theta = \pi / (\Lambda^2 + 2)$

$\Lambda = 1 \to$ equilateral triangle

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.
def vertices_regular_polygon(n, *, r_c=1)

Regular polygon vertices.

Parameters

n : int
Polygon order (number of sides/vertices).
r_c : float, int
Radius $r_c$ of the inscribing circle.

Returns

numpy.ndarray
2-d array with first column $x$ and second column $y$.

Classes

class PointsBase (x, y)

Points base class with $x$ and $y$.

Parameters

x, y : array_like

shape: (n_points,)

Initial $x$ and $y$ positions.

Expand source code Browse git
class PointsBase(abc.ABC):
    """Points base class with $x$ and $y$."""

    def __init__(self, x, y):
        """
        Parameters
        ----------
        x, y : array_like
            shape: `(n_points,)`

            Initial $x$ and $y$ positions.
        """
        x = np.atleast_1d(np.asarray(x, dtype=float))  # should be view if `x` is ndarray
        y = np.atleast_1d(np.asarray(y, dtype=float))
        assert x.shape == y.shape and x.ndim == 1

        self._xy = np.column_stack((x, y))

        self._points = None

    @abc.abstractmethod
    def _update_points(self):
        """Update `_points` list of corresponding point objects."""
        ...

    def __repr__(self):
        self._update_points()  # ensure consistency
        n_show = min(len(self._points), 10)
        s_points = "\n".join(f"  {v}" for v in self._points[:n_show])
        if n_show < self.n:
            s_points += "\n  ..."
        return f"{self.__class__.__name__}(\n{s_points}\n)"

    @property
    def n(self):
        """Number of points."""
        return self._xy.shape[0]

    @property
    def x(self):
        """Array of $x$ positions (should be a view)."""
        return self._xy[:, 0]

    @property
    def y(self):
        """Array of $y$ positions (should be a view)."""
        return self._xy[:, 1]

    @property
    def xy(self):
        """2-d array of $(x, y)$ coordinates -- each row is the coordinate of one point.
        This is the data array on which the others depend.
        """
        return self._xy

    @xy.setter
    def xy(self, xy):
        warnings.warn("The coordinates are not intended to be modified this way. Doing nothing.")
        # Elements can still be modified though! And through the other views to `_xy` as well.

    @abc.abstractmethod
    def state_mat_full(self):
        """Full state matrix (could be same as `xy` but should return a copy).
        Columns should be in the same order as the class init positional parameters.
        """
        ...

    @abc.abstractmethod
    def plot(self):
        """Plot state."""
        ...

    def __add__(self, other):
        if hasattr(other, "xy"):  # other points collection
            xy = np.append(self.xy, other.xy, axis=0)
        else:  # vector for translation?
            try:
                xyp = np.asarray(other)
                assert xyp.shape == (2,)
            except (TypeError, AssertionError) as e:
                raise TypeError(f"{other!r} is unsuitable for adding to {type(self)}.") from e
            else:
                xy = self.xy + xyp

        return self.__class__(*xy.T)

    # def __iadd__(self, other):

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            xy = self.xy * other
            return self.__class__(*xy.T)
        else:
            raise TypeError(f"Multiplication by {type(other)} is unsupported.")

    def __rmul__(self, other):
        return self.__mul__(other)

    def rotate(self, theta, *, units="deg", inplace=False):
        """Rotate coordinates about the origin by angle `theta` (units `'rad'` or `'deg'`)."""
        if inplace:
            raise NotImplementedError
        if units not in ("rad", "deg"):
            raise ValueError
        theta_deg = theta if units == "deg" else np.rad2deg(theta)
        rotmat = rotmat_2d(theta_deg)
        xy = self.xy.copy()
        for i in range(xy.shape[0]):  # over rows
            xy[i] = rotate_2d(xy[i], rotmat=rotmat)

        return self.__class__(*xy.T)

Ancestors

  • abc.ABC

Subclasses

Instance variables

prop n

Number of points.

Expand source code
@property
def n(self):
    """Number of points."""
    return self._xy.shape[0]
prop x

Array of $x$ positions (should be a view).

Expand source code
@property
def x(self):
    """Array of $x$ positions (should be a view)."""
    return self._xy[:, 0]
prop xy

2-d array of $(x, y)$ coordinates – each row is the coordinate of one point. This is the data array on which the others depend.

Expand source code
@property
def xy(self):
    """2-d array of $(x, y)$ coordinates -- each row is the coordinate of one point.
    This is the data array on which the others depend.
    """
    return self._xy
prop y

Array of $y$ positions (should be a view).

Expand source code
@property
def y(self):
    """Array of $y$ positions (should be a view)."""
    return self._xy[:, 1]

Methods

def plot(self)

Plot state.

def rotate(self, theta, *, units='deg', inplace=False)

Rotate coordinates about the origin by angle theta (units 'rad' or 'deg').

def state_mat_full(self)

Full state matrix (could be same as xy but should return a copy). Columns should be in the same order as the class init positional parameters.

class Tracer (x: float, y: float)

Tracer – a vorton with $\Gamma=0$ (no circulation/mass) that knows its current position.

Expand source code Browse git
class Tracer(NamedTuple):
    r"""Tracer -- a vorton with $\Gamma=0$ (no circulation/mass) that knows its current position."""

    x: float
    """$x$ position"""
    y: float
    """$y$ position"""

Ancestors

  • builtins.tuple

Instance variables

var x : float

$x$ position

var y : float

$y$ position

class Tracers (x, y)

Collection of Tracers.

Parameters

x, y : array_like

shape: (n_tracers,)

Tracer initial $x$ and $y$ positions.

Expand source code Browse git
class Tracers(PointsBase):
    """Collection of `Tracer`s."""

    def __init__(self, x, y):
        """
        Parameters
        ----------
        x, y : array_like
            shape: `(n_tracers,)`

            Tracer initial $x$ and $y$ positions.
        """
        super().__init__(x=x, y=y)

    def _update_points(self):
        self._points = [Tracer(x, y) for x, y in self._xy]

    @property
    def tracers(self):
        """List of `Tracer` instances corresponding to the coordinates.

        .. warning::
           Modifying this will not update the `Tracers` data.
        """
        self._update_points()  # ensure consistency
        return self._points

    def state_mat_full(self):
        """Full state mat for tracers doesn't include G."""
        # warnings.warn("Note that `state_mat_full` for tracers is the same as `state_mat` (no G).")
        return self._xy.copy()

    def plot(self, *, connect=False, adjustable="box", ax=None, **kwargs):
        """Plot tracers, with points connected if `connect=True`."""
        fig, ax = _maybe_new_fig(ax=ax, **kwargs)

        x, y = self.x, self.y
        fmt = "-o" if connect else "o"
        ax.plot(x, y, fmt, c="0.5", ms=4, label="tracers")

        ax.set(
            xlabel="$x$",
            ylabel="$y$",
        )
        ax.set_aspect("equal", adjustable)
        fig.legend()
        ax.grid(True)
        fig.set_tight_layout(True)

Ancestors

Static methods

def circles(ns=(10, 20, 34, 50), rs=(0.5, 1, 1.5, 2))

Create concentric circle arrangement of Tracers using points_circles().

Parameters

ns : array_like
Number of points in each circle.
rs : array_like
Radii of each circle (one for each value of ns).

Returns

Tracers
 
def grid(nx, ny, *, xbounds=(-2, 2), ybounds=(-2, 2), dxy=None)

Create gridded arrangement of Tracers using points_grid().

Parameters

nx, ny : int
Number of points in the grid in each direction.
xbounds, ybounds : array_like
Inclusive bounds in each direction (lower, upper).
dxy : float, optional
Overrides xbounds and ybounds, setting both to (-dxy, dxy); more convenient if finer-grained control is not needed.

Returns

Tracers
 
def randn(n, *, mu_x=0, mu_y=0, sig_x=1, sig_y=1)

Create Tracers by sampling from normal distributions using points_randn().

Parameters

n : int
Number of points.
mu_x, mu_y : float
Mean/center of the distribution in each direction.
sig_x, sig_y : float
Standard deviation of the distribution in each direction.

Returns

Tracers
 
def randu(n, *, dx=2, dy=2)

Create Tracers by sampling from uniform random distributions using points_randu().

Parameters

n : int
Number of points.
dx, dy : float
$x$ positions will be sampled from $[$-dx, dx$)$, and $y$ similarly.

Returns

Tracers
 
def spiral(n, *, rmin=0, rmax=2, revs=3, kind='Archimedean', spacing='linear')

Create spiral arrangement of Tracers using points_spiral().

Parameters

n : int
Number of points.
rmin : float
Minimum radius (distance from the center for the innermost point). Normally should be 0 (not really a spiral without the 0 point).
rmax : float
Maximum radius (distance from the center for the outermost point).
revs : float
Total number of revolutions in the spiral.
kind : str, {'Archimedean', "Fermat's", 'logarithmic'}
Type of spiral.
spacing : str, {'linear', 'log', 'inv-exp', '1/x'}
Method for spacing $\theta$ values on the spiral.

Returns

Tracers
 

Instance variables

prop tracers

List of Tracer instances corresponding to the coordinates.

Warning

Modifying this will not update the Tracers data.

Expand source code
@property
def tracers(self):
    """List of `Tracer` instances corresponding to the coordinates.

    .. warning::
       Modifying this will not update the `Tracers` data.
    """
    self._update_points()  # ensure consistency
    return self._points

Methods

def plot(self, *, connect=False, adjustable='box', ax=None, **kwargs)

Plot tracers, with points connected if connect=True.

def state_mat_full(self)

Full state mat for tracers doesn't include G.

Inherited members

class Vorton (G: float, x: float, y: float)

A vorton that knows its current state (position and strength).

See Also

Vortons
For a more detailed description.
Expand source code Browse git
class Vorton(NamedTuple):
    """A vorton that knows its current state (position and strength).

    See also
    --------
    Vortons : For a more detailed description.
    """

    G: float
    r"""$\Gamma$, the strength of the circulation, with sign to indicate direction."""
    x: float
    """$x$ position"""
    y: float
    """$y$ position"""

Ancestors

  • builtins.tuple

Instance variables

var G : float

$\Gamma$, the strength of the circulation, with sign to indicate direction.

var x : float

$x$ position

var y : float

$y$ position

class Vortons (G, x, y)

Collection of Vortons.

Parameters

G, x, y : array_like

shape: (n_vortons,)

G: $\Gamma$s ("G" for Gamma).

$\Gamma$ represents the strength of the circulation, with sign to indicate direction. In fluid dynamics, circulation $\Gamma$ is the line integral of velocity or flux of vorticity vectors through a surface (here the $xy$-plane).

x: $x$ positions

y: $y$ positions

Expand source code Browse git
class Vortons(PointsBase):
    """Collection of `Vorton`s."""

    def __init__(self, G, x, y):
        r"""

        Parameters
        ----------
        G, x, y : array_like
            shape: `(n_vortons,)`

            `G`: $\Gamma$s ("G" for [Gamma](https://en.wikipedia.org/wiki/Gamma)).

            $\Gamma$ represents the strength of the circulation, with sign to indicate direction.
            In fluid dynamics, circulation $\Gamma$ is the line integral of velocity
            or flux of vorticity vectors through a surface (here the $xy$-plane).

            `x`: $x$ positions

            `y`: $y$ positions

        """
        super().__init__(x=x, y=y)

        self.G = np.atleast_1d(np.asarray(G, dtype=float))
        r"""Array of vorton strengths ($\Gamma$)."""
        # if np.any(self.G == 0):
        #     warnings.warn(
        #         "Tracers should be in a `Tracers` instance. "
        #         "The ability to add them here may be removed in the future."
        #     )
        assert self.G.ndim == 1 and self.G.size == self.n  # n_vortons

        # the state matrix has shape (n_vortons, n_pos_dims) (G excluded since time-invariant)
        x = np.asarray(x, dtype=float)
        y = np.asarray(y, dtype=float)
        self.state_mat = np.column_stack((x, y))
        """2-d array of $(x, y)$ coordinates -- each row is the coordinate of one vorton."""

    def _update_points(self):
        self._points = [Vorton(G, x, y) for G, x, y in self.state_mat_full()]

    def vortons(self):
        """List of corresponding `Vorton` objects."""
        self._update_points()
        return self._points

    def state_mat_full(self):
        """Return full state matrix: ($G$, $x$, $y$ / `Vortons.G`, `Vortons.x`, `Vortons.y`) as 3 columns."""
        return np.column_stack((self.G, self.xy))

    # Seems to return a view into self.G, so ok to be property
    @property
    def G_col(self):
        """`Vortons.G` as a column vector."""
        return self.G[:, np.newaxis]

    def C(self):
        r"""Calculate $C$.

        $$
        C = \sum_{\alpha, \beta = 1; \alpha \neq \beta}^{N}
            \Gamma_{\alpha} \Gamma_{\beta} l_{\alpha \beta}^{2}
        $$

        $C$ is supposed to be a conserved quantity in this system.
        -- Chamecki (2005) eq. 15, which references Aref (1979)
        """
        n_vortons = self.n
        G = self.G
        C = 0
        for i, j in zip(*np.triu_indices(n_vortons, 1)):  # all combinations without repetition
            xi, yi = self.x[i], self.y[i]
            xj, yj = self.x[j], self.y[j]

            lij_sqd = (xi - xj) ** 2 + (yi - yj) ** 2

            Gi, Gj = G[i], G[j]

            C += Gi * Gj * lij_sqd

        return C

    def H(self):
        r"""Calculate $H$, the Hamiltonian of the system.

        $$
        H = -\frac{1}{4 \pi} \sum_{\alpha, \beta = 1; \alpha \neq \beta}^{N}
            \Gamma_{\alpha} \Gamma_{\beta}
            \ln | r_{\alpha} - r_{\beta} |
        $$
        """
        nv = self.n
        G = self.G
        r = self.state_mat  # vorton positions
        H = 0
        for a, b in zip(*np.triu_indices(nv, 1)):
            ra, rb = r[a], r[b]
            Ga, Gb = G[a], G[b]
            H += -1 / (4 * np.pi) * Ga * Gb * np.log(np.linalg.norm(ra - rb))

        return H

    def I(self):  # noqa: 743,741
        r"""Calculate $I$, the angular impulse of the system.

        $$
        I = \sum_{\alpha = 1}^{N} \Gamma_{\alpha} | r_{\alpha} |^2
        $$
        """
        G = self.G
        # r = self.state_mat
        x = self.x
        y = self.y

        # r_hat_sqd =

        return (G * (x**2 + y**2)).sum()

    # TODO: P and Q (coordinates of the center-of-vorticity)

    # TODO: results are not right for equi tri... need to check formulas
    def theta(self):
        r"""Calculate $\theta$, the action angles??

        Chamecki eq. 19
        """
        N = self.n
        I = self.I()  # noqa: 741
        H = self.H()

        # fmt: off
        return (2/(N-1))**(N*(N-1)/2) * I**(N*(N-1)) * np.exp(4*np.pi*H)
        # fmt: on

    def plot(self, *, ax=None, adjustable="datalim", **kwargs):
        """Plot the vortons.
        (Only their current positions, which are all `Vortons` knows about.)
        """
        fig, ax = _maybe_new_fig(ax=ax, **kwargs)

        # plot vorton positions
        c_Gp = "cadetblue"
        c_Gm = "salmon"
        G = self.G
        Gp, Gm = G > 0, G < 0
        x, y = self.x, self.y
        ax.plot(x[Gp], y[Gp], "o", ms=7, c=c_Gp, label=r"$\Gamma > 0$")
        ax.plot(x[Gm], y[Gm], "o", ms=7, c=c_Gm, label=r"$\Gamma < 0$")

        # plot center of mass
        x_cm, y_cm = self.cm()
        s_cm = f"({x_cm:.4g}, {y_cm:.4g})"
        ax.plot(x_cm, y_cm, "o", ms=13, c="gold", label=f"center of mass\n{s_cm}")

        # 2nd mom
        x_cm2, y_cm2 = self.moment(2)
        s_cm2 = f"({x_cm2:.4g}, {y_cm2:.4g})"
        ax.plot(x_cm2, y_cm2, "*", ms=13, c="0.4", label=f"mom2\n{s_cm2}")

        # 3nd mom
        # TODO: helper fn to DRY this
        x_cm3, y_cm3 = self.moment(3)
        s_cm3 = f"({x_cm3:.4g}, {y_cm3:.4g})"
        ax.plot(x_cm3, y_cm3, "*", ms=13, c="0.55", label=f"mom3\n{s_cm3}")

        ax.set(
            title=f"$C = {self.C():.4g}$",
            xlabel="$x$",
            ylabel="$y$",
        )
        ax.set_aspect("equal", adjustable)
        fig.legend()
        ax.grid(True)
        fig.tight_layout()

    def moment(self, n, *, abs_G=False, center=False):
        r"""Compute `n`-th moment.

        Parameters
        ----------
        n : int
            Which [moment](https://en.wikipedia.org/wiki/Moment_(mathematics)) to calculate.
        abs_G : bool
            Whether to take the absolute value of the $\Gamma$ values (false by default).
        center : bool
            `True`: evaluate moment wrt. center of mass from `Vortons.cm`

            `False`: evaluate moment wrt. $(0, 0)$
        """
        # seems like a moment but that might not be the correct terminology...
        G = self.G_col
        if abs_G:
            G = np.abs(G)
        G_tot = G.sum()

        x = self.state_mat  # x, y (columns)

        c = self.cm() if center else 0

        x_mom = (G * (x - c) ** n).sum(axis=0) / G_tot  # sum along vortons dim, giving a position
        # ^ maybe this should be x - x_cm here...

        return x_mom

    # Chamecki notes suggest this should be called "center of vorticity" or "linear impulse"
    def center_of_mass(self):
        r"""Compute [center of mass](https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles)
        using $\Gamma$ (`Vortons.G`) as mass.
        Equivalent to `Vortons.moment` with `n=1`, `abs_G=True` (currently), `center=False`.
        """
        # TODO: what impact should sign of G have on cm? mass is always pos. but G can be neg.
        return self.moment(1, abs_G=True, center=False)

    def cm(self):
        """Alias for `Vortons.center_of_mass`."""
        return self.center_of_mass()

    def center_coords(self, inplace=False):
        """Make $(0, 0)$ the center of mass."""
        xy_cm = self.cm()
        x_cm, y_cm = xy_cm
        if not inplace:
            return Vortons(self.G, self.x - x_cm, self.y - y_cm)
        else:
            self.state_mat -= x_cm

    def _add_vortons(self, vortons, inplace=False):
        if inplace:
            raise NotImplementedError
        Gxy = np.append(self.state_mat_full(), vortons.state_mat_full(), axis=0)
        return self.__class__(*Gxy.T)

    def _maybe_add_tracers(self, tracers, inplace=False):
        if tracers is None:
            return self
        if inplace:
            raise NotImplementedError
        G = np.append(self.G, np.zeros((tracers.n,)))
        x, y = np.append(self.xy, tracers.xy, axis=0).T
        return self.__class__(G, x, y)

    def to_tracers(self):
        """Return `Tracers` instance corresponding to the vorton positions."""
        return Tracers(self.x, self.y)

    # Overriding base class so can treat tracers and vortons differently (and due to G)
    def __add__(self, other):
        if isinstance(other, self.__class__):
            return self._add_vortons(other)
        elif isinstance(other, Tracers):
            return self._maybe_add_tracers(other)
        else:  # try translation
            xy = (self.to_tracers() + other).xy
            return self.__class__(self.G, *xy.T)

    # def __iadd__

    # Overriding base class due to G
    def __mul__(self, other):
        if isinstance(other, (int, float)):
            xy = self.xy * other
            return self.__class__(self.G, *xy.T)
        else:  # keep message in sync with `PointsBase.__mul__`
            raise TypeError(f"Multiplication by {type(other)} is unsupported.")

    # Overriding base class due to G
    def rotate(self, theta, *, units="deg", inplace=False):
        """Rotate coordinates about the origin by angle `theta` (units `'rad'` or `'deg'`)."""
        xy = self.to_tracers().rotate(theta, units=units, inplace=inplace).xy
        return self.__class__(self.G, *xy.T)

    # TODO: indexing dunder methods

    # TODO: class method to take List[Vorton] and return a Vortons?

Ancestors

Static methods

def asterisk(n_limbs=5, n_per_limb=3, *, rmax=1, G=1)

Create asterisk arrangement of Vortons using points_asterisk().

Parameters

n_limbs : int
For example, 5 to get a 5-pointed asterisk
n_per_limb : int
Number of evenly-spaced points in the limb, not including the center!
rmax : float
Limb length (maximum radius for on-limb points)
G : float, array_like, optional

$\Gamma$ value(s) to use.

Single value or array-like vector of values.

default: 1

Returns

Vortons
 
def isos_triangle(*, theta_deg=None, Lambda=None, G=1)

Create isosceles triangle arrangement of Vortons using vertices_isos_triangle().

Parameters

theta_deg : float

Value of the two angles $\theta$ between the horizontal base and connections to the top point at $(0,1)$ in degrees.

$\theta = 72^{\circ} \to \Lambda_c$ (equal to $1/\sqrt{2}$)

$\theta = 60^{\circ} \to$ equilateral triangle (can also create with vertices_regular_polygon(), which gives control over size and location)

Lambda : float

$\Lambda \in (0, 1]$. Related to $\theta$ by $\theta = \pi / (\Lambda^2 + 2)$

$\Lambda = 1 \to$ equilateral triangle

G : float, array_like, optional

$\Gamma$ value(s) to use.

Single value or array-like vector of values.

default: 1

Returns

Vortons
 
def regular_polygon(n, *, r_c=1, G=1)

Create polygonal arrangement of Vortons using vertices_regular_polygon().

Parameters

n : int
Polygon order (number of sides/vertices).
r_c : float, int
Radius $r_c$ of the inscribing circle.
G : float, array_like, optional

$\Gamma$ value(s) to use.

Single value or array-like vector of values.

default: 1

Returns

Vortons
 

Instance variables

var G

Array of vorton strengths ($\Gamma$).

prop G_col

Vortons.G as a column vector.

Expand source code
@property
def G_col(self):
    """`Vortons.G` as a column vector."""
    return self.G[:, np.newaxis]
var state_mat

2-d array of $(x, y)$ coordinates – each row is the coordinate of one vorton.

Methods

def C(self)

Calculate $C$.

$$ C = \sum_{\alpha, \beta = 1; \alpha \neq \beta}^{N} \Gamma_{\alpha} \Gamma_{\beta} l_{\alpha \beta}^{2} $$

$C$ is supposed to be a conserved quantity in this system. – Chamecki (2005) eq. 15, which references Aref (1979)

def H(self)

Calculate $H$, the Hamiltonian of the system.

$$ H = -\frac{1}{4 \pi} \sum_{\alpha, \beta = 1; \alpha \neq \beta}^{N} \Gamma_{\alpha} \Gamma_{\beta} \ln | r_{\alpha} - r_{\beta} | $$

def I(self)

Calculate $I$, the angular impulse of the system.

$$ I = \sum_{\alpha = 1}^{N} \Gamma_{\alpha} | r_{\alpha} |^2 $$

def center_coords(self, inplace=False)

Make $(0, 0)$ the center of mass.

def center_of_mass(self)

Compute center of mass using $\Gamma$ (Vortons.G) as mass. Equivalent to Vortons.moment() with n=1, abs_G=True (currently), center=False.

def cm(self)
def moment(self, n, *, abs_G=False, center=False)

Compute n-th moment.

Parameters

n : int
Which moment to calculate.
abs_G : bool
Whether to take the absolute value of the $\Gamma$ values (false by default).
center : bool

True: evaluate moment wrt. center of mass from Vortons.cm()

False: evaluate moment wrt. $(0, 0)$

def plot(self, *, ax=None, adjustable='datalim', **kwargs)

Plot the vortons. (Only their current positions, which are all Vortons knows about.)

def state_mat_full(self)

Return full state matrix: ($G$, $x$, $y$ / Vortons.G, Vortons.x, Vortons.y) as 3 columns.

def theta(self)

Calculate $\theta$, the action angles??

Chamecki eq. 19

def to_tracers(self)

Return Tracers instance corresponding to the vorton positions.

def vortons(self)

List of corresponding Vorton objects.

Inherited members