Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT : Add MOS functionality to ScopeSim #357

Draft
wants to merge 47 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
42976d9
added option for single fov_volume from ApertureList
astronomyk Oct 13, 2022
09650a1
inital commit for UnresolvedSpectralTraceList
astronomyk Oct 15, 2022
8bff354
bunch of changes to book-keeping in SpectralTrace and co
astronomyk Oct 19, 2022
075c268
working common FOV for image slicer
astronomyk Oct 24, 2022
6f75f2e
Minor
astronomyk Dec 5, 2022
9e32509
working code for extending FOV beyond the aperture borders
astronomyk Dec 8, 2022
5cf0584
minor doc
astronomyk Dec 10, 2022
9f88505
restart of MOS code
astronomyk Jan 5, 2023
c23d97f
Merge remote-tracking branch 'origin/kl/psf_vor_slit' into kl/mos_branch
astronomyk Jan 5, 2023
73960a8
Merge remote-tracking branch 'origin/kl/unresolved_spec_trace_list' i…
astronomyk Jan 5, 2023
d6e2333
minor
astronomyk Jan 6, 2023
326f164
all previous SPEC modes (LSS; iFU) working again
astronomyk Jan 6, 2023
691ec1b
found problem with mos. Cube should be in fov.cube. fov.hdu should ha…
astronomyk Jan 6, 2023
e83eaf8
working code for MOS traces, tests are lacking
astronomyk Jan 9, 2023
5984baa
Ready? for MOSAIC meeting
astronomyk Jan 9, 2023
22389fb
added radial profile PSF for fibres, and tests
astronomyk Jan 26, 2023
7a6b458
minor
astronomyk Mar 27, 2023
6d272b2
Minor logging cmds to optical train
astronomyk Apr 20, 2023
3c37dc9
Merge branch 'dev_master' of https://github.com/AstarVienna/ScopeSim …
astronomyk Apr 20, 2023
e6f984e
merged in latest from dev_master
astronomyk Apr 20, 2023
0b40cd6
big finding for mosaic
astronomyk Apr 24, 2023
8868cbe
add pandas as dependencies
clementhottier Apr 24, 2023
187ab9d
add Effect to deal with trace generator
clementhottier Apr 24, 2023
931832f
fix the name of aperture
clementhottier Apr 24, 2023
90b8a94
minor
astronomyk Apr 24, 2023
e6e1ab9
Merge branch 'kl/mos_branch' of https://github.com/AstarVienna/ScopeS…
astronomyk Apr 24, 2023
37e16ee
Minor
astronomyk May 2, 2023
28a8e8e
resolve conflicts with kl's master branch
astronomyk Aug 21, 2023
a8fabbe
Merge branch 'dev_master' of https://github.com/AstarVienna/ScopeSim …
astronomyk Sep 26, 2023
793c5d6
merged pre-v0.7 from dev_master into mosaic branch
astronomyk Oct 17, 2023
84742f0
Merge branch 'dev_master' of https://github.com/AstarVienna/ScopeSim …
astronomyk Oct 17, 2023
e517bdd
merged with very latest dev_master
astronomyk Oct 17, 2023
3dc5f85
Merge branch 'dev_master' of https://github.com/AstarVienna/ScopeSim …
astronomyk Dec 15, 2023
b431a95
resolved conflicts with dev_master 16 Dec 2023
astronomyk Dec 16, 2023
56b97be
Merge branch 'master' into kl/mos_branch
astronomyk Dec 16, 2023
a81114d
synced with dev_master 2024.31.01
astronomyk Jan 31, 2024
246a615
lots of small things to get the MOSAIC simulator back up and running.…
astronomyk Jan 31, 2024
5028e1d
merged post-currsys-updates from dev_master into branch
astronomyk Feb 7, 2024
bac119f
stashing changes to mos
astronomyk Feb 13, 2024
0a4cdd7
Merge branch 'dev_master' into kl/mos_branch
astronomyk Feb 23, 2024
40e9656
added horizontal mapping of UnresolvedSpectralTrace
astronomyk Feb 23, 2024
37a60ca
mosaic specific hack for multiple fibre traces
astronomyk Mar 5, 2024
bef5303
start to refactor the trace generator for mosaic
astronomyk Mar 5, 2024
cc43780
tests for the mosaic PSF effects
astronomyk Mar 5, 2024
d07a177
added Trace- and TraceHDUListGenerators to SpecTraceListUtils.py to a…
astronomyk Mar 7, 2024
a7239a3
Refactored MosaicSpectralTraceList and removed TraceGenerator
astronomyk Mar 8, 2024
8a5ac4a
test file for bundle trans corr
astronomyk Mar 13, 2024
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
164 changes: 140 additions & 24 deletions scopesim/effects/apertures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from matplotlib.path import Path as MPLPath # rename to avoid conflict with pathlib
from astropy.io import fits
from astropy import units as u
from astropy.table import Table
from astropy.table import Table, Column

