tomoAO 0.0.1__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.
tomoao-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 cmcorreia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tomoao-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: tomoAO
3
+ Version: 0.0.1
4
+ Summary: Python package with atmospheric tomography generic functions and tools
5
+ Home-page: https://github.com/cmcorreia/PyTomo
6
+ Author: SpaceODT
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Natural Language :: English
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: aotools
16
+ Requires-Dist: numpy
17
+ Requires-Dist: astropy
18
+ Dynamic: author
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: summary
27
+
28
+ # tomoAO
29
+ Python package with atmospheric tomography generic functions and tools
30
+
31
+ ## Installation
32
+
33
+ 1. Install OOPAO
34
+ ```sh
35
+ pip install git+https://github.com/cheritier/OOPAO
36
+ ```
37
+
38
+ 2. Install cupy according to your cuda version (Optional)
39
+ ```sh
40
+ pip install cupy-cuda11x
41
+ ```
42
+ ```sh
43
+ pip install cupy-cuda12x
44
+ ```
45
+
46
+ 3. Install tomoAO via pip
47
+ ```sh
48
+ pip install tomoAO
49
+ ```
50
+
51
+ Or clone the repository
52
+ ```sh
53
+ git clone https://github.com/cmcorreia/PyTomo
54
+ pip install -e 'path to tomoAO'
55
+ ```
tomoao-0.0.1/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # tomoAO
2
+ Python package with atmospheric tomography generic functions and tools
3
+
4
+ ## Installation
5
+
6
+ 1. Install OOPAO
7
+ ```sh
8
+ pip install git+https://github.com/cheritier/OOPAO
9
+ ```
10
+
11
+ 2. Install cupy according to your cuda version (Optional)
12
+ ```sh
13
+ pip install cupy-cuda11x
14
+ ```
15
+ ```sh
16
+ pip install cupy-cuda12x
17
+ ```
18
+
19
+ 3. Install tomoAO via pip
20
+ ```sh
21
+ pip install tomoAO
22
+ ```
23
+
24
+ Or clone the repository
25
+ ```sh
26
+ git clone https://github.com/cmcorreia/PyTomo
27
+ pip install -e 'path to tomoAO'
28
+ ```
29
+
30
+
31
+
32
+
tomoao-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
tomoao-0.0.1/setup.py ADDED
@@ -0,0 +1,45 @@
1
+ import io
2
+ import os
3
+ from setuptools import setup, find_packages
4
+
5
+
6
+ def read(*paths, **kwargs):
7
+ """Read the contents of a text file safely.
8
+ # >>> read("project_name", "VERSION")
9
+ '0.1.0'
10
+ # >>> read("README.md")
11
+ # ...
12
+ """
13
+
14
+ content = ""
15
+ with io.open(
16
+ os.path.join(os.path.dirname(__file__), *paths),
17
+ encoding=kwargs.get("encoding", "utf8"),
18
+ ) as open_file:
19
+ content = open_file.read().strip()
20
+ return content
21
+
22
+
23
+ setup(
24
+ name = 'tomoAO',
25
+ version=open("tomoAO/version.py").read().split('=')[1].strip().strip('"'),
26
+ license = 'MIT',
27
+ author = 'SpaceODT',
28
+ description='Python package with atmospheric tomography generic functions and tools',
29
+ long_description=read("README.md"),
30
+ long_description_content_type='text/markdown',
31
+ packages = find_packages(),
32
+ url='https://github.com/cmcorreia/PyTomo',
33
+ install_requires = [
34
+ 'aotools',
35
+ 'numpy',
36
+ 'astropy'
37
+ ],
38
+ classifiers=[
39
+ 'Programming Language :: Python :: 3',
40
+ 'License :: OSI Approved :: MIT License',
41
+ 'Operating System :: OS Independent',
42
+ 'Intended Audience :: Science/Research',
43
+ 'Natural Language :: English',
44
+ ]
45
+ )
@@ -0,0 +1 @@
1
+ from .io_functions import *
@@ -0,0 +1,47 @@
1
+ import configparser
2
+ import os
3
+ import numpy as np
4
+ from astropy.io import fits
5
+
6
+
7
+ def load_from_ini(config_file, ao_mode=None, config_dir=os.path.dirname(__file__)):
8
+ # Instantiate configparser
9
+ config = configparser.ConfigParser()
10
+ config.optionxform = str
11
+ # Path of the configuration file is given as the "configdir" argument
12
+ # Read the configuration file
13
+ config.read(config_dir + config_file)
14
+
15
+ # Get the section of the config file containing the reconstructor
16
+ # parameters for the chosen AO_MODE
17
+ if ao_mode is None:
18
+ ao_mode = 'default'
19
+
20
+ parm = dict(config[ao_mode].items())
21
+ # breakpoint()
22
+ #print(parm)
23
+ # for key, value in parm.items():
24
+ # print(key, value)
25
+ # print(eval(value))
26
+ # print("----------------")
27
+ # breakpoint()
28
+ return {key: eval(value) for key, value in
29
+ parm.items()} # returns a dictionary with entries in specified data types
30
+
31
+
32
+ def open_influence_matrix(config, baseline_folder=None, filename=None):
33
+ if baseline_folder is None:
34
+ baseline_folder = config["path2imx"]
35
+
36
+ if filename is None:
37
+ filename = config["influence_matrix"]
38
+
39
+ #with fits.open(baseline_folder + "influence_matrix_5Mar.fits") as hdul:
40
+ with fits.open(baseline_folder+filename) as hdul:
41
+ hdul.verify('fix')
42
+ IM = hdul[0].data
43
+
44
+ # For now, concatenating n_channels times the same IM
45
+ n_channels = config["n_lgs"]
46
+
47
+ return np.array([IM] * n_channels)
@@ -0,0 +1 @@
1
+ from .recontructors import *
@@ -0,0 +1,297 @@
1
+ """
2
+ Created on Tue Apr 18 15:25:00 2023
3
+
4
+ @author: ccorreia@spaceodt.net
5
+ """
6
+
7
+
8
+ import tomoAO.tools.tomography_tools as tools
9
+
10
+
11
+ import numpy as np
12
+ cuda_available = False
13
+ try:
14
+ import cupy as cp
15
+ if cp.cuda.is_available():
16
+ cuda_available = True
17
+ except:
18
+ cuda_available = False
19
+
20
+
21
+ import matplotlib.pyplot as plt
22
+ from scipy.sparse import block_diag
23
+
24
+ from time import time
25
+
26
+
27
+
28
+
29
+
30
+
31
+ class tomoReconstructor:
32
+ def __init__(self, aoSys, weight_vector=None, alpha=None,
33
+ model='zonal', noise_covariance=None, lag=0,
34
+ weightOptimDir=-1, os=2, zernikeMode=None):
35
+ """Initialization function.
36
+
37
+ Instantiate a LinearMMSE reconstructor
38
+
39
+ :param tel: Telescope object [Telescope]
40
+ :param atmModel: atmosphere object [Atmosphere]
41
+ :param guideStar: a list of guide star objects [list of Source objects]
42
+ :param mmseStar: a list of target star objects [list of Source objects]
43
+ :param dm: deformable mirror object [Deformable Mirror]
44
+ :param outputRecGrid: a mask where the phase is to be reconstructed
45
+ :param validSubapMask: a multi-dim mask with the valid subapertures per WFS channel [bool]
46
+ :param model: whether 'zonal' (default) or modal [str]
47
+ :param noiseCovariance: the noise covariance matrix as a scalar [float] or a matrix [ndarray]
48
+ :param lag: the AO system lag that can be compensated through tomography [float]
49
+ :param weightOptimDir: a vector with the relative weights for each optimization direction [float]
50
+ :param os: the over-sampling factor [1, 2, 4] (default=2) to apply to the reconstructed phase w.r.t the inpup slopes-maps [int]
51
+ :param zernikeMode: zernike modes used for modal removal sampled with the required os factor [ndarray]
52
+
53
+ :return: This class instantiates the direct matrices and generates an MMSE reconstructor
54
+ """
55
+
56
+ self.tel = aoSys.tel
57
+ self.atmModel = aoSys.atm
58
+
59
+
60
+ if isinstance(aoSys.lgsAst, list):
61
+ # It's already a list, no need to convert
62
+ self.guideStar = aoSys.lgsAst
63
+ else:
64
+ # Convert to a list
65
+ self.guideStar = [aoSys.lgsAst]
66
+ self.nGuideStar = len(self.guideStar)
67
+
68
+
69
+ if isinstance(aoSys.mmseStar, list):
70
+ # It's already a list, no need to convert
71
+ self.mmseStar = aoSys.mmseStar
72
+ else:
73
+ # Convert to a list
74
+ self.mmseStar = [aoSys.mmseStar]
75
+
76
+ self.nMmseStar = len(self.mmseStar) if self.mmseStar is not None else 0
77
+
78
+
79
+ self.dm = aoSys.dm
80
+
81
+ self._alpha = alpha
82
+ self._lag = lag
83
+ self.weightOptimDir = weightOptimDir
84
+ self.os = os
85
+ self.model = model
86
+ self.zernikeMode = zernikeMode
87
+
88
+ self.outputRecGrid = aoSys.outputReconstructiongrid
89
+
90
+ #All valid subap mask
91
+ self.unfiltered_subap_mask = aoSys.unfiltered_subap_mask
92
+
93
+ #Valid subap mask
94
+ self._filtered_subap_mask = aoSys.filtered_subap_mask
95
+
96
+ if weight_vector is None:
97
+ self.weight_vector = self.computeDefaultWeightVector()
98
+ else:
99
+ self.weight_vector = weight_vector
100
+
101
+ if noise_covariance is None:
102
+ self.noise_covariance = self.computeDefaultNoiseCovariance()
103
+ else:
104
+ self.noise_covariance = noise_covariance
105
+
106
+
107
+
108
+
109
+
110
+
111
+ # self._weight_vector = weight_vector
112
+ if isinstance(self.weightOptimDir, str):
113
+ if self.weightOptimDir.lower() == 'avr' or self.weightOptimDir.lower() == 'average':
114
+ self.weightOptimDir = 1 / self.nMmseStar * np.ones(self.nMmseStar)
115
+ else:
116
+ raise ValueError('Keyword for optimization weights not recognized')
117
+ elif self.weightOptimDir != -1: # Default optimization in individual directions
118
+ if len(self.weightOptimDir) != self.nMmseStar:
119
+ raise ValueError('The weights are not the same size as the number of optimization directions')
120
+ else:
121
+ self.weightOptimDir = self.weightOptimDir / np.sum(self.weightOptimDir)
122
+
123
+ # %% scale r0 in case of different wavelengths
124
+ wvlScale = self.guideStar[0].wavelength / self.atmModel.wavelength
125
+ self.atmModel.r0 = self.atmModel.r0 * wvlScale ** 1.2
126
+
127
+
128
+ # Gamma, gridMask = tools.sparseGradientMatrixAmplitudeWeighted(self.unfiltered_subap_mask, amplMask=None,
129
+ # os=self.os)
130
+ #
131
+ # Gamma = [Gamma] * self.nGuideStar # Replicate Gamma nMmseStar times
132
+ # gridMask = [gridMask] * self.nGuideStar
133
+ # Gamma = [np.asarray(mat) for mat in Gamma]
134
+ # self.Gamma = block_diag(Gamma)
135
+ # self.gridMask = gridMask
136
+
137
+
138
+ # %% FITTING MATRIX
139
+ if self.dm is not None:
140
+ iFittingMatrix = 2*self.dm.modes[self.outputRecGrid.flatten("F"),]
141
+
142
+ if cuda_available:
143
+ self.fittingMatrix = cp.linalg.pinv(cp.asarray(iFittingMatrix), rcond=1e-3).get()
144
+ else:
145
+ self.fittingMatrix = np.linalg.pinv(iFittingMatrix, rcond=1e-3)
146
+ else:
147
+ self.fittingMatrix = None
148
+
149
+ # %% RECONSTRUCTOR
150
+ self.buildReconstructor()
151
+
152
+
153
+
154
+
155
+ @property
156
+ def noise_covariance(self):
157
+ return self._noise_covariance
158
+
159
+ @noise_covariance.setter
160
+ def noise_covariance(self, val):
161
+ if val is not None:
162
+ if self.model == 'modal' and isinstance(val, (int, float)):
163
+ n_mode = len(self.zernikeMode)
164
+ val = [[val] * n_mode] * n_mode
165
+ val = [[val[i][j] if i == j else 0 for j in range(n_mode)] for i in range(n_mode)]
166
+ elif isinstance(val, (int, float)):
167
+ val = val * np.eye(self.Gamma.shape[0])
168
+ else:
169
+ #Default noise covariance matrix TODO: Evaluate if this should be the default setting
170
+ val = 1e-3 * self.alpha * np.diag(1 / (self.weight_vector.flatten(order='F') + 1e-8))
171
+
172
+ self._noise_covariance = val
173
+
174
+
175
+ def computeDefaultNoiseCovariance(self):
176
+ self.noise_covariance = None
177
+
178
+
179
+ @property
180
+ def weight_vector(self):
181
+ return self._weight_vector
182
+
183
+ @weight_vector.setter
184
+ def weight_vector(self, val):
185
+
186
+ if val is not None:
187
+ self._weight_vector = val
188
+
189
+ else:
190
+ # #Default weight vector TODO: Evaluate if this should be the default setting
191
+ weight_vector = np.ones([2 * np.count_nonzero(self.unfiltered_subap_mask), self.nGuideStar])
192
+ filteredAllValidMask = self.filtered_subap_mask.T[self.unfiltered_subap_mask]
193
+ filteredAllValidMask = np.tile(filteredAllValidMask, 2)
194
+ weight_vector[~filteredAllValidMask] = 0
195
+ self._weight_vector = weight_vector
196
+
197
+
198
+ def computeDefaultWeightVector(self):
199
+ self.weight_vector = None
200
+
201
+
202
+ @property
203
+ def filtered_subap_mask(self):
204
+ return self._filtered_subap_mask
205
+
206
+ @filtered_subap_mask.setter
207
+ def filtered_subap_mask(self, val):
208
+ self._filtered_subap_mask = val
209
+
210
+
211
+ @property
212
+ def alpha(self):
213
+ return self._alpha
214
+
215
+ @alpha.setter
216
+ def alpha(self, val):
217
+ self._alpha = val
218
+
219
+
220
+ @property
221
+ def R_unfiltered(self):
222
+ return self._R_unfiltered
223
+
224
+ def buildCovarianceMatrices(self):
225
+ print("Building the covariance matrices")
226
+ self.Cox = []
227
+ for i in range(len(self.mmseStar)):
228
+ res = tools.spatioAngularCovarianceMatrix(self.tel, self.atmModel, [self.mmseStar[i]], self.guideStar,
229
+ self.unfiltered_subap_mask, self.os)
230
+ self.Cox.append(res)
231
+
232
+
233
+ self.Cxx = tools.spatioAngularCovarianceMatrix(self.tel, self.atmModel, self.guideStar, self.guideStar,
234
+ self.unfiltered_subap_mask, self.os)
235
+
236
+
237
+ def buildGamma(self):
238
+ print("Building Gamma")
239
+ Gamma, gridMask = tools.sparseGradientMatrixAmplitudeWeighted(self.unfiltered_subap_mask, amplMask=None,
240
+ os=self.os)
241
+
242
+ Gamma = [Gamma] * self.nGuideStar # Replicate Gamma nMmseStar times
243
+ gridMask = [gridMask] * self.nGuideStar
244
+ Gamma = [np.asarray(mat) for mat in Gamma]
245
+ self.Gamma = block_diag(Gamma).todense()
246
+ self.gridMask = gridMask
247
+
248
+
249
+ def buildReconstructor(self):
250
+ print("Using GPU" if cuda_available else "GPU not available, using CPU")
251
+
252
+ start = time()
253
+ self.buildGamma()
254
+ print(f"Took {time()-start} seconds to build Gamma")
255
+
256
+
257
+ start = time()
258
+ self.buildCovarianceMatrices()
259
+ print(f"Took {time()-start} seconds to build the covariance matrices")
260
+
261
+
262
+ print("Building the reconstructor")
263
+ start = time()
264
+
265
+
266
+ if cuda_available:
267
+ self.Cox = cp.asarray(self.Cox)
268
+ self.Cxx = cp.asarray(self.Cxx)
269
+ self.noise_covariance = cp.asarray(self.noise_covariance)
270
+ self.Gamma = cp.asarray(self.Gamma)
271
+
272
+ if self.weightOptimDir == -1:
273
+ self.Reconstructor = [None] * self.nMmseStar
274
+
275
+ for k in range(self.nMmseStar):
276
+
277
+ if cuda_available:
278
+ self.Reconstructor[k] = (
279
+ self.Cox[k] @ self.Gamma.T @ cp.linalg.pinv(self.Gamma @ self.Cxx @ self.Gamma.T + self.noise_covariance)).get()
280
+ else:
281
+ self.Reconstructor[k] = self.Cox[k] @ self.Gamma.T @ np.linalg.pinv(self.Gamma @ self.Cxx @ self.Gamma.T + self.noise_covariance)
282
+
283
+ else: # weighted sum over all the optimisation directions
284
+ # This code uses the ss stars to compute a weighted average tomographic
285
+ # reconstructor over all of them
286
+ self.Reconstructor = [None]
287
+ CoxWAvr = np.sum([ self.Cox[k] * self.weightOptimDir[k] for k in range(self.nMmseStar)], axis=0)
288
+ self.Reconstructor[0] = CoxWAvr @ self.Gamma.T @ np.linalg.pinv(self.Gamma @ self.Cxx @ self.Gamma.T + self.noise_covariance)
289
+
290
+
291
+ self._R_unfiltered = self.fittingMatrix @ self.Reconstructor[0]
292
+ print(f"Took {time()-start} seconds to build the reconstructor")
293
+
294
+
295
+
296
+
297
+
@@ -0,0 +1,114 @@
1
+ import tomoAO.tools.tomography_tools as tools
2
+ from tomoAO.Reconstruction.reconClassType import tomoReconstructor
3
+ from tomoAO.IO import load_from_ini
4
+ from tomoAO.Simulation import AOSystem
5
+
6
+ from scipy.linalg import block_diag
7
+
8
+ import numpy as np
9
+ cuda_available = False
10
+ try:
11
+ import cupy as cp
12
+ if cp.cuda.is_available():
13
+ cuda_available = True
14
+ except:
15
+ cuda_available = False
16
+
17
+
18
+ def tomographic_reconstructor_phase_space(config_file, config_dir, alpha, weight_vector=None, noise_covariance=None, debug=False):
19
+
20
+ config_vars = load_from_ini(config_file, ao_mode="MLAO", config_dir=config_dir)
21
+
22
+ aoSys = AOSystem(config_vars)
23
+
24
+ rec = tomoReconstructor(aoSys, alpha=alpha, weight_vector=weight_vector, noise_covariance=noise_covariance, os=2)
25
+
26
+ if debug:
27
+ return rec
28
+
29
+ return rec._R_unfiltered
30
+
31
+
32
+
33
+ def tomographic_reconstructor_dm_space(aoSys, IM, weight, alpha, Cn=None):
34
+
35
+ # %% LGS-to-LGS cross-covariance matrix
36
+ Cxx = tools.spatioAngularCovarianceMatrix(aoSys.tel, aoSys.atm, aoSys.lgsAst, aoSys.lgsAst,
37
+ aoSys.act_mask, os=1, dm_space=True)
38
+ # %% LGS-to-science cross-covariance matrix
39
+ Cox = []
40
+ for i in range(len([aoSys.mmseStar])):
41
+ res = tools.spatioAngularCovarianceMatrix(aoSys.tel, aoSys.atm, [[aoSys.mmseStar][i]],
42
+ aoSys.lgsAst,
43
+ aoSys.act_mask, os=1, dm_space=True)
44
+ Cox.append(res)
45
+
46
+ Cox = Cox[0]
47
+
48
+ # TODO this scaling was chosen roughly to match the original invcov matrix read from file
49
+ Cxx = Cxx / 36
50
+ Cox = Cox / 36
51
+
52
+ if Cn is None:
53
+ Cn = 1e-3 * alpha * np.diag(1 / (weight.flatten(order='F') + 1e-8))
54
+
55
+ IM = block_diag(*IM)
56
+
57
+ if cuda_available:
58
+ Cox = cp.asarray(Cox)
59
+ IM = cp.asarray(IM)
60
+ Cxx = cp.asarray(Cxx)
61
+ Cn = cp.asarray(Cn)
62
+
63
+ return (Cox @ IM.T @ np.linalg.pinv(IM @ Cxx @ IM.T + Cn)).get()
64
+
65
+ return Cox @ IM.T @ np.linalg.pinv(IM @ Cxx @ IM.T + Cn)
66
+
67
+
68
+
69
+
70
+ def averaging_bayesian_reconstructor_dm_space(IM, weight_vector, inv_cov_mat, alpha):
71
+ """
72
+ This function computes the Bayesian reconstructor as:
73
+
74
+ :math:`R = {(H^{T} W^{T} + A)}^{-1} H^{T} W^{T}`
75
+
76
+ Args:
77
+ IM:
78
+ weight_vector:
79
+ inv_cov_mat:
80
+ median_intensity:
81
+
82
+ Returns:
83
+
84
+ """
85
+
86
+ n_lgs = weight_vector.shape[1]
87
+
88
+ IMw = IM.copy()
89
+ n_act = IM.shape[-1]
90
+
91
+ for k in range(n_lgs):
92
+ W = np.tile(np.reshape(weight_vector[:, k], (-1, 1)), (1, n_act))
93
+ IMw[k, :, :] = IMw[k, :, :] * W
94
+
95
+ IMw = block_diag(*IMw)
96
+ IM = block_diag(*IM)
97
+
98
+ # perform the regularized inversion
99
+ return 1 / n_lgs * np.hstack([np.eye(n_act)] * n_lgs) @ np.linalg.inv(
100
+ IMw.T @ IM + alpha * 1e-3 * block_diag(*[inv_cov_mat]*n_lgs) + 1.) @ (
101
+ IMw.T) # the one is there to penalize piston
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
@@ -0,0 +1 @@
1
+ from .aoSystem import AOSystem
@@ -0,0 +1,170 @@
1
+ """
2
+ Created on Tue Apr 18 15:25:00 2023
3
+
4
+ @author: ccorreia@spaceodt.net
5
+ """
6
+
7
+ import tomoAO.tools.tomography_tools as tools
8
+
9
+ # %% USE OOPAO, define a geometry and compute the cross-covariance matrix for all the layers
10
+ import numpy as np
11
+
12
+ from OOPAO.Atmosphere import Atmosphere
13
+ from OOPAO.DeformableMirror import DeformableMirror
14
+ from OOPAO.MisRegistration import MisRegistration
15
+ from OOPAO.Source import Source
16
+ from OOPAO.Telescope import Telescope
17
+ from OOPAO.ShackHartmann import ShackHartmann
18
+
19
+
20
+
21
+
22
+ class AOSystem:
23
+ def __init__(self, param):
24
+ # %% ----------------------- TELESCOPE ----------------------------------
25
+
26
+ # create the Telescope object (not Keck for now)
27
+ tel = Telescope(resolution=param['resolution'],
28
+ diameter=param['diameter'],
29
+ samplingTime=param['samplingTime'],
30
+ centralObstruction=param['centralObstruction'])
31
+
32
+ thickness_spider = 0.05 # size in m
33
+ angle = [45, 135, 225, 315] # in degrees
34
+ offset_X = [-0.4, 0.4, 0.4, -0.4] # shift offset of the spider
35
+ offset_Y = None
36
+
37
+ tel.apply_spiders(angle, thickness_spider, offset_X=offset_X, offset_Y=offset_Y)
38
+
39
+
40
+ # data = loadmat(f'{param["path2matrices"]}tel_pupil.mat')
41
+ # pupil = data['pup'] # Extract the array
42
+ #
43
+ # tel.pupil = pupil
44
+ # %% ----------------------- NGS ----------------------------------
45
+ # create the Source object
46
+ ngs = Source(optBand=param['opticalBand'],
47
+ magnitude=param['magnitude'],
48
+ altitude=param['srcAltitude'])
49
+
50
+ # combine the NGS to the telescope using '*' operator:
51
+ ngs * tel
52
+ # %% LGS objects
53
+
54
+
55
+ lgsAst = [Source(optBand=param['opticalBand'],
56
+ magnitude=param['lgs_magnitude'],
57
+ altitude=param['lgs_altitude'],
58
+ coordinates=[param['lgs_zenith'][kLgs], param['lgs_azimuth'][kLgs]])
59
+ for kLgs in range(param["n_lgs"])]
60
+
61
+ # %% science targets
62
+ sciSrc = Source(optBand='K',
63
+ magnitude=0,
64
+ altitude=np.inf,
65
+ coordinates=[0, 0])
66
+
67
+ # %% science targets
68
+
69
+ recCalSrc = Source(optBand='Na',
70
+ magnitude=0,
71
+ altitude=np.inf,
72
+ coordinates=[0, 0])
73
+ # %% ----------------------- ATMOSPHERE ----------------------------------
74
+
75
+ # create the Atmosphere object
76
+
77
+ atm = Atmosphere(telescope=tel,
78
+ r0=param['r0'],
79
+ L0=param['L0'],
80
+ windSpeed=param['windSpeed'],
81
+ fractionalR0=param['fractionnalR0'],
82
+ windDirection=param['windDirection'],
83
+ altitude=np.array(param['altitude']),
84
+ param=param)
85
+
86
+
87
+ # %% ----------------------- DEFORMABLE MIRROR ----------------------------------
88
+ # mis-registrations object
89
+ misReg = MisRegistration(param)
90
+
91
+
92
+ # set coordinate vector to match the Keck actuator location
93
+ act_mask = np.loadtxt(param["actuator_mask"], dtype=bool, delimiter=",")
94
+ if act_mask.shape[0] != param['nActuator']:
95
+ act_mask = np.pad(act_mask, pad_width=int(param['nSubapExtra']/2), mode='constant', constant_values=0)
96
+
97
+ X, Y = tools.meshgrid(param['nActuator'], tel.D, offset_x=0.0, offset_y=0.0, stretch_x=1, stretch_y=1)
98
+
99
+
100
+ coordinates = np.array([X[act_mask], Y[act_mask]]).T
101
+
102
+ self.dm_coordinates = coordinates
103
+ # if no coordinates specified, create a cartesian dm
104
+ resolution = tel.resolution
105
+
106
+ # TODO this cannot be set by default, since the wavefront resolution is set only during the MMSE reconstructor. THere is a loophole here!
107
+ #tel.resolution = 2 * param['nSubaperture'] + 1
108
+ tel.resolution = param['dm_resolution']# this is to compute a low-resolution DM IF, where low-resolution is the wavefront reconstruction resolution
109
+ dm = DeformableMirror(telescope=tel,
110
+ nSubap=param['nSubaperture'],
111
+ mechCoupling=param['mechanicalCoupling'],
112
+ misReg=misReg,
113
+ coordinates=coordinates,
114
+ pitch=tel.D / (param['nActuator'] - 1))
115
+
116
+
117
+ dm.act_mask = act_mask
118
+ tel.resolution = resolution
119
+ # %% ----------------------- Wave Front Sensor ----------------------------------
120
+ wfs = ShackHartmann(telescope=tel,
121
+ nSubap=param['nSubaperture'],
122
+ lightRatio=0.5)
123
+
124
+
125
+ unfiltered_subap_mask = np.loadtxt(param["unfiltered_subap_mask"],
126
+ dtype=bool, delimiter=",")
127
+
128
+ if unfiltered_subap_mask.shape[0] != param['nSubaperture']:
129
+ unfiltered_subap_mask = np.pad(unfiltered_subap_mask,
130
+ pad_width=int(param['nSubapExtra']/2),
131
+ mode='constant',
132
+ constant_values=0)
133
+
134
+
135
+ filtered_subap_mask = np.loadtxt(param["filtered_subap_mask"],
136
+ dtype=bool, delimiter=",")
137
+
138
+ if filtered_subap_mask.shape[0] != param['nSubaperture']:
139
+ filtered_subap_mask = np.pad(filtered_subap_mask,
140
+ pad_width=int(param['nSubapExtra']/2),
141
+ mode='constant',
142
+ constant_values=0)
143
+
144
+
145
+ wfs.valid_subapertures = unfiltered_subap_mask
146
+ wfs.subap_mask = unfiltered_subap_mask #Force existence of subap_mask variable for backwards compatibility
147
+
148
+
149
+
150
+ # %% ----------------------- Wave Front Reconstruction ----------------------------------
151
+
152
+ outputReconstructiongrid = tools.reconstructionGrid(unfiltered_subap_mask, param['os'], dm_space=False)
153
+
154
+
155
+ # %% ----------------------- Self Allocation ----------------------------------
156
+
157
+ self.param = param
158
+ self.atm = atm
159
+ self.tel = tel
160
+ self.dm = dm
161
+ self.wfs = wfs
162
+ self.lgsAst = lgsAst
163
+ self.mmseStar = ngs
164
+ self.misreg = misReg
165
+ self.outputReconstructiongrid = outputReconstructiongrid
166
+ self.sciSrc = sciSrc
167
+ self.act_mask = act_mask
168
+ self.unfiltered_subap_mask = unfiltered_subap_mask
169
+ self.filtered_subap_mask = filtered_subap_mask
170
+
@@ -0,0 +1,6 @@
1
+ # # from .reconClassType import LinearMMSE
2
+ # import tomoAO.Reconstruction.recontructors
3
+ import tomoAO.IO
4
+ import tomoAO.Simulation
5
+ import tomoAO.Reconstruction
6
+ from .version import __version__
File without changes
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Tue Apr 18 15:25:00 2023
5
+
6
+ @author: ccorreia@spaceodt.net
7
+ """
8
+
9
+ import numpy as np
10
+ cuda_available = False
11
+ try:
12
+ import cupy as cp
13
+ if cp.cuda.is_available():
14
+ cuda_available = True
15
+ except:
16
+ cuda_available = False
17
+
18
+ from scipy.sparse import csr_matrix
19
+ from scipy.special import gamma, kv
20
+
21
+ import math
22
+
23
+
24
+
25
+ def bessel_i(n, x, terms=10):
26
+ x = cp.asarray(x, dtype=cp.float64)
27
+ sum_result = cp.zeros_like(x, dtype=cp.float64)
28
+
29
+ for m in range(terms):
30
+ term = (1 / (math.factorial(m) * math.gamma(m + n + 1))) * (x / 2) ** (2 * m + n)
31
+ sum_result += term
32
+
33
+ return sum_result
34
+
35
+ def bessel_k(n, x, terms=10):
36
+ x = cp.asarray(x, dtype=cp.float64)
37
+
38
+ if cp.any(x <= 0):
39
+ raise ValueError("x must be positive for K_n(x)")
40
+
41
+ i_n = bessel_i(n, x, terms)
42
+ i_neg_n = bessel_i(-n, x, terms)
43
+
44
+ return (np.pi / 2) * (i_neg_n - i_n) / np.sin(n * cp.pi)
45
+
46
+
47
+ def covariance_matrix(rho, r0, L0):
48
+ L0r0ratio = (L0 / r0) ** (5 / 3)
49
+ cst = (24 * gamma(6 / 5) / 5) ** (5 / 6) * \
50
+ (gamma(11 / 6) / (2 ** (5 / 6) * np.pi ** (8 / 3))) * L0r0ratio
51
+ out = np.ones(rho.shape, dtype=rho.dtype) * (24 * gamma(6 / 5) / 5) ** (5 / 6) * \
52
+ (gamma(11 / 6) * gamma(5 / 6) /
53
+ (2 * np.pi ** (8 / 3))) * L0r0ratio
54
+ index = rho != 0
55
+ u = 2 * np.pi * rho[index] / L0
56
+ if cuda_available:
57
+ out[index] = cst * u ** (5 / 6) * bessel_k(5 / 6, cp.asarray(u), terms=10).get()
58
+
59
+ else:
60
+ out[index] = cst * u ** (5 / 6) * kv(5 / 6, u)
61
+
62
+ return out
63
+
64
+
65
+
66
+ def dists(rho1, rho2):
67
+ dist = np.abs(np.subtract.outer(rho1.flatten(), rho2.flatten()))
68
+ return dist
69
+
70
+
71
+ def meshgrid(nPts, D, offset_x=0, offset_y=0, stretch_x=1, stretch_y=1):
72
+ x = np.linspace(-D / 2, D / 2, nPts)
73
+ X, Y = np.meshgrid(x * stretch_x, x * stretch_y)
74
+ X = X + offset_x
75
+ Y = Y + offset_y
76
+ return X, Y
77
+
78
+
79
+ def reconstructionGrid(mask, os, dm_space=False):
80
+ # os stands for oversampling. Can be 1 or 2.
81
+ # If os=1, reconstructionGrid pitch = subaperturePitch,
82
+ # if os=2, reconstructionGrid pitch = subaperturePitch/2
83
+ from scipy.signal import convolve2d
84
+ if os == 1 and dm_space:
85
+ val = mask
86
+ elif os == 1 and not dm_space:
87
+ val = convolve2d(np.ones((2, 2)), mask).astype('bool')
88
+ elif os == 2:
89
+ nElements = os * mask.shape[0] + 1 # Linear number of lenslet+actuator
90
+ validLensletActuator = np.zeros((nElements, nElements), dtype=bool)
91
+ index = np.arange(1, nElements, 2) # Lenslet index
92
+ validLensletActuator[np.ix_(index, index)] = mask
93
+ kernel = np.ones((3, 3))
94
+ output = convolve2d(validLensletActuator, kernel, mode='same')
95
+ val = output.astype(bool)
96
+ return val
97
+
98
+
99
+ def spatioAngularCovarianceMatrix(tel, atm, src1, src2, mask, os, dm_space=0):
100
+ # Compute the discrete reconstruction mask from the subap_mask passed on as input
101
+
102
+ recIdx = reconstructionGrid(mask, os, dm_space)
103
+ if src1 == src2: # auto-covariance matrix
104
+
105
+ arcsec2radian = np.pi / 180 / 3600
106
+ nPts = recIdx.shape[0]
107
+ crossCovCell = np.empty((len(src1), len(src2)), dtype=object)
108
+ for i in range(0, len(src1)):
109
+ for j in range(0, len(src2)):
110
+ if j >= i:
111
+
112
+ phaseCovElem = np.zeros((nPts ** 2, nPts ** 2, len(atm.altitude)))
113
+ for l in range(0, len(atm.altitude)):
114
+ # CROSS COVARIANCE BETWEEN TWO STARS
115
+ # STAR 1
116
+ coneCompressionFactor = 1 - \
117
+ atm.altitude[l] / src1[i].altitude
118
+ x = src1[i].coordinates[0] * atm.altitude[l] * \
119
+ arcsec2radian * \
120
+ np.cos(src1[i].coordinates[1] * np.pi / 180)
121
+ y = src1[i].coordinates[0] * atm.altitude[l] * \
122
+ arcsec2radian * \
123
+ np.sin(src1[i].coordinates[1] * np.pi / 180)
124
+ X, Y = meshgrid(nPts, tel.D, offset_x=x, offset_y=y,
125
+ stretch_x=coneCompressionFactor, stretch_y=coneCompressionFactor)
126
+
127
+ rho1 = X + 1j * Y
128
+
129
+
130
+ # STAR 2
131
+ coneCompressionFactor = 1 - \
132
+ atm.altitude[l] / src2[j].altitude
133
+ x = src2[j].coordinates[0] * atm.altitude[l] * \
134
+ arcsec2radian * \
135
+ np.cos(src2[j].coordinates[1] * np.pi / 180)
136
+ y = src2[j].coordinates[0] * atm.altitude[l] * \
137
+ arcsec2radian * \
138
+ np.sin(src2[j].coordinates[1] * np.pi / 180)
139
+ X, Y = meshgrid(nPts, tel.D, offset_x=x, offset_y=y,
140
+ stretch_x=coneCompressionFactor, stretch_y=coneCompressionFactor)
141
+ rho2 = X + 1j * Y
142
+
143
+ dist = dists(rho1.T, rho2.T) # not sure why, using the transpose makes it equal to Matlab's
144
+
145
+ phaseCovElem[:, :, l] = covariance_matrix(
146
+ dist, atm.r0, atm.L0) * atm.fractionalR0[l]
147
+
148
+
149
+ crossCovCell[i, j] = np.sum(phaseCovElem, axis=2)[
150
+ recIdx.flatten("F"), :][:, recIdx.flatten("F")]
151
+
152
+ # populate the off-diagonal blocks left null in the previous step
153
+ for i in range(0, len(src1)):
154
+ for j in range(0, len(src1)):
155
+ if j < i:
156
+ crossCovCell[i, j] = np.transpose(crossCovCell[j, i])
157
+
158
+ crossCovMat = np.block(
159
+ [[crossCovCell[i, j] for j in range(len(src1))] for i in range(len(src2))])
160
+ crossCovMat = np.vstack(crossCovMat)
161
+ return crossCovMat
162
+ else:
163
+ # breakpoint()
164
+ arcsec2radian = np.pi / 180 / 3600
165
+ nPts = recIdx.shape[0]
166
+ crossCovCell = np.empty((len(src1), len(src2)), dtype=object)
167
+ for i in range(0, len(src1)):
168
+ for j in range(0, len(src2)):
169
+ phaseCovElem = np.zeros((nPts ** 2, nPts ** 2, len(atm.altitude)))
170
+ for l in range(0, len(atm.altitude)):
171
+ # CROSS COVARIANCE BETWEEN TWO STARS
172
+ # STAR 1
173
+ coneCompressionFactor = 1 - \
174
+ atm.altitude[l] / src1[i].altitude
175
+ x = src1[i].coordinates[0] * atm.altitude[l] * \
176
+ arcsec2radian * \
177
+ np.cos(src1[i].coordinates[1] * np.pi / 180)
178
+ y = src1[i].coordinates[0] * atm.altitude[l] * \
179
+ arcsec2radian * \
180
+ np.sin(src1[i].coordinates[1] * np.pi / 180)
181
+ X, Y = meshgrid(nPts, tel.D, offset_x=x, offset_y=y,
182
+ stretch_x=coneCompressionFactor, stretch_y=coneCompressionFactor)
183
+ rho1 = X + 1j * Y
184
+ # STAR 2
185
+ coneCompressionFactor = 1 - \
186
+ atm.altitude[l] / src2[j].altitude
187
+ x = src2[j].coordinates[0] * atm.altitude[l] * \
188
+ arcsec2radian * \
189
+ np.cos(src2[j].coordinates[1] * np.pi / 180)
190
+ y = src2[j].coordinates[0] * atm.altitude[l] * \
191
+ arcsec2radian * \
192
+ np.sin(src2[j].coordinates[1] * np.pi / 180)
193
+ X, Y = meshgrid(nPts, tel.D, offset_x=x, offset_y=y,
194
+ stretch_x=coneCompressionFactor, stretch_y=coneCompressionFactor)
195
+ rho2 = X + 1j * Y
196
+
197
+ dist = dists(rho1.T, rho2.T) # not sure why, using the transpose makes it equal to Matlab's
198
+ phaseCovElem[:, :, l] = covariance_matrix(
199
+ dist, atm.r0, atm.L0) * atm.fractionalR0[l]
200
+ crossCovCell[i, j] = np.sum(phaseCovElem, axis=2)[
201
+ recIdx.flatten("F"), :][:, recIdx.flatten("F")]
202
+ crossCovMat = np.block(
203
+ [[crossCovCell[i, j] for j in range(len(src2))] for i in range(len(src1))])
204
+ if len(src1) > 1:
205
+ crossCovMat = np.vstack(crossCovMat)
206
+ return crossCovMat
207
+
208
+
209
+
210
+
211
+
212
+ def sparseGradientMatrixAmplitudeWeighted(validLenslet, amplMask, os=2):
213
+
214
+ nLenslet = validLenslet.shape[0]
215
+
216
+ nMap = os * nLenslet + 1
217
+
218
+ if amplMask is None:
219
+ amplMask = np.ones((nMap, nMap))
220
+
221
+ nValidLenslet_ = np.count_nonzero(validLenslet)
222
+ dsa = 1
223
+
224
+ i0x = np.tile([0, 1, 2], 3)
225
+
226
+ j0x = np.repeat([0, 1, 2], 3)
227
+
228
+ i0y = np.tile([0, 1, 2], 3)
229
+
230
+ j0y = np.repeat([0, 1, 2], 3)
231
+
232
+ s0x = np.array([-1 / 4, -1 / 2, -1 / 4, 0, 0, 0, 1 / 4, 1 / 2, 1 / 4]) * (1 / dsa)
233
+
234
+ s0y = -np.array([1 / 4, 0, -1 / 4, 1 / 2, 0, -1 / 2, 1 / 4, 0, -1 / 4]) * (1 / dsa)
235
+
236
+ Gv = np.array([[-2, 2, -1, 1], [-2, 2, -1, 1], [-1, 1, -2, 2], [-1, 1, -2, 2]])
237
+
238
+ i_x = np.zeros(9 * nValidLenslet_)
239
+ j_x = np.zeros(9 * nValidLenslet_)
240
+ s_x = np.zeros(9 * nValidLenslet_)
241
+ i_y = np.zeros(9 * nValidLenslet_)
242
+ j_y = np.zeros(9 * nValidLenslet_)
243
+ s_y = np.zeros(9 * nValidLenslet_)
244
+
245
+ jMap0, iMap0 = np.meshgrid(np.arange(0, 3), np.arange(0, 3))
246
+
247
+ gridMask = np.zeros((nMap, nMap), dtype=bool)
248
+
249
+ u = np.arange(0, 9)
250
+
251
+ for jLenslet in range(0, nLenslet):
252
+ jOffset = os * (jLenslet)
253
+ for iLenslet in range(0, nLenslet):
254
+ if validLenslet[iLenslet, jLenslet]:
255
+ I = (iLenslet) * os + 1
256
+ J = (jLenslet) * os + 1
257
+
258
+ a = amplMask[I - 1:I + os, J - 1:J + os]
259
+
260
+ numIllum = np.sum(a)
261
+
262
+ if numIllum == (os + 1) ** 2:
263
+ iOffset = os * (iLenslet)
264
+ i_x[u] = i0x + iOffset
265
+ j_x[u] = j0x + jOffset
266
+ s_x[u] = s0x
267
+ i_y[u] = i0y + iOffset
268
+ j_y[u] = j0y + jOffset
269
+ s_y[u] = s0y
270
+ u = u + (os + 1) ** 2
271
+
272
+ gridMask[iMap0 + iOffset, jMap0 + jOffset] = True
273
+
274
+ indx = np.ravel_multi_index((i_x.astype(int), j_x.astype(int)), (nMap, nMap), order='F')
275
+ indy = np.ravel_multi_index((i_y.astype(int), j_y.astype(int)), (nMap, nMap), order='F')
276
+
277
+ v = np.tile(np.arange(0, 2 * nValidLenslet_), (u.size, 1)).T
278
+
279
+ Gamma = csr_matrix((np.concatenate((s_x, s_y)), (v.flatten(), np.concatenate((indx, indy)))),
280
+ shape=(2 * nValidLenslet_, nMap ** 2))
281
+ Gamma = Gamma.todense()
282
+ Gamma = Gamma[:, gridMask.flatten("F")]
283
+
284
+ return Gamma, gridMask
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: tomoAO
3
+ Version: 0.0.1
4
+ Summary: Python package with atmospheric tomography generic functions and tools
5
+ Home-page: https://github.com/cmcorreia/PyTomo
6
+ Author: SpaceODT
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Natural Language :: English
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: aotools
16
+ Requires-Dist: numpy
17
+ Requires-Dist: astropy
18
+ Dynamic: author
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: summary
27
+
28
+ # tomoAO
29
+ Python package with atmospheric tomography generic functions and tools
30
+
31
+ ## Installation
32
+
33
+ 1. Install OOPAO
34
+ ```sh
35
+ pip install git+https://github.com/cheritier/OOPAO
36
+ ```
37
+
38
+ 2. Install cupy according to your cuda version (Optional)
39
+ ```sh
40
+ pip install cupy-cuda11x
41
+ ```
42
+ ```sh
43
+ pip install cupy-cuda12x
44
+ ```
45
+
46
+ 3. Install tomoAO via pip
47
+ ```sh
48
+ pip install tomoAO
49
+ ```
50
+
51
+ Or clone the repository
52
+ ```sh
53
+ git clone https://github.com/cmcorreia/PyTomo
54
+ pip install -e 'path to tomoAO'
55
+ ```
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ tomoAO/__init__.py
5
+ tomoAO/version.py
6
+ tomoAO.egg-info/PKG-INFO
7
+ tomoAO.egg-info/SOURCES.txt
8
+ tomoAO.egg-info/dependency_links.txt
9
+ tomoAO.egg-info/requires.txt
10
+ tomoAO.egg-info/top_level.txt
11
+ tomoAO/IO/__init__.py
12
+ tomoAO/IO/io_functions.py
13
+ tomoAO/Reconstruction/__init__.py
14
+ tomoAO/Reconstruction/reconClassType.py
15
+ tomoAO/Reconstruction/recontructors.py
16
+ tomoAO/Simulation/__init__.py
17
+ tomoAO/Simulation/aoSystem.py
18
+ tomoAO/tools/__init__.py
19
+ tomoAO/tools/tomography_tools.py
@@ -0,0 +1,3 @@
1
+ aotools
2
+ numpy
3
+ astropy
@@ -0,0 +1 @@
1
+ tomoAO