Coverage for src/airball/stars.py: 71%

1 import numpy as _np

2 import rebound as _rebound

3 import warnings as _warnings

4 import pickle as _pickle

5 from scipy.stats import uniform as _uniform

6 from . import tools as _tools

7 from .tools import UnitSet as _UnitSet

8 from . import units as _u

9

10 try: from collections.abc import MutableMapping # Required for Python>=3.9

11 except: from collections import MutableMapping

12

13 class 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)

27

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

31

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

38

39 @property

40 def UNIT_SYSTEM(self):

41 return self.units.UNIT_SYSTEM

42

43 @UNIT_SYSTEM.setter

44 def UNIT_SYSTEM(self, UNIT_SYSTEM):

45 self.units.UNIT_SYSTEM = UNIT_SYSTEM

46

47 @property

48 def m(self):

49 return self._mass.to(self.units['mass'])

50

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']

54

55 @property

56 def mass(self):

57 return self.m

58

59 @mass.setter

60 def mass(self, value):

61 self.m = value

62

63 @property

64 def b(self):

65 return self._impact_parameter.to(self.units['length'])

66

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']

70

71 @property

72 def impact_parameter(self):

73 return self.b

74

75 @impact_parameter.setter

76 def impact_parameter(self, value):

77 self.b = value

78

79 @property

80 def v(self):

81 return self._velocity.to(self.units['velocity'])

82

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']

86

87 @property

88 def velocity(self):

89 return self.v

90

91 @velocity.setter

92 def velocity(self, value):

93 self.v = value

94

95 @property

96 def inc(self):

97 return self._inclination.to(self.units['angle'])

98

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']

102

103 @property

104 def inclination(self):

105 return self.inc

106

107 @inc.setter

108 def inclination(self, value):

109 self.inc = value

110

111 @property

112 def omega(self):

113 return self._argument_periastron.to(self.units['angle'])

114

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']

118

119 @property

120 def argument_periastron(self):

121 return self.omega

122

123 @argument_periastron.setter

124 def argument_periastron(self, value):

125 self.omega = value

126

127 @property

128 def Omega(self):

129 return self._longitude_ascending_node.to(self.units['angle'])

130

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']

134

135 @property

136 def longitude_ascending_node(self):

137 return self.Omega

138

139 @longitude_ascending_node.setter

140 def longitude_ascending_node(self, value):

141 self.Omega = value

142

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)

148

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]

156

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])

164

165 def q(self, sim):

166 return _tools.star_q(sim, self)

167

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)

181

182 def __str__(self):

183 return self.stats(returned=True)

184

185 def __repr__(self):

186 return self.stats(returned=True)

187

188 def __len__(self):

189 return NotImplemented

190

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

198

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

205

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)

214

215

216 class 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()

227

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

234

235 self._environment = kwargs.get('environment', None)

236

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

248

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.']

252

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

291

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

338

339 @property

340 def N(self):

341 return self._Nstars

342

343 @property

344 def shape(self):

345 return self._shape

346

347

348 @property

349 def median_mass(self):

350 return _np.median([mass.value for mass in self.m]) * self.units['mass']

351

352 @property

353 def mean_mass(self):

354 return _np.mean([mass.value for mass in self.m]) * self.units['mass']

355

356 def __getitem__(self, key):

357 int_types = int, _np.integer

358

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)

365

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)

369

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)

382

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)

403

404 raise _tools.InvalidKeyException()

405

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()

411

412 def __delitem__(self, key):

413 raise ValueError('Cannot delete Star elements from Stars array.')

414

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)

418

419 def __len__(self):

420 return self.N

421

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

429

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

436

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)

445

446 def copy(self):

447 '''Returns a deep copy of the data.'''

448 return self[:]

449

450 def sort(self, key, sim=None, argsort=False):

451 '''Alias for `sortby`.'''

452 return self.sortby(key, sim, argsort)

453

454 def argsort(self, key, sim=None):

455 '''Alias for `sortby(argsort=True)`.'''

456 return self.sortby(key, sim, argsort=True)

457

458 def argsortby(self, key, sim=None):

459 '''Alias for `sortby(argsort=True)`.'''

460 return self.sortby(key, sim, argsort=True)

461

462 def sortby(self, key, sim=None, argsort=False, **kwargs):

463 '''

464 Sort the Stars in ascending order by a defining parameter.

465

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

472

473 q: periapsis (requires REBOUND Simulation)

474 e: eccentricity (requires REBOUND Simulation)

475

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 '''

479

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()

499

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]

508

509 def save(self, filename):

510 with open(filename, 'wb') as pfile:

511 _pickle.dump(self, pfile, protocol=_pickle.HIGHEST_PROTOCOL)

512

513 @classmethod

514 def load(self, filename):

515 return _pickle.load(open(filename, 'rb'))

516

517 @property

518 def m(self):

519 return self._m << self.units['mass']

520

521 @property

522 def mass(self):

523 return self.m

524

525 @property

526 def b(self):

527 return self._b << self.units['length']

528

529 @property

530 def impact_parameter(self):

531 return self.b

532

533 @property

534 def v(self):

535 return self._v << self.units['velocity']

536

537 @property

538 def velocity(self):

539 return self.v

540

541 @property

542 def inc(self):

543 return self._inc << self.units['angle']

544

545 @property

546 def inclination(self):

547 return self.inc

548

549 @property

550 def omega(self):

551 return self._omega << self.units['angle']

552

553 @property

554 def argument_periastron(self):

555 return self.omega

556

557 @property

558 def Omega(self):

559 return self._Omega << self.units['angle']

560

561 @property

562 def longitude_ascending_node(self):

563 return self.Omega

564

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)

570

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]

578

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])

586

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)

591

592 numerator = self.b * self.v*self.v

593 return _np.sqrt(1 + (numerator/mu)**2.)

594

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)

599

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))

603

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)

617

618 def __str__(self):

619 return self.stats(returned=True)

620

621 def __repr__(self):

622 return self.stats(returned=True)

623

624

625################################

626###### Custom Exceptions #######

627################################

628

629 class InvalidStarException(Exception):

630 def __init__(self): super().__init__('Object is not a valid airball.Star object.')

631

632 class UnspecifiedParameterException(Exception):

633 def __init__(self, message): super().__init__(message)

634

635 class OverspecifiedParametersException(Exception):

636 def __init__(self, message): super().__init__(message)

637

638 class ListLengthException(Exception):

639 def __init__(self, message): super().__init__(f'List arguments must be same length or shape. {message}')

640

641 class IncompatibleListException(Exception):

642 def __init__(self): super().__init__('The given list type is not compatible. Please use a Python list or an ndarray.')

643

644 class InvalidValueForKeyException(Exception):

645 def __init__(self): super().__init__('Invalid value for key.')

646

647 class InvalidParameterTypeException(Exception):

648 def __init__(self): super().__init__('The given parameter value is not a valid type.')

649