Skip to content

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