• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dsavransky / EXOSIMS / 14561753842

20 Apr 2025 05:25PM UTC coverage: 65.777% (+0.07%) from 65.708%
14561753842

push

github

web-flow
Merge pull request #408 from dsavransky/performance_optimization

Performance optimizations.

604 of 731 new or added lines in 15 files covered. (82.63%)

22 existing lines in 9 files now uncovered.

9681 of 14718 relevant lines covered (65.78%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.62
/EXOSIMS/Prototypes/SimulatedUniverse.py
1
import astropy.constants as const
1✔
2
import astropy.units as u
1✔
3
import numpy as np
1✔
4
from keplertools.keplerSTM import planSys
1✔
5

6
from EXOSIMS.util.deltaMag import deltaMag
1✔
7
from EXOSIMS.util.eccanom import eccanom
1✔
8
from EXOSIMS.util.get_dirs import get_cache_dir
1✔
9
from EXOSIMS.util.get_module import get_module
1✔
10
from EXOSIMS.util.vprint import vprint
1✔
11

12

13
class SimulatedUniverse(object):
1✔
14
    r""":ref:`SimulatedUniverse` Prototype
15

16
    Args:
17
        fixedPlanPerStar (int, optional):
18
            If set, every system will have the same number of planets.
19
            Defaults to None
20
        Min (float, optional):
21
            Initial mean anomaly for all planets.  If set, every planet
22
            has the same mean anomaly at mission start. Defaults to None
23
        cachedir (str, optional):
24
            Full path to cachedir.
25
            If None (default) use default (see :ref:`EXOSIMSCACHE`)
26
        lucky_planets (bool):
27
            Used downstream in survey simulation. If True, planets are
28
            observed at optimal times. Defaults to False
29
        commonSystemPlane (bool):
30
            Planet inclinations are sampled as normally distributed about a
31
            common system plane. Defaults to False
32
        commonSystemPlaneParams (list(float)):
33
            [inclination mean, inclination standard deviation, Omega mean, Omega
34
            standard deviation] defining the normal distribution of
35
            inclinations and longitudes of the ascending node about a common
36
            system plane in units of degrees.  Ignored if commonSystemPlane is
37
            False. Defaults to [0 2.25, 0, 2.25], where the standard deviation
38
            is approximately the standard deviation of solar system planet
39
            inclinations.
40
        commonSystemnEZ (bool):
41
            Assume same zodi for planets in the same system. Defaults to False.
42
        **specs:
43
            :ref:`sec:inputspec`
44

45
    Attributes:
46
        _outspec (dict):
47
            :ref:`sec:outspec`
48
        a (astropy.units.quantity.Quantity):
49
             Planet semi-major axis (length units)
50
        BackgroundSources (:ref:`BackgroundSources`):
51
            BackgroundSources object
52
        cachedir (str):
53
            Path to the EXOSIMS cache directory (see :ref:`EXOSIMSCACHE`)
54
        commonSystemPlaneParams (list):
55
            2 element list of [mean, standard deviation] in units of degrees,
56
            describing the distribution of inclinations relative to a common orbital
57
            plane.  Ignored if commonSystemPlane is False.
58
        commonSystemPlane (bool):
59
            If False, planet inclinations are independently drawn for all planets,
60
            including those in the same target system.  If True, inclinations will be
61
            drawn from a normal distribution defined by
62
            commonSystemPlaneParams and added to a single inclination value drawn
63
            for each system.
64
        Completeness (:ref:`Completeness`):
65
            Completeness object
66
        d (astropy.units.quantity.Quantity):
67
            Current orbital radius magnitude (length units)
68
        dMag (numpy.ndarray):
69
            Current planet :math:`\Delta\mathrm{mag}`
70
        e (numpy.ndarray):
71
            Planet eccentricity
72
        fixedPlanPerStar (int or None):
73
            If set, every system has the same number of planets, given by
74
            this attribute
75
        I (astropy.units.quantity.Quantity):
76
            Planet inclinations (angle units)
77
        lucky_planets (bool):
78
            If True, planets are observed at optimal times.
79
        M0 (astropy.units.quantity.Quantity):
80
            Initial planet mean anomaly (at mission start time).
81
        Min (float or None):
82
            Input constant initial mean anomaly.  If none, initial
83
            mean anomaly is randomly distributed from a uniform distribution in
84
            [0, 360] degrees.
85
        Mp (astropy.units.quantity.Quantity):
86
            Planet mass.
87
        nPlans (int):
88
            Number of planets in all target systems.
89
        O (astropy.units.quantity.Quantity):
90
            Planet longitude of the ascending node (angle units)
91
        OpticalSystem (:ref:`OpticalSystem`):
92
            Optical System object
93
        p (numpy.ndarray):
94
            Planet geometric albedo
95
        phi (numpy.ndarray):
96
            Current value of planet phase function.
97
        phiIndex (numpy.ndarray):
98
            Intended for use with input
99
            'whichPlanetPhaseFunction'='realSolarSystemPhaseFunc'
100
            When None, the default is the phi_lambert function, otherwise it is Solar
101
            System Phase Functions
102
        plan2star (numpy.ndarray):
103
            Index of host star or each planet.  Indexes attributes of TargetsList.
104
        planet_atts (list):
105
            List of planet attributes
106
        PlanetPhysicalModel (:ref:`PlanetPhysicalModel`):
107
            Planet physical model object.
108
        PlanetPopulation (:ref:`PlanetPopulation`):
109
            Planet population object.
110
        PostProcessing (:ref:`PostProcessing`):
111
            Postprocessing object.
112
        r (astropy.units.quantity.Quantity):
113
            Current planet orbital radius (3xnPlans). Length units.
114
        Rp (astropy.units.quantity.Quantity):
115
            Planet radius (length units).
116
        s (astropy.units.quantity.Quantity):
117
            Current planet projected separation. Length units.
118
        sInds (numpy.ndarray):
119
            Indices of stars with planets.  Equivalent to unique entries of
120
            ``plan2star``.
121
        nEZ (numpy.ndarray):
122
            Number of exozodi for each planet.
123
        TargetList (:ref:`TargetList`):
124
            Target list object.
125
        v (astropy.units.quantity.Quantity):
126
            Current orbital velocity vector (3xnPlans). Velocity units.
127
        w (astropy.units.quantity.Quantity):
128
            Planet argument of periapsis.
129
        WA (astropy.units.quantity.Quantity):
130
            Current planet angular separation (angle units)
131
        ZodiacalLight (:ref:`ZodiacalLight`):
132
            Zodiacal light object.
133

134

135
    .. note::
136

137
        When generating planets, :ref:`PlanetPopulation` attribute ``eta`` is
138
        treated as the rate parameter of a Poisson distribution.
139
        Each target's number of planets is a Poisson random variable
140
        sampled with :math:`\lambda\equiv\eta`.
141

142
    .. warning::
143

144
        All attributes described as 'current' are updated only when planets are
145
        observed.  As such, during mission simulations, these values for different
146
        planets correspond to different times (bookkept in the survey simulation
147
        object).
148

149
    """
150

151
    _modtype = "SimulatedUniverse"
1✔
152

153
    def __init__(
1✔
154
        self,
155
        fixedPlanPerStar=None,
156
        Min=None,
157
        cachedir=None,
158
        lucky_planets=False,
159
        commonSystemPlane=False,
160
        commonSystemPlaneParams=[0, 2.25, 0, 2.25],
161
        commonSystemnEZ=True,
162
        **specs,
163
    ):
164
        self.AU_div_day = u.AU / u.day
1✔
165
        self.AU3_div_day2 = u.AU**3 / u.day**2
1✔
166
        self.AU2pc = (1 * u.AU).to_value("pc")
1✔
167
        self.rad2arcsec = (1 * u.radian).to_value("arcsec")
1✔
168
        # start the outspec
169
        self._outspec = {}
1✔
170

171
        # load the vprint function (same line in all prototype module constructors)
172
        self.vprint = vprint(specs.get("verbose", True))
1✔
173
        self.lucky_planets = lucky_planets
1✔
174
        self._outspec["lucky_planets"] = lucky_planets
1✔
175
        self.commonSystemPlane = bool(commonSystemPlane)
1✔
176
        self._outspec["commonSystemPlane"] = commonSystemPlane
1✔
177
        assert (
1✔
178
            len(commonSystemPlaneParams) == 4
179
        ), "commonSystemPlaneParams must be a four-element list"
180
        self.commonSystemPlaneParams = commonSystemPlaneParams
1✔
181
        self._outspec["commonSystemPlaneParams"] = commonSystemPlaneParams
1✔
182

183
        # Set the number of exozodi
184
        self.commonSystemnEZ = commonSystemnEZ
1✔
185

186
        # save fixed number of planets to generate
187
        self.fixedPlanPerStar = fixedPlanPerStar
1✔
188
        self._outspec["fixedPlanPerStar"] = fixedPlanPerStar
1✔
189

190
        # get cache directory
191
        self.cachedir = get_cache_dir(cachedir)
1✔
192
        self._outspec["cachedir"] = self.cachedir
1✔
193
        specs["cachedir"] = self.cachedir
1✔
194

195
        # check if KnownRVPlanetsUniverse has correct input modules
196
        if specs["modules"]["SimulatedUniverse"] == "KnownRVPlanetsUniverse":
1✔
197
            val = (
1✔
198
                specs["modules"]["TargetList"] == "KnownRVPlanetsTargetList"
199
                and specs["modules"]["PlanetPopulation"] == "KnownRVPlanets"
200
            )
201
            assert val, (
1✔
202
                "KnownRVPlanetsUniverse must use KnownRVPlanetsTargetList "
203
                "and KnownRVPlanets"
204
            )
205
        else:
206
            val = (
1✔
207
                specs["modules"]["TargetList"] == "KnownRVPlanetsTargetList"
208
                or specs["modules"]["PlanetPopulation"] == "KnownRVPlanets"
209
            )
210
            assert not (val), (
1✔
211
                "KnownRVPlanetsTargetList or KnownRVPlanets should not be used "
212
                "with this SimulatedUniverse"
213
            )
214

215
        # import TargetList class
216
        self.TargetList = get_module(specs["modules"]["TargetList"], "TargetList")(
1✔
217
            **specs
218
        )
219

220
        # bring inherited class objects to top level of Simulated Universe
221
        TL = self.TargetList
1✔
222
        self.StarCatalog = TL.StarCatalog
1✔
223
        self.PlanetPopulation = TL.PlanetPopulation
1✔
224
        self.PlanetPhysicalModel = TL.PlanetPhysicalModel
1✔
225
        self.OpticalSystem = TL.OpticalSystem
1✔
226
        self.ZodiacalLight = TL.ZodiacalLight
1✔
227
        self.BackgroundSources = TL.BackgroundSources
1✔
228
        self.PostProcessing = TL.PostProcessing
1✔
229
        self.Completeness = TL.Completeness
1✔
230

231
        # initial constant mean anomaly
232
        assert isinstance(Min, (int, float)) or (
1✔
233
            Min is None
234
        ), "Min may be int, float, or None"
235
        if Min is not None:
1✔
236
            self.Min = float(Min) * u.deg
1✔
237
        else:
238
            self.Min = Min
1✔
239
        self._outspec["Min"] = Min
1✔
240

241
        # list of possible planet attributes
242
        self.planet_atts = [
1✔
243
            "plan2star",
244
            "a",
245
            "e",
246
            "I",
247
            "O",
248
            "w",
249
            "M0",
250
            "Min",
251
            "Rp",
252
            "Mp",
253
            "p",
254
            "r",
255
            "v",
256
            "d",
257
            "s",
258
            "phi",
259
            "nEZ",
260
            "dMag",
261
            "WA",
262
        ]
263

264
        self.phiIndex = (
1✔
265
            None  # Used to switch select specific phase function for each planet
266
        )
267

268
        # generate orbital elements, albedos, radii, and masses
269
        self.gen_physical_properties(**specs)
1✔
270

271
        # find initial position-related parameters: position, velocity, planet-star
272
        # distance, apparent separation, number of zodis
273
        self.init_systems()
1✔
274

275
    def __str__(self):
1✔
276
        """String representation of Simulated Universe object
277

278
        When the command 'print' is used on the Simulated Universe object,
279
        this method will return the values contained in the object
280

281
        """
282

283
        for att in self.__dict__:
1✔
284
            print("%s: %r" % (att, getattr(self, att)))
1✔
285

286
        return "Simulated Universe class object attributes"
1✔
287

288
    def gen_physical_properties(self, **specs):
1✔
289
        """Generates the planetary systems' physical properties.
290

291
        Populates arrays of the orbital elements, albedos, masses and radii
292
        of all planets, and generates indices that map from planet to parent star.
293

294
        Args:
295
            **specs:
296
                :ref:`sec:inputspec`
297

298
        Returns:
299
            None
300

301
        """
302

303
        PPop = self.PlanetPopulation
1✔
304
        ZL = self.ZodiacalLight
1✔
305
        TL = self.TargetList
1✔
306

307
        if self.fixedPlanPerStar is not None:  # Must be an int for fixedPlanPerStar
1✔
308
            # Create array of length TL.nStars each w/ value ppStar
309
            targetSystems = np.ones(TL.nStars).astype(int) * self.fixedPlanPerStar
1✔
310
        else:
311
            # treat eta as the rate parameter of a Poisson distribution
312
            targetSystems = np.random.poisson(lam=PPop.eta, size=TL.nStars)
1✔
313

314
        plan2star = []
1✔
315
        for j, n in enumerate(targetSystems):
1✔
316
            plan2star = np.hstack((plan2star, [j] * n))
1✔
317
        self.plan2star = plan2star.astype(int)
1✔
318
        self.sInds = np.unique(self.plan2star)
1✔
319
        self.nPlans = len(self.plan2star)
1✔
320

321
        # The prototype StarCatalog module is made of one single G star at 1pc.
322
        # In that case, the SimulatedUniverse prototype generates one Jupiter
323
        # at 5 AU to allow for characterization testing.
324
        # Also generates at least one Jupiter if no planet was generated.
325
        if (TL.Name[0].startswith("Prototype") and (TL.nStars == 1)) or (
1✔
326
            self.nPlans == 0
327
        ):
328
            if self.nPlans == 0:
1✔
329
                self.vprint("No planets were generated. Creating single fake planet.")
1✔
330
            else:
331
                self.vprint(
1✔
332
                    (
333
                        "Prototype StarCatalog with 1 target. "
334
                        "Creating single fake planet."
335
                    )
336
                )
337
            self.plan2star = np.array([0], dtype=int)
1✔
338
            self.sInds = np.unique(self.plan2star)
1✔
339
            self.nPlans = len(self.plan2star)
1✔
340
            self.a = np.array([5.0]) * u.AU
1✔
341
            self.e = np.array([0.0])
1✔
342
            self.I = np.array([0.0]) * u.deg
1✔
343
            self.O = np.array([0.0]) * u.deg
1✔
344
            self.w = np.array([0.0]) * u.deg
1✔
345
            self.gen_M0()
1✔
346
            self.Rp = np.array([10.0]) * u.earthRad
1✔
347
            self.Mp = np.array([300.0]) * u.earthMass
1✔
348
            self.p = np.array([0.6])
1✔
349
        else:
350
            # sample all of the orbital and physical parameters
351
            self.I, self.O, self.w = PPop.gen_angles(
1✔
352
                self.nPlans,
353
                commonSystemPlane=self.commonSystemPlane,
354
                commonSystemPlaneParams=self.commonSystemPlaneParams,
355
            )
356
            self.setup_system_planes()
1✔
357

358
            self.a, self.e, self.p, self.Rp = PPop.gen_plan_params(self.nPlans)
1✔
359
            if PPop.scaleOrbits:
1✔
360
                self.a *= np.sqrt(TL.L[self.plan2star])
1✔
361
            self.gen_M0()  # initial mean anomaly
1✔
362
            self.Mp = PPop.gen_mass(self.nPlans)  # mass
1✔
363

364
        if self.commonSystemnEZ:
1✔
365
            # Assign the same nEZ to all planets in the system
366
            self.nEZ = ZL.gen_systemnEZ(TL.nStars)[self.plan2star]
1✔
367
        else:
368
            # Assign a unique nEZ to each planet
UNCOV
369
            self.nEZ = ZL.gen_systemnEZ(self.nPlans)
×
370

371
        self.phiIndex = np.asarray(
1✔
372
            []
373
        )  # Used to switch select specific phase function for each planet
374

375
    def gen_M0(self):
1✔
376
        """Set initial mean anomaly for each planet"""
377
        if self.Min is not None:
1✔
378
            self.M0 = np.ones((self.nPlans,)) * self.Min
1✔
379
        else:
380
            self.M0 = np.random.uniform(360, size=int(self.nPlans)) * u.deg
1✔
381

382
    def init_systems(self):
1✔
383
        """Finds initial time-dependant parameters. Assigns each planet an
384
        initial position, velocity, planet-star distance, apparent separation,
385
        phase function, surface brightness of exo-zodiacal light, delta
386
        magnitude, and working angle.
387

388
        This method makes use of the systems' physical properties (masses,
389
        distances) and their orbital elements (a, e, I, O, w, M0).
390
        """
391

392
        PPMod = self.PlanetPhysicalModel
1✔
393
        TL = self.TargetList
1✔
394

395
        a = self.a.to("AU").value  # semi-major axis
1✔
396
        e = self.e  # eccentricity
1✔
397
        I = self.I.to("rad").value  # inclinations #noqa: E741
1✔
398
        O = self.O.to("rad").value  # right ascension of the ascending node #noqa: E741
1✔
399
        w = self.w.to("rad").value  # argument of perigee
1✔
400
        M0 = self.M0.to("rad").value  # initial mean anomany
1✔
401
        E = eccanom(M0, e)  # eccentric anomaly
1✔
402
        Mp = self.Mp  # planet masses
1✔
403

404
        # This is the a1 a2 a3 and b1 b2 b3 are the euler angle transformation from
405
        # the I,J,K refernece frame to an x,y,z frame
406
        a1 = np.cos(O) * np.cos(w) - np.sin(O) * np.cos(I) * np.sin(w)
1✔
407
        a2 = np.sin(O) * np.cos(w) + np.cos(O) * np.cos(I) * np.sin(w)
1✔
408
        a3 = np.sin(I) * np.sin(w)
1✔
409
        A = a * np.vstack((a1, a2, a3)) * u.AU
1✔
410
        b1 = -np.sqrt(1 - e**2) * (
1✔
411
            np.cos(O) * np.sin(w) + np.sin(O) * np.cos(I) * np.cos(w)
412
        )
413
        b2 = np.sqrt(1 - e**2) * (
1✔
414
            -np.sin(O) * np.sin(w) + np.cos(O) * np.cos(I) * np.cos(w)
415
        )
416
        b3 = np.sqrt(1 - e**2) * np.sin(I) * np.cos(w)
1✔
417
        B = a * np.vstack((b1, b2, b3)) * u.AU
1✔
418
        r1 = np.cos(E) - e
1✔
419
        r2 = np.sin(E)
1✔
420

421
        mu = const.G * (Mp + TL.MsTrue[self.plan2star])
1✔
422
        self.mu_AU3_div_day2 = mu.to_value(self.AU3_div_day2)
1✔
423
        v1 = np.sqrt(mu / self.a**3) / (1 - e * np.cos(E))
1✔
424
        v2 = np.cos(E)
1✔
425

426
        self.r = (A * r1 + B * r2).T.to("AU")  # position
1✔
427
        self.v = (v1 * (-A * r2 + B * v2)).T.to("AU/day")  # velocity
1✔
428
        self.s = np.linalg.norm(self.r[:, 0:2], axis=1)  # apparent separation
1✔
429
        self.d = np.linalg.norm(self.r, axis=1)  # planet-star distance
1✔
430
        try:
1✔
431
            self.phi = PPMod.calc_Phi(
1✔
432
                np.arccos(self.r[:, 2] / self.d), phiIndex=self.phiIndex
433
            )  # planet phase
434
        except u.UnitTypeError:
×
435
            self.d = self.d * self.r.unit  # planet-star distance
×
436
            self.phi = PPMod.calc_Phi(
×
437
                np.arccos(self.r[:, 2] / self.d), phiIndex=self.phiIndex
438
            )  # planet phase
439

440
        self.dMag = deltaMag(self.p, self.Rp, self.d, self.phi)  # delta magnitude
1✔
441
        try:
1✔
442
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
1✔
443
                "arcsec"
444
            )  # working angle
