foscat 2025.6.3__py3-none-any.whl → 2025.7.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.
- foscat/BkBase.py +3 -0
- foscat/BkNumpy.py +5 -0
- foscat/BkTensorflow.py +5 -0
- foscat/BkTorch.py +5 -0
- foscat/FoCUS.py +379 -237
- foscat/HealSpline.py +211 -0
- foscat/scat_cov.py +289 -133
- {foscat-2025.6.3.dist-info → foscat-2025.7.1.dist-info}/METADATA +1 -1
- {foscat-2025.6.3.dist-info → foscat-2025.7.1.dist-info}/RECORD +12 -11
- {foscat-2025.6.3.dist-info → foscat-2025.7.1.dist-info}/WHEEL +0 -0
- {foscat-2025.6.3.dist-info → foscat-2025.7.1.dist-info}/licenses/LICENSE +0 -0
- {foscat-2025.6.3.dist-info → foscat-2025.7.1.dist-info}/top_level.txt +0 -0
foscat/HealSpline.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from scipy.interpolate import interp1d
|
|
2
|
+
import foscat.CircSpline as sc
|
|
3
|
+
import foscat.Spline1D as sc1d
|
|
4
|
+
import numpy as np
|
|
5
|
+
import healpy as hp
|
|
6
|
+
|
|
7
|
+
class heal_spline:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
level):
|
|
11
|
+
nside=2**level
|
|
12
|
+
self.nside_store=2**(level//2)
|
|
13
|
+
self.spline_tree={}
|
|
14
|
+
|
|
15
|
+
self.nside=nside
|
|
16
|
+
#compute colatitude
|
|
17
|
+
idx_th=np.zeros([4*nside],dtype='int')
|
|
18
|
+
n=0
|
|
19
|
+
d=0
|
|
20
|
+
for k in range(nside):
|
|
21
|
+
d+=4
|
|
22
|
+
idx_th[k]=n
|
|
23
|
+
n+=d
|
|
24
|
+
|
|
25
|
+
for k in range(2*nside-1):
|
|
26
|
+
idx_th[k+nside]=n
|
|
27
|
+
n+=d
|
|
28
|
+
|
|
29
|
+
for k in range(nside):
|
|
30
|
+
idx_th[k+3*nside-1]=n
|
|
31
|
+
n+=d
|
|
32
|
+
d-=4
|
|
33
|
+
idx_th[-1]=12*nside**2
|
|
34
|
+
|
|
35
|
+
th0_val,ph0_val=hp.pix2ang(self.nside,idx_th[:-1])
|
|
36
|
+
self.th0_val=th0_val
|
|
37
|
+
self.ph0_val=ph0_val
|
|
38
|
+
|
|
39
|
+
self.idx_th=idx_th
|
|
40
|
+
|
|
41
|
+
#init spline table
|
|
42
|
+
|
|
43
|
+
self.spline_lat=sc1d.Spline1D(4*self.nside-1,3)
|
|
44
|
+
|
|
45
|
+
#convert colatitude in ring index
|
|
46
|
+
self.f_interp_th = interp1d(np.concatenate([[0],(th0_val[:-1]+th0_val[1:])/2,[np.pi]],0),
|
|
47
|
+
np.arange(4*self.nside)/(4*self.nside),
|
|
48
|
+
kind='cubic', fill_value='extrapolate')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ang2weigths(self,th,ph,threshold=1E-2,nest=True):
|
|
52
|
+
th0=self.f_interp_th(th).flatten()
|
|
53
|
+
|
|
54
|
+
idx_lat,w_th=self.spline_lat.eval(th0.flatten())
|
|
55
|
+
|
|
56
|
+
www = np.zeros([4,4,th0.shape[0]])
|
|
57
|
+
all_idx = np.zeros([4,4,th0.shape[0]],dtype='int')
|
|
58
|
+
|
|
59
|
+
iring_tab=np.unique(idx_lat)
|
|
60
|
+
for iring in iring_tab:
|
|
61
|
+
spline_table=sc.CircSpline(self.idx_th[iring+1]-self.idx_th[iring],3)
|
|
62
|
+
for k in range(4):
|
|
63
|
+
iii=np.where(idx_lat[k]==iring)[0]
|
|
64
|
+
idx,w=spline_table.eval((ph[iii]-self.ph0_val[iring])/(2*np.pi))
|
|
65
|
+
idx=idx+self.idx_th[iring]
|
|
66
|
+
for m in range(4):
|
|
67
|
+
www[k,m,iii]=w[m]*w_th[k,iii]
|
|
68
|
+
all_idx[k,m,iii]=idx[m]
|
|
69
|
+
|
|
70
|
+
www=www.reshape(16,www.shape[2])
|
|
71
|
+
all_idx=all_idx.reshape(16,all_idx.shape[2])
|
|
72
|
+
|
|
73
|
+
heal_idx,inv_idx = np.unique(all_idx,
|
|
74
|
+
return_inverse=True)
|
|
75
|
+
all_idx = inv_idx
|
|
76
|
+
if nest:
|
|
77
|
+
heal_idx = hp.ring2nest(self.nside,heal_idx)
|
|
78
|
+
self.cell_ids = heal_idx
|
|
79
|
+
|
|
80
|
+
hit=np.bincount(all_idx.flatten(),weights=www.flatten())
|
|
81
|
+
www[hit[all_idx]<threshold]=0.0
|
|
82
|
+
www=www/np.sum(www,0)[None,:]
|
|
83
|
+
return www,all_idx,heal_idx
|
|
84
|
+
|
|
85
|
+
def P(self,x,www,all_idx):
|
|
86
|
+
return np.sum(www*x[all_idx],0)
|
|
87
|
+
|
|
88
|
+
#PT(y) must return a 1D NumPy array of shape (N,)
|
|
89
|
+
def PT(self,y,www,all_idx,hit):
|
|
90
|
+
value=np.bincount(all_idx.flatten(),weights=(www*y[None,:]).flatten())
|
|
91
|
+
return value*hit
|
|
92
|
+
|
|
93
|
+
# the data is of dimension M
|
|
94
|
+
# the x is of dimension N=12*nside**2
|
|
95
|
+
|
|
96
|
+
def conjugate_gradient_normal_equation(self,data, x0, www, all_idx, max_iter=100, tol=1e-8, verbose=True):
|
|
97
|
+
"""
|
|
98
|
+
Solve (PᵗP)x = Pᵗy using explicit Conjugate Gradient without scipy.cg.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
----------
|
|
102
|
+
P : function(x) → forward operator (ℝⁿ → ℝᵐ)
|
|
103
|
+
PT : function(y) → adjoint operator (ℝᵐ → ℝⁿ)
|
|
104
|
+
data : array_like, observed data y ∈ ℝᵐ
|
|
105
|
+
x0 : array_like, initial guess for x ∈ ℝⁿ
|
|
106
|
+
max_iter: maximum number of iterations
|
|
107
|
+
tol : convergence tolerance on relative residual
|
|
108
|
+
verbose : if True, print convergence info
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
-------
|
|
112
|
+
x : estimated solution ∈ ℝⁿ
|
|
113
|
+
"""
|
|
114
|
+
x = x0.copy()
|
|
115
|
+
|
|
116
|
+
hit=np.bincount(all_idx.flatten(),weights=www.flatten())
|
|
117
|
+
hit[hit>0]=1/hit[hit>0]
|
|
118
|
+
|
|
119
|
+
# Compute b = Pᵗ y # This part could be distributed easily
|
|
120
|
+
b = self.PT(data,www,all_idx,hit)
|
|
121
|
+
|
|
122
|
+
# Compute initial residual r = b - A x = b - Pᵗ P x
|
|
123
|
+
Ax = self.PT(self.P(x,www,all_idx),www,all_idx,hit)# This part could be distributed easily
|
|
124
|
+
r = b - Ax
|
|
125
|
+
|
|
126
|
+
# Initial direction
|
|
127
|
+
p = r.copy()
|
|
128
|
+
rs_old = np.dot(r, r)
|
|
129
|
+
|
|
130
|
+
for i in range(max_iter):
|
|
131
|
+
# Apply A p = Pᵗ P p
|
|
132
|
+
Ap = self.PT(self.P(p,www,all_idx),www,all_idx,hit)# This part could be distributed easily
|
|
133
|
+
|
|
134
|
+
alpha = rs_old / np.dot(p, Ap)
|
|
135
|
+
x += alpha * p
|
|
136
|
+
r -= alpha * Ap
|
|
137
|
+
|
|
138
|
+
rs_new = np.dot(r, r)
|
|
139
|
+
|
|
140
|
+
if verbose and i%50==0:
|
|
141
|
+
print(f"Iter {i:03d}: residual = {np.sqrt(rs_new):.3e}")
|
|
142
|
+
|
|
143
|
+
if np.sqrt(rs_new) < tol:
|
|
144
|
+
if verbose:
|
|
145
|
+
print(f"Converged. Iter {i:03d}: residual = {np.sqrt(rs_new):.3e}")
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
p = r + (rs_new / rs_old) * p
|
|
149
|
+
rs_old = rs_new
|
|
150
|
+
|
|
151
|
+
return x
|
|
152
|
+
|
|
153
|
+
def Fit(self,X,th,ph,threshold=1E-2,nest=True, max_iter=100, tol=1e-8):
|
|
154
|
+
|
|
155
|
+
www,all_idx,hidx=self.ang2weigths(th,ph,threshold=threshold,nest=nest)
|
|
156
|
+
|
|
157
|
+
self.heal_idx=hidx
|
|
158
|
+
self.spline=self.conjugate_gradient_normal_equation(X,
|
|
159
|
+
(self.heal_idx*0).astype('float'),
|
|
160
|
+
www,
|
|
161
|
+
all_idx
|
|
162
|
+
, max_iter=max_iter,
|
|
163
|
+
tol=tol
|
|
164
|
+
)
|
|
165
|
+
scale=(self.nside//self.nside_store)**2
|
|
166
|
+
h,ih=np.unique(hidx//scale,return_inverse=True)
|
|
167
|
+
for k in range(h.shape[0]):
|
|
168
|
+
spl=np.zeros([scale])
|
|
169
|
+
spl[hidx[ih==k]-scale*h[k]]=self.spline[ih==k]
|
|
170
|
+
self.spline_tree[h[k]]=spl
|
|
171
|
+
|
|
172
|
+
def SetParam(self,nside,spline,heal_idx):
|
|
173
|
+
|
|
174
|
+
self.heal_idx=heal_idx
|
|
175
|
+
self.nside=nside
|
|
176
|
+
self.spline=spline
|
|
177
|
+
self.level=int(np.log2(nside))
|
|
178
|
+
self.nside_store=2**(int(self.level//2))
|
|
179
|
+
|
|
180
|
+
self.spline_tree={}
|
|
181
|
+
|
|
182
|
+
scale=(self.nside//self.nside_store)**2
|
|
183
|
+
h,ih=np.unique(heal_idx//scale,return_inverse=True)
|
|
184
|
+
for k in range(h.shape[0]):
|
|
185
|
+
spl=np.zeros([scale])
|
|
186
|
+
spl[heal_idx[ih==k]-scale*h[k]]=self.spline[ih==k]
|
|
187
|
+
self.spline_tree[h[k]]=spl
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def Transform(self,th,ph,threshold=1E-2,nest=True):
|
|
192
|
+
|
|
193
|
+
www,all_idx,hidx=self.ang2weigths(th,ph,threshold=threshold,nest=nest)
|
|
194
|
+
|
|
195
|
+
x=np.zeros([hidx.shape[0]])
|
|
196
|
+
scale=(self.nside//self.nside_store)**2
|
|
197
|
+
h,ih=np.unique(hidx//scale,return_inverse=True)
|
|
198
|
+
for k in range(h.shape[0]):
|
|
199
|
+
if h[k] in self.spline_tree:
|
|
200
|
+
spl=self.spline_tree[h[k]]
|
|
201
|
+
x[ih==k]=spl[hidx[ih==k]-scale*h[k]]
|
|
202
|
+
data=self.P(x,www,all_idx)
|
|
203
|
+
return data
|
|
204
|
+
|
|
205
|
+
def FitTransform(self,X,th,ph,threshold=1E-2,nest=True):
|
|
206
|
+
|
|
207
|
+
self.Fit(X,th,ph)
|
|
208
|
+
|
|
209
|
+
t,p=hp.pix2ang(self.nside,self.heal_idx,nest=True)
|
|
210
|
+
|
|
211
|
+
return self.Transform(t,p)
|