diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 699837c9b4f..8586743168e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ on: env: CACHE_NUMBER: 0 # increase to reset cache manually - PYTEST_FLAGS: --tardis-refdata=${{ github.workspace }}/tardis-refdata --tardis-snapshot-data=${{ github.workspace }}/tardis-regressions + PYTEST_FLAGS: --tardis-refdata=${{ github.workspace }}/tardis-refdata --tardis-regression-data=${{ github.workspace }}/tardis-regression-data --cov=tardis --cov-report=xml --cov-report=html CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -62,7 +62,7 @@ jobs: - name: Restore LFS cache uses: actions/cache/restore@v3 - id: lfs-cache + id: lfs-cache-refdata with: path: tardis-refdata/.git/lfs key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 @@ -70,28 +70,60 @@ jobs: - name: Git LFS Pull run: git lfs pull working-directory: tardis-refdata - if: steps.lfs-cache.outputs.cache-hit != 'true' + if: steps.lfs-cache-refdata.outputs.cache-hit != 'true' - name: Git LFS Checkout run: git lfs checkout working-directory: tardis-refdata - if: steps.lfs-cache.outputs.cache-hit == 'true' + if: steps.lfs-cache-refdata.outputs.cache-hit == 'true' - name: Save LFS cache if not found # uses fake ternary # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 if: ${{ steps.lfs-cache.outputs.cache-hit != 'true' && always() || false }} uses: actions/cache/save@v3 - id: lfs-cache-save + id: lfs-cache-refdata-save with: path: tardis-refdata/.git/lfs key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 - - name: Clone tardis-sn/tardis-regressions + - name: Clone tardis-sn/tardis-regression-data uses: actions/checkout@v4 with: - repository: tardis-sn/tardis-regressions - path: tardis-regressions + repository: tardis-sn/tardis-regression-data + path: tardis-regression-data + + - name: Create LFS file list + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + working-directory: tardis-refdata + + - name: Restore LFS cache + uses: actions/cache/restore@v3 + id: lfs-cache-regression-data + with: + path: tardis-regression-data/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 + + - name: Git LFS Pull + run: git lfs pull + working-directory: tardis-regression-data + if: steps.lfs-cache-regression-data.outputs.cache-hit != 'true' + + - name: Git LFS Checkout + run: git lfs checkout + working-directory: tardis-regression-data + if: steps.lfs-cache-regression-data.outputs.cache-hit == 'true' + + - name: Save LFS cache if not found + # uses fake ternary + # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 + if: ${{ steps.lfs-cache.outputs.cache-hit != 'true' && always() || false }} + uses: actions/cache/save@v3 + id: lfs-cache-regression-data-save + with: + path: tardis-regression-data/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 + - name: Setup environment uses: conda-incubator/setup-miniconda@v2 diff --git a/docs/io/visualization/abundance_widget.ipynb b/docs/io/visualization/abundance_widget.ipynb index 0fd25710f40..b0ec82fa6a8 100644 --- a/docs/io/visualization/abundance_widget.ipynb +++ b/docs/io/visualization/abundance_widget.ipynb @@ -113,7 +113,7 @@ "metadata": {}, "outputs": [], "source": [ - "# widget = CustomAbundanceWidget.from_hdf(\"demo.hdf\")" + "# widget = CustomAbundanceWidget.from_hdf(\"demo.h5\")" ] }, { diff --git a/docs/io/visualization/demo.hdf b/docs/io/visualization/demo.hdf deleted file mode 100644 index 7bcaa696bd5..00000000000 Binary files a/docs/io/visualization/demo.hdf and /dev/null differ diff --git a/docs/io/visualization/generating_widgets.ipynb b/docs/io/visualization/generating_widgets.ipynb index 333e9020697..9b43ed3eb21 100644 --- a/docs/io/visualization/generating_widgets.ipynb +++ b/docs/io/visualization/generating_widgets.ipynb @@ -127,7 +127,7 @@ }, "outputs": [], "source": [ - "# shell_info_widget = shell_info_from_hdf('demo.hdf')\n", + "# shell_info_widget = shell_info_from_hdf('demo.h5')\n", "# shell_info_widget.display()" ] }, diff --git a/docs/io/visualization/sdec_plot.ipynb b/docs/io/visualization/sdec_plot.ipynb index 9f52dbdb6aa..b4fd8df69f7 100644 --- a/docs/io/visualization/sdec_plot.ipynb +++ b/docs/io/visualization/sdec_plot.ipynb @@ -22,7 +22,7 @@ "source": [ "# We filter out warnings throughout this notebook\n", "import warnings\n", - "warnings.filterwarnings('ignore');\n", + "warnings.filterwarnings('ignore')\n", "\n", "# Due to the large size of the SDEC plots in SVG format, we request output as a\n", "# high-resolution PNG\n", @@ -54,7 +54,7 @@ "# We download the atomic data needed to run the simulation\n", "download_atom_data('kurucz_cd23_chianti_H_He')\n", "\n", - "sim = run_tardis(\"tardis_example.yml\", virtual_packet_logging=True)" + "sim = run_tardis(\"tardis_example.yml\", virtual_packet_logging=True)\n" ] }, { @@ -139,7 +139,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl();" + "plotter.generate_plot_mpl()" ] }, { @@ -162,7 +162,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(\"real\");" + "plotter.generate_plot_mpl(\"real\")" ] }, { @@ -198,7 +198,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(packet_wvl_range=[3000, 9000] * u.AA);" + "plotter.generate_plot_mpl(packet_wvl_range=[3000, 9000] * u.AA)" ] }, { @@ -226,7 +226,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(packet_wvl_range=[2000, 8000] * u.AA, nelements = 3);" + "plotter.generate_plot_mpl(packet_wvl_range=[2000, 8000] * u.AA, nelements = 3)" ] }, { @@ -254,7 +254,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(packet_wvl_range=[2000, 8000] * u.AA, species_list = ['Si II', 'S I-V', 'Ca']);" + "plotter.generate_plot_mpl(packet_wvl_range=[2000, 8000] * u.AA, species_list = ['Si II', 'S I-V', 'Ca'])" ] }, { @@ -275,7 +275,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(nelements = 3, species_list = ['Si II', 'S I-V', 'Ca']);" + "plotter.generate_plot_mpl(nelements = 3, species_list = ['Si II', 'S I-V', 'Ca'])" ] }, { @@ -297,7 +297,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(distance=100 * u.Mpc);" + "plotter.generate_plot_mpl(distance=100 * u.Mpc)" ] }, { @@ -338,7 +338,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(observed_spectrum = (observed_spectrum_wavelength, observed_spectrum_flux), distance = 6 * u.Mpc);" + "plotter.generate_plot_mpl(observed_spectrum = (observed_spectrum_wavelength, observed_spectrum_flux), distance = 6 * u.Mpc)" ] }, { @@ -360,7 +360,7 @@ }, "outputs": [], "source": [ - "plotter.generate_plot_mpl(show_modeled_spectrum=False);" + "plotter.generate_plot_mpl(show_modeled_spectrum=False)" ] }, { @@ -382,7 +382,7 @@ }, "outputs": [], "source": [ - "# To list all avaialble options (or parameters) with their description\n", + "# To list all available options (or parameters) with their description\n", "help(plotter.generate_plot_mpl)" ] }, @@ -490,7 +490,7 @@ }, "outputs": [], "source": [ - "# To list all avaialble options (or parameters) with their description\n", + "# To list all available options (or parameters) with their description\n", "help(plotter.generate_plot_ply)" ] }, @@ -513,7 +513,7 @@ }, "outputs": [], "source": [ - "hdf_plotter = SDECPlotter.from_hdf(\"demo.hdf\")" + "# hdf_plotter = SDECPlotter.from_hdf(\"demo.h5\") ## Files is too large - just as an example" ] }, { @@ -535,7 +535,7 @@ "outputs": [], "source": [ "# Static plot with virtual packets mode\n", - "hdf_plotter.generate_plot_mpl();" + "# hdf_plotter.generate_plot_mpl()" ] }, { @@ -550,7 +550,7 @@ "outputs": [], "source": [ "# Static plot with real packets mode\n", - "hdf_plotter.generate_plot_mpl(\"real\");" + "# hdf_plotter.generate_plot_mpl(\"real\")" ] }, { @@ -565,7 +565,7 @@ "outputs": [], "source": [ "# Interactive plot with virtual packets mode\n", - "hdf_plotter.generate_plot_ply()" + "# hdf_plotter.generate_plot_ply()" ] } ], diff --git a/tardis/conftest.py b/tardis/conftest.py index 00bcb0ec413..5c45a307db9 100644 --- a/tardis/conftest.py +++ b/tardis/conftest.py @@ -1,3 +1,19 @@ +import os +from pathlib import Path + +import pandas as pd +import pytest +from astropy.version import version as astropy_version + +from tardis.io.configuration.config_reader import Configuration +from tardis.io.util import YAMLLoader, yaml_load_file +from tardis.simulation import Simulation +from tardis.tests.fixtures.atom_data import * +from tardis.tests.fixtures.regression_data import regression_data + +# ensuring that regression_data is not removed by ruff +assert regression_data is not None + """Configure Test Suite. This file is used to configure the behavior of pytest when using the Astropy @@ -7,10 +23,6 @@ """ -import os -from pathlib import Path - -from astropy.version import version as astropy_version # For Astropy 3.0 and later, we can use the standalone pytest plugin if astropy_version < "3.0": @@ -70,32 +82,24 @@ def pytest_configure(config): # To ignore some specific deprecation warning messages for Python version # MAJOR.MINOR or later, add: # warnings_to_ignore_by_pyver={(MAJOR, MINOR): ['Message to ignore']} -# from astropy.tests.helper import enable_deprecations_as_exceptions # noqa +# from astropy.tests.helper import enable_deprecations_as_exceptions # enable_deprecations_as_exceptions() # ------------------------------------------------------------------------- # Here the TARDIS testing stuff begins # ------------------------------------------------------------------------- -import re -import pytest -import pandas as pd -from tardis.io.util import yaml_load_file, YAMLLoader -from tardis.io.configuration.config_reader import Configuration -from tardis.simulation import Simulation -from tardis.util.syrupy_extensions import ( - SingleFileSanitizedNames, - NumpySnapshotExtenstion, - PandasSnapshotExtenstion, -) - -pytest_plugins = "syrupy" - def pytest_addoption(parser): parser.addoption( "--tardis-refdata", default=None, help="Path to Tardis Reference Folder" ) + parser.addoption( + "--tardis-regression-data", + default=None, + help="Path to the TARDIS regression data directory", + ) + parser.addoption( "--integration-tests", dest="integration-tests", @@ -108,13 +112,6 @@ def pytest_addoption(parser): default=False, help="generate reference data instead of testing", ) - - parser.addoption( - "--tardis-snapshot-data", - default=None, - help="Path to Tardis Snapshot Folder", - ) - parser.addoption( "--less-packets", action="store_true", @@ -168,9 +165,6 @@ def tardis_snapshot_path(request): ) -from tardis.tests.fixtures.atom_data import * - - @pytest.yield_fixture(scope="session") def tardis_ref_data(tardis_ref_path, generate_reference): if generate_reference: @@ -238,23 +232,3 @@ def simulation_verysimple(config_verysimple, atomic_dataset): sim = Simulation.from_config(config_verysimple, atom_data=atomic_data) sim.iterate(4000) return sim - - -# ------------------------------------------------------------------------- -# fixtures and plugins for syrupy/regression data testing -# ------------------------------------------------------------------------- - - -@pytest.fixture -def pandas_snapshot_extention(): - return PandasSnapshotExtenstion - - -@pytest.fixture -def numpy_snapshot_extension(): - return NumpySnapshotExtenstion - - -@pytest.fixture -def singlefilesanitized(): - return SingleFileSanitizedNames diff --git a/tardis/data/atomic_data_repo.yml b/tardis/data/atomic_data_repo.yml index 0309eb1ffc9..334084b121f 100644 --- a/tardis/data/atomic_data_repo.yml +++ b/tardis/data/atomic_data_repo.yml @@ -1,7 +1,7 @@ default: kurucz_cd23_chianti_H_He kurucz_cd23_chianti_H_He: - url: https://dev.azure.com/tardis-sn/TARDIS/_apis/git/repositories/tardis-refdata/items?path=atom_data/kurucz_cd23_chianti_H_He.h5&resolveLfs=true + url: https://media.githubusercontent.com/media/tardis-sn/tardis-refdata/master/atom_data/kurucz_cd23_chianti_H_He.h5 mirrors: - https://dev.azure.com/tardis-sn/TARDIS/_apis/git/repositories/tardis-refdata/items?path=atom_data/kurucz_cd23_chianti_H_He.h5&resolveLfs=true - https://media.githubusercontent.com/media/tardis-sn/tardis-refdata/master/atom_data/kurucz_cd23_chianti_H_He.h5 diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_base.py b/tardis/montecarlo/montecarlo_numba/tests/test_base.py index 320826cf68d..4c73232aa40 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_base.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_base.py @@ -1,10 +1,7 @@ -import pytest -import pandas as pd -import os -import numpy.testing as npt from copy import deepcopy -from tardis.base import run_tardis -from pandas.testing import assert_frame_equal + +import numpy.testing as npt +import pytest from tardis.montecarlo import ( montecarlo_configuration as montecarlo_configuration, @@ -35,8 +32,7 @@ def montecarlo_main_loop_config( def test_montecarlo_main_loop( montecarlo_main_loop_config, - tardis_ref_path, - request, + regression_data, atomic_dataset, ): atomic_dataset = deepcopy(atomic_dataset) @@ -48,26 +44,21 @@ def test_montecarlo_main_loop( montecarlo_main_loop_simulation.run_convergence() montecarlo_main_loop_simulation.run_final() - compare_fname = os.path.join( - tardis_ref_path, "montecarlo_1e5_compare_data.h5" + expected_hdf_store = regression_data.sync_hdf_store( + montecarlo_main_loop_simulation ) - if request.config.getoption("--generate-reference"): - montecarlo_main_loop_simulation.to_hdf(compare_fname, overwrite=True) # Load compare data from refdata - expected_nu = pd.read_hdf( - compare_fname, key="/simulation/transport/output_nu" - ).values - expected_energy = pd.read_hdf( - compare_fname, key="/simulation/transport/output_energy" - ).values - expected_nu_bar_estimator = pd.read_hdf( - compare_fname, key="/simulation/transport/nu_bar_estimator" - ).values - expected_j_estimator = pd.read_hdf( - compare_fname, key="/simulation/transport/j_estimator" - ).values + expected_nu = expected_hdf_store["/simulation/transport/output_nu"] + expected_energy = expected_hdf_store["/simulation/transport/output_energy"] + expected_nu_bar_estimator = expected_hdf_store[ + "/simulation/transport/nu_bar_estimator" + ] + expected_j_estimator = expected_hdf_store[ + "/simulation/transport/j_estimator" + ] + expected_hdf_store.close() actual_energy = montecarlo_main_loop_simulation.transport.output_energy actual_nu = montecarlo_main_loop_simulation.transport.output_nu actual_nu_bar_estimator = ( @@ -84,11 +75,9 @@ def test_montecarlo_main_loop( npt.assert_allclose(actual_nu.value, expected_nu, rtol=1e-13) -@pytest.mark.xfail(reason="need to store virtual packet data in hdf5") def test_montecarlo_main_loop_vpacket_log( montecarlo_main_loop_config, - tardis_ref_path, - request, + regression_data, atomic_dataset, ): atomic_dataset = deepcopy(atomic_dataset) @@ -102,25 +91,24 @@ def test_montecarlo_main_loop_vpacket_log( montecarlo_main_loop_simulation.run_convergence() montecarlo_main_loop_simulation.run_final() - compare_fname = os.path.join( - tardis_ref_path, "montecarlo_1e5_compare_data.h5" + expected_hdf_store = regression_data.sync_hdf_store( + montecarlo_main_loop_simulation ) - if request.config.getoption("--generate-reference"): - montecarlo_main_loop_simulation.to_hdf(compare_fname, overwrite=True) - # Load compare data from refdata - expected_nu = pd.read_hdf( - compare_fname, key="/simulation/transport/output_nu" - ).values - expected_energy = pd.read_hdf( - compare_fname, key="/simulation/transport/output_energy" - ).values - expected_nu_bar_estimator = pd.read_hdf( - compare_fname, key="/simulation/transport/nu_bar_estimator" - ).values - expected_j_estimator = pd.read_hdf( - compare_fname, key="/simulation/transport/j_estimator" - ).values + expected_nu = expected_hdf_store["/simulation/transport/output_nu"] + expected_energy = expected_hdf_store["/simulation/transport/output_energy"] + expected_nu_bar_estimator = expected_hdf_store[ + "/simulation/transport/nu_bar_estimator" + ] + expected_j_estimator = expected_hdf_store[ + "/simulation/transport/j_estimator" + ] + expected_vpacket_log_nus = expected_hdf_store[ + "/simulation/transport/virt_packet_nus" + ] + expected_vpacket_log_energies = expected_hdf_store[ + "/simulation/transport/virt_packet_energies" + ] transport = montecarlo_main_loop_simulation.transport actual_energy = transport.output_energy @@ -129,11 +117,27 @@ def test_montecarlo_main_loop_vpacket_log( actual_j_estimator = montecarlo_main_loop_simulation.transport.j_estimator actual_vpacket_log_nus = transport.virt_packet_nus actual_vpacket_log_energies = transport.virt_packet_energies - + expected_hdf_store.close() # Compare npt.assert_allclose( - actual_nu_bar_estimator, expected_nu_bar_estimator, rtol=1e-13 + actual_nu_bar_estimator, + expected_nu_bar_estimator, + rtol=1e-12, + atol=1e-15, + ) + npt.assert_allclose( + actual_j_estimator, expected_j_estimator, rtol=1e-12, atol=1e-15 + ) + npt.assert_allclose( + actual_energy.value, expected_energy, rtol=1e-12, atol=1e-15 + ) + npt.assert_allclose(actual_nu.value, expected_nu, rtol=1e-12, atol=1e-15) + npt.assert_allclose( + actual_vpacket_log_nus, expected_vpacket_log_nus, rtol=1e-12, atol=1e-15 + ) + npt.assert_allclose( + actual_vpacket_log_energies, + expected_vpacket_log_energies, + rtol=1e-12, + atol=1e-15, ) - npt.assert_allclose(actual_j_estimator, expected_j_estimator, rtol=1e-13) - npt.assert_allclose(actual_energy.value, expected_energy, rtol=1e-13) - npt.assert_allclose(actual_nu.value, expected_nu, rtol=1e-13) diff --git a/tardis/plasma/tests/conftest.py b/tardis/plasma/tests/conftest.py deleted file mode 100644 index dab8927219e..00000000000 --- a/tardis/plasma/tests/conftest.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -import pytest -from syrupy.location import PyTestLocation - -SNAPSHOT_LOCATION = "plasma" - - -@pytest.fixture -def snapshot_pd(snapshot, tardis_snapshot_path, pandas_snapshot_extention): - refpath = tardis_snapshot_path.joinpath(SNAPSHOT_LOCATION) - - class PandasSnapshotExtenstionRefdata(pandas_snapshot_extention): - @classmethod - def dirname(cls, *, test_location: "PyTestLocation") -> str: - return str(Path(test_location.filepath).parent.joinpath(refpath)) - - return snapshot.use_extension(PandasSnapshotExtenstionRefdata) - - -@pytest.fixture -def snapshot_np(snapshot, tardis_snapshot_path, numpy_snapshot_extension): - refpath = tardis_snapshot_path.joinpath(SNAPSHOT_LOCATION) - - class NumpySnapshotExtenstionRefdata(numpy_snapshot_extension): - @classmethod - def dirname(cls, *, test_location: "PyTestLocation") -> str: - return str(Path(test_location.filepath).parent.joinpath(refpath)) - - return snapshot.use_extension(NumpySnapshotExtenstionRefdata) - - -@pytest.fixture -def snapshot(snapshot, tardis_snapshot_path, singlefilesanitized): - refpath = tardis_snapshot_path.joinpath(SNAPSHOT_LOCATION) - - class SingleFileSanitizedRefdata(singlefilesanitized): - @classmethod - def dirname(cls, *, test_location: "PyTestLocation") -> str: - return str(Path(test_location.filepath).parent.joinpath(refpath)) - - return snapshot.use_extension(SingleFileSanitizedRefdata) diff --git a/tardis/plasma/tests/test_complete_plasmas.py b/tardis/plasma/tests/test_complete_plasmas.py index 6231eb11e06..ed7d4428852 100644 --- a/tardis/plasma/tests/test_complete_plasmas.py +++ b/tardis/plasma/tests/test_complete_plasmas.py @@ -1,11 +1,19 @@ -import os import warnings +from pathlib import Path import pandas as pd import pytest +from numpy import testing as npt +from pandas import testing as pdt from tardis.io.configuration.config_reader import Configuration from tardis.simulation import Simulation +from tardis.tests.fixtures.regression_data import RegressionData + +PLASMA_CONFIG_FPATH = ( + Path("tardis") / "plasma" / "tests" / "data" / "plasma_base_test_config.yml" +) + ionization = [ {"ionization": "nebular"}, @@ -47,7 +55,7 @@ {"helium_treatment": "recomb-nlte", "delta_treatment": 0.5}, ] -config_list = ( +CONFIG_LIST = ( ionization + excitation + radiative_rates_type @@ -65,10 +73,12 @@ def idfn(fixture_value): This function creates a string from a dictionary. We use it to obtain a readable name for the config fixture. """ - return str("-".join([f"{k}:{v}" for k, v in fixture_value.items()])) + return "-".join([f"{k}:{v}" for k, v in fixture_value.items()]) + +class TestPlasma: + regression_data = None -class TestPlasma(object): general_properties = [ "beta_rad", "g_electron", @@ -127,16 +137,11 @@ class TestPlasma(object): @pytest.fixture(scope="class") def chianti_he_db_fpath(self, tardis_ref_path): - return os.path.abspath( - os.path.join(tardis_ref_path, "atom_data", "chianti_He.h5") - ) + return (tardis_ref_path / "atom_data" / "chianti_He.h5").absolute() - @pytest.fixture(scope="class", params=config_list, ids=idfn) + @pytest.fixture(scope="class", params=CONFIG_LIST, ids=idfn) def config(self, request): - config_path = os.path.join( - "tardis", "plasma", "tests", "data", "plasma_base_test_config.yml" - ) - config = Configuration.from_yaml(config_path) + config = Configuration.from_yaml(PLASMA_CONFIG_FPATH) hash_string = "" for prop, value in request.param.items(): hash_string = "_".join((hash_string, prop)) @@ -148,55 +153,70 @@ def config(self, request): else: config.plasma[prop] = value hash_string = "_".join((hash_string, str(value))) - hash_string = os.path.join("plasma_unittest", hash_string) - setattr(config.plasma, "save_path", hash_string) + hash_string = f"plasma_unittest{hash_string}" + config.plasma.save_path = hash_string + request.cls.regression_data = RegressionData(request) + request.cls.regression_data.fname = f"{hash_string}.h5" return config @pytest.fixture(scope="class") - def plasma(self, chianti_he_db_fpath, config): - config["atom_data"] = chianti_he_db_fpath + def plasma( + self, + chianti_he_db_fpath, + config, + ): + config["atom_data"] = str(chianti_he_db_fpath) sim = Simulation.from_config(config) + self.regression_data.sync_hdf_store(sim.plasma, update_fname=False) return sim.plasma @pytest.mark.parametrize("attr", combined_properties) - def test_plasma_properties(self, plasma, attr, snapshot_pd, snapshot_np): + def test_plasma_properties(self, plasma, attr): + key = f"plasma/{attr}" + try: + expected = pd.read_hdf(self.regression_data.fpath, key) + except KeyError: + pytest.skip(f"Key {key} not found in regression data") + if hasattr(plasma, attr): actual = getattr(plasma, attr) - if hasattr(actual, "unit"): - actual = actual.value - if actual.ndim == 1: + if attr == "selected_atoms": + npt.assert_allclose(actual.values, expected.values) + elif actual.ndim == 1: actual = pd.Series(actual) + pdt.assert_series_equal(actual, expected) else: actual = pd.DataFrame(actual) - if isinstance(actual, (pd.DataFrame, pd.Series)): - assert snapshot_pd == actual - else: - assert snapshot_np == actual + pdt.assert_frame_equal(actual, expected) else: warnings.warn(f'Property "{attr}" not found') - def test_levels(self, plasma, snapshot_pd, snapshot_np): + def test_levels(self, plasma): actual = pd.DataFrame(plasma.levels) - if isinstance(actual, (pd.DataFrame, pd.Series)): - assert snapshot_pd == actual - else: - assert snapshot_np == actual + key = f"plasma/levels" + expected = pd.read_hdf(self.regression_data.fpath, key) + pdt.assert_frame_equal(actual, expected) @pytest.mark.parametrize("attr", scalars_properties) - def test_scalars_properties(self, plasma, attr, snapshot_pd, snapshot_np): + def test_scalars_properties(self, plasma, attr): actual = getattr(plasma, attr) if hasattr(actual, "cgs"): actual = actual.cgs.value - if isinstance(actual, (pd.DataFrame, pd.Series)): - assert snapshot_pd == actual - else: - assert snapshot_np == actual + key = f"plasma/scalars" + expected = pd.read_hdf(self.regression_data.fpath, key)[attr] + npt.assert_equal(actual, expected) - def test_helium_treatment(self, plasma, snapshot): + def test_helium_treatment(self, plasma): actual = plasma.helium_treatment - assert snapshot == actual + key = f"plasma/scalars" + expected = pd.read_hdf(self.regression_data.fpath, key)[ + "helium_treatment" + ] + assert actual == expected - def test_zeta_data(self, plasma, snapshot_np): + def test_zeta_data(self, plasma): if hasattr(plasma, "zeta_data"): actual = plasma.zeta_data - assert snapshot_np == actual.values + key = f"plasma/zeta_data" + expected = pd.read_hdf(self.regression_data.fpath, key) + npt.assert_allclose(actual, expected.values) diff --git a/tardis/plasma/tests/test_hdf_plasma.py b/tardis/plasma/tests/test_hdf_plasma.py index 01c91cb386b..d0c4f4c6433 100644 --- a/tardis/plasma/tests/test_hdf_plasma.py +++ b/tardis/plasma/tests/test_hdf_plasma.py @@ -1,4 +1,6 @@ -import pandas as pd +import numpy as np +import numpy.testing as npt +import pandas.testing as pdt import pytest ### @@ -43,48 +45,54 @@ @pytest.mark.parametrize("attr", plasma_properties_list) -def test_hdf_plasma(simulation_verysimple, attr, snapshot_np): +def test_hdf_plasma(simulation_verysimple, attr, regression_data): if hasattr(simulation_verysimple.plasma, attr): actual = getattr(simulation_verysimple.plasma, attr) + expected = regression_data.sync_ndarray(actual) if hasattr(actual, "cgs"): actual = actual.cgs.value - assert snapshot_np == actual + npt.assert_allclose(actual, expected) -def test_hdf_levels(simulation_verysimple, snapshot_pd): - actual = getattr(simulation_verysimple.plasma, "levels") +def test_hdf_levels(simulation_verysimple, regression_data): + actual = simulation_verysimple.plasma.levels.to_frame() + expected = regression_data.sync_dataframe(actual) if hasattr(actual, "cgs"): - actual = actual.cgs.value - assert snapshot_pd == pd.DataFrame(actual) + raise ValueError("should not ever happen") + pdt.assert_frame_equal(actual, expected) -scalars_list = ["time_explosion", "link_t_rad_t_electron"] +SCALARS_LIST = ["time_explosion", "link_t_rad_t_electron"] -@pytest.mark.parametrize("attr", scalars_list) -def test_hdf_scalars(simulation_verysimple, attr, snapshot_np): +@pytest.mark.parametrize("attr", SCALARS_LIST) +def test_hdf_scalars(simulation_verysimple, attr, regression_data): actual = getattr(simulation_verysimple.plasma, attr) if hasattr(actual, "cgs"): actual = actual.cgs.value - assert snapshot_np == actual + expected = regression_data.sync_ndarray(actual) + npt.assert_allclose(actual, expected) -def test_hdf_helium_treatment(simulation_verysimple, snapshot): - actual = getattr(simulation_verysimple.plasma, "helium_treatment") - assert snapshot == actual +def test_hdf_helium_treatment(simulation_verysimple, regression_data): + actual = simulation_verysimple.plasma.helium_treatment + expected = regression_data.sync_str(actual) + assert actual == expected -def test_atomic_data_uuid(simulation_verysimple, snapshot): - actual = getattr(simulation_verysimple.plasma.atomic_data, "uuid1") - assert snapshot == actual +def test_atomic_data_uuid(simulation_verysimple, regression_data): + actual = simulation_verysimple.plasma.atomic_data.uuid1 + expected = regression_data.sync_str(actual) + assert actual == expected -collection_properties = ["t_rad", "w", "density"] +COLLECTION_PROPERTIES = ["t_rad", "w", "density"] -@pytest.mark.parametrize("attr", collection_properties) -def test_collection(simulation_verysimple, attr, snapshot_np): +@pytest.mark.parametrize("attr", COLLECTION_PROPERTIES) +def test_collection(simulation_verysimple, attr, regression_data): actual = getattr(simulation_verysimple.plasma, attr) + expected = regression_data.sync_ndarray(actual) if hasattr(actual, "cgs"): actual = actual.cgs.value - assert snapshot_np == actual + npt.assert_allclose(actual, expected) diff --git a/tardis/plasma/tests/test_nlte_excitation.py b/tardis/plasma/tests/test_nlte_excitation.py index 884f28af20a..19f5c4bf7f5 100644 --- a/tardis/plasma/tests/test_nlte_excitation.py +++ b/tardis/plasma/tests/test_nlte_excitation.py @@ -1,4 +1,5 @@ import numpy as np +import numpy.testing as npt import pandas as pd import pytest @@ -8,7 +9,7 @@ ) -def test_prepare_bound_bound_rate_matrix(nlte_atomic_dataset, snapshot_np): +def test_prepare_bound_bound_rate_matrix(nlte_atomic_dataset, regression_data): """ Using a simple case of nlte_exc for HI, checks if prepare_bound_bound_rate_matrix generates the correct data. """ @@ -72,7 +73,8 @@ def test_prepare_bound_bound_rate_matrix(nlte_atomic_dataset, snapshot_np): # if this test fails the first thing to check is if the reshape in the # methods made a view or a copy. If it's a copy rewrite the function. # TODO: allow rtol=1e-6 - assert snapshot_np == np.array(actual_rate_matrix) + expected_rate_matrix = regression_data.sync_ndarray(actual_rate_matrix) + npt.assert_allclose(actual_rate_matrix, expected_rate_matrix, rtol=1e-6) @pytest.mark.parametrize( @@ -98,7 +100,7 @@ def test_coll_exc_deexc_matrix( coll_exc_coeff_values, coll_deexc_coeff_values, number_of_levels, - snapshot_np, + regression_data, ): """ Checks the NLTERateEquationSolver.create_coll_exc_deexc_matrix for simple values of species with 3 levels. @@ -113,4 +115,7 @@ def test_coll_exc_deexc_matrix( obtained_coeff_matrix = NLTERateEquationSolver.create_coll_exc_deexc_matrix( exc_coeff, deexc_coeff, number_of_levels ) - assert snapshot_np == obtained_coeff_matrix + expected_obtained_coeff_matrix = regression_data.sync_ndarray( + obtained_coeff_matrix + ) + npt.assert_allclose(expected_obtained_coeff_matrix, obtained_coeff_matrix) diff --git a/tardis/plasma/tests/test_nlte_solver.py b/tardis/plasma/tests/test_nlte_solver.py index bdd1bb0962d..e70152b9062 100644 --- a/tardis/plasma/tests/test_nlte_solver.py +++ b/tardis/plasma/tests/test_nlte_solver.py @@ -1,7 +1,8 @@ import numpy as np import pandas as pd import pytest -from numpy.testing import assert_allclose +import numpy.testing as npt + from tardis.plasma.properties import NLTERateEquationSolver from tardis.plasma.properties.ion_population import IonNumberDensity @@ -139,7 +140,7 @@ def test_rate_matrix( simple_total_rad_recomb_coefficients, simple_total_col_ion_coefficients, simple_total_col_recomb_coefficients, - snapshot_np, + regression_data, ): """ Using a simple case of nlte_ion for HI and HeII, checks if the calculate_rate_matrix generates the correct data. @@ -157,7 +158,8 @@ def test_rate_matrix( ) # TODO: decimal=6 # allow for assert_almost_equal - assert snapshot_np == np.array(actual_rate_matrix) + expected_rate_matrix = regression_data.sync_ndarray(actual_rate_matrix) + npt.assert_allclose(actual_rate_matrix, expected_rate_matrix, rtol=1e-6) def test_jacobian_matrix( @@ -168,7 +170,7 @@ def test_jacobian_matrix( simple_total_rad_recomb_coefficients, simple_total_col_ion_coefficients, simple_total_col_recomb_coefficients, - snapshot_np, + regression_data, ): """ Using a simple case of nlte_ion for HI and HeII, @@ -206,7 +208,10 @@ def test_jacobian_matrix( ) # TODO: allow for assert_almost_equal - assert snapshot_np == actual_jacobian_matrix + expected_jacobian_matrix = regression_data.sync_ndarray( + actual_jacobian_matrix + ) + npt.assert_allclose(actual_jacobian_matrix, expected_jacobian_matrix) @pytest.fixture @@ -256,7 +261,7 @@ def test_critical_case_w1(nlte_raw_plasma_w1): ion_number_density_lte[ ion_number_density_lte < 1e-10 ] = 0.0 # getting rid of small numbers. - assert_allclose( + npt.assert_allclose( ion_number_density_lte, ion_number_density_nlte, rtol=1e-2, @@ -294,7 +299,7 @@ def test_critical_case_w0(nlte_raw_plasma_w0): ion_number_density_lte[ ion_number_density_lte < 1e-10 ] = 0.0 # getting rid of small numbers. - assert_allclose( + npt.assert_allclose( ion_number_density_lte, ion_number_density_nlte, rtol=1e-2, diff --git a/tardis/plasma/tests/test_plasma_contiuum.py b/tardis/plasma/tests/test_plasma_contiuum.py index 633b47a8036..f0eaa28e170 100644 --- a/tardis/plasma/tests/test_plasma_contiuum.py +++ b/tardis/plasma/tests/test_plasma_contiuum.py @@ -1,9 +1,11 @@ import numpy as np +import numpy.testing as npt from tardis.plasma.properties import YgData -def test_exp1_times_exp(snapshot_np): +def test_exp1_times_exp(regression_data): x = np.array([499.0, 501.0, 710.0]) actual = YgData.exp1_times_exp(x) - assert snapshot_np == actual + expected = regression_data.sync_ndarray(actual) + npt.assert_allclose(actual, expected) diff --git a/tardis/plasma/tests/test_tardis_model_density_config.py b/tardis/plasma/tests/test_tardis_model_density_config.py index e26b923e827..20c9ec46cda 100644 --- a/tardis/plasma/tests/test_tardis_model_density_config.py +++ b/tardis/plasma/tests/test_tardis_model_density_config.py @@ -1,3 +1,5 @@ +import numpy.testing as npt +import pandas.testing as pdt import pytest from tardis.io.configuration.config_reader import Configuration @@ -28,17 +30,21 @@ def raw_plasma( ) -def test_electron_densities(raw_plasma, snapshot_np): - assert snapshot_np == raw_plasma.electron_densities[8] - assert snapshot_np == raw_plasma.electron_densities[3] +def test_electron_densities(raw_plasma, regression_data): + actual = raw_plasma.electron_densities + expected = regression_data.sync_ndarray(actual) + npt.assert_allclose(actual, expected) -def test_isotope_number_densities(request, raw_simulation_state, snapshot_np): - composition = raw_simulation_state.composition - assert snapshot_np == composition.isotopic_number_density.loc[(28, 56), 0] - assert snapshot_np == composition.isotopic_number_density.loc[(28, 58), 1] +def test_isotope_number_densities( + request, raw_simulation_state, regression_data +): + actual = raw_simulation_state.composition.isotopic_number_density + expected = regression_data.sync_dataframe(actual) + pdt.assert_frame_equal(actual, expected) -def test_t_rad(raw_plasma, snapshot_np): - assert snapshot_np == raw_plasma.t_rad[5] - assert snapshot_np == raw_plasma.t_rad[3] +def test_t_rad(raw_plasma, regression_data): + actual = raw_plasma.t_rad + expected = regression_data.sync_ndarray(actual) + npt.assert_array_equal(actual, expected) diff --git a/tardis/tests/fixtures/regression_data.py b/tardis/tests/fixtures/regression_data.py new file mode 100644 index 00000000000..0375204b506 --- /dev/null +++ b/tardis/tests/fixtures/regression_data.py @@ -0,0 +1,169 @@ +import os +import re +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from tardis.io.util import HDFWriterMixin + + +class RegressionData: + def __init__(self, request) -> None: + self.request = request + regression_data_path = Path( + request.config.getoption("--tardis-regression-data") + ) + if regression_data_path is None: + pytest.skip("--tardis-regression-data was not specified") + self.regression_data_path = Path( + os.path.expandvars(regression_data_path.expanduser()) + ) + self.enable_generate_reference = request.config.getoption( + "--generate-reference" + ) + self.fname = f"{self.fname_prefix}.UNKNOWN_FORMAT" + + @property + def module_name(self): + return self.request.node.module.__name__ + + @property + def test_name(self): + return self.request.node.name + + @property + def fname_prefix(self): + double_under = re.compile(r"[:\[\]{}]") + no_space = re.compile(r'[,"\']') # quotes and commas + + name = double_under.sub("__", self.test_name) + name = no_space.sub("", name) + return name + + @property + def relative_regression_data_dir(self): + relative_data_dir = Path(self.module_name.replace(".", "/")) + if self.request.cls is not None: + relative_data_dir /= HDFWriterMixin.convert_to_snake_case( + self.request.cls.__name__ + ) + return relative_data_dir + + @property + def absolute_regression_data_dir(self): + return self.regression_data_path / self.relative_regression_data_dir + + @property + def fpath(self): + return self.absolute_regression_data_dir / self.fname + + def sync_dataframe(self, data, key="data"): + """ + Synchronizes the dataframe with the regression data. + + Parameters + ---------- + data : DataFrame + The dataframe to be synchronized. + key : str, optional + The key to use for storing the dataframe in the regression data file. Defaults to "data". + + Returns + ------- + DataFrame or None + The synchronized dataframe if `enable_generate_reference` is `False`, otherwise `None`. + """ + self.fname = f"{self.fname_prefix}.h5" + if self.enable_generate_reference: + self.fpath.parent.mkdir(parents=True, exist_ok=True) + data.to_hdf( + self.fpath, + key=key, + ) + pytest.skip("Skipping test to generate reference data") + else: + return pd.read_hdf(self.fpath, key=key) + + def sync_ndarray(self, data): + """ + Synchronizes the ndarray with the regression data. + + Parameters + ---------- + data : ndarray + The ndarray to be synchronized. + + Returns + ------- + ndarray or None + The synchronized ndarray if `enable_generate_reference` is `False`, otherwise `None`. + """ + self.fname = f"{self.fname_prefix}.npy" + if self.enable_generate_reference: + self.fpath.parent.mkdir(parents=True, exist_ok=True) + self.fpath.parent.mkdir(parents=True, exist_ok=True) + np.save(self.fpath, data) + pytest.skip("Skipping test to generate reference data") + else: + return np.load(self.fpath) + + def sync_str(self, data): + """ + Synchronizes the string with the regression data. + + Parameters + ---------- + data : str + The string to be synchronized. + + Returns + ------- + str or None + The synchronized string if `enable_generate_reference` is `False`, otherwise `None`. + """ + self.fname = f"{self.fname_prefix}.txt" + if self.enable_generate_reference: + self.fpath.parent.mkdir(parents=True, exist_ok=True) + with self.fpath.open("w") as fh: + fh.write(data) + pytest.skip( + f"Skipping test to generate regression_data {fpath} data" + ) + else: + with self.fpath.open("r") as fh: + return fh.read() + + def sync_hdf_store(self, tardis_module, update_fname=True): + """ + Synchronizes the HDF store with the regression data. + + Parameters + ---------- + tardis_module : object + The module to be synchronized. + update_fname : bool, optional + Whether to update the file name. Defaults to True. + + Returns + ------- + HDFStore or None + The synchronized HDF store if `enable_generate_reference` is `False`, otherwise `None`. + """ + if update_fname: + self.fname = f"{self.fname_prefix}.h5" + if self.enable_generate_reference: + self.fpath.parent.mkdir(parents=True, exist_ok=True) + with pd.HDFStore(self.fpath, mode="w") as store: + tardis_module.to_hdf(store, overwrite=True) + pytest.skip( + f"Skipping test to generate regression_data {self.fpath} data" + ) + else: + return pd.HDFStore(self.fpath, mode="r") + + +@pytest.fixture(scope="function") +def regression_data(request): + return RegressionData(request) diff --git a/tardis/util/syrupy_extensions.py b/tardis/util/syrupy_extensions.py deleted file mode 100644 index 050fca3a1bc..00000000000 --- a/tardis/util/syrupy_extensions.py +++ /dev/null @@ -1,129 +0,0 @@ -import re -from typing import Any, List, Tuple - -import numpy as np -import pandas as pd -from syrupy.data import SnapshotCollection -from syrupy.extensions.single_file import SingleFileSnapshotExtension, WriteMode -from syrupy.location import PyTestLocation -from syrupy.types import SerializableData, SnapshotIndex - - -class SingleFileSanitizedNames(SingleFileSnapshotExtension): - # changing write mode to text helps avoid an error message - # that comes when files are serialised in syrupy in bytes - # either way we won't be serialising files in most cases in bytes - _write_mode = WriteMode.TEXT - _file_extension = "txt" - - # would change names of all snapshots generated - # that use this class- making filenames compliant with python standards. - @classmethod - def get_snapshot_name( - cls, *, test_location: "PyTestLocation", index: "SnapshotIndex" - ) -> str: - original_name = SingleFileSnapshotExtension.get_snapshot_name( - test_location=test_location, index=index - ) - double_under = r"[:\[\]{}]" - no_space = r'[,"\']' # quotes and commas - - name = re.sub(double_under, "__", original_name) - name = re.sub(no_space, "", name) - - return f"{name}" - - -class NumpySnapshotExtenstion(SingleFileSanitizedNames): - _file_extension = "npy" - - def matches(self, *, serialized_data, snapshot_data): - try: - if ( - np.testing.assert_allclose( - np.array(snapshot_data), np.array(serialized_data) - ) - is not None - ): - return False - else: - return True - - except: - return False - - def _read_snapshot_data_from_location( - self, *, snapshot_location: str, snapshot_name: str, session_id: str - ): - # see https://github.com/tophat/syrupy/blob/f4bc8453466af2cfa75cdda1d50d67bc8c4396c3/src/syrupy/extensions/base.py#L139 - try: - return np.load(snapshot_location) - - except OSError: - return None - - @classmethod - def _write_snapshot_collection( - cls, *, snapshot_collection: SnapshotCollection - ) -> None: - # see https://github.com/tophat/syrupy/blob/f4bc8453466af2cfa75cdda1d50d67bc8c4396c3/src/syrupy/extensions/base.py#L161 - - filepath, data = ( - snapshot_collection.location, - next(iter(snapshot_collection)).data, - ) - - np.save(filepath, data) - - def serialize(self, data: SerializableData, **kwargs: Any) -> str: - return data - - -class PandasSnapshotExtenstion(SingleFileSanitizedNames): - _file_extension = "h5" - - def matches(self, *, serialized_data, snapshot_data): - try: - comparer = { - pd.Series: pd.testing.assert_series_equal, - pd.DataFrame: pd.testing.assert_frame_equal, - } - try: - comp_func = comparer[type(serialized_data)] - except KeyError: - raise ValueError( - "Can only compare Series and Dataframes with PandasSnapshotExtenstion." - ) - - if comp_func(serialized_data, snapshot_data) is not None: - return False - else: - return True - - except: - return False - - def _read_snapshot_data_from_location( - self, *, snapshot_location: str, snapshot_name: str, session_id: str - ): - # see https://github.com/tophat/syrupy/blob/f4bc8453466af2cfa75cdda1d50d67bc8c4396c3/src/syrupy/extensions/base.py#L139 - try: - data = pd.read_hdf(snapshot_location) - return data - - except OSError: - return None - - @classmethod - def _write_snapshot_collection( - cls, *, snapshot_collection: SnapshotCollection - ) -> None: - # see https://github.com/tophat/syrupy/blob/f4bc8453466af2cfa75cdda1d50d67bc8c4396c3/src/syrupy/extensions/base.py#L161 - filepath, data = ( - snapshot_collection.location, - next(iter(snapshot_collection)).data, - ) - data.to_hdf(filepath, "/data") - - def serialize(self, data: SerializableData, **kwargs: Any) -> str: - return data