445
        except u.UnitTypeError:
×
446
            self.s = self.s * self.r.unit
×
447
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
×
448
                "arcsec"
449
            )  # working angle
450

451
    def propag_system(self, sInd, dt):
1✔
452
        """Propagates planet time-dependant parameters: position, velocity,
453
        planet-star distance, apparent separation, phase function, surface brightness
454
        of exo-zodiacal light, delta magnitude, and working angle.
455

456
        This method uses the Kepler state transition matrix to propagate a
457
        planet's state (position and velocity vectors) forward in time using
458
        the Kepler state transition matrix.
459

460
        Args:
461
            sInd (int):
462
                Index of the target system of interest
463
            dt (~astropy.units.Quantity(float)):
464
                Time increment in units of day, for planet position propagation
465

466
        Returns:
467
            None
468
        """
469

470
        PPMod = self.PlanetPhysicalModel
1✔
471
        TL = self.TargetList
1✔
472

473
        # Get dt in days
474
        dt = dt.to_value("day")
1✔
475

476
        assert np.isscalar(
1✔
477
            sInd
478
        ), "Can only propagate one system at a time, sInd must be scalar."
479
        # check for planets around this target
480
        pInds = np.where(self.plan2star == sInd)[0]
1✔
481
        if len(pInds) == 0:
