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

dsavransky / EXOSIMS / 20436234891

22 Dec 2025 03:24PM UTC coverage: 65.7% (-0.07%) from 65.768%
20436234891

push

github

web-flow
Merge pull request #452 from CoreySpohn/fixed_nEZ_val

Add optional fixed_nEZ_val

14 of 20 new or added lines in 6 files covered. (70.0%)

17 existing lines in 5 files now uncovered.

9830 of 14962 relevant lines covered (65.7%)

0.66 hits per line

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

85.57
/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 True.
42
        fixed_nEZ_val (float):
43
            A value representing the number of exozodi applied to *every* star. Defaults to None.
44
        **specs:
45
            :ref:`sec:inputspec`
46

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

136

137
    .. note::
138

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

144
    .. warning::
145

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

151
    """
152

153
    _modtype = "SimulatedUniverse"
1✔
154

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

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

186
        # Set the number of exozodi
187
        self.commonSystemnEZ = commonSystemnEZ
1✔
188
        self._outspec["commonSystemnEZ"] = commonSystemnEZ
1✔
189

190
        # A fixed number of exozodi for every system
191
        self.fixed_nEZ_val = fixed_nEZ_val
1✔
192
        self._outspec["fixed_nEZ_val"] = fixed_nEZ_val
1✔
193

194
        # save fixed number of planets to generate
195
        self.fixedPlanPerStar = fixedPlanPerStar
1✔
196
        self._outspec["fixedPlanPerStar"] = fixedPlanPerStar
1✔
197

198
        # get cache directory
199
        self.cachedir = get_cache_dir(cachedir)
1✔
200
        self._outspec["cachedir"] = self.cachedir
1✔
201
        specs["cachedir"] = self.cachedir
1✔
202

203
        # check if KnownRVPlanetsUniverse has correct input modules
204
        if specs["modules"]["SimulatedUniverse"] == "KnownRVPlanetsUniverse":
1✔
205
            val = (
1✔
206
                specs["modules"]["TargetList"] == "KnownRVPlanetsTargetList"
207
                and specs["modules"]["PlanetPopulation"] == "KnownRVPlanets"
208
            )
209
            assert val, (
1✔
210
                "KnownRVPlanetsUniverse must use KnownRVPlanetsTargetList "
211
                "and KnownRVPlanets"
212
            )
213
        else:
214
            val = (
1✔
215
                specs["modules"]["TargetList"] == "KnownRVPlanetsTargetList"
216
                or specs["modules"]["PlanetPopulation"] == "KnownRVPlanets"
217
            )
218
            assert not (val), (
1✔
219
                "KnownRVPlanetsTargetList or KnownRVPlanets should not be used "
220
                "with this SimulatedUniverse"
221
            )
222

223
        # import TargetList class
224
        self.TargetList = get_module(specs["modules"]["TargetList"], "TargetList")(
1✔
225
            **specs
226
        )
227

228
        # bring inherited class objects to top level of Simulated Universe
229
        TL = self.TargetList
1✔
230
        self.StarCatalog = TL.StarCatalog
1✔
231
        self.PlanetPopulation = TL.PlanetPopulation
1✔
232
        self.PlanetPhysicalModel = TL.PlanetPhysicalModel
1✔
233
        self.OpticalSystem = TL.OpticalSystem
1✔
234
        self.ZodiacalLight = TL.ZodiacalLight
1✔
235
        self.BackgroundSources = TL.BackgroundSources
1✔
236
        self.PostProcessing = TL.PostProcessing
1✔
237
        self.Completeness = TL.Completeness
1✔
238

239
        # initial constant mean anomaly
240
        assert isinstance(Min, (int, float)) or (
1✔
241
            Min is None
242
        ), "Min may be int, float, or None"
243
        if Min is not None:
1✔
244
            self.Min = float(Min) * u.deg
1✔
245
        else:
246
            self.Min = Min
1✔
247
        self._outspec["Min"] = Min
1✔
248

249
        # list of possible planet attributes
250
        self.planet_atts = [
1✔
251
            "plan2star",
252
            "a",
253
            "e",
254
            "I",
255
            "O",
256
            "w",
257
            "M0",
258
            "Min",
259
            "Rp",
260
            "Mp",
261
            "p",
262
            "r",
263
            "v",
264
            "d",
265
            "s",
266
            "phi",
267
            "nEZ",
268
            "dMag",
269
            "WA",
270
        ]
271

272
        self.phiIndex = (
1✔
273
            None  # Used to switch select specific phase function for each planet
274
        )
275

276
        # generate orbital elements, albedos, radii, and masses
277
        self.gen_physical_properties(**specs)
1✔
278

279
        # find initial position-related parameters: position, velocity, planet-star
280
        # distance, apparent separation, number of zodis
281
        self.init_systems()
1✔
282

283
    def __str__(self):
1✔
284
        """String representation of Simulated Universe object
