From 7c570630147ac61394c4956e23a2ef91eca58101 Mon Sep 17 00:00:00 2001 From: Kevin Meagher <11620178+kjmeagher@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:07:53 -0600 Subject: [PATCH] use pytest parallelization instead of subtest --- .github/workflows/tests.yml | 34 +++++++++- .pre-commit-config.yaml | 2 +- pyproject.toml | 18 ++--- src/i3astropy/__init__.py | 1 + tests/test_coord.py | 131 ++++++++++++++++++------------------ tests/test_time.py | 75 +++++++++++---------- 6 files changed, 142 insertions(+), 119 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bd4a846..b5ad20f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,6 +12,10 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + actions: read + checks: write jobs: Tests: runs-on: ${{ matrix.os }} @@ -34,12 +38,40 @@ jobs: - name: Install i3astropy run: python3 -m pip install .[test] - name: Run Unit Tests - run: pytest + run: pytest --junit-xml=test-results-${{matrix.os}}-${{matrix.python-version}}.junit.xml + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + if-no-files-found: error + name: test-results-${{matrix.os}}-${{matrix.python-version}}.junit.xml + path: test-results-${{matrix.os}}-${{matrix.python-version}}.junit.xml - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 with: fail_ci_if_error: false verbose: true + publish-test-results: + name: "Publish Tests Results" + needs: Tests + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + contents: read + if: always() + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: . + pattern: test-results-* + merge-multiple: true + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: "*.xml" + deduplicate_classes_by_file_name: true Docs: runs-on: ubuntu-22.04 strategy: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e18cbf7..f9457ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: additional_dependencies: [numpy] files: src/i3astropy - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.2.1 hooks: - id: ruff args: [--fix, --show-fixes] diff --git a/pyproject.toml b/pyproject.toml index 8a553c5..bba4754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,24 +34,12 @@ requires-python = "~=3.9" dev = ["pre-commit"] docs = ['mkdocs'] examples = ["matplotlib"] -test = ["pytest", "pytest-cov", "pytest-subtests"] +test = ["pytest", "pytest-cov"] [project.urls] Collaboration = "https://icecube.wisc.edu" Source = "https://github.com/icecube/i3astropy" -[tool.black] -line-length = 108 -target-version = ['py39'] - -[tool.isort] -ensure_newline_before_comments = true -force_grid_wrap = 0 -include_trailing_comma = true -line_length = 108 -multi_line_output = 3 -use_parentheses = true - [tool.mypy] allow_subclassing_any = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] @@ -90,7 +78,9 @@ ignore = [ "S101", # assert-used "D213", # multi-line-summary-second-line incompatible with multi-line-summary-first-line "D203", # one-blank-line-before-class" incompatible with no-blank-line-before-class - "RUF012" # mutable-class-default + "RUF012", # mutable-class-default + "COM812", # confilcts with formatter + "ISC001" # confilcts with formatter ] select = ["ALL"] diff --git a/src/i3astropy/__init__.py b/src/i3astropy/__init__.py index 268533b..e6437ae 100644 --- a/src/i3astropy/__init__.py +++ b/src/i3astropy/__init__.py @@ -37,6 +37,7 @@ class I3Time(TimeFormat): However, when initializing Time objects astropy converts all parameters to float64, which for values DAQ times close to the end of the year can result in a loss of precision of up to 64 DAQ ticks (6.4 nanoseconds). + """ name = "i3time" # Unique format name diff --git a/tests/test_coord.py b/tests/test_coord.py index b017b5e..30fde62 100755 --- a/tests/test_coord.py +++ b/tests/test_coord.py @@ -12,15 +12,15 @@ import numpy as np import pytest from astropy import units as u -from astropy.coordinates import ICRS, Angle, SkyCoord, get_moon, get_sun +from astropy.coordinates import ICRS, Angle, SkyCoord, get_body, get_sun from astropy.time import Time from astropy.units import day, deg, hour -from numpy.testing import assert_allclose from i3astropy import I3Dir with contextlib.suppress(ImportError): from icecube import astro +approx = pytest.approx def test_j2000_to_i3dir(): @@ -29,37 +29,37 @@ def test_j2000_to_i3dir(): # The first point of Ares should be grid north at noon on the vernal equinox i3fpa = SkyCoord(ra=0 * u.deg, dec=0 * u.deg, frame="icrs", obstime=obs_time).transform_to(I3Dir()) - assert_allclose(i3fpa.zen.degree, 90, atol=0.15) - assert_allclose(i3fpa.az.degree, 90, atol=0.2) + assert i3fpa.zen.degree == approx(90, abs=0.15) + assert i3fpa.az.degree == approx(90, abs=0.2) # RA = 90 should be Grid East i3ra90 = SkyCoord(ra=90 * u.deg, dec=0 * u.deg, frame="icrs", obstime=obs_time).transform_to( I3Dir(), ) - assert_allclose(i3ra90.zen.degree, 90, atol=0.003) - assert_allclose(i3ra90.az.degree, 0, atol=0.2) + assert i3ra90.zen.degree == approx(90, abs=0.003) + assert i3ra90.az.degree == approx(0, abs=0.2) # RA =180 should be grid south i3ra180 = SkyCoord(ra=180 * u.deg, dec=0 * u.deg, frame="icrs", obstime=obs_time).transform_to( I3Dir(), ) - assert_allclose(i3ra180.zen.degree, 90, atol=0.15) - assert_allclose(i3ra180.az.degree, 270, atol=0.2) + assert i3ra180.zen.degree == approx(90, abs=0.15) + assert i3ra180.az.degree == approx(270, abs=0.2) # RA =270 should be grid west i3ra270 = SkyCoord(ra=270 * u.deg, dec=0 * u.deg, frame="icrs", obstime=obs_time).transform_to( I3Dir(), ) - assert_allclose(i3ra270.zen.degree, 90, atol=0.01) - assert_allclose(i3ra270.az.degree, 180, atol=0.2) + assert i3ra270.zen.degree == approx(90, abs=0.01) + assert i3ra270.az.degree == approx(180, abs=0.2) # celestial north pole should be nadir i3np = SkyCoord(ra=0 * u.deg, dec=+90 * u.deg, frame="icrs", obstime=obs_time).transform_to(I3Dir()) - assert_allclose(i3np.zen.degree, 180, atol=0.15) + assert i3np.zen.degree == approx(180, abs=0.15) # celestial south pole should be zenith i3sp = SkyCoord(ra=0 * u.deg, dec=-90 * u.deg, frame="icrs", obstime=obs_time).transform_to(I3Dir()) - assert_allclose(i3sp.zen.degree, 0, atol=0.15) + assert i3sp.zen.degree == approx(0, abs=0.15) def test_j2000_to_i3dir_array(): @@ -72,26 +72,26 @@ def test_j2000_to_i3dir_array(): zen = [90, 90, 90, 90, 180, 0] i3dir = SkyCoord(ra=ras, dec=dec, frame="icrs", obstime=obs_time).transform_to(I3Dir()) - assert_allclose(i3dir.az.degree[:4], azi, atol=0.2) - assert_allclose(i3dir.zen.degree, zen, atol=0.2) + assert i3dir.az.degree[:4] == approx(azi, abs=0.2) + assert i3dir.zen.degree == approx(zen, abs=0.2) i3dir = ICRS(ra=ras, dec=dec).transform_to(I3Dir(obstime=obs_time)) - assert_allclose(i3dir.az.degree[:4], azi, atol=0.2) - assert_allclose(i3dir.zen.degree, zen, atol=0.2) + assert i3dir.az.degree[:4] == approx(azi, abs=0.2) + assert i3dir.zen.degree == approx(zen, abs=0.2) obs_time1 = obs_time + range(25) * hour i3dir = SkyCoord(ra=0 * deg, dec=0 * deg, obstime=obs_time1).transform_to(I3Dir()) azimuth = Angle(np.arange(90, 452, 15 + 1 / 24), unit=deg) azimuth = azimuth.wrap_at(360 * deg).degree - assert_allclose(i3dir.az.degree, azimuth, atol=0.2) - assert_allclose(i3dir.zen.degree, 90, atol=0.2) + assert i3dir.az.degree == approx(azimuth, abs=0.2) + assert i3dir.zen.degree == approx(90, abs=0.2) obs_time2 = obs_time + range(366) * day i3dir = SkyCoord(ra=0 * deg, dec=0 * deg).transform_to(I3Dir(obstime=obs_time2)) azimuth = Angle(np.linspace(90, 450, 366), unit=deg) azimuth = azimuth.wrap_at(360 * deg).degree - assert_allclose(i3dir.az.degree, azimuth, atol=0.2) - assert_allclose(i3dir.zen.degree, 90, atol=0.2) + assert i3dir.az.degree == approx(azimuth, abs=0.2) + assert i3dir.zen.degree == approx(90, abs=0.2) def test_i3dir_to_j2000(): @@ -100,78 +100,77 @@ def test_i3dir_to_j2000(): # grid east should be ra=90 grid_east = I3Dir(zen=90 * u.deg, az=0 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(grid_east.ra.degree, 90, atol=0.2) - assert_allclose(grid_east.dec.degree, 0, atol=0.01) + assert grid_east.ra.degree == approx(90, abs=0.2) + assert grid_east.dec.degree == approx(0, abs=0.01) # grid north should be Zero Point of Ares grid_north = I3Dir(zen=90 * u.deg, az=90 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(grid_north.ra.degree, 0, atol=0.2) - assert_allclose(grid_north.dec.degree, 0, atol=0.15) + assert grid_north.ra.degree == approx(0, abs=0.2) + assert grid_north.dec.degree == approx(0, abs=0.15) # grid west should be ra=270 grid_west = I3Dir(zen=90 * u.deg, az=180 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(grid_west.ra.degree, 270, atol=0.2) - assert_allclose(grid_west.dec.degree, 0, atol=0.02) + assert grid_west.ra.degree == approx(270, abs=0.2) + assert grid_west.dec.degree == approx(0, abs=0.02) # grid south should be ra=180 grid_south = I3Dir(zen=90 * u.deg, az=270 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(grid_south.ra.degree, 180, atol=0.2) - assert_allclose(grid_south.dec.degree, 0, atol=0.15) + assert grid_south.ra.degree == approx(180, abs=0.2) + assert grid_south.dec.degree == approx(0, abs=0.15) # zenith should be celestial south pole zenith = I3Dir(zen=0 * u.deg, az=0 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(zenith.dec.degree, -90, atol=0.15) + assert zenith.dec.degree == approx(-90, abs=0.15) # nadir should be celestial south pole nadir = I3Dir(zen=180 * u.deg, az=0 * u.deg, obstime=obs_time).transform_to(ICRS()) - assert_allclose(nadir.dec.degree, +90, atol=0.15) + assert nadir.dec.degree == approx(+90, abs=0.15) def test_sun(): """Conversions from the sun to I3Direction.""" # times when the Equation of time is stationary - assert_allclose(get_sun(Time("2020-04-15 12:00")).transform_to(I3Dir()).az.degree, 90, atol=0.02) - assert_allclose(get_sun(Time("2020-06-13 12:00")).transform_to(I3Dir()).az.degree, 90, atol=0.05) - assert_allclose(get_sun(Time("2020-09-01 12:00")).transform_to(I3Dir()).az.degree, 90, atol=0.04) - assert_allclose(get_sun(Time("2020-12-24 12:00")).transform_to(I3Dir()).az.degree, 90, atol=0.05) + assert get_sun(Time("2020-04-15 12:00")).transform_to(I3Dir()).az.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-06-13 12:00")).transform_to(I3Dir()).az.degree == approx(90, abs=0.05) + assert get_sun(Time("2020-09-01 12:00")).transform_to(I3Dir()).az.degree == approx(90, abs=0.04) + assert get_sun(Time("2020-12-24 12:00")).transform_to(I3Dir()).az.degree == approx(90, abs=0.05) # times when the Equation of time is maximum/minimum - assert_allclose(get_sun(Time("2020-02-11 12:14:15")).transform_to(I3Dir()).az.degree, 90, atol=0.02) - assert_allclose(get_sun(Time("2020-05-14 11:56:19")).transform_to(I3Dir()).az.degree, 90, atol=0.02) - assert_allclose(get_sun(Time("2020-07-26 12:06:36")).transform_to(I3Dir()).az.degree, 90, atol=0.02) - assert_allclose(get_sun(Time("2020-11-03 11:43:35")).transform_to(I3Dir()).az.degree, 90, atol=0.02) - - assert_allclose(get_sun(Time("2020-03-20 03:50")).transform_to(I3Dir()).zen.degree, 90, atol=0.02) - assert_allclose( - get_sun(Time("2020-06-20 21:43")).transform_to(I3Dir()).zen.degree, - 113.44, - 1, - ) - assert_allclose(get_sun(Time("2020-09-22 13:31")).transform_to(I3Dir()).zen.degree, 90, atol=0.02) - assert_allclose(get_sun(Time("2020-12-21 10:03")).transform_to(I3Dir()).zen.degree, 66.56, atol=0.02) + assert get_sun(Time("2020-02-11 12:14:15")).transform_to(I3Dir()).az.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-05-14 11:56:19")).transform_to(I3Dir()).az.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-07-26 12:06:36")).transform_to(I3Dir()).az.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-11-03 11:43:35")).transform_to(I3Dir()).az.degree == approx(90, abs=0.02) + + assert get_sun(Time("2020-03-20 03:50")).transform_to(I3Dir()).zen.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-06-20 21:43")).transform_to(I3Dir()).zen.degree == approx(113.44, abs=1) + assert get_sun(Time("2020-09-22 13:31")).transform_to(I3Dir()).zen.degree == approx(90, abs=0.02) + assert get_sun(Time("2020-12-21 10:03")).transform_to(I3Dir()).zen.degree == approx(66.56, abs=0.02) def test_sun_array(): """Conversions from the sun to I3Direction with arrays.""" - ref_time = Time("2020-03-20 0:00") + ref_time = Time("2020-01-01 12:00") day_offsets = np.arange(366) obs_time = ref_time + day_offsets * day sun1 = get_sun(obs_time).transform_to(I3Dir()) + d = 6.240_040_77 + 0.017_201_97 * (365.25 * (ref_time.ymdhms.year - 2000) + day_offsets) ref1 = I3Dir( - zen=(90 + 23.44 * np.sin(day_offsets / len(day_offsets) * 2 * np.pi)) * deg, - az=270 * deg, + zen=(90 + 23.44 * np.sin((day_offsets - 80) / len(day_offsets) * 2 * np.pi)) * deg, + az=(90 + (-7.659 * np.sin(d) + 9.863 * np.sin(2 * d + 3.5932)) / 4) * u.deg, ) - assert_allclose(sun1.zen.degree, ref1.zen.degree, rtol=0.02) - assert_allclose(sun1.az.degree, ref1.az.degree, rtol=0.02) - assert_allclose(0, sun1.separation(ref1).degree, atol=4.2) + assert sun1.zen.degree == approx(ref1.zen.degree, rel=0.02) + assert sun1.az.degree == approx(ref1.az.degree, rel=0.02) + assert sun1.separation(ref1).degree == approx(0, abs=1) + + ref_time = Time("2020-03-20 00:00") day_inc = np.linspace(0, 1, 1441) obs_time2 = ref_time + day_inc * day sun2 = get_sun(obs_time2).transform_to(I3Dir()) ref2 = I3Dir(zen=90 * deg, az=(268.2 + day_inc * 360) * deg) - assert_allclose(sun2.zen.degree, ref2.zen.degree, rtol=0.004) - assert_allclose((sun2.az - ref2.az).wrap_at(180 * deg).degree, 0, atol=0.1) - assert_allclose(0, sun2.separation(ref2).degree, atol=0.4) + assert sun2.zen.degree == approx(ref2.zen.degree, rel=0.004) + assert (sun2.az - ref2.az).wrap_at(180 * deg).degree == approx(0, abs=0.1) + assert sun2.separation(ref2).degree == approx(0, abs=0.4) @pytest.mark.skipif("astro" not in globals(), reason="Not in an icetray invironment") @@ -187,25 +186,25 @@ def test_icetray(): equa = i3dir.transform_to(ICRS()) ras, dec = astro.dir_to_equa(zen, azi, obs_time.mjd) - assert_allclose(equa.ra.radian, ras, atol=1e-3) - assert_allclose(equa.dec.radian, dec, atol=1e-5) + assert equa.ra.radian == approx(ras, abs=1e-3) + assert equa.dec.radian == approx(dec, abs=1e-5) crab = SkyCoord.from_name("Crab") i3crab = crab.transform_to(I3Dir(obstime=obs_time)) zenith, azimuth = astro.equa_to_dir(crab.ra.radian, crab.dec.radian, obs_time.mjd) - assert_allclose(zenith, i3crab.zen.radian, atol=1e-5) - assert_allclose(azimuth, i3crab.az.radian, atol=2e-5) + assert zenith == approx(i3crab.zen.radian, abs=1e-5) + assert azimuth == approx(i3crab.az.radian, abs=2e-5) i3sun = get_sun(obs_time).transform_to(I3Dir()) sun_zen, sun_azi = astro.sun_dir(obs_time.mjd) - assert_allclose(sun_zen, i3sun.zen.radian, atol=2e-5) - assert_allclose(sun_azi, i3sun.az.radian, atol=1e-4) + assert sun_zen == approx(i3sun.zen.radian, abs=2e-5) + assert sun_azi == approx(i3sun.az.radian, abs=1e-4) - i3moon = get_moon(obs_time).transform_to(I3Dir()) + i3moon = get_body("moon", obs_time).transform_to(I3Dir()) moon_zen, moon_azi = astro.moon_dir(obs_time.mjd) - assert_allclose(moon_zen, i3moon.zen.radian, atol=1e-4) - assert_allclose(moon_azi, i3moon.az.radian, atol=1e-4) + assert moon_zen == approx(i3moon.zen.radian, abs=1e-4) + assert moon_azi == approx(i3moon.az.radian, abs=1e-4) if __name__ == "__main__": - pytest.main(["-v", __file__, *sys.argv]) + sys.exit(pytest.main(["-v", __file__, *sys.argv[1:]])) diff --git a/tests/test_time.py b/tests/test_time.py index 9990442..0d61e89 100755 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -14,13 +14,13 @@ from astropy import units as u from astropy.time import Time from astropy.time.core import ScaleValueError -from numpy.testing import assert_allclose, assert_equal import i3astropy # noqa: F401 pylint: disable=W0611 with contextlib.suppress(ImportError): from icecube.dataclasses import I3Time # pylint: disable=E0611 +approx = pytest.approx DAQ_MS = int(1e7) DAQ_SEC = int(1e10) DAQ_MIN = 60 * DAQ_SEC @@ -62,39 +62,41 @@ ] -def test_times(subtests): +@pytest.mark.parametrize(("daq_year", "daq_time", "iso"), times) +def test_times(daq_year, daq_time, iso): """Test I3Times match a list of iso times.""" - for daq_year, daq_time, iso in times: - with subtests.test(daq_year=daq_year, daq_time=daq_time, iso=iso): - i3t = Time(daq_year, daq_time, format="i3time") - isot = Time(iso, format="iso", scale="utc") - assert i3t.scale == "utc" - assert np.all(i3t.isclose(isot)) - assert i3t.i3time.year == daq_year - assert i3t.i3time.daq_time == daq_time - assert isot.i3time.year == daq_year - assert isot.i3time.daq_time == daq_time - - delta = np.arange(0, 1000) - ddaq = daq_time + delta - assert_equal((isot + delta * 1e-10 * u.s).i3time.daq_time, ddaq) - - a, b = np.divmod(ddaq, 1000) - t = Time(daq_year, a * 1000, format="i3time") + b * 1e-10 * u.s - assert_equal(t.i3time.daq_time, ddaq) - - # allclose make the same assumptions about floating point that astropy makes - assert_allclose(Time(daq_year, ddaq, format="i3time").i3time.daq_time, ddaq) + i3t = Time(daq_year, daq_time, format="i3time") + isot = Time(iso, format="iso", scale="utc") + assert i3t.scale == "utc" + assert np.all(i3t.isclose(isot)) + assert i3t.i3time.year == daq_year + assert i3t.i3time.daq_time == daq_time + assert isot.i3time.year == daq_year + assert isot.i3time.daq_time == daq_time + + delta = np.arange(0, 1000) + ddaq = daq_time + delta + assert (isot + delta * 1e-10 * u.s).i3time.daq_time == approx(ddaq) + + a, b = np.divmod(ddaq, 1000) + t = Time(daq_year, a * 1000, format="i3time") + b * 1e-10 * u.s + assert t.i3time.daq_time == approx(ddaq) + + # make the same assumptions about floating point that astropy makes + assert Time(daq_year, ddaq, format="i3time").i3time.daq_time == approx(ddaq) + +def test_times_array(): + """Test I3Times match a list of iso times.""" years, daqtime, iso = zip(*times) i3t = Time(years, daqtime, format="i3time") isot = Time(iso, format="iso") assert np.all(i3t.isclose(isot)) - assert_equal(i3t.i3time.year, years) - assert_equal(i3t.i3time.daq_time, daqtime) - assert_equal(isot.i3time.year, years) - assert_equal(isot.i3time.daq_time, daqtime) + assert i3t.i3time.year == approx(years) + assert i3t.i3time.daq_time == approx(daqtime) + assert isot.i3time.year == approx(years) + assert isot.i3time.daq_time == approx(daqtime) with pytest.raises(ScaleValueError): Time(2020, 0, format="i3time", scale="tt") @@ -103,17 +105,16 @@ def test_times(subtests): @pytest.mark.skipif("I3Time" not in globals(), reason="Not in an icetray invironment") -def test_icetray(subtests): +@pytest.mark.parametrize(("daq_year", "daq_time", "iso"), times) +def test_icetray(daq_year, daq_time, iso): """Test i3astropy.I3Time matches dataclasses.I3Time.""" - for daq_year, daq_time, iso in times: - with subtests.test(daq_year=daq_year, daq_time=daq_time, iso=iso): - di3t = I3Time(daq_year, daq_time) - ai3t = Time(daq_year, daq_time, format="i3time") - isot = Time(iso, format="iso") - assert str(di3t)[: len(iso)] == iso - assert_allclose(di3t.mod_julian_day_double, ai3t.mjd, rtol=1e-9) - assert_allclose(di3t.mod_julian_day_double, isot.mjd, rtol=1e-9) + di3t = I3Time(daq_year, daq_time) + ai3t = Time(daq_year, daq_time, format="i3time") + isot = Time(iso, format="iso") + assert str(di3t)[: len(iso)] == iso + assert di3t.mod_julian_day_double == approx(ai3t.mjd, rel=1e-9) + assert di3t.mod_julian_day_double == approx(isot.mjd, rel=1e-9) if __name__ == "__main__": - pytest.main(["-v", __file__, *sys.argv]) + sys.exit(pytest.main(["-v", __file__, *sys.argv[1:]]))