1✔
482
            return
×
483
        # check for positive time increment
484
        assert dt >= 0, "Time increment (dt) to propagate a planet must be positive."
1✔
485
        if dt == 0:
1✔
486
            return
×
487

488
        # Calculate initial positions in AU and velocities in AU/day
489
        r0 = self.r[pInds].to_value(u.AU)
1✔
490
        v0 = self.v[pInds].to_value(self.AU_div_day)
1✔
491
        # stack dimensionless positions and velocities
492
        nPlans = pInds.size
1✔
493
        x0 = np.reshape(np.concatenate((r0, v0), axis=1), nPlans * 6)
1✔
494

495
        # Get vector of gravitational parameter in AU3/day2
496
        mu = self.mu_AU3_div_day2[pInds]
1✔
497

498
        # use keplertools.keplerSTM to propagate the system
499
        prop = planSys(x0, mu, epsmult=10.0)
1✔
500
        try:
1✔
501
            prop.takeStep(dt)
1✔
502
        except ValueError:
×
503
            # try again with larger epsmult and two steps to force convergence
504
            prop = planSys(x0, mu, epsmult=100.0)
×
505
            try:
×
NEW
506
                prop.takeStep(dt / 2.0)
×
NEW
507
                prop.takeStep(dt / 2.0)
