From c6a2926ad6fd25f994ea7816872150b44e8955de Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Jul 2026 20:01:57 +0200 Subject: [PATCH] 5 pipeline with unit tests (#10) --------- Co-authored-by: Julian Piribauer Co-authored-by: julian Reviewed-on: https://gitea.piribauer.ch/julian/Calabi-Yau-Period-Geometry/pulls/10 --- .gitea/ci-image/Dockerfile | 17 ++++++ .gitea/preparse.sh | 26 +++++++++ .gitea/workflows/ci.yml | 37 ++++++++++++ .gitignore | 10 ++++ pyproject.toml | 17 ++++++ sage/toric_topdata.sage | 10 ++-- tests/test_models.sage | 104 +++++++++++++++++++++++++++++++++ tests/test_topdata_and_disc.py | 34 +++++++++++ 8 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 .gitea/ci-image/Dockerfile create mode 100755 .gitea/preparse.sh create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 tests/test_models.sage create mode 100644 tests/test_topdata_and_disc.py diff --git a/.gitea/ci-image/Dockerfile b/.gitea/ci-image/Dockerfile new file mode 100644 index 0000000..8ddd6d3 --- /dev/null +++ b/.gitea/ci-image/Dockerfile @@ -0,0 +1,17 @@ +# Custom CI image for the arm64 (Raspberry Pi) Gitea Actions runner, +# since the official sagemath/sagemath image is amd64-only. +# +# Build & push commands: +# docker build -t gitea.piribauer.ch/julian/sage-ci:latest -f .gitea/ci-image/Dockerfile . +# docker login gitea.piribauer.ch -u julian +# docker push gitea.piribauer.ch/julian/sage-ci:latest + +FROM condaforge/miniforge3:latest + +RUN mamba install -y -c conda-forge sage ruff pytest \ + && mamba clean -afy + +RUN mamba install -y -c conda-forge nodejs \ + && mamba clean -afy + +WORKDIR /workspace diff --git a/.gitea/preparse.sh b/.gitea/preparse.sh new file mode 100755 index 0000000..17687c4 --- /dev/null +++ b/.gitea/preparse.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# This script is run in the CI container to preparse all .sage files into .sage.py files +# and can also be run locally for local linting/testing. + +set -e + +if command -v python >/dev/null 2>&1 && python -c "import sage.repl" >/dev/null 2>&1; then + run_python() { python "$@"; } +else + run_python() { sage --python "$@"; } +fi + +for f in $(find . -name "*.sage" -not -path "./playground/*"); do + run_python -c " +import sys +from sage.repl.preparse import preparse_file + +path = sys.argv[1] +with open(path) as fh: + src = fh.read() +with open(path + '.py', 'w') as fh: + fh.write('from sage.all import * # noqa: F401,F403\n') + fh.write(preparse_file(src)) +" "$f" +done diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..1e280d9 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + lint: + runs-on: ubuntu-latest + # Custom image (see .gitea/ci-image/Dockerfile) + container: + image: gitea.piribauer.ch/julian/sage-ci:latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Preparse .sage files + run: .gitea/preparse.sh + + - name: ruff check + run: python -m ruff check --no-respect-gitignore . + + test: + runs-on: ubuntu-latest + needs: lint + container: + image: gitea.piribauer.ch/julian/sage-ci:latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run tests + run: python -m pytest tests/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05fb66f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Sage preparser output (regenerated from .sage sources, not hand-maintained) +*.sage.py + +# Python / tooling caches +__pycache__/ +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ + +.venv/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4b852c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +# E: pycodestyle errors, F: pyflakes (undefined/unused names), I: isort (import order), W: pycodestyle warnings +select = ["E", "F", "I", "W"] +ignore = [ + "E501", # symbolic-math expressions routinely exceed a "normal" line length +] + +[tool.ruff.lint.per-file-ignores] +# *.sage.py is preparser output: star-import globals, semicolon preamble, and literal-substitution artifacts trip static analysis. +"*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"] + +# test_smoke.py star-imports sage.all and loads a .sage file, so ruff can't see where its names come from. +"tests/test_topdata_and_disc.py" = ["F403", "F405"] diff --git a/sage/toric_topdata.sage b/sage/toric_topdata.sage index a77ca66..597a1bd 100644 --- a/sage/toric_topdata.sage +++ b/sage/toric_topdata.sage @@ -2,7 +2,6 @@ import numpy as np import logging import os import json -from datetime import datetime, timezone import re # Logger @@ -102,7 +101,8 @@ class ToricPolytope(Polytope): if no_triangulation < 0 or no_triangulation >= len(self.triangulations): raise IndexError("Invalid triangulation index {} (available: 0..{}).".format(no_triangulation, len(self.triangulations) - 1)) - logger.info("Using triangulation index %d of [0..%d].", no_triangulation, len(self.triangulations) - 1) + if len(self.triangulations) > 1: + logger.info("Using triangulation index %d of [0..%d].", no_triangulation, len(self.triangulations) - 1) self.triangulation = self.triangulations[no_triangulation] def set_nef_partition(self, nef_partition): @@ -233,7 +233,7 @@ class ToricPolytope(Polytope): def _setup_discriminant_symbols(self): mori_rays = self.Mori_cone.rays() - a_vars = [var("a_{}".format(u), latex_name="a_{{}}".format(u)) for u in (1..len(mori_rays))] + a_vars = [var("a_{}".format(u), latex_name="a_{{{}}}".format(u)) for u in (1..len(mori_rays))] z_vars = var('z', n=len(mori_rays)+1, latex_name='z') # z[0] is superfluous lambda_vars = var('l', n=len(mori_rays)+1, latex_name='l') # l[0] is superfluous a_row = matrix(a_vars) @@ -275,7 +275,7 @@ class ToricPolytope(Polytope): if polynomials[0] == 0: polynomials = maxima.eliminate(equation_system, lambda_symbols[:-1]).sage() reverse_solve = True - except: + except Exception: # In one-parameter cases there may be nothing to eliminate. polynomials = equation_system logger.debug("No elimination needed for the equation system: %s", equation_system) @@ -289,7 +289,7 @@ class ToricPolytope(Polytope): else: last_lambda = lambda_symbols[-1] polynomials = [poly / last_lambda ** (poly.degree(last_lambda)) for poly in polynomials] - except: + except Exception: pass return polynomials diff --git a/tests/test_models.sage b/tests/test_models.sage new file mode 100644 index 0000000..d69e8c1 --- /dev/null +++ b/tests/test_models.sage @@ -0,0 +1,104 @@ +# Entries are (id, factory, expected_cy_dimension, expected_no_divs, expected_disc, +# expected_intersection_numbers_CY), where expected_disc is a list of +# (str(discriminant_factor), codimension) pairs as returned by ToricPolytope.disc(). + +TEST_MODELS = [ + ( + "K3_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 2, 4], model_name="K3_two_parameter_family"), + 2, # cy_dimension + 2, # no_divs + [("z2 - 1/4", 2), ("4096*z1^2*(4*z2 - 1) + 128*z1 - 1", 0)], + {(0, 0): 4, (0, 1): 2, (1, 1): 0}, + ), + ( + "CY3_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 2, 2, 2], model_name="CY3_two_parameter_family"), + 3, + 2, + [("z2 - 1/4", 3), ("65536*z1^2*(4*z2 - 1) + 512*z1 - 1", 0)], + {(0, 0, 0): 8, (0, 0, 1): 4, (0, 1, 1): 0, (1, 1, 1): 0}, + ), + ( + "CY4_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 1, 1, 8, 12], model_name="CY4_two_parameter_family"), + 4, + 2, + [ + ("z2 - 1/256", 2), + ("34828517376*z1^4*(256*z2 - 1) + 322486272*z1^3 - 1119744*z1^2 + 1728*z1 - 1", 0), + ], + {(0, 0, 0, 0): 64, (0, 0, 0, 1): 16, (0, 0, 1, 1): 4, (0, 1, 1, 1): 1, (1, 1, 1, 1): 0}, + ), + ( + "CICY3_one_parameter", + lambda: ToricPolytopeCICY([[3, 3]], model_name="CICY3_one_parameter"), + 3, + 1, + [("z1 - 1/46656", 0)], + {(0, 0, 0): 9}, + ), + ( + "CICY3_two_parameter", + lambda: ToricPolytopeCICY([[3], [3]], model_name="CICY3_two_parameter"), + 3, + 2, + [ + ( + "-19683*z1^3 - 2187*(27*z1 + 1)*z2^2 - 19683*z2^3 - 2187*z1^2 " + "- 81*(729*z1^2 - 189*z1 + 1)*z2 - 81*z1 - 1", + 0, + ) + ], + {(0, 0, 0): 0, (0, 0, 1): 3, (0, 1, 1): 3, (1, 1, 1): 0}, + ), + ( + "CICY5_two_parameter", + lambda: ToricPolytopeCICY([[6, 1], [0, 2]], model_name="CICY5_two_parameter"), + 5, + 2, + [ + ( + "16384*z2^7 - 28672*z2^6 + 21504*z2^5 - 448*(1647086*z1 - 5)*z2^3 - 8960*z2^4 " + "- 112*(8235430*z1 + 3)*z2^2 - 678223072849*z1^2 - 28*(4941258*z1 - 1)*z2 - 1647086*z1 - 1", + 0, + ) + ], + { + (0, 0, 0, 0, 0): 6, + (0, 0, 0, 0, 1): 12, + (0, 0, 0, 1, 1): 0, + (0, 0, 1, 1, 1): 0, + (0, 1, 1, 1, 1): 0, + (1, 1, 1, 1, 1): 0, + }, + ), + ( + "CICY3_two_parameter_manual", + lambda: ToricPolytope( + [ + [1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [-1, -1, 0, 0, 0, 0], + [0, 0, -1, -1, -1, -1], + ], + nef_partition=[[0, 1, 2, 3], [4, 5, 6, 7]], + model_name="CICY3_two_parameter_manual", + ), + 4, + 2, + [ + ( + "-14348907*z1^5 - 2657205*z1^4 - 196830*z1^3 - 29296875*(270*z1 + 1)*z2^2 " + "- 30517578125*z2^3 - 7290*z1^2 + 9375*(98415*z1^3 - 32805*z1^2 + 810*z1 - 1)*z2 " + "- 135*z1 - 1", + 0, + ) + ], + {(0, 0, 0, 0): 0, (0, 0, 0, 1): 0, (0, 0, 1, 1): 6, (0, 1, 1, 1): 8, (1, 1, 1, 1): 2}, + ), +] diff --git a/tests/test_topdata_and_disc.py b/tests/test_topdata_and_disc.py new file mode 100644 index 0000000..f85eb59 --- /dev/null +++ b/tests/test_topdata_and_disc.py @@ -0,0 +1,34 @@ +import os + +import pytest +from sage.all import * # noqa: F401 +from sage.geometry.lattice_polytope import set_palp_dimension + +load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage")) +load(os.path.join(os.path.dirname(__file__), "test_models.sage")) + +set_palp_dimension(11) + +TEST_MODELS_PARAMETRISED = [pytest.param(*row[1:], id=row[0]) for row in TEST_MODELS] + + +@pytest.mark.parametrize( + "make_model, expected_cy_dimension, expected_no_divs, expected_disc, expected_intersection_numbers_CY", + TEST_MODELS_PARAMETRISED, +) +def test_topdata_and_disc( + make_model, + expected_cy_dimension, + expected_no_divs, + expected_disc, + expected_intersection_numbers_CY, +): + polytope = make_model() + + discriminants = polytope.disc() + assert [(str(factor), codim) for factor, codim in discriminants] == expected_disc + + polytope.topdata() + assert polytope.cy_dimension == expected_cy_dimension + assert polytope.no_divs == expected_no_divs + assert polytope.intersection_numbers_CY == expected_intersection_numbers_CY