disko 1.4.0__tar.gz → 1.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {disko-1.4.0 → disko-1.4.2}/CHANGES.md +13 -0
- {disko-1.4.0 → disko-1.4.2}/Makefile +1 -1
- {disko-1.4.0 → disko-1.4.2}/PKG-INFO +4 -3
- {disko-1.4.0 → disko-1.4.2}/disko/__init__.py +0 -1
- {disko-1.4.0 → disko-1.4.2}/disko/disko.py +4 -118
- disko-1.4.2/disko/resolution.py +108 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_disko.py +0 -17
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_pylops_operator.py +0 -17
- {disko-1.4.0 → disko-1.4.2}/pyproject.toml +4 -3
- {disko-1.4.0 → disko-1.4.2}/uv.lock +12 -1
- disko-1.4.0/disko/resolution.py +0 -127
- {disko-1.4.0 → disko-1.4.2}/.github/workflows/package.yaml +0 -0
- {disko-1.4.0 → disko-1.4.2}/.github/workflows/testing.yaml +0 -0
- {disko-1.4.0 → disko-1.4.2}/.gitignore +0 -0
- {disko-1.4.0 → disko-1.4.2}/.travis.yml +0 -0
- {disko-1.4.0 → disko-1.4.2}/CITATION.cff +0 -0
- {disko-1.4.0 → disko-1.4.2}/LICENSE.txt +0 -0
- {disko-1.4.0 → disko-1.4.2}/MANIFEST.in +0 -0
- {disko-1.4.0 → disko-1.4.2}/README.md +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/bayes_cli.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/cli.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/draw_cli.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/draw_sky.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/fov/__init__.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/fov/factory.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/healpix_sphere.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/ms_helper.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/multivariate_gaussian.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/parser_support.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/projection_lsqr.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/rime.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/sphere.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/sphere_mesh.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/svd_cli.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/telescope_operator.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/__init__.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_meshsphere.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_multivariate.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_projection_lsqr.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_sphere.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_subsphere.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/tests/test_telescope_operator.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/disko/util.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/docker/Dockerfile +0 -0
- {disko-1.4.0 → disko-1.4.2}/docker/Makefile +0 -0
- {disko-1.4.0 → disko-1.4.2}/docker/docker-compose.yml +0 -0
- {disko-1.4.0 → disko-1.4.2}/install.sh +0 -0
- {disko-1.4.0 → disko-1.4.2}/movie/.gitignore +0 -0
- {disko-1.4.0 → disko-1.4.2}/movie/Makefile +0 -0
- {disko-1.4.0 → disko-1.4.2}/prof.py +0 -0
- {disko-1.4.0 → disko-1.4.2}/requirements.txt +0 -0
- {disko-1.4.0 → disko-1.4.2}/sequential_test.sh +0 -0
- {disko-1.4.0 → disko-1.4.2}/setup.cfg +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 1.4.2 (2026-06-22)
|
|
4
|
+
|
|
5
|
+
- Delegate angle parsing to `angle-parser` library; remove custom `parse_ending` and unit constants from `Resolution`
|
|
6
|
+
- Add `angle-parser>=0.2.0` dependency; remove fallback for `mas`, `uas` (now handled natively)
|
|
7
|
+
- Lower minimum gmsh version from 4.9 to 4.0
|
|
8
|
+
- Update optional `tart2ms` dependency from >=0.7.1 to >=0.9.0
|
|
9
|
+
- Update `uv.lock` for all dependency changes
|
|
10
|
+
|
|
11
|
+
## 1.4.1 (2026-06-22)
|
|
12
|
+
|
|
13
|
+
- Remove redundant `DirectImagingOperator` (10% code reduction); `DiSkOOperator` handles the exact adjoint directly via `A.T`.
|
|
14
|
+
- Fix FISTA and LSMR initial guess: Use true adjoint `np.abs(A.T @ d)` to guarantee a physically correct (non-mirrored) starting image.
|
|
15
|
+
|
|
3
16
|
## 1.4.0 (2026-06-22)
|
|
4
17
|
|
|
5
18
|
- Convert from pygmsh to gmsh native API; remove pygmsh dependency; add meshio
|
|
@@ -72,7 +72,7 @@ TART_ARGS=--fov 155deg --res 1deg --ms test_data/test.ms --debug --field 1 --HD
|
|
|
72
72
|
cygnus_lsmr:
|
|
73
73
|
${TIME} uv run disko --healpix --fov 3arcmin --ms ~/astro/cyg2052.ms --FITS --res 0.5arcsec --matrix-free --lsmr --nvis 5000 --alpha 0.01 --title 'cygnus_lsmr'
|
|
74
74
|
cygnus_fista:
|
|
75
|
-
${TIME} uv run disko --debug --healpix --fov 3arcmin --ms ~/astro/cyg2052.ms --FITS --res 1arcsec --matrix-free --fista --
|
|
75
|
+
${TIME} uv run disko --debug --healpix --fov 3arcmin --ms ~/astro/cyg2052.ms --FITS --res 1arcsec --matrix-free --fista --niter 200 --nvis 2000 --title 'cygnus_fista'
|
|
76
76
|
|
|
77
77
|
tart:
|
|
78
78
|
uv run disko --healpix ${TART_ARGS} --SVG --alpha=0.025 --tikhonov --title 'tart'
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: disko
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Discrete Sky Operator (DiSkO) Aperture Synthesis Radio Imaging
|
|
5
5
|
Author-email: Tim Molteno <tim@elec.ac.nz>
|
|
6
6
|
License: GPLv3
|
|
7
7
|
License-File: LICENSE.txt
|
|
8
8
|
Requires-Python: <3.14,>=3.11
|
|
9
|
+
Requires-Dist: angle-parser>=0.2.0
|
|
9
10
|
Requires-Dist: dask-ms<0.3.0,>=0.2.25
|
|
10
|
-
Requires-Dist: gmsh>=4.
|
|
11
|
+
Requires-Dist: gmsh>=4.0
|
|
11
12
|
Requires-Dist: h5py>=3.7
|
|
12
13
|
Requires-Dist: healpy<2.0.0,>=1.15.0
|
|
13
14
|
Requires-Dist: imageio>=0.6.0b1
|
|
@@ -22,7 +23,7 @@ Requires-Dist: tart-tools>=1.4.0
|
|
|
22
23
|
Requires-Dist: tart>=1.4.0
|
|
23
24
|
Requires-Dist: toolz>=0.11.2
|
|
24
25
|
Provides-Extra: ms
|
|
25
|
-
Requires-Dist: tart2ms>=0.
|
|
26
|
+
Requires-Dist: tart2ms>=0.9.0; extra == 'ms'
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
|
|
28
29
|
# Discrete Sky Operator (DiSkO) Synthesis Imaging
|
|
@@ -247,12 +247,12 @@ class DiSkOOperator(pylops.LinearOperator):
|
|
|
247
247
|
|
|
248
248
|
def _rmatvec(self, v):
|
|
249
249
|
r"""
|
|
250
|
-
Returns x = A^
|
|
250
|
+
Returns x = A^T * v, where A^T is the transpose of A.
|
|
251
251
|
|
|
252
252
|
x = ( T_real' T_imag') (v_real
|
|
253
253
|
v_imag)
|
|
254
254
|
|
|
255
|
-
Matrix-free: uses cached
|
|
255
|
+
Matrix-free: uses cached float64 harmonic blocks.
|
|
256
256
|
"""
|
|
257
257
|
assert v.shape == (self.M,)
|
|
258
258
|
n_u = self.u_arr.shape[0]
|
|
@@ -278,117 +278,6 @@ class DiSkOOperator(pylops.LinearOperator):
|
|
|
278
278
|
return ret
|
|
279
279
|
|
|
280
280
|
|
|
281
|
-
class DirectImagingOperator(pylops.LinearOperator):
|
|
282
|
-
r"""
|
|
283
|
-
This is the approximate inverse of the DiSkOOperator, and corresponds to
|
|
284
|
-
imaging by the discrete fourier transform
|
|
285
|
-
"""
|
|
286
|
-
|
|
287
|
-
def __init__(self, u_arr, v_arr, w_arr, data, frequencies, sphere):
|
|
288
|
-
super().__init__()
|
|
289
|
-
self.N = sphere.npix # Number of pixels
|
|
290
|
-
self.u_arr = u_arr
|
|
291
|
-
self.v_arr = v_arr
|
|
292
|
-
self.w_arr = w_arr
|
|
293
|
-
self.dtype = REAL_DATATYPE
|
|
294
|
-
|
|
295
|
-
try:
|
|
296
|
-
self.n_v, self.n_freq, self.npol = data.shape
|
|
297
|
-
except Exception:
|
|
298
|
-
raise RuntimeError("Data must be of the shape [n_v, n_freq, n_pol]")
|
|
299
|
-
|
|
300
|
-
self.M = self.n_v * self.n_freq
|
|
301
|
-
|
|
302
|
-
self.frequencies = frequencies
|
|
303
|
-
self.sphere = sphere
|
|
304
|
-
|
|
305
|
-
if self.sphere.l.shape[0] != self.N:
|
|
306
|
-
raise RuntimeError(
|
|
307
|
-
f"self.sphere.l.shape != self.N, {self.sphere.l.shape} != {self.N}"
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
self.shape = (self.N, self.M)
|
|
311
|
-
self.explicit = False # Can't be directly inverted
|
|
312
|
-
logger.info("Creating DirectImagingOperator data={}".format(self.shape))
|
|
313
|
-
|
|
314
|
-
def _compute_harmonics_block(self, p2j, u_blk, v_blk, w_blk):
|
|
315
|
-
"""Compute a block of the complex harmonics matrix.
|
|
316
|
-
|
|
317
|
-
Args:
|
|
318
|
-
p2j: complex frequency factor (1j * omega)
|
|
319
|
-
u_blk, v_blk, w_blk: 1D arrays of UVW coordinates for this
|
|
320
|
-
block of visibilities.
|
|
321
|
-
|
|
322
|
-
Returns:
|
|
323
|
-
H_block: complex array of shape (len(u_blk), self.N)
|
|
324
|
-
"""
|
|
325
|
-
phase = p2j * (
|
|
326
|
-
np.outer(u_blk, self.sphere.l)
|
|
327
|
-
+ np.outer(v_blk, self.sphere.m)
|
|
328
|
-
+ np.outer(w_blk, self.sphere.n_minus_1)
|
|
329
|
-
)
|
|
330
|
-
return np.exp(phase) * self.sphere.pixel_areas[np.newaxis, :]
|
|
331
|
-
|
|
332
|
-
def _block_size(self):
|
|
333
|
-
"""Number of visibility rows per block, targeting ~200 MB per block."""
|
|
334
|
-
return max(256, int(200 * 1024 * 1024 / (16 * max(1, self.N))))
|
|
335
|
-
|
|
336
|
-
def _matvec(self, v):
|
|
337
|
-
"""
|
|
338
|
-
Multiply by the measurements v, producing the sky.
|
|
339
|
-
Returns A * v.
|
|
340
|
-
|
|
341
|
-
sky = real(v_complex @ H) where H[i,j] is the harmonic at
|
|
342
|
-
visibility i, pixel j. Computed in blocks for memory efficiency.
|
|
343
|
-
"""
|
|
344
|
-
n_vis = self.n_v // 2
|
|
345
|
-
vis_complex = v[:n_vis] + 1.0j * v[n_vis:]
|
|
346
|
-
|
|
347
|
-
sky = np.zeros(self.N, dtype=self.dtype)
|
|
348
|
-
block_size = self._block_size()
|
|
349
|
-
|
|
350
|
-
for f in self.frequencies:
|
|
351
|
-
p2j = jomega(f)
|
|
352
|
-
for i_start in range(0, n_vis, block_size):
|
|
353
|
-
i_end = min(i_start + block_size, n_vis)
|
|
354
|
-
H_block = self._compute_harmonics_block(
|
|
355
|
-
p2j,
|
|
356
|
-
self.u_arr[i_start:i_end],
|
|
357
|
-
self.v_arr[i_start:i_end],
|
|
358
|
-
self.w_arr[i_start:i_end],
|
|
359
|
-
) # shape (block_size, N)
|
|
360
|
-
sky += np.real(vis_complex[i_start:i_end] @ H_block)
|
|
361
|
-
|
|
362
|
-
return sky
|
|
363
|
-
|
|
364
|
-
def _rmatvec(self, x):
|
|
365
|
-
r"""
|
|
366
|
-
Map sky vector x back to visibilities.
|
|
367
|
-
|
|
368
|
-
v_complex = conj(H) @ x, then split into real/imag parts.
|
|
369
|
-
"""
|
|
370
|
-
assert x.shape == (self.N,)
|
|
371
|
-
n_vis = self.n_v // 2
|
|
372
|
-
|
|
373
|
-
ret_complex = np.zeros(n_vis, dtype=COMPLEX_DATATYPE)
|
|
374
|
-
block_size = self._block_size()
|
|
375
|
-
|
|
376
|
-
for f in self.frequencies:
|
|
377
|
-
p2j = jomega(f)
|
|
378
|
-
for i_start in range(0, n_vis, block_size):
|
|
379
|
-
i_end = min(i_start + block_size, n_vis)
|
|
380
|
-
H_block = self._compute_harmonics_block(
|
|
381
|
-
p2j,
|
|
382
|
-
self.u_arr[i_start:i_end],
|
|
383
|
-
self.v_arr[i_start:i_end],
|
|
384
|
-
self.w_arr[i_start:i_end],
|
|
385
|
-
) # shape (block_size, N)
|
|
386
|
-
# v_complex[i] = dot(conj(H[i,:]), x) => v_block = H_block.conj() @ x
|
|
387
|
-
ret_complex[i_start:i_end] = H_block.conj() @ x
|
|
388
|
-
|
|
389
|
-
return np.concatenate((np.real(ret_complex), np.imag(ret_complex)))
|
|
390
|
-
|
|
391
|
-
|
|
392
281
|
class DiSkO(object):
|
|
393
282
|
def __init__(self, u_arr, v_arr, w_arr, frequency):
|
|
394
283
|
self.harmonics = {} # Temporary store for harmonics
|
|
@@ -589,9 +478,6 @@ class DiSkO(object):
|
|
|
589
478
|
logger.info("frequencies: {}".format(frequencies))
|
|
590
479
|
|
|
591
480
|
A = DiSkOOperator(self.u_arr, self.v_arr, self.w_arr, data, frequencies, sphere)
|
|
592
|
-
Apre = DirectImagingOperator(
|
|
593
|
-
self.u_arr, self.v_arr, self.w_arr, data, frequencies, sphere
|
|
594
|
-
)
|
|
595
481
|
d = data.flatten()
|
|
596
482
|
|
|
597
483
|
logger.info("Data.shape {}".format(data.shape))
|
|
@@ -611,7 +497,7 @@ class DiSkO(object):
|
|
|
611
497
|
sky, niter, cost_history = pylops.optimization.sparsity.fista(
|
|
612
498
|
Op=A,
|
|
613
499
|
y=d,
|
|
614
|
-
x0=np.abs(
|
|
500
|
+
x0=np.abs(A.T @ d),
|
|
615
501
|
eps=eps,
|
|
616
502
|
tol=1e-10,
|
|
617
503
|
niter=niter,
|
|
@@ -658,7 +544,7 @@ class DiSkO(object):
|
|
|
658
544
|
if lsmr is True:
|
|
659
545
|
if alpha < 0:
|
|
660
546
|
alpha = np.mean(self.rms)
|
|
661
|
-
x0 =
|
|
547
|
+
x0 = A.T @ d
|
|
662
548
|
|
|
663
549
|
sky, info = pylops.optimization.leastsquares.normal_equations_inversion(
|
|
664
550
|
A, Regs=None, y=d, x0=x0, epsI=alpha, show=True
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright Tim Molteno 2022-2026 tim@elec.ac.nz
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import astropy.constants as const
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Resolution:
|
|
10
|
+
"""
|
|
11
|
+
Degrees (°), minutes ('), seconds (")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, x_rad):
|
|
15
|
+
self.x_rad = x_rad
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_deg(cls, x_deg):
|
|
19
|
+
return cls(np.radians(x_deg))
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_rad(cls, x_rad):
|
|
23
|
+
return cls(x_rad)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_arcmin(cls, x_arcmin):
|
|
27
|
+
return cls(np.radians(x_arcmin / 60))
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_arcsec(cls, x_arcsec):
|
|
31
|
+
return cls(np.radians(x_arcsec / 3600))
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_string(cls, x_str):
|
|
35
|
+
from angle_parser import parse_angle
|
|
36
|
+
|
|
37
|
+
# Normalize common symbols to parseable forms
|
|
38
|
+
s = x_str.replace('"', "arcsec").replace("'", "arcmin")
|
|
39
|
+
|
|
40
|
+
return cls(parse_angle(s))
|
|
41
|
+
|
|
42
|
+
def radians(self):
|
|
43
|
+
return self.x_rad
|
|
44
|
+
|
|
45
|
+
def degrees(self):
|
|
46
|
+
return np.degrees(self.x_rad)
|
|
47
|
+
|
|
48
|
+
def arcmin(self):
|
|
49
|
+
return self.degrees() * 60
|
|
50
|
+
|
|
51
|
+
def arcsec(self):
|
|
52
|
+
return self.degrees() * 3600
|
|
53
|
+
|
|
54
|
+
def __repr__(self):
|
|
55
|
+
d = self.degrees()
|
|
56
|
+
if np.abs(d) > 1:
|
|
57
|
+
return f"{d:4.2f}deg"
|
|
58
|
+
|
|
59
|
+
if np.abs(self.arcmin()) > 1:
|
|
60
|
+
return f"{self.arcmin():4.2f}arcmin"
|
|
61
|
+
|
|
62
|
+
arcsec = self.arcsec()
|
|
63
|
+
if np.abs(arcsec) > 1:
|
|
64
|
+
return f"{arcsec:4.2f}arcsec"
|
|
65
|
+
|
|
66
|
+
mas = arcsec * 1000
|
|
67
|
+
if np.abs(mas) >= 1:
|
|
68
|
+
return f"{mas:4.2f}mas"
|
|
69
|
+
|
|
70
|
+
uas = mas * 1000
|
|
71
|
+
return f"{uas:4.2f}uas"
|
|
72
|
+
|
|
73
|
+
def get_min_baseline(self, frequency):
|
|
74
|
+
"""
|
|
75
|
+
Get the shortest baseline length (in meters) that will resolve
|
|
76
|
+
this resolution, at the specified frequency.
|
|
77
|
+
|
|
78
|
+
Double-slit interferometer (spacing d). Fringe maxima
|
|
79
|
+
occur at angles where
|
|
80
|
+
d * sin(theta) = n * wavelength
|
|
81
|
+
|
|
82
|
+
n = 1: sin(theta) = wavelength/d
|
|
83
|
+
n = 2: sin(theta) = 2*wavelength/d
|
|
84
|
+
|
|
85
|
+
angular spacing = wavelength / d
|
|
86
|
+
|
|
87
|
+
so d = spacing / theta
|
|
88
|
+
"""
|
|
89
|
+
wavelength = const.c.value / frequency
|
|
90
|
+
spacing = wavelength / self.x_rad
|
|
91
|
+
return spacing * 2 # Nyquist requires twice this...
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_baseline(cls, bl, frequency):
|
|
95
|
+
res_limit = cls.rayleigh_criterion(bl, frequency)
|
|
96
|
+
return cls(res_limit / 2) # Nyquist requires twice the resolution
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def rayleigh_criterion(bl, frequency):
|
|
100
|
+
"""
|
|
101
|
+
The accepted criterion for determining the diffraction limit to resolution
|
|
102
|
+
developed by Lord Rayleigh in the 19th century.
|
|
103
|
+
|
|
104
|
+
approx resolution given by first order Bessel functions
|
|
105
|
+
assuming array is a flat disk of length bl
|
|
106
|
+
"""
|
|
107
|
+
min_wl = const.c.value / frequency
|
|
108
|
+
return 1.220 * min_wl / bl
|
|
@@ -276,23 +276,6 @@ class TestDiSkO(unittest.TestCase):
|
|
|
276
276
|
|
|
277
277
|
dottest(Op, self.disko.n_v * 2, self.sphere.npix, tol=1e-04)
|
|
278
278
|
|
|
279
|
-
Op = disko.DirectImagingOperator(
|
|
280
|
-
self.disko.u_arr,
|
|
281
|
-
self.disko.v_arr,
|
|
282
|
-
self.disko.w_arr,
|
|
283
|
-
data,
|
|
284
|
-
frequencies,
|
|
285
|
-
self.sphere,
|
|
286
|
-
)
|
|
287
|
-
pylops.utils.dottest(
|
|
288
|
-
Op,
|
|
289
|
-
self.sphere.npix,
|
|
290
|
-
self.disko.n_v * 2,
|
|
291
|
-
rtol=1e-06,
|
|
292
|
-
complexflag=0,
|
|
293
|
-
raiseerror=True,
|
|
294
|
-
verb=True,
|
|
295
|
-
)
|
|
296
279
|
|
|
297
280
|
def test_tiny_gamma(self):
|
|
298
281
|
"""
|
|
@@ -123,23 +123,6 @@ class TestPylopsOperator(unittest.TestCase):
|
|
|
123
123
|
|
|
124
124
|
dottest(Op, self.disko.n_v * 2, self.sphere.npix, tol=1e-04)
|
|
125
125
|
|
|
126
|
-
Op = disko.DirectImagingOperator(
|
|
127
|
-
self.disko.u_arr,
|
|
128
|
-
self.disko.v_arr,
|
|
129
|
-
self.disko.w_arr,
|
|
130
|
-
data,
|
|
131
|
-
frequencies,
|
|
132
|
-
self.sphere,
|
|
133
|
-
)
|
|
134
|
-
pylops.utils.dottest(
|
|
135
|
-
Op,
|
|
136
|
-
self.sphere.npix,
|
|
137
|
-
self.disko.n_v * 2,
|
|
138
|
-
rtol=1e-06,
|
|
139
|
-
complexflag=0,
|
|
140
|
-
raiseerror=True,
|
|
141
|
-
verb=True,
|
|
142
|
-
)
|
|
143
126
|
|
|
144
127
|
def test_pylops_tiny(self):
|
|
145
128
|
r"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "disko"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.2"
|
|
4
4
|
description = "Discrete Sky Operator (DiSkO) Aperture Synthesis Radio Imaging"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Tim Molteno",email = "tim@elec.ac.nz"}
|
|
@@ -21,14 +21,15 @@ dependencies = [
|
|
|
21
21
|
"pylops>=2.0",
|
|
22
22
|
"toolz>=0.11.2",
|
|
23
23
|
"imageio>=0.6.0b1",
|
|
24
|
-
"gmsh>=4.
|
|
24
|
+
"gmsh>=4.0",
|
|
25
25
|
"meshio>=5.0",
|
|
26
26
|
"dask-ms>=0.2.25,<0.3.0",
|
|
27
|
+
"angle-parser>=0.2.0",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
30
31
|
ms = [
|
|
31
|
-
"tart2ms>=0.
|
|
32
|
+
"tart2ms>=0.9.0",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[dependency-groups]
|
|
@@ -6,6 +6,15 @@ resolution-markers = [
|
|
|
6
6
|
"python_full_version < '3.12'",
|
|
7
7
|
]
|
|
8
8
|
|
|
9
|
+
[[package]]
|
|
10
|
+
name = "angle-parser"
|
|
11
|
+
version = "0.2.0"
|
|
12
|
+
source = { registry = "https://pypi.org/simple" }
|
|
13
|
+
sdist = { url = "https://files.pythonhosted.org/packages/2a/f7/d23c265e13daa3885406da19dd9798fc121ce0fb008252b38b4a18b8d432/angle_parser-0.2.0.tar.gz", hash = "sha256:83f9444113e4ab0da243fb8139d3dcd20d2b0facf63253e97fd41ece47e0f22f", size = 2748, upload-time = "2026-07-07T02:03:43.583Z" }
|
|
14
|
+
wheels = [
|
|
15
|
+
{ url = "https://files.pythonhosted.org/packages/71/45/4250a354331ea3a36e7abd14fa1313bb87e59c197061a009afa66b0c24ba/angle_parser-0.2.0-py3-none-any.whl", hash = "sha256:8bba1bf8b03acdc04ffb7dfe21a74981ecceaab671c3d36bf95fd34a6271c30c", size = 2840, upload-time = "2026-07-07T02:03:42.288Z" },
|
|
16
|
+
]
|
|
17
|
+
|
|
9
18
|
[[package]]
|
|
10
19
|
name = "appdirs"
|
|
11
20
|
version = "1.4.4"
|
|
@@ -370,9 +379,10 @@ wheels = [
|
|
|
370
379
|
|
|
371
380
|
[[package]]
|
|
372
381
|
name = "disko"
|
|
373
|
-
version = "1.
|
|
382
|
+
version = "1.4.1"
|
|
374
383
|
source = { editable = "." }
|
|
375
384
|
dependencies = [
|
|
385
|
+
{ name = "angle-parser" },
|
|
376
386
|
{ name = "dask-ms" },
|
|
377
387
|
{ name = "gmsh" },
|
|
378
388
|
{ name = "h5py" },
|
|
@@ -403,6 +413,7 @@ dev = [
|
|
|
403
413
|
|
|
404
414
|
[package.metadata]
|
|
405
415
|
requires-dist = [
|
|
416
|
+
{ name = "angle-parser", specifier = "==0.2.0" },
|
|
406
417
|
{ name = "dask-ms", specifier = ">=0.2.25,<0.3.0" },
|
|
407
418
|
{ name = "gmsh", specifier = ">=4.9" },
|
|
408
419
|
{ name = "h5py", specifier = ">=3.7" },
|
disko-1.4.0/disko/resolution.py
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright Tim Molteno 2022-2026 tim@elec.ac.nz
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
import astropy.constants as const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def parse_ending(x_str, ending):
|
|
11
|
-
if x_str.endswith(ending):
|
|
12
|
-
return float(x_str.split(ending)[0])
|
|
13
|
-
return None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
DEGREES = "deg"
|
|
17
|
-
MINUTES = 'arcmin'
|
|
18
|
-
SECONDS = 'arcsec'
|
|
19
|
-
MILLIARCSECONDS = 'mas'
|
|
20
|
-
MICROARCSECONDS = 'uas'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def rayleigh_criterion(bl, frequency):
|
|
24
|
-
'''
|
|
25
|
-
The accepted criterion for determining the diffraction limit to resolution
|
|
26
|
-
developed by Lord Rayleigh in the 19th century.
|
|
27
|
-
|
|
28
|
-
approx resolution given by first order Bessel functions
|
|
29
|
-
assuming array is a flat disk of length bl
|
|
30
|
-
'''
|
|
31
|
-
min_wl = const.c.value / frequency
|
|
32
|
-
return (1.220 * min_wl / bl)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Resolution:
|
|
36
|
-
'''
|
|
37
|
-
Degrees (°), minutes ('), seconds (")
|
|
38
|
-
'''
|
|
39
|
-
|
|
40
|
-
def __init__(self, x_rad):
|
|
41
|
-
self.x_rad = x_rad
|
|
42
|
-
|
|
43
|
-
@classmethod
|
|
44
|
-
def from_deg(cls, x_deg):
|
|
45
|
-
return cls(np.radians(x_deg))
|
|
46
|
-
|
|
47
|
-
@classmethod
|
|
48
|
-
def from_rad(cls, x_rad):
|
|
49
|
-
return cls(x_rad)
|
|
50
|
-
|
|
51
|
-
@classmethod
|
|
52
|
-
def from_arcmin(cls, x_arcmin):
|
|
53
|
-
return cls(np.radians(x_arcmin/60))
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def from_arcsec(cls, x_arcsec):
|
|
57
|
-
return cls(np.radians(x_arcsec/3600))
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def from_string(cls, x_str):
|
|
61
|
-
endings = [MICROARCSECONDS, MILLIARCSECONDS, SECONDS, MINUTES, DEGREES]
|
|
62
|
-
deg_factors = [3600*1000000, 3600*1000, 3600, 60, 1]
|
|
63
|
-
parsed = [parse_ending(x_str, e) for e in endings]
|
|
64
|
-
for p, f in zip(parsed, deg_factors):
|
|
65
|
-
if p is not None:
|
|
66
|
-
return cls.from_deg(p/f)
|
|
67
|
-
|
|
68
|
-
return cls.from_deg(float(x_str))
|
|
69
|
-
|
|
70
|
-
def radians(self):
|
|
71
|
-
return self.x_rad
|
|
72
|
-
|
|
73
|
-
def degrees(self):
|
|
74
|
-
return np.degrees(self.x_rad)
|
|
75
|
-
|
|
76
|
-
def arcmin(self):
|
|
77
|
-
return self.degrees() * 60
|
|
78
|
-
|
|
79
|
-
def arcsec(self):
|
|
80
|
-
return self.degrees() * 3600
|
|
81
|
-
|
|
82
|
-
def mas(self):
|
|
83
|
-
return np.de
|
|
84
|
-
|
|
85
|
-
def __repr__(self):
|
|
86
|
-
d = self.degrees()
|
|
87
|
-
if np.abs(d) > 1:
|
|
88
|
-
return f"{d:4.2f}deg"
|
|
89
|
-
|
|
90
|
-
if np.abs(self.arcmin()) > 1:
|
|
91
|
-
return f"{self.arcmin():4.2f}{MINUTES}"
|
|
92
|
-
|
|
93
|
-
arcsec = self.arcsec()
|
|
94
|
-
if np.abs(arcsec) > 1:
|
|
95
|
-
return f"{arcsec:4.2f}{SECONDS}"
|
|
96
|
-
|
|
97
|
-
mas = arcsec * 1000
|
|
98
|
-
if np.abs(mas) >= 1:
|
|
99
|
-
return f"{mas:4.2f}{MILLIARCSECONDS}"
|
|
100
|
-
|
|
101
|
-
uas = mas * 1000
|
|
102
|
-
return f"{uas:4.2f}{MICROARCSECONDS}"
|
|
103
|
-
|
|
104
|
-
def get_min_baseline(self, frequency):
|
|
105
|
-
'''
|
|
106
|
-
Get the shortest baseline length (in meters) that will resolve
|
|
107
|
-
this resolution, at the specified frequency.
|
|
108
|
-
|
|
109
|
-
Double-slit interferometer (spacing d). Fringe maxima
|
|
110
|
-
occur at angles where
|
|
111
|
-
d * sin(theta) = n * wavelength
|
|
112
|
-
|
|
113
|
-
n = 1: sin(theta) = wavelength/d
|
|
114
|
-
n = 2: sin(theta) = 2*wavelength/d
|
|
115
|
-
|
|
116
|
-
angular spacing = wavelength / d
|
|
117
|
-
|
|
118
|
-
so d = spacing / theta
|
|
119
|
-
'''
|
|
120
|
-
wavelength = const.c.value / frequency
|
|
121
|
-
spacing = wavelength / self.x_rad
|
|
122
|
-
return spacing*2 # Nyquist requires twice this...
|
|
123
|
-
|
|
124
|
-
@classmethod
|
|
125
|
-
def from_baseline(cls, bl, frequency):
|
|
126
|
-
res_limit = rayleigh_criterion(bl, frequency)
|
|
127
|
-
return cls(res_limit / 2) # Nyquist requires twice the resolution
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|