Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ extend-exclude ='''
'''

[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
check_untyped_defs = true
files = "sourcefinder"
16 changes: 10 additions & 6 deletions sourcefinder/accessors/casaimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from casacore.tables import table as casacore_table

from sourcefinder.utils import is_valid_beam_tuple
from sourcefinder.accessors.dataaccessor import DataAccessor
from sourcefinder.utility.coordinates import WCS
from sourcefinder.utility.coordinates import mjd2datetime
Expand Down Expand Up @@ -48,12 +49,15 @@ def __init__(self, url, plane=0, beam=None):
self.freq_eff, self.freq_bw = self.parse_frequency(table)
self.pixelsize = self.parse_pixelsize()

if beam:
(bmaj, bmin, bpa) = beam
else:
bmaj, bmin, bpa = self.parse_beam(table)
self.beam = self.degrees2pixels(bmaj, bmin, bpa, self.pixelsize[0],
self.pixelsize[1])
if is_valid_beam_tuple(beam) or not is_valid_beam_tuple(self.beam):
# An argument-supplied beam overrides a beam derived from
# (bmaj, bmin, bpa) in a config.toml. Only if those two options
# are not specified, we parse the beam from the header.
bmaj, bmin, bpa = beam if is_valid_beam_tuple(beam) else (
self.parse_beam(table))
self.beam = self.degrees2pixels(
bmaj, bmin, bpa, self.pixelsize[0], self.pixelsize[1]
)

def parse_data(self, table, plane=0):
"""
Expand Down
57 changes: 40 additions & 17 deletions sourcefinder/accessors/dataaccessor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import logging

import numpy
import numpy as np
from math import degrees, sqrt, sin, pi, cos
from dataclasses import dataclass
from dataclasses import dataclass, field
from sourcefinder.utils import is_valid_beam_tuple
from sourcefinder.utility.coordinates import WCS
from sourcefinder.config import ImgConf
from typing import Optional, cast

logger = logging.getLogger(__name__)

Expand All @@ -16,7 +19,7 @@ class DataAccessor:
Data accessors provide a uniform way for the ImageData class (i.e.,
generic image representation) to access the various ways in which
images may be stored (FITS files, arrays in memory, potentially HDF5,
etc).
etc.).

This class cannot be instantiated directly, but should be subclassed
and the abstract properties provided. Note that all abstract
Expand Down Expand Up @@ -66,17 +69,26 @@ class DataAccessor:
provides key info in a simple dict format.
"""

beam: tuple
centre_ra: float
centre_decl: float
data: numpy.ndarray
data: np.ndarray
freq_bw: float
freq_eff: float
pixelsize: tuple
tau_time: float
taustart_ts: float
url: str
wcs: WCS
beam: Optional[tuple[float, float, float]] = field(default=None)
conf: Optional[ImgConf] = field(default=None, repr=False)

def __post_init__(self):
if self.conf is not None:
beam_tuple = (self.conf.bmaj, self.conf.bmin, self.conf.bpa)
if is_valid_beam_tuple(beam_tuple):
deltax, deltay = self.pixelsize
self.beam = DataAccessor.degrees2pixels(*beam_tuple,
deltax, deltay)

def extract_metadata(self) -> dict:
"""
Expand All @@ -94,22 +106,30 @@ def extract_metadata(self) -> dict:
A dictionary containing key-value pairs of class attributes
formatted for database storage.
"""
return {
metadata = {
'tau_time': self.tau_time,
'freq_eff': self.freq_eff,
'freq_bw': self.freq_bw,
'taustart_ts': self.taustart_ts,
'url': self.url,
'beam_smaj_pix': self.beam[0],
'beam_smin_pix': self.beam[1],
'beam_pa_rad': self.beam[2],
'centre_ra': self.centre_ra,
'centre_decl': self.centre_decl,
'deltax': self.pixelsize[0],
'deltay': self.pixelsize[1],
}

def parse_pixelsize(self):

if is_valid_beam_tuple(self.beam):
beam = cast(tuple[float, float, float], self.beam)
metadata.update({
'beam_smaj_pix': beam[0],
'beam_smin_pix': beam[1],
'beam_pa_rad': beam[2],
})

return metadata

def parse_pixelsize(self) -> tuple[float, float]:
"""
Returns
-------
Expand Down Expand Up @@ -141,7 +161,8 @@ def parse_pixelsize(self):
return deltax, deltay

@staticmethod
def degrees2pixels(bmaj, bmin, bpa, deltax, deltay):
def degrees2pixels(bmaj, bmin, bpa, deltax, deltay) -> (
tuple)[float, float, float]:
"""
Convert beam in degrees to beam in pixels and radians.
For example, FITS beam parameters are in degrees.
Expand All @@ -161,12 +182,14 @@ def degrees2pixels(bmaj, bmin, bpa, deltax, deltay):

Returns
-------
semimaj : float
Beam semi-major axis in pixels.
semimin : float
Beam semi-minor axis in pixels.
theta : float
Beam position angle in radians.
tuple
A tuple containing:
- semimaj : float
Beam semi-major axis in pixels.
- semimin : float
Beam semi-minor axis in pixels.
- theta : float
Beam position angle in radians.
"""
theta = pi * bpa / 180
semimaj = (bmaj / 2.) * (sqrt(
Expand Down
19 changes: 12 additions & 7 deletions sourcefinder/accessors/fitsimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import astropy.io.fits as pyfits

from sourcefinder.utils import is_valid_beam_tuple
from sourcefinder.accessors.dataaccessor import DataAccessor
from sourcefinder.utility.coordinates import WCS

Expand Down Expand Up @@ -41,13 +42,17 @@ def __init__(self, url, plane=None, beam=None, hdu_index=0):
self.taustart_ts, self.tau_time = self.parse_times()
self.freq_eff, self.freq_bw = self.parse_frequency()
self.pixelsize = self.parse_pixelsize()
if beam:
(bmaj, bmin, bpa) = beam
else:
(bmaj, bmin, bpa) = self.parse_beam()
self.beam = self.degrees2pixels(
bmaj, bmin, bpa, self.pixelsize[0], self.pixelsize[1]
)

if is_valid_beam_tuple(beam) or not is_valid_beam_tuple(self.beam):
# An argument-supplied beam overrides a beam derived from
# (bmaj, bmin, bpa) in a config.toml. Only if those two options
# are not specified, we parse the beam from the header.
bmaj, bmin, bpa = beam if is_valid_beam_tuple(beam) else (
self.parse_beam())
self.beam = self.degrees2pixels(
bmaj, bmin, bpa, self.pixelsize[0], self.pixelsize[1]
)

self.centre_ra, self.centre_decl = self.calculate_phase_centre()

# Bonus attribute
Expand Down
18 changes: 11 additions & 7 deletions sourcefinder/accessors/fitsimageblob.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy

from sourcefinder.utils import is_valid_beam_tuple
from sourcefinder.accessors.fitsimage import FitsImage

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -40,13 +41,16 @@ def __init__(self, hdulist, plane=None, beam=None, hdu_index=0):
self.freq_eff, self.freq_bw
self.url = "_".join([str(x) for x in elements])

if beam:
(bmaj, bmin, bpa) = beam
else:
(bmaj, bmin, bpa) = self.parse_beam()
self.beam = self.degrees2pixels(
bmaj, bmin, bpa, self.pixelsize[0], self.pixelsize[1]
)
if is_valid_beam_tuple(beam) or not is_valid_beam_tuple(self.beam):
# An argument-supplied beam overrides a beam derived from
# (bmaj, bmin, bpa) in a config.toml. Only if those two options
# are not specified, we parse the beam from the header.
bmaj, bmin, bpa = beam if is_valid_beam_tuple(beam) else (
self.parse_beam())
self.beam = self.degrees2pixels(
bmaj, bmin, bpa, self.pixelsize[0], self.pixelsize[1]
)

self.centre_ra, self.centre_decl = self.calculate_phase_centre()

# Bonus attribute
Expand Down
14 changes: 11 additions & 3 deletions sourcefinder/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,17 @@ def __init__(self, data, beam, wcs, conf: Conf = Conf(image=ImgConf(),
# single precision is good enough in all cases.
self.rawdata = np.ascontiguousarray(data, dtype=np.float32)
self.wcs = wcs # a utility.coordinates.wcs instance
self.beam = beam # tuple of (semimaj, semimin, theta) with semimaj and
# semimin in pixel coordinates and theta, the position angle, in
# radians.

if utils.is_valid_beam_tuple(beam):
self.beam = beam # tuple of (semimaj, semimin, theta) with
# semimaj and semimin in pixel coordinates and theta, the position
# angle, in radians.
else:
raise ValueError(("Partial beam specification: one or more of "
"(bmaj, bmin, bpa) are not specified, "
"adequately, image processing is not possible.",
RuntimeWarning))

# These three quantities are only dependent on the beam, so should be
# calculated once the beam is known and not for each source separately.
self.fudge_max_pix_factor = utils.fudge_max_pix(beam[0], beam[1],
Expand Down
7 changes: 7 additions & 0 deletions sourcefinder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import math
import numpy as np
from numbers import Real
import scipy.integrate
from scipy.ndimage import distance_transform_edt

Expand Down Expand Up @@ -202,6 +203,12 @@ def generate_result_maps(data, sourcelist):

return gaussian_map, residual_map

def is_valid_beam_tuple(b) -> bool:
return (
isinstance(b, tuple)
and len(b) == 3
and all(isinstance(x, Real) and x is not None for x in b)
)

def calculate_correlation_lengths(semimajor, semiminor):
"""Calculate the Condon correlation lengths.
Expand Down