Introduction#

spotter is a package to compute forward models of fluxes and spectra time-series of non-uniform stars.

Its main advantage over other frameworks is its capability to model small scale features thanks to the surface being subdivided into equal-area pixels. In order to do that, spotter exploits the HEALPix subdivision scheme (through the python package healpy).

Here is an introduction on how to define a stellar surface with spotter.

Star object#

We can define a Star object that will contain the map of the star plus some of its parameters such as limb darkening

import matplotlib.pyplot as plt
from spotter import Star, show

star = Star.from_sides(16, u=(0.5, 0.2))

plt.figure(figsize=(3, 3))
show(star)
../../_images/c9466ca71a9258a047b9a0e894ec06384c688ad077196865a418e2c9178fd423.png

As you can see, we defined the map using a number of sides, not pixels, which is how maps are defined in the HEALPix grid. Check out the HEALPix documentation for more information.

Map pixels values#

The pixels value of a star can be directly accessed with

star.y
Array([[1., 1., 1., ..., 1., 1., 1.]], dtype=float32)

The shape of this array is (wavelength, pixels), with the first dimension being for the wavelength axis. Using JAX, here is how we would set a specific pixel i to a certain value.

i = 1248

new_y = star.y.at[0, i].set(0.0)
star = star.set(y=new_y)

plt.figure(figsize=(3, 3))
show(star)
../../_images/5fbbb1eb98351f9f1c26d2b448240bd717109c7d3662ced528cfefaea6151d63.png

Spots#

As an example let’s define a map with some spots on it. We will first define a surface with more pixels, a non-zero inclination and a rotation period

import numpy as np

star = star.from_sides(50, inc=1.3, u=(0.5, 0.2), period=1.0)

Let’s now add some noise, a spot and faculae

star = star.set(y=1 + 0.05 * np.random.rand(star.size))  # noise
star = star.set(y=star.y - 0.6 * star.spot(0.3, 0.5, 0.15, 25))  # spot

faculae = np.sum(
    [
        star.spot(0.3, 0.8, 0.05, 60) * 0.3 * star.y,
        star.spot(-0.3, -0.5, 0.06, 60) * 0.35 * star.y,
        star.spot(-0.5, -0.4, 0.03, 60) * 0.3 * star.y,
    ],
    0,
)

star = star.set(y=star.y + faculae)

show(star)
../../_images/48020b2717abdeae8373bf6d13d6c3960398eb41d7f15a51e1ea147bde58c378.png

Light curves#

Once a surface defined, spotter can be used to compute its light curve.

import jax
from spotter.light_curves import light_curve

time = np.linspace(0, star.period, 200)

plt.plot(time, light_curve(star, time)[0])
plt.xlabel("phase")
_ = plt.ylabel("flux")
../../_images/252f8836e7e9dc5235ef91acc275db9d3c8f4fd9684b73d61019a541e11b66c1.png

Note

All functions in spotter are carefully implemented with JAX, meaning that they can be vectorized and run on GPUs. For example this could be useful to evaluate the flux of a very large number of maps.

spotter can also be used to compute the light curve of a transited star

from spotter.star import transited_star
from spotter.light_curves import transit_light_curve

# impact parameter
b = np.linspace(-2, 2, 100)

flux = jax.vmap(lambda b: transit_light_curve(star, y=b, r=0.3)[0])(b)

plt.figure(figsize=(7, 3))
plt.subplot(121)
plt.plot(b, flux)
plt.xlabel("impact parameter")
plt.ylabel("flux")

plt.subplot(122)
show(transited_star(star, y=0.3, r=0.2))
../../_images/5475a1ba884f4144b30988ec314ffea7df6562ce0b318cbe5dffe88870749208.png

Important

As you can see the transit light curve model contains errors due to the pixelization of the star. This is why spotter is considered an approximate model only. Check out this page for more details about spotter’s precision.

JAX and spotter core functions#

Using spotter core functions directly, other observables can be computed as well. Here are some examples

Ensemble of rotation light curves#

from spotter import distributions
from spotter.core import amplitude, spot

# number of healpix sides
N = 16

# generate maps with a single random spot
n_spots = 500
key = jax.random.PRNGKey(0)
lats = distributions.latitude_uniform_pdf(key, (n_spots,))
lons = np.random.uniform(0, 2 * np.pi, n_spots)
radii = np.random.uniform(0.01, 0.9, n_spots)
contrast = np.random.uniform(0.01, 0.9, n_spots)
ys = 1 - jax.vmap(jax.jit(spot, static_argnums=(0,)), in_axes=(None, 0, 0, 0))(
    N, lats, lons, radii
)

