Compare commits
7
Commits
master
...
3cc7bc0dda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cc7bc0dda | ||
|
|
53a1eb6ca7 | ||
|
|
80072b291f | ||
|
|
d649d5047f | ||
|
|
3e6660a414 | ||
|
|
1f930b01fc | ||
|
|
ece770ab48 |
@@ -0,0 +1,27 @@
|
|||||||
|
# Custom CI image for the arm64 (Raspberry Pi) Gitea Actions runner.
|
||||||
|
#
|
||||||
|
# The official sagemath/sagemath image is amd64-only; conda-forge's `sage`
|
||||||
|
# package is the one place SageMath is actually published for linux-aarch64,
|
||||||
|
# so this bakes it (plus the lint/test tools) into a single image, built and
|
||||||
|
# pushed once rather than reinstalled on every CI run.
|
||||||
|
#
|
||||||
|
# Build & push (run directly on the Pi, or any arm64 machine with Docker):
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# Rebuild and re-push whenever this Dockerfile changes (e.g. bumping the sage
|
||||||
|
# version) or the pinned tool versions need updating.
|
||||||
|
|
||||||
|
FROM condaforge/miniforge3:latest
|
||||||
|
|
||||||
|
RUN mamba install -y -c conda-forge sage ruff pytest \
|
||||||
|
&& mamba clean -afy
|
||||||
|
|
||||||
|
# actions/checkout@v4 (and other JS-based actions) run via `node` inside this
|
||||||
|
# container -- act_runner doesn't inject a runtime of its own when a custom
|
||||||
|
# `container:` image is set, so one has to be present here.
|
||||||
|
RUN mamba install -y -c conda-forge nodejs \
|
||||||
|
&& mamba clean -afy
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Reproduces the CI "Preparse .sage files" step locally, so `ruff check` sees
|
||||||
|
# the same generated *.sage.py files that the pipeline lints. Run this before
|
||||||
|
# `ruff check --no-respect-gitignore .` to catch issues that only show up in
|
||||||
|
# the generated output.
|
||||||
|
#
|
||||||
|
# Uses the sage.repl.preparse.preparse_file() Python API directly rather than
|
||||||
|
# the `sage --preparse` CLI flag: the conda-forge `sage` binary used in CI is
|
||||||
|
# a cut-down entry point that doesn't support `--preparse` (or `--python`) at
|
||||||
|
# all, unlike the official sagemath CLI. Whichever `python` on PATH can
|
||||||
|
# actually `import sage.repl` is used to run it -- in CI that's the conda
|
||||||
|
# env's own `python`; locally (official sage install) it's `sage --python`.
|
||||||
|
#
|
||||||
|
# The generated files are gitignored; clean them up afterwards with:
|
||||||
|
# git clean -x sage/ playground/
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Custom image (see .gitea/ci-image/Dockerfile): the official
|
||||||
|
# sagemath/sagemath image is amd64-only and this runner is arm64, and
|
||||||
|
# ruff/pytest are baked in here so jobs don't reinstall them every run.
|
||||||
|
container:
|
||||||
|
image: gitea.piribauer.ch/julian/sage-ci:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Preparse .sage files
|
||||||
|
# Turns each *.sage file into real Python (*.sage.py) so ruff can
|
||||||
|
# parse it, and prepends the `from sage.all import *` that `sage`
|
||||||
|
# normally injects at runtime, so Sage's globals (ZZ, var, matrix, ...)
|
||||||
|
# resolve instead of looking like undefined names.
|
||||||
|
run: .gitea/preparse.sh
|
||||||
|
|
||||||
|
- name: ruff check
|
||||||
|
# --no-respect-gitignore: *.sage.py is gitignored (it's generated, see
|
||||||
|
# the previous step) but that's exactly what we need to lint here.
|
||||||
|
# `python` here is the conda env's own interpreter -- it already has
|
||||||
|
# sage/ruff/pytest importable, `sage --python` isn't a real flag on
|
||||||
|
# the conda-forge sage CLI.
|
||||||
|
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
@@ -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/
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
target-version = "py311"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
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 generated by `sage --preparse` from *.sage sources.
|
||||||
|
# - F403/F405/F821/E741: Sage's runtime injects hundreds of globals (ZZ, var,
|
||||||
|
# matrix, LatticePolytope, ...) via `from sage.all import *` before
|
||||||
|
# executing these files, so plain static analysis can't see where names
|
||||||
|
# come from.
|
||||||
|
# - E402/E702/I001: Sage's preparser itself emits an import-then-semicolon-
|
||||||
|
# joined-constants preamble (e.g. `_sage_const_1 = Integer(1); ...`) ahead
|
||||||
|
# of the file's own imports; that's Sage's boilerplate, not this project's
|
||||||
|
# code style.
|
||||||
|
# - W291/W293: the preparser replaces every integer literal with
|
||||||
|
# `_sage_const_N ` (trailing space included) to preserve token boundaries,
|
||||||
|
# so trailing-whitespace warnings fire mechanically on almost every line
|
||||||
|
# with a number in it -- not something `ruff format` could fix anyway,
|
||||||
|
# since it wouldn't change the .sage source that generated it.
|
||||||
|
# - W292: whether the reconstructed file ends in a real trailing newline
|
||||||
|
# depends on how many blank lines the preparser appends, which varies by
|
||||||
|
# Sage version -- not something worth pinning tool versions over.
|
||||||
|
"*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"]
|
||||||
|
|
||||||
|
# tests/test_smoke.py does `from sage.all import *` and `load(...)` a .sage
|
||||||
|
# file to get at its classes -- the same dynamic-namespace situation as
|
||||||
|
# *.sage.py above, just in a hand-written file: ruff can't see that
|
||||||
|
# Polytope/ToricPolytope/etc. come from the loaded file.
|
||||||
|
"tests/test_smoke.py" = ["F403", "F405"]
|
||||||
@@ -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
|
||||||
@@ -233,7 +232,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 +274,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 +288,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
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sage.all import * # noqa: F401,F403
|
||||||
|
|
||||||
|
load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_polytope_rejects_too_few_points():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Polytope([[0, 0], [1, 0]]) # 2 points in dimension 2: not enough
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_quintic_topdata():
|
||||||
|
# Vertices e_1, ..., e_4, -e_1-...-e_4 -- the worked example from
|
||||||
|
# ToricPolytope's own docstring.
|
||||||
|
points = [
|
||||||
|
[1, 0, 0, 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
[-1, -1, -1, -1],
|
||||||
|
]
|
||||||
|
|
||||||
|
polytope = ToricPolytope(points, model_name="ci_smoke_test_quintic")
|
||||||
|
assert polytope.dimension == 4
|
||||||
|
assert len(polytope.triangulations) >= 1
|
||||||
|
|
||||||
|
polytope.topdata()
|
||||||
|
assert polytope.cy_dimension == 3
|
||||||
|
assert polytope.no_divs == 1 # P^4 has Picard rank 1
|
||||||
|
assert polytope.intersection_numbers_CY == {(0, 0, 0): 5} # classical quintic self-intersection
|
||||||
Reference in New Issue
Block a user