Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 28 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,8 @@ jobs:
python-version: ["3.11", "3.13"]
runs-on: [ubuntu-latest, macos-latest, windows-latest]

# TODO: figure out OpenBLAS install
# ``ERROR: Dependency "OpenBLAS" not found, tried pkgconfig and cmake``
# include:
# - python-version: pypy-3.11
# runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down Expand Up @@ -113,8 +105,6 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand All @@ -139,3 +129,31 @@ jobs:
uses: codecov/codecov-action@v5.3.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

check_figures:
name: Check Figures
runs-on: ${{ matrix.runs-on }}
needs: [smoke]
strategy:
fail-fast: false
matrix:
python-version: ["3.12"]
runs-on: [ubuntu-latest]

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install the project
run: uv sync --extra all --group test-all

- name: Test package
run: uv run nox -s test_mpl
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Project files
/tests/mpl_figure/baseline

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
32 changes: 24 additions & 8 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
"""Doctest configuration."""

from collections.abc import Callable, Iterable, Sequence
from doctest import ELLIPSIS, NORMALIZE_WHITESPACE

from sybil import Sybil
from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser, SkipParser
from sybil import Document, Region, Sybil
from sybil.parsers import myst, rest
from sybil.sybil import SybilCollection

from optional_dependencies import OptionalDependencyEnum, auto
from optional_dependencies.utils import chain_checks, get_version, is_installed

pytest_collect_file = Sybil(
optionflags = ELLIPSIS | NORMALIZE_WHITESPACE

parsers: Sequence[Callable[[Document], Iterable[Region]]] = [
myst.DocTestDirectiveParser(optionflags=optionflags),
myst.PythonCodeBlockParser(doctest_optionflags=optionflags),
myst.SkipParser(),
]

readme = Sybil(parsers=parsers, patterns=["*.md"])
docs = Sybil(
parsers=[
DocTestParser(optionflags=NORMALIZE_WHITESPACE | ELLIPSIS),
PythonCodeBlockParser(),
SkipParser(),
rest.DocTestParser(optionflags=NORMALIZE_WHITESPACE | ELLIPSIS),
rest.PythonCodeBlockParser(),
rest.SkipParser(),
],
patterns=["*.rst", "*.py"],
).pytest()
patterns=["*.rst"],
)
python = Sybil(
parsers=[*parsers, rest.DocTestParser(optionflags=optionflags)], patterns=["*.py"]
)

pytest_collect_file = SybilCollection((readme, docs, python)).pytest()


class OptDeps(OptionalDependencyEnum):
Expand Down
66 changes: 52 additions & 14 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
nox.options.sessions = ["lint", "tests", "doctests"]
nox.options.default_venv_backend = "uv|virtualenv"

# ===================================================================
# Linting


@nox.session
def lint(session: nox.Session) -> None:
Expand All @@ -36,38 +39,73 @@ def pylint(session: nox.Session) -> None:
session.run("pylint", "galax", *session.posargs)


# ===================================================================
# Testing


@nox.session
def tests(session: nox.Session) -> None:
"""Run the unit and regular tests."""
def tests_standard(session: nox.Session) -> None:
"""Run the regular tests: src, README, docs, tests/unit."""
session.install("-e", ".[test]")
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" # TODO: set in a better way
session.run("pytest", *session.posargs)
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1"
session.run("pytest", "src", "README", "docs", "tests/unit", *session.posargs)


@nox.session
def tests_all(session: nox.Session) -> None:
"""Run the unit and regular tests."""
"""Run all the tests."""
session.install("-e", ".[test-all]")
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" # TODO: set in a better way
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1"
session.run("pytest", *session.posargs)


@nox.session
def doctests(session: nox.Session) -> None:
"""Run the regular tests and doctests."""
session.install(".[test]")
"""Run the doctests: README, docs, src -- including mpl tests."""
session.install(".[test,test-mpl]")
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1"
session.run(
"pytest",
*("README", "docs", "src/galax"),
"--mpl",
*session.posargs,
)