285

286
        When the command 'print' is used on the Simulated Universe object,
287
        this method will return the values contained in the object
288

289
        """
290

291
        for att in self.__dict__:
1✔
292
            print("%s: %r" % (att, getattr(self, att)))
1✔
293

294
        return "Simulated Universe class object attributes"
1✔
295

296
    def gen_physical_properties(self, **specs):
1✔
297
        """Generates the planetary systems' physical properties.
298

299
        Populates arrays of the orbital elements, albedos, masses and radii
300
        of all planets, and generates indices that map from planet to parent star.
301

302
        Args:
303
            **specs:
304
                :ref:`sec:inputspec`
305

306
        Returns:
307
            None
308

309
        """
310

311
        PPop = self.PlanetPopulation
1✔
312
        ZL = self.ZodiacalLight
1✔
313
        TL = self.TargetList
1✔
314

315
        if self.fixedPlanPerStar is not None:  # Must be an int for fixedPlanPerStar
1✔
316
            # Create array of length TL.nStars each w/ value ppStar
317
            targetSystems = np.ones(TL.nStars).astype(int) * self.fixedPlanPerStar
1✔
318
        else:
319
            # treat eta as the rate parameter of a Poisson distribution
320
            targetSystems = np.random.poisson(lam=PPop.eta, size=TL.nStars)
1✔
321

322
        plan2star = []
1✔
323
        for j, n in enumerate(targetSystems):
1✔
324
            plan2star = np.hstack((plan2star, [j] * n))
1✔
325
        self.plan2star = plan2star.astype(int)
1✔
326
        self.sInds = np.unique(self.plan2star)
1✔
327
        self.nPlans = len(self.plan2star)
1✔
328

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

366
            self.a, self.e, self.p, self.Rp = PPop.gen_plan_params(self.nPlans)
1✔
367
            if PPop.scaleOrbits:
1✔
368
                self.a *= np.sqrt(TL.L[self.plan2star])
1✔
369
            self.gen_M0()  # initial mean anomaly
1✔
370
            self.Mp = PPop.gen_mass(self.nPlans)  # mass
1✔
371

372
        if self.fixed_nEZ_val is not None:
1✔
NEW
373
            self.nEZ = np.ones((self.nPlans,)) * self.fixed_nEZ_val
×
374
        elif self.commonSystemnEZ:
1✔
375
            # Assign the same nEZ to all planets in the system
376
            self.nEZ = ZL.gen_systemnEZ(TL.nStars)[self.plan2star]
1✔
377
        else:
378
            # Assign a unique nEZ to each planet
379
            self.nEZ = ZL.gen_systemnEZ(self.nPlans)
×
380

381
        self.phiIndex = np.asarray(
1✔
382
            []
383
        )  # Used to switch select specific phase function for each planet
384

385
    def gen_M0(self):
1✔
386
        """Set initial mean anomaly for each planet"""
387
        if self.Min is not None:
1✔
388
            self.M0 = np.ones((self.nPlans,)) * self.Min
1✔
389
        else:
390
            self.M0 = np.random.uniform(360, size=int(self.nPlans)) * u.deg
1✔
391

392
    def init_systems(self):
1✔
393
        """Finds initial time-dependant parameters. Assigns each planet an
394
        initial position, velocity, planet-star distance, apparent separation,
395
        phase function, surface brightness of exo-zodiacal light, delta
396
        magnitude, and working angle.
397

398
        This method makes use of the systems' physical properties (masses,
399
        distances) and their orbital elements (a, e, I, O, w, M0).
