ncpolopt 0.1.0__py3-none-any.whl
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.
- ncpolopt/__init__.py +98 -0
- ncpolopt/__main__.py +37 -0
- ncpolopt/_logging.py +62 -0
- ncpolopt/block_structure.py +304 -0
- ncpolopt/chordal.py +256 -0
- ncpolopt/equality_elimination.py +91 -0
- ncpolopt/expressions.py +124 -0
- ncpolopt/hierarchies/__init__.py +18 -0
- ncpolopt/hierarchies/moroder.py +96 -0
- ncpolopt/hierarchies/rdm.py +233 -0
- ncpolopt/hierarchies/steering.py +245 -0
- ncpolopt/moment.py +164 -0
- ncpolopt/monomial_sets.py +103 -0
- ncpolopt/monomials.py +172 -0
- ncpolopt/physics.py +549 -0
- ncpolopt/problem.py +154 -0
- ncpolopt/relaxation.py +855 -0
- ncpolopt/sdp_problem.py +352 -0
- ncpolopt/sdpa_writer.py +187 -0
- ncpolopt/solution.py +356 -0
- ncpolopt/solvers/__init__.py +20 -0
- ncpolopt/solvers/_common.py +57 -0
- ncpolopt/solvers/base.py +103 -0
- ncpolopt/solvers/cvxpy_solver.py +177 -0
- ncpolopt/solvers/mosek_solver.py +242 -0
- ncpolopt/solvers/picos_solver.py +234 -0
- ncpolopt/solvers/registry.py +174 -0
- ncpolopt/solvers/sdpa_solver.py +97 -0
- ncpolopt/substitutions.py +481 -0
- ncpolopt/variables.py +188 -0
- ncpolopt-0.1.0.dist-info/METADATA +131 -0
- ncpolopt-0.1.0.dist-info/RECORD +34 -0
- ncpolopt-0.1.0.dist-info/WHEEL +4 -0
- ncpolopt-0.1.0.dist-info/entry_points.txt +3 -0
ncpolopt/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""ncpolopt: sparse SDP relaxations of polynomial optimization problems.
|
|
2
|
+
|
|
3
|
+
Builds semidefinite programming (SDP) relaxations -- the NPA hierarchy
|
|
4
|
+
for noncommuting operators, the Lasserre hierarchy for commuting
|
|
5
|
+
variables -- and solves them with pluggable backends (cvxpy, MOSEK,
|
|
6
|
+
cvxopt, SDPA).
|
|
7
|
+
|
|
8
|
+
The package models an optimization problem as a :class:`Problem` of
|
|
9
|
+
operators or variables, adds an objective and constraints, then builds a
|
|
10
|
+
:class:`~ncpolopt.relaxation.NpaRelaxation` at a chosen hierarchy level
|
|
11
|
+
and solves it to obtain an immutable :class:`~ncpolopt.solution.Solution`.
|
|
12
|
+
|
|
13
|
+
Quickstart (a small noncommutative problem, see the README for details)::
|
|
14
|
+
|
|
15
|
+
import ncpolopt as nc
|
|
16
|
+
|
|
17
|
+
X = nc.generate_operators("x", 2, hermitian=True)
|
|
18
|
+
problem = nc.Problem(
|
|
19
|
+
X,
|
|
20
|
+
objective=X[0] * X[1] + X[1] * X[0],
|
|
21
|
+
inequalities=[-X[1] ** 2 + X[1] + 0.5],
|
|
22
|
+
substitutions={X[0] ** 2: X[0]},
|
|
23
|
+
)
|
|
24
|
+
solution = problem.solve(level=2)
|
|
25
|
+
print(solution.primal) # -0.75
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
31
|
+
|
|
32
|
+
from ._logging import setup_logging
|
|
33
|
+
from .chordal import find_clique_index, find_variable_cliques, sliding_cliques
|
|
34
|
+
from .expressions import flatten
|
|
35
|
+
from .hierarchies import MoroderHierarchy, RdmHierarchy, SteeringHierarchy
|
|
36
|
+
from .moment import MomentEntry, MomentExpr
|
|
37
|
+
from .monomials import get_all_monomials, get_monomials
|
|
38
|
+
from .physics import (
|
|
39
|
+
Probability,
|
|
40
|
+
bosonic_constraints,
|
|
41
|
+
correlator,
|
|
42
|
+
define_objective_with_I,
|
|
43
|
+
fermionic_constraints,
|
|
44
|
+
generate_measurements,
|
|
45
|
+
get_neighbors,
|
|
46
|
+
get_next_neighbors,
|
|
47
|
+
maximum_violation,
|
|
48
|
+
pauli_constraints,
|
|
49
|
+
projective_measurement_constraints,
|
|
50
|
+
)
|
|
51
|
+
from .problem import Problem
|
|
52
|
+
from .relaxation import NpaRelaxation
|
|
53
|
+
from .sdpa_writer import read_sdpa_out, write_dat_s
|
|
54
|
+
from .solution import Solution
|
|
55
|
+
from .solvers.base import SolverKind, SolverSettings
|
|
56
|
+
from .variables import generate_operators, generate_variables, get_support
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
__version__ = version("ncpolopt")
|
|
60
|
+
except PackageNotFoundError: # pragma: no cover - editable installs always provide it
|
|
61
|
+
__version__ = "0.0.0"
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"MomentEntry",
|
|
65
|
+
"MomentExpr",
|
|
66
|
+
"MoroderHierarchy",
|
|
67
|
+
"NpaRelaxation",
|
|
68
|
+
"Probability",
|
|
69
|
+
"Problem",
|
|
70
|
+
"RdmHierarchy",
|
|
71
|
+
"Solution",
|
|
72
|
+
"SolverKind",
|
|
73
|
+
"SolverSettings",
|
|
74
|
+
"SteeringHierarchy",
|
|
75
|
+
"__version__",
|
|
76
|
+
"bosonic_constraints",
|
|
77
|
+
"correlator",
|
|
78
|
+
"define_objective_with_I",
|
|
79
|
+
"fermionic_constraints",
|
|
80
|
+
"find_clique_index",
|
|
81
|
+
"find_variable_cliques",
|
|
82
|
+
"flatten",
|
|
83
|
+
"generate_measurements",
|
|
84
|
+
"generate_operators",
|
|
85
|
+
"generate_variables",
|
|
86
|
+
"get_all_monomials",
|
|
87
|
+
"get_monomials",
|
|
88
|
+
"get_neighbors",
|
|
89
|
+
"get_next_neighbors",
|
|
90
|
+
"get_support",
|
|
91
|
+
"maximum_violation",
|
|
92
|
+
"pauli_constraints",
|
|
93
|
+
"projective_measurement_constraints",
|
|
94
|
+
"read_sdpa_out",
|
|
95
|
+
"setup_logging",
|
|
96
|
+
"sliding_cliques",
|
|
97
|
+
"write_dat_s",
|
|
98
|
+
]
|
ncpolopt/__main__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Command-line entry point for ncpolopt.
|
|
2
|
+
|
|
3
|
+
Prints the package version, the platform it runs on, and the SDP solvers
|
|
4
|
+
that are currently usable. The actual problem building happens through the
|
|
5
|
+
Python API; the CLI exists so ``pip install ncpolopt`` provides a useful
|
|
6
|
+
``ncpolopt`` command out of the box, and ``python -m ncpolopt`` works too.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import platform
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from ._logging import setup_logging
|
|
17
|
+
from .solvers.registry import available_solvers
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
"""Run the CLI: report version, platform and available solvers."""
|
|
22
|
+
parser = argparse.ArgumentParser(prog="ncpolopt", description=__doc__)
|
|
23
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
24
|
+
args = parser.parse_args() # noqa: F841 - kept for future subcommands
|
|
25
|
+
setup_logging()
|
|
26
|
+
print(f"ncpolopt {__version__}")
|
|
27
|
+
print(f"Python {platform.python_version()} on {platform.platform()}")
|
|
28
|
+
solvers = available_solvers()
|
|
29
|
+
if solvers:
|
|
30
|
+
print("Available solvers: " + ", ".join(s.name for s in solvers))
|
|
31
|
+
else:
|
|
32
|
+
print("No SDP solver found. Install one, e.g. `pip install ncpolopt[cvxpy]`.")
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
ncpolopt/_logging.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Logging helpers shared by all ncpolopt modules.
|
|
2
|
+
|
|
3
|
+
The package never prints to stdout directly; every module obtains a named
|
|
4
|
+
logger through :func:`module_logger` and exposes a user-facing ``verbose``
|
|
5
|
+
level that maps to standard logging severities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
_LOG_LEVELS: dict[int, int] = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def module_logger(name: str) -> logging.Logger:
|
|
16
|
+
"""Return the logger for a module, quiet by default.
|
|
17
|
+
|
|
18
|
+
The returned logger inherits the root handler configuration; call
|
|
19
|
+
:func:`setup_logging` once from an entry point to enable output.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
name: The ``__name__`` of the calling module.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A logger that only emits warnings unless the package is configured
|
|
26
|
+
for verbose output.
|
|
27
|
+
"""
|
|
28
|
+
return logging.getLogger(name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def verbosity_to_level(verbose: int) -> int:
|
|
32
|
+
"""Map the package's ``verbose`` integer to a logging severity.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
verbose: 0 (warnings only), 1 (info) or 2 (debug). Higher values
|
|
36
|
+
are clamped to debug.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The matching ``logging`` level constant.
|
|
40
|
+
"""
|
|
41
|
+
return _LOG_LEVELS.get(verbose, logging.DEBUG)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def setup_logging(level: int = logging.INFO) -> None:
|
|
45
|
+
"""Configure the root logger with a console handler.
|
|
46
|
+
|
|
47
|
+
Idempotent: calling it repeatedly does not stack duplicate handlers.
|
|
48
|
+
The handler carries the ``"ncpolopt"`` name so tests (and the function
|
|
49
|
+
itself) can recognize and detach it.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
level: The logging severity to emit, e.g. ``logging.INFO``.
|
|
53
|
+
"""
|
|
54
|
+
root = logging.getLogger()
|
|
55
|
+
root.setLevel(level)
|
|
56
|
+
for handler in root.handlers:
|
|
57
|
+
if getattr(handler, "name", None) == "ncpolopt":
|
|
58
|
+
return
|
|
59
|
+
handler = logging.StreamHandler()
|
|
60
|
+
handler.name = "ncpolopt"
|
|
61
|
+
handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s"))
|
|
62
|
+
root.addHandler(handler)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Typed description of the diagonal block structure of an SDP relaxation.
|
|
2
|
+
|
|
3
|
+
Ported from the legacy ``SdpRelaxation._calculate_block_structure`` method.
|
|
4
|
+
The old implementation tracked
|
|
5
|
+
the layout in two parallel arrays -- ``block_struct`` (block sizes) and
|
|
6
|
+
``localizing_monomial_sets`` (an indexed list padded with ``None`` entries,
|
|
7
|
+
with each equality's localizing basis duplicated) -- and constraint
|
|
8
|
+
processing recovered a block's basis by index arithmetic between the two.
|
|
9
|
+
This module replaces the pair with a single typed list of :class:`BlockSpec`
|
|
10
|
+
objects; each spec carries the metadata the construction layer needs, so no
|
|
11
|
+
parallel bookkeeping survives.
|
|
12
|
+
|
|
13
|
+
The block order is: parameter blocks, moment matrix blocks, extra moment
|
|
14
|
+
matrix blocks, then the constraint blocks (inequalities, moment inequalities,
|
|
15
|
+
equalities, moment equalities) in the same order the constraints are
|
|
16
|
+
processed downstream.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from sympy import S
|
|
26
|
+
|
|
27
|
+
from .expressions import convert_relational, flatten
|
|
28
|
+
from .moment import MomentExpr, as_moment_expr
|
|
29
|
+
from .monomials import ncdegree, pick_monomials_up_to_degree, unique
|
|
30
|
+
from .variables import find_variable_set
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BlockKind(Enum):
|
|
34
|
+
"""The role of a diagonal block in the SDP relaxation.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
PARAMETER: One free 1x1 variable per problem parameter.
|
|
38
|
+
MOMENT: The moment matrix over a monomial basis.
|
|
39
|
+
LOCALIZING: The localizing matrix of an inequality constraint.
|
|
40
|
+
SCALAR_INEQUALITY: A 1x1 inequality block over moments.
|
|
41
|
+
SCALAR_EQUALITY: A 1x1 equality block; equalities are enforced by
|
|
42
|
+
pairs of such blocks with opposite signs.
|
|
43
|
+
COPY: An extra moment matrix copying the first moment block.
|
|
44
|
+
NEW: An unconstrained extra moment matrix block.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
PARAMETER = "parameter"
|
|
48
|
+
MOMENT = "moment"
|
|
49
|
+
LOCALIZING = "localizing"
|
|
50
|
+
SCALAR_INEQUALITY = "scalar_inequality"
|
|
51
|
+
SCALAR_EQUALITY = "scalar_equality"
|
|
52
|
+
COPY = "copy"
|
|
53
|
+
NEW = "new"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class BlockSpec:
|
|
58
|
+
"""Metadata of one diagonal block of the SDP.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
kind: The role of the block.
|
|
62
|
+
size: Number of rows (and columns) of the block.
|
|
63
|
+
constraint: The constraint expression the block enforces, in the
|
|
64
|
+
normalized (``>= 0``) form and negated for the second half of an
|
|
65
|
+
equality pair. Only set for localizing and scalar blocks.
|
|
66
|
+
localizing_set: The monomials of the localizing basis. Only set for
|
|
67
|
+
localizing and scalar equality blocks.
|
|
68
|
+
monomial_set_index: Index of the monomial set for moment blocks.
|
|
69
|
+
impose_ppt: Apply the partial transpose to this block after
|
|
70
|
+
construction (extra moment matrices).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
kind: BlockKind
|
|
74
|
+
size: int
|
|
75
|
+
constraint: Any = None
|
|
76
|
+
localizing_set: tuple[Any, ...] = ()
|
|
77
|
+
monomial_set_index: int = -1
|
|
78
|
+
impose_ppt: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class BlockStructure:
|
|
83
|
+
"""The ordered block layout of an SDP relaxation.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
blocks: Block specs in the order they appear on the block diagonal.
|
|
87
|
+
constraint_to_blocks: Maps each constraint expression to the indices
|
|
88
|
+
of the blocks enforcing it. Inequalities map to a single block
|
|
89
|
+
index; equalities to the pair ``(first, first + half)`` covering
|
|
90
|
+
the + and - copies. Relational inputs are additionally registered
|
|
91
|
+
under their converted (``>= 0``) form.
|
|
92
|
+
warnings: Non-fatal build warnings, e.g. constraints whose degree
|
|
93
|
+
exceeds the relaxation level.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
blocks: tuple[BlockSpec, ...] = ()
|
|
97
|
+
constraint_to_blocks: dict[Any, tuple[int, ...]] = field(default_factory=dict)
|
|
98
|
+
warnings: tuple[str, ...] = ()
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def constraint_starting_block(self) -> int:
|
|
102
|
+
"""Index of the first constraint block (after the moment matrices)."""
|
|
103
|
+
for index, block in enumerate(self.blocks):
|
|
104
|
+
if block.kind in (
|
|
105
|
+
BlockKind.LOCALIZING,
|
|
106
|
+
BlockKind.SCALAR_INEQUALITY,
|
|
107
|
+
BlockKind.SCALAR_EQUALITY,
|
|
108
|
+
):
|
|
109
|
+
return index
|
|
110
|
+
return len(self.blocks)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _block_size(monomials: list[Any]) -> int:
|
|
114
|
+
"""Size of a moment block over a monomial set (possibly rectangular).
|
|
115
|
+
|
|
116
|
+
A rectangular set ``[A, B]`` spans the tensor product basis ``A x B``,
|
|
117
|
+
so its block is ``len(A) * len(B)`` rows tall. The old code returned
|
|
118
|
+
``len(A)`` here and left the generation code to produce the product --
|
|
119
|
+
an inconsistency the Moroder hierarchy patched by overriding the block
|
|
120
|
+
size. The product size is fixed at the source instead.
|
|
121
|
+
"""
|
|
122
|
+
if len(monomials) > 0 and isinstance(monomials[0], list):
|
|
123
|
+
return len(monomials[0]) * len(monomials[1])
|
|
124
|
+
return len(monomials)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compute_block_structure(
|
|
128
|
+
*,
|
|
129
|
+
variables: Any,
|
|
130
|
+
monomial_sets: list[list[Any]],
|
|
131
|
+
level: int,
|
|
132
|
+
inequalities: list[Any] | None,
|
|
133
|
+
equalities: list[Any] | None,
|
|
134
|
+
momentinequalities: list[Any] | None,
|
|
135
|
+
momentequalities: list[Any] | None,
|
|
136
|
+
parameters: list[Any] | None,
|
|
137
|
+
extramomentmatrices: list[list[str]] | None,
|
|
138
|
+
removeequalities: bool,
|
|
139
|
+
localizing_monomials: list[list[Any]] | None,
|
|
140
|
+
) -> BlockStructure:
|
|
141
|
+
"""Compute the ordered block layout of the SDP relaxation.
|
|
142
|
+
|
|
143
|
+
The localizing basis of a polynomial constraint of degree d at level l
|
|
144
|
+
consists of all basis monomials of degree up to (2l - d)/2; constraints
|
|
145
|
+
exceeding degree 2l yield a warning and the trivial basis.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
variables: The problem variables (or list of variable lists).
|
|
149
|
+
monomial_sets: One monomial basis per moment matrix block.
|
|
150
|
+
level: The relaxation level.
|
|
151
|
+
inequalities: Polynomial inequality constraints.
|
|
152
|
+
equalities: Polynomial equality constraints.
|
|
153
|
+
momentinequalities: Inequalities over moment expressions.
|
|
154
|
+
momentequalities: Equalities over moment expressions.
|
|
155
|
+
parameters: Parameter variables, each becoming its own 1x1 block.
|
|
156
|
+
extramomentmatrices: Per extra moment matrix, the option strings
|
|
157
|
+
``"copy"`` and/or ``"ppt"``.
|
|
158
|
+
removeequalities: If True, equalities are eliminated algebraically
|
|
159
|
+
and produce no blocks.
|
|
160
|
+
localizing_monomials: Per-constraint override of the localizing
|
|
161
|
+
basis; None entries request the automatic basis.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The block structure with constraint-to-block lookup.
|
|
165
|
+
"""
|
|
166
|
+
blocks: list[BlockSpec] = []
|
|
167
|
+
constraint_to_blocks: dict[Any, tuple[int, ...]] = {}
|
|
168
|
+
warnings: list[str] = []
|
|
169
|
+
|
|
170
|
+
# Parameter blocks precede everything else; each parameter becomes its
|
|
171
|
+
# own SDP variable, so its block is just a free 1x1 matrix.
|
|
172
|
+
if parameters is not None:
|
|
173
|
+
blocks.extend(BlockSpec(BlockKind.PARAMETER, 1) for _ in parameters)
|
|
174
|
+
|
|
175
|
+
# One moment matrix block per monomial set.
|
|
176
|
+
for index, monomials in enumerate(monomial_sets):
|
|
177
|
+
blocks.append(
|
|
178
|
+
BlockSpec(BlockKind.MOMENT, _block_size(monomials), monomial_set_index=index)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Extra moment matrices (copy of the first block, or unconstrained new
|
|
182
|
+
# blocks) repeat the base block sizes.
|
|
183
|
+
if extramomentmatrices is not None:
|
|
184
|
+
for options in extramomentmatrices:
|
|
185
|
+
kind = BlockKind.COPY if "copy" in options else BlockKind.NEW
|
|
186
|
+
impose_ppt = "ppt" in options
|
|
187
|
+
for index, monomials in enumerate(monomial_sets):
|
|
188
|
+
blocks.append(
|
|
189
|
+
BlockSpec(
|
|
190
|
+
kind,
|
|
191
|
+
_block_size(monomials),
|
|
192
|
+
monomial_set_index=index,
|
|
193
|
+
impose_ppt=impose_ppt,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Constraint blocks, in the same order the constraints are processed
|
|
198
|
+
# downstream: inequalities, moment inequalities, equalities, moment
|
|
199
|
+
# equalities.
|
|
200
|
+
constraints = list(flatten([inequalities]))
|
|
201
|
+
n_inequalities = len(inequalities) if inequalities is not None else 0
|
|
202
|
+
n_polynomial_inequalities = n_inequalities
|
|
203
|
+
if momentinequalities is not None:
|
|
204
|
+
constraints += list(momentinequalities)
|
|
205
|
+
n_inequalities += len(momentinequalities)
|
|
206
|
+
if not removeequalities and equalities is not None:
|
|
207
|
+
constraints += list(flatten([equalities]))
|
|
208
|
+
# Moment equalities are NOT part of the generic list: they get their
|
|
209
|
+
# scalar block pair from the dedicated loop below, and adding them here
|
|
210
|
+
# would double their blocks.
|
|
211
|
+
|
|
212
|
+
for k, raw_constraint in enumerate(constraints):
|
|
213
|
+
constraint = as_moment_expr(raw_constraint)
|
|
214
|
+
if isinstance(constraint, MomentExpr):
|
|
215
|
+
# Moment constraints act on 1x1 blocks with the trivial basis.
|
|
216
|
+
localizing_set = [S.One]
|
|
217
|
+
elif k < n_polynomial_inequalities or k >= n_inequalities:
|
|
218
|
+
# A genuine polynomial constraint (inequality or equality):
|
|
219
|
+
# compute its localizing basis from the relaxation degree.
|
|
220
|
+
if constraint.is_Relational:
|
|
221
|
+
constraint = convert_relational(constraint)
|
|
222
|
+
order = ncdegree(constraint)
|
|
223
|
+
if order > 2 * level:
|
|
224
|
+
warnings.append(
|
|
225
|
+
f"A constraint has degree {order}. Either choose a higher "
|
|
226
|
+
"level relaxation or ensure that a mixed-order relaxation "
|
|
227
|
+
"has the necessary monomials"
|
|
228
|
+
)
|
|
229
|
+
localization_order = (2 * level - order) // 2
|
|
230
|
+
if level == -1:
|
|
231
|
+
localization_order = 0
|
|
232
|
+
if (
|
|
233
|
+
localizing_monomials is not None
|
|
234
|
+
and localizing_monomials[k] is not None
|
|
235
|
+
):
|
|
236
|
+
localizing_set = localizing_monomials[k]
|
|
237
|
+
else:
|
|
238
|
+
index = find_variable_set(variables, constraint)
|
|
239
|
+
localizing_set = pick_monomials_up_to_degree(
|
|
240
|
+
monomial_sets[index], localization_order
|
|
241
|
+
)
|
|
242
|
+
if len(localizing_set) == 0:
|
|
243
|
+
localizing_set = [S.One]
|
|
244
|
+
else:
|
|
245
|
+
localizing_set = [S.One]
|
|
246
|
+
localizing_set = unique(localizing_set)
|
|
247
|
+
ln = len(localizing_set)
|
|
248
|
+
if k < n_inequalities:
|
|
249
|
+
# Inequality block: the localizing matrix over the basis.
|
|
250
|
+
kind = (
|
|
251
|
+
BlockKind.LOCALIZING
|
|
252
|
+
if not isinstance(constraint, MomentExpr)
|
|
253
|
+
else BlockKind.SCALAR_INEQUALITY
|
|
254
|
+
)
|
|
255
|
+
block_index = len(blocks)
|
|
256
|
+
blocks.append(
|
|
257
|
+
BlockSpec(
|
|
258
|
+
kind,
|
|
259
|
+
ln,
|
|
260
|
+
constraint=constraint,
|
|
261
|
+
localizing_set=tuple(localizing_set),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
constraint_to_blocks[constraint] = (block_index,)
|
|
265
|
+
if constraint is not raw_constraint:
|
|
266
|
+
constraint_to_blocks[raw_constraint] = (block_index,)
|
|
267
|
+
else:
|
|
268
|
+
# Equality: ln*(ln+1) one-by-one blocks in two halves. The first
|
|
269
|
+
# half enforces M_y(+eq), the second M_y(-eq); together they pin
|
|
270
|
+
# every entry of the localizing matrix to zero. The lookup entry
|
|
271
|
+
# (first, first + half) mirrors the old _constraint_to_block_index
|
|
272
|
+
# convention used by get_dual.
|
|
273
|
+
half = ln * (ln + 1) // 2
|
|
274
|
+
first = len(blocks)
|
|
275
|
+
blocks.extend(
|
|
276
|
+
BlockSpec(
|
|
277
|
+
BlockKind.SCALAR_EQUALITY,
|
|
278
|
+
1,
|
|
279
|
+
constraint=constraint * sign,
|
|
280
|
+
localizing_set=tuple(localizing_set),
|
|
281
|
+
)
|
|
282
|
+
for sign in (1, -1)
|
|
283
|
+
for _ in range(half)
|
|
284
|
+
)
|
|
285
|
+
constraint_to_blocks[constraint] = (first, first + half)
|
|
286
|
+
if constraint is not raw_constraint:
|
|
287
|
+
constraint_to_blocks[raw_constraint] = (first, first + half)
|
|
288
|
+
|
|
289
|
+
# Moment equalities: two 1x1 blocks per constraint (+ and -).
|
|
290
|
+
if not removeequalities and momentequalities is not None:
|
|
291
|
+
for meq in momentequalities:
|
|
292
|
+
first = len(blocks)
|
|
293
|
+
blocks.extend(
|
|
294
|
+
BlockSpec(
|
|
295
|
+
BlockKind.SCALAR_EQUALITY,
|
|
296
|
+
1,
|
|
297
|
+
constraint=meq * sign,
|
|
298
|
+
localizing_set=(S.One,),
|
|
299
|
+
)
|
|
300
|
+
for sign in (1, -1)
|
|
301
|
+
)
|
|
302
|
+
constraint_to_blocks[meq] = (first, first + 1)
|
|
303
|
+
|
|
304
|
+
return BlockStructure(tuple(blocks), constraint_to_blocks, tuple(warnings))
|