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.
Files changed (53) hide show
  1. {disko-1.4.0 → disko-1.4.2}/CHANGES.md +13 -0
  2. {disko-1.4.0 → disko-1.4.2}/Makefile +1 -1
  3. {disko-1.4.0 → disko-1.4.2}/PKG-INFO +4 -3
  4. {disko-1.4.0 → disko-1.4.2}/disko/__init__.py +0 -1
  5. {disko-1.4.0 → disko-1.4.2}/disko/disko.py +4 -118
  6. disko-1.4.2/disko/resolution.py +108 -0
  7. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_disko.py +0 -17
  8. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_pylops_operator.py +0 -17
  9. {disko-1.4.0 → disko-1.4.2}/pyproject.toml +4 -3
  10. {disko-1.4.0 → disko-1.4.2}/uv.lock +12 -1
  11. disko-1.4.0/disko/resolution.py +0 -127
  12. {disko-1.4.0 → disko-1.4.2}/.github/workflows/package.yaml +0 -0
  13. {disko-1.4.0 → disko-1.4.2}/.github/workflows/testing.yaml +0 -0
  14. {disko-1.4.0 → disko-1.4.2}/.gitignore +0 -0
  15. {disko-1.4.0 → disko-1.4.2}/.travis.yml +0 -0
  16. {disko-1.4.0 → disko-1.4.2}/CITATION.cff +0 -0
  17. {disko-1.4.0 → disko-1.4.2}/LICENSE.txt +0 -0
  18. {disko-1.4.0 → disko-1.4.2}/MANIFEST.in +0 -0
  19. {disko-1.4.0 → disko-1.4.2}/README.md +0 -0
  20. {disko-1.4.0 → disko-1.4.2}/disko/bayes_cli.py +0 -0
  21. {disko-1.4.0 → disko-1.4.2}/disko/cli.py +0 -0
  22. {disko-1.4.0 → disko-1.4.2}/disko/draw_cli.py +0 -0
  23. {disko-1.4.0 → disko-1.4.2}/disko/draw_sky.py +0 -0
  24. {disko-1.4.0 → disko-1.4.2}/disko/fov/__init__.py +0 -0
  25. {disko-1.4.0 → disko-1.4.2}/disko/fov/factory.py +0 -0
  26. {disko-1.4.0 → disko-1.4.2}/disko/healpix_sphere.py +0 -0
  27. {disko-1.4.0 → disko-1.4.2}/disko/ms_helper.py +0 -0
  28. {disko-1.4.0 → disko-1.4.2}/disko/multivariate_gaussian.py +0 -0
  29. {disko-1.4.0 → disko-1.4.2}/disko/parser_support.py +0 -0
  30. {disko-1.4.0 → disko-1.4.2}/disko/projection_lsqr.py +0 -0
  31. {disko-1.4.0 → disko-1.4.2}/disko/rime.py +0 -0
  32. {disko-1.4.0 → disko-1.4.2}/disko/sphere.py +0 -0
  33. {disko-1.4.0 → disko-1.4.2}/disko/sphere_mesh.py +0 -0
  34. {disko-1.4.0 → disko-1.4.2}/disko/svd_cli.py +0 -0
  35. {disko-1.4.0 → disko-1.4.2}/disko/telescope_operator.py +0 -0
  36. {disko-1.4.0 → disko-1.4.2}/disko/tests/__init__.py +0 -0
  37. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_meshsphere.py +0 -0
  38. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_multivariate.py +0 -0
  39. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_projection_lsqr.py +0 -0
  40. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_sphere.py +0 -0
  41. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_subsphere.py +0 -0
  42. {disko-1.4.0 → disko-1.4.2}/disko/tests/test_telescope_operator.py +0 -0
  43. {disko-1.4.0 → disko-1.4.2}/disko/util.py +0 -0
  44. {disko-1.4.0 → disko-1.4.2}/docker/Dockerfile +0 -0
  45. {disko-1.4.0 → disko-1.4.2}/docker/Makefile +0 -0
  46. {disko-1.4.0 → disko-1.4.2}/docker/docker-compose.yml +0 -0
  47. {disko-1.4.0 → disko-1.4.2}/install.sh +0 -0
  48. {disko-1.4.0 → disko-1.4.2}/movie/.gitignore +0 -0
  49. {disko-1.4.0 → disko-1.4.2}/movie/Makefile +0 -0
  50. {disko-1.4.0 → disko-1.4.2}/prof.py +0 -0
  51. {disko-1.4.0 → disko-1.4.2}/requirements.txt +0 -0
  52. {disko-1.4.0 → disko-1.4.2}/sequential_test.sh +0 -0
  53. {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 --alpha 1e-7 --niter 200 --nvis 10000 --title 'cygnus_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.0
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.9
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.7.1; extra == 'ms'
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
@@ -8,7 +8,6 @@ import logging # noqa: F401
8
8
  # Public API — imported by external users
9
9
  from .cli import disko_from_ms # noqa: F401
10
10
  from .disko import ( # noqa: F401
11
- DirectImagingOperator,
12
11
  DiSkO,
13
12
  DiSkOOperator,
14
13
  get_all_uvw,
@@ -247,12 +247,12 @@ class DiSkOOperator(pylops.LinearOperator):
247
247
 
248
248
  def _rmatvec(self, v):
249
249
  r"""
250
- Returns x = A^H * v, where A^H is the conjugate transpose of 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 float32 harmonic blocks.
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(Apre @ d),
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 = Apre @ d
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.0"
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.9",
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.7.1",
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.3.3"
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" },
@@ -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