-
-
Notifications
You must be signed in to change notification settings - Fork 38
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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') | ||
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?) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:
cal1d.refined_lines
and modifying those inputs as necessary before callingcal1d(spec_targ)
.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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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:
Does that sound reasonable to you or is there something missing or in the wrong order for this workflow?
There was a problem hiding this comment.
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?