400
        """
401

402
        PPMod = self.PlanetPhysicalModel
1✔
403
        TL = self.TargetList
1✔
404

405
        a = self.a.to("AU").value  # semi-major axis
1✔
406
        e = self.e  # eccentricity
1✔
407
        I = self.I.to("rad").value  # inclinations #noqa: E741
1✔
408
        O = self.O.to("rad").value  # right ascension of the ascending node #noqa: E741
1✔
409
        w = self.w.to("rad").value  # argument of perigee
1✔
410
        M0 = self.M0.to("rad").value  # initial mean anomany
1✔
411
        E = eccanom(M0, e)  # eccentric anomaly
1✔
412
        Mp = self.Mp  # planet masses
1✔
413

414
        # This is the a1 a2 a3 and b1 b2 b3 are the euler angle transformation from
415
        # the I,J,K refernece frame to an x,y,z frame
416
        a1 = np.cos(O) * np.cos(w) - np.sin(O) * np.cos(I) * np.sin(w)
1✔
417
        a2 = np.sin(O) * np.cos(w) + np.cos(O) * np.cos(I) * np.sin(w)
1✔
418
        a3 = np.sin(I) * np.sin(w)
1✔
419
        A = a * np.vstack((a1, a2, a3)) * u.AU
1✔
420
        b1 = -np.sqrt(1 - e**2) * (
1✔
421
            np.cos(O) * np.sin(w) + np.sin(O) * np.cos(I) * np.cos(w)
422
        )
423
        b2 = np.sqrt(1 - e**2) * (
1✔
424
            -np.sin(O) * np.sin(w) + np.cos(O) * np.cos(I) * np.cos(w)
425
        )
426
        b3 = np.sqrt(1 - e**2) * np.sin(I) * np.cos(w)
1✔
427
        B = a * np.vstack((b1, b2, b3)) * u.AU
1✔
428
        r1 = np.cos(E) - e
1✔
429
        r2 = np.sin(E)
1✔
430

431
        mu = const.G * (Mp + TL.MsTrue[self.plan2star])
1✔
432
        self.mu_AU3_div_day2 = mu.to_value(self.AU3_div_day2)
1✔
433
        v1 = np.sqrt(mu / self.a**3) / (1 - e * np.cos(E))
1✔
434
        v2 = np.cos(E)
1✔
435

436
        self.r = (A * r1 + B * r2).T.to("AU")  # position
1✔
437
        self.v = (v1 * (-A * r2 + B * v2)).T.to("AU/day")  # velocity
1✔
438
        self.s = np.linalg.norm(self.r[:, 0:2], axis=1)  # apparent separation
1✔
439
        self.d = np.linalg.norm(self.r, axis=1)  # planet-star distance
1✔
440
        try:
1✔
441
            self.phi = PPMod.calc_Phi(
1✔
442
                np.arccos(self.r[:, 2] / self.d), phiIndex=self.phiIndex
443
            )  # planet phase
444
        except u.UnitTypeError:
×
445
            self.d = self.d * self.r.unit  # planet-star distance
×
446
            self.phi = PPMod.calc_Phi(
×
447
                np.arccos(self.r[:, 2] / self.d), phiIndex=self.phiIndex
448
            )  # planet phase
449

450
        self.dMag = deltaMag(self.p, self.Rp, self.d, self.phi)  # delta magnitude
1✔
451
        try:
1✔
452
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
1✔
453
                "arcsec"
454
            )  # working angle
455
        except u.UnitTypeError:
×
456
            self.s = self.s * self.r.unit
×
457
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
×
458
                "arcsec"
459
            )  # working angle
460

461
    def propag_system(self, sInd, dt):
1✔
462
        """Propagates planet time-dependant parameters: position, velocity,
463
        planet-star distance, apparent separation, phase function, surface brightness
464
        of exo-zodiacal light, delta magnitude, and working angle.
465

466
        This method uses the Kepler state transition matrix to propagate a
467
        planet's state (position and velocity vectors) forward in time using
468
        the Kepler state transition matrix.
469

470
        Args:
471
            sInd (int):
472
                Index of the target system of interest
473
            dt (~astropy.units.Quantity(float)):
474
                Time increment in units of day, for planet position propagation
