Skip to content

Commit

Permalink
Merge pull request #337 from AstarVienna/hb/fix331stackedstars
Browse files Browse the repository at this point in the history
Properly stack stars
  • Loading branch information
hugobuddel committed Jan 12, 2024
2 parents 7495631 + 15141de commit f187c6d
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 6 deletions.
22 changes: 20 additions & 2 deletions scopesim/optics/fov.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from copy import deepcopy
from itertools import chain
from collections.abc import Iterable

import numpy as np
from scipy.interpolate import interp1d
Expand Down Expand Up @@ -389,13 +390,28 @@ def make_image_hdu(self, use_photlam=False):

for flux, weight, x, y in self._make_image_tablefields(fluxes):
if utils.from_currsys(self.meta["sub_pixel"]):
# These x and y should not be arrays when sub_pixel is
# enabled, it is therefore not necessary to deploy the fix
# below in the else-branch.
assert not isinstance(x, Iterable), "x must be an integer"
canvas_image_hdu.data[y, x] += flux * weight
else:
# Mask out any stars that were pushed out of the fov by rounding
mask = ((x < canvas_image_hdu.data.shape[1]) *
(y < canvas_image_hdu.data.shape[0]))
canvas_image_hdu.data[y[mask], x[mask]
] += flux[mask] * weight[mask]

# This used to contain this line:
# canvas_image_hdu.data[y[mask], x[mask]] += flux[mask] * weight[mask]
# However, that is wrong when there are duplicate (x, y) pairs.
# In those cases, only the last source flux is added to the
# pixel. Therefor it is necessary to iterate over the sources.
# The stacking of stars is tested by TestStackedStars in
# test_flux_is_conserved_through_full_system.py

for yi, xi, fluxi, weighti in zip(
y[mask], x[mask], flux[mask], weight[mask]):
canvas_image_hdu.data[yi, xi] += fluxi * weighti


canvas_image_hdu.data = sum(self._make_image_backfields(fluxes),
start=canvas_image_hdu.data)
Expand Down Expand Up @@ -581,6 +597,8 @@ def make_cube_hdu(self):
start=canvas_cube_hdu.data)

for flux, x, y in self._make_cube_tablefields(specs):
# To prevent adding array values in this manner.
assert not isinstance(x, Iterable), "x should be integer"
canvas_cube_hdu.data[:, y, x] += flux

