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

Concept: pseudocode proposal for wavelength calibration infrastructure #152

Closed
wants to merge 1 commit into from
Closed
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
119 changes: 119 additions & 0 deletions notebook_sandbox/wavelength_calibration_proposal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
class CalibrationLine(fit_to_spectrum, wavelength, pixel,
refinement_method=None, refinement_kwargs={}):
# fit_to_spectrum must be 1D spectrum1d object
# refinement_method == None: do NOT refine, use actual values as anchors, no kwargs
# refinement_method == 'uphill'/'downhill', go up/downhill from estimated pixel, no kwargs
# refinement_method == 'min'/'max', find min/max within pixel range provided by range kwarg
# refinement_method == 'gaussian', fit gaussian to spectrum within pixel range provided bye range kwarg

@classmethod
def by_name(cls, fit_to_spectrum, line_name, pixel,
refinement_method=None, refinement_kwargs={}):
# this does wavelength lookup and passes that wavelength to __init__ allowing the user
# to call CalibrationLine.by_name(spectrum, 'H_alpha', pixel=40)
# We could also have a type-check for ``wavelength`` above, this just feels more verbose
return cls(...)

def refine(self, fit_to_spectrum=None, return_object=False):
# finds the center of the line according to refinement_method/kwargs and returns
# either the pixel value or a CalibrationLine object with pixel changed to the refined value
fit_to_spectrum = self.fit_to_spectrum if fit_to_spectrum is None else fit_to_spectrum
refined_pixel = do_refinement(fit_to_spectrum, self.refinement_method, **self.refinement_kwargs)
if return_object:
return CalibrationLine(fit_to_spectrum, self.wavelength, refined_pixel, ...)
return refined_pixel

@cached_property
def refined_pixel(self):
# returns the refined pixel for self.fit_to_spectrum
return self.refine()

@property
def with_refined_pixel(self):
# returns a copy of this object, but with the pixel updated to the refined value
return self.refine(return_object=True)


class WavelengthCalibration1D(fit_to_spectrum, lines, model=Linear1D,
default_refinement_method=None, default_refinement_kwargs={}):
# fit_to_spectrum must be 1-dimensional
# lines are coerced to CalibrationLine objects if passed as tuples with default_refinement_method and default_refinement_kwargs as defaults
@classmethod
def autoidentify(cls, fit_to_spectrum, line_list, model=Linear1D, ...):
# line_list could be a string ("common stellar") or an object
# this builds a list of CalibrationLine objects and passes to __init__
return cls(...)

@property
def refined_lines(self):
return [l.with_refined_pixel for l in self.lines]

@property
def refined_pixels(self):
# useful for plotting over spectrum to change input lines/refinement options
return [(l.wavelength, l.refined_pixel) for l in self.lines]

@cached_property
def wcs(self):
# computes and returns WCS after fitting self.model to self.refined_pixels
return WCS(...)

def __call__(self, apply_to_spectrum=None):
# returns spectrum1d with wavelength calibration applied
# actual line refinement and WCS solution should already be done so that this can be called on multiple science sources
apply_to_spectrum = self.fit_to_spectrum if apply_to_spectrum is None else apply_to_spectrum
apply_to_spectrum.wcs = apply_to_spectrum # might need a deepcopy!
return apply_to_spectrum


class WavelengthCalibration2D(fit_to_spectrum, trace, lines, model=Linear1D,
default_refinement_method=None, default_refinement_kwargs={}):
# fit_to_spectrum must be 2-dimensional
# lines are coerced to CalibrationLine objects if passed as tuples with default_refinement_method and default_refinement_kwargs as defaults
@classmethod
def autoidentify(cls, fit_to_spectrum, trace, line_list, model=Linear1D, ...):
# does this do 2D identification, or just search a 1d spectrum exactly at the trace?
# or should we require using WavelengthCalibration1D.autoidentify?
return cls(...)

@property
def refined_lines(self):
return [[(l.wavelength, l.refine(fit_to_spectrum.get_row(row), return_object=False)) for l in self.lines] for row in rows]

@property
def refined_pixels(self):
return [[(l.wavelength, l.refine(fit_to_spectrum.get_row(row), return_object=True)) for l in self.lines] for row in rows]

@cached_property
def wcs(self):
# computes and returns WCS after fitting self.model to self.refined_pixels
return WCS(...)

def __call__(self, apply_to_spectrum=None):
# returns spectrum1d with wavelength calibration applied
# actual line refinement and WCS solution should already be done so that this can be called on multiple science sources
apply_to_spectrum = self.fit_to_spectrum if apply_to_spectrum is None else apply_to_spectrum
apply_to_spectrum.wcs = apply_to_spectrum # might need a deepcopy!
return apply_to_spectrum