from .effects import Effect
from ..optics import image_plane_utils as imp_utils
from ..base_classes import FOVSetupBase
from ..base_classes import FOVSetupBase, FieldOfViewBase

from ..utils import (quantify, quantity_from_table, from_currsys, check_keys,
figure_factory, get_logger)
Expand Down Expand Up @@ -70,6 +70,10 @@ class ApertureMask(Effect):

Other Parameters
----------------
extend_fov_beyond_slit : float
[arcsec] Default 0. Increases the on-sky size of the FOV by this
amount in all directions when extracting flux from the Source object.

pixel_scale : float
[arcsec] Defaults to ``"!INST.pixel_scale"`` from the config

Expand All @@ -89,6 +93,7 @@ def __init__(self, **kwargs):

super().__init__(**kwargs)
params = {
"extend_fov_beyond_slit": 0,
"pixel_scale": "!INST.pixel_scale",
"no_mask": True,
"angle": 0,
Expand Down Expand Up @@ -118,9 +123,13 @@ def apply_to(self, obj, **kwargs):
u.arcsec).to(u.arcsec).value
y = quantity_from_table("y", self.table,
u.arcsec).to(u.arcsec).value
obj.shrink(["x", "y"], ([min(x), max(x)], [min(y), max(y)]))
dr = self.meta["extend_fov_beyond_slit"]
x_min, x_max = min(x) - dr, max(x) + dr
y_min, y_max = min(y) - dr, max(y) + dr
obj.shrink(["x", "y"], ([x_min, x_max], [y_min, y_max]))

# ..todo: HUGE HACK - Get rid of this!
# Assume the slit coord 'xi' is along the x axis
for vol in obj.volumes:
vol["meta"]["xi_min"] = min(x) * u.arcsec
vol["meta"]["xi_max"] = max(x) * u.arcsec
Expand Down Expand Up @@ -232,6 +241,25 @@ class ApertureList(Effect):

Examples
--------
Defining the apertures of an image slicer IFU::

- name: image_slicer
description: collection of slits corresponding to the image slicer apertures
class: ApertureList
kwargs:
filename: "INS_ifu_apertures.dat"
fov_for_each_aperture : True
extend_fov_beyond_slit : 2 # [arcsec]

Where the file ``INS_ifu_apertures.dat`` includes this table::

id left right bottom top angle conserve_image shape
0 -14 14 -5 -3 0 True rect
1 -14 14 -3 -1 0 True rect
2 -14 14 -1 1 0 True rect
3 -14 14 1 3 0 True rect
4 -14 14 3 5 0 True rect


File format
-----------
Expand Down Expand Up @@ -261,7 +289,7 @@ class ApertureList(Effect):
.. note:: ``shape`` values ``"rect"`` and ``4`` do not produce equal results

Both use 4 equidistant points around an ellipse constrained by
[``left``, ..., etc]. However ``"rect"`` aims to fill the contraining
[``left``, ..., etc]. However ``"rect"`` aims to fill the constraining
area, while ``4`` simply uses 4 points on the ellipse.
Consequently, ``4`` results in a diamond shaped mask covering only
half of the constraining area filled by ``"rect"``.
Expand All @@ -272,12 +300,14 @@ class ApertureList(Effect):
def __init__(self, **kwargs):
super().__init__(**kwargs)
params = {
"fov_for_each_aperture": True,
"extend_fov_beyond_slit": 0,
"pixel_scale": "!INST.pixel_scale",
"n_round_corners": 32, # number of corners use to estimate ellipse
"no_mask": False, # .. todo:: is this necessary when we have conserve_image?
"report_plot_include": True,
"report_table_include": True,
"report_table_rounding": 4,
"report_table_rounding": 4
}
self.meta["z_order"] = [81, 281]
self.meta.update(params)
Expand All @@ -291,23 +321,49 @@ def __init__(self, **kwargs):
def apply_to(self, obj, **kwargs):
"""See parent docstring."""
if isinstance(obj, FOVSetupBase):
new_vols = []
for row in self.table:
vols = obj.extract(["x", "y"], ([row["left"], row["right"]],
[row["bottom"], row["top"]]))
for vol in vols:
vol["meta"]["aperture_id"] = row["id"]

