Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ trim_trailing_whitespace=true
indent_style=space
indent_size=4

[*.yml]
[{*.yml,*.toml}]
indent_style=space
indent_size=2
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ name: CI

on:
push:
# branches:
# - 'main'
# - '*.*'
# - '!*backport*'
# tags:
# - 'v*'
# - '!*dev*'
# - '!*pre*'
# - '!*post*'
branches:
- 'main'
- '*.*'
- '!*backport*'
tags:
- 'v*'
- '!*dev*'
- '!*pre*'
- '!*post*'
pull_request:
# Allow manual runs through the web UI
workflow_dispatch:
Expand Down
1 change: 1 addition & 0 deletions changelog/83.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a basic visibility forward fitting method (`xrayvision.vis_forward_fit.forward_fit.vis_forward_fit`).
14 changes: 14 additions & 0 deletions docs/reference/forward_fit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.. vis_forward_fit:

Vis Forward Fit ('xrayvision.vis_forward_fit')
**********************************************

The ``vis_forward_fit`` submodule contains the visibility forward fitting methods

.. automodapi:: xrayvision.vis_forward_fit

.. automodapi:: xrayvision.vis_forward_fit.forward_fit
:include-all-objects:

.. automodapi:: xrayvision.vis_forward_fit.sources
:include-all-objects:
1 change: 1 addition & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ Reference
transform
utils
visibility
forward_fit

../whatsnew/index
1 change: 1 addition & 0 deletions examples/rhessi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
======================================

Create images from RHESSI visibility data

"""

import astropy.units as apu
Expand Down
38 changes: 29 additions & 9 deletions examples/stix.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from xrayvision.clean import vis_clean
from xrayvision.imaging import vis_psf_map, vis_to_map
from xrayvision.mem import mem, resistant_mean
from xrayvision.vis_forward_fit.forward_fit import vis_forward_fit
from xrayvision.vis_forward_fit.sources import Source, SourceList

###############################################################################
# Create images from STIX visibility data.
Expand All @@ -33,13 +35,13 @@
###############################################################################
# Lets have a look at the point spread function (PSF) or dirty beam

psf_map = vis_psf_map(stix_vis, shape=(129, 129) * apu.pixel, pixel_size=2 * apu.arcsec / apu.pix, scheme="uniform")
psf_map = vis_psf_map(stix_vis, shape=(129, 129) * apu.pixel, pixel_size=1 * apu.arcsec / apu.pix, scheme="uniform")
psf_map.plot()

###############################################################################
# Back projection

backproj_map = vis_to_map(stix_vis, shape=(129, 129) * apu.pixel, pixel_size=2 * apu.arcsec / apu.pix, scheme="uniform")
backproj_map = vis_to_map(stix_vis, shape=(129, 129) * apu.pixel, pixel_size=1 * apu.arcsec / apu.pix, scheme="uniform")
backproj_map.plot()

###############################################################################
Expand All @@ -48,7 +50,7 @@
clean_map, model_map, resid_map = vis_clean(
stix_vis,
shape=[129, 129] * apu.pixel,
pixel_size=[2, 2] * apu.arcsec / apu.pix,
pixel_size=[1, 1] * apu.arcsec / apu.pix,
clean_beam_width=20 * apu.arcsec,
niter=100,
)
Expand All @@ -62,17 +64,30 @@
percent_lambda = 2 / (snr_value**2 + 90)

mem_map = mem(
stix_vis, shape=[129, 129] * apu.pixel, pixel_size=[2, 2] * apu.arcsec / apu.pix, percent_lambda=percent_lambda
stix_vis, shape=[129, 129] * apu.pixel, pixel_size=[1, 1] * apu.arcsec / apu.pix, percent_lambda=percent_lambda
)

###############################################################################
# VIS_FWD_FIT

sources = SourceList([Source("circular", 15, 1, 2, 5)])

vis_fwd_map = vis_forward_fit(stix_vis, sources, shape=[129, 129] * apu.pixel, pixel_size=[1, 1] * apu.arcsec / apu.pix)

vis_fwd_pso_map = vis_forward_fit(
stix_vis, sources, method="PSO", shape=[129, 129] * apu.pixel, pixel_size=[1, 1] * apu.arcsec / apu.pix
)
mem_map.plot()

###############################################################################
# Comparison
fig = plt.figure(figsize=(10, 10))
fig.add_subplot(221, projection=psf_map)
fig.add_subplot(222, projection=backproj_map)
fig.add_subplot(223, projection=clean_map)
fig.add_subplot(224, projection=mem_map)
fig.add_subplot(231, projection=psf_map)
fig.add_subplot(232, projection=backproj_map)
fig.add_subplot(233, projection=clean_map)
fig.add_subplot(234, projection=mem_map)
fig.add_subplot(235, projection=mem_map)
fig.add_subplot(236, projection=mem_map)

axs = fig.get_axes()
psf_map.plot(axes=axs[0])
axs[0].set_title("PSF")
Expand All @@ -82,4 +97,9 @@
axs[2].set_title("Clean")
mem_map.plot(axes=axs[3])
axs[3].set_title("MEM")
vis_fwd_map.plot(axes=axs[4])
axs[4].set_title("VIS_FWRDFIT")
vis_fwd_pso_map.plot(axes=axs[5])
axs[5].set_title("VIS_FWRDFIT_PSO")

plt.show()
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies = [
"numpy>=1.24.0",
"packaging>=23.0",
"scipy>=1.13",
"xarray>=2023.5.0"
"xarray>=2023.5.0",
]
dynamic = ["version"]
keywords = ["solar", "physics", "solar", "sun", "x-rays"]
Expand All @@ -42,7 +42,10 @@ classifiers = [
map = [
"sunpy[map]>=5.1.0"
]
all = ["xrayvisim[map]"]
pso = [
"pymoo>=0.6.1.3"
]
all = ["xrayvisim[map,pso]"]
tests = [
"matplotlib>=3.8.0",
"pytest-astropy>=0.11.0",
Expand Down
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ filterwarnings =
# Until update code need to ignore missing WCS
ignore:.*:sunpy.util.exceptions.SunpyMetadataWarning
ignore:.*divide by zero.*:RuntimeWarning
ignore:The.*feasible.*:DeprecationWarning
# Can't use ipython in PyCharm debugger without this
#ignore::DeprecationWarning
7 changes: 6 additions & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exclude = [
]

[lint]
select = ["E", "F", "W", "UP", "PT"]
select = ["E", "F", "W", "UP", "PT", "C"]
extend-ignore = [
# pycodestyle (E, W)
"E501", # LineTooLong # TODO! fix
Expand All @@ -27,7 +27,12 @@ extend-ignore = [
"docs/conf.py" = ["E402"]
"docs/*.py" = [
"INP001", # Implicit-namespace-package. The examples are not a package.
"D100"
]
"examples/*.py" = [
"D"
]

"__init__.py" = ["E402", "F401", "F403"]
"test_*.py" = ["B011", "D", "E402", "PGH001", "S101"]
# Need to import clients to register them, but don't use them in file
Expand Down
1 change: 0 additions & 1 deletion xrayvision/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ def vis_clean(
map :
Return a `sunpy.map.Map` by default or array only if `False`
"""

