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

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 

9 

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

11except: from collections import MutableMapping 

12 

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) 

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 

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

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 

629class InvalidStarException(Exception): 

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

631 

632class UnspecifiedParameterException(Exception): 

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

634 

635class OverspecifiedParametersException(Exception): 

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

637 

638class ListLengthException(Exception): 

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

640 

641class IncompatibleListException(Exception): 

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

643 

644class InvalidValueForKeyException(Exception): 

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

646 

647class InvalidParameterTypeException(Exception): 

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

649