ps-eor 1.0__tar.gz → 1.2__tar.gz
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.
- {ps_eor-1.0 → ps_eor-1.2}/CHANGELOG.md +40 -1
- {ps_eor-1.0 → ps_eor-1.2}/PKG-INFO +2 -2
- ps_eor-1.2/ps_eor/data/instruments/ska_low_aavs2_sefd.csv +36 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/datacube.py +136 -54
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/covariance.py +4 -3
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/fitter.py +12 -6
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/obssimu.py +69 -21
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/simu.py +21 -15
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/tools/pstool.py +20 -6
- {ps_eor-1.0 → ps_eor-1.2}/pyproject.toml +1 -1
- {ps_eor-1.0 → ps_eor-1.2}/.gitignore +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/LICENSE +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/README.md +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/__init__.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/config/default_ml_gpr_settings.toml +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/aartfaac_a12_hba_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/aartfaac_a12_lba_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/lofar_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/mwa_rev1_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/nenufar80_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/nenufar_full_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/ovro-lwa_enu_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/ska1_low_aa2_enu_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/ska1_low_aastar_enu_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/data/instruments/ska1_low_enu_statpos.data +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/fgfit.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/filtering.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/fitutil.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/flagger.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/__init__.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/config.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/kernels.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/multidata.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/params.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/priors.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/regressor.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/samplers.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/ml_gpr/vae.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/pscart.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/pspec.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/pssph.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/psutil.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/sphcube.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/sphutil.py +0 -0
- {ps_eor-1.0 → ps_eor-1.2}/ps_eor/tools/__init__.py +0 -0
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
# ChangeLog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.2 - 2026-10-26
|
|
4
|
+
|
|
5
|
+
- [Changed] `SkaLow.get_sefd` now interpolates the AAVS2-based SKAO station-sensitivity lookup
|
|
6
|
+
(zenith, cold-sky LST 0-8h) instead of the pre-hardware SKA1-LFAA design-target specification.
|
|
7
|
+
Effective XX and YY SEFD values are stored separately, interpolated with independent cubic
|
|
8
|
+
splines, then combined using the SKAO Stokes-I convention. Values shift, especially above
|
|
9
|
+
220 MHz where the old table was flat-extrapolated instead of following the lookup roll-off.
|
|
10
|
+
`tsys_sky` is no longer a parameter because the lookup already fixes the sky condition
|
|
11
|
+
|
|
12
|
+
## 1.1 - 2026-09-11
|
|
13
|
+
|
|
14
|
+
- [Fixed] Fix a thermal-noise normalization regression introduced in 1.0 and limited to the
|
|
15
|
+
convenience default `SimuGridded.get_noise_std_cube(sefd=None)`. That path passed the effective
|
|
16
|
+
Stokes-I SEFD into equations that already include the two-polarization average, applying the
|
|
17
|
+
sensitivity gain twice. It also affected `simu_noise_ps_zrange`, which relied on this default.
|
|
18
|
+
Corrected results from these two paths have sqrt(2) higher visibility-noise amplitude and twice
|
|
19
|
+
the noise power. Existing low-level `datacube` calculations supplied with `get_sefd`, ML-GPR,
|
|
20
|
+
`simu_noise_img`, `simu_noise_ps`, and the power-spectrum estimators retain their previous
|
|
21
|
+
numerical normalization. The polarization average is now explicit through `n_pol_averaged`;
|
|
22
|
+
the deprecated `get_i_sefd` helper is no longer used internally
|
|
23
|
+
- [Fixed] `DEx` declared a primary beam name that resolved to nothing, so `SimuGridded.get_ps_gen`
|
|
24
|
+
raised for it. `pstool simu_noise_ps`, `pstool simu_noise_ps_zrange` and `--fov pb_fwhm` were
|
|
25
|
+
unusable with that telescope, including the example in `obssimu`'s module docstring. Its beam now
|
|
26
|
+
follows the configured antenna separation, the same aperture its SEFD already uses
|
|
27
|
+
- [Fixed] `pstool simu_noise_ps_zrange` built its redshift grid with
|
|
28
|
+
`arange(z_min, z_max + delta_z, delta_z)`, which steps one past `z_max` unless the span is an
|
|
29
|
+
exact multiple of `delta_z`. That slice selects no channel and the run died on an empty reduction
|
|
30
|
+
- [New] `get_samples` and every `SimuMultiGPCube` draw method take an `rng` argument -- a seed, a
|
|
31
|
+
`Generator`, or None -- so simulated cubes are reproducible. This was the one path the 1.0 move to
|
|
32
|
+
`numpy.random.Generator` missed. A seed is resolved once per call, so the draws within it advance
|
|
33
|
+
rather than repeat
|
|
34
|
+
- [Changed] Run the CI pipeline on tags
|
|
35
|
+
- [Changed] `test_independent_epochs_uncorrelated` asserted on one epoch pair's correlation, which
|
|
36
|
+
has std 0.09 under independence and reaches 0.31, so it failed about one run in forty. It now
|
|
37
|
+
averages the 28 pairs of an 8-epoch seeded draw, where the same statistic stays under 0.061 and a
|
|
38
|
+
20% shared variance fraction reads 0.197
|
|
39
|
+
- [Changed] Assert the sampler convergence warnings the test suite provokes rather than leaving them
|
|
40
|
+
unread in the run summary
|
|
41
|
+
- [Changed] Cover `pstool` and the VAE, and add numerical regressions for the power-spectrum
|
|
42
|
+
estimators, taking the package from 64% to 70%
|
|
4
43
|
|
|
5
44
|
## 1.0 - 2026-08-03
|
|
6
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ps_eor
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2
|
|
4
4
|
Summary: Power Spectra generation for 21-cm experiments and Foreground modeling/removal
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/flomertens/ps_eor
|
|
6
6
|
Project-URL: Documentation, https://ps-eor.readthedocs.io
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
frequency_mhz,sefd_xx_jy,sefd_yy_jy
|
|
2
|
+
49.92,12691.708008,12706.784180
|
|
3
|
+
58.88,6430.699219,6456.514648
|
|
4
|
+
69.12,4400.614258,4429.739258
|
|
5
|
+
79.36,3287.111084,3280.634033
|
|
6
|
+
88.32,2657.459473,2628.958252
|
|
7
|
+
89.60,2591.589600,2559.727051
|
|
8
|
+
99.84,2175.688477,2124.371094
|
|
9
|
+
108.80,1984.341553,1935.180176
|
|
10
|
+
119.04,1879.441406,1837.543213
|
|
11
|
+
129.28,1817.179443,1761.833984
|
|
12
|
+
139.52,1726.300049,1702.780762
|
|
13
|
+
149.76,1720.707642,1640.811279
|
|
14
|
+
154.88,1770.666016,1661.329468
|
|
15
|
+
160.00,1881.431885,1749.433594
|
|
16
|
+
168.96,1985.992188,1876.033081
|
|
17
|
+
179.20,2034.554932,1939.044189
|
|
18
|
+
185.60,2016.972046,1949.946777
|
|
19
|
+
189.44,1996.074097,1918.264404
|
|
20
|
+
199.68,1980.122925,1835.012085
|
|
21
|
+
209.92,1960.942261,1874.337402
|
|
22
|
+
216.32,2126.444092,1988.125122
|
|
23
|
+
218.88,2162.237061,2007.597900
|
|
24
|
+
229.12,2321.589111,2117.384033
|
|
25
|
+
239.36,2255.394531,2111.037354
|
|
26
|
+
249.60,2109.016846,2050.640625
|
|
27
|
+
259.84,2191.391846,2023.246094
|
|
28
|
+
268.80,2117.823975,1980.030396
|
|
29
|
+
279.04,2129.421387,2049.493408
|
|
30
|
+
289.28,2223.825195,2104.812500
|
|
31
|
+
299.52,2374.633545,2311.893066
|
|
32
|
+
309.76,2340.869141,2267.793945
|
|
33
|
+
320.00,2227.362793,2119.056885
|
|
34
|
+
328.96,2285.774170,2193.820068
|
|
35
|
+
339.20,2279.281982,2268.093018
|
|
36
|
+
349.44,2503.468262,2453.347900
|
|
@@ -66,6 +66,14 @@ def _fmhz(freqs, precision=3):
|
|
|
66
66
|
return np.round(freqs * 1e-6, precision)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _validate_n_pol_averaged(n_pol_averaged):
|
|
70
|
+
"""Validate the effective number of independent polarization products."""
|
|
71
|
+
if (not np.isscalar(n_pol_averaged)
|
|
72
|
+
or not np.isfinite(n_pol_averaged)
|
|
73
|
+
or n_pol_averaged <= 0):
|
|
74
|
+
raise ValueError('n_pol_averaged must be a finite, strictly positive scalar')
|
|
75
|
+
|
|
76
|
+
|
|
69
77
|
def get_common_idx(cube1, cube2):
|
|
70
78
|
"""Return frequency and spatial-mode masks shared by two cubes."""
|
|
71
79
|
freqs = np.intersect1d(_fmhz(cube1.freqs), _fmhz(cube2.freqs))
|
|
@@ -1499,40 +1507,62 @@ class CartDataCube(DataCube):
|
|
|
1499
1507
|
|
|
1500
1508
|
return cube
|
|
1501
1509
|
|
|
1502
|
-
def estimate_sefd(self, sefd_jansky=True):
|
|
1503
|
-
"""Overall
|
|
1510
|
+
def estimate_sefd(self, sefd_jansky=True, n_pol_averaged=2):
|
|
1511
|
+
"""Overall single-polarization SEFD from this cube and its weights.
|
|
1512
|
+
|
|
1513
|
+
Args:
|
|
1514
|
+
sefd_jansky (bool): return Jy (else Kelvin).
|
|
1515
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
1516
|
+
into the visibility cube (one for XX/YY, two for Stokes I).
|
|
1504
1517
|
|
|
1505
1518
|
Returns:
|
|
1506
1519
|
float: the SEFD, in Jy (``sefd_jansky=True``) or Kelvin.
|
|
1507
1520
|
"""
|
|
1508
1521
|
assert self.weights is not None
|
|
1509
1522
|
|
|
1510
|
-
return self.weights.estimate_sefd(
|
|
1523
|
+
return self.weights.estimate_sefd(
|
|
1524
|
+
self, sefd_jansky=sefd_jansky,
|
|
1525
|
+
n_pol_averaged=n_pol_averaged,
|
|
1526
|
+
)
|
|
1511
1527
|
|
|
1512
|
-
def estimate_uv_sefd(self, sefd_jansky=True):
|
|
1528
|
+
def estimate_uv_sefd(self, sefd_jansky=True, n_pol_averaged=2):
|
|
1513
1529
|
"""SEFD per uv sample (see :meth:`estimate_sefd` for units).
|
|
1514
1530
|
|
|
1531
|
+
Args:
|
|
1532
|
+
sefd_jansky (bool): return Jy (else Kelvin).
|
|
1533
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
1534
|
+
into the visibility cube.
|
|
1535
|
+
|
|
1515
1536
|
Returns:
|
|
1516
1537
|
CartDataCube: the per-mode SEFD.
|
|
1517
1538
|
"""
|
|
1518
1539
|
assert self.weights is not None
|
|
1519
1540
|
|
|
1520
|
-
return self.weights.estimate_uv_sefd(
|
|
1541
|
+
return self.weights.estimate_uv_sefd(
|
|
1542
|
+
self, sefd_jansky=sefd_jansky,
|
|
1543
|
+
n_pol_averaged=n_pol_averaged,
|
|
1544
|
+
)
|
|
1521
1545
|
|
|
1522
|
-
def estimate_freqs_sefd(self, sefd_jansky=True, sefd_poly_fit_deg=0
|
|
1546
|
+
def estimate_freqs_sefd(self, sefd_jansky=True, sefd_poly_fit_deg=0,
|
|
1547
|
+
n_pol_averaged=2):
|
|
1523
1548
|
"""SEFD per frequency channel (see :meth:`estimate_sefd` for units).
|
|
1524
1549
|
|
|
1525
1550
|
Args:
|
|
1526
1551
|
sefd_jansky (bool): return Jy (else Kelvin).
|
|
1527
1552
|
sefd_poly_fit_deg (int): if > 0, smooth the SEFD with a polynomial
|
|
1528
1553
|
of this degree in log-log space.
|
|
1554
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
1555
|
+
into the visibility cube.
|
|
1529
1556
|
|
|
1530
1557
|
Returns:
|
|
1531
1558
|
ndarray: the SEFD per channel, shape ``(n_freqs,)``.
|
|
1532
1559
|
"""
|
|
1533
1560
|
assert self.weights is not None
|
|
1534
1561
|
|
|
1535
|
-
sefd = self.weights.estimate_freqs_sefd(
|
|
1562
|
+
sefd = self.weights.estimate_freqs_sefd(
|
|
1563
|
+
self, sefd_jansky=sefd_jansky,
|
|
1564
|
+
n_pol_averaged=n_pol_averaged,
|
|
1565
|
+
)
|
|
1536
1566
|
|
|
1537
1567
|
if sefd_poly_fit_deg > 0:
|
|
1538
1568
|
sefd_fct = np.poly1d(np.polyfit(np.log(self.freqs), np.log(sefd), sefd_poly_fit_deg))
|
|
@@ -1712,14 +1742,22 @@ class CartDataCube(DataCube):
|
|
|
1712
1742
|
print(f'Filtering {n_filt} modes ({n_filt / n_tot * 100:.2f} %)')
|
|
1713
1743
|
self.filter_uv_from_index(idx_uv)
|
|
1714
1744
|
|
|
1715
|
-
def filter_sefd_uv(self, max_sefd, min_sefd=0):
|
|
1745
|
+
def filter_sefd_uv(self, max_sefd, min_sefd=0, n_pol_averaged=2):
|
|
1716
1746
|
"""Keep, in place, only uv modes whose mean estimated SEFD is within
|
|
1717
1747
|
``[min_sefd, max_sefd]``.
|
|
1718
1748
|
|
|
1749
|
+
Args:
|
|
1750
|
+
max_sefd: maximum retained single-polarization SEFD.
|
|
1751
|
+
min_sefd: minimum retained single-polarization SEFD.
|
|
1752
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
1753
|
+
into the visibility cube.
|
|
1754
|
+
|
|
1719
1755
|
Returns:
|
|
1720
1756
|
ndarray: boolean mask of the kept modes.
|
|
1721
1757
|
"""
|
|
1722
|
-
sefd = self.estimate_uv_sefd(
|
|
1758
|
+
sefd = self.estimate_uv_sefd(
|
|
1759
|
+
n_pol_averaged=n_pol_averaged,
|
|
1760
|
+
).data.mean(axis=0)
|
|
1723
1761
|
idx_uv = (sefd >= min_sefd) & (sefd <= max_sefd)
|
|
1724
1762
|
n_filt = np.sum(~idx_uv)
|
|
1725
1763
|
n_tot = float(len(self.uu))
|
|
@@ -2390,24 +2428,28 @@ class CartWeightCube(CartDataCube):
|
|
|
2390
2428
|
return CartWeightCube(1 / noise_scale ** 2, noise_cube.uu, noise_cube.vv,
|
|
2391
2429
|
noise_cube.freqs, meta)
|
|
2392
2430
|
|
|
2393
|
-
def estimate_sefd(self, noise_cube, sefd_jansky=True, axis=None
|
|
2394
|
-
|
|
2431
|
+
def estimate_sefd(self, noise_cube, sefd_jansky=True, axis=None,
|
|
2432
|
+
n_pol_averaged=2):
|
|
2433
|
+
"""Single-polarization SEFD estimated from a noise realization.
|
|
2395
2434
|
|
|
2396
2435
|
Args:
|
|
2397
2436
|
noise_cube: a noise :class:`CartDataCube` on the same geometry.
|
|
2398
2437
|
sefd_jansky (bool): return Jy (else Kelvin).
|
|
2399
2438
|
axis: reduce over this axis (0 -> per uv mode, 1 -> per channel,
|
|
2400
2439
|
None -> a single overall value).
|
|
2440
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2441
|
+
into ``noise_cube`` (one for XX/YY, two for Stokes I).
|
|
2401
2442
|
|
|
2402
2443
|
Returns:
|
|
2403
2444
|
The SEFD (MAD-averaged over ``axis``); scalar or array per ``axis``.
|
|
2404
2445
|
"""
|
|
2446
|
+
_validate_n_pol_averaged(n_pol_averaged)
|
|
2405
2447
|
df = self.meta.chan_width
|
|
2406
2448
|
int_time = self.meta.int_time
|
|
2407
2449
|
|
|
2408
2450
|
w = abs(self.get_slice(noise_cube.freqs[0], noise_cube.freqs[-1]).data)
|
|
2409
2451
|
w[w == 0] = np.nan
|
|
2410
|
-
sefd = noise_cube.data * (
|
|
2452
|
+
sefd = noise_cube.data * (n_pol_averaged * df * int_time) ** 0.5 * w ** 0.5
|
|
2411
2453
|
|
|
2412
2454
|
# Kelvin noise is normalized to the image field of view.
|
|
2413
2455
|
if sefd_jansky:
|
|
@@ -2421,27 +2463,45 @@ class CartWeightCube(CartDataCube):
|
|
|
2421
2463
|
|
|
2422
2464
|
return psutil.mad(sefd, axis=axis)
|
|
2423
2465
|
|
|
2424
|
-
def estimate_uv_sefd(self, noise_cube, sefd_jansky=True):
|
|
2466
|
+
def estimate_uv_sefd(self, noise_cube, sefd_jansky=True, n_pol_averaged=2):
|
|
2425
2467
|
"""SEFD per uv sample (see :meth:`estimate_sefd`).
|
|
2426
2468
|
|
|
2469
|
+
Args:
|
|
2470
|
+
noise_cube: a noise :class:`CartDataCube` on the same geometry.
|
|
2471
|
+
sefd_jansky (bool): return Jy (else Kelvin).
|
|
2472
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2473
|
+
into ``noise_cube``.
|
|
2474
|
+
|
|
2427
2475
|
Returns:
|
|
2428
2476
|
CartDataCube: the per-mode SEFD (single averaged channel).
|
|
2429
2477
|
"""
|
|
2430
|
-
sefd = self.estimate_sefd(
|
|
2478
|
+
sefd = self.estimate_sefd(
|
|
2479
|
+
noise_cube, sefd_jansky=sefd_jansky, axis=0,
|
|
2480
|
+
n_pol_averaged=n_pol_averaged,
|
|
2481
|
+
)
|
|
2431
2482
|
|
|
2432
2483
|
return CartDataCube(sefd[None, :], self.uu, self.vv, np.array([self.freqs.mean()]),
|
|
2433
2484
|
self.meta.copy())
|
|
2434
2485
|
|
|
2435
|
-
def estimate_freqs_sefd(self, noise_cube, sefd_jansky=True):
|
|
2486
|
+
def estimate_freqs_sefd(self, noise_cube, sefd_jansky=True, n_pol_averaged=2):
|
|
2436
2487
|
"""SEFD per frequency channel (see :meth:`estimate_sefd`).
|
|
2437
2488
|
|
|
2489
|
+
Args:
|
|
2490
|
+
noise_cube: a noise :class:`CartDataCube` on the same geometry.
|
|
2491
|
+
sefd_jansky (bool): return Jy (else Kelvin).
|
|
2492
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2493
|
+
into ``noise_cube``.
|
|
2494
|
+
|
|
2438
2495
|
Returns:
|
|
2439
2496
|
ndarray: the SEFD per channel, shape ``(n_freqs,)``.
|
|
2440
2497
|
"""
|
|
2441
|
-
return self.estimate_sefd(
|
|
2498
|
+
return self.estimate_sefd(
|
|
2499
|
+
noise_cube, sefd_jansky=sefd_jansky, axis=1,
|
|
2500
|
+
n_pol_averaged=n_pol_averaged,
|
|
2501
|
+
)
|
|
2442
2502
|
|
|
2443
2503
|
def scale_with_noise_cube(self, noise_cube, sefd_poly_fit_deg=0, sefd_filter_n_bins=0,
|
|
2444
|
-
expected_sefd=None, scale_freqs=False):
|
|
2504
|
+
expected_sefd=None, scale_freqs=False, n_pol_averaged=2):
|
|
2445
2505
|
"""Set ``uv_scale`` (in place) so the weights reflect the SEFD measured
|
|
2446
2506
|
from ``noise_cube`` -- down-weighting noisier uv modes.
|
|
2447
2507
|
|
|
@@ -2454,8 +2514,12 @@ class CartWeightCube(CartDataCube):
|
|
|
2454
2514
|
expected_sefd: reference SEFD the scale normalises to (default: the
|
|
2455
2515
|
median measured SEFD).
|
|
2456
2516
|
scale_freqs (bool): also apply a per-channel SEFD scaling.
|
|
2517
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2518
|
+
into ``noise_cube``.
|
|
2457
2519
|
"""
|
|
2458
|
-
sefd_uv = self.estimate_uv_sefd(
|
|
2520
|
+
sefd_uv = self.estimate_uv_sefd(
|
|
2521
|
+
noise_cube, n_pol_averaged=n_pol_averaged,
|
|
2522
|
+
).data
|
|
2459
2523
|
if expected_sefd is None:
|
|
2460
2524
|
expected_sefd = np.nanmedian(sefd_uv)
|
|
2461
2525
|
|
|
@@ -2476,7 +2540,9 @@ class CartWeightCube(CartDataCube):
|
|
|
2476
2540
|
fill_value='extrapolate')(self.ru)
|
|
2477
2541
|
|
|
2478
2542
|
if scale_freqs:
|
|
2479
|
-
sefd_freqs = self.estimate_freqs_sefd(
|
|
2543
|
+
sefd_freqs = self.estimate_freqs_sefd(
|
|
2544
|
+
noise_cube, n_pol_averaged=n_pol_averaged,
|
|
2545
|
+
)
|
|
2480
2546
|
self.uv_scale = self.uv_scale * (np.nanmedian(sefd_freqs) / sefd_freqs)[:, None] ** 2
|
|
2481
2547
|
|
|
2482
2548
|
self.uv_scale[~np.isfinite(self.uv_scale)] = 1
|
|
@@ -2499,43 +2565,63 @@ class CartWeightCube(CartDataCube):
|
|
|
2499
2565
|
"""Reset ``uv_scale`` to one."""
|
|
2500
2566
|
self.uv_scale = np.ones((1, len(self.uu)))
|
|
2501
2567
|
|
|
2568
|
+
def _noise_complex_std_per_unit_weight(self, sefd, time, n_pol_averaged=2,
|
|
2569
|
+
sefd_jansky=True, fake_apply_win_fct=False):
|
|
2570
|
+
"""Return the full complex RMS before division by UV-cell weights."""
|
|
2571
|
+
_validate_n_pol_averaged(n_pol_averaged)
|
|
2572
|
+
|
|
2573
|
+
df = self.meta.chan_width
|
|
2574
|
+
int_time = self.meta.int_time
|
|
2575
|
+
total_time = self.meta.total_time
|
|
2576
|
+
f = time / total_time
|
|
2577
|
+
|
|
2578
|
+
noise_complex_std = np.atleast_1d(
|
|
2579
|
+
sefd / (n_pol_averaged * df * int_time * f) ** 0.5,
|
|
2580
|
+
)[:, None]
|
|
2581
|
+
|
|
2582
|
+
if fake_apply_win_fct:
|
|
2583
|
+
# Scale as if a spatial tapering had been applied.
|
|
2584
|
+
noise_complex_std = noise_complex_std * self.meta.win_fct_power ** .5
|
|
2585
|
+
|
|
2586
|
+
if sefd_jansky:
|
|
2587
|
+
lamb = const.c.value / self.freqs
|
|
2588
|
+
fov = (self.meta.shape[0] * self.meta.res) ** 2
|
|
2589
|
+
jy2k = ((1e-26 * lamb ** 2) / (2 * const.k_B.value)) / fov
|
|
2590
|
+
noise_complex_std = jy2k[:, None] * noise_complex_std
|
|
2591
|
+
|
|
2592
|
+
return noise_complex_std
|
|
2593
|
+
|
|
2502
2594
|
def simulate_noise(self, sefd, time, hermitian=True, weights_uncertainity_ratio=None,
|
|
2503
|
-
sefd_jansky=True, fake_apply_win_fct=False, rng=None
|
|
2595
|
+
sefd_jansky=True, fake_apply_win_fct=False, rng=None,
|
|
2596
|
+
n_pol_averaged=2):
|
|
2504
2597
|
"""Draw a thermal-noise visibility cube for a given SEFD and integration.
|
|
2505
2598
|
|
|
2506
2599
|
Noise per mode follows the radiometer equation, scaled by these weights.
|
|
2507
2600
|
|
|
2508
2601
|
Args:
|
|
2509
|
-
sefd:
|
|
2602
|
+
sefd: single-polarization station SEFD (Jy if ``sefd_jansky``,
|
|
2603
|
+
else K).
|
|
2510
2604
|
time: total integration time, in seconds.
|
|
2511
2605
|
hermitian (bool): make the realization Hermitian (real image).
|
|
2512
2606
|
weights_uncertainity_ratio: if set, also perturb the weights randomly
|
|
2513
2607
|
by up to this ratio (see :meth:`random_scale`).
|
|
2514
2608
|
sefd_jansky (bool): interpret ``sefd`` in Jy (else Kelvin).
|
|
2515
2609
|
fake_apply_win_fct (bool): scale as if a spatial taper were applied.
|
|
2610
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2611
|
+
into the simulated visibilities (one for XX/YY, two for Stokes I).
|
|
2516
2612
|
|
|
2517
2613
|
Returns:
|
|
2518
2614
|
CartDataCube: the simulated noise cube.
|
|
2519
2615
|
"""
|
|
2520
|
-
df = self.meta.chan_width
|
|
2521
|
-
int_time = self.meta.int_time
|
|
2522
2616
|
total_time = self.meta.total_time
|
|
2523
2617
|
f = time / total_time
|
|
2524
2618
|
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
# Scale noise_rms as if a spatial tapering had been applied.
|
|
2532
|
-
noise_rms = noise_rms * self.meta.win_fct_power ** .5
|
|
2533
|
-
|
|
2534
|
-
if sefd_jansky:
|
|
2535
|
-
lamb = const.c.value / self.freqs
|
|
2536
|
-
fov = (self.meta.shape[0] * self.meta.res) ** 2
|
|
2537
|
-
jy2k = ((1e-26 * lamb ** 2) / (2 * const.k_B.value)) / fov
|
|
2538
|
-
noise_rms = jy2k[:, None] * noise_rms
|
|
2619
|
+
noise_complex_std = self._noise_complex_std_per_unit_weight(
|
|
2620
|
+
sefd, time, n_pol_averaged=n_pol_averaged,
|
|
2621
|
+
sefd_jansky=sefd_jansky,
|
|
2622
|
+
fake_apply_win_fct=fake_apply_win_fct,
|
|
2623
|
+
)
|
|
2624
|
+
component_std = noise_complex_std / np.sqrt(2)
|
|
2539
2625
|
|
|
2540
2626
|
rng = np.random.default_rng(rng)
|
|
2541
2627
|
w = self.get() ** 0.5
|
|
@@ -2554,11 +2640,13 @@ class CartWeightCube(CartDataCube):
|
|
|
2554
2640
|
|
|
2555
2641
|
hs = (self.data.shape[0], len(idx1))
|
|
2556
2642
|
noise_data = np.zeros_like(self.data, dtype=np.complex128)
|
|
2557
|
-
noise_data[:, idx1] =
|
|
2643
|
+
noise_data[:, idx1] = component_std / w[:, idx1] * \
|
|
2558
2644
|
(rng.standard_normal(hs) + 1j * rng.standard_normal(hs))
|
|
2559
2645
|
noise_data[:, idx2] = np.conj(noise_data[:, idx1])
|
|
2560
2646
|
else:
|
|
2561
|
-
noise_data =
|
|
2647
|
+
noise_data = component_std / w * (
|
|
2648
|
+
rng.standard_normal(s) + 1j * rng.standard_normal(s)
|
|
2649
|
+
)
|
|
2562
2650
|
|
|
2563
2651
|
weights = self.new_with_data(self.data * f)
|
|
2564
2652
|
|
|
@@ -2570,37 +2658,31 @@ class CartWeightCube(CartDataCube):
|
|
|
2570
2658
|
|
|
2571
2659
|
return noise_cube
|
|
2572
2660
|
|
|
2573
|
-
def get_noise_std_cube(self, sefd, time, fake_apply_win_fct=False
|
|
2661
|
+
def get_noise_std_cube(self, sefd, time, fake_apply_win_fct=False,
|
|
2662
|
+
n_pol_averaged=2):
|
|
2574
2663
|
"""The *expected* per-mode thermal-noise standard deviation (not a
|
|
2575
2664
|
realization -- see :meth:`simulate_noise` for that).
|
|
2576
2665
|
|
|
2577
2666
|
Args:
|
|
2578
|
-
sefd:
|
|
2667
|
+
sefd: single-polarization station SEFD, in Jy.
|
|
2579
2668
|
time: total integration time, in seconds.
|
|
2580
2669
|
fake_apply_win_fct (bool): scale as if a spatial taper were applied.
|
|
2670
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
2671
|
+
into the visibility cube (one for XX/YY, two for Stokes I).
|
|
2581
2672
|
|
|
2582
2673
|
Returns:
|
|
2583
2674
|
NoiseStdCube: the per-mode noise standard deviation.
|
|
2584
2675
|
"""
|
|
2585
|
-
df = self.meta.chan_width
|
|
2586
|
-
int_time = self.meta.int_time
|
|
2587
2676
|
total_time = self.meta.total_time
|
|
2588
2677
|
f = time / total_time
|
|
2589
2678
|
|
|
2590
|
-
noise_rms =
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
jy2k = ((1e-26 * lamb ** 2) / (2 * const.k_B.value)) / fov
|
|
2595
|
-
noise_rms = (jy2k * noise_rms)[:, None]
|
|
2596
|
-
|
|
2679
|
+
noise_rms = self._noise_complex_std_per_unit_weight(
|
|
2680
|
+
sefd, time, n_pol_averaged=n_pol_averaged,
|
|
2681
|
+
fake_apply_win_fct=fake_apply_win_fct,
|
|
2682
|
+
)
|
|
2597
2683
|
w = self.get() ** 0.5
|
|
2598
2684
|
noise_rms = noise_rms / w
|
|
2599
2685
|
|
|
2600
|
-
if fake_apply_win_fct:
|
|
2601
|
-
# Scale noise_rms as if a spatial tapering had been applied.
|
|
2602
|
-
noise_rms = noise_rms * self.meta.win_fct_power ** .5
|
|
2603
|
-
|
|
2604
2686
|
weights = self.new_with_data(self.data * f)
|
|
2605
2687
|
|
|
2606
2688
|
noise_cube = NoiseStdCube(noise_rms, self.uu, self.vv, self.freqs.copy(),
|
|
@@ -234,11 +234,12 @@ def nearest_positive_definite(A, maxtries=10):
|
|
|
234
234
|
nearest_postive_definite = nearest_positive_definite
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
def get_samples(X, n, K, complex_type=True, nearest_pd=True, method='eigh'):
|
|
237
|
+
def get_samples(X, n, K, complex_type=True, nearest_pd=True, method='eigh', rng=None):
|
|
238
238
|
"""Draw ``n`` Gaussian realizations with covariance ``K``.
|
|
239
239
|
|
|
240
240
|
``X`` supplies the mean-vector length. The result has shape
|
|
241
|
-
``(len(X), n)`` and is complex unless ``complex_type=False``.
|
|
241
|
+
``(len(X), n)`` and is complex unless ``complex_type=False``. ``rng`` is a
|
|
242
|
+
seed, a :class:`numpy.random.Generator`, or None for a fresh draw. Raises
|
|
242
243
|
:class:`numpy.linalg.LinAlgError` if K is not finite; with
|
|
243
244
|
``nearest_pd=True``, a non-positive-definite matrix is repaired first.
|
|
244
245
|
"""
|
|
@@ -246,7 +247,7 @@ def get_samples(X, n, K, complex_type=True, nearest_pd=True, method='eigh'):
|
|
|
246
247
|
raise np.linalg.LinAlgError('covariance contains non-finite values')
|
|
247
248
|
if nearest_pd and not is_positive_definite(K):
|
|
248
249
|
K = nearest_positive_definite(K)
|
|
249
|
-
rg = np.random.default_rng()
|
|
250
|
+
rg = np.random.default_rng(rng)
|
|
250
251
|
n_draws = 2 * n if complex_type else n
|
|
251
252
|
draws = rg.multivariate_normal(
|
|
252
253
|
np.zeros_like(X).squeeze(), K, n_draws, method=method).T
|
|
@@ -55,10 +55,12 @@ class MLGPRForegroundFitter(fgfit.AbstractForegroundFitter):
|
|
|
55
55
|
|
|
56
56
|
if self.config.kern.noise.use_simulated_noise_cube:
|
|
57
57
|
if self.config.kern.noise.use_sefd_freqs_estimate:
|
|
58
|
-
sefd = noise_cube.estimate_freqs_sefd(
|
|
58
|
+
sefd = noise_cube.estimate_freqs_sefd(
|
|
59
|
+
sefd_poly_fit_deg=1, n_pol_averaged=2,
|
|
60
|
+
)
|
|
59
61
|
print(f'Using simulated noise with mean SEFD={sefd.mean():.1f} Jy')
|
|
60
62
|
else:
|
|
61
|
-
sefd = noise_cube.estimate_sefd()
|
|
63
|
+
sefd = noise_cube.estimate_sefd(n_pol_averaged=2)
|
|
62
64
|
print(f'Using simulated noise with SEFD={sefd:.1f} Jy')
|
|
63
65
|
|
|
64
66
|
w = noise_cube.weights.copy()
|
|
@@ -67,12 +69,16 @@ class MLGPRForegroundFitter(fgfit.AbstractForegroundFitter):
|
|
|
67
69
|
sefd_filter_n_bins=sefd_filter_n_bins)
|
|
68
70
|
|
|
69
71
|
if self.config.kern.noise.use_noise_std:
|
|
70
|
-
noise_cube = w.get_noise_std_cube(
|
|
71
|
-
|
|
72
|
+
noise_cube = w.get_noise_std_cube(
|
|
73
|
+
sefd, noise_cube.meta.total_time,
|
|
74
|
+
fake_apply_win_fct=True, n_pol_averaged=2,
|
|
75
|
+
)
|
|
72
76
|
noise_cube.weights = w
|
|
73
77
|
else:
|
|
74
|
-
noise_cube = w.simulate_noise(
|
|
75
|
-
|
|
78
|
+
noise_cube = w.simulate_noise(
|
|
79
|
+
sefd, noise_cube.meta.total_time,
|
|
80
|
+
fake_apply_win_fct=True, n_pol_averaged=2,
|
|
81
|
+
)
|
|
76
82
|
|
|
77
83
|
return noise_cube
|
|
78
84
|
|
|
@@ -16,7 +16,7 @@ Sensitivity models can be used independently of a UV simulation::
|
|
|
16
16
|
|
|
17
17
|
freqs = np.arange(50, 80, .2) * 1e6
|
|
18
18
|
telescope = obssimu.Telescope.from_name('nenufar')
|
|
19
|
-
|
|
19
|
+
single_pol_sefd = telescope.get_sefd(freqs)
|
|
20
20
|
|
|
21
21
|
For an array with an available station layout, configure the declination, hour
|
|
22
22
|
angle range, and time resolution before choosing a gridding strategy::
|
|
@@ -34,7 +34,10 @@ angle range, and time resolution before choosing a gridding strategy::
|
|
|
34
34
|
fov_deg=telescope.fov,
|
|
35
35
|
min_weight=1,
|
|
36
36
|
)
|
|
37
|
-
noise_std = coverage.get_noise_std_cube(
|
|
37
|
+
noise_std = coverage.get_noise_std_cube(
|
|
38
|
+
total_time_sec=100 * 3600,
|
|
39
|
+
n_pol_averaged=2,
|
|
40
|
+
)
|
|
38
41
|
|
|
39
42
|
The same :class:`SimuGridded` object provides a power-spectrum estimator whose
|
|
40
43
|
frequency range, UV limits, primary beam, and default weights match the
|
|
@@ -67,6 +70,7 @@ wavelengths, angles in degrees unless a parameter explicitly ends in
|
|
|
67
70
|
|
|
68
71
|
import itertools
|
|
69
72
|
import os
|
|
73
|
+
import warnings
|
|
70
74
|
from enum import Enum, auto
|
|
71
75
|
from importlib.resources import files as resource_files
|
|
72
76
|
from typing import ClassVar
|
|
@@ -231,7 +235,7 @@ class Telescope:
|
|
|
231
235
|
"""
|
|
232
236
|
|
|
233
237
|
def get_i_sefd(self, freq):
|
|
234
|
-
"""Return the Stokes-I SEFD in Jy.
|
|
238
|
+
"""Return the effective Stokes-I SEFD in Jy (deprecated).
|
|
235
239
|
|
|
236
240
|
Args:
|
|
237
241
|
freq (np.ndarray or float): Frequency in Hz.
|
|
@@ -239,6 +243,12 @@ class Telescope:
|
|
|
239
243
|
Returns:
|
|
240
244
|
ndarray or float: Stokes-I SEFD in Jy.
|
|
241
245
|
"""
|
|
246
|
+
warnings.warn(
|
|
247
|
+
'get_i_sefd() is deprecated; use get_sefd() and specify '
|
|
248
|
+
'n_pol_averaged instead.',
|
|
249
|
+
DeprecationWarning,
|
|
250
|
+
stacklevel=2,
|
|
251
|
+
)
|
|
242
252
|
|
|
243
253
|
return 1 / np.sqrt(2) * self.get_sefd(freq)
|
|
244
254
|
|
|
@@ -317,7 +327,6 @@ class DEx(Telescope):
|
|
|
317
327
|
fov = 120
|
|
318
328
|
du = 1
|
|
319
329
|
only_drift_mode = True
|
|
320
|
-
pb_name = name
|
|
321
330
|
coord_type = CoordinateType.ENU
|
|
322
331
|
location = EarthLocation(lon=116.67081524 * units.deg, lat=-26.70122102627586 * units.deg,
|
|
323
332
|
height=0.0 * units.m)
|
|
@@ -333,6 +342,19 @@ class DEx(Telescope):
|
|
|
333
342
|
self.sep_antenna = sep_antenna
|
|
334
343
|
self.n_antenna_side = n_antenna_side
|
|
335
344
|
|
|
345
|
+
@property
|
|
346
|
+
def pb_name(self):
|
|
347
|
+
"""Dipole beam name, following the configured antenna separation.
|
|
348
|
+
|
|
349
|
+
:meth:`get_sefd` models one element as an aperture of diameter
|
|
350
|
+
``sep_antenna`` (see :meth:`Telescope.get_dipole_aeff`), so the beam
|
|
351
|
+
uses the same aperture. The tapering matches the other dipole arrays
|
|
352
|
+
here (``a12_lba``, ``ovro_lwa``). Unlike every other telescope the
|
|
353
|
+
element geometry is a constructor argument, so this cannot be a fixed
|
|
354
|
+
class attribute.
|
|
355
|
+
"""
|
|
356
|
+
return f'ant_{self.sep_antenna}_1.1_gaussian'
|
|
357
|
+
|
|
336
358
|
def get_stat_pos(self):
|
|
337
359
|
"""Generate ENU antenna positions for the square layout.
|
|
338
360
|
|
|
@@ -373,17 +395,31 @@ class SkaLow(Telescope):
|
|
|
373
395
|
"""Return the packaged SKA-Low AA4 ENU station layout."""
|
|
374
396
|
return os.path.join(INSTRU_DIR, 'ska1_low_enu_statpos.data')
|
|
375
397
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
398
|
+
# AAVS2-based SKAO station-sensitivity lookup at zenith, effectively
|
|
399
|
+
# averaged over cold-sky LST 0-8 h. XX and YY are averaged over time
|
|
400
|
+
# independently following ska-ost-senscalc, then stored in a packaged CSV.
|
|
401
|
+
_sefd_table = np.genfromtxt(
|
|
402
|
+
os.path.join(INSTRU_DIR, 'ska_low_aavs2_sefd.csv'),
|
|
403
|
+
delimiter=',',
|
|
404
|
+
names=True,
|
|
405
|
+
)
|
|
406
|
+
_sefd_freqs = _sefd_table['frequency_mhz'] * 1e6
|
|
407
|
+
_sefd_xx_interpolator = scipy.interpolate.UnivariateSpline(
|
|
408
|
+
_sefd_freqs, _sefd_table['sefd_xx_jy'], s=0,
|
|
409
|
+
)
|
|
410
|
+
_sefd_yy_interpolator = scipy.interpolate.UnivariateSpline(
|
|
411
|
+
_sefd_freqs, _sefd_table['sefd_yy_jy'], s=0,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def get_sefd(self, freq):
|
|
415
|
+
"""The interpolated single-polarization-equivalent SKA-Low SEFD in Jy."""
|
|
416
|
+
sefd_xx = self._sefd_xx_interpolator(freq)
|
|
417
|
+
sefd_yy = self._sefd_yy_interpolator(freq)
|
|
385
418
|
|
|
386
|
-
|
|
419
|
+
# ska-ost-senscalc defines SEFD_I = hypot(SEFD_XX, SEFD_YY) / 2.
|
|
420
|
+
# Returning the equivalent single-polarization value keeps
|
|
421
|
+
# get_noise_std_cube(..., n_pol_averaged=2) consistent with SEFD_I.
|
|
422
|
+
return np.hypot(sefd_xx, sefd_yy) / np.sqrt(2)
|
|
387
423
|
|
|
388
424
|
|
|
389
425
|
class SkaLowAAstar(SkaLow):
|
|
@@ -1083,11 +1119,17 @@ class SimuGridded:
|
|
|
1083
1119
|
return np.atleast_1d(self.telescope_simu.telescop.get_sefd(self.weights.freqs))
|
|
1084
1120
|
|
|
1085
1121
|
def get_i_sefd(self):
|
|
1086
|
-
"""The Stokes-I SEFD across
|
|
1122
|
+
"""The effective Stokes-I SEFD across frequencies (deprecated).
|
|
1087
1123
|
|
|
1088
1124
|
Returns:
|
|
1089
1125
|
ndarray: Stokes-I SEFD in Jy.
|
|
1090
1126
|
"""
|
|
1127
|
+
warnings.warn(
|
|
1128
|
+
'get_i_sefd() is deprecated; use get_sefd() and specify '
|
|
1129
|
+
'n_pol_averaged instead.',
|
|
1130
|
+
DeprecationWarning,
|
|
1131
|
+
stacklevel=2,
|
|
1132
|
+
)
|
|
1091
1133
|
|
|
1092
1134
|
return 1 / np.sqrt(2) * self.get_sefd()
|
|
1093
1135
|
|
|
@@ -1135,7 +1177,8 @@ class SimuGridded:
|
|
|
1135
1177
|
|
|
1136
1178
|
return pspec.PowerSpectraCart(eor, ps_conf, pb)
|
|
1137
1179
|
|
|
1138
|
-
def get_noise_std_cube(self, total_time_sec, sefd=None, min_weight=1
|
|
1180
|
+
def get_noise_std_cube(self, total_time_sec, sefd=None, min_weight=1,
|
|
1181
|
+
n_pol_averaged=2):
|
|
1139
1182
|
"""Estimate the thermal-noise standard deviation in every UV cell.
|
|
1140
1183
|
|
|
1141
1184
|
A window recorded during gridding is included through its equivalent
|
|
@@ -1146,21 +1189,26 @@ class SimuGridded:
|
|
|
1146
1189
|
|
|
1147
1190
|
Args:
|
|
1148
1191
|
total_time_sec: Target total integration time in seconds.
|
|
1149
|
-
sefd: Optional
|
|
1150
|
-
frequency. By default use the telescope model.
|
|
1192
|
+
sefd: Optional single-polarization station SEFD in Jy, scalar or
|
|
1193
|
+
one value per frequency. By default use the telescope model.
|
|
1151
1194
|
min_weight: Remove cells whose weight is below this value.
|
|
1195
|
+
n_pol_averaged: number of independent polarization products averaged
|
|
1196
|
+
into the visibility cube (one for XX/YY, two for Stokes I).
|
|
1152
1197
|
|
|
1153
1198
|
Returns:
|
|
1154
1199
|
ps_eor.datacube.NoiseStdCube: Real standard deviation per frequency
|
|
1155
1200
|
and UV cell.
|
|
1156
1201
|
"""
|
|
1157
1202
|
if sefd is None:
|
|
1158
|
-
sefd = self.
|
|
1203
|
+
sefd = self.get_sefd()
|
|
1159
1204
|
|
|
1160
1205
|
fake_apply_win_fct = 'PEWINFCT' in self.weights.meta
|
|
1161
1206
|
|
|
1162
|
-
noise_std = self.weights.get_noise_std_cube(
|
|
1163
|
-
|
|
1207
|
+
noise_std = self.weights.get_noise_std_cube(
|
|
1208
|
+
sefd, total_time_sec,
|
|
1209
|
+
fake_apply_win_fct=fake_apply_win_fct,
|
|
1210
|
+
n_pol_averaged=n_pol_averaged,
|
|
1211
|
+
)
|
|
1164
1212
|
noise_std.filter_min_weight(min_weight)
|
|
1165
1213
|
|
|
1166
1214
|
return noise_std
|
|
@@ -37,9 +37,10 @@ def _bind_signal_kern(kern, uv_bins, mean_fmhz):
|
|
|
37
37
|
'expected UVScaledKernel, VAEKernTorch, or a sum of those')
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def get_multi_samples(X, ns, Ks):
|
|
40
|
+
def get_multi_samples(X, ns, Ks, rng=None):
|
|
41
41
|
"""Draw ns[i] realizations from each per-uv-bin covariance Ks[i]."""
|
|
42
|
-
|
|
42
|
+
rng = np.random.default_rng(rng)
|
|
43
|
+
return [get_samples(X, n, K, rng=rng) for n, K in zip(ns, Ks, strict=False)]
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _contains_product(kern):
|
|
@@ -95,28 +96,28 @@ class SimuMultiGPCube:
|
|
|
95
96
|
simulated data can be compared with, or added to, that cube."""
|
|
96
97
|
return SimuMultiGPCube(cube, **kwargs)
|
|
97
98
|
|
|
98
|
-
def get_from_kern(self, kern):
|
|
99
|
+
def get_from_kern(self, kern, rng=None):
|
|
99
100
|
"""Cube drawn from a kernel's covariance.
|
|
100
101
|
|
|
101
102
|
Takes a UVScaledKernel, VAEKernTorch, or a sum of those; binding it to
|
|
102
103
|
this geometry is done here, so it may be passed unbound.
|
|
103
104
|
"""
|
|
104
105
|
fmhz = self.template.freqs * 1e-6
|
|
105
|
-
return self._draw(self._K_from_kern(kern, fmhz))
|
|
106
|
+
return self._draw(self._K_from_kern(kern, fmhz), rng=rng)
|
|
106
107
|
|
|
107
|
-
def get_from_K(self, K):
|
|
108
|
+
def get_from_K(self, K, rng=None):
|
|
108
109
|
"""Cube drawn from an explicit (n_bins, n_freq, n_freq) covariance."""
|
|
109
|
-
return self._draw(K)
|
|
110
|
+
return self._draw(K, rng=rng)
|
|
110
111
|
|
|
111
|
-
def get_from_ps3d(self, k_mean, ps3d):
|
|
112
|
+
def get_from_ps3d(self, k_mean, ps3d, rng=None):
|
|
112
113
|
"""Cube drawn from a spherically averaged 21-cm power spectrum.
|
|
113
114
|
|
|
114
115
|
ps3d is delta(k) on the k_mean grid. Requires a simulator built with
|
|
115
116
|
:meth:`new`, since the normalization needs res and n_pix.
|
|
116
117
|
"""
|
|
117
|
-
return self._draw(self._K_from_ps(k_mean, ps3d, self.template.freqs))
|
|
118
|
+
return self._draw(self._K_from_ps(k_mean, ps3d, self.template.freqs), rng=rng)
|
|
118
119
|
|
|
119
|
-
def get_noise(self, variance):
|
|
120
|
+
def get_noise(self, variance, rng=None):
|
|
120
121
|
"""White-noise cube of constant variance.
|
|
121
122
|
|
|
122
123
|
Use this to make the noise cube a fit needs;
|
|
@@ -125,9 +126,9 @@ class SimuMultiGPCube:
|
|
|
125
126
|
"""
|
|
126
127
|
n_freq = len(self.template.freqs)
|
|
127
128
|
K = variance * np.array([np.eye(n_freq)] * len(self.uv_bins))
|
|
128
|
-
return self.get_from_K(K)
|
|
129
|
+
return self.get_from_K(K, rng=rng)
|
|
129
130
|
|
|
130
|
-
def get_epochs_from_kern(self, kern, epoch_times):
|
|
131
|
+
def get_epochs_from_kern(self, kern, epoch_times, rng=None):
|
|
131
132
|
"""One cube per epoch, with the cross-epoch coherence a TimeKron implies.
|
|
132
133
|
|
|
133
134
|
Takes a TimeKron expression and epoch times in days. Sums are drawn
|
|
@@ -135,7 +136,10 @@ class SimuMultiGPCube:
|
|
|
135
136
|
coherent epochs. Expressions containing a product are drawn from their
|
|
136
137
|
full joint epoch-frequency covariance, since a product of sums is not
|
|
137
138
|
separable component by component.
|
|
139
|
+
|
|
140
|
+
``rng`` is a seed, a :class:`numpy.random.Generator`, or None.
|
|
138
141
|
"""
|
|
142
|
+
rng = np.random.default_rng(rng)
|
|
139
143
|
comps = list(_iter_leaf_kernels(kern))
|
|
140
144
|
if not all(isinstance(c, TimeKron) for c in comps):
|
|
141
145
|
raise TypeError('get_epochs_from_kern expects a TimeKron or a sum of TimeKrons')
|
|
@@ -151,7 +155,7 @@ class SimuMultiGPCube:
|
|
|
151
155
|
K_joint, joint_x = self._K_from_time_kern(kern, fmhz, times)
|
|
152
156
|
n_f = len(fmhz)
|
|
153
157
|
for K_i, idx in zip(K_joint, idxs, strict=False):
|
|
154
|
-
draw = get_samples(joint_x, int(np.sum(idx)), K_i)
|
|
158
|
+
draw = get_samples(joint_x, int(np.sum(idx)), K_i, rng=rng)
|
|
155
159
|
for t, cube in enumerate(cubes):
|
|
156
160
|
cube.data[:, idx] = draw[t * n_f:(t + 1) * n_f]
|
|
157
161
|
return cubes
|
|
@@ -182,7 +186,8 @@ class SimuMultiGPCube:
|
|
|
182
186
|
|
|
183
187
|
for K_i, idx in zip(K_f, idxs, strict=False):
|
|
184
188
|
n = int(np.sum(idx))
|
|
185
|
-
Z = {r: get_samples(fmhz, n, K_i
|
|
189
|
+
Z = {r: get_samples(fmhz, n, K_i, rng=rng)
|
|
190
|
+
for r in active} # independent (n_f, n) draws
|
|
186
191
|
for t in range(n_t):
|
|
187
192
|
for r in active:
|
|
188
193
|
if abs(L_t[t, r]) > 1e-12:
|
|
@@ -205,12 +210,13 @@ class SimuMultiGPCube:
|
|
|
205
210
|
with torch.no_grad():
|
|
206
211
|
return kern(x, x).to_dense().numpy(), joint_x
|
|
207
212
|
|
|
208
|
-
def _draw(self, K):
|
|
213
|
+
def _draw(self, K, rng=None):
|
|
214
|
+
rng = np.random.default_rng(rng)
|
|
209
215
|
c = self.template.new_with_data(np.zeros_like(self.template.data))
|
|
210
216
|
fmhz = c.freqs * 1e-6
|
|
211
217
|
idxs = get_uv_bin_masks(c.ru, self.uv_bins)
|
|
212
218
|
for idx, Ki in zip(idxs, K, strict=False):
|
|
213
|
-
c.data[:, idx] = get_samples(fmhz, np.sum(idx), Ki)
|
|
219
|
+
c.data[:, idx] = get_samples(fmhz, np.sum(idx), Ki, rng=rng)
|
|
214
220
|
return c
|
|
215
221
|
|
|
216
222
|
def _K_from_kern(self, kern, fmhz):
|
|
@@ -1194,8 +1194,10 @@ def simu_noise_img(uv_simu_h5, total_time_hour, out_filename, min_weight):
|
|
|
1194
1194
|
print(f'Simulating thermal noise with mean SEFD = {uv_simu.get_sefd().mean()} Jy ...')
|
|
1195
1195
|
print(f'{len(freqs)} frequency channels between {freqs.min() * 1e-6:.1f} and '
|
|
1196
1196
|
f'{freqs.max() * 1e-6:.1f} MHz')
|
|
1197
|
-
noise_cube = uv_simu.weights.simulate_noise(
|
|
1198
|
-
|
|
1197
|
+
noise_cube = uv_simu.weights.simulate_noise(
|
|
1198
|
+
uv_simu.get_sefd(), total_time_hour * 3600,
|
|
1199
|
+
hermitian=False, n_pol_averaged=2,
|
|
1200
|
+
)
|
|
1199
1201
|
noise_cube.filter_min_weight(min_weight, replace=True)
|
|
1200
1202
|
noise_img = noise_cube.image()
|
|
1201
1203
|
print(f'RMS noise @ mid frequency {noise_img.data[len(freqs) // 2].std()} Kelvin')
|
|
@@ -1219,7 +1221,7 @@ def simu_noise_img(uv_simu_h5, total_time_hour, out_filename, min_weight):
|
|
|
1219
1221
|
@click.option('--filter_kpar_min', help='Filter mode below kpar_min', default=None, type=float)
|
|
1220
1222
|
@click.option('--filter_wedge_theta', help='Filter mode below the wedge line', default=0,
|
|
1221
1223
|
type=float)
|
|
1222
|
-
@click.option('--sefd', help='
|
|
1224
|
+
@click.option('--sefd', help='Single-polarization station SEFD (Jy)', default=None, type=float)
|
|
1223
1225
|
@click.option('--n_incoherent_avg', help='Number of incoherent averaging', default=1, type=int)
|
|
1224
1226
|
@click.option('--min_weight', help='Minimum number of visibilities in a uv-cell', default=10,
|
|
1225
1227
|
type=int, show_default=True)
|
|
@@ -1254,7 +1256,10 @@ def simu_noise_ps(uv_simu_h5, total_time_hour, out_filename, kmin, kmax, nks,
|
|
|
1254
1256
|
|
|
1255
1257
|
print(f'{len(freqs)} frequency channels between {freqs.min() * 1e-6:.1f} and '
|
|
1256
1258
|
f'{freqs.max() * 1e-6:.1f} MHz')
|
|
1257
|
-
noise_std = uv_simu.get_noise_std_cube(
|
|
1259
|
+
noise_std = uv_simu.get_noise_std_cube(
|
|
1260
|
+
total_time_hour * 3600, sefd=sefd,
|
|
1261
|
+
min_weight=min_weight, n_pol_averaged=2,
|
|
1262
|
+
)
|
|
1258
1263
|
pss = {}
|
|
1259
1264
|
pss['ps'] = ps_gen.get_ps(noise_std)
|
|
1260
1265
|
pss['ps2d'] = ps_gen.get_ps2d(noise_std)
|
|
@@ -1316,7 +1321,12 @@ def simu_noise_ps_zrange(uv_simu_h5, total_time_hour, k, out_filename, bandwidth
|
|
|
1316
1321
|
simu_gridded = obssimu.SimuGridded.load(uv_simu_h5)
|
|
1317
1322
|
z_max = np.round(psutil.freq_to_z(simu_gridded.weights.freqs.min() + bandwidth / 2 * 1e6), 1)
|
|
1318
1323
|
z_min = np.round(psutil.freq_to_z(simu_gridded.weights.freqs.max() - bandwidth / 2 * 1e6), 1)
|
|
1324
|
+
# arange's stop is exclusive, so the + delta_z is there to keep z_max. It
|
|
1325
|
+
# also admits one step past it whenever (z_max - z_min) is not a multiple of
|
|
1326
|
+
# delta_z, and that slice lands outside the coverage: an empty frequency
|
|
1327
|
+
# selection, and no delay modes to take a minimum over.
|
|
1319
1328
|
zs = np.arange(z_min, z_max + delta_z, delta_z)
|
|
1329
|
+
zs = zs[zs <= z_max + 1e-9]
|
|
1320
1330
|
|
|
1321
1331
|
k_bin = [(1 - dk_over_k / 2) * k, (1 + dk_over_k / 2) * k]
|
|
1322
1332
|
|
|
@@ -1334,11 +1344,15 @@ def simu_noise_ps_zrange(uv_simu_h5, total_time_hour, k, out_filename, bandwidth
|
|
|
1334
1344
|
ps_gen = uv_simu_z.get_ps_gen(filter_kpar_min, filter_wedge_theta)
|
|
1335
1345
|
|
|
1336
1346
|
print(f'Redshift z = {z}')
|
|
1337
|
-
|
|
1347
|
+
sefd = uv_simu_z.get_sefd()
|
|
1348
|
+
print(f'Simulating thermal noise with mean SEFD = {sefd.mean():.1f} Jy ...')
|
|
1338
1349
|
print(f'{len(freqs)} frequency channels between {freqs.min() * 1e-6:.1f} and '
|
|
1339
1350
|
f'{freqs.max() * 1e-6:.1f} MHz')
|
|
1340
1351
|
|
|
1341
|
-
noise_std = uv_simu_z.get_noise_std_cube(
|
|
1352
|
+
noise_std = uv_simu_z.get_noise_std_cube(
|
|
1353
|
+
total_time_hour * 3600, sefd=sefd,
|
|
1354
|
+
min_weight=min_weight, n_pol_averaged=2,
|
|
1355
|
+
)
|
|
1342
1356
|
|
|
1343
1357
|
ps3d = ps_gen.get_ps3d(k_bin, noise_std)
|
|
1344
1358
|
if n_incoherent_avg > 1:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|