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

Perform corrections to Line Analysis Flux Results #1564

Merged
merged 46 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
88cfc86
Perform corrections to Line Analysis for JWST
duytnguyendtn Aug 12, 2022
e74c0e4
Codestyle
duytnguyendtn Aug 12, 2022
5a3cad4
Changelog
duytnguyendtn Aug 12, 2022
ab3063a
Fix typo
duytnguyendtn Aug 12, 2022
0438506
Fix typo
duytnguyendtn Aug 12, 2022
a118e86
Multiply by PIXAR_SR if conditions are met
duytnguyendtn Aug 15, 2022
cac64a6
Codestyle
duytnguyendtn Aug 15, 2022
54b98c4
Codestyle
duytnguyendtn Aug 15, 2022
982a377
Update CHANGES.rst
duytnguyendtn Aug 15, 2022
e762634
Prefer FITS TELESCOP over ASDF telescope
duytnguyendtn Aug 15, 2022
a71301c
Add docs
duytnguyendtn Aug 15, 2022
f44aa0b
Add primary header fallback, explicitly define supported collapse fun…
duytnguyendtn Aug 15, 2022
31d2f7b
Add jwst flux unit tests
duytnguyendtn Aug 16, 2022
549299f
Codestyle
duytnguyendtn Aug 16, 2022
9301ec4
Fix Data labelling bug
duytnguyendtn Aug 16, 2022
cae45bf
Fix test formatting
duytnguyendtn Aug 16, 2022
d6d5e85
Enforce spectral equivalency
duytnguyendtn Aug 16, 2022
316af5c
Fix typo
duytnguyendtn Aug 16, 2022
ffed328
PIXAR_SR should check meta first
duytnguyendtn Aug 16, 2022
972a1f4
Force _primary_header in testing if it doesn't exist
duytnguyendtn Aug 16, 2022
1c32f30
Remove PIXAR_SR correction from plugin
duytnguyendtn Aug 17, 2022
ac16fd2
Fix assertion to hardcoded flux unit
duytnguyendtn Aug 17, 2022
23a9f7e
Remove jwst keyword requirement
duytnguyendtn Aug 18, 2022
4a7cf97
Codestyle
duytnguyendtn Aug 18, 2022
320f91a
Update line flux test truth values
duytnguyendtn Aug 18, 2022
61debce
Implement new w/m2 algorithm
duytnguyendtn Aug 31, 2022
fda9b00
Unit is not callable
duytnguyendtn Sep 1, 2022
1aeca73
Replace cube fluxunit test fixtures with cube generator
duytnguyendtn Sep 1, 2022
a4d8ea7
First pass tests
duytnguyendtn Sep 1, 2022
fbbffa5
Filter FITS unit warning
duytnguyendtn Sep 1, 2022
1e7121a
Fix regex filter
duytnguyendtn Sep 1, 2022
f6a2529
Move changelog to 2.11
duytnguyendtn Sep 1, 2022
99fb4fc
Raw string for ignore message
duytnguyendtn Sep 1, 2022
c2e1e49
Remove warning filter
duytnguyendtn Sep 1, 2022
9d34a52
Specify ignore action for warnings filter
duytnguyendtn Sep 1, 2022
7ac6af8
Filter warning for entire test module
duytnguyendtn Sep 1, 2022
9380d31
Fix cube custom unit generator
duytnguyendtn Sep 1, 2022
01f60bc
Adjust Data Label lookup
duytnguyendtn Sep 1, 2022
eee47e0
Rename lineflux test module
duytnguyendtn Sep 2, 2022
ebbe983
Add Area 1 Gaussian Test
duytnguyendtn Sep 2, 2022
c360db4
Update Changelog
duytnguyendtn Sep 2, 2022
f9c513c
Combine assertion checks with astropy quantity check
duytnguyendtn Sep 2, 2022
31ab2ee
Add additional science validation tests
duytnguyendtn Sep 6, 2022
5631edc
Code review comments: Formatting and Wording
duytnguyendtn Sep 7, 2022
f9589a6
Carry along uncertainties
duytnguyendtn Sep 7, 2022
1557d16
Manually convert uncertainty
duytnguyendtn Sep 7, 2022
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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ New Features