×
508
            except ValueError:
×
509
                raise ValueError("planSys error")
×
510

511
        # split off position and velocity vectors
512
        x1 = np.array(np.hsplit(prop.x0, 2 * nPlans))
1✔
513
        rind = np.array(range(0, len(x1), 2))  # even indices
1✔
514
        vind = np.array(range(1, len(x1), 2))  # odd indices
1✔
515

516
        # update planets' position, velocity, planet-star distance, apparent,
517
        # separation, phase function, exozodi surface brightness, delta magnitude and
518
        # working angle
519
        self.r[pInds] = x1[rind] << u.AU
1✔
520
        self.v[pInds] = x1[vind] << self.AU_div_day
1✔
521

522
        try:
1✔
523
            self.d[pInds] = np.linalg.norm(self.r[pInds].to_value(u.AU), axis=1) << u.AU
1✔
524
            if len(self.phiIndex) == 0:
1✔
525
                self.phi[pInds] = PPMod.calc_Phi(
1✔
526
                    np.arccos(
527
                        self.r[pInds, 2].to_value(u.AU) / self.d[pInds].to_value(u.AU)
528
                    )
529
                    << u.rad,
530
                    phiIndex=self.phiIndex,
531
                )
532
            else:
533
                self.phi[pInds] = PPMod.calc_Phi(
×
534
                    np.arccos(
535
                        self.r[pInds, 2].to_value(u.AU) / self.d[pInds].to_value(u.AU)
536
                    )
537
                    << u.rad,
538
                    phiIndex=self.phiIndex[pInds],
539
                )