# various supported syntaxes for creating a calibration object (do we want this much flexibility?):
cal1d = WavelengthCalibration1D(spec1d_lamp, (CalibrationLine(5500, 20), CalibrationLine(6200, 32)))
cal1d = WavelengthCalibration1D(spec1d_lamp, (CalibrationLine.by_name('H_alpha', 20, 'uphill'), CalibrationLine.by_name('Lyman_alpha', 32, 'max', {'range': 10})))
cal1d = WavelengthCalibration1D(spec1d_lamp, ((5500, 20), ('Lyman_alpha', 32)))
cal1d = WavelengthCalibration1D(spec1d_lamp, ((5500, 20), (6000, 30)), default_refinement_method='gaussian')
Comment on lines +102 to +105
Copy link
Member Author

Choose a reason for hiding this comment

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

Do we want to support per-line or global refinement options, or both?

Choose a reason for hiding this comment

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

In my mind, the user would do a first pass automatically and then refine the lines that need refinement. I wonder if the refinement to the line centroid should happen earlier and not while the wavelength solution is calculated. To follow Tim's process (and if I understand this code correctly), this represents step 3 (fit model), while the centroid refinement happens in step 2 (identify).

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right, this proposal merges Tim's steps 2 and 3 into a single call, but does allow the user to inspect the results from step 2 and modify the inputs. My reasoning for having these as a single class is:

  1. I don't expect the wavelength solution to be expensive at all, so having it computed and then modifying the inputs shouldn't be a computational "burden". And although both steps are covered in the same class, the user technically can defer step 3 by asking for cal1d.refined_lines and modifying those inputs as necessary before calling cal1d(spec_targ).
  2. The 2D case (reidentify, "step 4") requires information about how to refine the lines at any given "row", and I think it's elegant to have similar inputs to the 1D and 2D cases, so I like having refinement information passed to the 1D case as well. Alternatively, we could split the 2D case into two steps: the first that just refines the positions across all the rows and returns those positions and the second that then fits a wavelength solution.

In other words, I would suggest we either merge steps 2 & 3 from Tim's summary below or also split step 4 into "reidentify lines across rows" and "fit 2D model".

Copy link
Contributor

Choose a reason for hiding this comment

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

In my mind, the user would do a first pass automatically and then refine the lines that need refinement. I wonder if the refinement to the line centroid should happen earlier and not while the wavelength solution is calculated. To follow Tim's process (and if I understand this code correctly), this represents step 3 (fit model), while the centroid refinement happens in step 2 (identify).

this is correct. my step 2 has a step 2a: find lines and centroid them and step 2b: match lines to lines with known wavelengths. centroids shouldn't need to be tweaked. there will be iteration between steps 2b and 3 to find new matches as the wavelength solution is refined and then refining the solution further with more matched lines.

Copy link
Member Author

Choose a reason for hiding this comment

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

@tepickering - I think the steps you state as "match lines to lines with known wavelengths. centroids shouldn't need to be tweaked. there will be iteration between steps 2b and 3 to find new matches as the wavelength solution is refined and then refining the solution further with more matched lines." would fall within this proposal's autoidentify classmethod. That is imagined to take a line list instead of a manual list of wavelength-pixel pairs, find the lines in the spectrum, match to the lines in the list, and create that list of wavelength-pixel pairs automatically. At that point they can still be refined when passing on to the 2D step.

So modifying the original example:

cal1d = WavelengthCalibration1D.autoidentify(spec1d_lamp, common_stellar_line_list)
cal1d.lines  # list of wavelength-pixel pairs determined automatically by identifying and matching lines between the observations and the line-list.  User can plot, inspect, modify, etc.

cal2d = WavelengthCalibration2D(spec2d_lamp, trace_lamp, cal1d.lines)
calibrated_spec2d = cal2d(spec2d_source)

Does that sound reasonable to you or is there something missing or in the wrong order for this workflow?

Copy link
Member Author

Choose a reason for hiding this comment

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

@tepickering - just wanted to revist this... do you think the modification about would accomplish your use-case? Or if not, do you have suggestions for how to alter the API in order to support your steps better?

cal1d = WavelengthCalibration1D.autoidentify(spec1d_lamp, common_stellar_line_list)

# once we have that object, we can access the fitted wavelength solution via the WCS and apply it to
# a spectrum (either the same or separate from the spectrum used for fitting)
targ_wcs = cal1d.wcs
spec1d_targ1_cal = cal1d(spec1d_targ1)
spec1d_targ2_cal = cal1d(spec1d_targ2)


# we sould either start a 2D calibration the same way (except also passing the trace) or by passing
# the refined lines from the 1D calibration as the starting point
cal2d = WavelengthCalibration2D(spec2d_lamp, trace_lamp, ((5500, 20), (6000, 30)))
cal2d = WavelengthCalibration2D(spec2d_lamp, trace_lamp, cal1d.refined_lines)
spec2d_targ_cal = cal2d(spec2d_targ, trace_targ?)
Copy link
Member Author

Choose a reason for hiding this comment

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

Does 2D calibration need to support different trace for lamp and source? If so, how would that work?

Copy link
Member

@eteq eteq Dec 2, 2022

Choose a reason for hiding this comment

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

I think the answer is "no" here in one 2D case, but see the broader comment I'm about to leave for a bit more nuance
in the other case. ("no" means "what you have here is fine")