475

476
        Returns:
477
            None
478
        """
479

480
        PPMod = self.PlanetPhysicalModel
1✔
481
        TL = self.TargetList
1✔
482

483
        # Get dt in days
484
        dt = dt.to_value("day")
1✔
485

486
        assert np.isscalar(
1✔
487
            sInd
488
        ), "Can only propagate one system at a time, sInd must be scalar."
489
        # check for planets around this target
490
        pInds = np.where(self.plan2star == sInd)[0]
1✔
491
        if len(pInds) == 0:
1✔
492
            return
×
493
        # check for positive time increment
494
        assert dt >= 0, "Time increment (dt) to propagate a planet must be positive."
1✔
495
        if dt == 0:
1✔
496
            return
×
497

498
        # Calculate initial positions in AU and velocities in AU/day
499
        r0 = self.r[pInds].to_value(u.AU)
1✔
500
        v0 = self.v[pInds].to_value(self.AU_div_day)
1✔
501
        # stack dimensionless positions and velocities
502
        nPlans = pInds.size
1✔
503
        x0 = np.reshape(np.concatenate((r0, v0), axis=1), nPlans * 6)
1✔
504

505
        # Get vector of gravitational parameter in AU3/day2
506
        mu = self.mu_AU3_div_day2[pInds]
1✔
507

508
        # use keplertools.keplerSTM to propagate the system
509
        prop = planSys(x0, mu, epsmult=10.0)
1✔
510
        try:
1✔
511
            prop.takeStep(dt)
1✔
512
        except ValueError:
×
513
            # try again with larger epsmult and two steps to force convergence
514
            prop = planSys(x0, mu, epsmult=100.0)
×
515
            try:
×
516
                prop.takeStep(dt / 2.0)
×
517
                prop.takeStep(dt / 2.0)
×
518
            except ValueError:
×
519
                raise ValueError("planSys error")
×
520

521
        # split off position and velocity vectors
522
        x1 = np.array(np.hsplit(prop.x0, 2 * nPlans))
1✔
523
        rind = np.array(range(0, len(x1), 2))  # even indices
1✔
524
        vind = np.array(range(1, len(x1), 2))  # odd indices
1✔
525

526
        # update planets' position, velocity, planet-star distance, apparent,
527
        # separation, phase function, exozodi surface brightness, delta magnitude and
528
        # working angle
529
        self.r[pInds] = x1[rind] << u.AU
1✔
530
        self.v[pInds] = x1[vind] << self.AU_div_day
1✔
531

532
        try:
1✔
533
            self.d[pInds] = np.linalg.norm(self.r[pInds].to_value(u.AU), axis=1) << u.AU
1✔
534
            if len(self.phiIndex) == 0:
1✔
535
                self.phi[pInds] = PPMod.calc_Phi(
1✔
536
                    np.arccos(
537
                        self.r[pInds, 2].to_value(u.AU) / self.d[pInds].to_value(u.AU)
538
                    )
539
                    << u.rad,
540
                    phiIndex=self.phiIndex,
541
                )
542
            else:
543
                self.phi[pInds] = PPMod.calc_Phi(
×
544
                    np.arccos(
545
                        self.r[pInds, 2].to_value(u.AU) / self.d[pInds].to_value(u.AU)
546
                    )
547
                    << u.rad,
548
                    phiIndex=self.phiIndex[pInds],
549
                )
550
        except u.UnitTypeError:
×
551
            self.d[pInds] = np.linalg.norm(self.r[pInds], axis=1) * self.r.unit
×
552
            if len(self.phiIndex) == 0:
×
553
                self.phi[pInds] = PPMod.calc_Phi(
×
554
                    np.arccos(self.r[pInds, 2] / self.d[pInds]), phiIndex=self.phiIndex
555
                )
556
            else:
557
                self.phi[pInds] = PPMod.calc_Phi(
×
558
                    np.arccos(self.r[pInds, 2] / self.d[pInds]),
559
                    phiIndex=self.phiIndex[pInds],
560
                )
561

562
        self.dMag[pInds] = deltaMag(
1✔
563
            self.p[pInds], self.Rp[pInds], self.d[pInds], self.phi[pInds]
564
        )
565
        try:
1✔
566
            # self.s[pInds] = np.linalg.norm(self.r[pInds, 0:2], axis=1)
567
            self.s[pInds] = (
1✔
568
                np.linalg.norm(self.r[pInds, 0:2].to_value(u.AU), axis=1) << u.AU
569
            )
570
            # self.WA[pInds] = np.arctan(self.s[pInds] / TL.dist[sInd]).to("arcsec")
571
            self.WA[pInds] = (
1✔
572
                np.arctan(
573
                    self.s[pInds].to_value(u.AU)
574
                    * self.AU2pc
575
                    / TL.dist[sInd].to_value(u.pc)
576
                )
577
                * self.rad2arcsec
578
                << u.arcsec
579
            )
580
        except u.UnitTypeError:
×
581
            self.s[pInds] = np.linalg.norm(self.r[pInds, 0:2], axis=1) * self.r.unit
×
582
            self.WA[pInds] = np.arctan(self.s[pInds] / TL.dist[sInd]).to("arcsec")
×
583

584
    def scale_JEZ(self, sInd, mode, pInds=None):
1✔
585
        """Scales the exozodi intensity to match the current mission state.
