diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bebae91..f164c0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,12 +11,12 @@ repos: args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] exclude: ".*(.fits|.fts|.fit|.txt|tca.*|extern.*|.rst|.md|docs/conf.py)$" - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.1.3' + rev: 'v0.1.7' hooks: - id: ruff args: ['--fix', '--unsafe-fixes'] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black exclude: ".*(.fits|.fts|.fit|.txt|.csv)$" diff --git a/aiapy/calibrate/tests/test_uncertainty.py b/aiapy/calibrate/tests/test_uncertainty.py index 0643d21..ec5b616 100644 --- a/aiapy/calibrate/tests/test_uncertainty.py +++ b/aiapy/calibrate/tests/test_uncertainty.py @@ -1,4 +1,3 @@ -from pathlib import Path from contextlib import nullcontext import astropy.units as u @@ -68,49 +67,3 @@ def test_flags(include_preflight, include_eve, include_chianti, expectation): include_chianti=include_chianti, ) assert isinstance(errors, u.Quantity) - - -@pytest.mark.parametrize( - ("channel", "counts", "include_eve", "include_preflight", "include_chianti"), - [[c, 10 * u.ct / u.pixel] + 3 * [False] for c in CHANNELS] - + [ - [171 * u.angstrom, 1000 * u.ct / u.pix, True, False, False], - [171 * u.angstrom, 1000 * u.ct / u.pix, False, False, True], - ], -) -def test_error_consistent(idl_environment, channel, counts, include_eve, include_preflight, include_chianti): - idl = """ - common aia_bp_error_common,common_errtable - common_errtable=aia_bp_read_error_table('{{ error_table }}') - data = {{ data }} - channel = {{ channel }} - error=aia_bp_estimate_error(data,channel,n_sample=1{{ include_eve }}{{ include_preflight }}{{ include_chianti }}) - """ - error_table = Path(idl_environment.ssw_home) / "sdo" / "aia" / "response" / "aia_V3_error_table.txt" - ssw = idl_environment.run( - idl, - save_vars=["error"], - args={ - "channel": channel.to("angstrom").value, - "data": counts.to("ct pixel-1").value, - "error_table": error_table, - "include_eve": ",/evenorm" if include_eve else "", - # NOTE: use of this keyword is actually broken in SSW so these - # tests only set it to False until it works, but consistency with - # these results has been verified - "include_preflight": ",/cal" if include_preflight else "", - "include_chianti": ",/temperature" if include_chianti else "", - }, - verbose=False, - ) - error_ssw = ssw["error"] * counts.unit - error = estimate_error( - counts, - channel, - include_eve=include_eve, - include_preflight=include_preflight, - include_chianti=include_chianti, - error_table=error_table, - compare_idl=True, - ) - assert u.allclose(error, error_ssw, rtol=1e-4) diff --git a/aiapy/conftest.py b/aiapy/conftest.py index 492fa45..5a2d7f8 100644 --- a/aiapy/conftest.py +++ b/aiapy/conftest.py @@ -1,21 +1,15 @@ +import contextlib + import astropy.units as u import pytest import sunpy.data.test import sunpy.map # Force MPL to use non-gui backends for testing. -try: +with contextlib.suppress(ImportError): import matplotlib -except ImportError: - pass -else: - matplotlib.use("Agg") -# Do not require hissw for tests -try: - import hissw -except ImportError: - pass + matplotlib.use("Agg") @pytest.fixture() @@ -26,26 +20,5 @@ def aia_171_map(): @pytest.fixture(scope="session") -def idl_environment(): - if idl_available(): - return hissw.Environment(ssw_packages=["sdo/aia"], ssw_paths=["aia"]) - pytest.skip( - "A working IDL installation is not available. You will not be able to run portions of the test suite.", - ) - - -@pytest.fixture(scope="session") -def ssw_home(): - if idl_available(): - return hissw.Environment().ssw_home - return None - - -def idl_available(): - try: - import hissw - - hissw.Environment().run("") - return True - except Exception: # NOQA - return False +def channels(): + return [94, 131, 171, 193, 211, 304, 335] * u.angstrom diff --git a/aiapy/psf/tests/conftest.py b/aiapy/psf/tests/conftest.py index 1677ab7..4951334 100644 --- a/aiapy/psf/tests/conftest.py +++ b/aiapy/psf/tests/conftest.py @@ -1,17 +1,11 @@ """ Shared fixtures for PSF tests. """ -import astropy.units as u import pytest import aiapy.psf -@pytest.fixture(scope="module") -def channels(): - return [94, 131, 171, 193, 211, 304, 335] * u.angstrom - - @pytest.fixture() def psf(channels): return aiapy.psf.psf(channels[0], use_preflightcore=True, diffraction_orders=[-1, 0, 1]) diff --git a/aiapy/psf/tests/test_psf.py b/aiapy/psf/tests/test_psf.py index 5c648ec..de3b0dc 100644 --- a/aiapy/psf/tests/test_psf.py +++ b/aiapy/psf/tests/test_psf.py @@ -14,25 +14,6 @@ ] -@pytest.fixture() -def psf_full(channels): - return aiapy.psf.psf(channels[0], use_preflightcore=True) - - -@pytest.fixture(scope="module") -def psf_idl(idl_environment, channels): - """ - The point spread function as calculated by aia_calc_psf.pro. - """ - r = idl_environment.run( - "psf = aia_calc_psf({{channel}},/use_preflightcore)", - args={"channel": f"{channels[0].value:.0f}"}, - save_vars=["psf"], - verbose=False, - ) - return r["psf"] - - @pytest.mark.parametrize("use_preflightcore", [True, False]) def test_filter_mesh_parameters(use_preflightcore, channels): params = aiapy.psf.filter_mesh_parameters(use_preflightcore=use_preflightcore) @@ -44,18 +25,3 @@ def test_filter_mesh_parameters(use_preflightcore, channels): def test_psf(psf): assert isinstance(psf, np.ndarray) assert psf.shape == (4096, 4096) - - -def test_psf_consistent(psf_full, psf_idl): - """ - Check whether PSF is consistent with IDL calculation. - - .. note:: This test will take a very long time to run. - """ - # NOTE: The IDL and Python PSF functions have been found to - # agree within 0.2% for all points along the PSF arms for - # both the preflight and non-preflight cases. - # NOTE: Only compare values above some threshold as the - # rest of the PSF is essentially noise - i_valid = np.where(psf_idl > 1e-10) - assert np.allclose(psf_full[i_valid], psf_idl[i_valid], atol=0.0, rtol=2e-3) diff --git a/aiapy/tests/test_idl.py b/aiapy/tests/test_idl.py new file mode 100644 index 0000000..d294ec4 --- /dev/null +++ b/aiapy/tests/test_idl.py @@ -0,0 +1,125 @@ +""" +Contains all the the IDL specific tests for aiapy. +""" +from pathlib import Path + +import astropy.units as u +import numpy as np +import pytest +from sunpy import log + +import aiapy.psf +from aiapy.calibrate import estimate_error + +CHANNELS = [94, 131, 171, 193, 211, 304, 335, 1600, 1700, 4500] * u.angstrom + + +def idl_available(): + try: + import hissw + + hissw.Environment().run("") + return True + except Exception as e: # NOQA + log.waring(e) + return False + + +@pytest.fixture(scope="session") +def idl_environment(): + if idl_available(): + import hissw + + return hissw.Environment( + ssw_packages=["sdo/aia"], + ssw_paths=["aia"], + ) + pytest.skip( + "A working IDL installation is not available. You will not be able to run portions of the test suite.", + ) + + +@pytest.fixture(scope="session") +def ssw_home(idl_environment): + return idl_environment.ssw_home if idl_available() else None + + +@pytest.mark.parametrize( + ("channel", "counts", "include_eve", "include_preflight", "include_chianti"), + [[c, 10 * u.ct / u.pixel] + 3 * [False] for c in CHANNELS] + + [ + [171 * u.angstrom, 1000 * u.ct / u.pix, True, False, False], + [171 * u.angstrom, 1000 * u.ct / u.pix, False, False, True], + ], +) +def test_error_consistent(idl_environment, channel, counts, include_eve, include_preflight, include_chianti): + idl = """ + common aia_bp_error_common,common_errtable + common_errtable=aia_bp_read_error_table('{{ error_table }}') + data = {{ data }} + channel = {{ channel }} + error=aia_bp_estimate_error(data,channel,n_sample=1{{ include_eve }}{{ include_preflight }}{{ include_chianti }}) + """ + error_table = Path(idl_environment.ssw_home) / "sdo" / "aia" / "response" / "aia_V3_error_table.txt" + ssw = idl_environment.run( + idl, + save_vars=["error"], + args={ + "channel": channel.to("angstrom").value, + "data": counts.to("ct pixel-1").value, + "error_table": error_table, + "include_eve": ",/evenorm" if include_eve else "", + # NOTE: use of this keyword is actually broken in SSW so these + # tests only set it to False until it works, but consistency with + # these results has been verified + "include_preflight": ",/cal" if include_preflight else "", + "include_chianti": ",/temperature" if include_chianti else "", + }, + verbose=False, + ) + error_ssw = ssw["error"] * counts.unit + error = estimate_error( + counts, + channel, + include_eve=include_eve, + include_preflight=include_preflight, + include_chianti=include_chianti, + error_table=error_table, + compare_idl=True, + ) + assert u.allclose(error, error_ssw, rtol=1e-4) + + +@pytest.fixture(scope="session") +def psf_full(channels): + return aiapy.psf.psf(channels[0], use_preflightcore=True) + + +@pytest.fixture(scope="module") +@pytest.mark.parametrize("channel", CHANNELS) +def psf_idl(idl_environment, channels): + """ + The point spread function as calculated by aia_calc_psf.pro. + """ + r = idl_environment.run( + "psf = aia_calc_psf({{channel}},/use_preflightcore)", + args={"channel": f"{channels[0].value:.0f}"}, + save_vars=["psf"], + verbose=False, + ) + return r["psf"] + + +def test_psf_consistent(psf_full, psf_idl): + """ + Check whether PSF is consistent with IDL calculation. + + .. note:: This test will take a very long time to run. + """ + # NOTE: The IDL and Python PSF functions have been found to + # agree within 0.2% for all points along the PSF arms for + # both the preflight and non-preflight cases. + # NOTE: Only compare values above some threshold as the + # rest of the PSF is essentially noise + i_valid = np.where(psf_idl > 1e-10) + assert np.allclose(psf_full[i_valid], psf_idl[i_valid], atol=0.0, rtol=2e-3)