540
        except u.UnitTypeError:
×
541
            self.d[pInds] = np.linalg.norm(self.r[pInds], axis=1) * self.r.unit
×
542
            if len(self.phiIndex) == 0:
×
543
                self.phi[pInds] = PPMod.calc_Phi(
×
544
                    np.arccos(self.r[pInds, 2] / self.d[pInds]), phiIndex=self.phiIndex
545
                )
546
            else:
547
                self.phi[pInds] = PPMod.calc_Phi(
×
548
                    np.arccos(self.r[pInds, 2] / self.d[pInds]),
549
                    phiIndex=self.phiIndex[pInds],
550
                )
551

552
        self.dMag[pInds] = deltaMag(
1✔
553
            self.p[pInds], self.Rp[pInds], self.d[pInds], self.phi[pInds]
554
        )
555
        try:
1✔
556
            # self.s[pInds] = np.linalg.norm(self.r[pInds, 0:2], axis=1)
557
            self.s[pInds] = (
1✔
558
                np.linalg.norm(self.r[pInds, 0:2].to_value(u.AU), axis=1) << u.AU
559
            )
560
            # self.WA[pInds] = np.arctan(self.s[pInds] / TL.dist[sInd]).to("arcsec")
561
            self.WA[pInds] = (
1✔
562
                np.arctan(
563
                    self.s[pInds].to_value(u.AU)
564
                    * self.AU2pc
565
                    / TL.dist[sInd].to_value(u.pc)
566
                )
567
                * self.rad2arcsec
568
                << u.arcsec
569
            )
