Revision b484fdebfd316e42c82dc695a9aa3e4e60eeea4f authored by Hanno Rein on 22 September 2023, 20:58:04 UTC, committed by Hanno Rein on 22 September 2023, 20:58:04 UTC
1 parent cba329c
simulation.py
from ctypes import Structure, c_double, POINTER, c_uint32, c_float, c_int, c_uint, c_uint32, c_int64, c_long, c_ulong, c_ulonglong, c_void_p, c_char_p, CFUNCTYPE, byref, create_string_buffer, addressof, pointer, cast, c_char, c_size_t, string_at
from . import clibrebound, Escape, NoParticles, Encounter, Collision, SimulationError, ParticleNotFound, M_to_E
from .citations import cite
from .particle import Particle
from .units import units_convert_particle, check_units, convert_G, hash_to_unit
from .tools import hash as rebhash
import math
import os
import sys
import ctypes.util
import warnings
try:
# Required for Python>=3.9
from collections.abc import MutableMapping
except:
from collections import MutableMapping
import types
### The following enum and class definitions need to
### consitent with those in rebound.h
INTEGRATORS = {"ias15": 0, "whfast": 1, "sei": 2, "leapfrog": 4, "none": 7, "janus": 8, "mercurius": 9, "saba": 10, "eos": 11, "bs": 12, "whfast512":21}
BOUNDARIES = {"none": 0, "open": 1, "periodic": 2, "shear": 3}
GRAVITIES = {"none": 0, "basic": 1, "compensated": 2, "tree": 3, "mercurius": 4, "jacobi": 5}
COLLISIONS = {"none": 0, "direct": 1, "tree": 2, "mercurius": 3, "line": 4, "linetree": 5}
VISUALIZATIONS = {"none": 0, "opengl": 1, "webgl": 2}
WHFAST_KERNELS = {"default": 0, "modifiedkick": 1, "composition": 2, "lazy": 3}
WHFAST_COORDINATES = {"jacobi": 0, "democraticheliocentric": 1, "whds": 2}
SABA_TYPES = {
"1": 0x0, "2": 0x1, "3": 0x2, "4": 0x3,
"cm1": 0x100, "cm2": 0x101, "cm3": 0x102, "cm4": 0x103,
"cl1": 0x200, "cl2": 0x201, "cl3": 0x202, "cl4": 0x203,
"10,4": 0x4, "8,6,4": 0x5, "10,6,4": 0x6,
"h8,4,4": 0x7, "h8,6,4": 0x8, "h10,6,4": 0x9,
}
EOS_TYPES = {
"lf": 0x00,
"lf4": 0x01,
"lf6": 0x02,
"lf8": 0x03,
"lf4_2": 0x04,
"lf8_6_4": 0x05,
"plf7_6_4": 0x06,
"pmlf4": 0x07,
"pmlf6": 0x08,
}
# Format: Majorerror, id, message
BINARY_WARNINGS = [
(True, 1, "Cannot read binary file. Check filename and file contents."),
(False, 2, "Binary file was saved with a different version of REBOUND. Binary format might have changed."),
(False, 4, "You have to reset function pointers after creating a reb_simulation struct with a binary file."),
(False, 8, "Binary file might be corrupted. Number of particles found does not match particle number expected."),
(True, 16, "Error while reading binary file (file was closed).",),
(True, 32, "Index out of range.",),
(True, 64, "Error while trying to seek file.",),
(False, 128, "Encountered unkown field in file. File might have been saved with a different version of REBOUND."),
(True, 256, "Integrator type is not supported by this simulation archive version."),
(False, 512, "The binary file seems to be corrupted. An attempt has been made to read the uncorrupted parts of it."),
(True, 1024, "Reading old SimulationArchives (version < 2) is no longer supported. If you need to read such an archive, use a REBOUND version <= 3.26.3"),
]
class reb_hash_pointer_pair(Structure):
_fields_ = [("hash", c_uint32),
("index", c_int)]
class reb_binary_field_descriptor(Structure):
def __repr__(self):
return '<{0}.{1} object at {2}, type={3}, dtype={4}, name=\'{5}\'>'.format(self.__module__, type(self).__name__, hex(id(self)), self.type, self.dtype, self.name.decode("ascii"))
_fields_ = [("type", c_uint),
("dtype", c_int),
("name", c_char*1024),
("offset", c_size_t),
("offset_N", c_size_t),
("element_size", c_size_t),
]
def binary_field_descriptor_list():
fd_pointer_t = POINTER(reb_binary_field_descriptor)
fd_pointer = (reb_binary_field_descriptor*3).in_dll(clibrebound, "reb_binary_field_descriptor_list")
fd_pointer = cast(fd_pointer, fd_pointer_t) # not sure why I have to do it this way
l = []
i=0
while True:
l.append(fd_pointer[i])
if fd_pointer[i].name == b'end':
break
i += 1
return l
class Rotation(Structure):
"""
This class facilitates rotations of Vec3d objects, and provides various convenience functions
for commonly used rotations in celestial mechanics.
"""
def __init__(self, ix=None, iy=None, iz=None, r=None, angle=None, axis=None):
"""
Rotations are implemented as quaternions r + (ix)i + (iy)j + (iz)k. To initialize one
can directly pass a set of the real numbers (ix, iy, iz, r). Alternatively one can pass
an axis vector for the rotation axis, and an angle of rotation (counter-clockwise around axis).
Only one full set of (r, ix, iy, iz) OR (angle, axis) must be passed.
Arguments
---------
ix : float
Coefficient of i in quaternion r + (ix)i + (iy)j + (iz)k
iy : float
Coefficient of j in quaternion r + (ix)i + (iy)j + (iz)k
iz : float
Coefficient of k in quaternion r + (ix)i + (iy)j + (iz)k
r : float
Real part of quaternion r + (ix)i + (iy)j + (iz)k
angle: float
Angle (in radians) by which to rotate counterclockwise around passed axis
axis: rebound.Vec3d, list, numpy array, or character
3D vector specifying the axis of rotation. The characters "x", "y", "z" are
shorthand for [1,0,0], [0,1,0], [0,0,1].
"""
cart = [ix, iy, iz, r]
angle_axis = [angle, axis]
if cart.count(None) == len(cart) and angle_axis.count(None) == len(angle_axis):
super(Rotation, self).__init__(0.0,0.0,0.0,1.0) # Identity
return
if cart.count(None) != 0 and angle_axis.count(None) == len(angle_axis):
raise ValueError("You need to specify all four parameters ix, iy, iz, r.")
if angle_axis.count(None) != 0 and cart.count(None) == len(cart):
raise ValueError("You need to specify both angle and axis.")
if cart.count(None) < len(cart) and angle_axis.count(None) < len(angle_axis):
raise ValueError("Cannot mix parameters ix, iy, iz, r with angle, axis.")
if cart.count(None) == 0:
super(Rotation, self).__init__(ix, iy, iz, r)
if angle_axis.count(None) == 0:
clibrebound.reb_rotation_init_angle_axis.restype = Rotation
q = clibrebound.reb_rotation_init_angle_axis(c_double(angle), Vec3d(axis)._vec3d)
super(Rotation, self).__init__(q.ix, q.iy, q.iz, q.r)
@classmethod
def from_to(cls, fromv, tov):
"""
Returns a Rotation object that maps the 3D fromv vector to the 3D tov vector, i.e., Rotation * fromv = tov.
Specifically, the rotation is done counterclockwise around the fromv cross tov axis.
Arguments
---------
fromv: list-like (e.g. list, numpy array), or character
Input 3D vector that Rotation will map to vector tov
tov: list-like (e.g. list, numpy array), or character
Output 3D vector when Rotation is applied to fromv
Examples
--------
>>> fromv = [2,3,-5] # An arbitrary vector
>>> rot = rebound.Rotation.from_to(fromv=fromv, tov=[0,0,1]) # Get a rotation that maps fromv to the z axis
>>> print(rot * fromv) # When our rotation acts on fromv, we get back [0,0,1]
"""
# "from" is a keyword, need to use somethign else: "fromv"
_from = Vec3d(fromv)
_to = Vec3d(tov)
clibrebound.reb_rotation_init_from_to.restype = cls
q = clibrebound.reb_rotation_init_from_to(_from._vec3d, _to._vec3d)
return q
@classmethod
def orbit(cls, Omega=0.0, inc=0.0, omega=0.0):
"""
Consider an orbit, which in a reference coordinate system has standard orbital angles Omega, inc, omega.
This returns a rotation object 'rot' that rotates vectors into this inclined orbital plane. The vector [1,0,0]
is rotated such that it points towards the pericenter of the orbit. Murray & Dermott Eq. 2.121 (left hand side)
Arguments
---------
Omega: float
Longitude of ascending node (default 0)
inc: float
Inclination (default 0)
omega: float
Argument of pericenter (default 0)
Examples
--------
>>> a, e, inc, Omega, omega = 1, 0.1, 0.2, 0.3, 0.4 # Make up arbitrary numbers
>>> rot = rebound.Rotation.orbit(Omega=Omega, inc=inc, omega=omega) # Initialize a Rotation to that specific orbit
>>> sim = rebound.Simulation()
>>> sim.add(m=1)
>>> sim.add(a=a, e=e, inc=inc, Omega=Omega, omega=omega) # 3D orbit
>>> sim.add(a=a, e=e) # Orbit in the xy plane
>>> print(sim.particles[1].xyz)
>>> print(rot * sim.particles[2].xyz) # Same location as other particle after applying rot
"""
clibrebound.reb_rotation_init_orbit.restype = cls
q = clibrebound.reb_rotation_init_orbit(c_double(Omega), c_double(inc), c_double(omega))
return q
@classmethod
def to_new_axes(cls, newz, newx=None):
"""
Returns a rotation object that rotates vectors into a new coordinate system with the z axis pointing along
the vector newz. If newx is passed, the new x axis will point along newx. If not, the new x axis defaults
to the intersection betweeen the new xy plane (normal to newz) and the original xy plane (specifically along
the z cross newz direction). The new y axis completes a right-handed coordinate system.
If passed, newx should be perpendicular to newz. This function will only take the component of newx that is
perpendicular to newz, in order to avoid any rounding issues.
Arguments
---------
newz: list-like (e.g. list, numpy array)
3D vector to use as the new z axis
newx: list-like (e.g. list, numpy array)
3D vector to use as the new x axis. If not passed, defaults to the intersection between the new xy plane
(normal to newz) and the original xy plane (along the z cross newz direction).
Examples
--------
>>> sim = rebound.Simulation() # Initialize a sim with two Jupiters on arbitrary inclined orbits
>>> sim.add(m=1)
>>> sim.add(m=1e-3, a=1, inc=0.3, Omega=4.2)
>>> sim.add(m=1e-3, a=2, inc=0.1, Omega=0.5) # Get a rotation to new axes where z points along total ang. momentum L. Didn't specify newx,
>>> rot = rebound.Rotation.to_new_axes(newz=sim.angular_momentum()) # so it points along line of nodes between our original xy plane
>>> sim.rotate(rot) # and the new plane perp. to L. Rotate our simulation into this new reference system
>>> print(sim.angular_momentum()) # Now the total angular momentum points in the z direction as we expect.
"""
if not newx: # newx not specified, newx will point along z cross newz (line of nodes)
clibrebound.reb_vec3d_cross.restype = _Vec3d
newx = Vec3d(clibrebound.reb_vec3d_cross(Vec3d(0,0,1)._vec3d, Vec3d(newz)._vec3d))
mag = (newx.x**2 + newx.y**2 + newx.z**2)**(0.5)
if mag < 1e-15: # z and newz point in the same direction, so line of nodes undefined, don't rotate x
newx.x, newx.y, newx.z = 1,0,0
clibrebound.reb_rotation_init_to_new_axes.restype = cls
q = clibrebound.reb_rotation_init_to_new_axes(Vec3d(newz)._vec3d, Vec3d(newx)._vec3d)
return q
def inverse(self):
"""
Returns a Rotation object's inverse rotation.
"""
clibrebound.reb_rotation_inverse.restype = Rotation
q = clibrebound.reb_rotation_inverse(self)
return q
def __mul__(self, other):
if isinstance(other, Rotation):
clibrebound.reb_rotation_mul.restype = Rotation
q = clibrebound.reb_rotation_mul(self, other)
return q
if isinstance(other, Particle):
p = other.copy()
p.rotate(self)
return p
if isinstance(other, Simulation):
s = other.copy()
s.rotate(self)
return s
try:
vec = Vec3d(other) # make copy if vec3d, try to convert to vec3d if list-like
clibrebound.reb_vec3d_irotate(byref(vec._vec3d), self) # rotate vector in place
return vec
except:
return NotImplemented
def __repr__(self):
return '<{0}.{1} object at {2}, ix={3}, iy={4}, iz={5}, r={6}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.ix, self.iy, self.iz, self.r)
_fields_ = [("ix", c_double),
("iy", c_double),
("iz", c_double),
("r", c_double)]
class _Vec3d(Structure):
"""
Internal use only. Not used as Vec3d directly because assigments to numpy arrays don't worl
"""
_fields_ = [("x", c_double),
("y", c_double),
("z", c_double)]
class Vec3d:
"""
Class for 3D Cartesian vectors.
"""
_vec3d = None
@property
def __array_interface__(self):
return {"shape": (3,), "typestr": "<f8", "data": self._vec3d}
def __init__(self, *args):
if len(args) == 1:
vec = args[0]
if isinstance(vec,_Vec3d):
vec = [vec.x, vec.y, vec.z]
elif isinstance(vec,str):
vec = vec.lower()
if vec != "x" and vec !="y" and vec != "z":
raise ValueError("When passing a string to create a Vec3D, it needs to be one of 'x', 'y', or 'z'")
if vec == "x":
vec = [1,0,0]
elif vec == "y":
vec = [0,1,0]
if vec == "z":
vec = [0,0,1]
else:
vec = [float(vec[0]), float(vec[1]), float(vec[2])]
elif len(args) >= 3:
vec = [float(args[0]), float(args[1]), float(args[2])]
self._vec3d =_Vec3d(vec[0],vec[1],vec[2])
def __mul__(self, other):
try:
return Vec3d([self.x*other, self.y*other, self.z*other])
except:
return NotImplemented
def __truediv__(self, other):
if other==0.:
raise ZeroDivisionError
try:
return Vec3d([self.x/other, self.y/other, self.z/other])
except:
return NotImplemented
def __add__(self, other):
try:
o = Vec3d(other)
return Vec3d([self[0]+other[0], self[1]+other[1], self[2]+other[2]])
except:
return NotImplemented
def __sub__(self, other):
try:
o = Vec3d(other)
return Vec3d([self[0]-other[0], self[1]-other[1], self[2]-other[2]])
except:
return NotImplemented
def rotate(self, q):
if not isinstance(q, Rotation):
raise NotImplementedError
clibrebound.reb_vec3d_irotate(byref(_vec3d), q)
return self
def normalize(self):
clibrebound.reb_vec3d_normalize.restype = _Vec3d
r = clibrebound.reb_vec3d_normalize(self._vec3d)
self._vec3d = r._vec3d
return self
def __getitem__(self, key):
if not isinstance(key, int):
raise IndexError("Index must be an integer.")
if key < 0 or key >= 3:
raise IndexError("Vec3d has exactly three elements and can therefore not access the item with index "+str(key)+".")
if key == 0:
return self._vec3d.x
if key == 1:
return self._vec3d.y
if key == 2:
return self._vec3d.z
def __setitem__(self, key, value):
if not isinstance(key, int):
raise IndexError("Index must be an integer.")
if key < 0 or key >= 3:
raise IndexError("Vec3d has exactly three elements and can therefore not access the item with index "+str(key)+".")
if key == 0:
self._vec3d.x = c_double(value)
if key == 1:
self._vec3d.y = c_double(value)
if key == 2:
self._vec3d.z = c_double(value)
@property
def x(self):
return self._vec3d.x
@x.setter
def x(self, v):
self._vec3d.x = v
@property
def y(self):
return self._vec3d.y
@y.setter
def y(self, v):
self._vec3d.y = v
@property
def z(self):
return self._vec3d.z
@z.setter
def z(self, v):
self._vec3d.z = v
def __repr__(self):
return '<{0}.{1} object at {2}, [{3}, {4}, {5}]>'.format(self.__module__, type(self).__name__, hex(id(self)), self._vec3d.x, self._vec3d.y, self._vec3d.z)
class reb_dp7(Structure):
_fields_ = [("p0", POINTER(c_double)),
("p1", POINTER(c_double)),
("p2", POINTER(c_double)),
("p3", POINTER(c_double)),
("p4", POINTER(c_double)),
("p5", POINTER(c_double)),
("p6", POINTER(c_double))]
class reb_ghostbox(Structure):
_fields_ = [("shiftx", c_double),
("shifty", c_double),
("shiftz", c_double),
("shiftvx", c_double),
("shiftvy", c_double),
("shiftvz", c_double)]
class reb_collision(Structure):
_fields_ = [("p1", c_int),
("p2", c_int),
("gb", reb_ghostbox),
("ri", c_int)]
def __repr__(self):
return '<{0}.{1} object at {2}, p1={3}, p2={4}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.p1, self.p2)
class reb_simulation_integrator_sei(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_sei.
It controls the behaviour of the symplectic SEI integrator for shearing
sheet calculations. It is described in Rein and Tremaine (2011).
This struct should be accessed via the simulation class only. Here is an
example:
>>> sim = rebound.Simulation()
>>> sim.ri_sei.OMEGA = 1.58
:ivar float OMEGA:
The epicyclic frequency OMEGA. For simulations making use of shearing
sheet boundary conditions, REBOUND needs to know the epicyclic frequency.
By default, OMEGA is 1. For more details read Rein and Tremaine 2011.
:ivar float OMEGAZ:
The z component of the epicyclic frequency OMEGA. By default, it is assuming
OMEGAZ is the same as OMEGA.
"""
_fields_ = [("OMEGA", c_double),
("OMEGAZ", c_double),
("_lastdt", c_double),
("_sindt", c_double),
("_tandt", c_double),
("_sindtz", c_double),
("_tandtz", c_double)]
class reb_simulation_integrator_ias15(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_ias15.
It controls the behaviour of the SEI integrator. See Rein & Spiegel (2015)
for more information.
Example usage:
>>> sim = rebound.Simulation()
>>> sim.integrator = "ias15"
>>> sim.ri_ias15.epsilon = 0.
:ivar float epsilon:
Controls the precision of the integrator. Set to 0 for fixed timesteps.
:ivar float min_dt:
IAS15 is an adaptive method. This sets the minimum timestep.
:ivar float adaptive_mode:
Determines how the adaptive timestep is chosen.
This replaces the previous epsilon_global variable.
TODO: list options.
"""
def __repr__(self):
return '<{0}.{1} object at {2}, epsilon={3}, min_dt={4}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.epsilon, self.min_dt)
_fields_ = [("epsilon", c_double),
("min_dt", c_double),
("adaptive_mode", c_uint),
("_iterations_max_exceeded", c_ulong),
("_allocated_N", c_uint),
("_at", POINTER(c_double)),
("_x0", POINTER(c_double)),
("_v0", POINTER(c_double)),
("_a0", POINTER(c_double)),
("_csx", POINTER(c_double)),
("_csv", POINTER(c_double)),
("_csa0", POINTER(c_double)),
("_g", reb_dp7),
("_b", reb_dp7),
("_csb", reb_dp7),
("_e", reb_dp7),
("_br", reb_dp7),
("_er", reb_dp7),
("_map", POINTER(c_int)),
("_map_allocated_n", c_uint),
]
class reb_simulation_integrator_saba(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_saba.
It controls the behaviour of the SABA integrator family.
See Rein, Tamayo, and Brown (2019) for more details.
:ivar str type:
Set the type of SABA integrator manually. The type can also be set by setting
the integrator field in the REBOUND simulation.
:ivar int safe_mode:
This variable acts the same as for WHFast.
If safe_mode is 1 (default) particles can be modified between
timesteps and particle velocities and positions are always synchronised.
If you set safe_mode to 0, the speed and accuracy of the integrator will improve.
However, make sure you are aware of the consequences. Read the iPython tutorial
on advanced WHFast usage to learn more.
Example usage:
>>> sim = rebound.Simulation()
>>> sim.integrator = "SABA(10,6,4)"
>>> sim = rebound.Simulation()
>>> sim.integrator = "SABA"
>>> sim.ri_saba.type = "(10,6,4)"
>>> sim = rebound.Simulation()
>>> sim.integrator = "SABACL4"
>>> sim.ri_saba.safe_mode = 0
"""
_fields_ = [("_type", c_uint),
("safe_mode", c_uint),
("is_synchronized", c_uint),
("keep_unsynchronized", c_uint),
]
@property
def type(self):
"""
Get or set the type of SABA integrator.
"""
i = self._type
for name, _i in SABA_TYPES.items():
if i==_i:
return name
return i
@type.setter
def type(self, value):
if isinstance(value, int):
self._type = value
elif isinstance(value, basestring):
value = value.lower().replace(" ", "").replace("(", "").replace(")", "")
if value in SABA_TYPES:
self._type = SABA_TYPES[value]
else:
raise ValueError("Warning. SABA type not found.")
class reb_simulation_integrator_whfast(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_whfast.
It controls the behaviour of the symplectic WHFast integrator described
in Rein and Tamayo (2015) and in Rein, Tamayo, and Brown (2019).
This struct should be accessed via the simulation class only. Here is an
example:
>>> sim = rebound.Simulation()
>>> sim.integrator = "whfast"
>>> sim.ri_whfast.corrector = 11
>>> sim.ri_whfast.kernel = "lazy"
:ivar int corrector:
The order of the symplectic corrector in the WHFast integrator.
By default, the symplectic correctors are turned off (=0). For high
accuracy simulation set this value to 11 or 17. For more details read
Rein and Tamayo (2015).
:ivar int corrector2:
Second correctors (C2 of Wisdom et al 1996).
By default, the second symplectic correctors are turned off (=0).
Set to 1 to turn them on.
:ivar int/string kernel:
Kernel option. Set to 0 for the default WH kernel (standard kick step).
Other options are "modifiedkick" (1), "composition" (2), "lazy" (3).
:ivar int recalculate_coordinates_this_timestep:
Sets a flag that tells WHFast that the particles have changed.
Setting this flag to 1 (default 0) triggers the WHFast integrator
to recalculate Jacobi/heliocenctric coordinates. This is needed
if the user changes the particle position, velocity or mass
in-between timesteps. After every timestep the flag is set back
to 0, so if you continuously update the particles manually,
you need to set this flag to 1 after every timestep.
:ivar string coordinates:
Sets the internal coordinate system that WHFast is using. By default
it uses ``'jacobi'`` (=0) coordinates. Other options are
``'democraticheliocentric'`` (=1) and ``'whds'`` (=2). See Hernandez
and Dehnen (2017) for more information.
:ivar int safe_mode:
If safe_mode is 1 (default) particles can be modified between
timesteps and particle velocities and positions are always synchronised.
If you set safe_mode to 0, the speed and accuracy of WHFast improve.
However, make sure you are aware of the consequences. Read the iPython tutorial
on advanced WHFast usage to learn more.
"""
_fields_ = [("corrector", c_uint),
("corrector2", c_uint),
("_kernel", c_uint),
("_coordinates", c_uint),
("recalculate_coordinates_this_timestep", c_uint),
("safe_mode", c_uint),
("keep_unsynchronized", c_uint),
("_p_jh", POINTER(Particle)),
("_p_temp", POINTER(Particle)),
("is_synchronized", c_uint),
("_allocated_N", c_uint),
("_allocated_Ntmp", c_uint),
("_timestep_warning", c_uint),
("_recalculate_coordinates_but_not_synchronized_warning", c_uint)]
def __repr__(self):
return '<{0}.{1} object at {2}, safe_mode={3}, keep_unsynchonized={4}, is_synchronized={5}, corrector={6}, corrector2={7}, kernel={8}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.safe_mode, self.keep_unsynchronized, self.is_synchronized, self.corrector, self.corrector2, self.kernel)
@property
def coordinates(self):
"""
Get or set the internal coordinate system.
Available coordinate systems are:
- ``'jacobi'`` (default)
- ``'democraticheliocentric'``
- ``'whds'``
"""
i = self._coordinates
for name, _i in WHFAST_COORDINATES.items():
if i==_i:
return name
return i
@coordinates.setter
def coordinates(self, value):
if isinstance(value, int):
self._coordinates = c_uint(value)
elif isinstance(value, basestring):
value = value.lower()
if value in WHFAST_COORDINATES:
self._coordinates = WHFAST_COORDINATES[value]
else:
raise ValueError("Warning. Coordinate system not found.")
@property
def kernel(self):
"""
Get or set the WHFast Kernel.
Available kernels are:
- ``'default'`` (standard WH kernel, kick)
- ``'modifiedkick'`` (modified kick for newtonian gravity)
- ``'composition'`` (Wisdom's composition method)
- ``'lazy'`` (Lazy implementer's method)
"""
i = self._kernel
for name, _i in WHFAST_KERNELS.items():
if i==_i:
return name
return i
@kernel.setter
def kernel(self, value):
if isinstance(value, int):
self._kernel = c_uint(value)
elif isinstance(value, basestring):
value = value.lower().replace(" ", "")
if value in WHFAST_KERNELS:
self._kernel = WHFAST_KERNELS[value]
else:
raise ValueError("Warning. Kernel not found.")
class Orbit(Structure):
"""
A class containing orbital parameters for a particle.
This is an abstraction of the reb_orbit data structure in C.
When using the various REBOUND functions using Orbits, all angles are in radians.
The following image illustrated the most important angles used.
In REBOUND the reference direction is the positive x direction, the reference plane
is the xy plane.
.. image:: images/orbit.png
:width: 500px
:height: 450px
Image from wikipedia. CC-BY-SA-3.
Attributes
----------
d : float
radial distance from reference
v : float
velocity relative to central object's velocity
h : float
specific angular momentum
P : float
orbital period (negative if hyperbolic)
n : float
mean motion (negative if hyperbolic)
a : float
semimajor axis
e : float
eccentricity
inc : float
inclination
Omega : float
longitude of ascending node
omega : float
argument of pericenter
pomega : float
longitude of pericenter
f : float
true anomaly
M : float
mean anomaly
E : float
eccentric anomaly (requires solving Kepler's equation - only calculated when needed)
l : float
mean longitude = Omega + omega + M
theta : float
true longitude = Omega + omega + f
T : float
time of pericenter passage
rhill : float
Hill radius ( =a*pow(m/(3M),1./3.) )
pal_h : float
Cartesian component of the eccentricity ( = e*sin(pomega) )
pal_k : float
Cartesian component of the eccentricity ( = e*cos(pomega) )
pal_ix : float
Cartesian component of the inclination ( = 2*sin(i/2)*cos(Omega) )
pal_iy : float
Cartesian component of the inclination ( = 2*sin(i/2)*sin(Omega) )
hvec : Vec3d
Specific angular momentum vector
evec : Vec3d
Eccentricity vector (mag = eccentricity, points toward pericenter)
"""
_fields_ = [("d", c_double),
("v", c_double),
("h", c_double),
("P", c_double),
("n", c_double),
("a", c_double),
("e", c_double),
("inc", c_double),
("Omega", c_double),
("omega", c_double),
("pomega", c_double),
("f", c_double),
("M", c_double),
("l", c_double),
("theta", c_double),
("T", c_double),
("rhill", c_double),
("pal_h", c_double),
("pal_k", c_double),
("pal_ix", c_double),
("pal_iy", c_double),
("hvec", _Vec3d),
("evec", _Vec3d)]
def __str__(self):
"""
Returns a string with the semi-major axis and eccentricity of the orbit.
"""
return "<rebound.Orbit instance, a={0} e={1} inc={2} Omega={3} omega={4} f={5}>".format(str(self.a),str(self.e), str(self.inc), str(self.Omega), str(self.omega), str(self.f))
@property
def E(self):
return M_to_E(self.e, self.M)
class Simulation(Structure):
"""
This is the REBOUND Simulation Class.
In encapsulated an entire REBOUND simulation and is an abstraction of the C struct reb_simulation.
### Examples
Most simulation parameters can be directly changed with the property syntax:
>>> sim = rebound.Simulation()
>>> sim.G = 1. # Sets the graviational constant (default 1)
>>> sim.softening = 1. # Sets the graviational softening parameter (default 0)
>>> sim.testparticle_type = 1 # Allows massive particles to feel influence from testparticles (default 0)
>>> sim.dt = 0.1 # Sets the timestep (will change for adaptive integrators such as IAS15).
>>> sim.t = 0. # Sets the current simulation time (default 0)
>>> print(sim.N) # Gets the current number of particles
>>> print(sim.N_active) # Gets the current number of active particles
By calling rebound.Simulation() as shown above, you create a new simulation object
The following example creates a simulation, saves it to a file and then creates
a copy of the simulation store in the binary file.
>>> sim = rebound.Simulation()
>>> sim.add(m=1.)
>>> sim.add(m=1.e-3,x=1.,vy=1.)
>>> sim.save("simulation.bin")
>>> sim_copy = rebound.Simulation("simulation.bin")
Similarly, you can create a simulation, from a simulation archive
by specifying the snapshot you want to load.
>>> sim = rebound.Simulation("archive.bin", 34)
or
>>> sim = rebound.Simulation(filename="archive.bin", snapshot=34)
Finally, you can also create a new Simulation by passing a bytes object of a
SimulationArchive to Simulation():
>>> sim = rebound.Simulation(open("archive.bin","rb").read())
"""
def __new__(cls, *args, **kw):
# Handle arguments
filename = None
if len(args)>0:
# If first argument is of type bytes, then this is unpickling a Simulation
if isinstance(args[0], bytes):
l = len(args[0])
buft = c_char * l
buf = buft.from_buffer_copy(args[0])
# Note: Not calling SimulationArchive.
# Doing this manually here because we need to keep the reference to buf.
# So we can later access the contents of the archive to get the simulation.
sa = SimulationArchive.__new__(SimulationArchive, None, None)
w = c_int(0)
clibrebound.reb_read_simulationarchive_from_buffer_with_messages(byref(sa), byref(buf), c_size_t(l), None, byref(w))
sim = super(Simulation,cls).__new__(cls)
clibrebound.reb_init_simulation(byref(sim))
clibrebound.reb_create_simulation_from_simulationarchive_with_messages(byref(sim),byref(sa),c_int(-1),byref(w))
for majorerror, value, message in BINARY_WARNINGS:
if w.value & value:
if majorerror:
raise RuntimeError(message)
else:
# Just a warning
warnings.warn(message, RuntimeWarning)
return sim
# Otherwise assume first argument is filename
filename = args[0]
if "filename" in kw:
filename = kw["filename"]
snapshot = -1
if len(args)>1:
snapshot = args[1]
if "snapshot" in kw:
snapshot = kw["snapshot"]
# Create simulation
if filename is None:
# Create a new simulation
sim = super(Simulation,cls).__new__(cls)
clibrebound.reb_init_simulation(byref(sim))
return sim
else:
# Recreate exisitng simulation
sa = SimulationArchive(filename,process_warnings=False)
sim = super(Simulation,cls).__new__(cls)
clibrebound.reb_init_simulation(byref(sim))
w = sa.warnings # warnings will be appended to previous warnings (as to not repeat them)
clibrebound.reb_create_simulation_from_simulationarchive_with_messages(byref(sim),byref(sa),c_int(snapshot),byref(w))
for majorerror, value, message in BINARY_WARNINGS:
if w.value & value:
if majorerror:
raise RuntimeError(message)
else:
# Just a warning
warnings.warn(message, RuntimeWarning)
return sim
def __init__(self,filename=None,snapshot=None):
self.save_messages = 1 # Warnings will be checked within python
def __repr__(self):
return '<{0}.{1} object at {2}, N={3}, t={4}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.N, self.t)
@classmethod
def from_archive(cls, filename,snapshot=-1):
""" rebound.Simulation.from_archive(filename,snapshot) is deprecated and will be removed in the futute. Use rebound.Simulation(filename,snapshot) instead """
warnings.warn( "rebound.Simulation.from_archive(filename,snapshot) is deprecated and will be removed in the future. Use rebound.Simulation(filename,snapshot) instead", FutureWarning)
return cls(filename=filename,snapshot=snapshot)
@classmethod
def from_file(cls, filename):
""" rebound.Simulation.from_file(filename) is deprecated and will be removed in the future. Use rebound.Simulation(filename) instead """
warnings.warn( "rebound.Simulation.from_file(filename) is deprecated and will be removed in the future. Use rebound.Simulation(filename) instead", FutureWarning)
return cls(filename=filename)
def copy(self):
"""
Returns a deep copy of a REBOUND simulation. You need to reset
any function pointers on the copy.
Returns
-------
A rebound.Simulation object.
"""
w = c_int(0)
sim = Simulation()
clibrebound.reb_copy_simulation_with_messages(byref(sim),byref(self),byref(w))
for majorerror, value, message in BINARY_WARNINGS:
if w.value & value:
if majorerror:
raise RuntimeError(message)
else:
# Just a warning
warnings.warn(message, RuntimeWarning)
return sim
def cite(self):
"""
Generate citations
This function generates citations to papers relevant to the current
setting of the simulation.
"""
txt, bib = cite(self)
# one could check for REBOUNDx here, then append txt and bib accordingly
print(txt + "\n\n\n" + bib)
def getWidget(self,**kwargs):
"""
Wrapper function that returns a new widget attached to this simulation.
Widgets provide real-time 3D visualizations from within a Jupyter notebook.
See the Widget class for more details on the possible arguments.
Arguments
---------
All arguments passed to this wrapper function will be passed to /Widget class.
Returns
-------
A rebound.Widget object.
Examples
--------
>>> sim = rebound.Simulation()
>>> sim.add(m=1.)
>>> sim.add(m=1.e-3,x=1.,vy=1.)
>>> sim.getWidget()
"""
from .widget import Widget # ondemand
from ipywidgets import DOMWidget
from IPython.display import display, HTML
if not hasattr(self, '_widgets'):
self._widgets = []
def display_heartbeat(simp):
for w in self._widgets:
w.refresh(simp,isauto=1)
self.visualization = VISUALIZATIONS["webgl"]
clibrebound.reb_display_init_data(byref(self))
self._dhbf = AFF(display_heartbeat)
self._display_heartbeat = self._dhbf
display(HTML(Widget.getClientCode())) # HACK! Javascript should go into custom.js
newWidget = Widget(self,**kwargs)
self._widgets.append(newWidget)
newWidget.refresh(isauto=0)
return newWidget
def refreshWidgets(self):
"""
This function manually refreshed all widgets attached to this simulation.
You want to call this function if any particle data has been manually changed.
"""
if hasattr(self, '_widgets'):
for w in self._widgets:
w.refresh(isauto=0)
else:
raise RuntimeError("No widgets found")
# Simulation Archive tools
def automateSimulationArchive(self, filename, interval=None, walltime=None, step=None, deletefile=False):
"""
This function automates taking snapshots during a simulation using the Simulation Archive.
Instead of using this function, one can also call simulationarchive_snapshot() manually
to create snapshots.
Arguments
---------
filename : str
Filename of the binary file.
interval : float
Interval between outputs in code units.
walltime : float
Interval between outputs in wall time (seconds).
Useful when using IAS15 with adaptive timesteps.
step : int
Interval between outputs in number of timesteps.
Useful when outputs need to be spaced exactly.
deletefile: bool
False (default) appends to archive if it exists.
True deletes filename and starts a new archive.
Examples
--------
The following example creates a simulation, then
initializes the Simulation Archive and integrates
it forward in time.
>>> sim = rebound.Simulation()
>>> sim.add(m=1.)
>>> sim.add(m=1.e-3,x=1.,vy=1.)
>>> sim.automateSimulationArchive("sa.bin",interval=1000.)
>>> sim.integrate(1e8)
The SimulationArchive can later be read in using the following syntax:
>>> sa = rebound.SimulationArchive("sa.bin")
>>> sim = sa[0] # get the first snapshot in the SA file (initial conditions)
>>> sim = sa[-1] # get the last snapshot in the SA file
"""
modes = sum(1 for i in [interval, walltime,step] if i is not None)
if modes != 1:
raise AttributeError("Need to specify either interval, walltime, or step")
if deletefile and os.path.isfile(filename):
os.remove(filename)
# reset intervals so that automate functions C set sim->next consistently
self.simulationarchive_auto_interval=0
self.simulationarchive_auto_walltime=0
self.simulationarchive_auto_step=0
if interval:
clibrebound.reb_simulationarchive_automate_interval(byref(self), c_char_p(filename.encode("ascii")), c_double(interval))
if walltime:
clibrebound.reb_simulationarchive_automate_walltime(byref(self), c_char_p(filename.encode("ascii")), c_double(walltime))
if step:
clibrebound.reb_simulationarchive_automate_step(byref(self), c_char_p(filename.encode("ascii")), c_ulonglong(step))
self.process_messages()
def simulationarchive_snapshot(self, filename, deletefile=False):
"""
Take a snapshot and save it to a SimulationArchive file.
If the file does not exist yet, a new one will be created.
If the file does exist, a snapshot will be appended.
Arguments
---------
filename : str
Filename of the binary file.
deletefile: bool
False (default) appends to archive if it exists.
True deletes filename and starts a new archive.
"""
if deletefile and os.path.isfile(filename):
os.remove(filename)
clibrebound.reb_simulationarchive_snapshot(byref(self), c_char_p(filename.encode("ascii")))
self.process_messages()
@property
def simulationarchive_filename(self):
"""
Returns the current SimulationArchive filename in use.
Do not set manually. Use sim.automateSimulationArchive() instead
"""
return self._simulationarchive_filename
# Message and memory management functions
def process_messages(self):
clibrebound.reb_get_next_message.restype = c_int
buf = create_string_buffer(c_int.in_dll(clibrebound, "reb_max_messages_length").value)
while clibrebound.reb_get_next_message(byref(self), buf):
msg = buf.value.decode("ascii")
if msg[0]=='w':
warnings.warn(msg[1:], RuntimeWarning)
elif msg[0]=='e':
raise RuntimeError(msg[1:])
# Pickling methods: return SimulationArchive binary
def __reduce__(self):
buf = c_char_p()
size = c_size_t()
clibrebound.reb_output_binary_to_stream(byref(self), byref(buf), byref(size))
s = bytes(string_at(buf, size=size.value)) #make copy
clibrebound.reb_output_free_stream(buf) # free original
return (Simulation, (s,))
# Other operators
def __del__(self):
if self._b_needsfree_ == 1: # to avoid, e.g., sim.particles[1]._sim.contents.G creating a Simulation instance to get G, and then freeing the C simulation when it immediately goes out of scope
clibrebound.reb_free_pointers(byref(self))
def __eq__(self, other):
# This ignores the walltime parameter
if not isinstance(other,Simulation):
return NotImplemented
clibrebound.reb_diff_simulations.restype = c_int
ret = clibrebound.reb_diff_simulations(byref(self), byref(other),c_int(2))
return not ret
def diff(self, other):
if not isinstance(other,Simulation):
return NotImplemented
clibrebound.reb_diff_simulations_char.restype = c_char_p
output = clibrebound.reb_diff_simulations_char(byref(other), byref(self))
print(output.decode("utf-8"))
def __add__(self, other):
if not isinstance(other,Simulation):
return NotImplemented
c = self.copy()
return c.__iadd__(other)
def __iadd__(self, other):
if not isinstance(other,Simulation):
return NotImplemented
clibrebound.reb_simulation_iadd.restype = c_int
ret = clibrebound.reb_simulation_iadd(byref(self), byref(other))
if ret==-1:
raise RuntimeError("Cannot add simulations. Check that the simulations have the same number of particles")
return self
def __sub__(self, other):
if not isinstance(other,Simulation):
return NotImplemented
c = self.copy()
return c.__isub__(other)
def __isub__(self, other):
if not isinstance(other,Simulation):
return NotImplemented
clibrebound.reb_simulation_isub.restype = c_int
ret = clibrebound.reb_simulation_isub(byref(self), byref(other))
if ret==-1:
raise RuntimeError("Cannot subtract simulations. Check that the simulations have the same number of particles")
return self
def __mul__(self, other):
try:
other = float(other)
except:
return NotImplemented
c = self.copy()
c.multiply(other, other)
return c
def __imul__(self, other):
try:
other = float(other)
except:
return NotImplemented
self.multiply(other, other)
return self
def __rmul__(self, other):
try:
other = float(other)
except:
return NotImplemented
c = self.copy()
c.multiply(other, other)
return c
def __div__(self, other):
return self.__truediv__(other)
def __idiv__(self, other):
return self.__itruediv__(other)
def __truediv__(self, other):
try:
other = float(other)
except:
return NotImplemented
c = self.copy()
if other==0.:
raise ZeroDivisionError
c.multiply(1./other, 1./other)
return c
def __itruediv__(self, other):
try:
other = float(other)
except:
return NotImplemented
if other==0.:
raise ZeroDivisionError
self.multiply(1./other, 1./other)
return self
def multiply(self, scalar_pos, scalar_vel):
try:
scalar_pos = float(scalar_pos)
scalar_vel = float(scalar_vel)
except:
raise ValueError("Cannot multiply simulation with non-scalars.")
clibrebound.reb_simulation_imul(byref(self), c_double(scalar_pos), c_double(scalar_vel))
def rotate(self, q):
if not isinstance(q, Rotation):
return NotImplemented
clibrebound.reb_simulation_irotate(byref(self), q)
#ODE functions
def create_ode(self, length, needs_nbody=True):
clibrebound.reb_create_ode.restype = POINTER(ODE)
ode_p = clibrebound.reb_create_ode(byref(self), c_int(length))
ode_p.contents.needs_nbody = c_int(needs_nbody)
return ODE.from_address(ctypes.addressof(ode_p.contents))
# Status functions
def status(self, showParticles=True, showAllFields=True):
"""
Prints a summary of the current status
of the simulation.
"""
from rebound import __version__, __build__
s= ""
s += "---------------------------------\n"
s += "REBOUND version: \t%s\n" %__version__
s += "REBOUND built on: \t%s\n" %__build__
s += "Number of particles: \t%d\n" %self.N
s += "Selected integrator: \t" + self.integrator + "\n"
s += "Simulation time: \t%.16e\n" %self.t
s += "Current timestep: \t%f\n" %self.dt
s += "---------------------------------\n"
if self.N>0 and showParticles:
for p in self.particles:
s += str(p) + "\n"
s += "---------------------------------\n"
print(s, end="")
if showAllFields:
print("The following fields have non-default values:")
newsim = Simulation()
clibrebound.reb_diff_simulations_char.restype = c_char_p
output = clibrebound.reb_diff_simulations_char(byref(newsim), byref(self))
print(output.decode("utf-8"))
# Set function pointer for additional forces
@property
def additional_forces(self):
"""
Get or set a function pointer for calculating additional forces in the simulation.
The argument can be a python function or something that can
be cast to a C function of type CFUNCTYPE(None,POINTER(Simulaton)).
If the forces are velocity dependent, the flag
force_is_velocity_dependent needs to be set to 1. Otherwise,
the particle structures might contain incorrect velocity
values.
"""
raise AttributeError("You can only set C function pointers from python.")
@additional_forces.setter
def additional_forces(self, func):
self._afp = AFF(func)
self._additional_forces = self._afp
@property
def pre_timestep_modifications(self):
"""
Get or set a function pointer for pre-timestep modifications.
The argument can be a python function or something that can be cast to a C function or a
python function.
"""
raise AttributeError("You can only set C function pointers from python.")
@pre_timestep_modifications.setter
def pre_timestep_modifications(self, func):
self._pretmp = AFF(func)
self._pre_timestep_modifications = self._pretmp
@property
def post_timestep_modifications(self):
"""
Get or set a function pointer for post-timestep modifications.
The argument can be a python function or something that can be cast to a C function or a
python function.
"""
raise AttributeError("You can only set C function pointers from python.")
@post_timestep_modifications.setter
def post_timestep_modifications(self, func):
self._posttmp = AFF(func)
self._post_timestep_modifications = self._posttmp
@property
def heartbeat(self):
"""
Set a function pointer for a heartbeat function.
The heartbeat function is called every timestep and can be used
to monitor long simulations, check for stalled simulations and
output debugging information.
The argument can be a python function or something that can be
cast to a C function or a python function.
The function called will receive a pointer to the simulation
object as its argument.
Examples
--------
>>> import rebound
>>> sim = rebound.Simulation()
>>> sim.add(m=1.)
>>> sim.add(m=1e-3, a=1)
>>> def heartbeat(sim):
>>> # sim is a pointer to the simulation object,
>>> # thus use contents to access object data.
>>> # See ctypes documentation for details.
>>> print(sim.contents.t)
>>> sim.heartbeat = heartbeat
>>> sim.integrate(1.)
"""
raise AttributeError("You can only set C function pointers from python (not get).")
@heartbeat.setter
def heartbeat(self, func):
self._hb = AFF(func)
self._heartbeat = self._hb
@property
def coefficient_of_restitution(self):
"""
Get or set a function pointer that defined the coefficient of restitution.
"""
raise AttributeError("You can only set C function pointers from python.")
@coefficient_of_restitution.setter
def coefficient_of_restitution(self, func):
self._corfp = CORFF(func)
self._coefficient_of_restitution = self._corfp
@property
def collision_resolve(self):
"""
Get or set a function pointer for collision resolving routine.
Possible options for setting:
1) Function pointer
2) "merge": two colliding particles will merge)
3) "hardsphere": two colliding particles will bounce off using a set coefficient of restitution
"""
raise AttributeError("You can only set C function pointers from python.")
@collision_resolve.setter
def collision_resolve(self, func):
if func == "merge":
clibrebound.reb_set_collision_resolve.restype = None
clibrebound.reb_set_collision_resolve(byref(self), clibrebound.reb_collision_resolve_merge)
elif func == "hardsphere":
clibrebound.reb_set_collision_resolve.restype = None
clibrebound.reb_set_collision_resolve(byref(self), clibrebound.reb_collision_resolve_hardsphere)
elif func == "halt":
clibrebound.reb_set_collision_resolve.restype = None
clibrebound.reb_set_collision_resolve(byref(self), clibrebound.reb_collision_resolve_halt)
else:
self._colrfp = COLRFF(func)
self._collision_resolve = self._colrfp
@property
def free_particle_ap(self):
"""
Get or set a function pointer for freeing the ap pointer whenever sim.remove is called.
"""
raise AttributeError("You can only set C function pointers from python.")
@free_particle_ap.setter
def free_particle_ap(self, func):
self._fpa = FPA(func)
self._free_particle_ap = self._fpa
# Setter/getter of parameters and constants
@property
def N_real(self):
"""
Get the current number of real (i.e. no variational/shadow) particles in the simulation.
"""
return self.N-self.N_var
@property
def integrator(self):
"""
Get or set the integrator module.
Available integrators include:
- ``'IAS15'`` (default)
- ``'WHFast'``
- ``'SEI'``
- ``'LEAPFROG'``
- ``'JANUS'``
- ``'MERCURIUS'``
- ``'WHCKL'``
- ``'WHCKM'``
- ``'WHCKC'``
- ``'SABA4'``
- ``'SABACL4'``
- ``'SABACM4'``
- ``'SABA(10,6,4)'``
- ``'EOS'``
- ``'BS'``
- ``'WHFast512'``
- ``'none'``
Check the online documentation for a full description of each of the integrators.
"""
i = self._integrator
for name, _i in INTEGRATORS.items():
if i==_i:
return name
return i
@integrator.setter
def integrator(self, value):
if isinstance(value, int):
self._integrator = c_int(value)
elif isinstance(value, basestring):
value = value.lower()
if value in INTEGRATORS:
self._integrator = INTEGRATORS[value]
# Shortcuts
elif value=="wh":
self.integrator = "whfast"
self.ri_whfast.corrector = 0
self.ri_whfast.kernel = "default"
elif value=="whc":
self.integrator = "whfast"
self.ri_whfast.corrector = 17
self.ri_whfast.kernel = "default"
elif value=="whckl":
self.integrator = "whfast"
self.ri_whfast.corrector = 17
self.ri_whfast.kernel = "lazy"
elif value=="whckm":
self.integrator = "whfast"
self.ri_whfast.corrector = 17
self.ri_whfast.kernel = "modifiedkick"
elif value=="whckc":
self.integrator = "whfast"
self.ri_whfast.corrector = 17
self.ri_whfast.kernel = "composition"
elif value[0:4]=="saba" and len(value)>4:
self.integrator = "saba"
self.ri_saba.type = value[4:]
else:
raise ValueError("Integrator not found.")
@property
def boundary(self):
"""
Get or set the boundary module.
Available boundary modules are:
- ``'none'`` (default)
- ``'open'``
- ``'periodic'``
- ``'shear'``
Check the online documentation for a full description of each of the modules.
"""
i = self._boundary
for name, _i in BOUNDARIES.items():
if i==_i:
return name
return i
@boundary.setter
def boundary(self, value):
if isinstance(value, int):
self._boundary = c_int(value)
elif isinstance(value, basestring):
value = value.lower()
if value in BOUNDARIES:
self._boundary = BOUNDARIES[value]
else:
raise ValueError("Warning. Boundary condition module not found.")
@property
def gravity(self):
"""
Get or set the gravity module.
Available gravity modules are:
- ``'none'``
- ``'basic'`` (default)
- ``'compensated'``
- ``'tree'``
Check the online documentation for a full description of each of the modules.
"""
i = self._gravity
for name, _i in GRAVITIES.items():
if i==_i:
return name
return i
@gravity.setter
def gravity(self, value):
if isinstance(value, int):
self._gravity = c_int(value)
elif isinstance(value, basestring):
value = value.lower()
if value in GRAVITIES:
self._gravity = GRAVITIES[value]
else:
raise ValueError("Warning. Gravity module not found.")
@property
def collision(self):
"""
Get or set the collision module.
Available collision modules are:
- ``'none'`` (default)
- ``'direct'``
- ``'tree'``
- ``'mercurius'``
- ``'direct'``
Check the online documentation for a full description of each of the modules.
"""
i = self._collision
for name, _i in COLLISIONS.items():
if i==_i:
return name
return i
@collision.setter
def collision(self, value):
if isinstance(value, int):
self._collision = c_int(value)
elif isinstance(value, basestring):
value = value.lower()
if value in COLLISIONS:
self.collision = COLLISIONS[value]
else:
raise ValueError("Warning. Collision module not found.")
# Units
@property
def units(self):
"""
Tuple of the units for length, time and mass. Can be set in any order, and strings are not case-sensitive. See ipython_examples/Units.ipynb for more information. You can check the units' exact values and add Additional units in rebound/rebound/units.py. Units should be set before adding particles to the simulation (will give error otherwise).
Currently supported Units
-------------------------
Times:
Hr : Hours
Yr : Julian years
Jyr : Julian years
Sidereal_yr : Sidereal year
Yr2pi : Year divided by 2pi, with year defined as orbital period of planet at 1AU around 1Msun star
Kyr : Kiloyears (Julian)
Myr : Megayears (Julian)
Gyr : Gigayears (Julian)
Lengths:
M : Meters
Cm : Centimeters
Km : Kilometers
AU : Astronomical Units
Masses:
Kg : Kilograms
Msun : Solar masses
Mmercury : Mercury masses
Mvenus : Venus masses
Mearth : Earth masses
Mmars : Mars masses
Mjupiter : Jupiter masses
Msaturn : Saturn masses
Muranus : Neptune masses
Mpluto : Pluto masses
Examples
--------
>>> sim = rebound.Simulation()
>>> sim.units = ('yr', 'AU', 'Msun')
"""
return {'length':hash_to_unit(self.python_unit_l), 'mass':hash_to_unit(self.python_unit_m), 'time':hash_to_unit(self.python_unit_t)}
@units.setter
def units(self, newunits):
newunits = check_units(newunits)
if self.N>0: # some particles are loaded
raise AttributeError("Error: You cannot set the units after populating the particles array. See ipython_examples/Units.ipynb.")
self.update_units(newunits)
def update_units(self, newunits):
clibrebound.reb_hash.restype = c_uint32
self.python_unit_l = clibrebound.reb_hash(c_char_p(newunits[0].encode("ascii")))
self.python_unit_t = clibrebound.reb_hash(c_char_p(newunits[1].encode("ascii")))
self.python_unit_m = clibrebound.reb_hash(c_char_p(newunits[2].encode("ascii")))
self.G = convert_G(newunits)
def convert_particle_units(self, *args):
"""
Will convert the units for the simulation (i.e. convert G) as well as the particles' cartesian elements.
Must have set sim.units ahead of calling this function, so REBOUND knows what units to convert from.
Parameters
----------
3 strings corresponding to units of time, length and mass. Can be in any order and aren't case-sensitive. You can add new units to rebound/rebound/units.py
"""
if self.python_unit_l == 0 or self.python_unit_m == 0 or self.python_unit_t == 0:
raise AttributeError("Must set sim.units before calling convert_particle_units in order to know what units to convert from.")
new_l, new_t, new_m = check_units(args)
for p in self.particles:
units_convert_particle(p, hash_to_unit(self.python_unit_l), hash_to_unit(self.python_unit_t), hash_to_unit(self.python_unit_m), new_l, new_t, new_m)
self.update_units((new_l, new_t, new_m))
# Variational Equations
def add_variation(self,order=1,first_order=None, first_order_2=None, testparticle=-1):
"""
This function adds a set of variational particles to the simulation.
If there are N real particles in the simulation, this functions adds N additional variational
particles. To see how many particles (real and variational) are in a simulation, use ``'sim.N'``.
To see how many variational particles are in a simulation use ``'sim.N_var'``.
Currently Leapfrog, WHFast and IAS15 support first order variational equations. IAS15 also
supports second order variational equations.
Parameters
----------
order : integer, optional
By default, the function adds a set of first order variational particles to the simulation. Set this flag to 2 for second order.
first_order : Variation, optional
Second order variational equations depend on their corresponding first order variational equations.
This parameter expects the Variation object corresponding to the first order variational equations.
first_order_2 : Variation, optional
Same as first_order. But allows to set two different indicies to calculated off-diagonal elements.
If omitted, then first_order will be used for both first order equations.
testparticle : int, optional
If set to a value >= 0, then only one variational particle will be added and be treated as a test particle.
Returns
-------
Returns Variation object (a copy--you can only modify it through its particles property or vary method).
"""
cur_var_config_N = self.var_config_N
if order==1:
index = clibrebound.reb_add_var_1st_order(byref(self),c_int(testparticle))
elif order==2:
if first_order is None:
raise AttributeError("Please specify corresponding first order variational equations when initializing second order variational equations.")
if first_order_2 is None:
first_order_2 = first_order
index = clibrebound.reb_add_var_2nd_order(byref(self),c_int(testparticle),c_int(first_order.index),c_int(first_order_2.index))
else:
raise AttributeError("Only variational equations of first and second order are supported.")
# Need a copy because location of original might shift if more variations added
s = Variation.from_buffer_copy(self.var_config[cur_var_config_N])
return s
# MEGNO
def init_megno(self, seed=None):
"""
This function initialises the chaos indicator MEGNO particles and enables their integration.
MEGNO is short for Mean Exponential Growth of Nearby orbits. It can be used to test
if a system is chaotic or not. In the backend, the integrator is integrating an additional set
of particles using the variational equation. Note that variational equations are better
suited for this than shadow particles. MEGNO is currently only supported in the IAS15
and WHFast integrators.
This function also needs to be called if you are interested in the Lyapunov exponent as it is
calculate with the help of MEGNO. See Rein and Tamayo 2015 for details on the implementation.
For more information on MEGNO see e.g. https://dx.doi.org/10.1051/0004-6361:20011189
"""
if seed is None:
clibrebound.reb_tools_megno_init(byref(self))
else:
clibrebound.reb_tools_megno_init_seed(byref(self), c_uint(seed))
def calculate_megno(self):
"""
Return the current MEGNO value.
Note that you need to call init_megno() before the start of the simulation.
"""
if self._calculate_megno==0:
raise RuntimeError("MEGNO cannot be calculated. Make sure to call init_megno() after adding all particles but before integrating the simulation.")
clibrebound.reb_tools_calculate_megno.restype = c_double
return clibrebound.reb_tools_calculate_megno(byref(self))
def calculate_lyapunov(self):
"""
Return the current Lyapunov Characteristic Number (LCN).
Note that you need to call init_megno() before the start of the simulation.
Different definitions of the LCN exist. Here, we're following Eq 24 of
Cincotta and Simo (2000): https://aas.aanda.org/articles/aas/abs/2000/20/h1686/h1686.html.
To get a timescale (the Lyapunov timescale), take the inverse of this quantity.
"""
if self._calculate_megno==0:
raise RuntimeError("Lyapunov Characteristic Number cannot be calculated. Make sure to call init_megno() after adding all particles but before integrating the simulation.")
clibrebound.reb_tools_calculate_lyapunov.restype = c_double
return clibrebound.reb_tools_calculate_lyapunov(byref(self))
# Particle add function, used to be called particle_add() and add_particle()
def add(self, particle=None, **kwargs):
"""
Adds a particle to REBOUND. Accepts one of the following:
1) A single Particle structure.
2) The particle's mass and a set of cartesian coordinates: m,x,y,z,vx,vy,vz.
3) The primary as a Particle structure, the particle's mass and a set of orbital elements: primary,m,a,anom,e,omega,inv,Omega,MEAN (see :class:`.Orbit` for the definition of orbital elements).
4) A name of an object (uses NASA Horizons to look up coordinates)
5) A list of particles or names.
"""
if particle is not None:
if isinstance(particle, Particle):
if (self.gravity == "tree" or self.collision == "tree") and self.root_size <=0.:
raise ValueError("The tree code for gravity and/or collision detection has been selected. However, the simulation box has not been configured yet. You cannot add particles until the the simulation box has a finite size.")
clibrebound.reb_add(byref(self), particle)
elif isinstance(particle, list):
for p in particle:
self.add(p, **kwargs)
elif isinstance(particle,str):
if self.python_unit_l == 0 or self.python_unit_m == 0 or self.python_unit_t == 0:
self.units = ('AU', 'yr2pi', 'Msun')
self.G = 1.0
builtindatasets = ["solar system", "outer solar system"]
if particle.lower() == "solar system": # built in test dataset
data.add_solar_system(self)
elif particle.lower() == "outer solar system": # built in test dataset
data.add_outer_solar_system(self)
else:
if "frame" not in kwargs:
if hasattr(self, 'default_plane'):
kwargs["plane"] = self.default_plane # allow ASSIST to set default plane
self.add(horizons.getParticle(particle, **kwargs), hash=particle)
units_convert_particle(self.particles[-1], 'km', 's', 'kg', hash_to_unit(self.python_unit_l), hash_to_unit(self.python_unit_t), hash_to_unit(self.python_unit_m))
else:
raise ValueError("Argument passed to add() not supported.")
else:
self.add(Particle(simulation=self, **kwargs))
if hasattr(self, '_widgets'):
self._display_heartbeat(pointer(self))
# Particle getter functions
@property
def particles(self):
"""
Returns a Particles object that allows users to access particles like a dictionary using indices, hashes, or strings.
The Particles object uses pointers and thus the contents update
as the simulation progresses. Note that the pointers could change,
for example when a particle is added or removed from the simulation.
"""
particles = Particles(self)
return particles
@particles.deleter
def particles(self):
"""
Remove all particles from the simulation
"""
clibrebound.reb_remove_all(byref(self))
self.process_messages()
def remove(self, index=None, hash=None, keepSorted=True):
"""
Removes a particle from the simulation.
Parameters
----------
index : int, optional
Specify particle to remove by index.
hash : c_uint32 or string, optional
Specifiy particle to remove by hash (if a string is passed, the corresponding hash is calculated).
keepSorted : bool, optional
By default, remove preserves the order of particles in the particles array.
Might set it to zero in cases with many particles and many removals to speed things up.
"""
if index is not None:
clibrebound.reb_remove(byref(self), index, keepSorted)
if hash is not None:
hash_types = c_uint32, c_uint, c_ulong
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
int_types = int,
else:
string_types = basestring,
int_types = int, long
if isinstance(hash, string_types):
clibrebound.reb_remove_by_hash(byref(self), rebhash(hash), keepSorted)
elif isinstance(hash, int_types):
clibrebound.reb_remove_by_hash(byref(self), c_uint32(hash), keepSorted)
elif isinstance(hash, hash_types):
clibrebound.reb_remove_by_hash(byref(self), hash, keepSorted)
if hasattr(self, '_widgets'):
self._display_heartbeat(pointer(self))
self.process_messages()
def particles_ascii(self, prec=8):
"""
Returns an ASCII string with all particles' masses, radii, positions and velocities.
Parameters
----------
prec : int, optional
Number of digits after decimal point. Default 8.
"""
s = ""
for p in self.particles:
s += (("%%.%de "%prec) * 8)%(p.m, p.r, p.x, p.y, p.z, p.vx, p.vy, p.vz) + "\n"
if len(s):
s = s[:-1]
return s
def add_particles_ascii(self, s):
"""
Adds particles from an ASCII string.
Parameters
----------
s : string
One particle per line. Each line should include particle's mass, radius, position and velocity.
"""
for l in s.split("\n"):
r = l.split()
if len(r):
try:
r = [float(x) for x in r]
p = Particle(simulation=self, m=r[0], r=r[1], x=r[2], y=r[3], z=r[4], vx=r[5], vy=r[6], vz=r[7])
self.add(p)
except:
raise AttributeError("Each line requires 8 floats corresponding to mass, radius, position (x,y,z) and velocity (x,y,z).")
# Orbit calculation
def calculate_orbits(self, primary=None, jacobi_masses=False):
"""
Calculate orbital parameters for all particles in the simulation.
By default, this functions returns the orbits in Jacobi coordinates.
If MEGNO is enabled, variational particles will be ignored.
Parameters
----------
primary : rebound.Particle, optional
Set the primary against which to reference the osculating orbit. Default (use Jacobi center of mass).
For heliocentric coordinates, pass the central object to this parameter.
jacobi_masses: bool
Whether to use jacobi primary mass in orbit calculation. (Default: False)
Returns
-------
Returns an array of Orbits of length N-1.
"""
orbits = []
if primary is None:
jacobi = True
primary = self.particles[0]
clibrebound.reb_get_com_of_pair.restype = Particle
else:
jacobi = False
for p in self.particles[1:self.N_real]:
if jacobi_masses is True:
interior_mass = primary.m
# orbit conversion uses mu=G*(p.m+primary.m) so set prim.m=Mjac-m so mu=G*Mjac
primary.m = self.particles[0].m*(p.m + interior_mass)/interior_mass - p.m
orbits.append(p.calculate_orbit(primary=primary))
primary.m = interior_mass # back to total mass of interior bodies to update com
else:
orbits.append(p.calculate_orbit(primary=primary))
if jacobi is True: # update com to include current particle for next iteration
primary = clibrebound.reb_get_com_of_pair(primary, p)
return orbits
# COM calculation
def calculate_com(self, first=0, last=None):
"""
Returns the center of momentum for all particles in the simulation.
Parameters
----------
first: int, optional
If ``first`` is specified, only calculate the center of momentum starting
from index=``first``.
last : int or None, optional
If ``last`` is specified only calculate the center of momentum up to
(but excluding) index=``last``. Same behavior as Python's range function.
Examples
--------
>>> sim = rebound.Simulation()
>>> sim.add(m=1, x=-20)
>>> sim.add(m=1, x=-10)
>>> sim.add(m=1, x=0)
>>> sim.add(m=1, x=10)
>>> sim.add(m=1, x=20)
>>> com = sim.calculate_com()
>>> com.x
0.0
>>> com = sim.calculate_com(first=2,last=4) # Considers indices 2,3
>>> com.x
5.0
"""
if last is None:
last = self.N_real
clibrebound.reb_get_com_range.restype = Particle
return clibrebound.reb_get_com_range(byref(self), c_int(first), c_int(last))
# Tools
def serialize_particle_data(self,**kwargs):
"""
Fast way to access serialized particle data via numpy arrays.
This function can directly set the values of numpy arrays to
current particle data. This is significantly faster than accessing
particle data via `sim.particles` as all the copying is done
on the C side.
No memory is allocated by this function.
It expects correctly sized numpy arrays as arguments. The argument
name indicates what kind of particle data is written to the array.
Possible argument names are "hash", "m", "r", "xyz", "vxvyvz", and
"xyzvxvyvz". The datatype for the "hash" array needs to be uint32.
The other arrays expect a datatype of float64. The lengths of
"hash", "m", "r" arrays need to be at least sim.N. The lengths of
xyz and vxvyvz need to be at least 3*sim.N. The length of
"xyzvxvyvz" arrays need to be 6*sim.N. Exceptions are raised
otherwise.
Note that this routine is only intended for special use cases
where speed is an issue. For normal use, it is recommended to
access particle data via the `sim.particles` array. Be aware of
potential issues that arise by directly accesing the memory of
numpy arrays (see numpy documentation for more details).
Examples
--------
This sets an array to the xyz positions of all particles:
>>> import numpy as np
>>> a = np.zeros((sim.N,3),dtype="float64")
>>> sim.serialize_particle_data(xyz=a)
>>> print(a)
To get all current radii of particles:
>>> a = np.zeros(sim.N,dtype="float64")
>>> sim.serialize_particle_data(r=a)
>>> print(a)
To get all current radii and hashes of particles:
>>> a = np.zeros(sim.N,dtype="float64")
>>> b = np.zeros(sim.N,dtype="uint32")
>>> sim.serialize_particle_data(r=a,hash=b)
>>> print(a,b)
"""
N = self.N
possible_keys = ["hash","m","r","xyz","vxvyvz","xyzvxvyvz"]
d = {x:None for x in possible_keys}
for k,v in kwargs.items():
if k in d:
if k == "hash":
if v.dtype!= "uint32":
raise AttributeError("Expected 'uint32' data type for '%s' array."%k)
if v.size<N:
raise AttributeError("Array '%s' is not large enough."%k)
d[k] = v.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
else:
if v.dtype!= "float64":
raise AttributeError("Expected 'float64' data type for %s array."%k)
if k in ["xyz", "vxvyvz"]:
minsize = 3*N
elif k in ["xyzvxvyvz"]:
minsize = 6*N
else:
minsize = N
if v.size<minsize:
raise AttributeError("Array '%s' is not large enough."%k)
d[k] = v.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
else:
raise AttributeError("Only '%s' are currently supported attributes for serialization." % "', '".join(d.keys()))
clibrebound.reb_serialize_particle_data(byref(self), d["hash"], d["m"], d["r"], d["xyz"], d["vxvyvz"], d["xyzvxvyvz"])
def set_serialized_particle_data(self,**kwargs):
"""
Fast way to set serialized particle data via numpy arrays.
This is the inverse of Simulation.serialize_particle_data()
and uses the same syntax
"""
N = self.N
possible_keys = ["hash","m","r","xyz","vxvyvz","xyzvxvyvz"]
d = {x:None for x in possible_keys}
for k,v in kwargs.items():
if k in d:
if k == "hash":
if v.dtype!= "uint32":
raise AttributeError("Expected 'uint32' data type for '%s' array."%k)
if v.size<N:
raise AttributeError("Array '%s' is not large enough."%k)
d[k] = v.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
else:
if v.dtype!= "float64":
raise AttributeError("Expected 'float64' data type for %s array."%k)
if k in ["xyz", "vxvyvz"]:
minsize = 3*N
elif k in ["xyzvxvyvz"]:
minsize = 6*N
else:
minsize = N
if v.size<minsize:
raise AttributeError("Array '%s' is not large enough."%k)
d[k] = v.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
else:
raise AttributeError("Only '%s' are currently supported attributes for serialization." % "', '".join(d.keys()))
clibrebound.reb_set_serialized_particle_data(byref(self), d["hash"], d["m"], d["r"], d["xyz"], d["vxvyvz"], d["xyzvxvyvz"])
def move_to_hel(self):
"""
This function moves all particles in the simulation to the heliocentric frame.
Note that the simulation will not stay in the heliocentric frame during integrations
as the heliocentric frame is not an inertial frame.
"""
clibrebound.reb_move_to_hel(byref(self))
def move_to_com(self):
"""
This function moves all particles in the simulation to a center of momentum frame.
In that frame, the center of mass is at the origin and does not move.
It makes sense to call this function at the beginning of the integration, especially
for the high accuracy integrators IAS15 and WHFast.
"""
clibrebound.reb_move_to_com(byref(self))
def calculate_energy(self):
"""
Returns the sum of potential and kinetic energy of all particles in the simulation.
"""
warnings.warn( "sim.calculate_energy() is deprecated and will be removed in the future. Use sim.energy() instead", FutureWarning)
clibrebound.reb_tools_energy.restype = c_double
return clibrebound.reb_tools_energy(byref(self))
def energy(self):
"""
Returns the sum of potential and kinetic energy of all particles in the simulation.
"""
clibrebound.reb_tools_energy.restype = c_double
return clibrebound.reb_tools_energy(byref(self))
def calculate_angular_momentum(self):
"""
Returns a list of the three (x,y,z) components of the total angular momentum of all particles in the simulation.
"""
warnings.warn( "sim.calculate_angular_momentum() is deprecated and will be removed in the future. Use sim.angular_momentum() instead", FutureWarning)
clibrebound.reb_tools_angular_momentum.restype = _Vec3d
return Vec3d(clibrebound.reb_tools_angular_momentum(byref(self)))
def angular_momentum(self):
"""
Returns a list of the three (x,y,z) components of the total angular momentum of all particles in the simulation.
"""
clibrebound.reb_tools_angular_momentum.restype = _Vec3d
return Vec3d(clibrebound.reb_tools_angular_momentum(byref(self)))
def configure_box(self, boxsize, root_nx=1, root_ny=1, root_nz=1):
"""
Initialize the simulation box.
This function only needs to be called it boundary conditions other than "none"
are used. In such a case the boxsize must be known and is set with this function.
Parameters
----------
boxsize : float
The size of one root box.
root_nx, root_ny, root_nz : int, optional
The number of root boxes in each direction. The total size of the simulation box
will be ``root_nx * boxsize``, ``root_ny * boxsize`` and ``root_nz * boxsize``.
By default, there will be exactly one root box in each direction.
"""
clibrebound.reb_configure_box(byref(self), c_double(boxsize), c_int(root_nx), c_int(root_ny), c_int(root_nz))
return
def configure_ghostboxes(self, nghostx=0, nghosty=0, nghostz=0):
"""
Initialize the ghost boxes.
This function only needs to be called it boundary conditions other than "none" or
"open" are used. In such a case the number of ghostboxes must be known and is set
with this function.
Parameters
----------
nghostx, nghosty, nghostz : int
The number of ghost boxes in each direction. All values default to 0 (no ghost boxes).
"""
clibrebound.nghostx = c_int(nghostx)
clibrebound.nghosty = c_int(nghosty)
clibrebound.nghostz = c_int(nghostz)
return
# Input/Output routines
def save(self, filename):
"""
Save the entire REBOUND simulation to a binary file.
"""
clibrebound.reb_output_binary(byref(self), c_char_p(filename.encode("ascii")))
# Integration
def step(self):
"""
Perform exactly one integration step with REBOUND. This function is rarely needed.
Instead, use integrate().
"""
clibrebound.reb_step(byref(self))
self.process_messages()
def steps(self, N_steps):
"""
Perform exactly N_steps integration steps with REBOUND. This function is rarely needed.
Instead, use integrate().
"""
clibrebound.reb_steps(byref(self),c_uint(N_steps))
self.process_messages()
def integrate(self, tmax, exact_finish_time=1):
"""
Main integration function. Call this function when you have setup your simulation and want to integrate it forward (or backward) in time. The function might be called many times to integrate the simulation in steps and create outputs in-between steps.
Parameters
----------
tmax : float
The final time of your simulation. If the current time is 100, and tmax=200, then after the calling the integrate routine, the time has advanced to t=200. If tmax is larger than or equal to the current time, no integration will be performed.
exact_finish_time: int, optional
This argument determines whether REBOUND should try to finish at the exact time (tmax) you give it or if it is allowed to overshoot. Overshooting could happen if one starts at t=0, has a timestep of dt=10 and wants to integrate to tmax=25. With ``exact_finish_time=1``, the integrator will choose the last timestep such that t is exactly 25 after the integration, otherwise t=30. Note that changing the timestep does affect the accuracy of symplectic integrators negatively.
Exceptions
----------
Exceptions are thrown when no more particles are left in the simulation or when a generic integration error occured.
If you specified exit_min_distance or exit_max_distance, then additional exceptions might thrown for escaping particles or particles that undergo a clos encounter.
Examples
--------
The typical usage is as follows. Note the use of ``np.linspace`` to create equally spaced outputs.
Using ``np.logspace`` can be used to easily produce logarithmically spaced outputs.
>>> import numpy as np
>>> for time in np.linspace(0,100.,10):
>>> sim.integrate(time)
>>> perform_output(sim)
"""
self.exact_finish_time = c_int(exact_finish_time)
ret_value = clibrebound.reb_integrate(byref(self), c_double(tmax))
if ret_value == 1:
self.process_messages()
raise SimulationError("An error occured during the integration.")
if ret_value == 2:
if self._odes_N>0:
raise NoParticles("No particles found. Will exit. Use BS integrator to integrate user-defined ODEs without any particles present.");
else:
raise NoParticles("No more particles left in simulation.")
if ret_value == 3:
raise Encounter("Two particles had a close encounter (d<exit_min_distance).")
if ret_value == 4:
raise Escape("A particle escaped (r>exit_max_distance).")
if ret_value == 5:
pass # User caused exit. Do not raise error message
if ret_value == 6:
raise KeyboardInterrupt
if ret_value == 7:
raise Collision("Two particles collided (d < r1+r2)")
self.process_messages()
def stop(self):
"""
Call this function to stop an integration, for example
from the heartbeat function.
"""
clibrebound.reb_stop(byref(self))
def integrator_reset(self):
"""
Call this function to reset temporary integrator variables
"""
clibrebound.reb_integrator_reset(byref(self))
def integrator_synchronize(self):
"""
Call this function if safe-mode is disabled and you need to synchronize particle positions and velocities between timesteps.
"""
clibrebound.reb_integrator_synchronize(byref(self))
def tree_update(self):
"""
Call this function to update the tree structure manually after removing particles.
"""
clibrebound.reb_tree_update(byref(self))
class Variation(Structure):
"""
REBOUND Variational Configuration Object.
This object encapsulated the configuration of one set of variational
equations in a REBOUND simulation. It is an abstraction of the
C struct reb_variational_configuration.
None of the fields in this struct should be changed after it has
been initialized.
One rebound simulation can include any number of first and second order
variational equations.
Note that variations are only encoded as particles for convenience.
A variational particle's position and velocity should be interpreted as a derivative, i.e. how much that position or velocity varies with respect to the first or second-order variation.
See ipython_examples/VariationalEquations.ipynb and Rein and Tamayo (2016) for details.
Examples
--------
>>> sim = rebound.Simulation() # Create a simulation
>>> sim.add(m=1.) # Add a star
>>> sim.add(m=1.e-3, a=1.) # a planet
>>> var_config = sim.add_variation() # Add a set of variational equations.
>>> var_config.particles[1].x = 1. # Initialize the variational particle corresponding to the planet
>>> sim.integrate(100.) # Integrate the simulation
>>> print(var_config.particles[0].vx) # Print the velocity of the variational particle corresponding to the star
"""
_fields_ = [
("_sim", POINTER(Simulation)),
("order", c_int),
("index", c_int),
("testparticle", c_int),
("index_1st_order_a", c_int),
("index_1st_order_b", c_int),
("_lrescale", c_double)]
def vary(self, particle_index, variation, variation2=None, primary=None):
"""
This function can be used to initialize the variational particles that are
part of a Variation.
Note that rather than using this convenience function, one can
also directly manipulate the particles' coordinate using the following
syntax:
>>> var = sim.add_variation()
>>> var.particles[0].x = 1.
The ``vary()`` function is useful for initializing variations corresponding to
changes in one of the orbital parameters for a particle on a bound
Keplerian orbit.
The function supports both first and second order variations in the following
classical orbital parameters:
a, e, inc, omega, Omega, f
as well as the Pal (2009) coordinates:
a, h, k, ix, iy, lambda
and in both cases the mass m of the particle. The advantage of the Pal coordinate
system is that all derivatives are well behaved (infinitely differentiable).
Classical orbital parameters on the other hand exhibit coordinate singularities,
for example when e=0.
The following example initializes the variational particles corresponding to a
change in the semi-major axis of the particle with index 1:
>>> var = sim.add_variation()
>>> var.vary(1,"a")
Parameters
----------
particle_index : int
The index of the particle that should be varied. The index starts at 0 and runs through N-1. The first particle added to a simulation receives the index 0, the second 1, and the on.
variation : string
This parameter determines which orbital parameter is varied.
variation2: string, optional
This is only used for second order variations which can depend on two varying parameters. If omitted, then it is assumed that the parameter variation is variation2.
primary: Particle, optional
By default, variational particles are created in the Heliocentric frame.
Set this parameter to use any other particles as a primary (e.g. the center of mass).
"""
if self.order==2 and variation2 is None:
variation2 = variation
if self._sim is not None:
sim = self._sim.contents
particles = sim.particles
else:
raise RuntimeError("Something went wrong. Cannot seem to find simulation corresponding to variation.")
if self.testparticle >= 0:
particles[self.index] = Particle(simulation=sim,particle=particles[particle_index], variation=variation, variation2=variation2, primary=primary)
else:
particles[self.index + particle_index] = Particle(simulation=sim,particle=particles[particle_index], variation=variation, variation2=variation2, primary=primary)
@property
def particles(self):
"""
Access the variational particles corresponding to this set of variational equations.
The function returns a list of particles which are sorted in the same way as those in
sim.particles
The particles are pointers and thus can be modified.
If there are N real particles, this function will also return a list of N particles (all of which are
variational particles).
"""
sim = self._sim.contents
ps = []
if self.testparticle>=0:
N = 1
else:
N = sim.N-sim.N_var
ParticleList = Particle*N
ps = ParticleList.from_address(ctypes.addressof(sim._particles.contents)+self.index*ctypes.sizeof(Particle))
return ps
@property
def lrescale(self):
"""
Access the lrescale parameter.
This is a property because sim.add_variation() returns a copy of the struct, so need to find up-to-date reb_variational_configuration struct in simulation.
"""
sim = self._sim.contents
for i in range(sim.var_config_N):
if sim.var_config[i].index == self.index:
return sim.var_config[i]._lrescale
raise SimulationError("An error occured while trying to find variational struct in simulation.")
@lrescale.setter
def lrescale(self, value):
"""
Set the lrescale parameter.
This is a property because sim.add_variation() returns a copy of the struct, so need to find up-to-date reb_variational_configuration struct in simulation.
"""
sim = self._sim.contents
for i in range(sim.var_config_N):
if sim.var_config[i].index == self.index:
self._lrescale = c_double(value)
sim.var_config[i]._lrescale = c_double(value)
return
raise SimulationError("An error occured while trying to find variational struct in simulation.")
class reb_particle_int(Structure):
_fields_ = [
("x", c_int64),
("y", c_int64),
("z", c_int64),
("vx", c_int64),
("vy", c_int64),
("vz", c_int64),
]
class reb_simulation_integrator_janus(Structure):
_fields_ = [
("scale_pos",c_double),
("scale_vel",c_double),
("order", c_uint),
("recalculate_integer_coordinates_this_timestep", c_uint),
("p_int", POINTER(reb_particle_int)),
("_allocated_N",c_uint),
]
class reb_simulation_integrator_eos(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_eos.
It controls the behaviour of the Embedded Operator Splitting methods. See Rein (2019)
for more details.
:ivar int,string phi0
Sets the Phi_0 operator splitting method
:ivar int,string phi1
Sets the Phi_1 operator splitting method
:ivar int n
Sets the number of substeps taken by Phi_1
:ivar int safe_mode
By default, safe_mode is on (1). Set to 0 (off) to combine
drift step at the beginning and end of the Phi0 integrator steps.
Example usage:
>>> sim = rebound.Simulation()
>>> sim.integrator = "eos"
>>> sim.ri_eos.phi0 = "LF8_6_4"
>>> sim.ri_eos.phi1 = "LF8"
>>> sim.ri_eos.n = 1
>>> sim.ri_eos.safe_mode = 0
"""
def __repr__(self):
return '<{0}.{1} object at {2}, safe_mode={3}, is_synchronized={4}, n={5}, phi0={6}, phi2={7}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.safe_mode, self.is_synchronized, self.n, self.phi0, self.phi1)
@property
def phi0(self):
"""
Get or set the type of operator splitting type for phi0.
"""
i = self._phi0
for name, _i in EOS_TYPES.items():
if i==_i:
return name
return i
@phi0.setter
def phi0(self, value):
if isinstance(value, int):
self._phi0 = value
elif isinstance(value, basestring):
value = value.lower().replace(" ", "").replace("(", "").replace(")", "")
if value in EOS_TYPES:
self._phi0 = EOS_TYPES[value]
else:
raise ValueError("Warning. EOS type %s not found."%value)
@property
def phi1(self):
"""
Get or set the type of operator splitting type for phi1.
"""
i = self._phi1
for name, _i in EOS_TYPES.items():
if i==_i:
return name
return i
@phi1.setter
def phi1(self, value):
if isinstance(value, int):
self._phi1 = value
elif isinstance(value, basestring):
value = value.lower().replace(" ", "").replace("(", "").replace(")", "")
if value in EOS_TYPES:
self._phi1 = EOS_TYPES[value]
else:
raise ValueError("Warning. EOS type %s not found."%value)
_fields_ = [
("_phi0",c_uint),
("_phi1",c_uint),
("n",c_uint),
("safe_mode",c_uint),
("is_synchronized",c_uint),
]
class reb_simulation_integrator_mercurius(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_mercurius.
It controls the behaviour of the MERCURIUS integrator. See Rein et al. (2019)
for more details.
:ivar float hillfac:
Switching radius in units of the hill radius.
Example usage:
>>> sim = rebound.Simulation()
>>> sim.integrator = "mercurius"
>>> sim.ri_mercurius.hillfac = 3.
"""
def __repr__(self):
return '<{0}.{1} object at {2}, safe_mode={3}, is_synchronized={4}, hillfac={5}>'.format(self.__module__, type(self).__name__, hex(id(self)), self.safe_mode, self.is_synchronized, self.hillfac)
_fields_ = [("_L", CFUNCTYPE(c_double, POINTER(Simulation), c_double, c_double)),
("hillfac", c_double),
("recalculate_coordinates_this_timestep", c_uint),
("recalculate_dcrit_this_timestep", c_uint),
("safe_mode", c_uint),
("is_synchronized", c_uint),
("mode", c_uint),
("_encounterN", c_uint),
("_encounterNactive", c_uint),
("_tponly_encounter", c_uint),
("_allocated_N", c_uint),
("_allocated_N_additionalforces", c_uint),
("_dcrit_allocated_N", c_uint),
("_dcrit", POINTER(c_double)),
("_particles_backup", POINTER(Particle)),
("_particles_backup_additionalforces", POINTER(Particle)),
("_encounter_map", POINTER(c_int)),
("_com_pos", _Vec3d),
("_com_vel", _Vec3d),
]
@property
def L(self):
raise AttributeError("You can only set C function pointers from python.")
@L.setter
def L(self, func):
if func == "mercury":
self._L = cast(clibrebound.reb_integrator_mercurius_L_mercury,MERCURIUSLF)
elif func == "C4":
self._L = cast(clibrebound.reb_integrator_mercurius_L_C4,MERCURIUSLF)
elif func == "C5":
self._L = cast(clibrebound.reb_integrator_mercurius_L_C5,MERCURIUSLF)
elif func == "infinity":
self._L = cast(clibrebound.reb_integrator_mercurius_L_minfinity,MERCURIUSLF)
else:
self._Lfp = MERCURIUSLF(func)
self._L = self._Lfp
class ODE(Structure):
@property
def derivatives(self):
raise AttributeError("You can only set C function pointers from python.")
@derivatives.setter
def derivatives(self, func):
self._dfp = ODEDER(func)
func.argtypes = self._dfp.argtypes # I do not understand why this is needed
self._derivatives = self._dfp
def update_particles(self):
clibrebound.reb_integrator_bs_update_particles(self.r, None)
ODE._fields_ = [
("length", c_uint),
("allocated_N", c_uint),
("needs_nbody", c_int),
("y", POINTER(c_double)),
("_scale", POINTER(c_double)),
("_C", POINTER(c_double)),
("_D", POINTER(POINTER(c_double))),
("_y1", POINTER(c_double)),
("_y0Dot", POINTER(c_double)),
("_yDot", POINTER(c_double)),
("_yTmp", POINTER(c_double)),
("_derivatives", CFUNCTYPE(None,POINTER(ODE), POINTER(c_double), POINTER(c_double), c_double)),
("_getscale", CFUNCTYPE(None,POINTER(ODE), POINTER(c_double), POINTER(c_double))),
("_pre_timestep", CFUNCTYPE(None,POINTER(ODE), POINTER(c_double))),
("_post_timestep", CFUNCTYPE(None,POINTER(ODE), POINTER(c_double))),
("r", POINTER(Simulation)),
("ref", c_void_p),
]
class reb_simulation_integrator_bs(Structure):
"""
This class is an abstraction of the C-struct reb_simulation_integrator_bs.
It controls the behaviour of the Gragg-Bulirsch-Stoer integrator.
"""
_fields_ = [
("_nbody_ode", POINTER(ODE)),
("_sequence", POINTER(c_int)),
("_costPerStep", POINTER(c_int)),
("_costPerTimeUnit", POINTER(c_double)),
("_optimalStep", POINTER(c_double)),
("_coeff", POINTER(c_double)),
("eps_abs", c_double),
("eps_rel", c_double),
("min_dt", c_double),
("max_dt", c_double),
("dt_proposed", c_double),
("_firstOrLastStep", c_int),
("_previousRejected", c_int),
("_targetIter", c_int),
("_user_ode_needs_nbody", c_int),
]
class timeval(Structure):
_fields_ = [("tv_sec",c_long),("tv_usec",c_long)]
class reb_display_data(Structure):
_fields_ = [("r", POINTER(Simulation)),
("r_copy", POINTER(Simulation)),
("particle_data", c_void_p),
("orbit_data", c_void_p),
("particles_copy", POINTER(Particle)),
("p_jh_copy", POINTER(Particle)),
("allocated_N", c_ulong),
("allocated_N_whfast", c_ulong),
("opengl_enabled", c_int),
("scale", c_double),
("mouse_x", c_double),
("mouse_y", c_double),
("retina", c_double),
# ignoring other data (never used)
]
Particle._fields_ = [("x", c_double),
("y", c_double),
("z", c_double),
("vx", c_double),
("vy", c_double),
("vz", c_double),
("ax", c_double),
("ay", c_double),
("az", c_double),
("m", c_double),
("r", c_double),
("lastcollision", c_double),
("c", c_void_p),
("_hash", c_uint32),
("ap", c_void_p),
("_sim", POINTER(Simulation))]
class reb_simulation_integrator_whfast512(Structure):
_fields_ = [("is_synchronized", c_uint),
("keep_unsynchronized", c_uint),
("_allocated_N", c_uint),
("gr_potential", c_uint),
("recalculate_constants", c_uint),
("_p_jh", POINTER(Particle)),
("_p_jh0", Particle)]
# Setting up fields after class definition (because of self-reference)
Simulation._fields_ = [
("t", c_double),
("G", c_double),
("softening", c_double),
("dt", c_double),
("dt_last_done", c_double),
("steps_done", c_ulonglong),
("N", c_uint),
("N_var", c_int),
("var_config_N", c_int),
("var_config", POINTER(Variation)),
("_var_rescale_warning", c_int),
("N_active", c_int),
("testparticle_type", c_int),
("testparticle_hidewarnings", c_int),
("_particle_lookup_table", POINTER(reb_hash_pointer_pair)),
("hash_ctr", c_int),
("N_lookup", c_int),
("allocated_N_lookup", c_int),
("allocated_N", c_uint),
("_particles", POINTER(Particle)),
("gravity_cs", POINTER(_Vec3d)),
("gravity_cs_allocated_N", c_int),
("_tree_root", c_void_p),
("_tree_needs_update", c_int),
("opening_angle2", c_double),
("_status", c_int),
("exact_finish_time", c_int),
("force_is_velocity_dependent", c_uint),
("gravity_ignore", c_uint),
("_output_timing_last", c_double),
("_display_clock", c_ulong),
("save_messages", c_int),
("messages", c_void_p),
("exit_max_distance", c_double),
("exit_min_distance", c_double),
("usleep", c_double),
("display_data", POINTER(reb_display_data)),
("track_energy_offset", c_int),
("energy_offset", c_double),
("walltime", c_double),
("python_unit_t",c_uint32),
("python_unit_l",c_uint32),
("python_unit_m",c_uint32),
("boxsize", _Vec3d),
("boxsize_max", c_double),
("root_size", c_double),
("root_n", c_int),
("root_nx", c_int),
("root_ny", c_int),
("root_nz", c_int),
("nghostx", c_int),
("nghosty", c_int),
("nghostz", c_int),
("collision_resolve_keep_sorted", c_int),
("collisions", c_void_p),
("collisions_allocated_N", c_int),
("minimum_collision_velocity", c_double),
("collisions_plog", c_double),
("max_radius", c_double*2),
("collisions_Nlog", c_long),
("_calculate_megno", c_int),
("_megno_Ys", c_double),
("_megno_Yss", c_double),
("_megno_cov_Yt", c_double),
("_megno_var_t", c_double),
("_megno_mean_t", c_double),
("_megno_mean_Y", c_double),
("_megno_n", c_long),
("rand_seed",c_uint),
("simulationarchive_version", c_int),
("simulationarchive_size_first", c_long),
("simulationarchive_size_snapshot", c_long),
("simulationarchive_auto_interval", c_double),
("simulationarchive_auto_walltime", c_double),
("simulationarchive_auto_step", c_ulonglong),
("simulationarchive_next", c_double),
("simulationarchive_next_step", c_ulonglong),
("_simulationarchive_filename", c_char_p),
("_visualization", c_int),
("_collision", c_int),
("_integrator", c_int),
("_boundary", c_int),
("_gravity", c_int),
("ri_sei", reb_simulation_integrator_sei),
("ri_whfast", reb_simulation_integrator_whfast),
("ri_whfast512", reb_simulation_integrator_whfast512),
("ri_saba", reb_simulation_integrator_saba),
("ri_ias15", reb_simulation_integrator_ias15),
("ri_mercurius", reb_simulation_integrator_mercurius),
("ri_janus", reb_simulation_integrator_janus),
("ri_eos", reb_simulation_integrator_eos),
("ri_bs", reb_simulation_integrator_bs),
("_odes", POINTER(POINTER(ODE))),
("_odes_N", c_int),
("_odes_allocated_N", c_int),
("_odes_warnings", c_int),
("_additional_forces", CFUNCTYPE(None,POINTER(Simulation))),
("_pre_timestep_modifications", CFUNCTYPE(None,POINTER(Simulation))),
("_post_timestep_modifications", CFUNCTYPE(None,POINTER(Simulation))),
("_heartbeat", CFUNCTYPE(None,POINTER(Simulation))),
("_display_heartbeat", CFUNCTYPE(None,POINTER(Simulation))),
("_coefficient_of_restitution", CFUNCTYPE(c_double,POINTER(Simulation), c_double)),
("_collision_resolve", CFUNCTYPE(c_int,POINTER(Simulation), reb_collision)),
("_free_particle_ap", CFUNCTYPE(None, POINTER(Particle))),
("_extras_cleanup", CFUNCTYPE(None, POINTER(Simulation))),
("extras", c_void_p),
]
POINTER_REB_SIM = POINTER(Simulation)
AFF = CFUNCTYPE(None,POINTER_REB_SIM)
ODEDER = CFUNCTYPE(None,POINTER(ODE), POINTER(c_double), POINTER(c_double), c_double)
ODESCALE = CFUNCTYPE(None,POINTER(ODE), POINTER(c_double), POINTER(c_double))
CORFF = CFUNCTYPE(c_double,POINTER_REB_SIM, c_double)
COLRFF = CFUNCTYPE(c_int, POINTER_REB_SIM, reb_collision)
MERCURIUSLF = CFUNCTYPE(c_double, POINTER_REB_SIM, c_double, c_double)
FPA = CFUNCTYPE(None, POINTER(Particle))
class Particles(MutableMapping):
"""
This class allows the user to access particles like a dictionary using the particle's 1) index 2) hash 3) string (which will be converted to hash).
Allows for negative indices and slicing.
"""
def __init__(self, sim):
self.sim = sim
@property
def _ps(self):
ParticleList = Particle*self.sim.N
pl = ParticleList.from_address(ctypes.addressof(self.sim._particles.contents))
pl._sim = self.sim # keep reference to sim until ParticleList is deallocated to avoid memory issues
return pl
def __getitem__(self, key):
hash_types = c_uint32, c_uint, c_ulong
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
int_types = int,
else:
string_types = basestring,
int_types = int, long,
if isinstance(key, slice):
return [self[i] for i in range(*key.indices(len(self)))]
if isinstance(key, int_types):
if key < 0: # accept negative indices
key += self.sim.N
if key < 0 or key >= self.sim.N:
raise AttributeError("Index {0} used to access particles out of range.".format(key))
return self._ps[key]
else:
clibrebound.reb_get_particle_by_hash.restype = POINTER(Particle)
if isinstance(key, string_types):
key = rebhash(key)
elif not isinstance(key, hash_types):
raise AttributeError("Expecting string, integer or ctypes.c_uint32 as argument to sim.particles. See UniquelyIdentifyingParticlesWithHashes.ipynb ipython_example.")
ptr = clibrebound.reb_get_particle_by_hash(byref(self.sim), key)
if ptr:
p = Particle
return p.from_address(ctypes.addressof(ptr.contents))
else:
raise ParticleNotFound("Particle was not found in the simulation.")
def __setitem__(self, key, value):
if isinstance(value, Particle):
value._sim = pointer(self.sim)
p = self[key]
if p.index == -1:
raise AttributeError("Can't set particle (particle not found in simulation).")
else:
self._ps[p.index] = value
def __delitem__(self, key):
pass
def __iter__(self):
for p in self._ps:
yield p
def __len__(self):
return self.sim.N
# Import at the end to avoid circular dependence
from . import horizons
from . import data
from .simulationarchive import SimulationArchive
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...