5 pipeline with unit tests (#10)
CI / lint (push) Successful in 12s
CI / test (push) Successful in 1m30s

---------

Co-authored-by: Julian Piribauer <julian.piribauer@gmail.com>
Co-authored-by: julian <julian.piribauer@gmail.com>
Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-07-29 20:01:57 +02:00
co-authored by Julian Piribauer
parent bb830640fc
commit c6a2926ad6
8 changed files with 250 additions and 5 deletions
+17
View File
@@ -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
+26
View File
@@ -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
+37
View File
@@ -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/
+10
View File
@@ -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/
+17
View File
@@ -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"]
+4 -4
View File
@@ -2,7 +2,6 @@ import numpy as np
import logging import logging
import os import os
import json import json
from datetime import datetime, timezone
import re import re
# Logger # Logger
@@ -102,6 +101,7 @@ class ToricPolytope(Polytope):
if no_triangulation < 0 or no_triangulation >= len(self.triangulations): if no_triangulation < 0 or no_triangulation >= len(self.triangulations):
raise IndexError("Invalid triangulation index {} (available: 0..{}).".format(no_triangulation, len(self.triangulations) - 1)) raise IndexError("Invalid triangulation index {} (available: 0..{}).".format(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) logger.info("Using triangulation index %d of [0..%d].", no_triangulation, len(self.triangulations) - 1)
self.triangulation = self.triangulations[no_triangulation] self.triangulation = self.triangulations[no_triangulation]
@@ -233,7 +233,7 @@ class ToricPolytope(Polytope):
def _setup_discriminant_symbols(self): def _setup_discriminant_symbols(self):
mori_rays = self.Mori_cone.rays() 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 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 lambda_vars = var('l', n=len(mori_rays)+1, latex_name='l') # l[0] is superfluous
a_row = matrix(a_vars) a_row = matrix(a_vars)
@@ -275,7 +275,7 @@ class ToricPolytope(Polytope):
if polynomials[0] == 0: if polynomials[0] == 0:
polynomials = maxima.eliminate(equation_system, lambda_symbols[:-1]).sage() polynomials = maxima.eliminate(equation_system, lambda_symbols[:-1]).sage()
reverse_solve = True reverse_solve = True
except: except Exception:
# In one-parameter cases there may be nothing to eliminate. # In one-parameter cases there may be nothing to eliminate.
polynomials = equation_system polynomials = equation_system
logger.debug("No elimination needed for the equation system: %s", equation_system) logger.debug("No elimination needed for the equation system: %s", equation_system)
@@ -289,7 +289,7 @@ class ToricPolytope(Polytope):
else: else:
last_lambda = lambda_symbols[-1] last_lambda = lambda_symbols[-1]
polynomials = [poly / last_lambda ** (poly.degree(last_lambda)) for poly in polynomials] polynomials = [poly / last_lambda ** (poly.degree(last_lambda)) for poly in polynomials]
except: except Exception:
pass pass
return polynomials return polynomials
+104
View File
@@ -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},
),
]
+34
View File
@@ -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