# ..todo: HUGE HACK - Get rid of this!
vol["meta"]["xi_min"] = row["left"] * u.arcsec
vol["meta"]["xi_max"] = row["right"] * u.arcsec
vol["conserve_image"] = row["conserve_image"]
vol["shape"] = row["shape"]
vol["angle"] = row["angle"]

new_vols += vols

obj.volumes = new_vols
dr = self.meta["extend_fov_beyond_slit"]
if self.meta["fov_for_each_aperture"] is True:
new_vols = []
for row in self.table:
x_min, x_max = row["left"] - dr, row["right"] + dr
y_min, y_max = row["bottom"] - dr, row["top"] + dr
vols = obj.extract(["x", "y"], ([x_min, x_max],
[y_min, y_max]))
for vol in vols:
vol["meta"]["aperture_id"] = row["id"]
vol["meta"]["extend_fov_beyond_slit"] = dr
vol["meta"]["sub_apertures"] = Table(rows=row)

# ..todo: HUGE HACK - Get rid of this!
# Assume the slit coord 'xi' is along the x axis
vol["meta"]["xi_min"] = row["left"] * u.arcsec
vol["meta"]["xi_max"] = row["right"] * u.arcsec
vol["conserve_image"] = row["conserve_image"]
vol["shape"] = row["shape"]
vol["angle"] = row["angle"]

new_vols += vols

obj.volumes = new_vols

else:
x_min = min(self.table["left"]) - dr
x_max = max(self.table["right"]) + dr
y_min = min(self.table["bottom"]) - dr
y_max = max(self.table["top"]) + dr

obj.shrink(["x", "y"], ([x_min, x_max], [y_min, y_max]))
vol = obj.volumes[0]
vol["meta"]["aperture_id"] = list(self.table["id"])
vol["meta"]["sub_apertures"] = self.table

# ..todo: HUGE HACK - Get rid of this!
# Assume the slit coord 'xi' is along the x axis
vol["meta"]["xi_min"] = x_min * u.arcsec
vol["meta"]["xi_max"] = x_max * u.arcsec
vol["conserve_image"] = True
vol["shape"] = "rect"
vol["angle"] = 0

return obj

Expand Down Expand Up @@ -373,8 +429,63 @@ def __add__(self, other):
raise ValueError("Secondary argument not of type ApertureList: "
f"{type(other) = }")

# def __getitem__(self, item):
# return self.get_apertures(item)[0]

class FibreApertureList(ApertureList):
"""
A subclass of ApertureList for fibre apertures

File format
-----------
Much like an ApertureList, a FibreApertureList can be initialised by either
of the three standard DataContainer methods.
The easiest is however to make an ASCII file with the following columns::

id x y r angle conserve_image shape
arcsec arcsec arcsec deg
int float float float float bool str/int

where:
- x,y : centres of the fibres
- r : radius (NOT diameter) of the fibre head.
- angle : rotation of the first vertex from x=0
- conserve_image : flag for maintaining spatial information of flux
- shape : string or int. See ApertureList for allowed strings

.. note:: FibreApertureList does not support Elliptical fibres.


"""
def __init__(self, **kwargs):
# Initialise with the super-super class, i.e. Effect, not ApertureList
super(ApertureList, self).__init__(**kwargs)
params = {"fov_for_each_aperture": True,
"extend_fov_beyond_slit": 0,
"pixel_scale": "!INST.pixel_scale",
"n_round_corners": 32, # number of corners use to estimate ellipse
"no_mask": False, # .. todo:: is this necessary when we have conserve_image?
"report_plot_include": True,
"report_table_include": True,
"report_table_rounding": 4}
self.meta["z_order"] = [81, 281]
self.meta.update(params)
self.meta.update(kwargs)

if self.table is not None:
required_keys = ["id", "x", "y", "r", "angle",
"conserve_image", "shape"]
check_keys(self.table.colnames, required_keys)

