Universal Gap function for use with L01, CU SIM and L2D

Overall goal

The overall goal here is to provide a simple and easy to use code that generates a masking function mimicking our best understanding of missing data with respect to LISA.

What does the code do?

The code takes in a dictionary of planned and unplanned gaps with their specific types, rates per year and duration in hours. It can then produce a binary mask

:nbsphinx-math:`begin{equation}

w(t) = 1 text{if} t not in T_{text{gap}} text{and} w(t) = boldsymbol{text{nan}} text{if} t in T_{text{gap}},. end{equation}`

[1]:
%reload_ext autoreload
%autoreload 2

import numpy as np
import matplotlib.pyplot as plt
from lisaglitch import GapMaskGenerator
from lisaconstants import TROPICALYEAR_J2000DAY


[2]:
# Set up simulation properties
A_YEAR = TROPICALYEAR_J2000DAY * 86400  # seconds in a year
dt = 0.25  # seconds
t_start = 0  # start time in seconds
t_obs = 0.5 * A_YEAR
sim_t = t_start + np.arange(0, t_obs, dt)  # time array

planseed = 2618240388
unplanseed = 3387490715

# Build dictionary of gap definitions, planned and unplanned. We must
# supply both of these, even if the rates are zero and durations are zero.
# We can cope with arbitrary number of gaps.
gap_definitions = {
    "planned": {
        "antenna repointing": {"rate_per_year": 26, "duration_hr": 3.3},
        "TM stray potential": {"rate_per_year": 2, "duration_hr": 24},
        "TTL calibration": {"rate_per_year": 4, "duration_hr": 48},
        # "Aliens": {"rate_per_year": 6, "duration_hr": 30*24}
    },
    "unplanned": {
        "platform safe mode": {"rate_per_year": 3, "duration_hr": 60},
        "payload safe mode": {"rate_per_year": 4, "duration_hr": 66},
        "QPD loss micrometeoroid": {"rate_per_year": 5, "duration_hr": 24},
        "HR GRS loss micrometeoroid": {"rate_per_year": 19, "duration_hr": 24},
        "WR GRS loss micrometeoroid": {"rate_per_year": 6, "duration_hr": 24},
    },
}

# Initialise the class with simulation properties and whether or not to treat gaps with
# nans or not.
gap_mask_gen = GapMaskGenerator(
    sim_t, gap_definitions, treat_as_nan=False, planseed=planseed, unplanseed=unplanseed
)

# Generate the mask as a np.array. Choose whether to include unplanned and/or planned gaps.
# num_zeros = np.sum(full_mask == 0)/len(full_mask)
full_mask = gap_mask_gen.generate_mask(include_unplanned=True, include_planned=True)
num_zeros = np.sum(full_mask == 0) / len(full_mask)
print("Usable data")
print(f"Duty cyle is = {100*(1-num_zeros)} %")

plt.plot(sim_t / 60 / 60 / 24, full_mask)
plt.xlabel(r"Time [days]")
plt.ylabel(r"Mask")
plt.title(r"Generic masking function")
plt.grid()
Usable data
Duty cyle is = 90.5473681205097 %
_images/gap_notebook_2_1.png

Some extra utilities

It is possible to save the masking function as a .h5 file. Similarly, we can load the .h5 file as a class object and generate the masking function.

[3]:
# Save the masking function to a hdf5 file.
gap_mask_gen.save_to_hdf5(full_mask, filename="gap_mask_data.h5")

# Load the masking function to a hdf5 file. Note that we do not need to instantiate the class.
# This will load in the masking function and all important attributes.
load_gating_func = GapMaskGenerator.from_hdf5(filename="gap_mask_data.h5")
plt.plot(
    sim_t / 60 / 60 / 24,
    load_gating_func.generate_mask(include_unplanned=True, include_planned=True),
)
[3]:
[<matplotlib.lines.Line2D at 0x130447770>]
_images/gap_notebook_4_1.png

