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/__init__.py +0 -0
- tabsim/config.py +904 -0
- tabsim/dask/__init__.py +0 -0
- tabsim/dask/coordinates.py +469 -0
- tabsim/dask/extras.py +60 -0
- tabsim/dask/interferometry.py +572 -0
- tabsim/dask/observation.py +969 -0
- tabsim/data/EDA2.itrf.txt +256 -0
- tabsim/data/MWA.itrf.txt +128 -0
- tabsim/data/MeerKAT.itrf.txt +64 -0
- tabsim/data/__init__.py +0 -0
- tabsim/data/aux_data/calibrator.skymodel +2 -0
- tabsim/data/aux_data/norad_satellite.rfimodel +61711 -0
- tabsim/data/aux_data/satellite.ole +3 -0
- tabsim/data/aux_data/satellite.rfimodel +3 -0
- tabsim/data/aux_data/stationary.loc +4 -0
- tabsim/data/aux_data/stationary.rfimodel +4 -0
- tabsim/data/config_files/sim_config_base.yaml +172 -0
- tabsim/data/config_files/tab_config_base.yaml +86 -0
- tabsim/jax/__init__.py +0 -0
- tabsim/jax/coordinates.py +1082 -0
- tabsim/jax/interferometry.py +667 -0
- tabsim/jax/utils.py +90 -0
- tabsim/plot.py +323 -0
- tabsim/scripts/__init__.py +1 -0
- tabsim/scripts/ast_region.py +56 -0
- tabsim/scripts/flag_data.py +195 -0
- tabsim/scripts/sim_vis.py +136 -0
- tabsim/scripts/tle_sat_region.py +191 -0
- tabsim/sky.py +137 -0
- tabsim/tle.py +753 -0
- tabsim/tools.py +111 -0
- tabsim/write.py +715 -0
- tabsim-0.0.1.dist-info/METADATA +991 -0
- tabsim-0.0.1.dist-info/RECORD +39 -0
- tabsim-0.0.1.dist-info/WHEEL +5 -0
- tabsim-0.0.1.dist-info/entry_points.txt +5 -0
- tabsim-0.0.1.dist-info/licenses/LICENSE +674 -0
- tabsim-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|