Tools
modde
MipFile
File reader for MODDE investigation files (.mip)
Source code in opti/tools/modde.py
class MipFile:
"""File reader for MODDE investigation files (.mip)"""
def __init__(self, filename):
"""Read in a MODDE file.
Args:
filename (str or path): path to MODDE file
"""
s = b"".join(open(filename, "rb").readlines())
# Split into blocks of 1024 bytes
num_blocks = len(s) // 1024
assert len(s) % 1024 == 0
blocks = [s[i * 1024 : (i + 1) * 1024] for i in range(num_blocks)]
# Split blocks into header (first 21 bytes) and body
headers = [b[0:21] for b in blocks]
data = [b[21:] for b in blocks]
# Parse the block headers to create a mapping {block_index: next_block_index}
block_order = {}
for i, header in enumerate(headers):
i_next = np.where([h.startswith(header[15:19]) for h in headers])[0]
block_order[i] = i_next[0] if len(i_next) == 1 else None
# Join all blocks that belong together and decode to UTF-8
self.parts = []
while len(block_order) > 0:
i = next(iter(block_order)) # get first key in ordered dict
s = b""
while i is not None:
s += data[i]
i = block_order.pop(i)
s = re.sub(b"\r|\x00", b"", s).decode("utf-8", errors="ignore")
self.parts.append(s)
self.settings = self._get_design_settings()
self.factors = {k: self.settings[k] for k in self.settings["Factors"]}
self.responses = {k: self.settings[k] for k in self.settings["Responses"]}
self.data = self._get_experimental_data()
def _get_experimental_data(self):
"""Parse the experimental data.
Returns:
pd.DataFrame: dataframe of experimental data
"""
part = [p for p in self.parts if p.startswith("ExpNo")][0]
return pd.read_csv(io.StringIO(part), delimiter="\t", index_col="ExpNo")
def _get_design_settings(self):
"""Parse the design settings.
Returns:
dict of dicts: dictionary with 'Factors', 'Responses', 'Options' as well as the individual variables.
"""
part = [p for p in self.parts if p.startswith("[Status]")][0]
settings = {}
for line in part.split("\n"):
if len(line) == 0:
continue
if line.startswith("["):
thing = line.strip("[]")
settings[thing] = {}
else:
key, value = line.split("=")
settings[thing][key] = value
return settings
__init__(self, filename)
special
Read in a MODDE file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filename |
str or path |
path to MODDE file |
required |
Source code in opti/tools/modde.py
def __init__(self, filename):
"""Read in a MODDE file.
Args:
filename (str or path): path to MODDE file
"""
s = b"".join(open(filename, "rb").readlines())
# Split into blocks of 1024 bytes
num_blocks = len(s) // 1024
assert len(s) % 1024 == 0
blocks = [s[i * 1024 : (i + 1) * 1024] for i in range(num_blocks)]
# Split blocks into header (first 21 bytes) and body
headers = [b[0:21] for b in blocks]
data = [b[21:] for b in blocks]
# Parse the block headers to create a mapping {block_index: next_block_index}
block_order = {}
for i, header in enumerate(headers):
i_next = np.where([h.startswith(header[15:19]) for h in headers])[0]
block_order[i] = i_next[0] if len(i_next) == 1 else None
# Join all blocks that belong together and decode to UTF-8
self.parts = []
while len(block_order) > 0:
i = next(iter(block_order)) # get first key in ordered dict
s = b""
while i is not None:
s += data[i]
i = block_order.pop(i)
s = re.sub(b"\r|\x00", b"", s).decode("utf-8", errors="ignore")
self.parts.append(s)
self.settings = self._get_design_settings()
self.factors = {k: self.settings[k] for k in self.settings["Factors"]}
self.responses = {k: self.settings[k] for k in self.settings["Responses"]}
self.data = self._get_experimental_data()
read_modde(filepath)
Read a problem specification from a MODDE file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filepath |
path-like |
path to MODDE .mip file |
required |
Returns:
Type | Description |
---|---|
opti.Problem |
problem specification |
Source code in opti/tools/modde.py
def read_modde(filepath):
"""Read a problem specification from a MODDE file.
Args:
filepath (path-like): path to MODDE .mip file
Returns:
opti.Problem: problem specification
"""
mip = MipFile(filepath)
inputs = []
outputs = []
constraints = []
formulation_parameters = []
# build input space
for name, props in mip.factors.items():
if props["Use"] == "Uncontrolled":
print(f"Uncontrolled factors not supported. Skipping {name}")
continue
if props["Type"] == "Formulation":
domain = [float(s) for s in props["Settings"].split(",")]
inputs.append(opti.Continuous(name=name, domain=domain))
formulation_parameters.append(name)
elif props["Type"] == "Quantitative":
domain = [float(s) for s in props["Settings"].split(",")]
inputs.append(opti.Continuous(name=name, domain=domain))
elif props["Type"] == "Multilevel":
domain = [float(s) for s in props["Settings"].split(",")]
inputs.append(opti.Discrete(name=name, domain=domain))
elif props["Type"] == "Qualitative":
domain = props["Settings"].split(",")
inputs.append(opti.Categorical(name=name, domain=domain))
inputs = opti.Parameters(inputs)
# build formulation constraint
constraints.append(
opti.constraint.LinearEquality(
names=formulation_parameters,
lhs=np.ones(len(formulation_parameters)),
rhs=1,
)
)
# build output space
for name, props in mip.responses.items():
# check if data available that allows to infer the domain
vmin = mip.data[name].min()
vmax = mip.data[name].max()
if np.isfinite(vmin) or np.isfinite(vmax) and vmin < vmax:
domain = [vmin, vmax]
else:
domain = [0, 1]
dim = opti.Continuous(name=name, domain=domain)
outputs.append(dim)
outputs = opti.Parameters(outputs)
# data
data = mip.data.drop(columns=["ExpName", "InOut"])
return opti.Problem(
inputs=inputs, outputs=outputs, constraints=constraints, data=data
)
noisify
noisify_problem(problem, noisifiers)
Creates a new problem that is based on the given one plus noise on the outputs.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
problem |
Problem |
given problem where we will add noise |
required |
noisifiers |
List[Callable] |
list of functions that add noise to the outputs |
required |
Returns: new problem with noise on the output
Source code in opti/tools/noisify.py
def noisify_problem(
problem: Problem,
noisifiers: List[Callable],
) -> Problem:
"""Creates a new problem that is based on the given one plus noise on the outputs.
Args:
problem: given problem where we will add noise
noisifiers: list of functions that add noise to the outputs
Returns: new problem with noise on the output
"""
def noisy_f(X):
return _add_noise_to_data(problem.f(X), noisifiers, problem.outputs)
if problem.data is not None:
data = problem.get_data()
X = data[problem.inputs.names]
Yn = _add_noise_to_data(
data[problem.outputs.names], noisifiers, problem.outputs
)
data = pd.concat([X, Yn], axis=1)
else:
data = None
return Problem(
inputs=problem.inputs,
outputs=problem.outputs,
objectives=problem.objectives,
constraints=problem.constraints,
output_constraints=problem.output_constraints,
f=noisy_f,
data=data,
name=problem.name,
)
noisify_problem_with_gaussian(problem, mu=0, sigma=0.05)
Given an instance of a problem, this returns the problem with additive Gaussian noise
Parameters:
Name | Type | Description | Default |
---|---|---|---|
problem |
Problem |
problem instance where we add the noise |
required |
mu |
float |
mean of the Gaussian noise to be added |
0 |
sigma |
float |
standard deviation of the Gaussian noise to be added |
0.05 |
Returns: input problem with additive Gaussian noise
Source code in opti/tools/noisify.py
def noisify_problem_with_gaussian(problem: Problem, mu: float = 0, sigma: float = 0.05):
"""
Given an instance of a problem, this returns the problem with additive Gaussian noise
Args:
problem: problem instance where we add the noise
mu: mean of the Gaussian noise to be added
sigma: standard deviation of the Gaussian noise to be added
Returns: input problem with additive Gaussian noise
"""
def noisify(y):
rv = norm(loc=mu, scale=sigma)
return y + rv.rvs(len(y))
return noisify_problem(problem, noisifiers=[noisify] * len(problem.outputs))
reduce
AffineTransform
Source code in opti/tools/reduce.py
class AffineTransform:
def __init__(self, equalities):
self.equalities = equalities
def augment_data(self, data: pd.DataFrame) -> pd.DataFrame:
"""Restore eliminated parameters in a dataframe."""
data = data.copy()
for name_lhs, names_rhs, coeffs in self.equalities:
data[name_lhs] = coeffs[-1]
for i, name in enumerate(names_rhs):
data[name_lhs] += coeffs[i] * data[name]
return data
def drop_data(self, data: pd.DataFrame) -> pd.DataFrame:
"""Drop eliminated parameters from a DataFrame."""
drop = []
for name_lhs, _, _ in self.equalities:
if name_lhs in data.columns:
drop.append(name_lhs)
return data.drop(columns=drop)
augment_data(self, data)
Restore eliminated parameters in a dataframe.
Source code in opti/tools/reduce.py
def augment_data(self, data: pd.DataFrame) -> pd.DataFrame:
"""Restore eliminated parameters in a dataframe."""
data = data.copy()
for name_lhs, names_rhs, coeffs in self.equalities:
data[name_lhs] = coeffs[-1]
for i, name in enumerate(names_rhs):
data[name_lhs] += coeffs[i] * data[name]
return data
drop_data(self, data)
Drop eliminated parameters from a DataFrame.
Source code in opti/tools/reduce.py
def drop_data(self, data: pd.DataFrame) -> pd.DataFrame:
"""Drop eliminated parameters from a DataFrame."""
drop = []
for name_lhs, _, _ in self.equalities:
if name_lhs in data.columns:
drop.append(name_lhs)
return data.drop(columns=drop)
check_existence_of_solution(A_aug)
Given an augmented coefficient matrix this function determines the existence (and uniqueness) of solution using the rank theorem.
Source code in opti/tools/reduce.py
def check_existence_of_solution(A_aug):
"""Given an augmented coefficient matrix this function determines the existence (and uniqueness) of solution using the rank theorem."""
A = A_aug[:, :-1]
b = A_aug[:, -1]
len_inputs = np.shape(A)[1]
# catch special cases
rk_A_aug = np.linalg.matrix_rank(A_aug)
rk_A = np.linalg.matrix_rank(A)
if rk_A == rk_A_aug:
if rk_A < len_inputs:
return # all good
else:
x = np.linalg.solve(A, b)
raise Exception(
f"There is a unique solution x for the linear equality constraints: x={x}"
)
elif rk_A < rk_A_aug:
raise Exception(
"There is no solution fulfilling the linear equality constraints."
)
check_problem_for_reduction(problem)
Check if the reduction can be applied or if a trivial case is present.
Source code in opti/tools/reduce.py
def check_problem_for_reduction(problem: Problem) -> bool:
"""Check if the reduction can be applied or if a trivial case is present."""
# are there any constraints?
if problem.constraints is None:
return False
# are there any linear equality constraints?
linear_equalities, _ = find_linear_equalities(problem.constraints)
if len(linear_equalities) == 0:
return False
# are there continuous inputs
continuous_inputs, _ = find_continuous_inputs(problem.inputs)
if len(continuous_inputs) == 0:
return False
# check that equality constraints only contain continuous inputs
for c in linear_equalities:
for name in c.names:
if name not in continuous_inputs.names:
raise Exception(
f"Linear equality constraint {c} contains a non-continuous parameter. Problem reduction is not supported."
)
return True
find_continuous_inputs(inputs)
Separate parameters into continuous and all other parameters.
Source code in opti/tools/reduce.py
def find_continuous_inputs(inputs: Parameters) -> Tuple[Parameters, Parameters]:
"""Separate parameters into continuous and all other parameters."""
continous_inputs = [p for p in inputs if isinstance(p, Continuous)]
other_inputs = [p for p in inputs if not isinstance(p, Continuous)]
return Parameters(continous_inputs), Parameters(other_inputs)
find_linear_equalities(constraints)
Separate constraints into linear equalities and all other constraints.
Source code in opti/tools/reduce.py
def find_linear_equalities(constraints: Constraints) -> Tuple[Constraints, Constraints]:
"""Separate constraints into linear equalities and all other constraints."""
linear_equalities = [c for c in constraints if isinstance(c, LinearEquality)]
other_constraints = [c for c in constraints if not isinstance(c, LinearEquality)]
return Constraints(linear_equalities), Constraints(other_constraints)
reduce_problem(problem)
Reduce a problem with linear constraints to a subproblem where linear equality constraints are eliminated.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
problem |
Problem |
problem to be reduced |
required |
Returns:
Type | Description |
---|---|
Tuple[opti.problem.Problem, opti.tools.reduce.AffineTransform] |
(problem, trafo). Problem is the reduced problem where linear equality constraints have been eliminated. trafo is the according transformation. |
Source code in opti/tools/reduce.py
def reduce_problem(problem: Problem) -> Tuple[Problem, AffineTransform]:
"""Reduce a problem with linear constraints to a subproblem where linear equality constraints are eliminated.
Args:
problem (Problem): problem to be reduced
Returns:
(problem, trafo). Problem is the reduced problem where linear equality constraints
have been eliminated. trafo is the according transformation.
"""
# check if the problem can be reduced
if not check_problem_for_reduction(problem):
return problem, AffineTransform([])
# find linear equality constraints
linear_equalities, other_constraints = find_linear_equalities(problem.constraints)
# only consider continuous inputs
continuous_inputs, other_inputs = find_continuous_inputs(problem.inputs)
# assemble Matrix A from equality constraints
N = len(linear_equalities)
M = len(continuous_inputs) + 1
names = np.concatenate((continuous_inputs.names, ["rhs"]))
A_aug = pd.DataFrame(data=np.zeros(shape=(N, M)), columns=names)
for i in range(len(linear_equalities)):
c = linear_equalities[i]
A_aug.loc[i, c.names] = c.lhs
A_aug.loc[i, "rhs"] = c.rhs
A_aug = A_aug.values
# catch special cases
check_existence_of_solution(A_aug)
# bring A_aug to reduced row-echelon form
A_aug_rref, pivots = rref(A_aug)
pivots = np.array(pivots)
A_aug_rref = np.array(A_aug_rref).astype(np.float64)
# formulate box bounds as linear inequality constraints in matrix form
B = np.zeros(shape=(2 * (M - 1), M))
B[: M - 1, : M - 1] = np.eye(M - 1)
B[M - 1 :, : M - 1] = -np.eye(M - 1)
B[: M - 1, -1] = continuous_inputs.bounds.loc["max"].copy()
B[M - 1 :, -1] = -continuous_inputs.bounds.loc["min"].copy()
# eliminate columns with pivot element
for i in range(len(pivots)):
p = pivots[i]
B[p, :] -= A_aug_rref[i, :]
B[p + M - 1, :] += A_aug_rref[i, :]
# build up reduced problem
_inputs = list(other_inputs.parameters.values())
for i in range(len(continuous_inputs)):
# add all inputs that were not eliminated
if i not in pivots:
_inputs.append(continuous_inputs[names[i]])
_inputs = Parameters(_inputs)
_constraints = other_constraints.constraints
for i in pivots:
# reduce equation system of upper bounds
ind = np.where(B[i, :-1] != 0)[0]
if len(ind) > 0 and B[i, -1] < np.inf:
c = LinearInequality(names=list(names[ind]), lhs=B[i, ind], rhs=B[i, -1])
_constraints.append(c)
else:
if B[i, -1] < -1e-16:
raise Exception("There is no solution that fulfills the constraints.")
# reduce equation system of lower bounds
ind = np.where(B[i + M - 1, :-1] != 0)[0]
if len(ind) > 0 and B[i + M - 1, -1] < np.inf:
c = LinearInequality(
names=list(names[ind]), lhs=B[i + M - 1, ind], rhs=B[i + M - 1, -1]
)
_constraints.append(c)
else:
if B[i + M - 1, -1] < -1e-16:
raise Exception("There is no solution that fulfills the constraints.")
_constraints = Constraints(_constraints)
# assemble equalities
_equalities = []
for i in range(len(pivots)):
name_lhs = names[pivots[i]]
names_rhs = []
coeffs = []
for j in range(len(names) - 1):
if A_aug_rref[i, j] != 0 and j != pivots[i]:
coeffs.append(-A_aug_rref[i, j])
names_rhs.append(names[j])
coeffs.append(A_aug_rref[i, -1])
_equalities.append([name_lhs, names_rhs, coeffs])
_data = problem.data
trafo = AffineTransform(_equalities)
_models = problem.models
if _models is not None:
warnings.warn("Models are currently not adapted in reduce_problem.")
if hasattr(problem, "f") and problem.f is not None:
def _f(X: pd.DataFrame) -> pd.DataFrame:
return problem.f(trafo.augment_data(X))
else:
_f = None
_problem = Problem(
inputs=_inputs,
outputs=deepcopy(problem.outputs),
objectives=deepcopy(problem.objectives),
constraints=deepcopy(_constraints),
f=_f,
models=_models,
data=_data,
optima=deepcopy(problem.optima),
name=deepcopy(problem.name),
)
# remove remaining dependencies of eliminated inputs from the problem
_problem = remove_eliminated_inputs(_problem, trafo)
return _problem, trafo
remove_eliminated_inputs(problem, transform)
Eliminates remaining occurences of eliminated inputs in linear constraints.
Source code in opti/tools/reduce.py
def remove_eliminated_inputs(problem: Problem, transform: AffineTransform) -> Problem:
"""Eliminates remaining occurences of eliminated inputs in linear constraints."""
inputs_names = problem.inputs.names
M = len(inputs_names)
# write the equalities for the backtransformation into one matrix
inputs_dict = {inputs_names[i]: i for i in range(M)}
# build up dict from problem.equalities e.g. {"xi1": [coeff(xj1), ..., coeff(xjn)], ... "xik":...}
coeffs_dict = {}
for i, e in enumerate(transform.equalities):
coeffs = np.zeros(M + 1)
for j, name in enumerate(e[1]):
coeffs[inputs_dict[name]] = e[2][j]
coeffs[-1] = e[2][-1]
coeffs_dict[e[0]] = coeffs
constraints = []
for c in problem.constraints:
# Nonlinear constraints supported
if not isinstance(c, (LinearEquality, LinearInequality)):
raise Exception(
"Elimination of variables is only supported for LinearEquality and LinearInequality constraints."
)
# no changes, if the constraint does not contain eliminated inputs
elif all(name in inputs_names for name in c.names):
constraints.append(c)
# remove inputs from the constraint that were eliminated from the inputs before
else:
_names = np.array(inputs_names)
_rhs = c.rhs
# create new lhs and rhs from the old one and knowledge from problem._equalities
_lhs = np.zeros(M)
for j, name in enumerate(c.names):
if name in inputs_names:
_lhs[inputs_dict[name]] += c.lhs[j]
else:
_lhs += c.lhs[j] * coeffs_dict[name][:-1]
_rhs -= c.lhs[j] * coeffs_dict[name][-1]
_names = _names[np.abs(_lhs) > 1e-16]
_lhs = _lhs[np.abs(_lhs) > 1e-16]
# create new Constraints
if isinstance(c, LinearEquality):
_c = LinearEquality(_names, _lhs, _rhs)
else:
_c = LinearInequality(_names, _lhs, _rhs)
# check if constraint is always fulfilled/not fulfilled
if len(_c.names) == 0 and _c.rhs >= 0:
pass
elif len(_c.names) == 0 and _c.rhs < 0:
raise Exception("Linear constraints cannot be fulfilled.")
elif np.isinf(_c.rhs):
pass
else:
constraints.append(_c)
problem.constraints = Constraints(constraints)
return problem
rref(A, tol=1e-08)
Computes the reduced row echelon form of a Matrix
Parameters:
Name | Type | Description | Default |
---|---|---|---|
A |
ndarray |
2d array representing a matrix. |
required |
tol |
float |
tolerance for rounding to 0 |
1e-08 |
Returns:
Type | Description |
---|---|
Tuple[numpy.ndarray, List[int]] |
(A_rref, pivots), where A_rref is the reduced row echelon form of A and pivots is a numpy array containing the pivot columns of A_rref |
Source code in opti/tools/reduce.py
def rref(A: np.ndarray, tol=1e-8) -> Tuple[np.ndarray, List[int]]:
"""Computes the reduced row echelon form of a Matrix
Args:
A (ndarray): 2d array representing a matrix.
tol (float): tolerance for rounding to 0
Returns:
(A_rref, pivots), where A_rref is the reduced row echelon form of A and pivots
is a numpy array containing the pivot columns of A_rref
"""
A = np.array(A, dtype=np.float64)
n, m = np.shape(A)
col = 0
row = 0
pivots = []
for col in range(m):
# does a pivot element exist?
if all(np.abs(A[row:, col]) < tol):
pass
# if yes: start elimination
else:
pivots.append(col)
max_row = np.argmax(np.abs(A[row:, col])) + row
# switch to most stable row
A[[row, max_row], :] = A[[max_row, row], :]
# normalize row
A[row, :] /= A[row, col]
# eliminate other elements from column
for r in range(n):
if r != row:
A[r, :] -= A[r, col] / A[row, col] * A[row, :]
row += 1
prec = int(-np.log10(tol))
return np.round(A, prec), pivots
sanitize
sanitize_problem(problem)
This creates a transformation of the problem with sanitized data. Thereby, we try to preserve relationships between inputs, outputs, and objectives.
More precisely, the resulting problem has the following properties:
- Inputs are named input_0
, input_1
, .... Outputs are named analogously.
- The data is scaled per feature to [0, 1]
.
- Coefficients of linear constraints are adapted to the data scaling.
- Models and evaluatons in terms of f
are dropped if there are any.
Currently unsuported are problems with - discrete or categorical variables, - nonlinear (in)equality input constraints, or - output constraints.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
problem |
Problem |
to be sanitized |
required |
Exceptions:
Type | Description |
---|---|
TypeError |
in case there are unsupported constraints, data is None, or there are output constraints |
Returns:
Type | Description |
---|---|
Problem |
Problem instance with sanitized labels and normalized data |
Source code in opti/tools/sanitize.py
def sanitize_problem(problem: Problem) -> Problem:
"""
This creates a transformation of the problem with sanitized data. Thereby, we try
to preserve relationships between inputs, outputs, and objectives.
More precisely, the resulting problem has the following properties:
- Inputs are named `input_0`, `input_1`, .... Outputs are named analogously.
- The data is scaled per feature to `[0, 1]`.
- Coefficients of linear constraints are adapted to the data scaling.
- Models and evaluatons in terms of `f` are dropped if there are any.
Currently unsuported are problems with
- discrete or categorical variables,
- nonlinear (in)equality input constraints, or
- output constraints.
Args:
problem: to be sanitized
Raises:
TypeError: in case there are unsupported constraints, data is None, or there are output constraints
Returns:
Problem instance with sanitized labels and normalized data
"""
if problem.data is None:
raise TypeError("we cannot sanitize a problem without data")
if problem.output_constraints is not None:
raise TypeError("output constraints are currently not supported")
if getattr(problem, "f", None) is not None:
warnings.warn("f is not sanitized but dropped")
if problem.models is not None:
warnings.warn("models are not sanitized but dropped")
inputs = _sanitize_params(problem.inputs, "input")
input_name_map = {pi.name: i.name for pi, i in zip(problem.inputs, inputs)}
normalized_in_data, xmin, Δx = _normalize_parameters_data(
problem.data, problem.inputs
)
outputs = _sanitize_params(problem.outputs, "output")
output_name_map = {pi.name: i.name for pi, i in zip(problem.outputs, outputs)}
normalized_out_data, ymin, Δy = _normalize_parameters_data(
problem.data, problem.outputs
)
normalized_in_data.columns = inputs.names
normalized_out_data.columns = outputs.names
normalized_data = pd.concat([normalized_in_data, normalized_out_data], axis=1)
normalized_data.reset_index(inplace=True, drop=True)
objectives = deepcopy(problem.objectives)
for obj in objectives:
sanitized_name = output_name_map[obj.name]
i = outputs.names.index(sanitized_name)
obj.name = sanitized_name
obj.parameter = sanitized_name
obj.target = (obj.target - ymin[i]) / Δy[i]
if hasattr(obj, "tolerance"):
obj.tolerance /= Δy[i]
constraints = deepcopy(problem.constraints)
if constraints is not None:
for c in constraints:
c.names = [input_name_map[n] for n in c.names]
if isinstance(c, (LinearEquality, LinearInequality)):
c.lhs = (c.lhs + xmin) * Δx
if c.rhs > 1e-5:
c.lhs = c.lhs / c.rhs
c.rhs = 1.0
elif isinstance(c, NChooseK):
pass
else:
raise TypeError(
"sanitizer only supports linear and n-choose-k constraints"
)
normalized_problem = Problem(
inputs=inputs,
outputs=outputs,
objectives=objectives,
constraints=constraints,
data=normalized_data,
)
return normalized_problem