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