mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Remove tuples from ConstraintFeatures
This commit is contained in:
@@ -88,10 +88,10 @@ class StaticLazyConstraintsComponent(Component):
|
||||
assert constraints is not None
|
||||
assert constraints.lazy is not None
|
||||
assert constraints.names is not None
|
||||
selected = tuple(
|
||||
selected = [
|
||||
(constraints.lazy[i] and constraints.names[i] not in self.enforced_cids)
|
||||
for i in range(len(constraints.lazy))
|
||||
)
|
||||
]
|
||||
n_removed = sum(selected)
|
||||
n_kept = sum(constraints.lazy) - n_removed
|
||||
self.pool = constraints[selected]
|
||||
@@ -174,7 +174,7 @@ class StaticLazyConstraintsComponent(Component):
|
||||
self.pool,
|
||||
tol=self.violation_tolerance,
|
||||
)
|
||||
is_violated = tuple(not i for i in is_satisfied)
|
||||
is_violated = [not i for i in is_satisfied]
|
||||
violated_constraints = self.pool[is_violated]
|
||||
satisfied_constraints = self.pool[is_satisfied]
|
||||
self.pool = satisfied_constraints
|
||||
|
||||
@@ -78,18 +78,18 @@ class VariableFeatures:
|
||||
|
||||
@dataclass
|
||||
class ConstraintFeatures:
|
||||
basis_status: Optional[Tuple[str, ...]] = None
|
||||
categories: Optional[Tuple[Optional[Hashable], ...]] = None
|
||||
dual_values: Optional[Tuple[float, ...]] = None
|
||||
names: Optional[Tuple[str, ...]] = None
|
||||
lazy: Optional[Tuple[bool, ...]] = None
|
||||
lhs: Optional[Tuple[Tuple[Tuple[str, float], ...], ...]] = None
|
||||
rhs: Optional[Tuple[float, ...]] = None
|
||||
sa_rhs_down: Optional[Tuple[float, ...]] = None
|
||||
sa_rhs_up: Optional[Tuple[float, ...]] = None
|
||||
senses: Optional[Tuple[str, ...]] = None
|
||||
slacks: Optional[Tuple[float, ...]] = None
|
||||
user_features: Optional[Tuple[Optional[Tuple[float, ...]], ...]] = None
|
||||
basis_status: Optional[List[str]] = None
|
||||
categories: Optional[List[Optional[Hashable]]] = None
|
||||
dual_values: Optional[List[float]] = None
|
||||
names: Optional[List[str]] = None
|
||||
lazy: Optional[List[bool]] = None
|
||||
lhs: Optional[List[List[Tuple[str, float]]]] = None
|
||||
rhs: Optional[List[float]] = None
|
||||
sa_rhs_down: Optional[List[float]] = None
|
||||
sa_rhs_up: Optional[List[float]] = None
|
||||
senses: Optional[List[str]] = None
|
||||
slacks: Optional[List[float]] = None
|
||||
user_features: Optional[List[Optional[List[float]]]] = None
|
||||
|
||||
def to_list(self, index: int) -> List[float]:
|
||||
features: List[float] = []
|
||||
@@ -107,7 +107,7 @@ class ConstraintFeatures:
|
||||
_clip(features)
|
||||
return features
|
||||
|
||||
def __getitem__(self, selected: Tuple[bool, ...]) -> "ConstraintFeatures":
|
||||
def __getitem__(self, selected: List[bool]) -> "ConstraintFeatures":
|
||||
return ConstraintFeatures(
|
||||
basis_status=self._filter(self.basis_status, selected),
|
||||
categories=self._filter(self.categories, selected),
|
||||
@@ -125,12 +125,12 @@ class ConstraintFeatures:
|
||||
|
||||
def _filter(
|
||||
self,
|
||||
obj: Optional[Tuple],
|
||||
selected: Tuple[bool, ...],
|
||||
) -> Optional[Tuple]:
|
||||
obj: Optional[List],
|
||||
selected: List[bool],
|
||||
) -> Optional[List]:
|
||||
if obj is None:
|
||||
return None
|
||||
return tuple(obj[i] for (i, selected_i) in enumerate(selected) if selected_i)
|
||||
return [obj[i] for (i, selected_i) in enumerate(selected) if selected_i]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -217,7 +217,7 @@ class FeaturesExtractor:
|
||||
if user_features_i is None:
|
||||
user_features.append(None)
|
||||
else:
|
||||
user_features.append(user_features_i)
|
||||
user_features.append(list(user_features_i))
|
||||
features.variables.categories = categories
|
||||
features.variables.user_features = user_features
|
||||
|
||||
@@ -229,7 +229,7 @@ class FeaturesExtractor:
|
||||
assert features.constraints is not None
|
||||
assert features.constraints.names is not None
|
||||
has_static_lazy = instance.has_static_lazy_constraints()
|
||||
user_features: List[Optional[Tuple[float, ...]]] = []
|
||||
user_features: List[Optional[List[float]]] = []
|
||||
categories: List[Optional[Hashable]] = []
|
||||
lazy: List[bool] = []
|
||||
for (cidx, cname) in enumerate(features.constraints.names):
|
||||
@@ -253,7 +253,7 @@ class FeaturesExtractor:
|
||||
f"Constraint features must be a list of numbers. "
|
||||
f"Found {type(f).__name__} instead for cname={cname}."
|
||||
)
|
||||
user_features.append(tuple(cf))
|
||||
user_features.append(list(cf))
|
||||
else:
|
||||
user_features.append(None)
|
||||
categories.append(None)
|
||||
@@ -261,9 +261,9 @@ class FeaturesExtractor:
|
||||
lazy.append(instance.is_constraint_lazy(cname))
|
||||
else:
|
||||
lazy.append(False)
|
||||
features.constraints.user_features = tuple(user_features)
|
||||
features.constraints.lazy = tuple(lazy)
|
||||
features.constraints.categories = tuple(categories)
|
||||
features.constraints.user_features = user_features
|
||||
features.constraints.lazy = lazy
|
||||
features.constraints.categories = categories
|
||||
|
||||
def _extract_user_features_instance(
|
||||
self,
|
||||
|
||||
@@ -36,3 +36,10 @@ class _RedirectOutput:
|
||||
) -> None:
|
||||
sys.stdout = self._original_stdout
|
||||
sys.stderr = self._original_stderr
|
||||
|
||||
|
||||
def _none_if_empty(obj: Any) -> Any:
|
||||
if len(obj) == 0:
|
||||
return None
|
||||
else:
|
||||
return obj
|
||||
|
||||
@@ -122,7 +122,7 @@ class GurobiSolver(InternalSolver):
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> Tuple[bool, ...]:
|
||||
) -> List[bool]:
|
||||
assert cf.names is not None
|
||||
assert cf.senses is not None
|
||||
assert cf.lhs is not None
|
||||
@@ -141,7 +141,7 @@ class GurobiSolver(InternalSolver):
|
||||
result.append(lhs >= cf.rhs[i] - tol)
|
||||
else:
|
||||
result.append(abs(cf.rhs[i] - lhs) <= tol)
|
||||
return tuple(result)
|
||||
return result
|
||||
|
||||
@overrides
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
@@ -209,37 +209,37 @@ class GurobiSolver(InternalSolver):
|
||||
raise Exception(f"unknown cbasis: {v}")
|
||||
|
||||
gp_constrs = model.getConstrs()
|
||||
constr_names = tuple(model.getAttr("constrName", gp_constrs))
|
||||
rhs, lhs, senses, slacks, basis_status = None, None, None, None, None
|
||||
constr_names = model.getAttr("constrName", gp_constrs)
|
||||
lhs: Optional[List] = None
|
||||
rhs, senses, slacks, basis_status = None, None, None, None
|
||||
dual_value, basis_status, sa_rhs_up, sa_rhs_down = None, None, None, None
|
||||
|
||||
if with_static:
|
||||
rhs = tuple(model.getAttr("rhs", gp_constrs))
|
||||
senses = tuple(model.getAttr("sense", gp_constrs))
|
||||
rhs = model.getAttr("rhs", gp_constrs)
|
||||
senses = model.getAttr("sense", gp_constrs)
|
||||
if with_lhs:
|
||||
lhs_l: List = [None for _ in gp_constrs]
|
||||
lhs = [None for _ in gp_constrs]
|
||||
for (i, gp_constr) in enumerate(gp_constrs):
|
||||
expr = model.getRow(gp_constr)
|
||||
lhs_l[i] = tuple(
|
||||
lhs[i] = [
|
||||
(self._var_names[expr.getVar(j).index], expr.getCoeff(j))
|
||||
for j in range(expr.size())
|
||||
)
|
||||
lhs = tuple(lhs_l)
|
||||
]
|
||||
|
||||
if self._has_lp_solution:
|
||||
dual_value = tuple(model.getAttr("pi", gp_constrs))
|
||||
basis_status = tuple(
|
||||
dual_value = model.getAttr("pi", gp_constrs)
|
||||
basis_status = list(
|
||||
map(
|
||||
_parse_gurobi_cbasis,
|
||||
model.getAttr("cbasis", gp_constrs),
|
||||
)
|
||||
)
|
||||
if with_sa:
|
||||
sa_rhs_up = tuple(model.getAttr("saRhsUp", gp_constrs))
|
||||
sa_rhs_down = tuple(model.getAttr("saRhsLow", gp_constrs))
|
||||
sa_rhs_up = model.getAttr("saRhsUp", gp_constrs)
|
||||
sa_rhs_down = model.getAttr("saRhsLow", gp_constrs)
|
||||
|
||||
if self._has_lp_solution or self._has_mip_solution:
|
||||
slacks = tuple(model.getAttr("slack", gp_constrs))
|
||||
slacks = model.getAttr("slack", gp_constrs)
|
||||
|
||||
return ConstraintFeatures(
|
||||
basis_status=basis_status,
|
||||
@@ -370,7 +370,7 @@ class GurobiSolver(InternalSolver):
|
||||
return self.model.status in [self.gp.GRB.INFEASIBLE, self.gp.GRB.INF_OR_UNBD]
|
||||
|
||||
@overrides
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
assert self.model is not None
|
||||
constrs = [self.model.getConstrByName(n) for n in names]
|
||||
self.model.remove(constrs)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from typing import Any, List, Optional, List
|
||||
|
||||
from miplearn.features import VariableFeatures, ConstraintFeatures
|
||||
from miplearn.instance.base import Instance
|
||||
@@ -59,7 +59,7 @@ class InternalSolver(ABC):
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> Tuple[bool, ...]:
|
||||
) -> List[bool]:
|
||||
"""
|
||||
Checks whether the current solution satisfies the given constraints.
|
||||
"""
|
||||
@@ -176,7 +176,7 @@ class InternalSolver(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
"""
|
||||
Removes the given constraints from the model.
|
||||
"""
|
||||
|
||||
@@ -21,7 +21,7 @@ from pyomo.opt.base.solvers import SolverFactory
|
||||
|
||||
from miplearn.features import VariableFeatures, ConstraintFeatures
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.solvers import _RedirectOutput
|
||||
from miplearn.solvers import _RedirectOutput, _none_if_empty
|
||||
from miplearn.solvers.internal import (
|
||||
InternalSolver,
|
||||
LPSolveStats,
|
||||
@@ -113,7 +113,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
self,
|
||||
cf: ConstraintFeatures,
|
||||
tol: float = 1e-5,
|
||||
) -> Tuple[bool, ...]:
|
||||
) -> List[bool]:
|
||||
assert cf.names is not None
|
||||
assert cf.lhs is not None
|
||||
assert cf.rhs is not None
|
||||
@@ -130,7 +130,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
result.append(lhs >= cf.rhs[i] - tol)
|
||||
else:
|
||||
result.append(abs(cf.rhs[i] - lhs) < tol)
|
||||
return tuple(result)
|
||||
return result
|
||||
|
||||
@overrides
|
||||
def build_test_instance_infeasible(self) -> Instance:
|
||||
@@ -165,7 +165,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
|
||||
names: List[str] = []
|
||||
rhs: List[float] = []
|
||||
lhs: List[Tuple[Tuple[str, float], ...]] = []
|
||||
lhs: List[List[Tuple[str, float]]] = []
|
||||
senses: List[str] = []
|
||||
dual_values: List[float] = []
|
||||
slacks: List[float] = []
|
||||
@@ -214,7 +214,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
raise Exception(
|
||||
f"Unknown expression type: {expr.__class__.__name__}"
|
||||
)
|
||||
lhs.append(tuple(lhsc))
|
||||
lhs.append(lhsc)
|
||||
|
||||
# Extract dual values
|
||||
if self._has_lp_solution:
|
||||
@@ -233,24 +233,13 @@ class BasePyomoSolver(InternalSolver):
|
||||
names.append(constr.name)
|
||||
_parse_constraint(constr)
|
||||
|
||||
rhs_t, lhs_t, senses_t = None, None, None
|
||||
slacks_t, dual_values_t = None, None
|
||||
if with_static:
|
||||
rhs_t = tuple(rhs)
|
||||
lhs_t = tuple(lhs)
|
||||
senses_t = tuple(senses)
|
||||
if self._has_lp_solution:
|
||||
dual_values_t = tuple(dual_values)
|
||||
if self._has_lp_solution or self._has_mip_solution:
|
||||
slacks_t = tuple(slacks)
|
||||
|
||||
return ConstraintFeatures(
|
||||
names=tuple(names),
|
||||
rhs=rhs_t,
|
||||
senses=senses_t,
|
||||
lhs=lhs_t,
|
||||
slacks=slacks_t,
|
||||
dual_values=dual_values_t,
|
||||
names=_none_if_empty(names),
|
||||
rhs=_none_if_empty(rhs),
|
||||
senses=_none_if_empty(senses),
|
||||
lhs=_none_if_empty(lhs),
|
||||
slacks=_none_if_empty(slacks),
|
||||
dual_values=_none_if_empty(dual_values),
|
||||
)
|
||||
|
||||
@overrides
|
||||
@@ -337,12 +326,6 @@ class BasePyomoSolver(InternalSolver):
|
||||
if self._has_lp_solution or self._has_mip_solution:
|
||||
values.append(v.value)
|
||||
|
||||
def _none_if_empty(obj: Any) -> Any:
|
||||
if len(obj) == 0:
|
||||
return None
|
||||
else:
|
||||
return obj
|
||||
|
||||
return VariableFeatures(
|
||||
names=_none_if_empty(names),
|
||||
types=_none_if_empty(types),
|
||||
@@ -379,7 +362,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
return self._termination_condition == TerminationCondition.infeasible
|
||||
|
||||
@overrides
|
||||
def remove_constraints(self, names: Tuple[str, ...]) -> None:
|
||||
def remove_constraints(self, names: List[str]) -> None:
|
||||
assert self.model is not None
|
||||
for name in names:
|
||||
constr = self._cname_to_constr[name]
|
||||
|
||||
@@ -69,18 +69,18 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
assert_equals(
|
||||
solver.get_constraints(),
|
||||
ConstraintFeatures(
|
||||
names=("eq_capacity",),
|
||||
rhs=(0.0,),
|
||||
lhs=(
|
||||
(
|
||||
names=["eq_capacity"],
|
||||
rhs=[0.0],
|
||||
lhs=[
|
||||
[
|
||||
("x[0]", 23.0),
|
||||
("x[1]", 26.0),
|
||||
("x[2]", 20.0),
|
||||
("x[3]", 18.0),
|
||||
("z", -1.0),
|
||||
),
|
||||
),
|
||||
senses=("=",),
|
||||
],
|
||||
],
|
||||
senses=["="],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -120,12 +120,12 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
_filter_attrs(
|
||||
solver.get_constraint_attrs(),
|
||||
ConstraintFeatures(
|
||||
basis_status=("N",),
|
||||
dual_values=(13.538462,),
|
||||
names=("eq_capacity",),
|
||||
sa_rhs_down=(-24.0,),
|
||||
sa_rhs_up=(2.0,),
|
||||
slacks=(0.0,),
|
||||
basis_status=["N"],
|
||||
dual_values=[13.538462],
|
||||
names=["eq_capacity"],
|
||||
sa_rhs_down=[-24.0],
|
||||
sa_rhs_up=[2.0],
|
||||
slacks=[0.0],
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -165,20 +165,20 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
_filter_attrs(
|
||||
solver.get_constraint_attrs(),
|
||||
ConstraintFeatures(
|
||||
names=("eq_capacity",),
|
||||
slacks=(0.0,),
|
||||
names=["eq_capacity"],
|
||||
slacks=[0.0],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Build new constraint and verify that it is violated
|
||||
cf = ConstraintFeatures(
|
||||
names=("cut",),
|
||||
lhs=((("x[0]", 1.0),),),
|
||||
rhs=(0.0,),
|
||||
senses=("<",),
|
||||
names=["cut"],
|
||||
lhs=[[("x[0]", 1.0)]],
|
||||
rhs=[0.0],
|
||||
senses=["<"],
|
||||
)
|
||||
assert_equals(solver.are_constraints_satisfied(cf), (False,))
|
||||
assert_equals(solver.are_constraints_satisfied(cf), [False])
|
||||
|
||||
# Add constraint and verify it affects solution
|
||||
solver.add_constraints(cf)
|
||||
@@ -187,28 +187,30 @@ def run_basic_usage_tests(solver: InternalSolver) -> None:
|
||||
_filter_attrs(
|
||||
solver.get_constraint_attrs(),
|
||||
ConstraintFeatures(
|
||||
names=("eq_capacity", "cut"),
|
||||
rhs=(0.0, 0.0),
|
||||
lhs=(
|
||||
(
|
||||
names=["eq_capacity", "cut"],
|
||||
rhs=[0.0, 0.0],
|
||||
lhs=[
|
||||
[
|
||||
("x[0]", 23.0),
|
||||
("x[1]", 26.0),
|
||||
("x[2]", 20.0),
|
||||
("x[3]", 18.0),
|
||||
("z", -1.0),
|
||||
),
|
||||
(("x[0]", 1.0),),
|
||||
),
|
||||
senses=("=", "<"),
|
||||
],
|
||||
[
|
||||
("x[0]", 1.0),
|
||||
],
|
||||
],
|
||||
senses=["=", "<"],
|
||||
),
|
||||
),
|
||||
)
|
||||
stats = solver.solve()
|
||||
assert_equals(stats.mip_lower_bound, 1030.0)
|
||||
assert_equals(solver.are_constraints_satisfied(cf), (True,))
|
||||
assert_equals(solver.are_constraints_satisfied(cf), [True])
|
||||
|
||||
# Remove the new constraint
|
||||
solver.remove_constraints(("cut",))
|
||||
solver.remove_constraints(["cut"])
|
||||
|
||||
# New constraint should no longer affect solution
|
||||
stats = solver.solve()
|
||||
|
||||
@@ -33,20 +33,20 @@ def sample() -> Sample:
|
||||
lazy_constraint_count=4,
|
||||
),
|
||||
constraints=ConstraintFeatures(
|
||||
names=("c1", "c2", "c3", "c4", "c5"),
|
||||
categories=(
|
||||
names=["c1", "c2", "c3", "c4", "c5"],
|
||||
categories=[
|
||||
"type-a",
|
||||
"type-a",
|
||||
"type-a",
|
||||
"type-b",
|
||||
"type-b",
|
||||
),
|
||||
lazy=(True, True, True, True, False),
|
||||
],
|
||||
lazy=[True, True, True, True, False],
|
||||
),
|
||||
),
|
||||
after_lp=Features(
|
||||
instance=InstanceFeatures(),
|
||||
constraints=ConstraintFeatures(names=("c1", "c2", "c3", "c4", "c5")),
|
||||
constraints=ConstraintFeatures(names=["c1", "c2", "c3", "c4", "c5"]),
|
||||
),
|
||||
after_mip=Features(
|
||||
extra={
|
||||
@@ -132,7 +132,7 @@ def test_usage_with_solver(instance: Instance) -> None:
|
||||
|
||||
# Should ask internal solver to remove some constraints
|
||||
assert internal.remove_constraints.call_count == 1
|
||||
internal.remove_constraints.assert_has_calls([call(("c3",))])
|
||||
internal.remove_constraints.assert_has_calls([call(["c3"])])
|
||||
|
||||
# LearningSolver calls after_iteration (first time)
|
||||
should_repeat = component.iteration_cb(solver, instance, None)
|
||||
@@ -141,7 +141,7 @@ def test_usage_with_solver(instance: Instance) -> None:
|
||||
# Should ask internal solver to verify if constraints in the pool are
|
||||
# satisfied and add the ones that are not
|
||||
assert sample.after_load.constraints is not None
|
||||
c = sample.after_load.constraints[False, False, True, False, False]
|
||||
c = sample.after_load.constraints[[False, False, True, False, False]]
|
||||
internal.are_constraints_satisfied.assert_called_once_with(c, tol=1.0)
|
||||
internal.are_constraints_satisfied.reset_mock()
|
||||
internal.add_constraints.assert_called_once_with(c)
|
||||
|
||||
@@ -65,26 +65,26 @@ def test_knapsack() -> None:
|
||||
assert_equals(
|
||||
_round(features.constraints),
|
||||
ConstraintFeatures(
|
||||
basis_status=("N",),
|
||||
categories=("eq_capacity",),
|
||||
dual_values=(13.538462,),
|
||||
names=("eq_capacity",),
|
||||
lazy=(False,),
|
||||
lhs=(
|
||||
(
|
||||
basis_status=["N"],
|
||||
categories=["eq_capacity"],
|
||||
dual_values=[13.538462],
|
||||
names=["eq_capacity"],
|
||||
lazy=[False],
|
||||
lhs=[
|
||||
[
|
||||
("x[0]", 23.0),
|
||||
("x[1]", 26.0),
|
||||
("x[2]", 20.0),
|
||||
("x[3]", 18.0),
|
||||
("z", -1.0),
|
||||
),
|
||||
),
|
||||
rhs=(0.0,),
|
||||
sa_rhs_down=(-24.0,),
|
||||
sa_rhs_up=(2.0,),
|
||||
senses=("=",),
|
||||
slacks=(0.0,),
|
||||
user_features=((0.0,),),
|
||||
],
|
||||
],
|
||||
rhs=[0.0],
|
||||
sa_rhs_down=[-24.0],
|
||||
sa_rhs_up=[2.0],
|
||||
senses=["="],
|
||||
slacks=[0.0],
|
||||
user_features=[[0.0]],
|
||||
),
|
||||
)
|
||||
assert_equals(
|
||||
@@ -98,39 +98,39 @@ def test_knapsack() -> None:
|
||||
|
||||
def test_constraint_getindex() -> None:
|
||||
cf = ConstraintFeatures(
|
||||
names=("c1", "c2", "c3"),
|
||||
rhs=(1.0, 2.0, 3.0),
|
||||
senses=("=", "<", ">"),
|
||||
lhs=(
|
||||
(
|
||||
names=["c1", "c2", "c3"],
|
||||
rhs=[1.0, 2.0, 3.0],
|
||||
senses=["=", "<", ">"],
|
||||
lhs=[
|
||||
[
|
||||
("x1", 1.0),
|
||||
("x2", 1.0),
|
||||
),
|
||||
(
|
||||
],
|
||||
[
|
||||
("x2", 2.0),
|
||||
("x3", 2.0),
|
||||
),
|
||||
(
|
||||
],
|
||||
[
|
||||
("x3", 3.0),
|
||||
("x4", 3.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
assert_equals(
|
||||
cf[True, False, True],
|
||||
cf[[True, False, True]],
|
||||
ConstraintFeatures(
|
||||
names=("c1", "c3"),
|
||||
rhs=(1.0, 3.0),
|
||||
senses=("=", ">"),
|
||||
lhs=(
|
||||
(
|
||||
names=["c1", "c3"],
|
||||
rhs=[1.0, 3.0],
|
||||
senses=["=", ">"],
|
||||
lhs=[
|
||||
[
|
||||
("x1", 1.0),
|
||||
("x2", 1.0),
|
||||
),
|
||||
(
|
||||
],
|
||||
[
|
||||
("x3", 3.0),
|
||||
("x4", 3.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user