PSFModels.jl

GitHubBuild StatusPkgEvalCoverageLicense

Installation

PSFModels can be added from the Julia package manager

julia>]

(@v1.6) pkg> add PSFModels

Getting Started

To import the library

julia> using PSFModels

None of the models are exported to avoid namespace clashes, but it can be verbose. You can either import names directly

julia> using PSFModels: Gaussian

julia> model = Gaussian(8)

or you can create an alias for PSFModels

# julia version 1.5 or below
using PSFModels
const M = PSFModels
# julia version 1.6 or above
using PSFModels as M

model = M.Gaussian(10)
PSFModels.PSFModelsModule

PSFModels

Statistical models for constructing point-spread functions (PSFs). These models act like matrices but without allocating any memory, which makes them efficient to fit and apply.

Models

The following models are currently implemented

Parameters

In general, the PSFs have a position, a full-width at half-maximum (FWHM) measure, and an amplitude. The position a 1-based pixel coordinate system, where (1, 1) represents the center of the bottom left pixel. This matches the indexing style of Julia as well as DS9, IRAF, SourceExtractor, and WCS. If a position is not specified, it is set to (0, 0). The FWHM is a consistent scale parameter for the models. All models support a scalar (isotropic) FWHM and a FWHM for each axis (diagonal).

Usage

Using the models should feel just like an array. In fact, PSFModels.PSFModel <: AbstractMatrix. However, no data is stored and no allocations have to be made. In other words, representing the models as matrices is merely a convenience, since typically astronomical data is stored in dense arrays.

julia> m = PSFModels.Gaussian(5); # fwhm of 5 pixels, centered at (0, 0)

julia> m[0, 0] # [y, x] for indexing
1.0

To control the amplitude, the best method is using scalar multiplication or division. These operations create another lazy object (ScaledPSFModel) that scales the original model without having to broadcast and potentially allocate.

julia> m_scaled = 20 * m;

julia> m_scaled(0, 0)
20.0

julia> m′ = m_scaled / 20;

julia> m′(0, 0)
1.0

Because the model is a matrix, it needs to have a size. In this case, the size is maxsize * FWHM pixels, centered around the origin, and rounded up. We can see how this alters the indices from a typical Matrix

julia> size(m)
(17, 17)

julia> axes(m)
(-8:8, -8:8)

if we want to collect the model into a dense matrix, regardless of the indexing (e.g. to prepare for cross-correlation), we can simply

julia> stamp = collect(m);

these axes are merely a convenience for bounding the model, since they accept any real number as input.

julia> m[100, 10000] # index-like inputs [y, x]
0.0

julia> m(2.4, 1.7) # valid for any real (x, y)
0.38315499005194587

By bounding the model, we get a cutout which can be applied to arrays with much larger dimensions without having to iterate over the whole matrix

julia> big_mat = ones(101, 101);

julia> model = PSFModels.Gaussian(51, 51, 2); # center of big_mat, fwhm=2

julia> ax = map(intersect, axes(big_mat), axes(model))
(48:54, 48:54)

julia> cutout = @view big_mat[ax...]
7×7 view(::Array{Float64,2}, 48:54, 48:54) with eltype Float64:
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0  1.0  1.0

julia> stamp = @view model[ax...];

julia> photsum = sum(cutout .* stamp)
4.5322418212890625

Nice- we only had to reduce ~50 pixels instead of ~10,000 to calculate the aperture sum, all in under a microsecond (on my machine).

Since the models are lazy, that means the type of the output can be specified, as long as it can be converted to from a real number (so no integer types).

julia> mbig = PSFModels.Gaussian{BigFloat}(12);

julia> sum(mbig)
163.07467408408593790971336380361822460116627553361468017101287841796875

finally, we provide plotting recipes from RecipesBase.jl, which can be seen in use in the API/Reference section.

using Plots
model = PSFModels.Gaussian(8)
plot(model)              # default axes
plot(model, 1:5, 1:5)    # custom axes
plot(model, axes(other)) # use axes from other array
Tip: Automatic Differentation

Forward-mode AD libraries tend to use dual numbers, which can cause headaches getting the types correct. We recommend using the primal vector's element type to avoid these headaches

# example generative model for position and scalar fwhm
model(X::AbstractVector{T}) where {T} = PSFModels.Gaussian{T}(X...)
source

Benchmarks

The benchmarks can be found in the bench/ folder. To run them, first install the python dependencies

$ pip install -r bench/requirements.txt

Then run the benchmark

$ julia --project=bench bench/bench.jl

System Information

Julia Version 1.5.0
Commit 96786e22cc (2020-08-01 23:44 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin18.7.0)
  CPU: Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-9.0.1 (ORCJIT, skylake)
Environment:
  JULIA_NUM_THREADS = 4

table = CSV.File(benchdir("results.csv")) |> DataFrame

3 rows × 3 columns

namepsfmodelsastropy
StringFloat64Float64
1Gaussian1.58327e-80.000164387
2AiryDisk3.69909e-80.000108041
3Moffat1.19269e-89.8272e-5
@df table groupedbar(:name, [:psfmodels :astropy];
    ylabel="time (s)", yscale=:log10, leg=:outertopright,
    label=["PSFModels.jl" "Astropy"], size=(500, 300))