Generate summary of gap configuration

We can easily generate a summary of the gap configuration using one of the functions in the class method.

[4]:
check = gap_mask_gen.summary(mask=full_mask, export_json_path="gap_mask_summary.json")
from pprint import pprint

pprint(check)
{'mask_analysis': {'duty_cycle_percent': np.float64(90.5474),
                   'number_of_gap_segments': 24,
                   'total_gap_hours': np.float64(414.3),
                   'total_gap_samples': 5965920},
 'planned_gaps': {'TM stray potential': {'duration_hr': 24,
                                         'duration_samples': 345600,
                                         'duration_sec': 86400,
                                         'rate_events_per_sec': 6.33775307356206e-08,
                                         'rate_events_per_year': 2},
                  'TTL calibration': {'duration_hr': 48,
                                      'duration_samples': 691200,
                                      'duration_sec': 172800,
                                      'rate_events_per_sec': 1.267550614712412e-07,
                                      'rate_events_per_year': 4},
                  'antenna repointing': {'duration_hr': 3.3,
                                         'duration_samples': 47520,
                                         'duration_sec': 11880.0,
                                         'rate_events_per_sec': 8.239078995630678e-07,
                                         'rate_events_per_year': 26}},
 'seeds': {'planned': 2618240388, 'unplanned': 3387490715},
 'simulation': {'dt': np.float64(0.25),
                'duration_days': np.float64(182.6210966435185),
                'duration_sec': np.float64(15778462.75),
                'n_data': 63113851},
 'unplanned_gaps': {'HR GRS loss micrometeoroid': {'duration_hr': 24,
                                                   'duration_samples': 345600,
                                                   'duration_sec': 86400,
                                                   'rate_events_per_sec': 6.020865419883957e-07,
                                                   'rate_events_per_year': 19},
                    'QPD loss micrometeoroid': {'duration_hr': 24,
                                                'duration_samples': 345600,
                                                'duration_sec': 86400,
                                                'rate_events_per_sec': 1.584438268390515e-07,
                                                'rate_events_per_year': 5},
                    'WR GRS loss micrometeoroid': {'duration_hr': 24,
                                                   'duration_samples': 345600,
                                                   'duration_sec': 86400,
                                                   'rate_events_per_sec': 1.901325922068618e-07,
                                                   'rate_events_per_year': 6},
                    'payload safe mode': {'duration_hr': 66,
                                          'duration_samples': 950400,
                                          'duration_sec': 237600,
                                          'rate_events_per_sec': 1.267550614712412e-07,
                                          'rate_events_per_year': 4},
                    'platform safe mode': {'duration_hr': 60,
                                           'duration_samples': 864000,
                                           'duration_sec': 216000,
                                           'rate_events_per_sec': 9.50662961034309e-08,
                                           'rate_events_per_year': 3}}}

Smooth tapers

So far the gap function generates a binary mask with eithers zeros or nans representing the gaps. For those within L2D, it may be preferable to smoothly taper the gap segments to zeros in order to reduce artefacts working in the frequency or time-frequency domains.

To facilitate this, we have taken the binary mask and applied a smooth taper on either side of each gap with the “length of the lobes” provided as input. An example is given below

[5]:
from lisagap import GapWindowGenerator

from lisagap import GapWindowGenerator

masking_func_from_window = GapWindowGenerator(gap_mask_gen)
[6]:
# Define the length of the tapering window on each side of the gap. Must input lobe lengths in hours.

# If we miss out a particular gap definition, it will not be tapered.