586

587
        The exozodi intensity is scaled by the inverse square of the planet-star
588
        distance, the system's fbeta value, and the number of exozodi.
589

590
        Args:
591
            sInd (int):
592
                Index of the target system of interest
593
            mode (dict):
594
                Selected observing mode
595
            pInds (numpy.ndarray):
596
                Planet indices. Default value (None) will return all planets in the system
597

598
        Returns:
599
            numpy.ndarray:
600
                Scaled exozodi intensity for each planet in the system in units of
601
                photon/s/m2/arcsec2
602
        """
603
        # Get the 1 AU value of JEZ for the system
604
        JEZ0 = self.TargetList.JEZ0[mode["hex"]][sInd]
1✔
605
        fbeta = self.TargetList.system_fbeta[sInd]
1✔
606

607
        # Scale JEZ by nEZ and the inverse square of the planet-star distance
608
        all_pinds = np.where(self.plan2star == sInd)[0]
1✔
609
        if pInds is None:
1✔
610
            pinds = all_pinds
1✔
611
        else:
612
            pinds = np.intersect1d(all_pinds, pInds)
1✔
613
        JEZ = JEZ0 * self.nEZ[pinds] * (1 / self.d[pinds].to("AU").value) ** 2 * fbeta
1✔
614
        return JEZ
1✔
615

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

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

624
        Args:
625
            beta (float):
626
                star-planet-observer phase angle in radians.
627

628
        """
629

630
        PPMod = self.PlanetPhysicalModel
1✔
631
        TL = self.TargetList
1✔
632

633
        a = self.a.to("AU").value  # semi-major axis
1✔
634
        e = self.e  # eccentricity
1✔
635
        I = self.I.to("rad").value  # noqa: E741 # inclinations
1✔
636
        O = self.O.to("rad").value  # noqa: E741 # long. of the ascending node
1✔
637
        w = self.w.to("rad").value  # argument of perigee
1✔
638
        Mp = self.Mp  # planet masses
1✔
639

640
        # make list of betas
641
        betas = beta * np.ones(w.shape)
1✔
642
        mask = np.cos(betas) / np.sin(I) > 1.0
1✔
643
        num = len(np.where(mask)[0])
1✔
644
        betas[mask] = np.pi / 2.0
1✔
645
        mask = np.cos(betas) / np.sin(I) < -1.0
1✔
646
        num += len(np.where(mask)[0])
1✔
647
        betas[mask] = np.pi / 2.0
1✔
648
        if num > 0:
1✔
649
            self.vprint("***Warning***")
1✔
650
            self.vprint(
1✔
651
                (
652
                    "{} planets out of {} could not be set to phase angle {} radians."
653
                ).format(num, self.nPlans, beta)
654
            )
655
            self.vprint("These planets are set to quadrature (phase angle pi/2)")
1✔
656

657
        # solve for true anomaly
658
        nu = np.arcsin(np.cos(betas) / np.sin(I)) - w
1✔
659

660
        # setup for position and velocity
661
        a1 = np.cos(O) * np.cos(w) - np.sin(O) * np.cos(I) * np.sin(w)