dirty_map = vis_to_map(vis, shape=shape, pixel_size=pixel_size, **kwargs)
dirty_beam_shape = [x.value * 3 + 1 if x.value * 3 % 2 == 0 else x.value * 3 for x in shape] * shape.unit
dirty_beam = vis_psf_image(vis, shape=dirty_beam_shape, pixel_size=pixel_size, **kwargs)
Expand Down
5 changes: 2 additions & 3 deletions xrayvision/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Force MPL to use non-gui backends for testing.
import matplotlib

try:
pass
except ImportError:
pass
else:
matplotlib.use("Agg")
# else:
# matplotlib.use("Agg")
2 changes: 1 addition & 1 deletion xrayvision/coordinates/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def projective_wcs_to_frame(wcs):
observer = None
for frame, attr_names in required_attrs.items():
attrs = [getattr(wcs.wcs.aux, attr_name) for attr_name in attr_names]
if all([attr is not None for attr in attrs]):
if all(attr is not None for attr in attrs):
kwargs = {"obstime": dateavg}
if rsun is not None:
kwargs["rsun"] = rsun
Expand Down
5 changes: 1 addition & 4 deletions xrayvision/mem.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def _estimate_flux(vis, shape, pixel, maxiter=1000, tol=1e-3):
Estimated total flux

"""

Hv, Lip, Visib = _prepare_for_optimise(pixel, shape, vis)

# PROJECTED LANDWEBER
Expand Down Expand Up @@ -237,7 +236,6 @@ def _get_mean_visibilities(vis, shape, pixel):
-------
Mean Visibilities
"""

if vis.amplitude_uncertainty is None:
amplitude_uncertainty = np.ones_like(vis.visibilities)
else:
Expand Down Expand Up @@ -374,7 +372,6 @@ def _proximal_operator(z, f, m, lamb, Lip, niter=250):
-------

"""

# INITIALIZATION OF THE DYKSTRA - LIKE SPLITTING
x = z[:]
p = np.zeros_like(x)
Expand Down Expand Up @@ -434,11 +431,11 @@ def _optimise_fb(Hv, Visib, Lip, flux, lambd, shape, pixel, maxiter, tol):
Maximum number of iterations
tol :
Tolerance value used in the stopping rule ( || x - x_old || <= tol || x_old ||)

Returns
-------
MEM Image
"""