- Profile viewers now support plotting with profiles "as steps". [#1595]

- Line flux in the Line Analysis plugin are reported in W/m2 if Spectral Flux is given
in Jy [#1564]

Cubeviz
^^^^^^^

Expand Down
2 changes: 2 additions & 0 deletions docs/specviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ secondary region can be created and selected as the region to fit the linear con
The statistics returned include the line centroid, gaussian sigma width, gaussian FWHM,
total flux, and equivalent width.

The line flux results are automatically converted to Watts/meter^2, when appropriate.

Redshift from Centroid
----------------------

Expand Down
53 changes: 51 additions & 2 deletions jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from glue_jupyter.common.toolbar_vuetify import read_icon
from traitlets import Bool, List, Float, Unicode, observe
from astropy import units as u
from specutils import analysis
from specutils import analysis, Spectrum1D
from specutils.manipulation import extract_region

from jdaviz.core.custom_traitlets import FloatHandleEmpty
Expand Down Expand Up @@ -332,7 +332,56 @@ def _uncertainty(result):
for function in FUNCTIONS:
# TODO: update specutils to allow ALL analysis to take regions and continuum so we
# don't need these if statements
if function == "Equivalent Width":
if function == "Line Flux":
flux_unit = spec_subtracted.flux.unit
# If the flux unit is equivalent to Jy, or Jy per spaxel for Cubeviz,
# enforce integration in frequency space
if (flux_unit.is_equivalent(u.Jy) or
flux_unit.is_equivalent(u.Jy/u.sr)):
# Perform integration in frequency space
freq_spec = Spectrum1D(
spectral_axis=spec_subtracted.spectral_axis.to(u.Hz,
equivalencies=u.spectral()),
flux=spec_subtracted.flux,
uncertainty=spec_subtracted.uncertainty)

raw_result = analysis.line_flux(freq_spec)
# When flux is equivalent to Jy, lineflux result should be shown in W/m2
if flux_unit.is_equivalent(u.Jy/u.sr):
final_unit = u.Unit('W/m2/sr')
else:
final_unit = u.Unit('W/m2')

temp_result = raw_result.to(final_unit)
if getattr(raw_result, 'uncertainty', None) is not None:
temp_result.uncertainty = raw_result.uncertainty.to(final_unit)

# If the flux unit is instead equivalent to power density
# (Jy, but defined in wavelength), enforce integration in wavelength space
elif (flux_unit.is_equivalent(u.Unit('W/m2/m')) or
flux_unit.is_equivalent(u.Unit('W/m2/m/sr'))):
# Perform integration in wavelength space using MKS unit (meters)
wave_spec = Spectrum1D(
spectral_axis=spec_subtracted.spectral_axis.to(u.m,
equivalencies=u.spectral()),
flux=spec_subtracted.flux,
uncertainty=spec_subtracted.uncertainty)
raw_result = analysis.line_flux(wave_spec)
# When flux is equivalent to Jy, lineflux result should be shown in W/m2
if flux_unit.is_equivalent(u.Unit('W/m2/m'/u.sr)):
final_unit = u.Unit('W/m2/sr')
else:
final_unit = u.Unit('W/m2')

temp_result = raw_result.to(final_unit)
if getattr(raw_result, 'uncertainty', None) is not None:
temp_result.uncertainty = raw_result.uncertainty.to(final_unit)
Comment on lines +376 to +378
Copy link
Member

Choose a reason for hiding this comment

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

hopefully this will be improved upstream some day... but does the trick for now!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I created astropy/specutils#980 in specutils to make sure we don't forget to discuss

Copy link
Member

Choose a reason for hiding this comment

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

my comment on these lines was referring to the fact that .uncertainty (or any arbitrary attribute added to a quantity) isn't supported by astropy and so is stripped from the quantity object when calling .to (see astropy/specutils#961 for some related discussion).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ahh right. Sorry my comment was meant as a direct response to your general review. Sorry, poor placement


# Otherwise, just rely on the default specutils line_flux result
else:
temp_result = analysis.line_flux(spec_subtracted)

elif function == "Equivalent Width":
if np.any(continuum <= 0):
temp_results.append({'function': function,
'result': '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def test_continuum_surrounding_spectral_subset(specviz_helper, spectrum1d):
plugin.width = 3

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), 350.89288537581467, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), 2.153181e-13, atol=1e-15)
Comment on lines 138 to +139
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because we're doing line flux unit conversion to all data, not just those from JWST, it has impacted these tests here. Because these values are marked as "not validated" as per the comment, I just copied over the results from the test and replaced them here, since it is suggested to me that the values don't actually matter. But I don't know if there is a better approach to this. If I know someone who will call me out and tell me there's a better way, it's @pllim !

Copy link
Contributor

Choose a reason for hiding this comment

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

This is where science validation becomes important. Have the POs also test this plugin with some non-JWST data and see if results are correct.

On our side:

  1. Does it make sense to convert unit for arbitrary data that we do not fully understand?
  2. We need to add some meaningful tests that actually preserve the scientific integrity of this plugin. Like it or not, someone is going to try to copy paste the value onto their papers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, do you understand this change? What was the unit before, what is the unit now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is definitely a PR that requires PO review, so I agree for the first, and last, point. Though I wonder whether a full scientific integrity test suite should be a blocker for this PR or not?

As for your other point, the main solace I can provide for the "arbitrary data" is that the conversion only happens when the units are specifically in the right bases. Could a user theoretically provide some other data that happens to have "the same unit" but mean something different? I don't know

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder whether a full scientific integrity test suite should be a blocker for this PR or not?

Well, if we want full QA, this is never going to go in. So I would say no, but we first need to understand why our existing test results have changed and is the change correct.

Copy link
Contributor

Choose a reason for hiding this comment

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

@pllim (1) Regarding arbitrary units, these shouldn't be touched by this PR. Specutils should know how to handle the units commonly used by astronomers: Jy, erg/s/cm^2/A, etc..., regardless of what instrument/telescope is used.
(2) The easiest meaningful test is a Gaussian with flux = 1 W/m^2. Not necessary to test with real data if this gives the right answer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here are the only two cases for spectra with physical units:
(1) energy/time/area/wavelength[/solid_angle] vs. wavelength or frequency---
integrate over wavelength, convert to W/m^2[/sr]
(2) energy/time/area/frequency[/solid_angle] vs. wavelength or frequency---
integrate over frequency, convert to W/m^2[/sr]



def test_continuum_spectral_same_value(specviz_helper, spectrum1d):
Expand Down Expand Up @@ -215,7 +215,7 @@ def test_continuum_subset_spectral_entire(specviz_helper, spectrum1d):
plugin.width = 3

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), -467.6854635447396, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), -2.79572e-13, atol=1e-15)


