Coverage for src/airball/stars.py: 71%
419 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 10:31 +0900
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 10:31 +0900
1import numpy as _np
2import rebound as _rebound
3import warnings as _warnings
4import pickle as _pickle
5from scipy.stats import uniform as _uniform
6from . import tools as _tools
7from .tools import UnitSet as _UnitSet
8from . import units as _u
10try: from collections.abc import MutableMapping # Required for Python>=3.9
11except: from collections import MutableMapping
13class Star:
14 '''
15 This is the AIRBALL Star class.
16 It encapsulates the relevant parameters for a given star.
17 Only the mass is an quantity intrinsic to the object.
18 The impact parameter, velocity, inclination, argument of periastron, and longitude of the ascending node quantities are defined with respect to the host star and plane passing through the star.
19 '''
20 def __init__(self, m, b, v, inc=None, omega=None, Omega=None, UNIT_SYSTEM=[], **kwargs) -> None:
21 '''
22 The Mass, m, (Msun), Impact Parameter, b, (AU), Velocity, v, (km/s)
23 Inclination, inc, (rad), Argument of the Periastron, omega (rad), and Longitude of the Ascending Node, Omega, (rad)
24 Or define an astropy.units system, i.e. UNIT_SYSTEM = [u.pc, u.Myr, u.solMass, u.rad, u.km/u.s].
25 '''
26 self.units = _UnitSet(UNIT_SYSTEM)
28 if inc == 'uniform' or inc == None: inc = 2.0*_np.pi * _uniform.rvs() - _np.pi
29 if omega == 'uniform' or omega == None: omega = 2.0*_np.pi * _uniform.rvs() - _np.pi
30 if Omega == 'uniform' or Omega == None: Omega = 2.0*_np.pi * _uniform.rvs() - _np.pi
32 self.mass = m
33 self.impact_parameter = b
34 self.velocity = v
35 self.inc = inc
36 self.argument_periastron = omega
37 self.longitude_ascending_node = Omega
39 @property
40 def UNIT_SYSTEM(self):
41 return self.units.UNIT_SYSTEM
43 @UNIT_SYSTEM.setter
44 def UNIT_SYSTEM(self, UNIT_SYSTEM):
45 self.units.UNIT_SYSTEM = UNIT_SYSTEM
47 @property
48 def m(self):
49 return self._mass.to(self.units['mass'])
51 @m.setter
52 def m(self, value):
53 self._mass = value.to(self.units['mass']) if _tools.isQuantity(value) else value * self.units['mass']
55 @property
56 def mass(self):
57 return self.m
59 @mass.setter
60 def mass(self, value):
61 self.m = value
63 @property
64 def b(self):
65 return self._impact_parameter.to(self.units['length'])
67 @b.setter
68 def b(self, value):
69 self._impact_parameter = value.to(self.units['length']) if _tools.isQuantity(value) else value * self.units['length']
71 @property
72 def impact_parameter(self):
73 return self.b
75 @impact_parameter.setter
76 def impact_parameter(self, value):
77 self.b = value
79 @property
80 def v(self):
81 return self._velocity.to(self.units['velocity'])
83 @v.setter
84 def v(self, value):
85 self._velocity = value.to(self.units['velocity']) if _tools.isQuantity(value) else value * self.units['velocity']
87 @property
88 def velocity(self):
89 return self.v
91 @velocity.setter
92 def velocity(self, value):
93 self.v = value
95 @property
96 def inc(self):
97 return self._inclination.to(self.units['angle'])
99 @inc.setter
100 def inc(self, value):
101 self._inclination = value.to(self.units['angle']) if _tools.isQuantity(value) else value * self.units['angle']
103 @property
104 def inclination(self):
105 return self.inc
107 @inc.setter
108 def inclination(self, value):
109 self.inc = value
111 @property
112 def omega(self):
113 return self._argument_periastron.to(self.units['angle'])
115 @omega.setter
116 def omega(self, value):
117 self._argument_periastron = value.to(self.units['angle']) if _tools.isQuantity(value) else value * self.units['angle']
119 @property
120 def argument_periastron(self):
121 return self.omega
123 @argument_periastron.setter
124 def argument_periastron(self, value):
125 self.omega = value
127 @property
128 def Omega(self):
129 return self._longitude_ascending_node.to(self.units['angle'])
131 @Omega.setter
132 def Omega(self, value):
133 self._longitude_ascending_node = value.to(self.units['angle']) if _tools.isQuantity(value) else value * self.units['angle']
135 @property
136 def longitude_ascending_node(self):
137 return self.Omega
139 @longitude_ascending_node.setter
140 def longitude_ascending_node(self, value):
141 self.Omega = value
143 @property
144 def impulse_gradient(self):
145 '''Calculate the impulse gradient for a flyby star.'''
146 G = (1 * _u.au**3 / _u.solMass / _u.yr2pi**2)
147 return ((2.0 * G * self.m) / (self.v * self.b**2.0)).to(_u.km/_u.s/_u.au)
149 @property
150 def params(self):
151 '''
152 Returns a list of the parameters of the Stars (with units) in order of:
153 Mass, m; Impact Parameter, b; Velocity, v; Inclination, inc; Argument of the Periastron, omega; and Longitude of the Ascending Node, Omega
154 '''
155 return [self.m, self.b, self.v, self.inc, self.omega, self.Omega]
157 @property
158 def param_values(self):
159 '''
160 Returns a list of the parameters of the Stars in order of:
161 Mass, m; Impact Parameter, b; Velocity, v; Inclination, inc; Argument of the Periastron, omega; and Longitude of the Ascending Node, Omega
162 '''
163 return _np.array([self.m.value, self.b.value, self.v.value, self.inc.value, self.omega.value, self.Omega.value])
165 def q(self, sim):
166 return _tools.star_q(sim, self)
168 def stats(self, returned=False):
169 '''
170 Prints a summary of the current stats of the Star.
171 '''
172 s = f"<{self.__module__}.{type(self).__name__} object at {hex(id(self))}, "
173 s += f"m= {self.mass:1.4g}, "
174 s += f"b= {self.impact_parameter:1.4g}, "
175 s += f"v= {self.velocity:1.4g}, "
176 s += f"inc= {self.inc:1.4g}, "
177 s += f"omega= {self.omega:1.4g}, "
178 s += f"Omega= {self.Omega:1.4g}>"
179 if returned: return s
180 else: print(s)
182 def __str__(self):
183 return self.stats(returned=True)
185 def __repr__(self):
186 return self.stats(returned=True)
188 def __len__(self):
189 return NotImplemented
191 def __eq__(self, other):
192 """Overrides the default implementation"""
193 if isinstance(other, Star):
194 data = ((self.m == other.m) and (self.b == other.b) and (self.v == other.v) and (self.inc == other.inc) and (self.omega == other.omega) and (self.Omega == other.Omega))
195 properties = (self.units == other.units)
196 return data and properties
197 return NotImplemented
199 def __ne__(self, other):
200 """Overrides the default implementation (unnecessary in Python 3)"""
201 x = self.__eq__(other)
202 if x is not NotImplemented:
203 return not x
204 return NotImplemented
206 def __hash__(self):
207 """Overrides the default implementation"""
208 data = []
209 for d in sorted(self.__dict__.items()):
210 try: data.append((d[0], tuple(d[1])))
211 except: data.append(d)
212 data = tuple(data)
213 return hash(data)
216class Stars(MutableMapping):
217 '''
218 This class allows the user to access stars like an array using the star's index.
219 Allows for negative indices and slicing.
220 The implementation uses astropy.Quantity and numpy ndarrays and only generates a airball.Star object when a single Star is requested.
221 '''
222 def __init__(self, filename=None, **kwargs) -> None:
223 try:
224 self.units = _UnitSet(kwargs['UNIT_SYSTEM'])
225 del kwargs['UNIT_SYSTEM']
226 except KeyError: self.units = _UnitSet()
228 if filename is not None:
229 try:
230 loaded = Stars.load(filename)
231 self.__dict__ = loaded.__dict__
232 except: raise Exception('Invalid filename.')
233 return
235 self._environment = kwargs.get('environment', None)
237 self._Nstars = 0
238 for key in kwargs:
239 try:
240 len(kwargs[key])
241 if _tools.isList(kwargs[key]) and len(kwargs[key]) > self.N:
242 self._Nstars = len(kwargs[key])
243 except: pass
244 if 'size' in kwargs and self.N != 0: raise OverspecifiedParametersException('If lists are given then size cannot be specified.')
245 elif 'size' in kwargs: self._Nstars = int(kwargs['size'])
246 elif self.N == 0: raise UnspecifiedParameterException('If no lists are given then size must be specified.')
247 else: pass
249 keys = ['m', 'b', 'v']
250 units = ['mass', 'length', 'velocity']
251 unspecifiedParameterExceptions = ['Mass, m, must be specified.', 'Impact Parameter, b, must be specified.', 'Velocity, v, must be specified.']
253 self._shape = None
254 for k,u,upe in zip(keys, units, unspecifiedParameterExceptions):
255 try:
256 # Check to see if was key is given.
257 value = kwargs[k]
258 # Check if length matches other key values.
259 if len(value) != self.N: raise ListLengthException(f'Difference of {len(value)} and {self.N} for {k}.')
260 # Length of value matches other key values, check if value is a list.
261 elif isinstance(value, list):
262 # Value is a list, try to turn list of Quantities into a ndarray Quantity.
263 try: quantityValue = _np.array([v.to(self.units[u]).value for v in value]) << self.units[u]
264 # Value was not a list of Quantities, turn list into ndarray and make a Quantity.
265 except: quantityValue = _np.array(value) << self.units[u]
266 # Value was not a list, check to see if value is an ndarray.
267 elif isinstance(value, _np.ndarray):
268 # Assume ndarray is a Quantity and try to convert ndarray into given units.
269 try: quantityValue = value.to(self.units[u])
270 # ndarray is not a Quantity so turn ndarray into a Quantity.
271 except: quantityValue = value << self.units[u]
272 # Value implements __len__, but is not a list or ndarray.
273 else: raise IncompatibleListException()
274 # This key is necessary and must be specified, raise and Exception.
275 except KeyError: raise UnspecifiedParameterException(upe)
276 # Value is not a list, so assume it is an int or float and generate an ndarray of the given value.
277 except TypeError:
278 value = value.to(self.units[u]) if _tools.isQuantity(value) else value * self.units[u]
279 quantityValue = _np.ones(self.N) * value
280 # Catch any additional Exceptions.
281 except Exception as err: raise err
282 # Store Quantity Value as class property.
283 if k == 'm': self._m = quantityValue
284 elif k == 'b': self._b = quantityValue
285 elif k == 'v': self._v = quantityValue
286 else: raise _tools.InvalidKeyException()
287 # Double check for consistent shapes.
288 if self._shape is not None:
289 if quantityValue.shape != self._shape: raise ListLengthException(f'Difference of {quantityValue.shape} and {self._shape} for {k}.')
290 else: self._shape = quantityValue.shape
292 for k in ['inc', 'omega', 'Omega']:
293 try:
294 # Check to see if was key is given.
295 value = kwargs[k]
296 # Check to see if value for key is string.
297 if isinstance(value, str):
298 # Value is a string, check to see if value for key is valid.
299 if value != 'uniform': raise InvalidValueForKeyException()
300 # Value 'uniform' for key is valid, now generate an array of values for key.
301 _shape = self.N if len(self._shape) == 1 else self._shape
302 quantityValue = (2.0*_np.pi * _uniform.rvs(size=_shape) - _np.pi) * self.units['angle']
303 # Value is not a string, check if length matches other key values.
304 elif len(value) != self.N: raise ListLengthException(f'Difference of {len(value)} and {self.N} for {k}.')
305 # Length of value matches other key values, check if value is a list.
306 elif isinstance(value, list):
307 # Value is a list, try to turn list of Quantities into a ndarray Quantity.
308 try: quantityValue = _np.array([v.to(self.units['angle']).value for v in value]) * self.units['angle']
309 # Value was not a list of Quantities, turn list into ndarray and make a Quantity.
310 except: quantityValue = _np.array(value) * self.units['angle']
311 # Value was not a list, check to see if value is an ndarray.
312 elif isinstance(value, _np.ndarray):
313 # Assume ndarray is a Quantity and try to convert ndarray into given units.
314 try: quantityValue = value.to(self.units['angle'])
315 # ndarray is not a Quantity so turn ndarray into a Quantity.
316 except: quantityValue = value * self.units['angle']
317 # Value implements __len__, but is not a list or ndarray.
318 else: raise IncompatibleListException()
319 # Key does not exist, assume the user wants an array of values to automatically be generated.
320 except KeyError:
321 _shape = self.N if len(self._shape) == 1 else self._shape
322 quantityValue = (2.0*_np.pi * _uniform.rvs(size=_shape) - _np.pi) * self.units['angle']
323 # Value is not a list, so assume it is an int or float and generate an ndarray of the given value.
324 except TypeError:
325 value = value.to(self.units['angle']) if _tools.isQuantity(value) else value * self.units['angle']
326 quantityValue = _np.ones(self.N) * value
327 # Catch any additional Exceptions.
328 except Exception as err: raise err
329 # Store Quantity Value as class property.
330 if k == 'inc': self._inc = quantityValue
331 elif k == 'omega': self._omega = quantityValue
332 elif k == 'Omega': self._Omega = quantityValue
333 else: raise _tools.InvalidKeyException()
334 # Double check for consistent shapes.
335 if self._shape is not None:
336 if quantityValue.shape != self._shape: raise ListLengthException(f'Difference of {quantityValue.shape} and {self._shape} for {k}.')
337 else: self._shape = quantityValue.shape
339 @property
340 def N(self):
341 return self._Nstars
343 @property
344 def shape(self):
345 return self._shape
348 @property
349 def median_mass(self):
350 return _np.median([mass.value for mass in self.m]) * self.units['mass']
352 @property
353 def mean_mass(self):
354 return _np.mean([mass.value for mass in self.m]) * self.units['mass']
356 def __getitem__(self, key):
357 int_types = int, _np.integer
359 # Basic indexing.
360 if isinstance(key, int_types):
361 # If the set of Stars is multi-dimensional, return the requested subset of stars as a set of Stars.
362 if len(self.m.shape) > 1: return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
363 # Otherwise return the requested Star.
364 else: return Star(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
366 # Allows for boolean array masking and indexing using a subset of indices.
367 if isinstance(key, _np.ndarray):
368 return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
370 # Allow for speed efficient slicing by returning a new set of Stars which are a subset of the original object.
371 if isinstance(key, slice):
372 # Check for number of elements returned by the slice.
373 numEl = _tools.numberOfElementsReturnedBySlice(*key.indices(self.N))
374 # If the slice requests the entire set, then simply return the set.
375 # if key == slice(None, None, None): return self # !!! **Note: this is a reference to the same object.** !!!
376 # If there are no elements requested, return the empty set.
377 if numEl == 0: return Stars(m=[], b=[], v=[], size=0)
378 # If only one element is requested, return a set of Stars with only one Star.
379 elif numEl == 1: return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM, size=1)
380 # Otherwise return a subset of the Stars defined by the slice.
381 else: return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
383 # Allow for Numpy style array indexing.
384 if isinstance(key, tuple):
385 # Check if Stars data is multi-dimensional.
386 if len(self.m.shape) == 1: raise IndexError(f'Too many indices: Stars are 1-dimensional, but {len(key)} were indexed.')
387 # Check to see if the tuple has a slice.
388 hasSlice = _tools.hasTrue([isinstance(k, slice) for k in key])
389 if hasSlice:
390 # Check the number of elements requested by the slice.
391 numEl = [_tools.numberOfElementsReturnedBySlice(*k.indices(self.m.shape[i])) if isinstance(k, slice) else 1 for i,k in enumerate(key)]
392 # If there are no elements requested, return the empty set.
393 if numEl.count(0) > 0: return Stars(m=[], b=[], v=[], size=0)
394 # If multiple elements are requested, return a set of Stars.
395 elif _np.any(_np.array(numEl) > 1): return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
396 # If only one element is requested, return a set of Stars with only one Star.
397 else:
398 # Check to see if the single element is an scalar or an array with only one element.
399 if self.m[key].isscalar: return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], size=1, UNIT_SYSTEM=self.units.UNIT_SYSTEM)
400 else: return Stars(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
401 # If there is no slice, the return the requested Star.
402 else: return Star(m=self.m[key], b=self.b[key], v=self.v[key], inc=self.inc[key], omega=self.omega[key], Omega=self.Omega[key], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
404 raise _tools.InvalidKeyException()
406 def __setitem__(self, key, value):
407 star_type = Star, Stars
408 if isinstance(value, star_type):
409 self.m[key], self.b[key], self.v[key], self.inc[key], self.omega[key], self.Omega[key] = value.params
410 else: raise InvalidStarException()
412 def __delitem__(self, key):
413 raise ValueError('Cannot delete Star elements from Stars array.')
415 def __iter__(self):
416 for i in list(_np.ndindex(self.m.shape)):
417 yield Star(m=self.m[i], b=self.b[i], v=self.v[i], inc=self.inc[i], omega=self.omega[i], Omega=self.Omega[i], UNIT_SYSTEM=self.units.UNIT_SYSTEM)
419 def __len__(self):
420 return self.N
422 def __eq__(self, other):
423 """Overrides the default implementation"""
424 if isinstance(other, Stars):
425 data = (_np.all(self.m == other.m) and _np.all(self.b == other.b) and _np.all(self.v == other.v) and _np.all(self.inc == other.inc) and _np.all(self.omega == other.omega) and _np.all(self.Omega == other.Omega))
426 properties = (self.N == other.N and self.units == other.units)
427 return data and properties
428 return NotImplemented
430 def __ne__(self, other):
431 """Overrides the default implementation (unnecessary in Python 3)"""
432 x = self.__eq__(other)
433 if x is not NotImplemented:
434 return not x
435 return NotImplemented
437 def __hash__(self):
438 """Overrides the default implementation"""
439 data = []
440 for d in sorted(self.__dict__.items()):
441 try: data.append((d[0], tuple(d[1])))
442 except: data.append(d)
443 data = tuple(data)
444 return hash(data)
446 def copy(self):
447 '''Returns a deep copy of the data.'''
448 return self[:]
450 def sort(self, key, sim=None, argsort=False):
451 '''Alias for `sortby`.'''
452 return self.sortby(key, sim, argsort)
454 def argsort(self, key, sim=None):
455 '''Alias for `sortby(argsort=True)`.'''
456 return self.sortby(key, sim, argsort=True)
458 def argsortby(self, key, sim=None):
459 '''Alias for `sortby(argsort=True)`.'''
460 return self.sortby(key, sim, argsort=True)
462 def sortby(self, key, sim=None, argsort=False, **kwargs):
463 '''
464 Sort the Stars in ascending order by a defining parameter.
466 m: mass
467 b: impact parameter
468 v: relative velocity at infinity
469 inc: inclination
470 omega: argument of the periastron
471 Omega: longitude of the ascending node
473 q: periapsis (requires REBOUND Simulation)
474 e: eccentricity (requires REBOUND Simulation)
476 The Stars can also be sorted arbitrarily by providing a list of indices of length N.
477 By setting argsort=True the indices used to sort the Stars will be returned instead.
478 '''
480 inds = _np.arange(self.N)
481 if key == 'm' or key == 'mass': inds = _np.argsort(self.m)
482 elif key == 'b' or key == 'impact' or key == 'impact param' or key == 'impact parameter': inds = _np.argsort(self.b)
483 elif key == 'v' or key == 'vinf' or key == 'v_inf' or key == 'velocity': inds = _np.argsort(self.v)
484 elif key == 'inc' or key == 'inclination' or key == 'i' or key == 'I': inds = _np.argsort(self.inc)
485 elif key == 'omega' or key == 'ω': inds = _np.argsort(self.omega)
486 elif key == 'Omega' or key == 'Ω': inds = _np.argsort(self.Omega)
487 elif key == 'q' or key == 'peri' or key == 'perihelion' or key == 'periastron' or key == 'periapsis':
488 if isinstance(sim, _rebound.Simulation):
489 inds = _np.argsort(self.q(sim))
490 else: raise InvalidParameterTypeException()
491 elif key == 'e' or key == 'eccentricity':
492 if isinstance(sim, _rebound.Simulation):
493 inds = _np.argsort(self.e(sim))
494 else: raise InvalidParameterTypeException()
495 elif _tools.isList(key):
496 if len(key) != self.N: raise ListLengthException(f'Difference of {len(key)} and {self.N}.')
497 inds = _np.array(key)
498 else: raise InvalidValueForKeyException()
500 if argsort: return inds
501 else:
502 self.m[:] = self.m[inds]
503 self.b[:] = self.b[inds]
504 self.v[:] = self.v[inds]
505 self.inc[:] = self.inc[inds]
506 self.omega[:] = self.omega[inds]
507 self.Omega[:] = self.Omega[inds]
509 def save(self, filename):
510 with open(filename, 'wb') as pfile:
511 _pickle.dump(self, pfile, protocol=_pickle.HIGHEST_PROTOCOL)
513 @classmethod
514 def load(self, filename):
515 return _pickle.load(open(filename, 'rb'))
517 @property
518 def m(self):
519 return self._m << self.units['mass']
521 @property
522 def mass(self):
523 return self.m
525 @property
526 def b(self):
527 return self._b << self.units['length']
529 @property
530 def impact_parameter(self):
531 return self.b
533 @property
534 def v(self):
535 return self._v << self.units['velocity']
537 @property
538 def velocity(self):
539 return self.v
541 @property
542 def inc(self):
543 return self._inc << self.units['angle']
545 @property
546 def inclination(self):
547 return self.inc
549 @property
550 def omega(self):
551 return self._omega << self.units['angle']
553 @property
554 def argument_periastron(self):
555 return self.omega
557 @property
558 def Omega(self):
559 return self._Omega << self.units['angle']
561 @property
562 def longitude_ascending_node(self):
563 return self.Omega
565 @property
566 def impulse_gradient(self):
567 '''Calculate the impulse gradient for a flyby star.'''
568 G = (1 * _u.au**3 / _u.solMass / _u.yr2pi**2)
569 return ((2.0 * G * self.m) / (self.v * self.b**2.0)).to(_u.km/_u.s/_u.au)
571 @property
572 def params(self):
573 '''
574 Returns a list of the parameters of the Stars (with units) in order of:
575 Mass, m; Impact Parameter, b; Velocity, v; Inclination, inc; Argument of the Periastron, omega; and Longitude of the Ascending Node, Omega
576 '''
577 return [self.m, self.b, self.v, self.inc, self.omega, self.Omega]
579 @property
580 def param_values(self):
581 '''
582 Returns a list of the parameters of the Stars in order of:
583 Mass, m; Impact Parameter, b; Velocity, v; Inclination, inc; Argument of the Periastron, omega; and Longitude of the Ascending Node, Omega
584 '''
585 return _np.array([self.m.value, self.b.value, self.v.value, self.inc.value, self.omega.value, self.Omega.value])
587 def e(self, sim):
588 sim_units = _tools.rebound_units(sim)
589 G = (sim.G * sim_units['length']**3 / sim_units['mass'] / sim_units['time']**2)
590 mu = G * (_tools.system_mass(sim) * sim_units['mass'] + self.m)
592 numerator = self.b * self.v*self.v
593 return _np.sqrt(1 + (numerator/mu)**2.)
595 def q(self, sim):
596 sim_units = _tools.rebound_units(sim)
597 G = (sim.G * sim_units['length']**3 / sim_units['mass'] / sim_units['time']**2)
598 mu = G * (_tools.system_mass(sim) * sim_units['mass'] + self.m)
600 numerator = self.b * self.v*self.v
601 star_e = _np.sqrt(1 + (numerator/mu)**2.)
602 return self.b * _np.sqrt((star_e - 1.0)/(star_e + 1.0))
604 def stats(self, returned=False):
605 '''
606 Prints a summary of the current stats of the Stars object.
607 '''
608 s = f"<{self.__module__}.{type(self).__name__} object at {hex(id(self))}, "
609 s += f"N={f'{self.N:,.0f}' if len(self.shape) == 1 else self.shape}"
610 s += f", m= {_np.min(self.m.value):,.2f}-{_np.max(self.m.value):,.2f} {self.units['mass']}"
611 s += f", b= {_np.min(self.b.value):,.0f}-{_np.max(self.b.value):,.0f} {self.units['length']}"
612 s += f", v= {_np.min(self.v.value):,.0f}-{_np.max(self.v.value):,.0f} {self.units['velocity']}"
613 s += f"{f', Environment={self._environment.name}' if self._environment is not None else ''}"
614 s += ">"
615 if returned: return s
616 else: print(s)
618 def __str__(self):
619 return self.stats(returned=True)
621 def __repr__(self):
622 return self.stats(returned=True)
625################################
626###### Custom Exceptions #######
627################################
629class InvalidStarException(Exception):
630 def __init__(self): super().__init__('Object is not a valid airball.Star object.')
632class UnspecifiedParameterException(Exception):
633 def __init__(self, message): super().__init__(message)
635class OverspecifiedParametersException(Exception):
636 def __init__(self, message): super().__init__(message)
638class ListLengthException(Exception):
639 def __init__(self, message): super().__init__(f'List arguments must be same length or shape. {message}')
641class IncompatibleListException(Exception):
642 def __init__(self): super().__init__('The given list type is not compatible. Please use a Python list or an ndarray.')
644class InvalidValueForKeyException(Exception):
645 def __init__(self): super().__init__('Invalid value for key.')
647class InvalidParameterTypeException(Exception):
648 def __init__(self): super().__init__('The given parameter value is not a valid type.')