570
        except u.UnitTypeError:
×
571
            self.s[pInds] = np.linalg.norm(self.r[pInds, 0:2], axis=1) * self.r.unit
×
572
            self.WA[pInds] = np.arctan(self.s[pInds] / TL.dist[sInd]).to("arcsec")
×
573

574
    def scale_JEZ(self, sInd, mode, pInds=None):
1✔
575
        """Scales the exozodi intensity by the inverse square of the planet-star distance.
576

577
        Args:
578
            sInd (int):
579
                Index of the target system of interest
580
            mode (dict):
581
                Selected observing mode
582
            pInds (numpy.ndarray):
583
                Planet indices. Default value (None) will return all planets in the system
584

585
        Returns:
586
            numpy.ndarray:
587
                Scaled exozodi intensity for each planet in the system in units of
588
                photon/s/m2/arcsec2
589
        """
590
        # Get the 1 AU value of JEZ for the system
591
        JEZ0 = self.TargetList.JEZ0[mode["hex"]][sInd]
1✔
592

593
        # Scale JEZ by nEZ and the inverse square of the planet-star distance
594
        all_pinds = np.where(self.plan2star == sInd)[0]
1✔
595
        if pInds is None:
1✔
596
            pinds = all_pinds
1✔
597
        else:
598
            pinds = np.intersect1d(all_pinds, pInds)
1✔
599
        JEZ = JEZ0 * self.nEZ[pinds] * (1 / self.d[pinds].to("AU").value) ** 2
1✔
600
        return JEZ
1✔
601

602
    def set_planet_phase(self, beta=np.pi / 2):
1✔
603
        """Positions all planets at input star-planet-observer phase angle
604
        where possible. For systems where the input phase angle is not achieved,
605
        planets are positioned at quadrature (phase angle of 90 deg).
606

607
        The position found here is not unique. The desired phase angle will be
608
        achieved at two points on the planet's orbit (for non-face on orbits).
609

610
        Args:
611
            beta (float):
612
                star-planet-observer phase angle in radians.
613

614
        """
615

616
        PPMod = self.PlanetPhysicalModel
1✔
617
        TL = self.TargetList
1✔
618

619
        a = self.a.to("AU").value  # semi-major axis
1✔
620
        e = self.e  # eccentricity
1✔
621
        I = self.I.to("rad").value  # noqa: E741 # inclinations
1✔
622
        O = self.O.to("rad").value  # noqa: E741 # long. of the ascending node
1✔
623
        w = self.w.to("rad").value  # argument of perigee
1✔
624
        Mp = self.Mp  # planet masses
1✔
625

626
        # make list of betas
627
        betas = beta * np.ones(w.shape)
1✔
628
        mask = np.cos(betas) / np.sin(I) > 1.0
