tabsim 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tabsim/config.py ADDED
@@ -0,0 +1,904 @@
1
+ import yaml
2
+ import re
3
+ import os
4
+ import sys
5
+ import shutil
6
+
7
+ import collections.abc
8
+ from typing import Tuple
9
+ from datetime import datetime
10
+
11
+ import dask.array as da
12
+ from dask.array.core import Array
13
+ import xarray as xr
14
+ import numpy as np
15
+ import pandas as pd
16
+
17
+ from tabsim.dask.observation import Observation
18
+ from tabsim.sky import generate_random_sky
19
+ from tabsim.plot import plot_uv, plot_src_alt, plot_angular_seps
20
+ from tabsim.write import write_ms, mk_obs_name, mk_obs_dir
21
+ from tabsim.jax.coordinates import calculate_fringe_frequency
22
+ from tabsim.tle import get_visible_satellite_tles
23
+
24
+ from daskms import xds_from_ms
25
+
26
+ from tqdm import tqdm
27
+ from importlib.resources import files
28
+
29
+ config_dir = files("tabsim.data").joinpath("config_files").__str__()
30
+
31
+ sim_base_config_path = os.path.join(config_dir, "sim_config_base.yaml")
32
+ tab_base_config_path = os.path.join(config_dir, "tab_config_base.yaml")
33
+ # extract_base_config_path = os.path.join(config_dir, "extract_config_base.yaml")
34
+ # pow_spec_base_config_path = os.path.join(
35
+ # config_dir, "extract_pow_spec_config_base.yaml"
36
+ # )
37
+
38
+ JD0 = 2459997.079914223 # 2023-02-21 13:55:04.589 UTC => GMSA = 0
39
+ MJD0 = JD0 - 2400000.5
40
+
41
+
42
+ def deep_update(d: dict, u: dict) -> dict:
43
+ """Recursively update a dictionary which includes subdictionaries.
44
+
45
+ Parameters
46
+ ----------
47
+ d : dict
48
+ Base dictionary to update.
49
+ u : dict
50
+ Update dictionary.
51
+
52
+ Returns
53
+ -------
54
+ dict
55
+ Updated dictionary.
56
+ """
57
+ for k, v in u.items():
58
+ if isinstance(v, collections.abc.Mapping):
59
+ d[k] = deep_update(d.get(k, {}), v)
60
+ else:
61
+ d[k] = v
62
+ return d
63
+
64
+
65
+ loader = yaml.SafeLoader
66
+ loader.add_implicit_resolver(
67
+ "tag:yaml.org,2002:float",
68
+ re.compile(
69
+ """^(?:
70
+ [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
71
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
72
+ |\\.[0-9_]+(?:[eE][-+][0-9]+)?
73
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*
74
+ |[-+]?\\.(?:inf|Inf|INF)
75
+ |\\.(?:nan|NaN|NAN))$""",
76
+ re.X,
77
+ ),
78
+ list("-+0123456789."),
79
+ )
80
+
81
+
82
+ def yaml_load(path):
83
+ config = yaml.load(open(path), Loader=loader)
84
+ return config
85
+
86
+
87
+ class Tee(object):
88
+ """https://stackoverflow.com/questions/17866724/python-logging-print-statements-while-having-them-print-to-stdout"""
89
+
90
+ def __init__(self, *files):
91
+ self.files = files
92
+
93
+ def write(self, obj):
94
+ for f in self.files:
95
+ f.write(obj)
96
+
97
+ def flush(self):
98
+ pass
99
+
100
+
101
+ def load_config(path: str, config_type: str = "sim") -> dict:
102
+ """Load a configuration file and populate default parameters where needed.
103
+
104
+ Parameters
105
+ ----------
106
+ path : str
107
+ Path to the yaml config file.
108
+ config_type : str, optional
109
+ Type of configuration file, by default "sim". Options are {"sim", "tab", "extract", "pow_spec"}.
110
+
111
+ Returns
112
+ -------
113
+ dict
114
+ Configuration dictionary.
115
+ """
116
+
117
+ config = yaml_load(path)
118
+ if config_type == "sim":
119
+ base_config = yaml_load(sim_base_config_path)
120
+ elif config_type == "tab":
121
+ base_config = yaml_load(tab_base_config_path)
122
+ elif config_type == "extract":
123
+ base_config = yaml_load(extract_base_config_path)
124
+ elif config_type == "pow_spec":
125
+ base_config = yaml_load(pow_spec_base_config_path)
126
+ else:
127
+ ValueError(
128
+ "A config type must be specified. Options are {sim, tab, extract, pow_spec}."
129
+ )
130
+
131
+ return deep_update(base_config, config)
132
+
133
+
134
+ def load_sky_model(file_path: str, freqs: Array, src_type: str) -> tuple:
135
+ """Read in a skymodel file and return parameters to be used in tabascal simulator.
136
+ A skymodel file is a csv file with the following columns:
137
+ (src_id, I, Q, U, V, ra, dec, spec_idx, ref_freq, major, minor, post_ang)
138
+ Depending on the 'src_type' the last 3 columns may be required. src_id, Q, U, and V
139
+ are not used.
140
+
141
+ Parameters
142
+ ----------
143
+ file_path : str
144
+ The file path to the skymodel file.
145
+ freqs : Array
146
+ Array of observation frequencies used to build a sky model
147
+ src_type : str
148
+ The source type to return. Options are {'point', 'gauss', 'exp'}.
149
+
150
+ Returns
151
+ -------
152
+ tuple
153
+ A tuple of dask arrays including at least (I, ra, dec) and some of
154
+ (major, minor, pos_ang).
155
+
156
+ Raises
157
+ ------
158
+ KeyError
159
+ The error is raised when one of the accepted source types is not chosen.
160
+ """
161
+
162
+ freqs = da.atleast_1d(freqs)
163
+
164
+ def calc_S_nu(I: Array, f0: Array, si: Array):
165
+ return (
166
+ I[:, None, None]
167
+ * da.ones((len(I), 1, len(freqs)))
168
+ * (freqs[None, None, :] / f0[:, None, None]) ** si[:, None, None]
169
+ )
170
+
171
+ src_list = [
172
+ da.atleast_1d(x) for x in np.loadtxt(file_path, skiprows=1, delimiter=",").T
173
+ ]
174
+
175
+ if src_type == "point":
176
+ src_id, I, Q, U, V, si, f0, ra, dec = src_list[:9]
177
+ S = calc_S_nu(I, f0, si)
178
+ return S, ra, dec
179
+ elif src_type == "gauss":
180
+ src_id, I, Q, U, V, si, f0, ra, dec, major, minor, pos_ang = src_list
181
+ S = calc_S_nu(I, f0, si)
182
+ return S, ra, dec, major, minor, pos_ang
183
+ elif src_type == "exp":
184
+ src_id, I, Q, U, V, si, f0, ra, dec, major = src_list[:10]
185
+ S = calc_S_nu(I, f0, si)
186
+ return S, ra, dec, major
187
+ else:
188
+ raise KeyError("'src_type' must be one of {'point', 'gauss', 'exp'}.")
189
+
190
+
191
+ def sigma_value(value: str | float | int, obs: Observation):
192
+ try:
193
+ if isinstance(value, str):
194
+ sigma = (np.mean(obs.noise_std) / np.sqrt(obs.n_time * obs.n_bl)).compute()
195
+ n_sig = float(value.replace("sigma", ""))
196
+ value = n_sig * sigma
197
+ return value
198
+ elif isinstance(value, float) or isinstance(value, int):
199
+ return float(value)
200
+ except:
201
+ raise ValueError()
202
+
203
+
204
+ def load_obs(obs_spec: dict) -> Observation:
205
+
206
+ tel_ = obs_spec["telescope"]
207
+ obs_ = obs_spec["observation"]
208
+ dask_ = obs_spec["dask"]
209
+
210
+ def arange(start: float, delta: float, n: int):
211
+ x = da.arange(start, start + n * delta, delta)[:n]
212
+ return x
213
+
214
+ if obs_["start_time_jd"]:
215
+ from tabascal.jax.coordinates import jd_to_mjd
216
+
217
+ start_time_mjd = jd_to_mjd(obs_["start_time_jd"])
218
+ elif obs_["start_time_isot"]:
219
+ from astropy.time import Time
220
+ from tabascal.jax.coordinates import jd_to_mjd
221
+
222
+ start_time_mjd = jd_to_mjd(
223
+ Time(obs_["start_time_isot"], format="isot", scale="ut1").jd
224
+ )
225
+ elif obs_["start_time_lha"] is not None:
226
+ gsa = obs_["start_time_lha"] - tel_["longitude"] + obs_["ra"]
227
+ start_time_mjd = MJD0 + (gsa / 360)
228
+ elif obs_["start_time"] is not None:
229
+ start_time = obs_["start_time"]
230
+ start_time_mjd = MJD0 + start_time / (24 * 3600)
231
+ else:
232
+ ValueError(
233
+ "A start time must be given in either the 'start_time', 'start_time_jd', 'start_time_isot' or 'start_time_lha'"
234
+ )
235
+
236
+ time_range = arange(0, obs_["int_time"], obs_["n_time"])
237
+ times_mjd = start_time_mjd + time_range / (24 * 3600)
238
+ freqs = arange(obs_["start_freq"], obs_["chan_width"], obs_["n_freq"])
239
+
240
+ obs = Observation(
241
+ latitude=tel_["latitude"],
242
+ longitude=tel_["longitude"],
243
+ elevation=tel_["elevation"],
244
+ ra=obs_["ra"],
245
+ dec=obs_["dec"],
246
+ times_mjd=times_mjd,
247
+ freqs=freqs,
248
+ int_time=obs_["int_time"],
249
+ chan_width=obs_["chan_width"],
250
+ SEFD=obs_["SEFD"],
251
+ ENU_path=tel_["enu_path"],
252
+ ITRF_path=tel_["itrf_path"],
253
+ n_ant=tel_["n_ant"],
254
+ dish_d=tel_["dish_d"],
255
+ random_seed=obs_["random_seed"],
256
+ auto_corrs=obs_["auto_corrs"],
257
+ no_w=obs_["no_w"],
258
+ n_int_samples=obs_["n_int"],
259
+ tel_name=tel_["name"],
260
+ target_name=obs_["target_name"],
261
+ max_chunk_MB=dask_["max_chunk_MB"],
262
+ )
263
+
264
+ print()
265
+ print(f"Theoretical synthesized beam width : {3600*obs.syn_bw:.0f} arcsec")
266
+
267
+ return obs
268
+
269
+
270
+ def add_power_spectrum_sources(obs: Observation, ps_rand: dict) -> None:
271
+
272
+ from tge import simulate_sky, Cl, Pk, beam_constants, lm_to_radec
273
+
274
+ const = beam_constants(
275
+ D=obs.dish_d.compute(), freq=obs.freqs[0].compute(), dBdT=None, f=1
276
+ )
277
+ beam = lambda x: 1
278
+ cl_ps_keys = ["A", "beta"]
279
+ cl_ps_args = {key: value for key, value in ps_rand.items() if key in cl_ps_keys}
280
+ pk_ps_keys = ["Po", "k0", "gamma"]
281
+ pk_ps_args = {key: value for key, value in ps_rand.items() if key in pk_ps_keys}
282
+ non_ps_keys = ["n_side", "fov_f", "type", "random_seed"]
283
+ # ps_args = {key: value for key, value in ps_rand.items() if key not in non_ps_keys}
284
+
285
+ if ps_rand["type"] == "Cl":
286
+ I, lxy = simulate_sky(
287
+ N_side=ps_rand["n_side"],
288
+ fov=ps_rand["fov_f"] * const["thetaFWHM"],
289
+ Cl=Cl,
290
+ PS_args=cl_ps_args,
291
+ beam=beam,
292
+ seed=ps_rand["random_seed"],
293
+ )
294
+ elif ps_rand["type"] == "Pk":
295
+ I, lxy = simulate_sky(
296
+ N_side=ps_rand["n_side"],
297
+ fov=ps_rand["fov_f"] * const["thetaFWHM"],
298
+ Pk=Pk,
299
+ PS_args=pk_ps_args,
300
+ beam=beam,
301
+ seed=ps_rand["random_seed"],
302
+ )
303
+ else:
304
+ ValueError("Keyword 'type' in section pow_spec.random must be one of {Cl, Pk}")
305
+
306
+ ra, dec = lm_to_radec(lxy, obs.ra, obs.dec).T
307
+ I = const["dBdT"] * da.asarray(I.reshape(-1, 1))
308
+ obs.addAstro(I[:, None, :], ra, dec)
309
+
310
+
311
+ def add_astro_sources(obs: Observation, obs_spec: dict) -> None:
312
+ """Add astronomical sources from the simulation config file to the observation object.
313
+
314
+ Parameters
315
+ ----------
316
+ obs : Observation
317
+ Observation object instance.
318
+ obs_spec : dict
319
+ Simulation config dictionary.
320
+ """
321
+
322
+ methods = {
323
+ "point": obs.addAstro,
324
+ "gauss": obs.addAstroGauss,
325
+ "exp": obs.addAstroExp,
326
+ }
327
+ ast_ = obs_spec["ast_sources"]
328
+
329
+ if ast_["pow_spec"]["random"]["type"]:
330
+ add_power_spectrum_sources(obs, ast_["pow_spec"]["random"])
331
+
332
+ for key in ast_.keys():
333
+
334
+ path = ast_[key]["path"]
335
+ if path is not None:
336
+ params = load_sky_model(path, obs.freqs, key)
337
+ print()
338
+ print(f"Adding {len(params[0])} {key} sources from {path} ...")
339
+ methods[key](*params)
340
+
341
+ if "n_src" in ast_[key]["random"]:
342
+ if ast_[key]["random"]["n_src"] > 0:
343
+ rand_ = ast_[key]["random"]
344
+ n_beam = rand_["n_beam"]
345
+ max_beam = rand_["max_sep"] / 3600 / n_beam
346
+ beam_width = np.min([obs.syn_bw, max_beam])
347
+ fov = np.min([obs.fov, 180.0])
348
+ print()
349
+ print(
350
+ f"Generating {rand_['n_src']} sources within {fov:.2f} deg FoV ({fov/2:.2f} radius) ..."
351
+ )
352
+ print(f"Minimum {n_beam*beam_width*3600:.1f} arcsec separation ...")
353
+
354
+ I, d_ra, d_dec = generate_random_sky(
355
+ n_src=rand_["n_src"],
356
+ min_I=sigma_value(rand_["min_I"], obs),
357
+ max_I=sigma_value(rand_["max_I"], obs),
358
+ freqs=obs.freqs,
359
+ fov=fov,
360
+ beam_width=beam_width,
361
+ random_seed=rand_["random_seed"],
362
+ n_beam=n_beam,
363
+ )
364
+ ra = (obs.ra + d_ra) % 360
365
+ dec = obs.dec + d_dec
366
+ dec = np.where(dec > 90, 180 - dec, dec)
367
+ dec = np.where(dec < -90, -180 - dec, dec)
368
+
369
+ if key == "point":
370
+ print()
371
+ print(f"Adding {rand_['n_src']} random {key} sources ...")
372
+ methods[key](I[:, None, :], ra, dec)
373
+ elif key == "gauss":
374
+ rng = np.random.default_rng(rand_["random_seed"] + 1)
375
+ pos_ang = rng.uniform(low=0.0, high=360.0, size=(rand_["n_src"],))
376
+ major_ = np.abs(
377
+ rng.normal(
378
+ loc=rand_["major_mean"],
379
+ scale=rand_["major_std"],
380
+ size=(rand_["n_src"],),
381
+ )
382
+ )
383
+ minor_ = np.abs(
384
+ rng.normal(
385
+ loc=rand_["minor_mean"],
386
+ scale=rand_["minor_std"],
387
+ size=(rand_["n_src"],),
388
+ )
389
+ )
390
+ major = np.where(major_ > minor_, major_, minor_)
391
+ minor = np.where(major_ < minor_, major_, minor_)
392
+ print()
393
+ print(f"Adding {rand_['n_src']} random {key} sources ...")
394
+ methods[key](
395
+ I[:, None, :],
396
+ major,
397
+ minor,
398
+ pos_ang,
399
+ ra,
400
+ dec,
401
+ )
402
+ elif key == "exp":
403
+ rng = np.random.default_rng(rand_["random_seed"] + 1)
404
+ shape = np.abs(
405
+ rng.normal(
406
+ loc=rand_["size_mean"],
407
+ scale=rand_["size_std"],
408
+ size=(rand_["n_src"],),
409
+ )
410
+ )
411
+ print()
412
+ print(f"Adding {rand_['n_src']} random {key} sources ...")
413
+ methods[key](I[:, None, :], shape, ra, dec)
414
+
415
+
416
+ def gauss(A: float, mean: float, sigma: float, x: Array) -> Array:
417
+ return A * da.exp(-0.5 * ((x - mean) / sigma) ** 2)
418
+
419
+
420
+ def generate_spectra(spec_df: pd.DataFrame, freqs: Array, id_key: str) -> tuple:
421
+
422
+ spectra = []
423
+ ids = []
424
+ for i, spec in spec_df.iterrows():
425
+ if spec["sig_type"].lower() == "gauss":
426
+ ids.append(spec[id_key])
427
+ spectra.append(
428
+ gauss(spec["power"], spec["freq"], spec["band_width"] / 2, freqs)
429
+ )
430
+ else:
431
+ print(f"sig_type : {spec['sig_type']} not supported.")
432
+
433
+ return np.array(ids), da.atleast_2d(spectra)
434
+
435
+
436
+ def add_satellite_sources(obs: Observation, obs_spec: dict) -> None:
437
+
438
+ sat_ = obs_spec["rfi_sources"]["satellite"]
439
+
440
+ # Circular path based Satellites
441
+ if len(sat_["sat_ids"]) > 0:
442
+ if sat_["circ_path"] is not None:
443
+ oles = pd.read_csv(sat_["circ_path"])
444
+ oles = oles[oles["sat_id"].isin(sat_["sat_ids"])]
445
+ path_ids = oles["sat_id"].values
446
+ else:
447
+ raise ValueError(
448
+ "'circ_path' must be populated to include satellites with circular orbits."
449
+ )
450
+
451
+ sat_spec = pd.read_csv(sat_["spec_model"])
452
+ sat_spec = sat_spec[sat_spec["sat_id"].isin(path_ids)]
453
+
454
+ if len(sat_spec) > 0:
455
+ spec_ids, spectra = generate_spectra(sat_spec, obs.freqs, "sat_id")
456
+ uids = np.unique(spec_ids)
457
+ for uid in uids:
458
+ Pv = (
459
+ sat_["power_scale"]
460
+ * da.sum(spectra[spec_ids == uid], axis=0)[None, None, :]
461
+ * da.ones((1, 1, obs.n_freq))
462
+ )
463
+ ole = oles[oles["sat_id"] == uid]
464
+ if len(ole) == 1:
465
+ print()
466
+ print("Adding satellite RFI source ...")
467
+ obs.addSatelliteRFI(
468
+ Pv,
469
+ ole["elevation"].values,
470
+ ole["inclination"].values,
471
+ ole["lon_asc_node"].values,
472
+ ole["periapsis"].values,
473
+ )
474
+ else:
475
+ print()
476
+ print(f"sat_id: {uid} multiply-defined.")
477
+ else:
478
+ print("No 'sat_ids' matching in 'spec_model' file given.")
479
+
480
+
481
+ def add_tle_satellite_sources(
482
+ obs: Observation, obs_spec: dict, spacetrack_path: str
483
+ ) -> None:
484
+
485
+ sat_ = obs_spec["rfi_sources"]["tle_satellite"]
486
+
487
+ # TLE path based Satellites
488
+ tle_cond = [
489
+ sat_["norad_ids_path"],
490
+ len(sat_["norad_ids"]) > 0,
491
+ len(sat_["sat_names"]) > 0,
492
+ ]
493
+ if np.any(tle_cond):
494
+
495
+ if spacetrack_path:
496
+ st_ = yaml_load(spacetrack_path)
497
+ else:
498
+ raise ValueError(
499
+ "Space-Track login details must be given to simulate TLE based satellites"
500
+ )
501
+
502
+ if sat_["norad_ids_path"] is None:
503
+ norad_ids = sat_["norad_ids"]
504
+ else:
505
+ norad_ids = np.concatenate(
506
+ [sat_["norad_ids"], np.loadtxt(sat_["norad_ids_path"], usecols=0)]
507
+ )
508
+
509
+ from astropy.time import Time
510
+
511
+ jd_step = sat_["vis_step"] / (24 * 60)
512
+ times_check = Time(
513
+ np.arange(obs.times_mjd[0], obs.times_mjd[-1] + jd_step, jd_step),
514
+ format="mjd",
515
+ )
516
+
517
+ norad_ids, tles = get_visible_satellite_tles(
518
+ st_["username"],
519
+ st_["password"],
520
+ times_check,
521
+ obs.latitude,
522
+ obs.longitude,
523
+ obs.elevation,
524
+ obs.ra,
525
+ obs.dec,
526
+ sat_["max_ang_sep"],
527
+ sat_["min_alt"],
528
+ sat_["sat_names"],
529
+ norad_ids,
530
+ sat_["tle_dir"],
531
+ )
532
+
533
+ print(f"NORAD IDs includeed : {norad_ids}")
534
+
535
+ sat_spec = pd.read_csv(sat_["norad_spec_model"])
536
+ sat_spec = sat_spec[sat_spec["norad_id"].isin(norad_ids)]
537
+
538
+ print(f"Spectral models for {len(sat_spec)} TLE satellites found.")
539
+
540
+ if len(sat_spec) > 0:
541
+ print()
542
+ print("Adding TLE-based satellite RFI sources ...")
543
+ ids, spectra = generate_spectra(sat_spec, obs.freqs, "norad_id")
544
+ uids = np.unique(ids)
545
+ for uid in tqdm(uids[: sat_["max_n_sat"]]):
546
+ Pv = (
547
+ sat_["power_scale"]
548
+ * da.sum(spectra[ids == uid], axis=0)[None, None, :]
549
+ * da.ones((1, 1, obs.n_freq))
550
+ )
551
+ tle = tles[norad_ids == uid]
552
+ if len(tle) == 1:
553
+ obs.addTLESatelliteRFI(Pv, [uid], tle)
554
+ else:
555
+ print()
556
+ print(f"norad_id: {uid} multiply-defined.")
557
+ else:
558
+ print("No NORAD IDs matching in 'norad_spec_model' file given.")
559
+
560
+
561
+ def add_stationary_sources(obs: Observation, obs_spec: dict) -> None:
562
+
563
+ stat_ = obs_spec["rfi_sources"]["stationary"]
564
+ if len(stat_["loc_ids"]) > 0:
565
+ ids = stat_["loc_ids"]
566
+ path_ids = []
567
+ if stat_["geo_path"] is not None:
568
+ geos = pd.read_csv(stat_["geo_path"])
569
+ geos = geos[geos["loc_id"].isin(ids)]
570
+ path_ids.append(geos["loc_id"].values)
571
+ else:
572
+ raise ValueError(
573
+ "'geo_path' must be populated to include stationary RFI sources."
574
+ )
575
+
576
+ if len(path_ids) == 0:
577
+ print("No location IDs matching in 'geo_path' file given.")
578
+ else:
579
+ path_ids = np.concatenate(path_ids)
580
+
581
+ stat_spec = pd.read_csv(stat_["spec_model"])
582
+ stat_spec = stat_spec[stat_spec["loc_id"].isin(ids)]
583
+
584
+ if len(stat_spec) > 0:
585
+ ids, spectra = generate_spectra(stat_spec, obs.freqs, "loc_id")
586
+ uids = np.unique(ids)
587
+ for uid in uids:
588
+ Pv = (
589
+ stat_["power_scale"]
590
+ * da.sum(spectra[ids == uid], axis=0)[None, None, :]
591
+ * da.ones((1, 1, obs.n_freq))
592
+ )
593
+ geo = geos[geos["loc_id"] == uid]
594
+ if len(geo) == 1:
595
+ print()
596
+ print("Adding stationary RFI source ...")
597
+ obs.addStationaryRFI(
598
+ Pv,
599
+ geo["latitude"].values,
600
+ geo["longitude"].values,
601
+ geo["elevation"].values,
602
+ )
603
+ else:
604
+ print()
605
+ print(f"loc_id: {uid} multiply-defined.")
606
+ else:
607
+ print("No locations IDs matching in 'spec_model' file given.")
608
+
609
+
610
+ def add_gains(obs: Observation, obs_spec: dict) -> None:
611
+
612
+ gains_ = obs_spec["gains"]
613
+ gain_offset = gains_["G0_mean"] != 1 or gains_["G0_std"] != 0
614
+ gain_var = gains_["Gt_std_amp"] != 0 or gains_["Gt_std_phase"] != 0
615
+ if gain_offset or gain_var:
616
+ print()
617
+ print("Adding gains ...")
618
+ obs.addGains(
619
+ gains_["G0_mean"],
620
+ gains_["G0_std"],
621
+ gains_["Gt_std_amp"],
622
+ gains_["Gt_std_phase"],
623
+ gains_["Gt_corr_amp"],
624
+ gains_["Gt_corr_phase"],
625
+ gains_["random_seed"],
626
+ )
627
+ else:
628
+ print()
629
+ print("No gains added ...")
630
+
631
+
632
+ def plot_diagnostics(obs: Observation, obs_spec: dict, save_path: str) -> None:
633
+
634
+ diag_ = obs_spec["diagnostics"]
635
+
636
+ if diag_["src_alt"]:
637
+ print()
638
+ print("Plotting source altitude ...")
639
+ plot_src_alt(obs, save_path)
640
+ if diag_["uv_cov"]:
641
+ print()
642
+ print("Plotting uv coverage ...")
643
+ plot_uv(obs, save_path)
644
+ if diag_["rfi_seps"]:
645
+ print()
646
+ print("Plotting RFI angular separations ...")
647
+ plot_angular_seps(obs, save_path)
648
+
649
+
650
+ def print_signal_specs(
651
+ vis_rfi: Array, vis_ast: Array, noise_data: Array, flags: Array
652
+ ) -> None:
653
+
654
+ rfi_amp = da.mean(da.abs(vis_rfi)).compute()
655
+ ast_amp = da.mean(da.abs(vis_ast)).compute()
656
+ noise = da.std(noise_data.real).compute()
657
+ flag_rate = 100 * da.mean(flags).compute()
658
+
659
+ print()
660
+ print(f"Mean RFI Amp. : {rfi_amp:.2f} Jy")
661
+ print(f"Mean AST Amp. : {ast_amp:.2f} Jy")
662
+ print(f"Vis Noise Amp. : {noise:.2f} Jy")
663
+ print(f"Flag Rate : {flag_rate:.1f} %")
664
+
665
+
666
+ def write_to_ms(xds: xr.Dataset, ms_path: str, overwrite: bool) -> None:
667
+ start = datetime.now()
668
+ print()
669
+ print(f"Writing visibilities to MS ...")
670
+ write_ms(xds, ms_path, overwrite)
671
+ end = datetime.now()
672
+ print(f"MS Write Time : {end - start}")
673
+
674
+
675
+ def write_to_zarr(obs: Observation, zarr_path: str, overwrite: bool) -> None:
676
+ start = datetime.now()
677
+ print()
678
+ print(f"Writing visibilities to zarr ...")
679
+ obs.write_to_zarr(zarr_path, overwrite)
680
+ end = datetime.now()
681
+ print(f"zarr Write Time : {end - start}")
682
+
683
+
684
+ def save_data(obs: Observation, obs_spec: dict, zarr_path: str, ms_path: str) -> None:
685
+
686
+ if obs_spec["output"]["zarr"] or obs_spec["output"]["ms"]:
687
+ print()
688
+ print("Calculating visibilities ...")
689
+ obs.calculate_vis(flags=obs_spec["output"]["flag_data"])
690
+
691
+ overwrite = obs_spec["output"]["overwrite"]
692
+
693
+ if obs_spec["output"]["zarr"] and obs_spec["output"]["ms"]:
694
+ write_to_zarr(obs, zarr_path, overwrite)
695
+ xds = xr.open_zarr(zarr_path)
696
+ write_to_ms(xds, ms_path, overwrite)
697
+ print_signal_specs(
698
+ xds.vis_rfi.data, xds.vis_ast.data, xds.noise_data.data, xds.flags.data
699
+ )
700
+ elif obs_spec["output"]["zarr"]:
701
+ write_to_zarr(obs, zarr_path, overwrite)
702
+ xds = xr.open_zarr(zarr_path)
703
+ print_signal_specs(
704
+ xds.vis_rfi.data, xds.vis_ast.data, xds.noise_data.data, xds.flags.data
705
+ )
706
+ elif obs_spec["output"]["ms"]:
707
+ write_to_ms(obs.dataset, ms_path, overwrite)
708
+ xds = xds_from_ms(ms_path)
709
+ print_signal_specs(
710
+ xds.RFI_MODEL_DATA.data,
711
+ xds.AST_MODEL_DATA.data,
712
+ xds.NOISE_DATA.data,
713
+ xds.FLAG.data,
714
+ )
715
+ else:
716
+ ValueError(
717
+ "No output format has been chosen. output: zarr: or output: ms: must be True."
718
+ )
719
+
720
+
721
+ def save_inputs(obs: Observation, obs_spec: dict, save_path: str) -> None:
722
+
723
+ for key in ["enu_path", "itrf_path"]:
724
+ path = obs_spec["telescope"][key]
725
+ if path is not None:
726
+ shutil.copy(path, save_path)
727
+
728
+ for key in obs_spec["ast_sources"].keys():
729
+ path = obs_spec["ast_sources"][key]["path"]
730
+ if path is not None:
731
+ shutil.copy(path, save_path)
732
+
733
+ key = (
734
+ 2
735
+ * [
736
+ "tle_satellite",
737
+ ]
738
+ + 2
739
+ * [
740
+ "satellite",
741
+ ]
742
+ + 2
743
+ * [
744
+ "stationary",
745
+ ]
746
+ )
747
+ subkey = [
748
+ "norad_ids_path",
749
+ "norad_spec_model",
750
+ "circ_path",
751
+ "spec_model",
752
+ "geo_path",
753
+ "spec_model",
754
+ ]
755
+ for key1, key2 in zip(key, subkey):
756
+ path = obs_spec["rfi_sources"][key1][key2]
757
+ if path is not None:
758
+ shutil.copy(path, save_path)
759
+
760
+ np.savetxt(os.path.join(save_path, "norad_ids.yaml"), obs.norad_ids, fmt="%i")
761
+
762
+ with open(os.path.join(save_path, "sim_config.yaml"), "w") as fp:
763
+ yaml.dump(obs_spec, fp)
764
+
765
+
766
+ def print_fringe_freq_sat(obs: Observation):
767
+ if len(obs.times_fine) == obs.n_int_samples:
768
+ n_int = int(obs.n_int_samples) - 1
769
+ elif obs.times[-1] - obs.times[0] > 120:
770
+ n_int = int(60 / obs.int_time) * int(obs.n_int_samples)
771
+ else:
772
+ n_int = obs.n_int_samples
773
+ fringe_params = [
774
+ {
775
+ "times": obs.times_fine[::n_int].compute(),
776
+ "freq": obs.freqs.max().compute(),
777
+ "rfi_xyz": da.concatenate(obs.rfi_satellite_xyz, axis=0)[
778
+ i, ::n_int
779
+ ].compute(),
780
+ "ants_itrf": obs.ITRF.compute(),
781
+ "ants_u": obs.ants_uvw[::n_int, :, 0].compute(),
782
+ "dec": obs.dec.compute(),
783
+ }
784
+ for i in range(obs.n_rfi_satellite)
785
+ ]
786
+ fringe_freq = [calculate_fringe_frequency(**f_params) for f_params in fringe_params]
787
+ f_sample = (
788
+ np.pi
789
+ * np.max(np.abs(fringe_freq))
790
+ * np.max(obs.rfi_satellite_A_app)
791
+ / np.sqrt(6 * obs.noise_std.mean()).compute()
792
+ )
793
+ n_int = int(np.ceil(obs.int_time * f_sample))
794
+
795
+ print()
796
+ print(f"Maximum Fringe Frequency is : {np.max(np.abs(fringe_freq)):.2f} Hz")
797
+ print(f"Maximum sampling rate is : {f_sample:.2f} Hz")
798
+ print(f"Recommended n_int is >= : {n_int:.0f} ({obs.n_int_samples} used)")
799
+
800
+
801
+ def print_fringe_freq_tle_sat(obs: Observation):
802
+ if len(obs.times_fine) == obs.n_int_samples:
803
+ n_int = obs.n_int_samples - 1
804
+ elif obs.times[-1] - obs.times[0] > 120:
805
+ n_int = int(60 / obs.int_time) * obs.n_int_samples
806
+ else:
807
+ n_int = obs.n_int_samples
808
+ fringe_params = [
809
+ {
810
+ "times_mjd": obs.times_mjd_fine[::n_int].compute(),
811
+ "freq": obs.freqs.max().compute(),
812
+ "rfi_xyz": da.concatenate(obs.rfi_tle_satellite_xyz, axis=0)[
813
+ i, ::n_int
814
+ ].compute(),
815
+ "ants_itrf": obs.ITRF.compute(),
816
+ "ants_u": obs.ants_uvw[::n_int, :, 0].compute(),
817
+ "dec": obs.dec.compute(),
818
+ }
819
+ for i in range(obs.n_rfi_tle_satellite)
820
+ ]
821
+ fringe_freq = [calculate_fringe_frequency(**f_params) for f_params in fringe_params]
822
+ f_sample = (
823
+ np.pi
824
+ * np.max(np.abs(fringe_freq))
825
+ * np.max(obs.rfi_tle_satellite_A_app)
826
+ / np.sqrt(6 * obs.noise_std.mean()).compute()
827
+ )
828
+ n_int = int(np.ceil(obs.int_time * f_sample))
829
+
830
+ print()
831
+ print(f"Maximum Fringe Frequency is : {np.max(np.abs(fringe_freq)):.2f} Hz")
832
+ print(
833
+ f"Maximum RFI Amplitude : {np.max(obs.rfi_tle_satellite_A_app):.5f} sqrt(Jy)"
834
+ )
835
+ print(f"Maximum sampling rate is : {f_sample:.2f} Hz")
836
+ print(f"Recommended n_int is >= : {n_int:.0f} ({obs.n_int_samples} used)")
837
+
838
+
839
+ def run_sim_config(
840
+ obs_spec: dict = None, config_path: str = None, spacetrack_path: str = None
841
+ ) -> Tuple[Observation, str]:
842
+
843
+ from tabsim.tle import id_generator
844
+
845
+ log_path = f"log_sim_{id_generator()}.txt"
846
+ log = open(log_path, "w")
847
+ backup = sys.stdout
848
+ sys.stdout = Tee(sys.stdout, log)
849
+
850
+ start = datetime.now()
851
+ print(datetime.now())
852
+
853
+ if config_path is not None:
854
+ obs_spec = load_config(config_path, config_type="sim")
855
+ elif obs_spec is None:
856
+ print("obs_spec or path must be defined.")
857
+ return None
858
+
859
+ obs = load_obs(obs_spec)
860
+ add_astro_sources(obs, obs_spec)
861
+ add_satellite_sources(obs, obs_spec)
862
+ if obs_spec["rfi_sources"]["tle_satellite"]["max_n_sat"] != 0:
863
+ add_tle_satellite_sources(obs, obs_spec, spacetrack_path)
864
+ add_stationary_sources(obs, obs_spec)
865
+ add_gains(obs, obs_spec)
866
+
867
+ # Change this for calculating all RFI sources
868
+ if obs.n_rfi_satellite > 0:
869
+ print_fringe_freq_sat(obs)
870
+ if obs.n_rfi_tle_satellite > 0:
871
+ print_fringe_freq_tle_sat(obs)
872
+
873
+ print(obs)
874
+
875
+ obs_name = mk_obs_name(
876
+ obs_spec["output"]["prefix"], obs, obs_spec["output"]["suffix"]
877
+ )
878
+ save_path, zarr_path, ms_path = mk_obs_dir(
879
+ obs_spec["output"]["path"], obs_name, obs_spec["output"]["overwrite"]
880
+ )
881
+
882
+ input_path = os.path.join(save_path, "input_data")
883
+
884
+ os.makedirs(input_path, exist_ok=True)
885
+ save_inputs(obs, obs_spec, input_path)
886
+
887
+ print()
888
+ print(f"Writing data to : {save_path}")
889
+
890
+ plot_diagnostics(obs, obs_spec, save_path)
891
+ save_data(obs, obs_spec, zarr_path, ms_path)
892
+
893
+ end = datetime.now()
894
+ print()
895
+ print(f"Total simulation time : {end - start}")
896
+ print()
897
+ print(datetime.now())
898
+
899
+ log.close()
900
+ shutil.copy(log_path, save_path)
901
+ os.remove(log_path)
902
+ sys.stdout = backup
903
+
904
+ return obs, save_path