def test_continuum_subset_spectral_subset2(specviz_helper, spectrum1d):
Expand Down Expand Up @@ -248,7 +248,7 @@ def test_continuum_subset_spectral_subset2(specviz_helper, spectrum1d):
plugin.width = 3

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), 32.520521, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), 1.482418e-14, atol=1e-16)


def test_continuum_surrounding_no_right(specviz_helper, spectrum1d):
Expand Down Expand Up @@ -276,7 +276,7 @@ def test_continuum_surrounding_no_right(specviz_helper, spectrum1d):
plugin.width = 3

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), 39.76685499263615, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), 4.204513e-14, atol=1e-16)


def test_continuum_surrounding_no_left(specviz_helper, spectrum1d):
Expand Down Expand Up @@ -304,7 +304,7 @@ def test_continuum_surrounding_no_left(specviz_helper, spectrum1d):
plugin.width = 3

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), 146.67186446784513, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), 7.570859e-14, atol=1e-16)


def test_subset_changed(specviz_helper, spectrum1d):
Expand Down Expand Up @@ -335,4 +335,4 @@ def test_subset_changed(specviz_helper, spectrum1d):
specviz_helper.app.state.drawer = True

# Values have not yet been validated
np.testing.assert_allclose(float(plugin.results[0]['result']), 350.89288537581467, atol=1e-5)
np.testing.assert_allclose(float(plugin.results[0]['result']), 2.153181e-13, atol=1e-15)
123 changes: 123 additions & 0 deletions jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from astropy.tests.helper import assert_quantity_allclose
import astropy.units as u
from glue.viewers.profile.state import FUNCTIONS as COLLAPSE_FUNCTIONS
import numpy as np
import pytest
from specutils import Spectrum1D

from jdaviz import Cubeviz

import warnings
warnings.filterwarnings('ignore')

# Maps input spectrum flux unit to expected line analysis flux unit
expected_lineflux_results = {
u.Jy/u.sr: u.Unit('W/(m2*sr)'),
u.Jy: u.Unit('W/m2'),
u.Unit('W/m2/m')/u.sr: u.Unit('W/(m2*sr)'),
u.Unit('W/m2/m'): u.Unit('W/m2')
}

test_cases = list(expected_lineflux_results.keys())


# Test cases for unit gaussian tests
def _gauss_with_unity_area(x, mean, sigma):
''' Gaussian function with area = 1 unit '''
dx = x - mean
n = 1/(sigma*np.sqrt(2*np.pi))
g = n * np.exp(-dx**2/(2*sigma**2))
return g


mn = 1.5
sig = 0.01

# 5 cases: replace fl_wave with fnu_freq, fnu_wave, flam_wave, flam_freq, or fl_wave
# to try the different cases here
unit_flux_gaussian_test_cases = []

# unit-flux gaussian in frequency space
freq = np.arange(1, 2, 0.001)*u.Hz
flux_freq = _gauss_with_unity_area(freq.value, mn, sig)*1.0E26*u.Jy
fnu_freq = Spectrum1D(spectral_axis=freq, flux=flux_freq)
unit_flux_gaussian_test_cases.append(fnu_freq)
fnu_wave = Spectrum1D(spectral_axis=fnu_freq.wavelength, flux=flux_freq)
unit_flux_gaussian_test_cases.append(fnu_wave)

