scatterbrane 0.1.0__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.
- scatterbrane-0.1.0/LICENSE +19 -0
- scatterbrane-0.1.0/PKG-INFO +65 -0
- scatterbrane-0.1.0/README.rst +41 -0
- scatterbrane-0.1.0/pyproject.toml +71 -0
- scatterbrane-0.1.0/scatterbrane/__init__.py +4 -0
- scatterbrane-0.1.0/scatterbrane/brane.py +481 -0
- scatterbrane-0.1.0/scatterbrane/tracks.py +397 -0
- scatterbrane-0.1.0/scatterbrane/utilities.py +190 -0
- scatterbrane-0.1.0/scatterbrane.egg-info/PKG-INFO +65 -0
- scatterbrane-0.1.0/scatterbrane.egg-info/SOURCES.txt +12 -0
- scatterbrane-0.1.0/scatterbrane.egg-info/dependency_links.txt +1 -0
- scatterbrane-0.1.0/scatterbrane.egg-info/requires.txt +5 -0
- scatterbrane-0.1.0/scatterbrane.egg-info/top_level.txt +1 -0
- scatterbrane-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) <2015> <Katherine Rosenfeld and Michael Johnson>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scatterbrane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A python module to simulate the effect of anisotropic scattering on astrophysical images.
|
|
5
|
+
Author-email: Katherine Rosenfeld <krosenf@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, http://krosenfeld.github.io/scatterbrane/
|
|
8
|
+
Keywords: scattering,astronomy,EHT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: astropy
|
|
19
|
+
Requires-Dist: imageio
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: scipy
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
scatterbrane
|
|
26
|
+
============
|
|
27
|
+
|
|
28
|
+
A python module for adding realistic scattering to astronomical images. This version
|
|
29
|
+
represents a very early release so please submit a pull request if you have trouble or
|
|
30
|
+
need help for your application.
|
|
31
|
+
|
|
32
|
+
For installation instructions and help getting started please see `the documentation <http://krosenfeld.github.io/scatterbrane/>`_.
|
|
33
|
+
|
|
34
|
+
Installation
|
|
35
|
+
------------
|
|
36
|
+
|
|
37
|
+
This project now uses ``pyproject.toml`` and `uv <https://docs.astral.sh/uv/>`_ for dependency management.
|
|
38
|
+
|
|
39
|
+
Install the package and its runtime dependencies with:
|
|
40
|
+
|
|
41
|
+
.. code-block:: bash
|
|
42
|
+
|
|
43
|
+
uv sync
|
|
44
|
+
|
|
45
|
+
Install the release and development tooling with:
|
|
46
|
+
|
|
47
|
+
.. code-block:: bash
|
|
48
|
+
|
|
49
|
+
uv sync --group dev
|
|
50
|
+
|
|
51
|
+
Release workflow
|
|
52
|
+
----------------
|
|
53
|
+
|
|
54
|
+
Version management is handled with ``bump-my-version``. To create a new tagged release:
|
|
55
|
+
|
|
56
|
+
.. code-block:: bash
|
|
57
|
+
|
|
58
|
+
uv run bump-my-version bump patch
|
|
59
|
+
|
|
60
|
+
This updates the package version, creates a commit, and creates a ``vX.Y.Z`` git tag. Pushing that tag to GitHub triggers the publish workflow for PyPI.
|
|
61
|
+
|
|
62
|
+
Reference
|
|
63
|
+
---------
|
|
64
|
+
|
|
65
|
+
`Johnson, M. D., & Gwinn, C. R. 2015, ApJ, 805, 180 <http://adsabs.harvard.edu/abs/2015ApJ...805..180J>`_
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
scatterbrane
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
A python module for adding realistic scattering to astronomical images. This version
|
|
5
|
+
represents a very early release so please submit a pull request if you have trouble or
|
|
6
|
+
need help for your application.
|
|
7
|
+
|
|
8
|
+
For installation instructions and help getting started please see `the documentation <http://krosenfeld.github.io/scatterbrane/>`_.
|
|
9
|
+
|
|
10
|
+
Installation
|
|
11
|
+
------------
|
|
12
|
+
|
|
13
|
+
This project now uses ``pyproject.toml`` and `uv <https://docs.astral.sh/uv/>`_ for dependency management.
|
|
14
|
+
|
|
15
|
+
Install the package and its runtime dependencies with:
|
|
16
|
+
|
|
17
|
+
.. code-block:: bash
|
|
18
|
+
|
|
19
|
+
uv sync
|
|
20
|
+
|
|
21
|
+
Install the release and development tooling with:
|
|
22
|
+
|
|
23
|
+
.. code-block:: bash
|
|
24
|
+
|
|
25
|
+
uv sync --group dev
|
|
26
|
+
|
|
27
|
+
Release workflow
|
|
28
|
+
----------------
|
|
29
|
+
|
|
30
|
+
Version management is handled with ``bump-my-version``. To create a new tagged release:
|
|
31
|
+
|
|
32
|
+
.. code-block:: bash
|
|
33
|
+
|
|
34
|
+
uv run bump-my-version bump patch
|
|
35
|
+
|
|
36
|
+
This updates the package version, creates a commit, and creates a ``vX.Y.Z`` git tag. Pushing that tag to GitHub triggers the publish workflow for PyPI.
|
|
37
|
+
|
|
38
|
+
Reference
|
|
39
|
+
---------
|
|
40
|
+
|
|
41
|
+
`Johnson, M. D., & Gwinn, C. R. 2015, ApJ, 805, 180 <http://adsabs.harvard.edu/abs/2015ApJ...805..180J>`_
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scatterbrane"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A python module to simulate the effect of anisotropic scattering on astrophysical images."
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Katherine Rosenfeld", email = "krosenf@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["scattering", "astronomy", "EHT"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"astropy",
|
|
27
|
+
"imageio",
|
|
28
|
+
"matplotlib",
|
|
29
|
+
"numpy",
|
|
30
|
+
"scipy",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Documentation = "http://krosenfeld.github.io/scatterbrane/"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"bump-my-version",
|
|
39
|
+
"build",
|
|
40
|
+
]
|
|
41
|
+
docs = [
|
|
42
|
+
"sphinx",
|
|
43
|
+
"sphinx-rtd-theme",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
packages = ["scatterbrane"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.dynamic]
|
|
50
|
+
version = { attr = "scatterbrane.__version__" }
|
|
51
|
+
|
|
52
|
+
[tool.bumpversion]
|
|
53
|
+
current_version = "0.1.0"
|
|
54
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
55
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
56
|
+
search = "{current_version}"
|
|
57
|
+
replace = "{new_version}"
|
|
58
|
+
regex = false
|
|
59
|
+
allow_dirty = false
|
|
60
|
+
commit = true
|
|
61
|
+
tag = true
|
|
62
|
+
sign_tags = false
|
|
63
|
+
tag_name = "v{new_version}"
|
|
64
|
+
tag_message = "Release {new_version}"
|
|
65
|
+
message = "Bump version: {current_version} -> {new_version}"
|
|
66
|
+
|
|
67
|
+
[[tool.bumpversion.files]]
|
|
68
|
+
filename = "pyproject.toml"
|
|
69
|
+
|
|
70
|
+
[[tool.bumpversion.files]]
|
|
71
|
+
filename = "scatterbrane/__init__.py"
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: brane
|
|
3
|
+
:platform: Unix
|
|
4
|
+
:synopsis: Simulate effect of anisotropic scattering.
|
|
5
|
+
|
|
6
|
+
.. moduleauthor:: Katherine Rosenfeld <krosenf@gmail.com>
|
|
7
|
+
.. moduleauthor:: Michael Johnson
|
|
8
|
+
|
|
9
|
+
Default settings are appropriate for Sgr A*
|
|
10
|
+
|
|
11
|
+
Resources:
|
|
12
|
+
Bower et al. (2004, 2006)
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import print_function
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import matplotlib.pyplot as plt
|
|
20
|
+
from scipy.interpolate import RectBivariateSpline
|
|
21
|
+
from scipy.ndimage.filters import gaussian_filter
|
|
22
|
+
from scipy.ndimage.interpolation import zoom,rotate
|
|
23
|
+
from numpy import (pi,sqrt,log,sin,cos,exp,ceil,arange,
|
|
24
|
+
min,abs,ones,radians,dot,transpose,
|
|
25
|
+
zeros_like,clip,empty,empty,empty_like,reshape)
|
|
26
|
+
from numpy.fft import fft2,fftfreq
|
|
27
|
+
from astropy.constants import au,pc
|
|
28
|
+
from astropy import units
|
|
29
|
+
import logging
|
|
30
|
+
from scatterbrane import utilities
|
|
31
|
+
|
|
32
|
+
__all__ = ["Brane"]
|
|
33
|
+
|
|
34
|
+
class Brane(object):
|
|
35
|
+
"""
|
|
36
|
+
Scattering simulation object.
|
|
37
|
+
|
|
38
|
+
:param model: ``(n, n)``
|
|
39
|
+
Numpy array of the source image.
|
|
40
|
+
:param dx: scalar
|
|
41
|
+
Resolution element of model in uas.
|
|
42
|
+
:param nphi: (optional) ``(2, )`` or scalar
|
|
43
|
+
Number of pixels in a screen. This may be a tuple specifying the dimensions of a rectangular screen.
|
|
44
|
+
:param screen_res: (optional) scalar
|
|
45
|
+
Size of a screen pixel in units of :math:`R_0`.
|
|
46
|
+
:param wavelength: (optional) scalar
|
|
47
|
+
Observing wavelength in meters.
|
|
48
|
+
:param dpc: (optional) scalar
|
|
49
|
+
Observer-Source distance in parsecs.
|
|
50
|
+
:param rpc: (optional) scalar
|
|
51
|
+
Observer-Scatterer distance in parsecs.
|
|
52
|
+
:param r0: (optional) scalar
|
|
53
|
+
Phase coherence length along major axis as preset string or in km.
|
|
54
|
+
:param r_outer: (optional) scalar
|
|
55
|
+
Outer scale of turbulence in units of :math:`R_0`.
|
|
56
|
+
:param r_inner: (optional) scalar
|
|
57
|
+
Inner scale of turbulence in units of :math:`R_0`.
|
|
58
|
+
:param alpha: (optional) string or scalar
|
|
59
|
+
Preset string or float to set power-law index for tubulence scaling (e.g., Kolmogorov has :math:`\\alpha= 5/3`)
|
|
60
|
+
:param anisotropy: (optional) scalar
|
|
61
|
+
Anisotropy for screen is the EW / NS elongation.
|
|
62
|
+
:param pa: (optional) scalar
|
|
63
|
+
Orientation of kernel's major axis, East of North, in degrees.
|
|
64
|
+
:param live_dangerously: (optional) bool
|
|
65
|
+
Skip the parameter checks?
|
|
66
|
+
:param think_positive: (optional) bool
|
|
67
|
+
Should we enforce that the source image has no negative pixel values?
|
|
68
|
+
|
|
69
|
+
:returns: An instance of a scattering simulation.
|
|
70
|
+
|
|
71
|
+
:Example:
|
|
72
|
+
|
|
73
|
+
.. code-block:: python
|
|
74
|
+
|
|
75
|
+
s = Brane(m,dx,nphi=2**12,screen_res=5.,wavelength=3.0e-3,dpc=8.4e3,rpc=5.8e3)
|
|
76
|
+
|
|
77
|
+
where ``s`` is the class instance, ``m`` is the image array, ``nphi`` is the number of screen pixels,
|
|
78
|
+
``wavelength`` is the observing wavelength.
|
|
79
|
+
|
|
80
|
+
.. note:: :math:`R_0` is the phase coherence length and Sgr A* defaults are from Bower et al. (2006).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self,model,dx,nphi=2**12,screen_res=2,\
|
|
84
|
+
wavelength=1.3e-3,dpc=8400,rpc=5800,r0 = 'sgra',\
|
|
85
|
+
r_outer=10000000,r_inner=12,alpha='kolmogorov',\
|
|
86
|
+
anisotropy=2.045,pa=78,match_screen_res=False,live_dangerously=False,think_positive=False):
|
|
87
|
+
|
|
88
|
+
# set initial members
|
|
89
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
90
|
+
self.live_dangerously = live_dangerously
|
|
91
|
+
self.think_positive = think_positive
|
|
92
|
+
|
|
93
|
+
self.wavelength = wavelength*1e-3 # observing wavelength in km
|
|
94
|
+
self.dpc = float(dpc) # earth-source distance in pc
|
|
95
|
+
self.rpc = float(rpc) # R: source-scatterer distance in pc
|
|
96
|
+
self.d = self.dpc - self.rpc # D: earth-scatterer distance in pc
|
|
97
|
+
self.m = self.d/self.rpc # magnification factor (D/R)
|
|
98
|
+
if r0 == 'sgra':
|
|
99
|
+
# major axis (EW) phase coherence length in km
|
|
100
|
+
self.r0 = 3136.67*(1.3e-6/self.wavelength)
|
|
101
|
+
else:
|
|
102
|
+
try:
|
|
103
|
+
self.r0 = float(r0)
|
|
104
|
+
except:
|
|
105
|
+
raise ValueError('Bad value for r0')
|
|
106
|
+
self.anisotropy = anisotropy # anisotropy for screen = (EW / NS elongation)
|
|
107
|
+
self.pa = pa # orientation of major axis, E of N (or CCW of +y)
|
|
108
|
+
|
|
109
|
+
# Fresnel scale in km
|
|
110
|
+
self.rf = sqrt(self.dpc*pc.to(units.km).value / (2*pi / self.wavelength) * self.m / (1+self.m)**2)
|
|
111
|
+
|
|
112
|
+
# compute pixel scale for image
|
|
113
|
+
if match_screen_res:
|
|
114
|
+
self.ips = 1
|
|
115
|
+
self.screen_dx = screen_res * self.r0
|
|
116
|
+
else:
|
|
117
|
+
self.screen_dx = screen_res * self.r0 # size of a screen pixel in km
|
|
118
|
+
self.ips = int(ceil(1e-6*dx*self.d*au.to(units.km).value/self.screen_dx)) # image pixel / screen pixel
|
|
119
|
+
|
|
120
|
+
# image arrays
|
|
121
|
+
self.dx = 1e6 * self.ips * (self.screen_dx / (au.to(units.km).value * self.d)) # image pixel scale in uas
|
|
122
|
+
self.nx = int(ceil(model.shape[-1] * dx / self.dx)) # number of image pixels
|
|
123
|
+
self.model = model # source model
|
|
124
|
+
self.model_dx = dx # source model resolution
|
|
125
|
+
self.iss = np.array([],dtype=np.float64) # scattered image
|
|
126
|
+
self.isrc = np.array([],dtype=np.float64) # source image at same scale as scattered image
|
|
127
|
+
|
|
128
|
+
# screen parameters
|
|
129
|
+
if type(nphi) == int:
|
|
130
|
+
self.nphi = (nphi,nphi) # size of screen array
|
|
131
|
+
else:
|
|
132
|
+
self.nphi = nphi
|
|
133
|
+
self.nphi = np.asarray(self.nphi)
|
|
134
|
+
self.r_inner = r_inner # inner turbulence scale in r0
|
|
135
|
+
self.r_outer = r_outer # outer turbulence scale in r0
|
|
136
|
+
#self.qmax = 1.*screen_res/r_inner # 1 / inner turbulence scale in pix
|
|
137
|
+
#self.qmin = 1.*screen_res/r_outer # 1 / outer tubulence scale in pix
|
|
138
|
+
if alpha == 'kolmogorov':
|
|
139
|
+
self.alpha = 5./3
|
|
140
|
+
else:
|
|
141
|
+
try:
|
|
142
|
+
self.alpha = float(alpha)
|
|
143
|
+
except:
|
|
144
|
+
raise ValueError('Bad value for alpha')
|
|
145
|
+
|
|
146
|
+
# use logger to report
|
|
147
|
+
self.chatter()
|
|
148
|
+
|
|
149
|
+
# includes sanity check
|
|
150
|
+
self.setModel(self.model,self.model_dx,think_positive=self.think_positive)
|
|
151
|
+
|
|
152
|
+
def _checkSanity(self):
|
|
153
|
+
'''
|
|
154
|
+
Check that screen parameters are consistent.
|
|
155
|
+
'''
|
|
156
|
+
|
|
157
|
+
# sanity check: is screen large enough?
|
|
158
|
+
assert np.ceil(self.nx*self.ips)+2 < np.min(self.nphi), \
|
|
159
|
+
"screen is not large enough: {0} > {1}".\
|
|
160
|
+
format(int(np.ceil(self.ips*self.nx)+2),np.min(self.nphi))
|
|
161
|
+
|
|
162
|
+
# sanity check: is image square?
|
|
163
|
+
assert self.model.shape[-1] == self.model.shape[-2], \
|
|
164
|
+
'source image must be square'
|
|
165
|
+
|
|
166
|
+
# sanity check: integer number of screen pixels / image pixel?
|
|
167
|
+
assert self.ips % 1 == 0, 'image/screen pixels should be an integer'
|
|
168
|
+
|
|
169
|
+
# is inner turbulence scale larger than r0?
|
|
170
|
+
#assert 1./self.qmax > self.r0/self.screen_dx, 'turbulence inner scale < r0'
|
|
171
|
+
assert self.r_inner > 1., 'turbulence inner scale < r0'
|
|
172
|
+
|
|
173
|
+
# check image smoothness
|
|
174
|
+
V = fft2(self.isrc)
|
|
175
|
+
freq = fftfreq(self.nx,d=self.dx*radians(1.)/(3600*1e6))
|
|
176
|
+
u = dot(transpose([np.ones(self.nx)]),[freq])
|
|
177
|
+
v = dot(transpose([freq]),[ones(self.nx)])
|
|
178
|
+
try:
|
|
179
|
+
if max(abs(V[sqrt(u*u+v*v) > (1.+self.m)*self.r_inner*self.r0/self.wavelength])) / self.isrc.sum() > 0.01:
|
|
180
|
+
self.logger.warning('image is not smooth enough: {0:g} > 0.01'.format(max(abs(V[sqrt(u*u+v*v) > (1.+self.m)*self.r_inner*self.r0/self.wavelength])) / self.isrc.sum()))
|
|
181
|
+
except ValueError:
|
|
182
|
+
self.logger.warning('r_inner is too large to test smoothness')
|
|
183
|
+
|
|
184
|
+
# is screen pixel smaller than inner turbulence scale?
|
|
185
|
+
#assert 1./self.qmax >= 1, 'screen pixel > turbulence inner scale'
|
|
186
|
+
assert self.r_inner*self.r0/self.screen_dx >= 1, 'screen pixel > turbulence inner scale'
|
|
187
|
+
|
|
188
|
+
if (self.rf*self.rf/self.r0/(self.ips*self.screen_dx) < 3):
|
|
189
|
+
self.logger.warning('WARNING: image resolution is approaching Refractive scale')
|
|
190
|
+
|
|
191
|
+
def chatter(self):
|
|
192
|
+
'''
|
|
193
|
+
Print information about the current scattering simulation where many parameters are cast as integers.
|
|
194
|
+
'''
|
|
195
|
+
|
|
196
|
+
fmt = "{0:32s} :: "
|
|
197
|
+
self.logger.info( (fmt + "{1:g}").format('Observing wavelength [mm]',1e6*self.wavelength) )
|
|
198
|
+
self.logger.info( (fmt + "{1:d}").format('Phase coherence length [km]',int(self.r0)) )
|
|
199
|
+
self.logger.info( (fmt + "{1:d}").format('Fresnel scale [km]',int(self.rf)) )
|
|
200
|
+
self.logger.info( (fmt + "{1:d}").format('Refractive scale [km]',int(self.rf**2/self.r0)) )
|
|
201
|
+
#self.logger.info( (fmt + "{1:d}").format('Inner turbulence scale [km]',int(self.screen_dx/self.qmax)))
|
|
202
|
+
self.logger.info( (fmt + "{1:d}").format('Inner turbulence scale [km]',int(self.r_inner*self.r0)))
|
|
203
|
+
self.logger.info( (fmt + "{1:d}").format('Screen resolution [km]',int(self.screen_dx)))
|
|
204
|
+
self.logger.info( (fmt + "{1:d} {2:d}").format('Linear filling factor [%,%]',*map(int,100.*self.nx*self.ips/self.nphi)) )
|
|
205
|
+
self.logger.info( (fmt + "{1:g}").format('Image resolution [uas]',self.dx))
|
|
206
|
+
self.logger.info( (fmt + "{1:d}").format('Image size',int(self.nx)))
|
|
207
|
+
|
|
208
|
+
def _generateEmptyScreen(self):
|
|
209
|
+
'''
|
|
210
|
+
Create an empty screen.
|
|
211
|
+
'''
|
|
212
|
+
if not self.live_dangerously: self._checkSanity()
|
|
213
|
+
self.phi = np.zeros(self.nphi)
|
|
214
|
+
|
|
215
|
+
def setModel(self,model,dx,think_positive=False):
|
|
216
|
+
'''
|
|
217
|
+
Set new model for the source.
|
|
218
|
+
|
|
219
|
+
:param model: ``(n, n)``
|
|
220
|
+
Numpy image array.
|
|
221
|
+
:param dx: scalar
|
|
222
|
+
Pixel size in microarcseconds.
|
|
223
|
+
:param think_positive: (optional) bool
|
|
224
|
+
Should we enforce that the source image has no negative pixel values?
|
|
225
|
+
'''
|
|
226
|
+
self.nx = int(ceil(model.shape[-1] * dx / self.dx)) # number of image pixels
|
|
227
|
+
self.model = model # source model
|
|
228
|
+
self.model_dx = dx # source model resolution
|
|
229
|
+
|
|
230
|
+
# load source image that has size and resolution compatible with the screen.
|
|
231
|
+
self.isrc = np.empty(2*(self.nx,))
|
|
232
|
+
self.think_positive = think_positive
|
|
233
|
+
|
|
234
|
+
M = self.model.shape[1] # size of original image array
|
|
235
|
+
f_img = RectBivariateSpline(self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
236
|
+
self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
237
|
+
self.model)
|
|
238
|
+
|
|
239
|
+
xx_,yy_ = np.meshgrid((np.arange(self.nx) - 0.5*(self.nx-1)),\
|
|
240
|
+
(np.arange(self.nx) - 0.5*(self.nx-1)),indexing='xy')
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
m = f_img.ev(yy_.flatten(),xx_.flatten()).reshape(2*(self.nx,))
|
|
244
|
+
self.isrc = m * (self.dx/self.model_dx)**2 # rescale for change in pixel size
|
|
245
|
+
|
|
246
|
+
if self.think_positive:
|
|
247
|
+
self.isrc[self.isrc < 0] = 0
|
|
248
|
+
|
|
249
|
+
if not self.live_dangerously: self._checkSanity()
|
|
250
|
+
|
|
251
|
+
def generatePhases(self,seed=None,save_phi=None):
|
|
252
|
+
'''
|
|
253
|
+
Generate screen phases.
|
|
254
|
+
|
|
255
|
+
:param seed: (optional) scalar
|
|
256
|
+
Seed for random number generator
|
|
257
|
+
:param save_phi: (optional) string
|
|
258
|
+
To save the screen, set this to the filename.
|
|
259
|
+
'''
|
|
260
|
+
|
|
261
|
+
# check setup
|
|
262
|
+
if not self.live_dangerously: self._checkSanity()
|
|
263
|
+
|
|
264
|
+
# seed the generator
|
|
265
|
+
if seed != None:
|
|
266
|
+
np.random.seed(seed=seed)
|
|
267
|
+
|
|
268
|
+
# include anisotropy
|
|
269
|
+
qx2 = dot(transpose([np.ones(self.nphi[0])]),[np.fft.rfftfreq(self.nphi[1])**2])
|
|
270
|
+
qy2 = dot(transpose([np.fft.fftfreq(self.nphi[0])**2*self.anisotropy**2]),[np.ones(self.nphi[1]//2+1)])
|
|
271
|
+
rr = qx2+qy2
|
|
272
|
+
rr[0,0] = 0.02 # arbitrary normalization
|
|
273
|
+
|
|
274
|
+
# generating phases with given power spectrum
|
|
275
|
+
size = rr.shape
|
|
276
|
+
qmax2 = (self.r_inner*self.r0/self.screen_dx)**-2
|
|
277
|
+
qmin2 = (self.r_outer*self.r0/self.screen_dx)**-2
|
|
278
|
+
phi_t = (1/sqrt(2) * sqrt(exp(-1./qmax2*rr) * (rr + qmin2)**(-0.5*(self.alpha+2.)))) \
|
|
279
|
+
* (np.random.normal(size=size) + 1j * np.random.normal(size=size))
|
|
280
|
+
|
|
281
|
+
# calculate phi
|
|
282
|
+
self.phi = np.fft.irfft2(phi_t)
|
|
283
|
+
|
|
284
|
+
# normalize structure function
|
|
285
|
+
nrm = self.screen_dx/(self.r0*sqrt(self._getPhi(1,0)))
|
|
286
|
+
self.phi *= nrm
|
|
287
|
+
|
|
288
|
+
# save screen
|
|
289
|
+
if save_phi != None:
|
|
290
|
+
np.save(save_phi,self.phi)
|
|
291
|
+
|
|
292
|
+
def _checkDPhi(self,nLag=5):
|
|
293
|
+
'''
|
|
294
|
+
Report the phase structure function for various lags.
|
|
295
|
+
|
|
296
|
+
:param nLag: (optional) int
|
|
297
|
+
Number of lags to report starting with 0.
|
|
298
|
+
'''
|
|
299
|
+
self.logger.info( "\nEstimates of the phase structure function at various lags:")
|
|
300
|
+
for i in np.arange(nLag):
|
|
301
|
+
self.logger.info( "lag ",i, self._getPhi(i,0), self._getPhi(0,i), self._getPhi(i,i))
|
|
302
|
+
|
|
303
|
+
def _getPhi(self,lag_x,lag_y):
|
|
304
|
+
'''
|
|
305
|
+
Empirical estimate for phase structure function
|
|
306
|
+
|
|
307
|
+
:param lag_x: int
|
|
308
|
+
Screen pixels to lag in x direction.
|
|
309
|
+
:param lag_y: int
|
|
310
|
+
Screen pixels to lag in y direction.
|
|
311
|
+
'''
|
|
312
|
+
assert (lag_x < self.nphi[0]) and (lag_x < self.nphi[1]), "lag choice larger than screen array"
|
|
313
|
+
# Y,X
|
|
314
|
+
if (lag_x == 0 and lag_y == 0):
|
|
315
|
+
return 0.
|
|
316
|
+
if (lag_x == 0):
|
|
317
|
+
return 1.*((self.phi[:-1*lag_y,:] - self.phi[lag_y:,:])**2).sum()/(self.nphi[0]*(self.nphi[1]-lag_y))
|
|
318
|
+
if (lag_y == 0):
|
|
319
|
+
return 1.*((self.phi[:,:-1*lag_x] - self.phi[:,lag_x:])**2).sum()/((self.nphi[0]-lag_x)*self.nphi[1])
|
|
320
|
+
else:
|
|
321
|
+
return (1.*((self.phi[:-1*lag_y,:-1*lag_x] - self.phi[lag_y:,lag_x:])**2).sum()/((self.nphi[1]-lag_y)*(self.nphi[0]-lag_x)))
|
|
322
|
+
|
|
323
|
+
def readScreen(self,filename):
|
|
324
|
+
'''
|
|
325
|
+
Read in screen phases from a file.
|
|
326
|
+
|
|
327
|
+
:param filename: string
|
|
328
|
+
File containing the screen phases.
|
|
329
|
+
'''
|
|
330
|
+
self.phi = np.fromfile(filename,dtype=np.float64).reshape(self.nphi)
|
|
331
|
+
|
|
332
|
+
def _calculate_dphi(self,move_pix=0):
|
|
333
|
+
'''
|
|
334
|
+
Calculate the screen gradient.
|
|
335
|
+
|
|
336
|
+
:param move_pix: (optional) int
|
|
337
|
+
Number of pixels to roll the screen (for time evolution).
|
|
338
|
+
|
|
339
|
+
:returns: ``(nx, nx)``, ``(nx, nx)``
|
|
340
|
+
numpy arrays containing the dx,dy components of the gradient vector.
|
|
341
|
+
|
|
342
|
+
.. note:: Includes factors of the Fresnel scale and the result is in units of the source image.
|
|
343
|
+
'''
|
|
344
|
+
ips = self.ips # number of screen pixels per image pixel
|
|
345
|
+
# -- when this != 1, some sinusoidal signal
|
|
346
|
+
# over time with period of image_resolution
|
|
347
|
+
nx = self.nx # number of image pixels
|
|
348
|
+
rf = self.rf / self.screen_dx # Fresnel scale in screen pixels
|
|
349
|
+
assert move_pix < (self.nphi[1] - self.nx*self.ips), 'screen is not large enough'
|
|
350
|
+
dphi_x = (0.5 * rf * rf / ips ) * \
|
|
351
|
+
(self.phi[0:ips*nx:ips,2+move_pix:ips*nx+2+move_pix:ips] -
|
|
352
|
+
self.phi[0:ips*nx:ips,0+move_pix:ips*nx+move_pix :ips])
|
|
353
|
+
dphi_y = (0.5 * rf * rf / ips ) * \
|
|
354
|
+
(self.phi[2:ips*nx+2:ips,0+move_pix:ips*nx+move_pix:ips] -
|
|
355
|
+
self.phi[0:ips*nx :ips,0+move_pix:ips*nx+move_pix:ips])
|
|
356
|
+
|
|
357
|
+
self.logger.debug('{0:d},{1:d}'.format(*dphi_x.shape))
|
|
358
|
+
return dphi_x,dphi_y
|
|
359
|
+
|
|
360
|
+
def scatter(self,move_pix=0,scale=1):
|
|
361
|
+
'''
|
|
362
|
+
Generate the scattered image which is stored in the ``iss`` member.
|
|
363
|
+
|
|
364
|
+
:param move_pix: (optional) int
|
|
365
|
+
Number of pixels to roll the screen (for time evolution).
|
|
366
|
+
:param scale: (optional) scalar
|
|
367
|
+
Scale factor for gradient. To simulate the scattering effect at another
|
|
368
|
+
wavelength this is (lambda_new/lambda_old)**2
|
|
369
|
+
'''
|
|
370
|
+
|
|
371
|
+
M = self.model.shape[-1] # size of original image array
|
|
372
|
+
N = self.nx # size of output image array
|
|
373
|
+
|
|
374
|
+
#if not self.live_dangerously: self._checkSanity()
|
|
375
|
+
|
|
376
|
+
# calculate phase gradient
|
|
377
|
+
dphi_x,dphi_y = self._calculate_dphi(move_pix=move_pix)
|
|
378
|
+
|
|
379
|
+
if scale != 1:
|
|
380
|
+
dphi_x *= scale
|
|
381
|
+
dphi_y *= scale
|
|
382
|
+
|
|
383
|
+
xx_,yy = np.meshgrid((np.arange(N) - 0.5*(N-1)),\
|
|
384
|
+
(np.arange(N) - 0.5*(N-1)),indexing='xy')
|
|
385
|
+
|
|
386
|
+
# check whether we care about PA of scattering kernel
|
|
387
|
+
if self.pa != None:
|
|
388
|
+
f_model = RectBivariateSpline(self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
389
|
+
self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
390
|
+
self.model)
|
|
391
|
+
|
|
392
|
+
# apply rotation
|
|
393
|
+
theta = -(90 * pi / 180) + np.radians(self.pa) # rotate CW 90 deg, then CCW by PA
|
|
394
|
+
xx_ += dphi_x
|
|
395
|
+
yy += dphi_y
|
|
396
|
+
xx = cos(theta)*xx_ - sin(theta)*yy
|
|
397
|
+
yy = sin(theta)*xx_ + cos(theta)*yy
|
|
398
|
+
self.iss = f_model.ev(yy.flatten(),xx.flatten()).reshape((self.nx,self.nx))
|
|
399
|
+
|
|
400
|
+
# rotate back and clip for positive values for I
|
|
401
|
+
if self.think_positive:
|
|
402
|
+
self.iss = clip(rotate(self.iss,-1*theta/np.pi*180,reshape=False),a_min=0,a_max=1e30) * (self.dx/self.model_dx)**2
|
|
403
|
+
else:
|
|
404
|
+
self.iss = rotate(self.iss,-1*theta/np.pi*180,reshape=False) * (self.dx/self.model_dx)**2
|
|
405
|
+
|
|
406
|
+
# otherwise do a faster lookup rather than the expensive interpolation.
|
|
407
|
+
else:
|
|
408
|
+
yyi = np.rint((yy+dphi_y+self.nx/2)).astype(np.int) % self.nx
|
|
409
|
+
xxi = np.rint((xx_+dphi_x+self.nx/2)).astype(np.int) % self.nx
|
|
410
|
+
if self.think_positive:
|
|
411
|
+
self.iss = clip(self.isrc[yyi,xxi],a_min=0,a_max=1e30)
|
|
412
|
+
else:
|
|
413
|
+
self.iss = self.isrc[yyi,xxi]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _load_src(self,stokes=(0,),think_positive=True):
|
|
417
|
+
'''
|
|
418
|
+
Load the source image from model (changes size and resolution to match the screen).
|
|
419
|
+
|
|
420
|
+
:param stokes: (optional) tuple
|
|
421
|
+
Stokes parameters to consider.
|
|
422
|
+
:param think_positive: (optional) bool
|
|
423
|
+
Should we enforce that the source image has no negative pixel values?
|
|
424
|
+
'''
|
|
425
|
+
M = self.model.shape[1] # size of original image array
|
|
426
|
+
N = self.nx # size of output image array
|
|
427
|
+
|
|
428
|
+
if len(self.model.shape) > 2:
|
|
429
|
+
self.isrc = np.empty((self.model.shape[-1],N,N))
|
|
430
|
+
else:
|
|
431
|
+
self.isrc = np.empty((1,N,N))
|
|
432
|
+
self.model = np.reshape(self.model,(1,M,M))
|
|
433
|
+
|
|
434
|
+
for s in stokes:
|
|
435
|
+
f_img = RectBivariateSpline(self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
436
|
+
self.model_dx/self.dx*(np.arange(M) - 0.5*(M-1)),\
|
|
437
|
+
self.model[s,:,:])
|
|
438
|
+
|
|
439
|
+
xx_,yy_ = np.meshgrid((np.arange(N) - 0.5*(N-1)),\
|
|
440
|
+
(np.arange(N) - 0.5*(N-1)),indexing='xy')
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
m = f_img.ev(yy_.flatten(),xx_.flatten()).reshape((self.nx,self.nx))
|
|
444
|
+
res = m * (self.dx/self.model_dx)**2 # rescale for change in pixel size
|
|
445
|
+
|
|
446
|
+
if s == 0 and think_positive:
|
|
447
|
+
res[res < 0] = 0
|
|
448
|
+
|
|
449
|
+
self.isrc[s,:,:] = res
|
|
450
|
+
|
|
451
|
+
self.model = np.squeeze(self.model)
|
|
452
|
+
self.isrc = np.squeeze(self.isrc)
|
|
453
|
+
|
|
454
|
+
def saveSettings(self,filename):
|
|
455
|
+
'''
|
|
456
|
+
Save screen settings to a file.
|
|
457
|
+
|
|
458
|
+
:param filename: string
|
|
459
|
+
settings filename
|
|
460
|
+
'''
|
|
461
|
+
f = open(filename,"w")
|
|
462
|
+
f.write("wavelength \t {0}\n".format(self.wavelength))
|
|
463
|
+
f.write("dpc \t {0}\n".format(self.dpc))
|
|
464
|
+
f.write("rpc \t {0}\n".format(self.rpc))
|
|
465
|
+
f.write("d \t {0}\n".format(self.d))
|
|
466
|
+
f.write("m \t {0}\n".format(self.m))
|
|
467
|
+
f.write("r0 \t {0}\n".format(self.r0))
|
|
468
|
+
f.write("anisotropy \t {0}\n".format(self.anisotropy))
|
|
469
|
+
f.write("pa \t {0}\n".format(self.pa))
|
|
470
|
+
f.write("nphix \t {0}\n".format(self.nphi[0])) # size of phase screen
|
|
471
|
+
f.write("nphiy \t {0}\n".format(self.nphi[1])) # size of phase screen
|
|
472
|
+
f.write("screen_dx \t {0}\n".format(self.screen_dx))
|
|
473
|
+
f.write("rf \t {0}\n".format(self.rf))
|
|
474
|
+
f.write("ips \t {0}\n".format(self.ips))
|
|
475
|
+
f.write("dx \t {0}\n".format(self.dx))
|
|
476
|
+
f.write("nx \t {0}\n".format(self.nx))
|
|
477
|
+
f.write("qmax \t {0}\n".format(self.r_inner)) # inner turbulence scale in r0
|
|
478
|
+
f.write("qmin \t {0}\n".format(self.r_outer)) # outer turbulence scale in r0
|
|
479
|
+
#f.write("qmax \t {0}\n".format(self.qmax)) # 1./inner turbulence scale in screen pixels
|
|
480
|
+
#f.write("qmin \t {0}\n".format(self.qmin)) # 1./inner turbulence scale in screen pixels
|
|
481
|
+
f.close()
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: tracks
|
|
3
|
+
:platform: Unix
|
|
4
|
+
:synopsis: Lightweight module for generating visibilities and closure phases
|
|
5
|
+
|
|
6
|
+
.. moduleauthor:: Katheirne Rosenfeld <krosenf@gmail.com>
|
|
7
|
+
.. moduleauthor:: Michael Johnson
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import print_function
|
|
12
|
+
|
|
13
|
+
import itertools
|
|
14
|
+
from numpy import (pi,array,cos,sin,cross,dot,asarray,linspace,sum,transpose,dot,exp,angle,
|
|
15
|
+
newaxis,prod,flipud,arange,zeros,complex64,abs,radians)
|
|
16
|
+
from numpy.fft import fftshift,fftfreq
|
|
17
|
+
from numpy.linalg import norm
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
__all__ = ["Target"]
|
|
21
|
+
|
|
22
|
+
class Target(object):
|
|
23
|
+
'''
|
|
24
|
+
Class for constructing visibility samples.
|
|
25
|
+
|
|
26
|
+
:param wavelength: (optional)
|
|
27
|
+
Scalar wavelength in meters.
|
|
28
|
+
:param position: (optional) ``(2, )``
|
|
29
|
+
A tuple with right ascension and declination of the source in (hours, degrees) or the preset string "sgra".
|
|
30
|
+
:param obspos: (optional)
|
|
31
|
+
A dictionary of observatories and their geocentric positions.
|
|
32
|
+
:param elevation_cuts: (optional) ``(2, )``
|
|
33
|
+
The upper and lower bound on the observing zenith angle.
|
|
34
|
+
|
|
35
|
+
.. note:: The zenith angle cut corresponds to whether the site can observe the source. As a result, for functions such as :func:`generateTracks`, the number of uv samples returned will be less than or equal to the number of samples set by the ``times`` argument. To disable the zenith angle cut you can initialize :class:`Target` with ``zenith_cuts`` = ``[180.,0.]``.
|
|
36
|
+
'''
|
|
37
|
+
|
|
38
|
+
def __init__(self,wavelength=1.3e-3,position='sgra',obspos='eht',zenith_cuts=[75.,0.]):
|
|
39
|
+
|
|
40
|
+
if position=='sgra':
|
|
41
|
+
ra,dec = (17+45./60+40.0409/3600,-1*(29+28.118/3600))
|
|
42
|
+
else:
|
|
43
|
+
ra,dec = position
|
|
44
|
+
|
|
45
|
+
# observing settings
|
|
46
|
+
self.dec = dec
|
|
47
|
+
self.ra = ra
|
|
48
|
+
self.wavelength = wavelength
|
|
49
|
+
self.cos_zenith_cuts = cos(radians(zenith_cuts))
|
|
50
|
+
|
|
51
|
+
# derived quantities
|
|
52
|
+
self.target_vec = array([cos(self.dec * pi / 180), 0., sin(self.dec * pi / 180)])
|
|
53
|
+
self.projU = cross([0.,0.,1.],self.target_vec)
|
|
54
|
+
self.projU /= norm(self.projU)
|
|
55
|
+
self.projV = -1.*cross(self.projU,self.target_vec)
|
|
56
|
+
|
|
57
|
+
# dictionary of observatories and their positions
|
|
58
|
+
l = self.wavelength # in meters
|
|
59
|
+
if obspos == 'eht':
|
|
60
|
+
self.obs = dict([("SMA",array([-5464523.400, -2493147.080, 2150611.750])/l),\
|
|
61
|
+
("SMT",array([-1828796.200, -5054406.800, 3427865.200])/l),\
|
|
62
|
+
("CARMA",array([-2397431.300, -4482018.900, 3843524.500])/l),\
|
|
63
|
+
("LMT",array([-768713.9637, -5988541.7982, 2063275.9472])/l),\
|
|
64
|
+
("ALMA",array([2225037.1851, -5441199.1620, -2479303.4629])/l),\
|
|
65
|
+
("PV",array([5088967.9000, -301681.6000, 3825015.8000])/l),\
|
|
66
|
+
("PDBI",array([4523998.40, 468045.240, 4460309.760])/l),\
|
|
67
|
+
("SPT",array([0.0, 0.0, -6359587.3])/l),\
|
|
68
|
+
("HAYSTACK",array([1492460, -4457299, 4296835])/l),\
|
|
69
|
+
]) # in wavelengths
|
|
70
|
+
else:
|
|
71
|
+
self.obs = obspos
|
|
72
|
+
for k in self.obs.iterkeys():
|
|
73
|
+
self.obs[k] /= l
|
|
74
|
+
|
|
75
|
+
def sites(self,s):
|
|
76
|
+
'''
|
|
77
|
+
Look up geocentric positions of sites by their string keyword.
|
|
78
|
+
|
|
79
|
+
:param s: List of site keys.
|
|
80
|
+
|
|
81
|
+
:returns: Array of site geocentric positions.
|
|
82
|
+
'''
|
|
83
|
+
return array([self.obs[s_] for s_ in s])
|
|
84
|
+
|
|
85
|
+
def _RR(self,v,theta):
|
|
86
|
+
'''
|
|
87
|
+
Rotates the vector v by angle theta.
|
|
88
|
+
|
|
89
|
+
:param v: A vector (x,y,z).
|
|
90
|
+
:param theta: Angle for CCW rotation in radians.
|
|
91
|
+
|
|
92
|
+
:returns: Rotated vector (x',y',z')
|
|
93
|
+
'''
|
|
94
|
+
return array([cos(theta)*v[0] - sin(theta)*v[1],\
|
|
95
|
+
sin(theta)*v[0] + cos(theta)*v[1],\
|
|
96
|
+
v[2]])
|
|
97
|
+
|
|
98
|
+
def _cosZenith(self,v,theta):
|
|
99
|
+
'''
|
|
100
|
+
:param v: ``(3, )`` vector.
|
|
101
|
+
:param theta: Scalar angle in radians.
|
|
102
|
+
|
|
103
|
+
:returns: cos(zenith angle) = sin(elevation)
|
|
104
|
+
'''
|
|
105
|
+
return dot(self._RR(v,theta),self.target_vec) / (norm(v) * norm(self.target_vec))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _RRelevcut(self,v,theta):
|
|
109
|
+
'''
|
|
110
|
+
Does vector v pass the zenith angle cut?
|
|
111
|
+
|
|
112
|
+
:param v: ``(3, )`` vector
|
|
113
|
+
:param theta: Scalar angle in radians.
|
|
114
|
+
'''
|
|
115
|
+
if abs(self._cosZenith(v,theta)-0.5*sum(self.cos_zenith_cuts)) < 0.5*sum([-1,1]*self.cos_zenith_cuts):
|
|
116
|
+
return True
|
|
117
|
+
else:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def generateUV(self,site_pair,theta):
|
|
121
|
+
'''
|
|
122
|
+
Generate uv baseline.
|
|
123
|
+
|
|
124
|
+
:param site_pair: ``(2, 3)``
|
|
125
|
+
Geocentric (x,y,z) positions of two sites.
|
|
126
|
+
:param theta: scalar
|
|
127
|
+
Angle in radians.
|
|
128
|
+
|
|
129
|
+
:returns: ``(2, )``
|
|
130
|
+
A uv point.
|
|
131
|
+
'''
|
|
132
|
+
return array([dot(self._RR(site_pair[0],theta)-self._RR(site_pair[1],theta),self.projU),\
|
|
133
|
+
dot(self._RR(site_pair[0],theta)-self._RR(site_pair[1],theta),self.projV)])
|
|
134
|
+
|
|
135
|
+
def hour(self,theta):
|
|
136
|
+
'''
|
|
137
|
+
Convert to UT time.
|
|
138
|
+
|
|
139
|
+
:param theta: ``(n, )`` vector of angles in radians.
|
|
140
|
+
|
|
141
|
+
:returns: ``(n, )`` vector of times in hours.
|
|
142
|
+
'''
|
|
143
|
+
return (theta / (2. * pi) * 24. + self.ra) % 24
|
|
144
|
+
|
|
145
|
+
def theta(self,hour):
|
|
146
|
+
'''
|
|
147
|
+
Convert from UT to angle.
|
|
148
|
+
|
|
149
|
+
:param hour: ``(n, )``
|
|
150
|
+
Vector of times in hours.
|
|
151
|
+
|
|
152
|
+
:returns: ``(n, )``
|
|
153
|
+
Vector of angles in radians.
|
|
154
|
+
'''
|
|
155
|
+
return ((hour - self.ra) / 24. * 2. * pi) % (2 * pi)
|
|
156
|
+
|
|
157
|
+
def _gst(self,hour):
|
|
158
|
+
return ((hour-12.) % 24.) - 12.
|
|
159
|
+
|
|
160
|
+
def generateTracks(self,sites,times=(0.,24.,int((24.-0.)*60./10.)),return_hour=False):
|
|
161
|
+
'''
|
|
162
|
+
Generates uv-samples where the sample rate is to be equally spaced in time.
|
|
163
|
+
For instance, if you want samples spaced by 5 minutes ranging from 1 UT to 4 UT
|
|
164
|
+
(non-inclusive) you would set ``times`` = ``(1, 4, int((4.-1.)*60./5.))``. As
|
|
165
|
+
another example, you would generate 128 samples separated by 10 seconds if
|
|
166
|
+
``times`` = ``(0., 128*10./3600, 128)``.
|
|
167
|
+
|
|
168
|
+
:param sites: ``(num_sites, 3)``
|
|
169
|
+
Numpy array of sites' geocentric positions.
|
|
170
|
+
:param times: (optional) ``(start, stop, num_samples)``
|
|
171
|
+
Tuple setting the spacing of the uv-samples in time. The ``start`` and ``stop`` times should be in hours and in the range [0,24]. ``stop`` is non-inclusive.
|
|
172
|
+
:param return_hour: (optional) bool
|
|
173
|
+
Return hour of uv samples as second item to unpack.
|
|
174
|
+
|
|
175
|
+
:returns: ``(num_measurements, 2)``
|
|
176
|
+
Numpy array with uv samples where ``num_measurements`` :math:`\\leq` ``num_samples``.
|
|
177
|
+
|
|
178
|
+
'''
|
|
179
|
+
|
|
180
|
+
times = linspace(times[0],times[1],num=times[2],endpoint=False)
|
|
181
|
+
uv = []
|
|
182
|
+
hr = []
|
|
183
|
+
for h in times:
|
|
184
|
+
theta = self.theta(h)
|
|
185
|
+
for baseline in itertools.combinations(sites,2):
|
|
186
|
+
if self._RRelevcut(baseline[0],theta) and self._RRelevcut(baseline[1],theta):
|
|
187
|
+
if return_hour:
|
|
188
|
+
hr.append(h)
|
|
189
|
+
hr.append(h)
|
|
190
|
+
uv.append(self.generateUV((baseline[0],baseline[1]),theta))
|
|
191
|
+
uv.append(self.generateUV((baseline[1],baseline[0]),theta))
|
|
192
|
+
|
|
193
|
+
if return_hour:
|
|
194
|
+
return (array(uv),array(hr))
|
|
195
|
+
else:
|
|
196
|
+
return asarray(uv)
|
|
197
|
+
|
|
198
|
+
def calculateCA(self,img,dx,uvQuadrangle):
|
|
199
|
+
'''
|
|
200
|
+
Calculate closure amplitudes for numpy arrays of 4 baselines:
|
|
201
|
+
|
|
202
|
+
.. math::
|
|
203
|
+
|
|
204
|
+
A_{abcd} = \\frac{|V_{ab}||V_{cd}|}{|V_{ac}||V_{bd}|}
|
|
205
|
+
|
|
206
|
+
:param img: ``(n, n)``
|
|
207
|
+
Numpy image array.
|
|
208
|
+
:param dx: scalar
|
|
209
|
+
Pixel size of image in microarcseconds.
|
|
210
|
+
:param uvQuadrangle: ``(num_measurements, 4, 2)``
|
|
211
|
+
The uv samples for which to calculate the closure amplitude. The baseline order along axis=1 should be (ab,cd,ac,bd).
|
|
212
|
+
|
|
213
|
+
:returns: ``(num_measurements, )``
|
|
214
|
+
Closure amplitudes.
|
|
215
|
+
'''
|
|
216
|
+
|
|
217
|
+
# case we are only given one time sample
|
|
218
|
+
if len(uvQuadrangle.shape) == 2: uvQuadrangle = uvQuadrangle[newxais,:]
|
|
219
|
+
|
|
220
|
+
ca = []
|
|
221
|
+
for quad in uvQuadrangle:
|
|
222
|
+
v = array([ abs(self.FTElementFast(img,dx,t)) for t in quad ])
|
|
223
|
+
ca.append( v[0]*v[1]/(v[2]*v[3]) )
|
|
224
|
+
return asarray(ca)
|
|
225
|
+
|
|
226
|
+
def calculateCP(self,img,dx,uvTriangle):
|
|
227
|
+
'''
|
|
228
|
+
Calculate closure phases for numpy arrays of 3 baselines that should form a closed triangle:
|
|
229
|
+
|
|
230
|
+
.. math::
|
|
231
|
+
|
|
232
|
+
\\phi_{abc} = \\mathrm{arg}(V_{ab}V_{bc}V_{ca})
|
|
233
|
+
|
|
234
|
+
:param img: ``(n, n)``
|
|
235
|
+
Numpy image array.
|
|
236
|
+
:param dx: scalar
|
|
237
|
+
Pixel size of image in microarcseconds.
|
|
238
|
+
:param uvTriangle: ``(num_measurements, 3, 2)``
|
|
239
|
+
The uv samples for which to calculate the closure phase. The baseline order along axis=1 should be (ab,bc,ca).
|
|
240
|
+
|
|
241
|
+
:returns: ``(num_measurements, )``
|
|
242
|
+
Closure phases in radians.
|
|
243
|
+
'''
|
|
244
|
+
|
|
245
|
+
# case where we are only given one time sample
|
|
246
|
+
if len(uvTriangle.shape) == 2: uvTriangle = uvTriangle[newaxis,:]
|
|
247
|
+
|
|
248
|
+
cp = []
|
|
249
|
+
for triang in uvTriangle:
|
|
250
|
+
v = [self.FTElementFast(img,dx,t) for t in triang]
|
|
251
|
+
cp.append(angle(prod(v)))
|
|
252
|
+
|
|
253
|
+
return asarray(cp)
|
|
254
|
+
|
|
255
|
+
def calculateBS(self,img,dx,uvTriangle):
|
|
256
|
+
'''
|
|
257
|
+
Calculate bispectrum for numpy arrays of 3 baselines that should form a closed triangle:
|
|
258
|
+
|
|
259
|
+
.. math::
|
|
260
|
+
|
|
261
|
+
B_{abc} = V_{ab}V_{bc}V_{ca}
|
|
262
|
+
|
|
263
|
+
:param img: ``(n, n)``
|
|
264
|
+
Image array.
|
|
265
|
+
:param dx: scalar
|
|
266
|
+
Pixel size of image in microarcseconds.
|
|
267
|
+
:param uvTriangle: ``(num_measurements, 3, 2)``
|
|
268
|
+
uv samples for which to calculate the bispectra. The baseline order along axis=1 should be (ab,bc,ca).
|
|
269
|
+
|
|
270
|
+
:returns: ``(num_measurements, )``
|
|
271
|
+
Complex bispectra.
|
|
272
|
+
'''
|
|
273
|
+
|
|
274
|
+
# case where we are only given one time sample
|
|
275
|
+
if len(uvTriangle.shape) == 2: uvTriangle = uvTriangle[newaxis,:]
|
|
276
|
+
|
|
277
|
+
bs = []
|
|
278
|
+
for triang in uvTriangle:
|
|
279
|
+
v = [self.FTElementFast(img,dx,t) for t in triang]
|
|
280
|
+
bs.append(prod(v))
|
|
281
|
+
|
|
282
|
+
return asarray(bs)
|
|
283
|
+
|
|
284
|
+
def generateQuadrilateralTracks(self,sites,times=(0.,24.,int((24.-0.)*60./10.)),return_hour=False):
|
|
285
|
+
'''
|
|
286
|
+
Generates a quadruplet of uv points for a quadrilateral (a,b,c,d) ensuring that all sites can see the source. See the docstring for :func:`generateTracks` for notes about the sampling rate.
|
|
287
|
+
|
|
288
|
+
**Suggested usage:** Feed the generated `uv` samples to :func:`calculateCA` which will return the closure amplitude quantity.
|
|
289
|
+
|
|
290
|
+
:param sites: ``(4, 3)``
|
|
291
|
+
Array of geocentric positions for 4 sites (a,b,c,d).
|
|
292
|
+
:param times: (optional) ``(start, stop, num_samples)``
|
|
293
|
+
Tuple setting the spacing of the uv-samples in time. The ``start`` and ``stop`` times should be in hours and in the range [0,24]. ``stop`` is non-inclusive.
|
|
294
|
+
:param return_hour: (optional)
|
|
295
|
+
Return hour of uv samples as second item to unpack.
|
|
296
|
+
|
|
297
|
+
:returns: ``(num_measurements, 4, 2)``
|
|
298
|
+
Array where the order of the baselines along the first dimension (axis=1) is (ab, cd, ac, bd).
|
|
299
|
+
'''
|
|
300
|
+
|
|
301
|
+
assert len(sites) == 4, "sites should form a quadrilateral"
|
|
302
|
+
times = linspace(times[0],times[1],num=times[2],endpoint=False)
|
|
303
|
+
uv = []
|
|
304
|
+
hr = []
|
|
305
|
+
for h in times:
|
|
306
|
+
theta = self.theta(h)
|
|
307
|
+
# zenith cut
|
|
308
|
+
if self._RRelevcut(sites[0],theta) and \
|
|
309
|
+
self._RRelevcut(sites[1],theta) and \
|
|
310
|
+
self._RRelevcut(sites[2],theta) and \
|
|
311
|
+
self._RRelevcut(sites[3],theta):
|
|
312
|
+
quad = (self.generateUV((sites[0],sites[1]),theta),self.generateUV((sites[2],sites[3]),theta),\
|
|
313
|
+
self.generateUV((sites[0],sites[2]),theta),self.generateUV((sites[1],sites[3]),theta))
|
|
314
|
+
uv.append(quad)
|
|
315
|
+
if return_hour: hr.append(h)
|
|
316
|
+
|
|
317
|
+
if return_hour:
|
|
318
|
+
return (array(uv),array(hr))
|
|
319
|
+
else:
|
|
320
|
+
return asarray(uv)
|
|
321
|
+
|
|
322
|
+
def generateTriangleTracks(self,sites,times=(0.,24.,int((24.-0.)*60./10.)),return_hour=False):
|
|
323
|
+
'''
|
|
324
|
+
Generates a triplet of uv points for a closed triangle of sites (a,b,c) ensuring that all sites can see the source.
|
|
325
|
+
See the docstring for :func:`generateTracks` for notes about the sampling rate.
|
|
326
|
+
|
|
327
|
+
**Suggested usage:** Feed the generated UV samples to :func:`calculateBS` or :func:`calculateCP` to calculate closure phase or bispectrum quanities.
|
|
328
|
+
|
|
329
|
+
:param sites: ``(3, 3)``
|
|
330
|
+
Array of geocentric positions for 3 sites (a,b,c).
|
|
331
|
+
:param times: (optional) ``(start, stop, num_samples)``
|
|
332
|
+
Tuple setting the spacing of the uv-samples in time. The ``start`` and ``stop`` times should be in hours and in the range [0,24]. ``stop`` is non-inclusive.
|
|
333
|
+
:param return_hour: (optional)
|
|
334
|
+
Return hour of uv samples as second item to unpack.
|
|
335
|
+
|
|
336
|
+
:returns: ``(num_measurements, 3, 2)``
|
|
337
|
+
Array of uv points where the order of the baselines along the first dimension (axis=1) is (ab,bc,ca).
|
|
338
|
+
'''
|
|
339
|
+
assert len(sites) == 3, "sites should form a triangle"
|
|
340
|
+
|
|
341
|
+
times = linspace(times[0],times[1],num=times[2],endpoint=False)
|
|
342
|
+
uv = []
|
|
343
|
+
hr = []
|
|
344
|
+
for h in times:
|
|
345
|
+
theta = self.theta(h)
|
|
346
|
+
# zenith cut
|
|
347
|
+
if self._RRelevcut(sites[0],theta) and \
|
|
348
|
+
self._RRelevcut(sites[1],theta) and \
|
|
349
|
+
self._RRelevcut(sites[2],theta):
|
|
350
|
+
triang = array([ self.generateUV((sites[0],sites[1]),theta),\
|
|
351
|
+
self.generateUV((sites[1],sites[2]),theta),\
|
|
352
|
+
self.generateUV((sites[2],sites[0]),theta) ])
|
|
353
|
+
uv.append(triang)
|
|
354
|
+
if return_hour: hr.append(h)
|
|
355
|
+
|
|
356
|
+
if return_hour:
|
|
357
|
+
return (array(uv),array(hr))
|
|
358
|
+
else:
|
|
359
|
+
return asarray(uv)
|
|
360
|
+
|
|
361
|
+
def calculateVisibilities(self,img,dx,uv):
|
|
362
|
+
'''
|
|
363
|
+
Calculate complex visibilities.
|
|
364
|
+
|
|
365
|
+
:param img: Image array.
|
|
366
|
+
:param dx: Pixel size in microarcseconds.
|
|
367
|
+
:param uv: ``(num_measurements, 2)`` An array of `uv` samples with dimension.
|
|
368
|
+
|
|
369
|
+
:returns: ``(num_measurements, )`` Scalar complex visibilities.
|
|
370
|
+
'''
|
|
371
|
+
|
|
372
|
+
# case of only 1 uv point
|
|
373
|
+
if len(uv.shape) == 1: uv = uv[newaxis,:]
|
|
374
|
+
|
|
375
|
+
num_samples = uv.shape[0]
|
|
376
|
+
V = zeros(num_samples,dtype=complex64)
|
|
377
|
+
for i in range(num_samples):
|
|
378
|
+
V[i] = self.FTElementFast(img,dx,uv[i,:])
|
|
379
|
+
return V
|
|
380
|
+
|
|
381
|
+
def FTElementFast(self,img,dx,baseline):
|
|
382
|
+
'''
|
|
383
|
+
Return complex visibility.
|
|
384
|
+
|
|
385
|
+
:param img: Image array.
|
|
386
|
+
:param dx: pixel size in microarcseconds.
|
|
387
|
+
:param baseline: (u,v) point in wavelengths.
|
|
388
|
+
|
|
389
|
+
:returns: complex visibility
|
|
390
|
+
|
|
391
|
+
'''
|
|
392
|
+
nx = img.shape[-1]
|
|
393
|
+
du = 3600.*1e6/(nx*dx*radians(1.))
|
|
394
|
+
ind = arange(nx)
|
|
395
|
+
return sum(img * dot(\
|
|
396
|
+
transpose([exp(-2*pi*1j/du/nx*baseline[1]*flipud(ind))]),\
|
|
397
|
+
[exp(-2*pi*1j/du/nx*baseline[0]*ind)]))
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
.. module:: utilities
|
|
4
|
+
:platform: Unix
|
|
5
|
+
:synopsis: Helpful function for ScatterBrane
|
|
6
|
+
|
|
7
|
+
.. moduleauthor:: Katherine Rosenfeld <krosenf@gmail.com>
|
|
8
|
+
.. moduleauthor:: Michael Johnson
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import print_function
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy.interpolate import RectBivariateSpline
|
|
15
|
+
from scipy.ndimage.filters import gaussian_filter
|
|
16
|
+
from astropy.io import fits
|
|
17
|
+
|
|
18
|
+
def smoothImage(img,dx,fwhm):
|
|
19
|
+
'''
|
|
20
|
+
Returns Image smoothed by a gaussian kernel.
|
|
21
|
+
|
|
22
|
+
:param img: ``(n, n)``
|
|
23
|
+
numpy array
|
|
24
|
+
:param dx: scalar
|
|
25
|
+
Pixel scale in microarcseconds
|
|
26
|
+
:param fwhm: scalar
|
|
27
|
+
Gaussian full width at half maximum in microarcseconds
|
|
28
|
+
'''
|
|
29
|
+
return gaussian_filter(img,fwhm/(2*np.sqrt(np.log(4)))/dx)
|
|
30
|
+
|
|
31
|
+
def getCoherenceLength(theta,wavelength=1.3e-3,magnification=0.448):
|
|
32
|
+
'''
|
|
33
|
+
:param theta: scalar
|
|
34
|
+
FWHM of scattering kernel at 1 cm in milli-arcseconds.
|
|
35
|
+
:param wavelength: (optional) scalar
|
|
36
|
+
Observing wavelength in meters
|
|
37
|
+
:param magnification: (optional) scalar
|
|
38
|
+
Magnification factor (scatterer-observer)/(source-scatterer).
|
|
39
|
+
|
|
40
|
+
:returns: scalar
|
|
41
|
+
Coherence length in km.
|
|
42
|
+
'''
|
|
43
|
+
#return (wavelength*1e-3)*np.sqrt(np.log(4))/(np.pi*np.sqrt(1+magnification)**2*np.radians(1e-3/3600*theta*(wavelength*1e2)**2))
|
|
44
|
+
return (wavelength*1e-3)*np.sqrt(np.log(4))/(np.pi*(1+magnification)*np.radians(1e-3/3600*theta*(wavelength*1e2)**2))
|
|
45
|
+
|
|
46
|
+
def ensembleSmooth(img,dx,brane,return_kernel=False):
|
|
47
|
+
'''
|
|
48
|
+
Generates ensemble averaged image given scattering kernel parameters.
|
|
49
|
+
|
|
50
|
+
:param img: ``(n, n)``
|
|
51
|
+
numpy array
|
|
52
|
+
:param dx: scalar
|
|
53
|
+
Pixel scale in microarcseconds
|
|
54
|
+
:param brane: Brane object.
|
|
55
|
+
:param return_kernel: (optional) bool
|
|
56
|
+
Return tuple with uv kernel (:func:`nump.fft.rfft2` format). See :func:`getUVKernel` for an alternate method.
|
|
57
|
+
'''
|
|
58
|
+
nx = img.shape[0]
|
|
59
|
+
|
|
60
|
+
# scattering kernel parameters in wavelengths
|
|
61
|
+
sigma_maj = brane.wavelength*np.sqrt(np.log(4)) / (np.pi*(1.+brane.m)*brane.r0) / (2*np.sqrt(np.log(4)))
|
|
62
|
+
sigma_min = sigma_maj / brane.anisotropy
|
|
63
|
+
v = np.dot(np.transpose([np.fft.fftfreq(nx,d=dx*np.radians(1.)/(3600*1e6))]),[np.ones(nx/2 + 1)])
|
|
64
|
+
u = np.dot(np.transpose([np.ones(nx)]),[np.fft.rfftfreq(nx,d=dx*np.radians(1.)/(3600*1e6))])
|
|
65
|
+
|
|
66
|
+
# rotate
|
|
67
|
+
if brane.pa != None:
|
|
68
|
+
theta = np.radians(90-brane.pa)
|
|
69
|
+
else:
|
|
70
|
+
theta = np.radians(0.)
|
|
71
|
+
u_ = np.cos(theta)*u - np.sin(theta)*v
|
|
72
|
+
v = np.sin(theta)*u + np.cos(theta)*v
|
|
73
|
+
|
|
74
|
+
# rotate
|
|
75
|
+
G = np.exp(-2*np.pi**2*(u_**2*sigma_maj**2 + v**2*sigma_min**2))
|
|
76
|
+
V = np.fft.rfft2(img)
|
|
77
|
+
|
|
78
|
+
if return_kernel:
|
|
79
|
+
return (np.fft.irfft2(V*G,s=img.shape),G)
|
|
80
|
+
else:
|
|
81
|
+
return np.fft.irfft2(V*G,s=img.shape)
|
|
82
|
+
|
|
83
|
+
def getUVKernel(u,v,brane):
|
|
84
|
+
'''
|
|
85
|
+
Get ensemble kernel in visibility plane for specified uv points. See func:`ensembleSmooth` for an althernate method.
|
|
86
|
+
|
|
87
|
+
:param u: ``(n, )``
|
|
88
|
+
Samples of u in units of wavelengths.
|
|
89
|
+
:param v: ``(n, )``
|
|
90
|
+
Samples of v in units of wavelengths.
|
|
91
|
+
:param brane: Brane object
|
|
92
|
+
|
|
93
|
+
:returns: ``(n, )`` Ensemble kernel complex visibility
|
|
94
|
+
'''
|
|
95
|
+
# scattering kernel parameters in wavelengths
|
|
96
|
+
sigma_maj = brane.wavelength*np.sqrt(np.log(4)) / (np.pi*(1.+brane.m)*brane.r0) / (2*np.sqrt(np.log(4)))
|
|
97
|
+
sigma_min = sigma_maj / brane.anisotropy
|
|
98
|
+
|
|
99
|
+
# rotate
|
|
100
|
+
if brane.pa != None:
|
|
101
|
+
theta = np.radians(90-brane.pa)
|
|
102
|
+
else:
|
|
103
|
+
theta = np.radians(0.)
|
|
104
|
+
|
|
105
|
+
u_ = np.cos(theta)*u - np.sin(theta)*v
|
|
106
|
+
v_ = np.sin(theta)*u + np.cos(theta)*v
|
|
107
|
+
|
|
108
|
+
# rotate and return
|
|
109
|
+
return np.exp(-2*np.pi**2*(u_**2*sigma_maj**2 + v_**2*sigma_min**2))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def loadSettings(filename):
|
|
113
|
+
'''
|
|
114
|
+
Loads simulation settings from a file generated by :func:`Brane.save_settings`.
|
|
115
|
+
|
|
116
|
+
:param filename: string
|
|
117
|
+
File name that contains simulation settings.
|
|
118
|
+
|
|
119
|
+
:returns: A dictionary with simulation settings.
|
|
120
|
+
|
|
121
|
+
'''
|
|
122
|
+
return dict(np.genfromtxt(filename,\
|
|
123
|
+
dtype=[('a','|S10'),('f','float')],delimiter='\t',autostrip=True))
|
|
124
|
+
|
|
125
|
+
def regrid(a,inx,idx,onx,odx):
|
|
126
|
+
'''
|
|
127
|
+
Regrids array with a new resolution and pixel number.
|
|
128
|
+
|
|
129
|
+
:param a: ``(n, n)``
|
|
130
|
+
Input numpy image
|
|
131
|
+
:param inx: int
|
|
132
|
+
Number of input pixels on a side
|
|
133
|
+
:param idx: scalar
|
|
134
|
+
Input resolution element
|
|
135
|
+
:param onx: int
|
|
136
|
+
Number of output pixels on a side
|
|
137
|
+
:param odx: scalar
|
|
138
|
+
Output resolution element
|
|
139
|
+
|
|
140
|
+
:returns: Array regridded to the new resolution and field of view.
|
|
141
|
+
'''
|
|
142
|
+
x = idx * (np.arange(inx) - 0.5 * (inx - 1))
|
|
143
|
+
f = RectBivariateSpline(x,x,a)
|
|
144
|
+
x_ = odx * (np.arange(onx) - 0.5 * (onx - 1))
|
|
145
|
+
xx_,yy_ = np.meshgrid(x_,x_,indexing='xy')
|
|
146
|
+
m = f.ev(yy_.flatten(),xx_.flatten()).reshape((onx,onx))
|
|
147
|
+
return m*(odx/idx)**2
|
|
148
|
+
|
|
149
|
+
def writefits(m,dx,dest='image.fits',obsra=266.4168370833333,obsdec=-29.00781055555555,freq=230e9):
|
|
150
|
+
'''
|
|
151
|
+
Write fits file with header. Defaults are set for Sgr A* at 1.3mm.
|
|
152
|
+
|
|
153
|
+
:param m: ``(n, n)``
|
|
154
|
+
numpy image array
|
|
155
|
+
:param dx: scalar
|
|
156
|
+
Pixel size in microarcseconds
|
|
157
|
+
:param dest: (optional) string
|
|
158
|
+
Output fits file name
|
|
159
|
+
:param obsra: (optional) scalar
|
|
160
|
+
Source right ascension
|
|
161
|
+
:param obsdec: (optional) scalar
|
|
162
|
+
Source declination
|
|
163
|
+
'''
|
|
164
|
+
hdu = fits.PrimaryHDU(m)
|
|
165
|
+
hdu.header['CDELT1'] = -1*dx*np.radians(1.)/(3600.*1e6)
|
|
166
|
+
hdu.header['CDELT2'] = dx*np.radians(1.)/(3600.*1e6)
|
|
167
|
+
hdu.header['OBSRA'] = obsra
|
|
168
|
+
hdu.header['OBSDEC'] = obsdec
|
|
169
|
+
hdu.header['FREQ'] = freq
|
|
170
|
+
hdu.writeto(dest,clobber=True)
|
|
171
|
+
|
|
172
|
+
def FTElementFast(img,dx,baseline):
|
|
173
|
+
'''
|
|
174
|
+
Return complex visibility.
|
|
175
|
+
|
|
176
|
+
:param img: ``(n, n)``
|
|
177
|
+
numpy image array
|
|
178
|
+
:param dx: scalar
|
|
179
|
+
Pixel size in microarcseconds
|
|
180
|
+
:param baseline: ``(2, )``
|
|
181
|
+
(u,v) point in wavelengths
|
|
182
|
+
|
|
183
|
+
.. note:: To shift center try multipliny by :math:`\\mathrm{exp}(\\pi i u n_x\\Delta_x)` and watch out for the axis orientation.
|
|
184
|
+
'''
|
|
185
|
+
nx = img.shape[-1]
|
|
186
|
+
du = 1./(nx * dx * np.radians(1.)/(3600*1e6))
|
|
187
|
+
ind = np.arange(nx)
|
|
188
|
+
return np.sum(img * np.dot(\
|
|
189
|
+
np.transpose([np.exp(-2j*np.pi/du/nx*baseline[1]*np.flipud(ind))]),\
|
|
190
|
+
[np.exp(-2j*np.pi/du/nx*baseline[0]*ind)]))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scatterbrane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A python module to simulate the effect of anisotropic scattering on astrophysical images.
|
|
5
|
+
Author-email: Katherine Rosenfeld <krosenf@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, http://krosenfeld.github.io/scatterbrane/
|
|
8
|
+
Keywords: scattering,astronomy,EHT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: astropy
|
|
19
|
+
Requires-Dist: imageio
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: scipy
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
scatterbrane
|
|
26
|
+
============
|
|
27
|
+
|
|
28
|
+
A python module for adding realistic scattering to astronomical images. This version
|
|
29
|
+
represents a very early release so please submit a pull request if you have trouble or
|
|
30
|
+
need help for your application.
|
|
31
|
+
|
|
32
|
+
For installation instructions and help getting started please see `the documentation <http://krosenfeld.github.io/scatterbrane/>`_.
|
|
33
|
+
|
|
34
|
+
Installation
|
|
35
|
+
------------
|
|
36
|
+
|
|
37
|
+
This project now uses ``pyproject.toml`` and `uv <https://docs.astral.sh/uv/>`_ for dependency management.
|
|
38
|
+
|
|
39
|
+
Install the package and its runtime dependencies with:
|
|
40
|
+
|
|
41
|
+
.. code-block:: bash
|
|
42
|
+
|
|
43
|
+
uv sync
|
|
44
|
+
|
|
45
|
+
Install the release and development tooling with:
|
|
46
|
+
|
|
47
|
+
.. code-block:: bash
|
|
48
|
+
|
|
49
|
+
uv sync --group dev
|
|
50
|
+
|
|
51
|
+
Release workflow
|
|
52
|
+
----------------
|
|
53
|
+
|
|
54
|
+
Version management is handled with ``bump-my-version``. To create a new tagged release:
|
|
55
|
+
|
|
56
|
+
.. code-block:: bash
|
|
57
|
+
|
|
58
|
+
uv run bump-my-version bump patch
|
|
59
|
+
|
|
60
|
+
This updates the package version, creates a commit, and creates a ``vX.Y.Z`` git tag. Pushing that tag to GitHub triggers the publish workflow for PyPI.
|
|
61
|
+
|
|
62
|
+
Reference
|
|
63
|
+
---------
|
|
64
|
+
|
|
65
|
+
`Johnson, M. D., & Gwinn, C. R. 2015, ApJ, 805, 180 <http://adsabs.harvard.edu/abs/2015ApJ...805..180J>`_
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.rst
|
|
3
|
+
pyproject.toml
|
|
4
|
+
scatterbrane/__init__.py
|
|
5
|
+
scatterbrane/brane.py
|
|
6
|
+
scatterbrane/tracks.py
|
|
7
|
+
scatterbrane/utilities.py
|
|
8
|
+
scatterbrane.egg-info/PKG-INFO
|
|
9
|
+
scatterbrane.egg-info/SOURCES.txt
|
|
10
|
+
scatterbrane.egg-info/dependency_links.txt
|
|
11
|
+
scatterbrane.egg-info/requires.txt
|
|
12
|
+
scatterbrane.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scatterbrane
|