1✔
629
        num = len(np.where(mask)[0])
1✔
630
        betas[mask] = np.pi / 2.0
1✔
631
        mask = np.cos(betas) / np.sin(I) < -1.0
1✔
632
        num += len(np.where(mask)[0])
1✔
633
        betas[mask] = np.pi / 2.0
1✔
634
        if num > 0:
1✔
635
            self.vprint("***Warning***")
1✔
636
            self.vprint(
1✔
637
                (
638
                    "{} planets out of {} could not be set to phase angle {} radians."
639
                ).format(num, self.nPlans, beta)
640
            )
641
            self.vprint("These planets are set to quadrature (phase angle pi/2)")
1✔
642

643
        # solve for true anomaly
644
        nu = np.arcsin(np.cos(betas) / np.sin(I)) - w
1✔
645

646
        # setup for position and velocity
647
        a1 = np.cos(O) * np.cos(w) - np.sin(O) * np.cos(I) * np.sin(w)
1✔
648
        a2 = np.sin(O) * np.cos(w) + np.cos(O) * np.cos(I) * np.sin(w)
1✔
649
        a3 = np.sin(I) * np.sin(w)
1✔
650
        A = np.vstack((a1, a2, a3))
1✔
651

652
        b1 = -(np.cos(O) * np.sin(w) + np.sin(O) * np.cos(I) * np.cos(w))
1✔
653
        b2 = -np.sin(O) * np.sin(w) + np.cos(O) * np.cos(I) * np.cos(w)
1✔
654
        b3 = np.sin(I) * np.cos(w)
1✔
655
        B = np.vstack((b1, b2, b3))
1✔
656

657
        r = a * (1.0 - e**2) / (1.0 - e * np.cos(nu))
1✔
658
        mu = const.G * (Mp + TL.MsTrue[self.plan2star])
1✔
659
        v1 = -np.sqrt(mu / (self.a * (1.0 - self.e**2))) * np.sin(nu)
1✔
660
        v2 = np.sqrt(mu / (self.a * (1.0 - self.e**2))) * (self.e + np.cos(nu))
1✔
661

662
        self.r = (A * r * np.cos(nu) + B * r * np.sin(nu)).T * u.AU  # position
1✔
663
        self.v = (A * v1 + B * v2).T.to("AU/day")  # velocity
1✔
664

665
        try:
1✔
666
            self.d = np.linalg.norm(self.r, axis=1)  # planet-star distance
1✔
667
            self.phi = PPMod.calc_Phi(
1✔
668
                np.arccos(self.r[:, 2].to("AU").value / self.d.to("AU").value) * u.rad,
669
                phiIndex=self.phiIndex,
670
            )  # planet phase
671
        except u.UnitTypeError:
×
672
            self.d = (
×
673
                np.linalg.norm(self.r, axis=1) * self.r.unit
674
            )  # planet-star distance
675
            self.phi = PPMod.calc_Phi(
×
676
                np.arccos(self.r[:, 2].to("AU").value / self.d.to("AU").value) * u.rad,
677
                phiIndex=self.phiIndex,
678
            )  # planet phase
679

680
        self.dMag = deltaMag(self.p, self.Rp, self.d, self.phi)  # delta magnitude
1✔
681

682
        try:
1✔
683
            self.s = np.linalg.norm(self.r[:, 0:2], axis=1)  # apparent separation
1✔
684
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
1✔
685
                "arcsec"
686
            )  # working angle
687
        except u.UnitTypeError:
×
688
            self.s = (
×
689
                np.linalg.norm(self.r[:, 0:2], axis=1) * self.r.unit
690
            )  # apparent separation
691
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
×
692
                "arcsec"
693
            )  # working angle
694

695
    def dump_systems(self):
1✔
696
        """Create a dictionary of planetary properties for archiving use.
697

698
        Args:
699
            None
700

701
        Returns:
702
            dict:
703
                Dictionary of planetary properties
704

705
        """
706

707
        systems = {
1✔
708
            "a": self.a,
709
            "e": self.e,
710
            "I": self.I,
711
            "O": self.O,
712
            "w": self.w,
713
            "M0": self.M0,
714
            "Mp": self.Mp,
715
            "mu": (
716
                const.G * (self.Mp + self.TargetList.MsTrue[self.plan2star])
717
            ).decompose(),
718
            "Rp": self.Rp,
719
            "p": self.p,
720
            "nEZ": self.nEZ,
721
            "plan2star": self.plan2star,
722
            "star": self.TargetList.Name[self.plan2star],
723
        }
724
        if self.commonSystemPlane:
1✔
725
            systems["systemInclination"] = self.TargetList.systemInclination
×
726
            systems["systemOmega"] = self.TargetList.systemOmega
×
727

728
        return systems
1✔
729

730
    def load_systems(self, systems):
