In many cases, one can just identify particles by their position in the particle array, e.g. using sim.particles[5]
. However, in cases where particles might get reordered in the particle array finding a particle might be difficult. This is why we added a hash attribute to particles.
In REBOUND particles might get rearranged when a tree code is used for the gravity or collision routine, when particles merge, when a particle leaves the simulation box, or when you manually remove or add particles. In general, therefore, the user should not assume that particles stay at the same index or in the same location in memory. The reliable way to access particles is to assign them hashes and to access particles through them. Assigning hashes make sim.particles
to behave like Python's dict
while keeping list-like integer-based indexing at the same time.
Note: When you don't assign particles a hash, they automatically get set to 0. The user is responsible for making sure hashes are unique, so if you set up particles without a hash and later set a particle's hash to 0, you don't know which one you'll get back when you access hash 0. See Possible Pitfalls below.
In this example, we show the basic usage of the hash attribute.
import rebound
sim = rebound.Simulation()
sim.add(m=1., hash=999)
sim.add(a=0.4, hash="mercury")
sim.add(a=1., hash="earth")
sim.add(a=5., hash="jupiter")
sim.add(a=7.)
We can now not only access the Earth particle with:
sim.particles[2]
<rebound.Particle object, m=0.0 x=1.0 y=0.0 z=0.0 vx=0.0 vy=1.0 vz=0.0>
but also with
sim.particles["earth"]
<rebound.Particle object, m=0.0 x=1.0 y=0.0 z=0.0 vx=0.0 vy=1.0 vz=0.0>
We can access particles with negative indices like a list. We can get the last particle with
sim.particles[-1]
<rebound.Particle object, m=0.0 x=7.0 y=0.0 z=0.0 vx=0.0 vy=0.3779644730092272 vz=0.0>
We can also set hash after particle is added.
sim.particles[-1].hash = 'pluto'
sim.particles['pluto']
<rebound.Particle object, m=0.0 x=7.0 y=0.0 z=0.0 vx=0.0 vy=0.3779644730092272 vz=0.0>
We usually use strings as hashes, however, under the hood hash is an unsigned integer (c_uint
). There is a function rebound.hash
that calculates actual hash of a string.
from rebound import hash as h
h("earth")
c_uint(1424801690)
The same function can be applied to integers. In this case it just casts the value to the underlying C datatype (c_uint
).
h(999)
c_uint(999)
h(-2)
c_uint(4294967294)
When we above set the hash to some value, REBOUND converted this value to an unsigned integer using the same rebound.hash
function.
sim.particles[0].hash # particle was created with sim.add(m=1., hash=999)
c_uint(999)
sim.particles[2].hash
# particle was created with sim.add(a=1., hash="earth")
# so the hash is the same as h("earth") above
c_uint(1424801690)
When we use string as an index to access particle, function rebound.hash
is applied to the index and a particle with this hash is returned. On the other hand, if we use integer index, it is not treated as a hash, REBOUND just returns a particle with given position in array, i.e. sim.particles[0]
is the first particle, etc.
We can access particles through their hash directly. However, to differentiate from passing an integer index, we have to first cast the hash to the underlying C datatype by using rebound.hash
manually.
sim.particles[h(999)]
<rebound.Particle object, m=1.0 x=0.0 y=0.0 z=0.0 vx=0.0 vy=0.0 vz=0.0>
which corresponds to particles[0]
as it should. sim.particles[999]
would try to access index 999, which doesn't exist in the simulation, and REBOUND would raise an AttributeError.
The hash attribute always returns the appropriate unsigned integer ctypes type. (Depending on your computer architecture, ctypes.c_uint32
can be an alias for another ctypes
type).
So we could also access the earth with:
sim.particles[h(1424801690)]
<rebound.Particle object, m=0.0 x=1.0 y=0.0 z=0.0 vx=0.0 vy=1.0 vz=0.0>
The numeric hashes could be useful in cases where you have a lot of particles you don't want to assign individual names, but you still need to keep track of them individually as they get rearranged:
for i in range(1,100):
sim.add(m=0., a=i, hash=i)
sim.particles[99].a
95.0
sim.particles[h(99)].a
98.99999999999999
The user is responsible for making sure the hashes are unique. If two particles share the same hash, you could get either one when you access them using their hash (in most cases the first hit in the particles
array). Two random strings used for hashes have a $\sim 10^{-9}$ chance of clashing. The most common case is setting a hash to 0:
sim = rebound.Simulation()
sim.add(m=1., hash=0)
sim.add(a=1., hash="earth")
sim.add(a=5.)
sim.particles[h(0)]
<rebound.Particle object, m=0.0 x=5.0 y=0.0 z=0.0 vx=0.0 vy=0.4472135954999579 vz=0.0>
Here we expected to get back the first particle, but instead got the last one. This is because we didn't assign a hash to the last particle and it got automatically set to 0. If we give hashes to all the particles in the simulation, then there's no clash:
sim = rebound.Simulation()
sim.add(m=1., hash=0)
sim.add(a=1., hash="earth")
sim.add(a=5., hash="jupiter")
sim.particles[h(0)]
<rebound.Particle object, m=1.0 x=0.0 y=0.0 z=0.0 vx=0.0 vy=0.0 vz=0.0>
Due to details of the ctypes
library, comparing two ctypes.c_uint32
instances for equality fails:
h(32) == h(32)
False
You have to compare the value
h(32).value == h(32).value
True
See the docs for further information: https://docs.python.org/3/library/ctypes.html