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.
@@ -0,0 +1,136 @@
1
+ import argparse
2
+ import os
3
+
4
+ from tabsim.config import load_config, run_sim_config
5
+
6
+
7
+ def get_abs_path(rel_path: str | None, work_dir: str) -> str | None:
8
+ if rel_path:
9
+ return os.path.abspath(os.path.join(work_dir, rel_path))
10
+ else:
11
+ return None
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(
16
+ description="Simulate an observation defined by a YAML config file."
17
+ )
18
+ parser.add_argument(
19
+ "-c",
20
+ "--config_path",
21
+ required=True,
22
+ help="File path to the observation config file.",
23
+ )
24
+ parser.add_argument(
25
+ "-r",
26
+ "--rfi_amp",
27
+ default=1.0,
28
+ type=float,
29
+ help="Scale the RFI power. Default is 1.0",
30
+ )
31
+ parser.add_argument(
32
+ "-a", "--n_ant", default=None, type=int, help="Number of antennas to include."
33
+ )
34
+ parser.add_argument(
35
+ "-i", "--n_int", default=None, type=int, help="Number of integration samples."
36
+ )
37
+ parser.add_argument(
38
+ "-n",
39
+ "--SEFD",
40
+ default=None,
41
+ type=float,
42
+ help="System Equivalent flux density in Jy. Same across frequency and antennas.",
43
+ )
44
+ parser.add_argument(
45
+ "-dt", "--int_time", default=None, type=float, help="Time step in seconds."
46
+ )
47
+ parser.add_argument(
48
+ "-nt", "--n_time", default=None, type=int, help="Number of time steps."
49
+ )
50
+ parser.add_argument(
51
+ "-o",
52
+ "--overwrite",
53
+ default=False,
54
+ action=argparse.BooleanOptionalAction,
55
+ help="Overwrite existing observation.",
56
+ )
57
+ parser.add_argument(
58
+ "-st", "--spacetrack", help="Path to Space-Track login details."
59
+ )
60
+ parser.add_argument(
61
+ "-ra", "--ra", type=float, help="Right Ascension of the observation."
62
+ )
63
+ args = parser.parse_args()
64
+ rfi_amp = args.rfi_amp
65
+ spacetrack_path = args.spacetrack
66
+
67
+ config_path = os.path.abspath(args.config_path)
68
+ work_dir = os.path.split(config_path)[0]
69
+
70
+ obs_spec = load_config(config_path, config_type="sim")
71
+
72
+ if args.ra is not None:
73
+ obs_spec["observation"]["ra"] = args.ra
74
+
75
+ config_st_path = obs_spec["rfi_sources"]["tle_satellite"]["spacetrack_path"]
76
+ if spacetrack_path:
77
+ obs_spec["rfi_sources"]["tle_satellite"]["spacetrack_path"] = os.path.abspath(
78
+ os.path.join(work_dir, spacetrack_path)
79
+ )
80
+ elif config_st_path:
81
+ config_st_path = get_abs_path(config_st_path, work_dir)
82
+ obs_spec["rfi_sources"]["tle_satellite"]["spacetrack_path"] = config_st_path
83
+ spacetrack_path = config_st_path
84
+
85
+ obs_spec["rfi_sources"]["tle_satellite"]["power_scale"] *= rfi_amp
86
+ obs_spec["rfi_sources"]["satellite"]["power_scale"] *= rfi_amp
87
+ obs_spec["rfi_sources"]["stationary"]["power_scale"] *= rfi_amp
88
+
89
+ obs_spec["output"]["overwrite"] = args.overwrite
90
+
91
+ if args.n_ant is not None:
92
+ obs_spec["telescope"]["n_ant"] = args.n_ant
93
+
94
+ if args.n_int is not None:
95
+ obs_spec["observation"]["n_int"] = args.n_int
96
+
97
+ suffix = obs_spec["output"]["suffix"]
98
+ if suffix:
99
+ suffix = f"{rfi_amp:.1e}RFI_" + suffix
100
+ else:
101
+ suffix = f"_{rfi_amp:.1e}RFI"
102
+ obs_spec["output"]["suffix"] = f"{rfi_amp:.1e}RFI"
103
+
104
+ if args.SEFD is not None:
105
+ obs_spec["observation"]["SEFD"] = args.SEFD
106
+
107
+ if args.int_time is not None:
108
+ obs_spec["observation"]["int_time"] = args.int_time
109
+
110
+ if args.n_time is not None:
111
+ obs_spec["observation"]["n_time"] = args.n_time
112
+
113
+ for ast in ["exp", "gauss", "point", "pow_spec"]:
114
+ obs_spec["ast_sources"][ast]["path"] = get_abs_path(
115
+ obs_spec["ast_sources"][ast]["path"], work_dir
116
+ )
117
+
118
+ obs_spec["output"]["path"] = get_abs_path(obs_spec["output"]["path"], work_dir)
119
+ obs_spec["rfi_sources"]["tle_satellite"]["norad_ids_path"] = get_abs_path(
120
+ obs_spec["rfi_sources"]["tle_satellite"]["norad_ids_path"], work_dir
121
+ )
122
+ obs_spec["rfi_sources"]["tle_satellite"]["norad_spec_model"] = get_abs_path(
123
+ obs_spec["rfi_sources"]["tle_satellite"]["norad_spec_model"], work_dir
124
+ )
125
+ obs_spec["telescope"]["enu_path"] = get_abs_path(
126
+ obs_spec["telescope"]["enu_path"], work_dir
127
+ )
128
+ obs_spec["telescope"]["itrf_path"] = get_abs_path(
129
+ obs_spec["telescope"]["itrf_path"], work_dir
130
+ )
131
+
132
+ return run_sim_config(obs_spec=obs_spec, spacetrack_path=spacetrack_path)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ obs, obs_path = main()
@@ -0,0 +1,191 @@
1
+ from astropy.coordinates import SkyCoord
2
+ import astropy.units as u
3
+ from regions import CircleSkyRegion, RectangleSkyRegion, TextSkyRegion
4
+
5
+ import xarray as xr
6
+ import numpy as np
7
+
8
+ from daskms import xds_from_ms, xds_from_table
9
+
10
+ import argparse
11
+
12
+ import os
13
+
14
+ import matplotlib.colors as mcolors
15
+
16
+ from tabsim.config import yaml_load
17
+ from tabsim.tle import get_tles_by_id, get_satellite_positions
18
+ from tabsim.jax.coordinates import itrf_to_xyz, mjd_to_jd
19
+
20
+ from astropy.coordinates import EarthLocation
21
+ from astropy.time import Time
22
+
23
+ from skyfield.api import load, EarthSatellite, wgs84
24
+
25
+
26
+ def sat_radec(tle: list[str], times_jd: np.ndarray, obs_xyz: np.ndarray) -> np.ndarray:
27
+
28
+ ts = load.timescale()
29
+ t = ts.ut1_jd(times_jd)
30
+
31
+ satellite = EarthSatellite(tle[0], tle[1], ts=ts)
32
+ location = EarthLocation(x=obs_xyz[0], y=obs_xyz[1], z=obs_xyz[2], unit="m")
33
+ observer = wgs84.latlon(
34
+ location.lat.degree, location.lon.degree, location.height.value
35
+ )
36
+ radec = (satellite - observer).at(t).radec()
37
+
38
+ return np.array([radec[0]._degrees, radec[1].degrees]).T
39
+
40
+
41
+ def main():
42
+
43
+ colors = list(mcolors.TABLEAU_COLORS.values())
44
+ n_c = len(colors)
45
+
46
+ parser = argparse.ArgumentParser(
47
+ description="Extract satellite paths to ds9 region file."
48
+ )
49
+ parser.add_argument(
50
+ "-m",
51
+ "--ms_path",
52
+ required=True,
53
+ help="File path to the tabascal zarr simulation file.",
54
+ )
55
+ parser.add_argument(
56
+ "-d",
57
+ "--max_d",
58
+ default=90.0,
59
+ type=float,
60
+ help="Maximum angular distance from phase centre (in degrees) of the satellite path to include.",
61
+ )
62
+ parser.add_argument(
63
+ "-st",
64
+ "--spacetrack",
65
+ help="Path to YAML config file containing Space-Track login details with 'username' and 'password'.",
66
+ )
67
+ parser.add_argument(
68
+ "-ni", "--norad_ids", help="NORAD IDs of satellites to include."
69
+ )
70
+ parser.add_argument(
71
+ "-np",
72
+ "--norad_path",
73
+ help="Path to YAML config file containing list of norad_ids.",
74
+ )
75
+ parser.add_argument(
76
+ "-td", "--tle_dir", default="./tles", help="Path to directory containing TLEs."
77
+ )
78
+ parser.add_argument(
79
+ "-s",
80
+ "--sim",
81
+ default=False,
82
+ action=argparse.BooleanOptionalAction,
83
+ help="Whether to use simulation coordinates. Only small difference from default skyfield coordinates.",
84
+ )
85
+
86
+ args = parser.parse_args()
87
+ ms_path = args.ms_path
88
+ max_d = args.max_d
89
+ spacetrack = yaml_load(args.spacetrack)
90
+ tle_dir = args.tle_dir
91
+ norad_ids = []
92
+ if args.norad_ids:
93
+ norad_ids += [int(x) for x in np.atleast_1d(args.norad_ids.split(","))]
94
+ if args.norad_path:
95
+ norad_ids += [
96
+ int(x) for x in np.atleast_1d(str(yaml_load(args.norad_path)).split())
97
+ ]
98
+
99
+ os.makedirs(tle_dir, exist_ok=True)
100
+
101
+ if ms_path[-1] == "/":
102
+ ms_path = ms_path[:-1]
103
+
104
+ region_path = os.path.join(os.path.split(ms_path)[0], "satelitte_paths.reg")
105
+
106
+ def xyz_to_radec(xyz):
107
+ if xyz.ndim == 2:
108
+ xyz = xyz[None, :, :]
109
+
110
+ xyz = xyz / np.linalg.norm(xyz, axis=-1, keepdims=True)
111
+ radec = np.zeros((*xyz.shape[:2], 2))
112
+ radec[:, :, 0] = np.arctan2(xyz[:, :, 1], xyz[:, :, 0])
113
+ radec[:, :, 1] = np.arcsin(xyz[:, :, 2])
114
+
115
+ return np.rad2deg(radec)
116
+
117
+ xds = xds_from_ms(ms_path)[0]
118
+ times_mjd = xds.TIME.data.compute()
119
+ times_jd = mjd_to_jd(np.linspace(np.min(times_mjd), np.max(times_mjd), 10))
120
+ epoch_jd = mjd_to_jd(np.mean(times_mjd))
121
+
122
+ tles_df = get_tles_by_id(
123
+ spacetrack["username"],
124
+ spacetrack["password"],
125
+ norad_ids,
126
+ epoch_jd,
127
+ tle_dir=tle_dir,
128
+ )
129
+ if len(tles_df) == 0:
130
+ raise ValueError("No TLEs found.")
131
+
132
+ tles = np.atleast_2d(tles_df[["TLE_LINE1", "TLE_LINE2"]].values)
133
+ ids = np.atleast_1d(tles_df["NORAD_CAT_ID"].values)
134
+ n_tles = len(tles)
135
+
136
+ print(f"Found {n_tles} matching TLEs.")
137
+
138
+ if n_tles > 0:
139
+
140
+ xds_src = xds_from_table(ms_path + "::SOURCE")[0]
141
+ ra, dec = np.rad2deg(xds_src.DIRECTION.data[0].compute())
142
+
143
+ xds_ants = xds_from_table(ms_path + "::ANTENNA")[0]
144
+ ants_itrf = np.mean(xds_ants.POSITION.data.compute(), axis=0, keepdims=True)
145
+
146
+ if args.sim:
147
+ # ants_xyz = itrf_to_xyz(ants_itrf, gmsa_from_jd(times_jd))[:, 0]
148
+ ants_xyz = itrf_to_xyz(
149
+ ants_itrf,
150
+ Time(times_jd, format="jd").sidereal_time("mean", "greenwich").hour
151
+ * 15,
152
+ )[:, 0]
153
+ rfi_xyz = get_satellite_positions(tles, times_jd)
154
+ xyz = rfi_xyz - ants_xyz[None, :, :]
155
+ radec = xyz_to_radec(xyz)
156
+ else:
157
+ radec = np.array([sat_radec(tle, times_jd, ants_itrf[0]) for tle in tles])
158
+
159
+ c0 = SkyCoord(ra, dec, unit="deg", frame="fk5")
160
+ c = SkyCoord(radec[:, :, 0], radec[:, :, 1], unit="deg", frame="fk5")
161
+
162
+ min_sep = c0.separation(c).min(axis=1).deg
163
+ print(
164
+ f"Minimum angular separation from target : {[round(x, 1) for x in min_sep]} deg."
165
+ )
166
+ print(
167
+ f"Only including satellites within {max_d:.1f} degrees of pointing direction."
168
+ )
169
+ idx = np.where(min_sep < max_d)[0]
170
+
171
+ min_idx = np.argmin(c0.separation(c)[:, :-1], axis=1)
172
+
173
+ ang_v = np.diff(radec, axis=1)
174
+ ang_theta = np.arctan2(ang_v[:, :, 1], ang_v[:, :, 0])
175
+
176
+ with open(region_path, "w") as fp:
177
+ for c_i, i in enumerate(idx):
178
+ # fp.write(RectangleSkyRegion(c0, 0.05*u.deg, 0.3*u.deg, ang_theta[i,min_idx[i]]*u.rad, visual={"color": colors[c_i%n_c]}).serialize(format="ctrf"))
179
+ # fp.write(TextSkyRegion(c[i,-1], str(ids[i]), visual={"fontsize": 16, "color": colors[c_i%n_c]}).serialize(format="ds9"))
180
+ for j in range(len(times_jd)):
181
+ fp.write(
182
+ CircleSkyRegion(
183
+ c[i, j],
184
+ radius=0.1 * u.deg,
185
+ visual={"color": colors[c_i % n_c]},
186
+ ).serialize(format="ds9")
187
+ )
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
tabsim/sky.py ADDED
@@ -0,0 +1,137 @@
1
+ import numpy as np
2
+ import jax.numpy as jnp
3
+ import dask.array as da
4
+
5
+
6
+ def uniform_points_disk(radius: float, n_src: int, random_seed: int | None = None):
7
+ """
8
+ Generate uniformly distributed random points on a disk.
9
+
10
+ Parameters:
11
+ -----------
12
+ radius: float
13
+ Radius of the disk.
14
+ n_src: int
15
+ The number of sources/points to generate.
16
+ seed: int
17
+ Random number generator seed/key.
18
+
19
+ Returns:
20
+ --------
21
+ points: array_like (2, n_src)
22
+ The coordinate positions of the random points centred on (0,0).
23
+ """
24
+ rng = np.random.default_rng(random_seed)
25
+ r = jnp.sqrt(rng.uniform(low=0.0, high=radius**2, size=(n_src,)))
26
+ theta = rng.uniform(low=0.0, high=2.0 * jnp.pi, size=(n_src,))
27
+
28
+ return r * jnp.array([jnp.cos(theta), jnp.sin(theta)])
29
+
30
+
31
+ def random_power_law(
32
+ n_src: int,
33
+ I_min: float = 1e-4,
34
+ I_max: float = 1.0,
35
+ alpha: float = 1.6,
36
+ random_seed: int = None,
37
+ ):
38
+ """Generate a random power law distribution of source fluxes with minimum source
39
+ flux defined by `I0`.
40
+
41
+ Parameters:
42
+ -----------
43
+ n_src: int
44
+ Number of source fluxes to draw.
45
+ I0: float
46
+ Minimum source flux.
47
+ alpha: float
48
+ Power law index.
49
+
50
+ Returns:
51
+ --------
52
+ I: array_like (n_src,)
53
+ Array of source fluxes."""
54
+
55
+ def inv_cdf(x):
56
+ return I_min * (1.0 - x) ** (1.0 / (1.0 - alpha))
57
+
58
+ rng = np.random.default_rng(random_seed)
59
+ rand_unif = rng.uniform(size=(n_src,))
60
+ I = np.array(inv_cdf(rand_unif))
61
+ while np.any(I > I_max):
62
+ idx = np.where(I > I_max)[0]
63
+ rand_unif = rng.uniform(size=(idx.shape[0],))
64
+ I[idx] = inv_cdf(rand_unif)
65
+
66
+ return I
67
+
68
+
69
+ def generate_random_sky(
70
+ n_src: int,
71
+ freqs: jnp.ndarray,
72
+ min_I: float = 1e-4,
73
+ max_I: float = 1.0,
74
+ I_power_law: float = 1.6,
75
+ spec_idx_mean: float = 0.7,
76
+ spec_idx_std: float = 0.2,
77
+ fov: float = 1.0,
78
+ beam_width: float = 0.0,
79
+ random_seed: int = None,
80
+ n_beam: int = 3,
81
+ ) -> tuple:
82
+ """
83
+ Generate uniformly distributed point sources inside the field of view with
84
+ a power law intensity distribution. Setting the beam width will make
85
+ sure souces are separated by 5 beam widths apart.
86
+
87
+ Parameters:
88
+ -----------
89
+ n_src: int
90
+ Number of sources to generate.
91
+ mean_I: float
92
+ Mean intensity of the sources.
93
+ freqs: array_like (n_freq,)
94
+ Frequencies to generate the sources at.
95
+ spec_idx_mean: float
96
+ Mean spectral index of the sources.
97
+ spec_idx_std: float
98
+ Standard deviation of the spectral index of the sources.
99
+ fov: float
100
+ Field of view to generate positions within. Same units as beam_width.
101
+ beam_width: float
102
+ Width of the resolving beam to ensure sources do not overlap. Sources will be
103
+ separated by >5*`beam_width`. Same units as fov.
104
+ random_seed: int
105
+ Random number generator seed/key.
106
+ n_beam: int
107
+ Number of beam_widths apart sources should be separated.
108
+
109
+ Returns:
110
+ --------
111
+ I: array_like (n_src, n_freq)
112
+ The sources intensities.
113
+ delta_ra: array_like
114
+ The sources right ascensions relative to (0,0).
115
+ delta_dec: array_like
116
+ The sources declinations relative to (0,0).
117
+ """
118
+ rng = np.random.default_rng(random_seed)
119
+
120
+ I = da.atleast_1d(random_power_law(n_src, min_I, max_I, I_power_law, rng))
121
+ positions = uniform_points_disk(fov / 2.0, 1, rng)
122
+ while positions.shape[1] < n_src:
123
+ n_sample = 2 * (n_src - positions.shape[1])
124
+ new_positions = uniform_points_disk(fov / 2.0, n_sample, rng)
125
+ positions = np.concatenate([positions, new_positions], axis=1)
126
+ s1, s2 = np.triu_indices(positions.shape[1], 1)
127
+ d = np.linalg.norm(positions[:, s1] - positions[:, s2], axis=0)
128
+ idx = np.where(d < n_beam * beam_width)[0]
129
+ remove_source_idx = np.unique(jnp.concatenate([s1[idx], s2[idx]]))
130
+ positions = np.delete(positions, remove_source_idx, axis=1)
131
+
132
+ d_ra, d_dec = positions[:, :n_src]
133
+
134
+ spectral_indices = rng.normal(loc=spec_idx_mean, scale=spec_idx_std, size=(n_src,))
135
+ I = I[:, None] * ((freqs[None, :] / freqs[0]) ** -spectral_indices[:, None])
136
+
137
+ return I, d_ra, d_dec