1✔
731
        """Load a dictionary of planetary properties (nominally created by dump_systems)
732

733
        Args:
734
            systems (dict):
735
                Dictionary of planetary properties corresponding to the output of
736
                dump_systems.
737

738
            Returns:
739
                None
740

741
        .. note::
742

743
            If keyword ``systemInclination`` is present in the dictionary, it
744
            is assumed that it was generated with ``commonSystemPlane`` set to
745
            True.
746

747
        .. warning::
748

749
            This method assumes that the exact same targetlist is being used as in the
750
            object that generated the systems dictionary.  If this assumption is
751
            violated unexpected results may occur.
752
        """
753

754
        self.a = systems["a"]
1✔
755
        self.e = systems["e"]
1✔
756
        self.I = systems["I"]  # noqa: E741
1✔
757
        self.O = systems["O"]  # noqa: E741
1✔
758
        self.w = systems["w"]
1✔
759
        self.M0 = systems["M0"]
1✔
760
        self.Mp = systems["Mp"]
1✔
761
        self.Rp = systems["Rp"]
1✔
762
        self.p = systems["p"]
1✔
763
        self.nEZ = systems["nEZ"]
1✔
764
        self.plan2star = systems["plan2star"]
1✔
765

766
        if "systemInclination" in systems:
1✔
767
            self.TargetList.systemInclination = systems["systemInclination"]
×
768
            self.commonSystemPlane = True
×
769
            if "systemOmega" in systems:
×
770
                # leaving as if for backwards compatibility with old dumped
771
                # params for now
772
                self.TargetList.systemOmega = systems["systemOmega"]
×
773

774
        self.init_systems()
1✔
775

776
    def dump_system_params(self, sInd=None):
1✔
777
        """Create a dictionary of time-dependant planet properties for a specific target
778

779
        Args:
780
            sInd (int):
781
                Index of the target system of interest. Default value (None) will
782
                return an empty dictionary with the selected parameters and their units.
783

784
        Returns:
785
            dict:
786
                Dictionary of time-dependant planet properties
787

788
        """
789

790
        # get planet indices
791
        if sInd is None:
1✔
792
            pInds = np.array([], dtype=int)
×
793
        else:
794
            pInds = np.where(self.plan2star == sInd)[0]
1✔
795

796
        # build dictionary
797
        system_params = {
1✔
798
            "d": self.d[pInds],
799
            "phi": self.phi[pInds],
800
            "dMag": self.dMag[pInds],
801
            "WA": self.WA[pInds],
802
        }
803

804
        return system_params
1✔
805

806
    def revise_planets_list(self, pInds):
1✔
807
        """Replaces Simulated Universe planet attributes with filtered values,
808
        and updates the number of planets.
809

810
        Args:
811
            pInds (~numpy.ndarray(int)):
812
                Planet indices to keep
813

814
        Returns:
815
            None
816

817
        .. warning::
818

819
            Throws AssertionError if all planets are removed
820

821
        """
822

823
        # planet attributes which are floats and should not be filtered
824
        bad_atts = ["Min"]
1✔
825

826
        if len(pInds) == 0:
1✔
827
            raise IndexError("Planets list filtered to empty.")
×
828

829
        for att in self.planet_atts:
1✔
830
            if att not in bad_atts:
1✔
831
                if getattr(self, att).size != 0:
1✔
832
                    setattr(self, att, getattr(self, att)[pInds])
1✔
833
        self.nPlans = len(pInds)
1✔
834
        assert self.nPlans, "Planets list is empty: nPlans = %r" % self.nPlans
1✔
835

836
    def revise_stars_list(self, sInds):
1✔
837
        """Revises the TargetList with filtered values, and updates the
838
        planets list accordingly.
839

840
        Args:
841
            sInds (~numpy.ndarray(int)):
842
                Star indices to keep
843

844
        Returns:
845
            None
846

847
        """
848
        self.TargetList.revise_lists(sInds)
1✔
849
        pInds = np.sort(
1✔
850
            np.concatenate([np.where(self.plan2star == x)[0] for x in sInds])
851
        )
852
        self.revise_planets_list(pInds)
1✔
853
        for i, ind in enumerate(sInds):
1✔
854
            self.plan2star[np.where(self.plan2star == ind)[0]] = i
1✔
855

856
    def setup_system_planes(self):
1✔
857
        """
858
        Helper function that augments the system planes if
859
        commonSystemPlane is true
860

861
        Args:
862
            None
863

864
        Returns:
865
            None
866
        """
867
        if self.commonSystemPlane:
1✔
868
            self.I += self.TargetList.systemInclination[self.plan2star]
×
869
            # Ensure all inclinations are in [0, pi]
870
            self.I = (self.I.to(u.deg).value % 180) * u.deg
×
871

872
            self.O += self.TargetList.systemOmega[self.plan2star]
×
873
            # Cut longitude of the ascending nodes to [0, 2pi]
874
            self.O = (self.O.to(u.deg).value % 360) * u.deg
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc