limTOD 1.3.0__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.
- limTOD/HPW_filter.py +785 -0
- limTOD/__init__.py +60 -0
- limTOD/flicker_model.py +75 -0
- limTOD/mpiutil.py +281 -0
- limTOD/simulator.py +1204 -0
- limTOD/sky_model.py +153 -0
- limTOD/visual.py +101 -0
- limtod-1.3.0.dist-info/METADATA +201 -0
- limtod-1.3.0.dist-info/RECORD +19 -0
- limtod-1.3.0.dist-info/WHEEL +5 -0
- limtod-1.3.0.dist-info/licenses/LICENSE +21 -0
- limtod-1.3.0.dist-info/top_level.txt +2 -0
- limtod_jax/__init__.py +78 -0
- limtod_jax/alm.py +122 -0
- limtod_jax/angles.py +123 -0
- limtod_jax/core.py +198 -0
- limtod_jax/hpx.py +63 -0
- limtod_jax/projection.py +78 -0
- limtod_jax/wigner.py +111 -0
limTOD/HPW_filter.py
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, List, Optional, Sequence, SupportsFloat, Tuple, Union, cast
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy import signal
|
|
6
|
+
from scipy.linalg import solve, LinAlgError
|
|
7
|
+
from limTOD.simulator import truncate_stacked_beam, generate_sky2sys_projection
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_filtfilt_matrix(n_samples: int, b: np.ndarray, a: np.ndarray) -> np.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
More accurate matrix representation of filtfilt operation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Create matrix by applying filtfilt to each standard basis vector
|
|
18
|
+
H = np.zeros((n_samples, n_samples))
|
|
19
|
+
|
|
20
|
+
for i in range(n_samples):
|
|
21
|
+
e_i = np.zeros(n_samples)
|
|
22
|
+
e_i[i] = 1.0
|
|
23
|
+
H[:, i] = signal.filtfilt(b, a, e_i)
|
|
24
|
+
|
|
25
|
+
return H
|
|
26
|
+
|
|
27
|
+
def HP_filter_TOD(n_samples: int, dtime: float, cutoff_freq: float = 0.001,
|
|
28
|
+
filter_order: int = 4,
|
|
29
|
+
preserve_dc: bool = False) -> np.ndarray:
|
|
30
|
+
"""
|
|
31
|
+
Apply high-pass Butterworth filter to the TOD.
|
|
32
|
+
Parameters:
|
|
33
|
+
-----------
|
|
34
|
+
n_samples : int
|
|
35
|
+
Number of samples in the TOD
|
|
36
|
+
dtime : float
|
|
37
|
+
Time interval between samples in seconds
|
|
38
|
+
cutoff_freq : float, default=0.001 Hz
|
|
39
|
+
Cutoff frequency for high-pass filter in unit of Hz
|
|
40
|
+
filter_order : int, default=4
|
|
41
|
+
Order of the Butterworth filter (typical range: 2-8)
|
|
42
|
+
Higher order = sharper cutoff but more edge effects
|
|
43
|
+
preserve_dc : bool, default=False
|
|
44
|
+
If True, add back the DC projection so the filter has unit gain
|
|
45
|
+
at ℓ=0 while still rejecting drifts between DC and the cutoff.
|
|
46
|
+
Constructed as H' = H (I - P) + P with P = J/n (the DC
|
|
47
|
+
projection). Useful only when the per-chunk mean is dominated
|
|
48
|
+
by sky (low-noise regime). In the realistic 1/f regime the
|
|
49
|
+
chunk mean is dominated by drift realisation, so preserve_dc
|
|
50
|
+
would let drift DC leak into the recovered map — keep the
|
|
51
|
+
default False unless you've verified this for your noise model.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
--------
|
|
55
|
+
HP_operator : array-like, shape (n_time, n_params)
|
|
56
|
+
High-pass filtered system temperature operator
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# Design a high-pass Butterworth filter
|
|
60
|
+
fs = 1.0 / dtime
|
|
61
|
+
nyquist = fs / 2.0
|
|
62
|
+
normalized_cutoff = cutoff_freq / nyquist # Normalized cutoff frequency for high-pass filter
|
|
63
|
+
|
|
64
|
+
# Validate normalized cutoff frequency
|
|
65
|
+
if normalized_cutoff <= 0:
|
|
66
|
+
raise ValueError(f"Cutoff frequency must be positive. Got cutoff_freq={cutoff_freq} Hz")
|
|
67
|
+
if normalized_cutoff >= 1:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Cutoff frequency ({cutoff_freq} Hz) must be less than Nyquist frequency ({nyquist} Hz). "
|
|
70
|
+
f"Normalized cutoff = {normalized_cutoff:.3f} >= 1.0"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
b, a = signal.butter(filter_order, normalized_cutoff, btype='high', analog=False)
|
|
74
|
+
|
|
75
|
+
H_exact = get_filtfilt_matrix(n_samples, b, a) # Exact matrix representation of filtfilt operation
|
|
76
|
+
if preserve_dc:
|
|
77
|
+
# H' x = H (I - P) x + P x = H (x - mean(x) 1) + mean(x) 1.
|
|
78
|
+
# Constants pass through unchanged; everything faster than the
|
|
79
|
+
# Butterworth cutoff is still attenuated.
|
|
80
|
+
P = np.ones((n_samples, n_samples)) / n_samples
|
|
81
|
+
H_exact = H_exact @ (np.eye(n_samples) - P) + P
|
|
82
|
+
return H_exact
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def wiener_filter_map(
|
|
86
|
+
TOD: np.ndarray,
|
|
87
|
+
operator: np.ndarray,
|
|
88
|
+
noise_variance: Optional[Union[float, np.floating, np.ndarray]] = None,
|
|
89
|
+
prior_inv_cov: Optional[Union[float, np.ndarray]] = None,
|
|
90
|
+
guess: Optional[np.ndarray] = None,
|
|
91
|
+
regularization: float = 1e-12,
|
|
92
|
+
return_full_cov: bool = False,
|
|
93
|
+
rolling_variance: bool = True,
|
|
94
|
+
) -> Union[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
|
95
|
+
"""
|
|
96
|
+
Apply Wiener filtering for mapmaking from time-ordered data.
|
|
97
|
+
|
|
98
|
+
The Wiener filter solves: (A^T N^-1 A + S^-1)^-1 A^T N^-1 d
|
|
99
|
+
where A is the operator, N is noise covariance, S is signal covariance, d is data
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
-----------
|
|
103
|
+
TOD : array-like, shape (n_time,)
|
|
104
|
+
Time-ordered data to be mapped
|
|
105
|
+
operator : array-like, shape (n_time, n_pixels)
|
|
106
|
+
Pointing/beam operator mapping sky pixels to TOD samples
|
|
107
|
+
noise_variance : float or array-like, optional
|
|
108
|
+
Noise variance. If None, estimated from TOD
|
|
109
|
+
prior_inv_cov : float or array-like, optional
|
|
110
|
+
Inverse of Prior covariance for the parameters. If None, uses uninformative prior
|
|
111
|
+
regularization : float, default=1e-12
|
|
112
|
+
Regularization parameter to ensure matrix invertibility
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
--------
|
|
116
|
+
sky_map : array, shape (n_pixels,)
|
|
117
|
+
Reconstructed sky map
|
|
118
|
+
uncertainty : array, shape (n_pixels,)
|
|
119
|
+
Per-pixel uncertainty (diagonal of covariance matrix)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# Convert inputs to numpy arrays
|
|
123
|
+
TOD = np.asarray(TOD)
|
|
124
|
+
operator = np.asarray(operator)
|
|
125
|
+
|
|
126
|
+
n_time, n_pixels = operator.shape
|
|
127
|
+
|
|
128
|
+
# Estimate noise variance if not provided
|
|
129
|
+
if noise_variance is None:
|
|
130
|
+
# Simple estimate: variance of high-pass filtered residuals
|
|
131
|
+
residual = TOD - operator @ np.linalg.pinv(operator) @ TOD
|
|
132
|
+
if rolling_variance:
|
|
133
|
+
# Cap the window at the TOD length: with the fixed window of 100
|
|
134
|
+
# samples, shorter TODs used to produce a truncated variance
|
|
135
|
+
# vector and an opaque matmul shape error downstream.
|
|
136
|
+
window_size = min(100, n_time)
|
|
137
|
+
half_window = window_size // 2
|
|
138
|
+
|
|
139
|
+
# Pad with first N and last N samples (reflected)
|
|
140
|
+
# This provides smoother boundaries than just repeating edge value
|
|
141
|
+
left_pad = residual[:half_window][::-1] # First N samples, reversed
|
|
142
|
+
right_pad = residual[-half_window:][::-1] # Last N samples, reversed
|
|
143
|
+
padded_residual = np.concatenate([left_pad, residual, right_pad])
|
|
144
|
+
|
|
145
|
+
# Apply rolling window to squared residuals
|
|
146
|
+
noise_variance = np.convolve(
|
|
147
|
+
padded_residual**2,
|
|
148
|
+
np.ones(window_size)/window_size,
|
|
149
|
+
mode='valid'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Trim to exact length if needed
|
|
153
|
+
if len(noise_variance) > len(residual):
|
|
154
|
+
# Take the middle portion
|
|
155
|
+
excess = len(noise_variance) - len(residual)
|
|
156
|
+
start = excess // 2
|
|
157
|
+
noise_variance = noise_variance[start:start+len(residual)]
|
|
158
|
+
else:
|
|
159
|
+
noise_variance = np.var(residual)
|
|
160
|
+
logger.info("Estimated noise variance: %.6f", noise_variance)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Create noise inverse covariance matrix (assume diagonal)
|
|
164
|
+
if np.isscalar(noise_variance):
|
|
165
|
+
N_inv = np.eye(n_time) / cast(float, noise_variance)
|
|
166
|
+
else:
|
|
167
|
+
noise_variance = np.asarray(noise_variance)
|
|
168
|
+
if len(noise_variance) != n_time:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"noise_variance has length {len(noise_variance)} but the TOD "
|
|
171
|
+
f"has {n_time} samples"
|
|
172
|
+
)
|
|
173
|
+
# Convert to dense diagonal matrix for consistent matrix operations
|
|
174
|
+
N_inv = np.diag(1.0 / noise_variance)
|
|
175
|
+
|
|
176
|
+
# Create signal inverse covariance matrix
|
|
177
|
+
if prior_inv_cov is None:
|
|
178
|
+
S_inv: np.ndarray = np.zeros((n_pixels, n_pixels)) # Uninformative prior
|
|
179
|
+
elif np.isscalar(prior_inv_cov):
|
|
180
|
+
S_inv = np.eye(n_pixels) * cast(float, prior_inv_cov)
|
|
181
|
+
elif cast(np.ndarray, prior_inv_cov).ndim == 1:
|
|
182
|
+
# Convert to dense diagonal matrix for consistent matrix operations
|
|
183
|
+
S_inv = np.diag(np.asarray(prior_inv_cov))
|
|
184
|
+
elif cast(np.ndarray, prior_inv_cov).ndim == 2:
|
|
185
|
+
S_inv = np.asarray(prior_inv_cov)
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError("prior_inv_cov must be a scalar, 1D array, or 2D array.")
|
|
188
|
+
|
|
189
|
+
if guess is None:
|
|
190
|
+
guess = np.zeros(n_pixels)
|
|
191
|
+
# Only override prior if none was provided
|
|
192
|
+
if prior_inv_cov is None:
|
|
193
|
+
S_inv = np.zeros((n_pixels, n_pixels)) # Uninformative prior
|
|
194
|
+
else:
|
|
195
|
+
guess = np.asarray(guess)
|
|
196
|
+
if len(guess) != n_pixels:
|
|
197
|
+
raise ValueError("Length of guess must match number of pixels.")
|
|
198
|
+
|
|
199
|
+
# Compute Wiener filter components
|
|
200
|
+
# If float type, transpose; if complex, conjugate transpose
|
|
201
|
+
if np.iscomplexobj(operator):
|
|
202
|
+
AtN = operator.conj().T @ N_inv # A^H N^-1
|
|
203
|
+
else:
|
|
204
|
+
AtN = operator.T @ N_inv # A^T N^-1
|
|
205
|
+
AtNA = AtN @ operator # A^dagger N^-1 A
|
|
206
|
+
|
|
207
|
+
# Add signal prior and regularization
|
|
208
|
+
covariance_inv = AtNA + S_inv + regularization * np.eye(n_pixels)
|
|
209
|
+
|
|
210
|
+
# Right-hand side: A^T N^-1 d + S^-1 mu
|
|
211
|
+
rhs = AtN @ TOD + S_inv @ guess
|
|
212
|
+
|
|
213
|
+
posterior_cov = None
|
|
214
|
+
try:
|
|
215
|
+
# Solve the linear system: (A^T N^-1 A + S^-1) x = A^T N^-1 d + S^-1 mu
|
|
216
|
+
sky_map = solve(covariance_inv, rhs, assume_a='pos')
|
|
217
|
+
|
|
218
|
+
# Compute uncertainties (diagonal of posterior covariance)
|
|
219
|
+
try:
|
|
220
|
+
posterior_cov = np.linalg.inv(covariance_inv)
|
|
221
|
+
uncertainty = np.sqrt(np.diag(posterior_cov))
|
|
222
|
+
except (LinAlgError, np.linalg.LinAlgError):
|
|
223
|
+
logger.warning(
|
|
224
|
+
"Could not compute full covariance matrix; using diagonal approximation."
|
|
225
|
+
)
|
|
226
|
+
uncertainty = 1.0 / np.sqrt(np.diag(covariance_inv))
|
|
227
|
+
|
|
228
|
+
except (LinAlgError, np.linalg.LinAlgError) as e:
|
|
229
|
+
logger.warning("Linear algebra error: %s; falling back to pseudo-inverse solution.", e)
|
|
230
|
+
sky_map = np.linalg.pinv(operator) @ TOD
|
|
231
|
+
uncertainty = np.ones(n_pixels) * np.nan
|
|
232
|
+
|
|
233
|
+
if return_full_cov:
|
|
234
|
+
# posterior_cov stayed None on the degraded paths (inv failure or the
|
|
235
|
+
# pseudo-inverse fallback) — returning it unbound used to NameError.
|
|
236
|
+
if posterior_cov is None:
|
|
237
|
+
raise np.linalg.LinAlgError(
|
|
238
|
+
"return_full_cov=True but the posterior covariance could not "
|
|
239
|
+
"be computed (the normal-equations matrix is numerically "
|
|
240
|
+
"singular); rerun with return_full_cov=False or increase "
|
|
241
|
+
"regularization/priors."
|
|
242
|
+
)
|
|
243
|
+
return sky_map, uncertainty, posterior_cov
|
|
244
|
+
else:
|
|
245
|
+
return sky_map, uncertainty
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# Alternative simplified version for quick mapmaking
|
|
251
|
+
def simple_wiener_map(
|
|
252
|
+
TOD: np.ndarray,
|
|
253
|
+
operator: np.ndarray,
|
|
254
|
+
noise_var: Optional[Union[float, np.floating]] = None,
|
|
255
|
+
) -> np.ndarray:
|
|
256
|
+
"""
|
|
257
|
+
Simplified Wiener filter assuming uninformative signal prior.
|
|
258
|
+
Equivalent to: (A^T A + lambda*I)^-1 A^T d
|
|
259
|
+
"""
|
|
260
|
+
if noise_var is None:
|
|
261
|
+
# Estimate from residuals
|
|
262
|
+
residual = TOD - operator @ np.linalg.pinv(operator) @ TOD
|
|
263
|
+
noise_var = np.var(residual)
|
|
264
|
+
|
|
265
|
+
AtA = operator.T @ operator
|
|
266
|
+
regularization = noise_var * 1e-6 # Small regularization
|
|
267
|
+
|
|
268
|
+
# Regularized normal equation
|
|
269
|
+
lhs = AtA + regularization * np.eye(AtA.shape[0])
|
|
270
|
+
rhs = operator.T @ TOD
|
|
271
|
+
|
|
272
|
+
sky_map = np.linalg.solve(lhs, rhs)
|
|
273
|
+
|
|
274
|
+
return sky_map
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class HPW_mapmaking:
|
|
279
|
+
"""
|
|
280
|
+
Map-making class for Time-Ordered Data (TOD) using high-pass filtering and Wiener filtering.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
*,
|
|
286
|
+
beam_map: np.ndarray,
|
|
287
|
+
LST_deg_list_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
288
|
+
lat_deg: float,
|
|
289
|
+
azimuth_deg_list_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
290
|
+
elevation_deg_list_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
291
|
+
selfrot_deg_list_group: Optional[Union[np.ndarray, Sequence[np.ndarray]]] = None,
|
|
292
|
+
threshold: float = 0.01,
|
|
293
|
+
Tsys_others_operator_group: Optional[Sequence[np.ndarray]] = None,
|
|
294
|
+
nside_hires: Optional[int] = None,
|
|
295
|
+
nside_target: Optional[int] = None,
|
|
296
|
+
beam_truncate_frac_thres: Optional[float] = None
|
|
297
|
+
) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Initialize the HPW_mapmaking class.
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
beam_map : array
|
|
303
|
+
The Healpix map of the beam pattern for a single frequency.
|
|
304
|
+
Input map can be:
|
|
305
|
+
a single array is considered I,
|
|
306
|
+
array with 3 rows:[I,Q,U]
|
|
307
|
+
array with 4 rows:[I,Q,U,V]
|
|
308
|
+
|
|
309
|
+
LST_deg_list_group : a LST list or a list of LST lists corresponding to each TOD in TOD_group.
|
|
310
|
+
e.g. [LST_deg_list_1, LST_deg_list_2, ...]
|
|
311
|
+
Note that it can be generated by limTOD.simulator.generate_LSTs_deg function. For example:
|
|
312
|
+
LST_deg_list = generate_LSTs_deg(
|
|
313
|
+
ant_latitude_deg,
|
|
314
|
+
ant_longitude_deg,
|
|
315
|
+
ant_height_m,
|
|
316
|
+
time_list,
|
|
317
|
+
start_time_utc=start_time_utc,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
lat_deg : float
|
|
321
|
+
The latitude of the observation site in degrees.
|
|
322
|
+
|
|
323
|
+
azimuth_deg_list_group : an azimuth array or a list of azimuth lists corresponding to each TOD in TOD_group.
|
|
324
|
+
e.g. [azimuth_deg_list_1, azimuth_deg_list_2, ...]
|
|
325
|
+
|
|
326
|
+
elevation_deg_list_group : an elevation array or a list of elevation lists corresponding to each TOD in TOD_group.
|
|
327
|
+
e.g. [elevation_deg_list_1, elevation_deg_list_2, ...]
|
|
328
|
+
|
|
329
|
+
threshold : float
|
|
330
|
+
The threshold to cut off the fractional beam response np.abs(beam[pixel])/beam_max, default is 0.01.
|
|
331
|
+
e.g., if threshold=0.01, only pixels with beam response larger than 1% of the maximum will be considered.
|
|
332
|
+
Note that this is the threshold for singling out pixels.
|
|
333
|
+
|
|
334
|
+
Tsys_others_operator_group : an array or a list of arrays, optional
|
|
335
|
+
The operator for other system temperature components (e.g., Trec and Tdiode) mapping to TOD.
|
|
336
|
+
|
|
337
|
+
nside_hires : int, optional
|
|
338
|
+
If provided, upgrade the beam map to this nside before processing.
|
|
339
|
+
This can help improve accuracy when the beam is narrow.
|
|
340
|
+
|
|
341
|
+
beam_truncate_frac_thres : float, optional
|
|
342
|
+
The fractional threshold value for beam truncation.
|
|
343
|
+
If specified, set all pixels with values below this fraction of the maximum pixel value to zero.
|
|
344
|
+
If None, use the other key word "threshold" as the value.
|
|
345
|
+
|
|
346
|
+
Note the difference between "threshold" and "beam_truncate_frac_thres":
|
|
347
|
+
"threshold" is used to determine which pixels to include in the mapmaking process based on their beam response.
|
|
348
|
+
"beam_truncate_frac_thres" is used to truncate the beam map itself before processing.
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
self.nside_hires = nside_hires
|
|
352
|
+
self.nside_target = nside_target
|
|
353
|
+
|
|
354
|
+
if beam_truncate_frac_thres is None:
|
|
355
|
+
beam_truncate_frac_thres = threshold
|
|
356
|
+
|
|
357
|
+
# If LST_deg_list_group[0] is a list, flatten it.
|
|
358
|
+
if isinstance(LST_deg_list_group[0], (list, np.ndarray)):
|
|
359
|
+
self.num_tods = len(LST_deg_list_group)
|
|
360
|
+
if not (self.num_tods == len(azimuth_deg_list_group) == len(elevation_deg_list_group)):
|
|
361
|
+
raise ValueError(
|
|
362
|
+
"Length of LST_deg_list_group, azimuth_deg_list_group, "
|
|
363
|
+
"elevation_deg_list_group must be the same."
|
|
364
|
+
)
|
|
365
|
+
LST_deg_list = np.concatenate(LST_deg_list_group)
|
|
366
|
+
azimuth_deg_list = np.concatenate(azimuth_deg_list_group)
|
|
367
|
+
elevation_deg_list = np.concatenate(elevation_deg_list_group)
|
|
368
|
+
if selfrot_deg_list_group is not None:
|
|
369
|
+
selfrot_deg_list = np.concatenate(selfrot_deg_list_group)
|
|
370
|
+
else:
|
|
371
|
+
selfrot_deg_list = np.zeros_like(LST_deg_list)
|
|
372
|
+
else:
|
|
373
|
+
self.num_tods = 1
|
|
374
|
+
# In this branch the groups are flat per-sample arrays (their
|
|
375
|
+
# first element is a scalar); the casts only narrow the static
|
|
376
|
+
# Union type and are identity operations at runtime.
|
|
377
|
+
LST_deg_list = cast(np.ndarray, LST_deg_list_group)
|
|
378
|
+
azimuth_deg_list = cast(np.ndarray, azimuth_deg_list_group)
|
|
379
|
+
elevation_deg_list = cast(np.ndarray, elevation_deg_list_group)
|
|
380
|
+
if selfrot_deg_list_group is not None:
|
|
381
|
+
selfrot_deg_list = cast(np.ndarray, selfrot_deg_list_group)
|
|
382
|
+
else:
|
|
383
|
+
selfrot_deg_list = np.zeros_like(LST_deg_list)
|
|
384
|
+
|
|
385
|
+
if beam_map.ndim == 1:
|
|
386
|
+
self.npol = 1
|
|
387
|
+
elif beam_map.ndim == 2:
|
|
388
|
+
self.npol = beam_map.shape[0]
|
|
389
|
+
else:
|
|
390
|
+
raise ValueError("beam_map must be a 1D or 2D array.")
|
|
391
|
+
|
|
392
|
+
if Tsys_others_operator_group is not None:
|
|
393
|
+
# Canonicalize to a list of per-TOD 2D operators: the docstring
|
|
394
|
+
# accepts "an array or a list of arrays", but a bare 2D array
|
|
395
|
+
# used to crash ([0] indexed a row) and the single-TOD
|
|
396
|
+
# concatenation mishandled the list form.
|
|
397
|
+
if isinstance(Tsys_others_operator_group, np.ndarray) and Tsys_others_operator_group.ndim == 2:
|
|
398
|
+
Tsys_others_operator_group = [Tsys_others_operator_group]
|
|
399
|
+
if len(Tsys_others_operator_group) != self.num_tods:
|
|
400
|
+
raise ValueError(
|
|
401
|
+
f"Tsys_others_operator_group has {len(Tsys_others_operator_group)} "
|
|
402
|
+
f"entries but there are {self.num_tods} TODs."
|
|
403
|
+
)
|
|
404
|
+
self.Tsys_others = True
|
|
405
|
+
self.n_params_others = Tsys_others_operator_group[0].shape[1]
|
|
406
|
+
else:
|
|
407
|
+
self.Tsys_others = False
|
|
408
|
+
self.n_params_others = 0
|
|
409
|
+
|
|
410
|
+
horizontal_mask = None # Not used in current implementation, but can be added as a feature later if needed.
|
|
411
|
+
|
|
412
|
+
self.pixel_indices = truncate_stacked_beam(
|
|
413
|
+
beam_map, LST_deg_list, lat_deg, azimuth_deg_list, elevation_deg_list, selfrot_deg_list,
|
|
414
|
+
horizontal_mask=horizontal_mask,
|
|
415
|
+
threshold=threshold,
|
|
416
|
+
nside_hires=self.nside_hires,
|
|
417
|
+
nside_target=self.nside_target
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
self.num_pixels = len(self.pixel_indices)
|
|
421
|
+
self.nsky_params = self.npol * self.num_pixels
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if self.num_tods > 1:
|
|
425
|
+
|
|
426
|
+
# List[np.ndarray] here (one operator per TOD); a single stacked
|
|
427
|
+
# ndarray in the num_tods == 1 branch below — hence Any.
|
|
428
|
+
self.Tsys_operators: Any = []
|
|
429
|
+
|
|
430
|
+
for i in range(self.num_tods):
|
|
431
|
+
LST_deg_list_i = LST_deg_list_group[i]
|
|
432
|
+
azimuth_deg_list_i = azimuth_deg_list_group[i]
|
|
433
|
+
elevation_deg_list_i = elevation_deg_list_group[i]
|
|
434
|
+
|
|
435
|
+
selfrot_deg_list = np.zeros_like(LST_deg_list_i) if selfrot_deg_list_group is None else selfrot_deg_list_group[i]
|
|
436
|
+
|
|
437
|
+
sky_operator_i = generate_sky2sys_projection(
|
|
438
|
+
beam_map, LST_deg_list_i, lat_deg, azimuth_deg_list_i, elevation_deg_list_i, selfrot_deg_list,
|
|
439
|
+
self.pixel_indices,
|
|
440
|
+
nside_hires=self.nside_hires,
|
|
441
|
+
nside_target=self.nside_target,
|
|
442
|
+
normalize_beam=False,
|
|
443
|
+
horizontal_mask=horizontal_mask,
|
|
444
|
+
truncate_frac_thres=beam_truncate_frac_thres
|
|
445
|
+
)
|
|
446
|
+
if Tsys_others_operator_group is not None:
|
|
447
|
+
other_operators = [np.zeros_like(item) for item in Tsys_others_operator_group]
|
|
448
|
+
other_operators[i] = Tsys_others_operator_group[i]
|
|
449
|
+
Tsys_operator_i = np.concatenate([sky_operator_i] + other_operators, axis=1)
|
|
450
|
+
else:
|
|
451
|
+
Tsys_operator_i = sky_operator_i
|
|
452
|
+
|
|
453
|
+
self.Tsys_operators.append(Tsys_operator_i)
|
|
454
|
+
|
|
455
|
+
else:
|
|
456
|
+
sky_operators = generate_sky2sys_projection(
|
|
457
|
+
beam_map, LST_deg_list, lat_deg, azimuth_deg_list, elevation_deg_list, selfrot_deg_list,
|
|
458
|
+
self.pixel_indices,
|
|
459
|
+
horizontal_mask=horizontal_mask,
|
|
460
|
+
normalize_beam=False,
|
|
461
|
+
nside_hires=self.nside_hires,
|
|
462
|
+
nside_target=self.nside_target,
|
|
463
|
+
truncate_frac_thres=beam_truncate_frac_thres
|
|
464
|
+
)
|
|
465
|
+
if Tsys_others_operator_group is not None:
|
|
466
|
+
self.Tsys_operators = np.concatenate(
|
|
467
|
+
[sky_operators] + list(Tsys_others_operator_group), axis=1
|
|
468
|
+
)
|
|
469
|
+
else:
|
|
470
|
+
self.Tsys_operators = sky_operators
|
|
471
|
+
|
|
472
|
+
def _filter_and_stack(
|
|
473
|
+
self,
|
|
474
|
+
TOD_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
475
|
+
dtime: float,
|
|
476
|
+
cutoff_freq_group: Optional[Union[float, Sequence[float]]],
|
|
477
|
+
gain_group: Union[float, np.ndarray, Sequence[Union[float, np.ndarray]]],
|
|
478
|
+
known_injection_group: Optional[Union[np.ndarray, Sequence[np.ndarray]]],
|
|
479
|
+
filter_order: int,
|
|
480
|
+
preserve_dc: bool,
|
|
481
|
+
use_high_pass: bool,
|
|
482
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
483
|
+
"""Calibrate, optionally high-pass filter, and stack every TOD and
|
|
484
|
+
its system operator into one overall linear system.
|
|
485
|
+
|
|
486
|
+
Returns ``(HP_cal_TOD_overall, HP_Tsys_operator_overall)`` and
|
|
487
|
+
records the per-TOD filter matrices in ``self.HP_exact``.
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def make_tod_filter(n_samples: int, cutoff_freq: Optional[float]) -> np.ndarray:
|
|
491
|
+
if not use_high_pass:
|
|
492
|
+
return np.eye(n_samples)
|
|
493
|
+
if cutoff_freq is None:
|
|
494
|
+
raise ValueError("cutoff_freq_group must be provided when use_high_pass=True.")
|
|
495
|
+
return HP_filter_TOD(
|
|
496
|
+
n_samples,
|
|
497
|
+
dtime,
|
|
498
|
+
cutoff_freq=cutoff_freq,
|
|
499
|
+
filter_order=filter_order,
|
|
500
|
+
preserve_dc=preserve_dc,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
self.HP_exact: List[np.ndarray] = []
|
|
504
|
+
if self.num_tods > 1:
|
|
505
|
+
|
|
506
|
+
for i in range(self.num_tods):
|
|
507
|
+
# The multi-TOD path requires per-TOD sequences; the casts
|
|
508
|
+
# narrow the scalar-or-sequence Unions (identity at runtime).
|
|
509
|
+
cutoff_freq = None if cutoff_freq_group is None else cast(Sequence[float], cutoff_freq_group)[i]
|
|
510
|
+
hp_filter_mat = make_tod_filter(len(TOD_group[i]), cutoff_freq)
|
|
511
|
+
self.HP_exact.append(hp_filter_mat)
|
|
512
|
+
calibrated_TOD_i = np.asarray(TOD_group[i]) / cast(Sequence[Any], gain_group)[i]
|
|
513
|
+
if known_injection_group is not None:
|
|
514
|
+
calibrated_TOD_i -= known_injection_group[i]
|
|
515
|
+
hp_cal_TOD_i = hp_filter_mat @ calibrated_TOD_i
|
|
516
|
+
hp_Tsys_operator_i = hp_filter_mat @ self.Tsys_operators[i]
|
|
517
|
+
|
|
518
|
+
if i == 0:
|
|
519
|
+
HP_Tsys_operator_overall = hp_Tsys_operator_i
|
|
520
|
+
HP_cal_TOD_overall = hp_cal_TOD_i
|
|
521
|
+
else:
|
|
522
|
+
HP_Tsys_operator_overall = np.concatenate([HP_Tsys_operator_overall, hp_Tsys_operator_i])
|
|
523
|
+
HP_cal_TOD_overall = np.concatenate([HP_cal_TOD_overall, hp_cal_TOD_i])
|
|
524
|
+
|
|
525
|
+
elif self.num_tods == 1:
|
|
526
|
+
TOD = TOD_group if isinstance(TOD_group, np.ndarray) and TOD_group.ndim == 1 else TOD_group[0]
|
|
527
|
+
cutoff_freq = cutoff_freq_group if isinstance(cutoff_freq_group, (int, float)) else (
|
|
528
|
+
None if cutoff_freq_group is None else cutoff_freq_group[0]
|
|
529
|
+
)
|
|
530
|
+
gain = gain_group if isinstance(gain_group, (int, float)) else gain_group[0]
|
|
531
|
+
calibrated_TOD = np.asarray(TOD) / gain
|
|
532
|
+
if known_injection_group is not None:
|
|
533
|
+
known_injection = known_injection_group if isinstance(known_injection_group, np.ndarray) and known_injection_group.ndim == 1 else known_injection_group[0]
|
|
534
|
+
calibrated_TOD -= known_injection
|
|
535
|
+
hp_filter_mat = make_tod_filter(len(TOD), cutoff_freq)
|
|
536
|
+
HP_cal_TOD_overall = hp_filter_mat @ calibrated_TOD
|
|
537
|
+
HP_Tsys_operator_overall = hp_filter_mat @ self.Tsys_operators
|
|
538
|
+
|
|
539
|
+
return HP_cal_TOD_overall, HP_Tsys_operator_overall
|
|
540
|
+
|
|
541
|
+
def _build_priors(
|
|
542
|
+
self,
|
|
543
|
+
Tsky_prior_mean: Optional[np.ndarray],
|
|
544
|
+
Tsys_other_prior_mean_group: Optional[Sequence[np.ndarray]],
|
|
545
|
+
Tsky_prior_inv_cov_diag: Optional[np.ndarray],
|
|
546
|
+
Tsys_other_prior_inv_cov_group: Optional[Sequence[np.ndarray]],
|
|
547
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
548
|
+
"""Assemble the joint prior mean vector and prior inverse-covariance
|
|
549
|
+
matrix over (sky pixels + other-Tsys parameters). Requires
|
|
550
|
+
``self.nparams`` to be set."""
|
|
551
|
+
Tsys_prior_mean = np.zeros(self.nparams)
|
|
552
|
+
if Tsky_prior_mean is not None:
|
|
553
|
+
if len(Tsky_prior_mean) != self.nsky_params:
|
|
554
|
+
raise ValueError("Length of Tsky_prior_mean must match number of sky parameters.")
|
|
555
|
+
Tsys_prior_mean[:self.nsky_params] = Tsky_prior_mean
|
|
556
|
+
counter = self.nsky_params
|
|
557
|
+
if Tsys_other_prior_mean_group is not None:
|
|
558
|
+
if not self.Tsys_others:
|
|
559
|
+
raise ValueError("Tsys_others_operator must be provided in initialization if Tsys_other_prior_mean_group is provided.")
|
|
560
|
+
if len(Tsys_other_prior_mean_group) != self.num_tods:
|
|
561
|
+
raise ValueError("Length of Tsys_other_prior_mean_group must match number of TODs.")
|
|
562
|
+
for Tsys_other_prior_mean_i in Tsys_other_prior_mean_group:
|
|
563
|
+
Tsys_prior_mean[counter:counter+len(Tsys_other_prior_mean_i)] = Tsys_other_prior_mean_i
|
|
564
|
+
counter += len(Tsys_other_prior_mean_i)
|
|
565
|
+
|
|
566
|
+
Tsys_prior_inv_cov = np.zeros((self.nparams, self.nparams))
|
|
567
|
+
if Tsky_prior_inv_cov_diag is not None:
|
|
568
|
+
Tsky_prior_inv_cov_diag = np.asarray(Tsky_prior_inv_cov_diag).reshape(-1) # flatten
|
|
569
|
+
if len(Tsky_prior_inv_cov_diag) != self.nsky_params:
|
|
570
|
+
raise ValueError("Length of Tsky_prior_inv_cov_diag must match number of sky parameters.")
|
|
571
|
+
Tsys_prior_inv_cov[:self.nsky_params, :self.nsky_params] = np.diag(Tsky_prior_inv_cov_diag)
|
|
572
|
+
|
|
573
|
+
counter = self.nsky_params
|
|
574
|
+
if Tsys_other_prior_inv_cov_group is not None:
|
|
575
|
+
if not self.Tsys_others:
|
|
576
|
+
raise ValueError("Tsys_others_operator must be provided in initialization if Tsys_other_prior_inv_cov_group is provided.")
|
|
577
|
+
if len(Tsys_other_prior_inv_cov_group) != self.num_tods:
|
|
578
|
+
raise ValueError("Length of Tsys_other_prior_inv_cov_group must match number of TODs.")
|
|
579
|
+
|
|
580
|
+
for Tsys_other_prior_inv_cov_i in Tsys_other_prior_inv_cov_group:
|
|
581
|
+
# Branch on THIS element's ndim: checking group[0] here used to
|
|
582
|
+
# misroute mixed 1D/2D groups, silently np.diag-ing a 2D
|
|
583
|
+
# covariance and discarding its off-diagonal entries.
|
|
584
|
+
Tsys_other_prior_inv_cov_i = np.asarray(Tsys_other_prior_inv_cov_i)
|
|
585
|
+
if Tsys_other_prior_inv_cov_i.ndim == 1:
|
|
586
|
+
n_others = len(Tsys_other_prior_inv_cov_i)
|
|
587
|
+
Tsys_prior_inv_cov[counter:counter+n_others, counter:counter+n_others] = np.diag(Tsys_other_prior_inv_cov_i)
|
|
588
|
+
counter += n_others
|
|
589
|
+
elif Tsys_other_prior_inv_cov_i.ndim == 2:
|
|
590
|
+
n_others = Tsys_other_prior_inv_cov_i.shape[0]
|
|
591
|
+
Tsys_prior_inv_cov[counter:counter+n_others, counter:counter+n_others] = Tsys_other_prior_inv_cov_i
|
|
592
|
+
counter += n_others
|
|
593
|
+
else:
|
|
594
|
+
raise ValueError("Each element in Tsys_other_prior_inv_cov_group must be a 1D or 2D array.")
|
|
595
|
+
|
|
596
|
+
return Tsys_prior_mean, Tsys_prior_inv_cov
|
|
597
|
+
|
|
598
|
+
def _normalize_noise_variance(
|
|
599
|
+
self,
|
|
600
|
+
noise_variance: Optional[Union[float, np.ndarray, List[Union[float, np.ndarray]], Tuple[Union[float, np.ndarray], ...]]],
|
|
601
|
+
TOD_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
602
|
+
n_overall: int,
|
|
603
|
+
) -> Optional[Union[float, np.ndarray]]:
|
|
604
|
+
"""Normalise per-TOD noise_variance into the 1D/scalar/None form
|
|
605
|
+
wiener_filter_map expects. Accepts None / scalar / 1D array /
|
|
606
|
+
list-of-(scalar|1D-array)."""
|
|
607
|
+
nv = noise_variance
|
|
608
|
+
if isinstance(nv, (list, tuple)):
|
|
609
|
+
if len(nv) != self.num_tods:
|
|
610
|
+
raise ValueError(
|
|
611
|
+
f"noise_variance list length {len(nv)} != num_tods {self.num_tods}"
|
|
612
|
+
)
|
|
613
|
+
tod_lengths = [len(TOD_group[i]) for i in range(self.num_tods)] \
|
|
614
|
+
if self.num_tods > 1 else [n_overall]
|
|
615
|
+
pieces: List[np.ndarray] = []
|
|
616
|
+
for i, nv_i in enumerate(nv):
|
|
617
|
+
if np.isscalar(nv_i):
|
|
618
|
+
pieces.append(np.full(tod_lengths[i], float(cast(SupportsFloat, nv_i))))
|
|
619
|
+
else:
|
|
620
|
+
nv_i = np.asarray(nv_i, dtype=float)
|
|
621
|
+
if nv_i.shape != (tod_lengths[i],):
|
|
622
|
+
raise ValueError(
|
|
623
|
+
f"noise_variance[{i}] shape {nv_i.shape} != ({tod_lengths[i]},)"
|
|
624
|
+
)
|
|
625
|
+
pieces.append(nv_i)
|
|
626
|
+
nv = np.concatenate(pieces)
|
|
627
|
+
return nv
|
|
628
|
+
|
|
629
|
+
def __call__(
|
|
630
|
+
self,
|
|
631
|
+
*,
|
|
632
|
+
TOD_group: Union[np.ndarray, Sequence[np.ndarray]],
|
|
633
|
+
dtime: float,
|
|
634
|
+
cutoff_freq_group: Optional[Union[float, Sequence[float]]] = None,
|
|
635
|
+
gain_group: Optional[Union[float, np.ndarray, Sequence[Union[float, np.ndarray]]]] = None,
|
|
636
|
+
known_injection_group: Optional[Union[np.ndarray, Sequence[np.ndarray]]] = None,
|
|
637
|
+
Tsky_prior_mean: Optional[np.ndarray] = None,
|
|
638
|
+
Tsky_prior_inv_cov_diag: Optional[np.ndarray] = None,
|
|
639
|
+
Tsys_other_prior_mean_group: Optional[Sequence[np.ndarray]] = None,
|
|
640
|
+
Tsys_other_prior_inv_cov_group: Optional[Sequence[np.ndarray]] = None,
|
|
641
|
+
noise_variance: Optional[Union[float, np.ndarray, List[Union[float, np.ndarray]], Tuple[Union[float, np.ndarray], ...]]] = None,
|
|
642
|
+
regularization: float = 1e-12,
|
|
643
|
+
return_full_cov: bool = False,
|
|
644
|
+
filter_order: int = 4,
|
|
645
|
+
preserve_dc: bool = False,
|
|
646
|
+
use_high_pass: bool = False,
|
|
647
|
+
) -> Union[
|
|
648
|
+
Tuple[np.ndarray, np.ndarray],
|
|
649
|
+
Tuple[np.ndarray, np.ndarray, List[np.ndarray], List[np.ndarray]],
|
|
650
|
+
]:
|
|
651
|
+
"""
|
|
652
|
+
TOD_group : a TOD array or a list of TOD arrays at the same frequency channel.
|
|
653
|
+
e.g. [TOD_1, TOD_2, ...]
|
|
654
|
+
|
|
655
|
+
gain_group : a gain array or a list of gain arrays corresponding to each TOD in TOD_group.
|
|
656
|
+
e.g. [gain_1, gain_2, ...]
|
|
657
|
+
gain_i can be a single value (constant gain) or an array with the same length as TOD_i.
|
|
658
|
+
If None, assumed to be 1. (i.e., TOD is already calibrated)
|
|
659
|
+
|
|
660
|
+
dtime : float
|
|
661
|
+
Time interval between samples in seconds.
|
|
662
|
+
|
|
663
|
+
cutoff_freq_group : list of float, optional
|
|
664
|
+
Cutoff frequency for high-pass filter in Hz. Required when
|
|
665
|
+
use_high_pass is True; ignored when use_high_pass is False.
|
|
666
|
+
|
|
667
|
+
known_injection_group : a list of known system temperature components to be subtracted from Tsys (calibrated TOD), each element corresponding to each TOD in TOD_group.
|
|
668
|
+
e.g. [mu_1, mu_2, ...]
|
|
669
|
+
A concrete example in MeerKLASS, mu_i can be time sequence of constant noise diode temperature, if we do not take it as a parameter.
|
|
670
|
+
|
|
671
|
+
Tsky_prior_mean : array, optional
|
|
672
|
+
Prior mean for the sky temperature map, the shape is (npol, num_pixels) for multi-polarization maps, or (num_pixels,) for single polarization map.
|
|
673
|
+
If None, assumed to be zero.
|
|
674
|
+
|
|
675
|
+
Tsky_prior_inv_cov_diag : array, optional
|
|
676
|
+
Diagonal of the prior inverse covariance for the sky temperature map, the shape can be:
|
|
677
|
+
(num_pixels,) : single polarization map.
|
|
678
|
+
(npol, num_pixels) : multi-polarization map.
|
|
679
|
+
If None, assumed to be uninformative prior (zero, i.e., infinite prior variance).
|
|
680
|
+
|
|
681
|
+
Tsys_other_prior_mean_group : a list of prior means for other system temperature components, each element corresponding to each TOD in TOD_group.
|
|
682
|
+
e.g. [Tsys_other_prior_mean_1, Tsys_other_prior_mean_2, ...]
|
|
683
|
+
If None, assumed to be zero.
|
|
684
|
+
|
|
685
|
+
noise_variance : float, 1D array, or list of (float | 1D array), optional
|
|
686
|
+
Per-sample noise variance. Three forms accepted:
|
|
687
|
+
* None (default): auto-estimate from the residual TOD - operator @ pinv(operator) @ TOD
|
|
688
|
+
via a 100-sample rolling window. NOTE: this estimate can be heavily biased
|
|
689
|
+
low when the operator does not span the projectable signal subspace —
|
|
690
|
+
it conflates un-projectable signal with noise, which mis-weights data
|
|
691
|
+
in the Wiener filter. Provide an explicit value when possible.
|
|
692
|
+
* scalar: uniform variance applied to every concatenated sample.
|
|
693
|
+
* 1D array of length sum(len(TOD_i)): per-sample variances over the
|
|
694
|
+
concatenated TOD ordering.
|
|
695
|
+
* list of length num_tods, each entry a scalar or 1D array of length
|
|
696
|
+
len(TOD_i): per-TOD variance, concatenated internally.
|
|
697
|
+
|
|
698
|
+
Tsys_other_prior_inv_cov_group : a list of prior inverse covariances for other system temperature components, each element corresponding to each TOD in TOD_group.
|
|
699
|
+
e.g. [Tsys_other_prior_inv_cov_1, Tsys_other_prior_inv_cov_2, ...]
|
|
700
|
+
The shape of each element can be:
|
|
701
|
+
(num_other_params,) : Diagonal of the inverse covariance matrix.
|
|
702
|
+
(num_other_params, num_other_params) : Full inverse covariance matrix.
|
|
703
|
+
But all elements must have the same shape.
|
|
704
|
+
If None, assumed to be uninformative prior (zero, i.e., infinite prior variance).
|
|
705
|
+
|
|
706
|
+
use_high_pass : bool, default=False
|
|
707
|
+
If True, apply the Butterworth high-pass filter to the TOD and
|
|
708
|
+
system operator. If False, use the identity matrix and solve the
|
|
709
|
+
unfiltered map-making problem.
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
--------
|
|
714
|
+
sky_estimation : array, the shape is (npol, num_pixels) for multi-polarization maps, or (num_pixels,) for single polarization map.
|
|
715
|
+
Reconstructed sky map(s).
|
|
716
|
+
sky_uncertainty : array, the shape is (npol, num_pixels) for multi-polarization maps, or (num_pixels,) for single polarization map.
|
|
717
|
+
Uncertainty map(s) (diagonal of covariance matrix).
|
|
718
|
+
Tsys_others_estimation_group : list of arrays, each with shape (num_other_params,)
|
|
719
|
+
Reconstructed other system temperature components for each TOD, only returned if Tsys_others_operator is provided.
|
|
720
|
+
Tsys_others_uncertainty_group : list of arrays, each with shape (num_other_params,)
|
|
721
|
+
Per-parameter uncertainty (diagonal of covariance matrix) for other system temperature components, only returned if Tsys_others_operator is provided.
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
if gain_group is None:
|
|
725
|
+
gain_group = [1.0]*self.num_tods
|
|
726
|
+
|
|
727
|
+
HP_cal_TOD_overall, HP_Tsys_operator_overall = self._filter_and_stack(
|
|
728
|
+
TOD_group, dtime, cutoff_freq_group, gain_group,
|
|
729
|
+
known_injection_group, filter_order, preserve_dc, use_high_pass,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# # Debug: print the shape of the overall operator
|
|
733
|
+
self.nparams = HP_Tsys_operator_overall.shape[1]
|
|
734
|
+
|
|
735
|
+
Tsys_prior_mean, Tsys_prior_inv_cov = self._build_priors(
|
|
736
|
+
Tsky_prior_mean, Tsys_other_prior_mean_group,
|
|
737
|
+
Tsky_prior_inv_cov_diag, Tsys_other_prior_inv_cov_group,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
nv = self._normalize_noise_variance(
|
|
741
|
+
noise_variance, TOD_group, len(HP_cal_TOD_overall)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
# Apply Wiener filter with the overall operator
|
|
745
|
+
result = wiener_filter_map(
|
|
746
|
+
HP_cal_TOD_overall,
|
|
747
|
+
HP_Tsys_operator_overall,
|
|
748
|
+
noise_variance=nv, # explicit if provided, else auto-estimated inside
|
|
749
|
+
prior_inv_cov=Tsys_prior_inv_cov,
|
|
750
|
+
guess=Tsys_prior_mean,
|
|
751
|
+
regularization=regularization,
|
|
752
|
+
return_full_cov=return_full_cov,
|
|
753
|
+
)
|
|
754
|
+
# wiener_filter_map returns a 3-tuple when return_full_cov=True; the
|
|
755
|
+
# posterior covariance is appended to this method's return values
|
|
756
|
+
# (a 2-name unpacking here used to crash on that flag).
|
|
757
|
+
if return_full_cov:
|
|
758
|
+
estmation, uncertainty, posterior_cov = cast(
|
|
759
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray], result
|
|
760
|
+
)
|
|
761
|
+
else:
|
|
762
|
+
estmation, uncertainty = cast(Tuple[np.ndarray, np.ndarray], result)
|
|
763
|
+
posterior_cov = None
|
|
764
|
+
|
|
765
|
+
sky_estimation = estmation[:self.nsky_params]
|
|
766
|
+
sky_uncertainty = uncertainty[:self.nsky_params]
|
|
767
|
+
|
|
768
|
+
if self.npol > 1:
|
|
769
|
+
sky_estimation = sky_estimation.reshape(self.npol, self.num_pixels)
|
|
770
|
+
sky_uncertainty = sky_uncertainty.reshape(self.npol, self.num_pixels)
|
|
771
|
+
|
|
772
|
+
outputs: Tuple[Any, ...] = (sky_estimation, sky_uncertainty)
|
|
773
|
+
if self.Tsys_others:
|
|
774
|
+
Tsys_others_estimation_group = []
|
|
775
|
+
Tsys_others_uncertainty_group = []
|
|
776
|
+
counter = self.nsky_params
|
|
777
|
+
for i in range(self.num_tods):
|
|
778
|
+
Tsys_others_estimation_group.append(estmation[counter:counter+self.n_params_others])
|
|
779
|
+
Tsys_others_uncertainty_group.append(uncertainty[counter:counter+self.n_params_others])
|
|
780
|
+
counter += self.n_params_others
|
|
781
|
+
outputs = outputs + (Tsys_others_estimation_group, Tsys_others_uncertainty_group)
|
|
782
|
+
if return_full_cov:
|
|
783
|
+
outputs = outputs + (posterior_cov,)
|
|
784
|
+
return outputs
|
|
785
|
+
|