taper_defs = {
    "planned": {
        "antenna repointing": {"lobe_lengths_hr": 5.0},
        "TM stray potential": {"lobe_lengths_hr": 0.5},
        "TTL calibration": {"lobe_lengths_hr": 2.0},
    },
    # "Aliens": {"lobe_lengths_hr" : 1 * 24}},
    "unplanned": {
        "platform safe mode": {"lobe_lengths_hr": 1.0},
        "HR GRS loss micrometeoroid": {"lobe_lengths_hr": 0.25},
        "QPD loss micrometeoroid": {"lobe_lengths_hr": 1.0},
        "HR GRS loss micrometeoroid": {"lobe_lengths_hr": 7.0},
        "WR GRS loss micrometeoroid": {"lobe_lengths_hr": 10.0},
    },
}

smoothed_mask = masking_func_from_window.generate_window(
    include_planned=True,
    include_unplanned=True,
    apply_tapering=True,
    taper_definitions=taper_defs,
)
[7]:
plt.plot(sim_t / 60 / 60 / 24, smoothed_mask)
plt.xlim([55.5, 57])
plt.xlabel(r"Time [days]")
plt.ylabel(r"Mask")
plt.title(r"Check antenna repointing lobe lengths")
plt.grid()
_images/gap_notebook_10_0.png

Build quality masking data set with flags

We can also use this class to build a masking function based off the nature of a data set. If a np.array is fed into a particular functino of the class, it can be read in and a further array with 1s for valid data and nans for invalid data can be produced

[8]:
gap_mask_gen_w_nans = GapMaskGenerator(
    sim_t,
    gap_definitions,
    planseed=2618240388,
    unplanseed=3387490715,
    treat_as_nan=True,
)

# Generate the mask as a np.array. Choose whether to include unplanned and/or planned gaps.
full_mask_w_nans = gap_mask_gen_w_nans.generate_mask(
    include_unplanned=True, include_planned=True
)


# Build the quality flags.
quality_flags = gap_mask_gen.build_quality_flags(full_mask_w_nans)
plt.plot(sim_t / 60 / 60 / 24, quality_flags)
plt.ylim([-0.1, 1.1])
plt.xlabel(r"Time [days]")
plt.ylabel(r"Mask")
plt.title(r"Generic masking function")
plt.grid()
# Check, what is the overall duty cycle?

duty_cycle = 100 * (len(quality_flags) - np.sum(quality_flags)) / len(quality_flags)
print("Duty cycle = ", duty_cycle, "%")
Duty cycle =  90.54736812050972 %
_images/gap_notebook_12_1.png

Proportional Tapering for External Gap Masks

The new apply_proportional_tapering static method allows you to apply smart tapering to gap masks loaded from external files (like .npy arrays). This is particularly useful when you have gap masks from other sources and want to apply proportional tapering based on gap duration.

Key Features:

  • Automatic gap detection: Finds both NaN and zero-valued gaps

  • Smart categorization: Different taper fractions for short, medium, and long gaps

  • Proportional tapering: Taper length scales with gap duration (e.g., 5% of gap length)

  • Tukey window: Uses optimal α calculation for smooth transitions

[10]:
build_masking_func = GapMaskGenerator.quality_flags_to_mask(quality_flags)
[51]:
# Apply proportional tapering using the static method
print("Applying proportional tapering...")

# Standard proportional tapering (5% each side for medium/long gaps)
tapered_standard = GapWindowGenerator.apply_proportional_tapering(
    build_masking_func,
    dt=dt,
    short_taper_fraction=1.0,  # 25% each side for short gaps
    medium_taper_fraction=0.05,  # 25% each side for medium gaps
    long_taper_fraction=0.05,  # 5% each side for long gaps
    short_gap_threshold_minutes=2,  # Gaps < 15 min are "short"
    long_gap_threshold_hours=0.5,  # Gaps > 1 hour are "long"
)
Applying proportional tapering...
[52]:
plt.plot(sim_t / 60 / 60 / 24, tapered_standard)
plt.xlim([55.5, 57])
plt.xlabel(r"Time [days]")
plt.ylabel(r"Mask")
plt.title(r"Check proportional tapering")
plt.grid()
_images/gap_notebook_16_0.png