@nox.session
def generate_mpl_tests(session: nox.Session) -> None:
"""Generate the mpl tests."""
session.install(".[test,test-mpl]")
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1"
session.run(
"pytest",
"tests",
"-m mpl_image_compare", # only run the mpl tests
"--mpl-generate-hash-library=tests/mpl_figure/hashes.json",
"--mpl-generate-path=tests/mpl_figure/baseline",
"--mpl-generate-summary=html,json,basic-json",
*session.posargs,
)


@nox.session
def test_mpl(session: nox.Session) -> None:
"""Test the figures."""
session.install(".[test,test-mpl]")
os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1"
session.run(
"pytest",
"--doctest-modules",
'--doctest-glob="*.rst"',
'--doctest-glob="*.md"',
'--doctest-glob="*.py"',
"docs",
"src/galax",
"tests",
"--mpl",
"-m mpl_image_compare", # only run the mpl tests
"--mpl-generate-summary=basic-html,json",
*session.posargs,
)


# ===================================================================


@nox.session(reuse_venv=True)
def docs(session: nox.Session) -> None:
"""Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links."""
Expand Down
15 changes: 13 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,17 @@
]
test = [
"nox>=2024.10.9",
"optype[numpy]>=0.9.0",
"pre-commit>=4.0.1",
"pytest-arraydiff>=0.6.1",
"pytest-cov>=5",
"pytest>=8.3",
"sybil>=8.0.0",
]
test-mpl = ["pytest-mpl>=0.17.0"]
]
test-mpl = [
"pytest-mpl>=0.17.0",
"matplotlib==3.9.2",
]


[tool.hatch]
Expand Down Expand Up @@ -229,6 +233,13 @@
testpaths = ["docs", "src/galax", "tests/"]
xfail_strict = true

# pytest-mpl settings
mpl-use-full-test-name = true
mpl-hash-library = "tests/mpl_figure/hashes.json"
mpl-baseline-path = "https://raw.githubusercontent.com/GalacticDynamics/galax-figure-tests/main/figures/"
mpl-deterministic = true



[tool.ruff]

