Defining a parameter space and running simulations

An investigation may require a large number of simulations to be carried out in order to explore a parameter space, which typically means that a set of nested loops must be written in order to run all the simulations.

In many cases, only the simulation parameters in the settings tree need to be changed to run a new simulation, but sometimes the sky model and/or telescope model also needs to be changed within a loop. Defining the parameters that need to vary is the first thing to do when writing a new simulation script.

Example: A four-dimensional parameter space

A simple example script which iterates over a 4-dimensional parameter space is shown below. In this case, the observation length (3 values: short, medium, long), the target field (3 values: EoR0, EoR1, EoR2), the ionospheric phase screen (2 values: on, off) and the sky model (2 values: GLEAM and A-team only) were all varied for a total of 36 simulations, and a CASA Measurement Set was written for each case.

Note how nested Python dictionaries are used to define groups of parameters that need to change on each iteration, and the update() method is used to merge one dictionary into another. Each dimension is iterated using the general form:

# Iterate over a dimension.
for key_name, params_to_update in dictionary.items():

    # Update the current settings dictionary.
    current_settings.update(params_to_update)

    # Iterate over the next dimension...

These are all standard Python constructs.

After all the parameters have been set up in the settings tree, an instance of oskar.Interferometer is created using it. Finally, calling oskar.Interferometer.run() will run each simulation.

  1#!/usr/bin/env python3
  2"""
  3Run simulations for SKA1-LOW direction-dependent effects.
  4https://confluence.skatelescope.org/display/SE/Simulations+with+Direction-Dependent+Effects
  5https://jira.skatelescope.org/browse/SIM-489
  6"""
  7
  8import copy
  9import os.path
 10
 11from astropy.io import fits
 12import numpy
 13import oskar
 14
 15from .utils import get_start_time
 16
 17
 18def bright_sources():
 19    """
 20    Returns a list of bright A-team sources.
 21    Does not include the Galactic Centre!
 22    """
 23    # For A: data from the Molonglo Southern 4 Jy sample (VizieR).
 24    # Others from GLEAM reference paper, Hurley-Walker et al. (2017), Table 2.
 25    return numpy.array(
 26        (
 27            [
 28                50.67375,
 29                -37.20833,
 30                528,
 31                0,
 32                0,
 33                0,
 34                178e6,
 35                -0.51,
 36                0,
 37                0,
 38                0,
 39                0,
 40            ],  # For
 41            [
 42                201.36667,
 43                -43.01917,
 44                1370,
 45                0,
 46                0,
 47                0,
 48                200e6,
 49                -0.50,
 50                0,
 51                0,
 52                0,
 53                0,
 54            ],  # Cen
 55            [
 56                139.52500,
 57                -12.09556,
 58                280,
 59                0,
 60                0,
 61                0,
 62                200e6,
 63                -0.96,
 64                0,
 65                0,
 66                0,
 67                0,
 68            ],  # Hyd
 69            [
 70                79.95833,
 71                -45.77889,
 72                390,
 73                0,
 74                0,
 75                0,
 76                200e6,
 77                -0.99,
 78                0,
 79                0,
 80                0,
 81                0,
 82            ],  # Pic
 83            [
 84                252.78333,
 85                4.99250,
 86                377,
 87                0,
 88                0,
 89                0,
 90                200e6,
 91                -1.07,
 92                0,
 93                0,
 94                0,
 95                0,
 96            ],  # Her
 97            [
 98                187.70417,
 99                12.39111,
100                861,
101                0,
102                0,
103                0,
104                200e6,
105                -0.86,
106                0,
107                0,
108                0,
109                0,
110            ],  # Vir
111            [
112                83.63333,
113                22.01444,
114                1340,
115                0,
116                0,
117                0,
118                200e6,
119                -0.22,
120                0,
121                0,
122                0,
123                0,
124            ],  # Tau
125            [
126                299.86667,
127                40.73389,
128                7920,
129                0,
130                0,
131                0,
132                200e6,
133                -0.78,
134                0,
135                0,
136                0,
137                0,
138            ],  # Cyg
139            [
140                350.86667,
141                58.81167,
142                11900,
143                0,
144                0,
145                0,
146                200e6,
147                -0.41,
148                0,
149                0,
150                0,
151                0,
152            ],  # Cas
153        )
154    )
155
156
157def main():
158    """Main function."""
159    # Load GLEAM catalogue data as a sky model.
160    sky_dir = "./"
161    gleam = fits.getdata(sky_dir + "GLEAM_EGC.fits", 1)
162    gleam_sky_array = numpy.column_stack(
163        (gleam["RAJ2000"], gleam["DEJ2000"], gleam["peak_flux_wide"])
164    )
165
166    # Define common base settings.
167    tel_dir = "./"
168    tel_model = "SKA1-LOW_SKO-0000422_Rev3_38m_SKALA4_spot_frequencies.tm"
169    common_settings = {
170        "simulator/max_sources_per_chunk": 65536,
171        "simulator/write_status_to_log_file": True,
172        "observation/start_frequency_hz": 125e6,  # First channel at 125 MHz.
173        "observation/frequency_inc_hz": 5e6,  # Channels spaced every 5 MHz.
174        "observation/num_channels": 11,
175        "telescope/input_directory": tel_dir + tel_model,
176        "interferometer/channel_bandwidth_hz": 100e3,  # 100 kHz-wide channels.
177        "interferometer/time_average_sec": 1.0,
178        "interferometer/max_time_samples_per_block": 4,
179    }
180
181    # Define observations.
182    observations = {
183        "short": {
184            "observation/length": 5 * 60,
185            "observation/num_time_steps": 300,
186            "telescope/external_tec_screen/input_fits_file": "screen_short_300_1.0.fits",
187        },
188        "medium": {
189            "observation/length": 30 * 60,
190            "observation/num_time_steps": 300,
191            "telescope/external_tec_screen/input_fits_file": "screen_medium_300_6.0.fits",
192        },
193        "long": {
194            "observation/length": 4 * 60 * 60,
195            "observation/num_time_steps": 240,
196            "telescope/external_tec_screen/input_fits_file": "screen_long_240_60.0.fits",
197        },
198    }
199
200    # Define fields.
201    fields = {
202        "EoR0": {
203            "observation/phase_centre_ra_deg": 0.0,
204            "observation/phase_centre_dec_deg": -27.0,
205        },
206        "EoR1": {
207            "observation/phase_centre_ra_deg": 60.0,
208            "observation/phase_centre_dec_deg": -30.0,
209        },
210        "EoR2": {
211            "observation/phase_centre_ra_deg": 170.0,
212            "observation/phase_centre_dec_deg": -10.0,
213        },
214    }
215
216    # Define ionosphere settings.
217    ionosphere = {
218        "ionosphere_on": {"telescope/ionosphere_screen_type": "External"},
219        "ionosphere_off": {"telescope/ionosphere_screen_type": "None"},
220    }
221
222    # Define sky model components.
223    sky_models = {
224        "A-team": oskar.Sky.from_array(bright_sources()),
225        "GLEAM": oskar.Sky.from_array(gleam_sky_array),
226    }
227
228    # Loop over observations.
229    for obs_name, obs_params in observations.items():
230        # Copy the base settings dictionary.
231        current_settings = copy.deepcopy(common_settings)
232
233        # Update current settings with observation parameters.
234        current_settings.update(obs_params)
235
236        # Loop over fields.
237        for field_name, field_params in fields.items():
238            # Update current settings with field parameters.
239            current_settings.update(field_params)
240
241            # Update current settings with start time.
242            ra0_deg = current_settings["observation/phase_centre_ra_deg"]
243            length_sec = current_settings["observation/length"]
244            start_time = get_start_time(ra0_deg, length_sec)
245            current_settings["observation/start_time_utc"] = start_time
246
247            # Loop over ionospheric screen on/off.
248            for iono_name, iono_params in ionosphere.items():
249                # Update current settings with ionosphere parameters.
250                current_settings.update(iono_params)
251
252                # Loop over sky model components.
253                for sky_name, sky_model in sky_models.items():
254                    # Update output MS filename based on current parameters.
255                    ms_name = "SKA_LOW_SIM"
256                    ms_name += "_" + obs_name
257                    ms_name += "_" + field_name
258                    ms_name += "_" + iono_name
259                    ms_name += "_" + sky_name
260                    ms_name += ".MS"
261
262                    # Check if the MS already exists (if so, skip).
263                    if os.path.isdir(ms_name):
264                        continue
265
266                    # Create the settings tree.
267                    settings = oskar.SettingsTree("oskar_sim_interferometer")
268                    settings.from_dict(current_settings)
269                    settings["interferometer/ms_filename"] = ms_name
270
271                    # Set up the simulator and run it.
272                    sim = oskar.Interferometer(settings=settings)
273                    sim.set_sky_model(sky_model)
274                    sim.run()
275
276
277if __name__ == "__main__":
278    main()

Example: An irregular frequency axis

Frequency channels which are regularly spaced can be run in one go (and written to a single Measurement Set if required) by specifying multiple channels in the settings. However, when running simulations at spot frequencies across a band, these will need to be run separately by explicitly looping over each one. All that is required is to define a list of frequencies and then loop over them, for example:

axis_freq_MHz = [50, 70, 110, 137, 160, 230, 320]
for freq_MHz in axis_freq_MHz:
    settings['observation/start_frequency_hz'] = freq_MHz * 1e6
    ...