amplitude = jax.jit(amplitude(N, u=star.u[0]))
values = amplitude(ys)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 18
     13 contrast = np.random.uniform(0.01, 0.9, n_spots)
     14 ys = 1 - jax.vmap(jax.jit(spot, static_argnums=(0,)), in_axes=(None, 0, 0, 0))(
     15     N, lats, lons, radii
     16 )
---> 18 amplitude = jax.jit(amplitude(N, u=star.u[0]))
     19 values = amplitude(ys)

File ~/checkouts/readthedocs.org/user_builds/spotter/envs/stable/lib/python3.10/site-packages/spotter/core.py:401, in amplitude(N_or_y, inc, u, undersampling)
    398 hp_resolution = resolution * undersampling
    399 phases = jnp.arange(0, 2 * jnp.pi, hp_resolution)
--> 401 mask, projected_area, limb_darkening = jax.jit(
    402     jax.vmap(jax.jit(mask_projected_limb), in_axes=(None, 0, None, None))
    403 )(N, phases, inc, u)
    405 geometry = mask * projected_area
    406 norm = (geometry * limb_darkening).sum(1)

    [... skipping hidden 35 frame]

File ~/checkouts/readthedocs.org/user_builds/spotter/envs/stable/lib/python3.10/site-packages/spotter/core.py:134, in mask_projected_limb(N_or_y, phase, inc, u, obl)
    108 def mask_projected_limb(N_or_y, phase=None, inc=None, u=None, obl=None):
    109     """
    110     Compute mask, projected area, and limb darkening for visible pixels.
    111 
   (...)
    132         Limb darkening factor for each pixel.
    133     """
--> 134     N, _ = _N_or_Y_to_N_n(N_or_y)
    135     X = vec(N)
    136     d = distance(X, equator_coords(phase, inc, obl))

File ~/checkouts/readthedocs.org/user_builds/spotter/envs/stable/lib/python3.10/site-packages/spotter/core.py:170, in _N_or_Y_to_N_n(N_or_y)
    168     N = N_or_y
    169 else:
--> 170     N = hp.npix2nside(N_or_y.size)
    171     n = N_or_y.size
    172 return N, n

File ~/checkouts/readthedocs.org/user_builds/spotter/envs/stable/lib/python3.10/site-packages/healpy/pixelfunc.py:1121, in npix2nside(npix)
   1089 """Give the nside parameter for the given number of pixels.
   1090 
   1091 Parameters
   (...)
   1118 ValueError: Wrong pixel number (it is not 12*nside**2)
   1119 """
   1120 if not isnpixok(npix):
-> 1121     raise ValueError("Wrong pixel number (it is not 12*nside**2)")
   1122 return int(np.sqrt(npix / 12.0))

ValueError: Wrong pixel number (it is not 12*nside**2)

Here we evaluated the amplitude of the rotational light curves of 500 maps, each containing a single random spot. Let’s estimate the processing time of this computation

%timeit amplitude(ys)
693 μs ± 12.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

i.e. about tens of \(\mu s\) per unique map!

Note

Notice here how we didn’t use the Star object at all. Sometimes, especially when dealing with a large number of stars, spotter’s objects are not as optimal as working directly with the stellar maps, especially when using JAX transforms.

Stellar contamination estimates#

Other features of spotter are useful to compute the spot coverage, either of the map itself

# trivial
spotted = star.y < 1.0
coverage = np.mean(spotted)

print(f"coverage: {coverage * 100:.2f}%")
coverage: 1.98%

or within a specific transit chord

from spotter.core import mask_projected_limb, transit_chord, vec

transit = transit_chord(star.N, 0.0, 0.2, star.inc)

mask, area, _ = mask_projected_limb(vec(star.N), u=star.u[0], inc=star.inc)

spotted = (star.y[0] < 1.0) * mask
transited = transit * mask > 0.0

# let's show the chord
show(star * transited, vmin=0.0, vmax=1.5)

# covering fraction in the chord
chord_coverage = (spotted * transited * area).sum() / (transited * area).sum()
print(f"chord coverage: {chord_coverage * 100:.2f}%")
chord coverage: 20.59%
../../_images/c8860de1b4457649e98c9336b8eb3262176d879696ad07c4be2249be050f7790.png

which is useful to compute transmission spectroscopy stellar contamination effects.