canvas_cube_hdu.data = sum(self._make_cube_backfields(specs),
Expand Down
5 changes: 5 additions & 0 deletions scopesim/optics/image_plane_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from typing import Tuple
from itertools import product
from collections.abc import Iterable

import numpy as np
from astropy import units as u
Expand Down Expand Up @@ -328,6 +329,8 @@ def _add_intpixel_sources_to_canvas(canvas_hdu, xpix, ypix, flux, mask):
canvas_hdu.header["comment"] = f"Adding {len(flux)} int-pixel files"
for xpx, ypx, flx, msk in zip(xpix.astype(int), ypix.astype(int),
flux, mask):
# To prevent adding array values in this manner.
assert not isinstance(xpx, Iterable), "xpx should be integer"
canvas_hdu.data[ypx, xpx] += flx.value * msk

return canvas_hdu
Expand All @@ -341,6 +344,8 @@ def _add_subpixel_sources_to_canvas(canvas_hdu, xpix, ypix, flux, mask):
xx, yy, fracs = sub_pixel_fractions(xpx, ypx)
for x, y, frac in zip(xx, yy, fracs):
if y < canvas_shape[0] and x < canvas_shape[1]:
# To prevent adding array values in this manner.
assert not isinstance(x, Iterable), "x should be integer"
canvas_hdu.data[y, x] += frac * flx.value

return canvas_hdu
Expand Down
7 changes: 5 additions & 2 deletions scopesim/source/source_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from collections.abc import Iterable

import numpy as np
from astropy import wcs, units as u
Expand Down Expand Up @@ -146,8 +147,10 @@ def make_imagehdu_from_table(x, y, flux, pix_scale=1*u.arcsec):
yint, xint = ypix.astype(int), xpix.astype(int)

image = np.zeros((np.max(xint) + 1, np.max(yint) + 1))
for ii in range(len(xint)):
image[xint[ii], yint[ii]] += flux[ii]
for xi, yi, fluxi in zip(xint, yint, flux):
# To prevent adding array values in this manner.
assert not isinstance(xi, Iterable), "xi should be integer"
image[xi, yi] += fluxi

hdu = fits.ImageHDU(data=image)
hdu.header.extend(the_wcs.to_header())
Expand Down
25 changes: 25 additions & 0 deletions scopesim/tests/mocks/py_objects/source_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ def _table_source():
return tbl_source


def _table_source_overlapping():
"""A table with sources that are exactly at the same place.
This allows testing whether the fluxes are correctly stacked.
Four sources are stacked at (-1, -1), and one source with 4 times
the weight is at (1, 1). Both regions should have the same flux.
"""
n = 101
unit = u.Unit("ph s-1 m-2 um-1")
wave = np.linspace(0.1150, 2.5, n) * u.um
specs = [SourceSpectrum(Empirical1D, points=wave,
lookup_table=1e9 * np.ones(n) * unit)]
w1 = 0.0005
tbl = Table(names=["x", "y", "ref", "weight", "spec_types"],
data=[[-20, -20, -20, -20, 20] * u.arcsec,
[-20, -20, -20, -20, 20] * u.arcsec,
[0, 0, 0, 0, 0],
[w1, w1, w1, w1, 4 * w1],
["F0II",] * 5,
])
tbl_source = Source(table=tbl, spectra=specs)
return tbl_source


def _image_source(dx=0, dy=0, angle=0, weight=1):
"""
Produce a source with 3 point sources on a random BG.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from scopesim.optics.optical_train import OpticalTrain
from scopesim.commands import UserCommands

from scopesim.tests.mocks.py_objects.source_objects import _image_source, \
_single_table_source
from scopesim.tests.mocks.py_objects.source_objects import (
_image_source, _single_table_source, _table_source_overlapping)

import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
Expand Down Expand Up @@ -82,3 +82,70 @@ def test_flux_is_conserved_for_yes_bg_emission(self, non_unity_cmds, tbl_src):
assert src_flux == approx(1) # u.Unit("ph s-1")
assert np.sum(im - np.median(im)) == approx(0.45, rel=1e-2)
assert np.median(im) == approx(1.5, abs=1e-2)


@pytest.mark.usefixtures("protect_currsys", "patch_all_mock_paths")
class TestStackedStars:
"""Test whether stars can be stacked."""

def test_stacked_stars(self):
"""Test whether stars can be stacked.
Four stacked faint stars should have the same magnitude as one bright star.
"""
stars = _table_source_overlapping()
temp_celsius = 5.0
dit = 10 # s
ndit = 1
s_filter = "H"
cmd = UserCommands(use_instrument="basic_instrument")

cmd.update(properties={
"!OBS.ndit": ndit,
"!OBS.dit": dit,
"!OBS.airmass": 1.0,
"!ATMO.background.filter_name": s_filter,
"!ATMO.temperature": temp_celsius,
"!TEL.temperature": temp_celsius,
"!DET.width": 512, # pixel
"!DET.height": 512,
})
micado = OpticalTrain(cmd)

# disabling effects
micado["dark_current"].include = False
micado["shot_noise"].include = False
micado["detector_linearity"].include = False
micado["exposure_action"].include = False
micado['readout_noise'].include = False
micado["source_fits_keywords"].include = False
micado["effects_fits_keywords"].include = False
micado["config_fits_keywords"].include = False
micado["extra_fits_keywords"].include = False
micado["telescope_reflection"].include = False
micado["qe_curve"].include = False
micado["static_surfaces"].include = False
micado.observe(stars)
hdus_h = micado.readout()

im_h = hdus_h[0][1].data
dx, dy = 255, 255

if PLOTS:
fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(12, 12))
axes.imshow(np.sqrt(im_h), norm=LogNorm(), cmap="inferno")
axes.set_title('H-band MORFEO "PSF Generic" ')
plt.show()

quadrants = im_h[:dx, :dy], im_h[dx:, :dy], im_h[:dx, dy:], im_h[dx:, dy:]
the_sums = [q.sum() for q in quadrants]
flux_one_star, empty1, empty2, flux_four_stacked_stars = the_sums

# Check whether the stars are equal.
assert flux_one_star == pytest.approx(flux_four_stacked_stars, rel=0.05)

# Check whether the empty skies are equal.
assert empty1 == pytest.approx(empty2, rel=0.05)

# Check whether the star is brighter than the sky.
assert flux_one_star > empty1 * 2

0 comments on commit f187c6d

Please sign in to comment.