1✔
662
        a2 = np.sin(O) * np.cos(w) + np.cos(O) * np.cos(I) * np.sin(w)
1✔
663
        a3 = np.sin(I) * np.sin(w)
1✔
664
        A = np.vstack((a1, a2, a3))
1✔
665

666
        b1 = -(np.cos(O) * np.sin(w) + np.sin(O) * np.cos(I) * np.cos(w))
1✔
667
        b2 = -np.sin(O) * np.sin(w) + np.cos(O) * np.cos(I) * np.cos(w)
1✔
668
        b3 = np.sin(I) * np.cos(w)
1✔
669
        B = np.vstack((b1, b2, b3))
1✔
670

671
        r = a * (1.0 - e**2) / (1.0 - e * np.cos(nu))
1✔
672
        mu = const.G * (Mp + TL.MsTrue[self.plan2star])
1✔
673
        v1 = -np.sqrt(mu / (self.a * (1.0 - self.e**2))) * np.sin(nu)
1✔
674
        v2 = np.sqrt(mu / (self.a * (1.0 - self.e**2))) * (self.e + np.cos(nu))
1✔
675

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

679
        try:
1✔
680
            self.d = np.linalg.norm(self.r, axis=1)  # planet-star distance
1✔
681
            self.phi = PPMod.calc_Phi(
1✔
682
                np.arccos(self.r[:, 2].to("AU").value / self.d.to("AU").value) * u.rad,
683
                phiIndex=self.phiIndex,
684
            )  # planet phase
685
        except u.UnitTypeError:
×
686
            self.d = (
×
687
                np.linalg.norm(self.r, axis=1) * self.r.unit
688
            )  # planet-star distance
689
            self.phi = PPMod.calc_Phi(
×
690
                np.arccos(self.r[:, 2].to("AU").value / self.d.to("AU").value) * u.rad,
691
                phiIndex=self.phiIndex,
692
            )  # planet phase
693

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

696
        try:
1✔
697
            self.s = np.linalg.norm(self.r[:, 0:2], axis=1)  # apparent separation
1✔
698
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
1✔
699
                "arcsec"
700
            )  # working angle
701
        except u.UnitTypeError:
×
702
            self.s = (
×
703
                np.linalg.norm(self.r[:, 0:2], axis=1) * self.r.unit
704
            )  # apparent separation
705
            self.WA = np.arctan(self.s / TL.dist[self.plan2star]).to(
×
706
                "arcsec"
707
            )  # working angle
708

709
    def dump_systems(self):
1✔
710
        """Create a dictionary of planetary properties for archiving use.
711

712
        Args:
713
            None
714

715
        Returns:
716
            dict:
717
                Dictionary of planetary properties
718

719
        """
720

721
        systems = {
1✔
722
            "a": self.a,
723
            "e": self.e,
724
            "I": self.I,
725
            "O": self.O,
726
            "w": self.w,
727
            "M0": self.M0,
728
            "Mp": self.Mp,
729
            "mu": (
730
                const.G * (self.Mp + self.TargetList.MsTrue[self.plan2star])
731
            ).decompose(),
732
            "Rp": self.Rp,
733
            "p": self.p,
734
            "nEZ": self.nEZ,
735
            "plan2star": self.plan2star,
736
            "star": self.TargetList.Name[self.plan2star],
737
        }
738
        if self.commonSystemPlane:
1✔
739
            systems["systemInclination"] = self.TargetList.systemInclination
×
740
            systems["systemOmega"] = self.TargetList.systemOmega
×
741

742
        return systems
1✔
743

744
    def load_systems(self, systems):
1✔
745
        """Load a dictionary of planetary properties (nominally created by dump_systems)
746

747
        Args:
748
            systems (dict):
749
                Dictionary of planetary properties corresponding to the output of
750
                dump_systems.
751

752
            Returns:
753
                None
754

755
        .. note::
756

757
            If keyword ``systemInclination`` is present in the dictionary, it
758
            is assumed that it was generated with ``commonSystemPlane`` set to
759
            True.
760

761
        .. warning::
762

763
            This method assumes that the exact same targetlist is being used as in the
764
            object that generated the systems dictionary.  If this assumption is
765
            violated unexpected results may occur.
766
        """