# 'f': value of the total flux of the image (taking into account the area of the pixel)
f = flux / (pixel[0] * pixel[1])
# 'm': total flux divided by the number of pixels of the image
Expand Down
38 changes: 20 additions & 18 deletions xrayvision/tests/test_transform.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import astropy.units as apu
import numpy as np
import pytest
from numpy.fft import fft2, fftshift, ifft2, ifftshift
from numpy.fft import fft2, ifft2
from numpy.testing import assert_allclose
from scipy import signal

Expand Down Expand Up @@ -377,34 +377,36 @@ def test_phase_center_equivalence():
assert np.allclose(data, img2)


def test_fft_equivalence():
# Odd (3, 3) so symmetric and chose shape and pixel size so xy values will run from 0 to 2 the same as in fft
# TODO: add same kind of test for even for fft2 then A[n/2] has both pos and negative nyquist frequencies
# e.g shape (2, 2), (3, 2), (2, 3)
shape = (3, 3)
pixel = (1, 1)
center = (1, 1)
@pytest.mark.parametrize("dim", ((2, 3, 4, 5, 6)))
def test_fft_equivalence(dim):
shape = np.array((dim, dim))
pixel = np.array((1, 1))

data = np.arange(np.prod(shape)).reshape(shape)
# In order to replicate the FFT need to make the product of x*u + y*v match mk/M + nl/N where M, N and the array
# dimensions so neex x, y to go from 0 to M-1, N-1 and u, v 0 to m-1/M, and n-1/N so need to chose parameters so
# this happens namely the center for u, v and the dft
uv_center = -1 / ((0 - shape / 2 + 0.5) / (shape * pixel))
xy_center = -1 * (0 - shape / 2 + 0.5)
vv = generate_uv(
shape[0] * apu.pix, phase_center=center[0] * apu.arcsec, pixel_size=pixel[0] * apu.arcsec / apu.pix
shape[0] * apu.pix, phase_center=uv_center[0] * apu.arcsec, pixel_size=pixel[0] * apu.arcsec / apu.pix
)
uu = generate_uv(
shape[1] * apu.pix, phase_center=center[1] * apu.arcsec, pixel_size=pixel[1] * apu.arcsec / apu.pix
shape[1] * apu.pix, phase_center=uv_center[1] * apu.arcsec, pixel_size=pixel[1] * apu.arcsec / apu.pix
)
u, v = np.meshgrid(uu, vv)
u = u.flatten()
v = v.flatten()

vis = dft_map(data, u=u, v=v, pixel_size=pixel * apu.arcsec / apu.pix, phase_center=center * apu.arcsec)
data = np.arange(np.prod(shape)).reshape(shape)
vis = dft_map(data, u=u, v=v, pixel_size=pixel * apu.arcsec / apu.pix, phase_center=xy_center * apu.arcsec)

ft = fft2(data)
fts = fftshift(ft)
vis = vis.reshape(shape)
# Convention in xrayvison has the minus sign on the forward transform but numpy has it on reverse
# Convention in xrayvison has the minus sign on the forward transform but numpy has it on reverse and the first
#
# The rtol seems to vary as the size increase I'm assuming poor round off error handling in the naive DFT/IDFT
vis_conj = np.conjugate(vis)
assert_allclose(fts, vis_conj, atol=1e-13)
assert_allclose(ft, vis_conj, rtol=1e-10, atol=1e-10)

vis_ft = ifftshift(vis_conj)
img = ifft2(vis_ft)
assert_allclose(np.real(img), data, atol=1e-14)
img = ifft2(vis_conj)
assert_allclose(np.real(img), data, rtol=1e-10, atol=1e-10)
2 changes: 1 addition & 1 deletion xrayvision/tests/test_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def test_vis_eq(visibilities):
def test_meta_eq(vis_meta):
meta = vis_meta
assert meta == meta
meta = vm.VisMeta(dict())
meta = vm.VisMeta({})
assert meta == meta


Expand Down
6 changes: 2 additions & 4 deletions xrayvision/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,8 @@ def dft_map(
x, y = np.meshgrid(x, y)
uv = np.vstack([u, v])
# Check units are correct for exp need to be dimensionless and then remove units for speed
if (uv[0, :] * x[0, 0]).unit == apu.dimensionless_unscaled and (
uv[1, :] * y[0, 0]
).unit == apu.dimensionless_unscaled:
uv = uv.value # type: ignore
if (uv.unit * x.unit) == apu.dimensionless_unscaled and (uv.unit * y.unit) == apu.dimensionless_unscaled:
uv = uv.value # src_type: ignore
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment. Should be # type: ignore not # src_type: ignore.

Suggested change
uv = uv.value # src_type: ignore
uv = uv.value # type: ignore

Copilot uses AI. Check for mistakes.
x = x.value
y = y.value

Expand Down
Empty file.
Loading
Loading