qqideal 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qqideal-0.1.0/LICENSE +21 -0
- qqideal-0.1.0/PKG-INFO +91 -0
- qqideal-0.1.0/README.md +67 -0
- qqideal-0.1.0/pyproject.toml +41 -0
- qqideal-0.1.0/setup.cfg +4 -0
- qqideal-0.1.0/src/qqideal/__init__.py +37 -0
- qqideal-0.1.0/src/qqideal/dimdeg.py +127 -0
- qqideal-0.1.0/src/qqideal/doublepoint.py +96 -0
- qqideal-0.1.0/src/qqideal/errors.py +52 -0
- qqideal-0.1.0/src/qqideal/ideal.py +316 -0
- qqideal-0.1.0/src/qqideal/py.typed +0 -0
- qqideal-0.1.0/src/qqideal/ring.py +483 -0
- qqideal-0.1.0/src/qqideal/verdict.py +118 -0
- qqideal-0.1.0/src/qqideal.egg-info/PKG-INFO +91 -0
- qqideal-0.1.0/src/qqideal.egg-info/SOURCES.txt +20 -0
- qqideal-0.1.0/src/qqideal.egg-info/dependency_links.txt +1 -0
- qqideal-0.1.0/src/qqideal.egg-info/requires.txt +5 -0
- qqideal-0.1.0/src/qqideal.egg-info/top_level.txt +1 -0
- qqideal-0.1.0/tests/test_doublepoint.py +94 -0
- qqideal-0.1.0/tests/test_ideal.py +247 -0
- qqideal-0.1.0/tests/test_ring.py +162 -0
- qqideal-0.1.0/tests/test_verdict.py +88 -0
qqideal-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DC Posch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
qqideal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qqideal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact ideals over QQ: python-flint for arithmetic, msolve for Groebner bases.
|
|
5
|
+
Author: DC Posch
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dcposch/qqideal
|
|
8
|
+
Project-URL: Source, https://github.com/dcposch/qqideal
|
|
9
|
+
Keywords: msolve,groebner,ideal,polynomial,computer-algebra
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: msolveio>=0.1.0
|
|
20
|
+
Requires-Dist: python-flint<1,>=0.8
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# qqideal
|
|
26
|
+
|
|
27
|
+
Exact ideals over QQ: python-flint for arithmetic, msolve for Gröbner bases, verdicts that
|
|
28
|
+
refuse to guess. Emptiness, dimension, and 0-dimensional degree from grevlex leading ideals.
|
|
29
|
+
No solver-mode I/O.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install qqideal
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That pulls [msolveio](https://pypi.org/project/msolveio/) and python-flint from PyPI. You also
|
|
38
|
+
need a system `msolve` 0.10.x binary on `PATH`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from qqideal import Ideal, Kind, Ring, ideal_verdict
|
|
44
|
+
|
|
45
|
+
R = Ring("x", "y")
|
|
46
|
+
I = Ideal(["x^2-1", "y-x"], ring=R)
|
|
47
|
+
|
|
48
|
+
verdict = ideal_verdict(I)
|
|
49
|
+
print(verdict.kind) # Kind.NONEMPTY
|
|
50
|
+
print(verdict.dim) # 0
|
|
51
|
+
print(verdict.degree) # 2
|
|
52
|
+
print(verdict.certainty) # Certainty.PROVEN
|
|
53
|
+
|
|
54
|
+
if verdict.kind is Kind.NONEMPTY: # never `if verdict:` -- that raises
|
|
55
|
+
print(I.groebner()) # (Poly('x - y', ...), Poly('y^2 - 1', ...))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`Verdict.__bool__` raises `TypeError`. `TIMEOUT` and `ERROR` are not answers, and a
|
|
59
|
+
truthiness test would silently fold them into one of the two that are.
|
|
60
|
+
|
|
61
|
+
`opens=` saturates before the test, so you can ask about the complement of a hypersurface:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
ideal_verdict(["x*y", "x"], ring=R, opens=["x"]).kind # Kind.EMPTY
|
|
65
|
+
Ideal(["x^2"], ring=R).radical_member("x").kind # Kind.EMPTY: x is in the radical
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Certainty
|
|
69
|
+
|
|
70
|
+
A unit ideal over Q from msolve `-g` is `Certainty.MODULAR`, not `PROVEN`: msolve 0.10.1
|
|
71
|
+
returns after its first modular prime and still prints characteristic 0. A nonempty result
|
|
72
|
+
over Q uses a lifted `-g 2` basis and is `Certainty.PROVEN`, as is any result over a prime
|
|
73
|
+
field.
|
|
74
|
+
|
|
75
|
+
## Saturation
|
|
76
|
+
|
|
77
|
+
`I.saturate(f)` is Rabinowitsch: it returns `I + (u*f - 1)` in the ring extended by one slack
|
|
78
|
+
variable. Its variety is `V(I) \ V(f)`, so emptiness, dimension, and degree are those of
|
|
79
|
+
`I : f^∞` -- but its generators are not that ideal written back in `R`, which would need an
|
|
80
|
+
elimination order msolve's Gröbner mode does not offer. `colon` is an alias, and in v0.1 the
|
|
81
|
+
colon is the saturation.
|
|
82
|
+
|
|
83
|
+
## Not in v0.1
|
|
84
|
+
|
|
85
|
+
Primary decomposition, positive-dimensional radicals, radical computation of any kind
|
|
86
|
+
(membership only), solver mode / `-P`, Macaulay2. These raise `NotImplementedError` rather
|
|
87
|
+
than returning an approximation.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT © 2026 DC Posch — <https://github.com/dcposch/qqideal>
|
qqideal-0.1.0/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# qqideal
|
|
2
|
+
|
|
3
|
+
Exact ideals over QQ: python-flint for arithmetic, msolve for Gröbner bases, verdicts that
|
|
4
|
+
refuse to guess. Emptiness, dimension, and 0-dimensional degree from grevlex leading ideals.
|
|
5
|
+
No solver-mode I/O.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install qqideal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That pulls [msolveio](https://pypi.org/project/msolveio/) and python-flint from PyPI. You also
|
|
14
|
+
need a system `msolve` 0.10.x binary on `PATH`.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from qqideal import Ideal, Kind, Ring, ideal_verdict
|
|
20
|
+
|
|
21
|
+
R = Ring("x", "y")
|
|
22
|
+
I = Ideal(["x^2-1", "y-x"], ring=R)
|
|
23
|
+
|
|
24
|
+
verdict = ideal_verdict(I)
|
|
25
|
+
print(verdict.kind) # Kind.NONEMPTY
|
|
26
|
+
print(verdict.dim) # 0
|
|
27
|
+
print(verdict.degree) # 2
|
|
28
|
+
print(verdict.certainty) # Certainty.PROVEN
|
|
29
|
+
|
|
30
|
+
if verdict.kind is Kind.NONEMPTY: # never `if verdict:` -- that raises
|
|
31
|
+
print(I.groebner()) # (Poly('x - y', ...), Poly('y^2 - 1', ...))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`Verdict.__bool__` raises `TypeError`. `TIMEOUT` and `ERROR` are not answers, and a
|
|
35
|
+
truthiness test would silently fold them into one of the two that are.
|
|
36
|
+
|
|
37
|
+
`opens=` saturates before the test, so you can ask about the complement of a hypersurface:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
ideal_verdict(["x*y", "x"], ring=R, opens=["x"]).kind # Kind.EMPTY
|
|
41
|
+
Ideal(["x^2"], ring=R).radical_member("x").kind # Kind.EMPTY: x is in the radical
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Certainty
|
|
45
|
+
|
|
46
|
+
A unit ideal over Q from msolve `-g` is `Certainty.MODULAR`, not `PROVEN`: msolve 0.10.1
|
|
47
|
+
returns after its first modular prime and still prints characteristic 0. A nonempty result
|
|
48
|
+
over Q uses a lifted `-g 2` basis and is `Certainty.PROVEN`, as is any result over a prime
|
|
49
|
+
field.
|
|
50
|
+
|
|
51
|
+
## Saturation
|
|
52
|
+
|
|
53
|
+
`I.saturate(f)` is Rabinowitsch: it returns `I + (u*f - 1)` in the ring extended by one slack
|
|
54
|
+
variable. Its variety is `V(I) \ V(f)`, so emptiness, dimension, and degree are those of
|
|
55
|
+
`I : f^∞` -- but its generators are not that ideal written back in `R`, which would need an
|
|
56
|
+
elimination order msolve's Gröbner mode does not offer. `colon` is an alias, and in v0.1 the
|
|
57
|
+
colon is the saturation.
|
|
58
|
+
|
|
59
|
+
## Not in v0.1
|
|
60
|
+
|
|
61
|
+
Primary decomposition, positive-dimensional radicals, radical computation of any kind
|
|
62
|
+
(membership only), solver mode / `-P`, Macaulay2. These raise `NotImplementedError` rather
|
|
63
|
+
than returning an approximation.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT © 2026 DC Posch — <https://github.com/dcposch/qqideal>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qqideal"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Exact ideals over QQ: python-flint for arithmetic, msolve for Groebner bases."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "DC Posch" }]
|
|
13
|
+
keywords = ["msolve", "groebner", "ideal", "polynomial", "computer-algebra"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"msolveio>=0.1.0",
|
|
24
|
+
"python-flint>=0.8,<1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/dcposch/qqideal"
|
|
32
|
+
Source = "https://github.com/dcposch/qqideal"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
qqideal = ["py.typed"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
qqideal-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Exact ideals over Q, with python-flint for arithmetic and msolve for
|
|
2
|
+
Groebner bases.
|
|
3
|
+
|
|
4
|
+
qqideal answers three questions about an ideal -- is its variety empty, what is
|
|
5
|
+
its dimension, and (when the dimension is zero) what is its degree -- and
|
|
6
|
+
refuses the rest. It is not a computer algebra system: anything outside that
|
|
7
|
+
slate raises :class:`NotImplementedError` rather than returning something
|
|
8
|
+
approximate.
|
|
9
|
+
|
|
10
|
+
msolve is called in Groebner mode only. Solver mode and ``-P`` parametrizations
|
|
11
|
+
are never invoked, so no output whose meaning depends on the mode is ever
|
|
12
|
+
interpreted.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .doublepoint import double_point_ideal
|
|
18
|
+
from .errors import MsolveInputError, QQIdealError, RingMismatch
|
|
19
|
+
from .ideal import Ideal
|
|
20
|
+
from .ring import Poly, Ring
|
|
21
|
+
from .verdict import Certainty, Kind, Verdict, ideal_verdict
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Ring",
|
|
27
|
+
"Poly",
|
|
28
|
+
"Ideal",
|
|
29
|
+
"Kind",
|
|
30
|
+
"Certainty",
|
|
31
|
+
"Verdict",
|
|
32
|
+
"ideal_verdict",
|
|
33
|
+
"double_point_ideal",
|
|
34
|
+
"QQIdealError",
|
|
35
|
+
"RingMismatch",
|
|
36
|
+
"MsolveInputError",
|
|
37
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Krull dimension and 0-dimensional degree, read off a leading ideal.
|
|
2
|
+
|
|
3
|
+
Both quantities are combinatorial once you have the leading monomials of a
|
|
4
|
+
Groebner basis: ``k[x]/I`` and ``k[x]/LT(I)`` have the same Hilbert function, so
|
|
5
|
+
they have the same dimension, and in the 0-dimensional case the same vector
|
|
6
|
+
space dimension over ``k``. Nothing here talks to msolve; the input is just
|
|
7
|
+
exponent vectors, which makes it cheap to test.
|
|
8
|
+
|
|
9
|
+
The convention for the unit ideal is ``dimension == -1``: ``LT(I)`` contains the
|
|
10
|
+
constant monomial, ``k[x]/I`` is the zero ring, and the variety is empty.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from itertools import product
|
|
16
|
+
from typing import Iterable, Sequence
|
|
17
|
+
|
|
18
|
+
__all__ = ["dimension", "degree", "dim_and_degree"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dimension(leading: Iterable[Sequence[int]], nvars: int) -> int:
|
|
22
|
+
"""Krull dimension of ``k[x1..xn]/I`` from the leading monomials of ``I``.
|
|
23
|
+
|
|
24
|
+
The dimension is the largest number of variables ``S`` such that ``LT(I)``
|
|
25
|
+
contains no monomial supported entirely inside ``S``.
|
|
26
|
+
|
|
27
|
+
:param leading: exponent vectors of the leading monomials, each of length
|
|
28
|
+
``nvars``. An empty collection means the zero ideal.
|
|
29
|
+
:param nvars: the number of variables.
|
|
30
|
+
:returns: the dimension, or ``-1`` for the unit ideal.
|
|
31
|
+
"""
|
|
32
|
+
supports = _supports(leading, nvars)
|
|
33
|
+
best = -1
|
|
34
|
+
|
|
35
|
+
def search(start: int, chosen: int, size: int) -> None:
|
|
36
|
+
nonlocal best
|
|
37
|
+
# Independence is downward closed, so a dependent set prunes the whole
|
|
38
|
+
# subtree below it.
|
|
39
|
+
if any(support & ~chosen == 0 for support in supports):
|
|
40
|
+
return
|
|
41
|
+
if size > best:
|
|
42
|
+
best = size
|
|
43
|
+
for index in range(start, nvars):
|
|
44
|
+
if size + (nvars - index) <= best:
|
|
45
|
+
break
|
|
46
|
+
search(index + 1, chosen | (1 << index), size + 1)
|
|
47
|
+
|
|
48
|
+
search(0, 0, 0)
|
|
49
|
+
return best
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def degree(leading: Iterable[Sequence[int]], nvars: int) -> int:
|
|
53
|
+
"""Number of standard monomials, i.e. ``dim_k k[x1..xn]/I``.
|
|
54
|
+
|
|
55
|
+
This is the affine degree of a 0-dimensional ideal, counted with
|
|
56
|
+
multiplicity.
|
|
57
|
+
|
|
58
|
+
:raises ValueError: unless the ideal is 0-dimensional. In positive dimension
|
|
59
|
+
there are infinitely many standard monomials and no such number.
|
|
60
|
+
"""
|
|
61
|
+
monomials = _normalize(leading, nvars)
|
|
62
|
+
bounds: list[int] = []
|
|
63
|
+
for index in range(nvars):
|
|
64
|
+
pure = [
|
|
65
|
+
monomial[index]
|
|
66
|
+
for monomial in monomials
|
|
67
|
+
if all(e == 0 for position, e in enumerate(monomial) if position != index)
|
|
68
|
+
]
|
|
69
|
+
if not pure:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"the ideal is not 0-dimensional: the leading ideal contains no "
|
|
72
|
+
f"pure power of variable {index}"
|
|
73
|
+
)
|
|
74
|
+
bounds.append(min(pure))
|
|
75
|
+
if any(bound == 0 for bound in bounds):
|
|
76
|
+
# A pure power with exponent 0 is the constant monomial: the unit ideal.
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
count = 0
|
|
80
|
+
for candidate in product(*(range(bound) for bound in bounds)):
|
|
81
|
+
if not any(_divides(monomial, candidate) for monomial in monomials):
|
|
82
|
+
count += 1
|
|
83
|
+
return count
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def dim_and_degree(
|
|
87
|
+
leading: Iterable[Sequence[int]], nvars: int
|
|
88
|
+
) -> tuple[int, int | None]:
|
|
89
|
+
"""``(dimension, degree)``, with ``degree`` only when the dimension is 0."""
|
|
90
|
+
monomials = _normalize(leading, nvars)
|
|
91
|
+
dim = dimension(monomials, nvars)
|
|
92
|
+
if dim != 0:
|
|
93
|
+
return dim, None
|
|
94
|
+
return dim, degree(monomials, nvars)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _normalize(
|
|
98
|
+
leading: Iterable[Sequence[int]], nvars: int
|
|
99
|
+
) -> tuple[tuple[int, ...], ...]:
|
|
100
|
+
monomials: list[tuple[int, ...]] = []
|
|
101
|
+
for monomial in leading:
|
|
102
|
+
exponents = tuple(int(e) for e in monomial)
|
|
103
|
+
if len(exponents) != nvars:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"leading monomial {exponents} has {len(exponents)} exponents, "
|
|
106
|
+
f"expected {nvars}"
|
|
107
|
+
)
|
|
108
|
+
if any(e < 0 for e in exponents):
|
|
109
|
+
raise ValueError(f"leading monomial {exponents} has a negative exponent")
|
|
110
|
+
monomials.append(exponents)
|
|
111
|
+
return tuple(monomials)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _supports(leading: Iterable[Sequence[int]], nvars: int) -> tuple[int, ...]:
|
|
115
|
+
"""One bitmask per leading monomial, marking which variables it uses."""
|
|
116
|
+
masks = set()
|
|
117
|
+
for monomial in _normalize(leading, nvars):
|
|
118
|
+
mask = 0
|
|
119
|
+
for index, exponent in enumerate(monomial):
|
|
120
|
+
if exponent:
|
|
121
|
+
mask |= 1 << index
|
|
122
|
+
masks.add(mask)
|
|
123
|
+
return tuple(masks)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _divides(monomial: Sequence[int], candidate: Sequence[int]) -> bool:
|
|
127
|
+
return all(a <= b for a, b in zip(monomial, candidate))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The double-point ideal of a polynomial parametrization.
|
|
2
|
+
|
|
3
|
+
For a plane curve parametrized by ``t -> (p(t), q(t))``, two distinct parameters
|
|
4
|
+
land on the same point exactly when ``p(s) = p(t)`` and ``q(s) = q(t)`` with
|
|
5
|
+
``s != t``. That is the ideal
|
|
6
|
+
|
|
7
|
+
I_DP = (p(s) - p(t), q(s) - q(t)) : (s - t)^oo
|
|
8
|
+
|
|
9
|
+
in ``QQ[s, t]``: saturating at ``s - t`` throws away the diagonal, which solves
|
|
10
|
+
the equations trivially and says nothing.
|
|
11
|
+
|
|
12
|
+
This module builds that ideal and stops there. It is not a test for whether a
|
|
13
|
+
parametrization is an embedding: an injective map can still fail to be one, and
|
|
14
|
+
the cusp ``t -> (t^2, t^3)`` is the standard example -- it has no double point
|
|
15
|
+
at all, and fails on the derivative instead. Deciding embedding needs the rest
|
|
16
|
+
of the postcheck (length, immersivity, distinct tangents), which is not here.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .errors import MsolveInputError, RingMismatch
|
|
22
|
+
from .ideal import Ideal
|
|
23
|
+
from .ring import Poly, Ring
|
|
24
|
+
|
|
25
|
+
__all__ = ["double_point_ideal"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def double_point_ideal(
|
|
29
|
+
p: "Poly | str",
|
|
30
|
+
q: "Poly | str",
|
|
31
|
+
*,
|
|
32
|
+
ring: Ring | None = None,
|
|
33
|
+
names: tuple[str, str] = ("s", "t"),
|
|
34
|
+
) -> Ideal:
|
|
35
|
+
"""Build ``(p(s)-p(t), q(s)-q(t)) : (s-t)^oo`` for the parametrization
|
|
36
|
+
``(p, q)``.
|
|
37
|
+
|
|
38
|
+
:param p: first coordinate, a univariate polynomial or a string.
|
|
39
|
+
:param q: second coordinate, likewise.
|
|
40
|
+
:param ring: the univariate source ring, required if ``p`` or ``q`` is a
|
|
41
|
+
string.
|
|
42
|
+
:param names: the two parameter names to use.
|
|
43
|
+
:returns: an :class:`~qqideal.Ideal`. As with
|
|
44
|
+
:meth:`~qqideal.Ideal.saturate`, the saturation is carried as a
|
|
45
|
+
Rabinowitsch ideal, so the ideal lives in ``QQ[s, t, u]`` with
|
|
46
|
+
``u*(s-t) - 1`` among its generators, and its variety is the set of
|
|
47
|
+
ordered pairs ``s != t`` with the same image. Emptiness, dimension and
|
|
48
|
+
degree are the ones of the double-point locus.
|
|
49
|
+
:raises MsolveInputError: if the source ring is not univariate.
|
|
50
|
+
"""
|
|
51
|
+
source = _source_ring(p, q, ring)
|
|
52
|
+
if source.nvars != 1:
|
|
53
|
+
raise MsolveInputError(
|
|
54
|
+
f"a parametrization is univariate; got {source} with "
|
|
55
|
+
f"{source.nvars} variables"
|
|
56
|
+
)
|
|
57
|
+
if len(names) != 2 or names[0] == names[1]:
|
|
58
|
+
raise MsolveInputError(f"names must be two distinct variables, got {names!r}")
|
|
59
|
+
|
|
60
|
+
poly_p = _coerce(source, p)
|
|
61
|
+
poly_q = _coerce(source, q)
|
|
62
|
+
|
|
63
|
+
plane = Ring(*names, characteristic=source.characteristic)
|
|
64
|
+
s, t = plane.gens()
|
|
65
|
+
equations = [
|
|
66
|
+
_substitute(poly_p, plane, 0) - _substitute(poly_p, plane, 1),
|
|
67
|
+
_substitute(poly_q, plane, 0) - _substitute(poly_q, plane, 1),
|
|
68
|
+
]
|
|
69
|
+
return Ideal(equations, ring=plane).saturate(s - t)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _source_ring(p: "Poly | str", q: "Poly | str", ring: Ring | None) -> Ring:
|
|
73
|
+
rings = {value.ring for value in (p, q) if isinstance(value, Poly)}
|
|
74
|
+
if ring is not None:
|
|
75
|
+
rings.add(ring)
|
|
76
|
+
if not rings:
|
|
77
|
+
raise MsolveInputError("ring= is required when p and q are strings")
|
|
78
|
+
if len(rings) > 1:
|
|
79
|
+
raise RingMismatch(
|
|
80
|
+
f"p and q must live in one ring, got {sorted(str(r) for r in rings)}"
|
|
81
|
+
)
|
|
82
|
+
return rings.pop()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _coerce(ring: Ring, value: "Poly | str") -> Poly:
|
|
86
|
+
return ring(value) if isinstance(value, str) else ring._check(value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _substitute(poly: Poly, plane: Ring, position: int) -> Poly:
|
|
90
|
+
"""Send the source variable to variable ``position`` of the plane ring."""
|
|
91
|
+
terms = {}
|
|
92
|
+
for exponents, coeff in poly.terms():
|
|
93
|
+
key = [0, 0]
|
|
94
|
+
key[position] = exponents[0]
|
|
95
|
+
terms[tuple(key)] = coeff
|
|
96
|
+
return plane._wrap(plane._ctx.from_dict(terms))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Exceptions raised by qqideal.
|
|
2
|
+
|
|
3
|
+
Two rules decide which exception a failure gets:
|
|
4
|
+
|
|
5
|
+
* A caller mistake -- an unparseable polynomial, a ring mismatch, a request
|
|
6
|
+
outside the v0.1 slate -- raises. Callers should not have to inspect a
|
|
7
|
+
:class:`~qqideal.Verdict` to discover they wrote ``x/2``.
|
|
8
|
+
* A solver failure -- msolve timing out, dying, or emitting bytes we refuse to
|
|
9
|
+
interpret -- becomes a :class:`~qqideal.Verdict` with
|
|
10
|
+
:attr:`~qqideal.Kind.TIMEOUT` or :attr:`~qqideal.Kind.ERROR`.
|
|
11
|
+
|
|
12
|
+
Input errors are reported as msolveio's :class:`~msolveio.MsolveInputError`
|
|
13
|
+
rather than a new type, so that a bad polynomial raises the same exception
|
|
14
|
+
whether qqideal or msolveio caught it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from msolveio import (
|
|
20
|
+
MsolveAmbiguous,
|
|
21
|
+
MsolveDied,
|
|
22
|
+
MsolveError,
|
|
23
|
+
MsolveInputError,
|
|
24
|
+
MsolveOutputError,
|
|
25
|
+
MsolveTimeout,
|
|
26
|
+
MsolveVersionUnsupported,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"QQIdealError",
|
|
31
|
+
"RingMismatch",
|
|
32
|
+
"MsolveError",
|
|
33
|
+
"MsolveInputError",
|
|
34
|
+
"MsolveOutputError",
|
|
35
|
+
"MsolveAmbiguous",
|
|
36
|
+
"MsolveTimeout",
|
|
37
|
+
"MsolveDied",
|
|
38
|
+
"MsolveVersionUnsupported",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QQIdealError(Exception):
|
|
43
|
+
"""Base class for errors qqideal raises on its own behalf."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RingMismatch(QQIdealError, TypeError):
|
|
47
|
+
"""Two polynomials from different rings were combined.
|
|
48
|
+
|
|
49
|
+
qqideal never coerces across rings implicitly: ``QQ[x]`` and ``QQ[x,y]``
|
|
50
|
+
are different rings, and so are ``QQ[x,y]`` and ``F_p[x,y]``. Use
|
|
51
|
+
:meth:`qqideal.Ring.embed` to move a polynomial deliberately.
|
|
52
|
+
"""
|