# unit-flux gaussian in wavelength space
lam = np.arange(1, 2, 0.001)*u.m
flux_wave = _gauss_with_unity_area(lam.value, mn, sig)*1.0*u.W/u.m**2/u.m
flam_wave = Spectrum1D(spectral_axis=lam, flux=flux_wave)
unit_flux_gaussian_test_cases.append(flam_wave)
flam_freq = Spectrum1D(spectral_axis=flam_wave.frequency, flux=flux_wave)
unit_flux_gaussian_test_cases.append(flam_freq)


def _calculate_line_flux(viz_helper):
'''
Returns the line flux calculation by opening the Line Analysis Plugin
Assumes the plugin hasn't been opened yet
'''
# Open the plugin and force the calculation
viz_helper.app.state.drawer = True
line_analysis_plugin = viz_helper.app.get_tray_item_from_name('specviz-line-analysis')
line_analysis_plugin.open_in_tray()
# Retrieve Results
for result in line_analysis_plugin.results:
if result['function'] == 'Line Flux':
return result


@pytest.mark.filterwarnings('ignore')
@pytest.mark.parametrize('spectra_fluxunit', test_cases)
def test_cubeviz_collapse_fluxunits(spectrum1d_cube_custom_fluxunit, spectra_fluxunit):
''' Calculates line flux and checks the units for each collapse function '''
data = spectrum1d_cube_custom_fluxunit(spectra_fluxunit)
for function in COLLAPSE_FUNCTIONS:
# Initialize Cubeviz with specific data and collapse function
cubeviz_helper = Cubeviz()
data_label = "Test Cube"
cubeviz_helper.load_data(data, data_label=data_label)
cubeviz_helper.app.get_viewer('spectrum-viewer').state.function = function

lineflux_result = _calculate_line_flux(cubeviz_helper)

autocollapsed_spectrum_unit = cubeviz_helper.specviz.get_spectra()[data_label + "[FLUX]"
].flux.unit
# Futureproofing: Eventually Cubeviz autocollapse will change the flux units of the
# spectra depending on whether the spectrum was collapsed OVER SPAXELS or not. Only
# do the assertion check if we KNOW what the expected lineflux results should be
if autocollapsed_spectrum_unit in expected_lineflux_results.keys():
assert u.Unit(lineflux_result['unit']) == expected_lineflux_results[spectra_fluxunit]


@pytest.mark.filterwarnings('ignore')
@pytest.mark.parametrize('test_case', unit_flux_gaussian_test_cases)
def test_unit_gaussian(specviz_helper, test_case):
'''
Test an Area 1 Gaussian and ensure the result returns in W/m2
Test provided by Patrick Ogle
'''
specviz_helper.load_data(test_case)

lineflux_result = _calculate_line_flux(specviz_helper)
assert_quantity_allclose(float(lineflux_result['result']) * u.Unit(lineflux_result['unit']),
1*u.Unit('W/m2'))


@pytest.mark.filterwarnings('ignore')
def test_unit_gaussian_mixed_units_per_steradian(specviz_helper):
'''
A special unit test of Area 1 with mixed units. Should return W/m2sr
Test provided by Patrick Ogle
'''
# unit-flux gaussian in wavelength space, mixed units, per steradian
lam_a = np.arange(1, 2, 0.001)*u.Angstrom
flx_wave = _gauss_with_unity_area(lam_a.value, mn, sig)*1E3*u.erg/u.s/u.cm**2/u.Angstrom/u.sr
fl_wave = Spectrum1D(spectral_axis=lam_a, flux=flx_wave)

specviz_helper.load_data(fl_wave)
lineflux_result = _calculate_line_flux(specviz_helper)
assert_quantity_allclose(float(lineflux_result['result']) * u.Unit(lineflux_result['unit']),
1*u.Unit('W/(m2sr)'))
16 changes: 13 additions & 3 deletions jdaviz/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ def spectrum_collection(spectrum1d):
return result


@pytest.fixture
def spectrum1d_cube():
flux = np.arange(16).reshape((2, 2, 4)) * u.Jy
def _create_spectrum1d_cube_with_fluxunit(fluxunit=u.Jy):

flux = np.arange(16).reshape((2, 2, 4)) * fluxunit
wcs_dict = {"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CTYPE3": "WAVE-LOG",
"CRVAL1": 205, "CRVAL2": 27, "CRVAL3": 4.622e-7,
"CDELT1": -0.0001, "CDELT2": 0.0001, "CDELT3": 8e-11,
Expand All @@ -168,6 +168,16 @@ def spectrum1d_cube():
return Spectrum1D(flux=flux, wcs=w)


@pytest.fixture
def spectrum1d_cube():
return _create_spectrum1d_cube_with_fluxunit(u.Jy)


@pytest.fixture
def spectrum1d_cube_custom_fluxunit():
return _create_spectrum1d_cube_with_fluxunit


@pytest.fixture
def mos_spectrum1d():
'''
Expand Down