Expand Down
Binary file not shown.
86 changes: 86 additions & 0 deletions tests/integration/matplotlib/test_dynamicssolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Test the `galax.dynamics.orbit` package contents."""

from itertools import combinations
from typing import Literal, TypeAlias

import matplotlib.pyplot as plt
import numpy as np
import optype.numpy as onp
import pytest
from matplotlib.axes import Axes
from matplotlib.figure import Figure

import quaxed.numpy as jnp
import unxt as u

import galax.coordinates as gc
import galax.dynamics as gd
import galax.potential as gp


@pytest.fixture
def potential() -> gp.KeplerPotential:
"""Kepler potential fixture."""
return gp.KeplerPotential(m_tot=u.Quantity(1e12, "Msun"), units="galactic")


@pytest.fixture
def field(potential: gp.KeplerPotential) -> gd.fields.HamiltonianField:
"""Hamiltonian field fixture."""
return gd.fields.HamiltonianField(potential)


@pytest.fixture
def solver() -> gd.DynamicsSolver:
"""Dynamics solver fixture."""
return gd.DynamicsSolver()


FigAx23: TypeAlias = tuple[Figure, onp.Array[tuple[Literal[2], Literal[3]], Axes]]


@pytest.fixture
def sixaxfig() -> FigAx23:
"""Six axis figure fixture."""
fig, axs = plt.subplots(2, 3, figsize=(12, 8))
return fig, axs


# =============================================================================


@pytest.mark.mpl_image_compare
def test_solution_plot(
solver: gd.DynamicsSolver, field: gd.fields.HamiltonianField, sixaxfig: FigAx23
) -> Figure:
"""Test plotting an orbit in a Kepler potential."""
# Solve the dynamical system
w0 = gc.PhaseSpacePosition(
q=u.Quantity([8.0, 0.0, 0.5], "kpc"),
p=u.Quantity([0.0, 220.0, 0.0], "km/s"),
t=u.Quantity(0.0, "Gyr"),
)
tf = u.Quantity(200, "Myr")
saveat = jnp.linspace(w0.t, tf, 1000)
soln = solver.solve(field, w0, tf, saveat=saveat)
q, p = soln.ys

# Plot the solution
fig, axs = sixaxfig
fig.suptitle("Phase space solution")

usys = field.units
labels = np.empty((2, 3), dtype="<U17")
labels[0, :] = [f"{x} [{usys['length']}]" for x in ["x", "y", "z"]]
labels[1, :] = [f"{v} [{usys['speed']}]" for v in [r"$v_x$", r"$v_y$", r"$v_z$"]]

for ax, (i, j) in zip(axs[0, :], combinations(range(3), 2), strict=True):
ax.plot(q[..., i], q[..., j])
ax.set(xlabel=labels[0, i], ylabel=labels[0, j])
for ax, (i, j) in zip(axs[1, :], combinations(range(3), 2), strict=True):
ax.plot(p[..., i], p[..., j])
ax.set(xlabel=labels[1, i], ylabel=labels[1, j])

fig.tight_layout()

return fig
8 changes: 4 additions & 4 deletions tests/integration/matplotlib/test_orbit.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,31 @@ def orbit(potential: gp.AbstractPotential, w0: gc.PhaseSpacePosition) -> gd.Orbi
# =============================================================================


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_orbit_plot(orbit: gd.Orbit) -> Figure:
"""Test plotting an orbit in a Kepler potential."""
ax = orbit.plot(x="x", y="y")

return ax.figure


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_orbit_plot_represent_as(orbit: gd.Orbit) -> Figure:
"""Test plotting an orbit in a Kepler potential."""
ax = orbit.plot(x="rho", y="d_z", vector_representation=cx.vecs.CylindricalPos)

return ax.figure


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_orbit_plot_scatter(orbit: gd.Orbit) -> Figure:
"""Test plotting an orbit in a Kepler potential."""
ax = orbit.plot(x="x", y="y", plot_function="scatter")

return ax.figure


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_orbit_plot_time_color(orbit: gd.Orbit) -> Figure:
"""Test plotting an orbit in a Kepler potential."""
ax = orbit.plot(x="x", y="y", plot_function="scatter", c="orbit.t")
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/matplotlib/test_potential.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import galax.potential as gp


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_kepler_potential_contours() -> Figure:
"""Test plotting Kepler potential contours."""
pot = gp.KeplerPotential(
Expand All @@ -24,7 +24,7 @@ def test_kepler_potential_contours() -> Figure:
return fig


@pytest.mark.mpl_image_compare(deterministic=True)
@pytest.mark.mpl_image_compare
def test_kernel_density_contours() -> Figure:
"""Test plotting kernel density contours."""
pot = gp.KeplerPotential(
Expand Down
9 changes: 9 additions & 0 deletions tests/mpl_figure/hashes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"integration.matplotlib.test_dynamicssolver.test_solution_plot": "cf2546a91582065e84f8e5494b31eff6219533bf7ce66000f835c5aaf9d6cc89",
"integration.matplotlib.test_orbit.test_orbit_plot": "4766db5ca76f7c347d4811587e3798f42eec1ae9234832459478d54e8563dbb0",
"integration.matplotlib.test_orbit.test_orbit_plot_represent_as": "83ca861ec069a9ddda18b09b469ed27c3b953dc87184116e17e2cb1f1c829c51",
"integration.matplotlib.test_orbit.test_orbit_plot_scatter": "e7f337382dada6558022a0888f2f741c90e74a6531ececa293e7754df273a4ee",
"integration.matplotlib.test_orbit.test_orbit_plot_time_color": "3a12252a7e2b676d8c3427b36b6d8c5753023e3a0b416b0d7433a9e7dcd91c92",
"integration.matplotlib.test_potential.test_kepler_potential_contours": "f21ac32338c8120f0f8851f1fef3c516bc906bb21452539868361cf86833e561",
"integration.matplotlib.test_potential.test_kernel_density_contours": "51f9e6ca266b063efdd9fd6914b09d689b9ff4be55554a5b0a981e54b972e8d3"
}
Loading
Loading