767

768
        self.a = systems["a"]
1✔
769
        self.e = systems["e"]
1✔
770
        self.I = systems["I"]  # noqa: E741
1✔
771
        self.O = systems["O"]  # noqa: E741
1✔
772
        self.w = systems["w"]
1✔
773
        self.M0 = systems["M0"]
1✔
774
        self.Mp = systems["Mp"]
1✔
775
        self.Rp = systems["Rp"]
1✔
776
        self.p = systems["p"]
1✔
777
        self.nEZ = systems["nEZ"]
1✔
778
        self.plan2star = systems["plan2star"]
1✔
779

780
        if "systemInclination" in systems:
1✔
781
            self.TargetList.systemInclination = systems["systemInclination"]
×
782
            self.commonSystemPlane = True
×
783
            if "systemOmega" in systems:
×
784
                # leaving as if for backwards compatibility with old dumped
785
                # params for now
786
                self.TargetList.systemOmega = systems["systemOmega"]
×
787

788
        self.init_systems()
1✔
789

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

793
        Args:
794
            sInd (int):
795
                Index of the target system of interest. Default value (None) will
796
                return an empty dictionary with the selected parameters and their units.
797

798
        Returns:
799
            dict:
800
                Dictionary of time-dependant planet properties
801

802
        """
803

804
        # get planet indices
805
        if sInd is None:
1✔
806
            pInds = np.array([], dtype=int)
×
807
        else:
808
            pInds = np.where(self.plan2star == sInd)[0]
1✔
809

810
        # build dictionary
811
        system_params = {
1✔
812
            "d": self.d[pInds],
813
            "phi": self.phi[pInds],
814
            "dMag": self.dMag[pInds],
815
            "WA": self.WA[pInds],
816
        }
817

818
        return system_params
1✔
819

820
    def revise_planets_list(self, pInds):
1✔
821
        """Replaces Simulated Universe planet attributes with filtered values,
822
        and updates the number of planets.
823

824
        Args:
825
            pInds (~numpy.ndarray(int)):
826
                Planet indices to keep
827

828
        Returns:
829
            None
830

831
        .. warning::
832

833
            Throws AssertionError if all planets are removed
834

835
        """
836

837
        # planet attributes which are floats and should not be filtered
838
        bad_atts = ["Min"]
1✔
839

840
        if len(pInds) == 0:
1✔
841
            raise IndexError("Planets list filtered to empty.")
×
842

843
        for att in self.planet_atts:
1✔
844
            if att not in bad_atts:
1✔
845
                if getattr(self, att).size != 0:
1✔
846
                    setattr(self, att, getattr(self, att)[pInds])
1✔
847
        self.nPlans = len(pInds)
1✔
848
        assert self.nPlans, "Planets list is empty: nPlans = %r" % self.nPlans
1✔
849

850
    def revise_stars_list(self, sInds):
1✔
851
        """Revises the TargetList with filtered values, and updates the
852
        planets list accordingly.
853

854
        Args:
855
            sInds (~numpy.ndarray(int)):
856
                Star indices to keep
857

858
        Returns:
859
            None
860

861
        """
862
        self.TargetList.revise_lists(sInds)
1✔
863
        pInds = np.sort(
1✔
864
            np.concatenate([np.where(self.plan2star == x)[0] for x in sInds])
865
        )
866
        self.revise_planets_list(pInds)
1✔
867
        for i, ind in enumerate(sInds):
1✔
868
            self.plan2star[np.where(self.plan2star == ind)[0]] = i
1✔
869

870
    def setup_system_planes(self):
1✔
871
        """
872
        Helper function that augments the system planes if
873
        commonSystemPlane is true
874

875
        Args:
876
            None
877

878
        Returns:
879
            None
880
        """
881
        if self.commonSystemPlane:
1✔
882
            self.I += self.TargetList.systemInclination[self.plan2star]
×
883
            # Ensure all inclinations are in [0, pi]
884
            self.I = (self.I.to(u.deg).value % 180) * u.deg
×
885

886
            self.O += self.TargetList.systemOmega[self.plan2star]
×
887
            # Cut longitude of the ascending nodes to [0, 2pi]
888
            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