left = self.table["x"] - self.table["r"]
right = self.table["x"] + self.table["r"]
bottom = self.table["y"] - self.table["r"]
top = self.table["y"] + self.table["r"]
left.name = "left"
right.name = "right"
bottom.name = "bottom"
top.name = "top"
self.table.add_columns([left, right, bottom, top])




class SlitWheel(Effect):
Expand Down Expand Up @@ -486,6 +597,11 @@ def current_slit(self):
return False
return self.slits[currslit]

@property
def display_name(self):
return f'{self.meta["name"]} : ' \
f'[{from_currsys(self.meta["current_slit"])}]'

def __getattr__(self, item):
return getattr(self.current_slit, item)

Expand Down
94 changes: 93 additions & 1 deletion scopesim/effects/psfs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np
from scipy.signal import convolve
from scipy.interpolate import RectBivariateSpline
from scipy.interpolate import RectBivariateSpline, interp1d

from astropy import units as u
from astropy.io import fits
Expand Down Expand Up @@ -189,6 +189,98 @@ def get_kernel(self, obj):
return self.kernel.astype(float)


class RadialProfilePSF(AnalyticalPSF):
"""
Creates a wavelength independent kernel image
"""
def __init__(self, **kwargs):
params = {"unit": "pixel",
"pixel_scale": "!INST.pixel_scale",
"plate_scale": "!INST.plate_scale"}
params.update(kwargs)
super().__init__(**params)
self.meta["z_order"] = [244, 744]

self.convolution_classes = ImagePlaneBase

self.required_keys = ["r", "z", "unit"]
if self.meta["unit"] == "mm":
self.required_keys += ["pixel_scale", "plate_scale"]
check_keys(self.meta, self.required_keys, action="error")

self.kernel = None

def get_kernel(self, obj):
if self.kernel is None:
dr = 1
if self.meta["unit"] == "mm":
dr = from_currsys(self.meta["pixel_scale"], self.cmds) / \
from_currsys(self.meta["plate_scale"], self.cmds)

rpix = np.array(self.meta["r"]) / dr
z = np.array(self.meta["z"])
fn = interp1d(rpix, z, "linear", fill_value=0, bounds_error=False)
rmax = np.ceil(max(rpix))
xx, yy = np.mgrid[-rmax:rmax+1, -rmax:rmax+1]
rr = (xx**2 + yy**2)**0.5
kernel = fn(rr)
kernel[kernel<0] = 0
kernel /= np.sum(kernel)
self.kernel = kernel

return self.kernel.astype(float)


class MosaicBundleTracePSF(RadialProfilePSF):
"""
A hack to get the mosaic bundle traces onto the detector, assuming full ADC

Basically it creates a 5x5 RadialProfilePSF (defined by r and z) and then
copies and pastes the kernel side-by-side for each fibre in the mosaic
bundle. Each fibre PSF kernel can be weighted by the filling factor of the
PSF in said fibre.

The main problem here is that it doesn't take into account any wavelength
dependent effects on the PSF. This could be an issue down the line.

"""

def __init__(self, **kwargs):
params = {"unit": "pixel",
"pixel_scale": "!INST.pixel_scale",
"plate_scale": "!INST.plate_scale",
"r": [0, 1, 2], # distance from centre of trace in [unit]
"z": [0.6, 0.2, 0], # relative flux of PSF for values in "r"
"trace_spacing": 2, # [unit]
"trace_flux_weights": [0.7] + [0.05] * 6 # Number of traces taken from the length of this list
}
params.update(kwargs)
super().__init__(**params)
self.meta["z_order"] = [244, 744]

self.convolution_classes = ImagePlaneBase
self.kernel = None
self.single_kernel = None

def get_kernel(self, obj):
self.single_kernel = super().get_kernel(obj)
kernel_list = [self.single_kernel * weight
for weight in self.meta["trace_flux_weights"]]

dr = 1
if self.meta["unit"] == "mm":
dr = from_currsys(self.meta["pixel_scale"], self.cmds) / \
from_currsys(self.meta["plate_scale"], self.cmds)
pad_arr = np.zeros([self.single_kernel.shape[0],
int(dr * self.meta["trace_spacing"])])
pad_list = [pad_arr] * len(kernel_list)

zipped_list = [j for i in zip(kernel_list, pad_list) for j in i][:-1] # this zips together the psf and pad lists
self.kernel = np.concatenate(zipped_list, axis=1)

return self.kernel.astype(float)


class NonCommonPathAberration(AnalyticalPSF):
"""
TBA.
Expand Down
Loading