Ejemplo n.º 1
0
def test_sum2():
    # Check it returns the same results with casadi and numpy
    a = np.array([[1, 2, 3], [1, 2, 3]])
    b = cas.SX(a)

    assert np.all(np.sum(a) == cas.DM(np.sum(b)))
    assert np.all(np.sum(a, axis=1) == cas.DM(np.sum(b, axis=1)))
Ejemplo n.º 2
0
def test_norm_2D():
    a = np.arange(9).reshape(3, 3)
    cas_a = cas.DM(a)

    assert np.linalg.norm(cas_a) == np.linalg.norm(a)

    assert np.all(np.linalg.norm(cas_a, axis=0) == np.linalg.norm(a, axis=0))

    assert np.all(np.linalg.norm(cas_a, axis=1) == np.linalg.norm(a, axis=1))
def test_cross_2D_input_first_axis():
    a = np.tile(np.array([1, 1, 1]), (3, 1)).T
    b = np.tile(np.array([1, 2, 3]), (3, 1)).T

    cas_a = cas.DM(a)
    cas_b = cas.DM(b)

    correct_result = np.cross(a, b, axis=0)
    cas_correct_result = cas.DM(correct_result)

    assert np.all(np.cross(a, cas_b, axis=0) == cas_correct_result)
    assert np.all(np.cross(cas_a, b, axis=0) == cas_correct_result)
    assert np.all(np.cross(cas_a, cas_b, axis=0) == cas_correct_result)
def test_cross_1D_input():
    a = np.array([1, 1, 1])
    b = np.array([1, 2, 3])

    cas_a = cas.DM(a)
    cas_b = cas.DM(b)

    correct_result = np.cross(a, b)
    cas_correct_result = cas.DM(correct_result)

    assert np.all(np.cross(a, cas_b) == cas_correct_result)
    assert np.all(np.cross(cas_a, b) == cas_correct_result)
    assert np.all(np.cross(cas_a, cas_b) == cas_correct_result)
Ejemplo n.º 5
0
def test_basic_logicals_numpy():
    a = np.array([True, True, False, False])
    b = np.array([True, False, True, False])

    assert np.all(
        a & b == np.array([True, False, False, False])
    )
Ejemplo n.º 6
0
def test_cas_vector():
    output = reflect_over_XZ_plane(cas.DM(vec))
    assert isinstance(output, cas.DM)
    assert np.all(
        output ==
        np.array([0, -1, 2])
    )
Ejemplo n.º 7
0
def test_numpy_equivalency_1D():
    inputs = [1, 2]

    a = array(inputs)
    a_np = np.array(inputs)

    assert np.all(a == a_np)
Ejemplo n.º 8
0
def test_where_casadi():
    a = cas.GenDM_ones(4)
    b = 2 * cas.GenDM_ones(4)

    c = np.where(cas.DM([1, 0, 1, 0]), a, b)

    assert np.all(c == cas.DM([1, 2, 1, 2]))
Ejemplo n.º 9
0
def test_reshape():
    a = np.array([
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],
        [10, 11, 12],
    ])
    b = cas.DM(a)

    test_inputs = [
        -1,
        (4, 3),
        (3, 4),
        (12, 1),
        (1, 12),
        (-1),
        (12, -1),
        (-1, 12),
    ]

    for i in test_inputs:
        ra = np.reshape(a, i)
        rb = np.reshape(b, i)
        if len(ra.shape) == 1:
            ra = ra.reshape(-1, 1)

        assert np.all(ra == rb)
Ejemplo n.º 10
0
def test_array_numpy_equivalency_2D():
    inputs = [[1, 2], [3, 4]]

    a = array(inputs)
    a_np = np.array(inputs)

    assert np.all(a == a_np)
Ejemplo n.º 11
0
def test_where_numpy():
    a = np.ones(4)
    b = 2 * np.ones(4)

    c = np.where(np.array([True, False, True, False]), a, b)

    assert np.all(c == np.array([1, 2, 1, 2]))
Ejemplo n.º 12
0
def test_np_square():
    assert np.all(
        reflect_over_XZ_plane(square) ==
        np.array([
            [0, -1, 2],
            [3, -4, 5],
            [6, -7, 8]
        ])
    )
Ejemplo n.º 13
0
def test_interpolated_model_at_vector():
    model = interpolated_model()
    x_data = {
        "x1": np.array([1.5, 2.5]),
        "x2": np.array([2.5, 3.5]),
    }
    assert np.all(
        model(x_data) == pytest.approx(underlying_function_2D(
            *x_data.values())))
Ejemplo n.º 14
0
def test_np_rectangular_tall():
    assert np.all(
        reflect_over_XZ_plane(rectangular_tall) ==
        np.array([
            [0, -1, 2],
            [3, -4, 5],
            [6, -7, 8],
            [9, -10, 11],
        ])
    )
Ejemplo n.º 15
0
def test_cas_square():
    output = reflect_over_XZ_plane(cas.DM(square))
    assert isinstance(output, cas.DM)
    assert np.all(
        output ==
        np.array([
            [0, -1, 2],
            [3, -4, 5],
            [6, -7, 8]
        ])
    )
Ejemplo n.º 16
0
def test_cas_rectangular_tall():
    output = reflect_over_XZ_plane(cas.DM(rectangular_tall))
    assert isinstance(output, cas.DM)
    assert np.all(
        output ==
        np.array([
            [0, -1, 2],
            [3, -4, 5],
            [6, -7, 8],
            [9, -10, 11],
        ])
    )
Ejemplo n.º 17
0
def test_interpn_linear_multiple_samples():
    ### NumPy test

    def value_func_3d(x, y, z):
        return 2 * x + 3 * y - z

    x = np.linspace(0, 5, 10)
    y = np.linspace(0, 5, 20)
    z = np.linspace(0, 5, 30)
    points = (x, y, z)
    values = value_func_3d(*np.meshgrid(*points, indexing="ij"))

    point = np.array([
        [2.21, 3.12, 1.15],
        [3.42, 0.81, 2.43]
    ])
    value = np.interpn(
        points, values, point
    )
    assert np.all(
        value == pytest.approx(
            value_func_3d(
                *[
                    point[:, i] for i in range(point.shape[1])
                ]
            )
        )
    )
    assert len(value) == 2

    ### CasADi test
    point = cas.DM(point)
    value = np.interpn(
        points, values, point
    )
    value_actual = value_func_3d(
        *[
            np.array(point[:, i]) for i in range(point.shape[1])
        ]
    )
    for i in range(len(value)):
        assert value[i] == pytest.approx(float(value_actual[i]))
    assert value.shape == (2,)
Ejemplo n.º 18
0
    def subject_to(self,
                   constraint: Union[cas.MX, bool, List],  # TODO add scale
                   ) -> cas.MX:
        """
        Initialize a new equality or inequality constraint(s).

        Args:
            constraint: A constraint that you want to hold true at the optimum.

                Inequality example:
                >>> x = opti.variable()
                >>> opti.subject_to(x >= 5)

                Equality example; also showing that you can directly constrain functions of variables:
                >>> x = opti.variable()
                >>> f = np.sin(x)
                >>> opti.subject_to(f == 0.5)

                You can also pass in a list of multiple constraints using list syntax. For example:
                >>> x = opti.variable()
                >>> opti.subject_to([
                >>>     x >= 5,
                >>>     x <= 10
                >>> ])

        Returns: The dual variable associated with the new constraint. If the `constraint` input is a list, returns
            a list of dual variables.

        """
        # Determine whether you're dealing with a single (possibly vectorized) constraint or a list of constraints.
        # If the latter, recursively apply them.
        if type(constraint) in (list, tuple):
            return [
                self.subject_to(each_constraint)  # return the dual of each constraint
                for each_constraint in constraint
            ]

        # If it's a proper constraint (MX-type and non-parametric),
        # pass it into the parent class Opti formulation and be done with it.
        if isinstance(constraint, cas.MX) and not self.advanced.is_parametric(constraint):
            super().subject_to(constraint)
            dual = self.dual(constraint)

            return dual
        else:  # Constraint is not valid because it is not MX type or is parametric.
            try:
                constraint_satisfied = np.all(self.value(constraint))
            except:
                raise TypeError(f"""Opti.subject_to could not determine the truthiness of your constraint, and it
                    doesn't appear to be a symbolic type or a boolean type. You supplied the following constraint:
                    {constraint}""")

            if constraint_satisfied or self.ignore_violated_parametric_constraints:
                # If the constraint(s) always evaluates True (e.g. if you enter "5 > 3"), skip it.
                # This allows you to toggle frozen variables without causing problems with setting up constraints.
                return None  # dual of an always-true constraint doesn't make sense to evaluate.
            else:
                # If any of the constraint(s) are always False (e.g. if you enter "5 < 3"), raise an error.
                # This indicates that the problem is infeasible as-written, likely because the user has frozen too
                # many decision variables using the Opti.variable(freeze=True) syntax.
                raise RuntimeError(f"""The problem is infeasible due to a constraint that always evaluates False. 
                This can happen if you've frozen too many decision variables, leading to an overconstrained problem.""")
Ejemplo n.º 19
0
def test_roll_casadi_2d():
    a = np.array([[1, 2, 3], [4, 5, 6]])
    b = cas.SX(a)

    assert np.all(cas.DM(np.roll(b, 1, axis=1)) == np.roll(a, 1, axis=1))
Ejemplo n.º 20
0
def test_roll_casadi():
    b = np.array([[3, 1, 2]])
    a = cas.SX(b)

    assert np.all(cas.DM(np.roll(a, 1)) == b)
Ejemplo n.º 21
0
def test_roll_onp():
    a = [1, 2, 3]
    b = [3, 1, 2]

    assert np.all(np.roll(a, 1) == b)
Ejemplo n.º 22
0
def test_can_convert_DM_to_ndarray():
    c = cas.DM([1, 2, 3])
    n = np.array(c)

    assert np.all(n == np.array([1, 2, 3]))
Ejemplo n.º 23
0
    def __init__(
        self,
        model: Callable[
            [Union[np.ndarray,
                   Dict[str, np.ndarray]], Dict[str, float]], np.ndarray],
        x_data: Union[np.ndarray, Dict[str, np.ndarray]],
        y_data: np.ndarray,
        parameter_guesses: Dict[str, float],
        parameter_bounds: Dict[str, tuple] = None,
        residual_norm_type: str = "L2",
        fit_type: str = "best",
        weights: np.ndarray = None,
        put_residuals_in_logspace: bool = False,
        verbose=True,
    ):
        """
        Fits an analytical model to n-dimensional unstructured data using an automatic-differentiable optimization approach.

        Args:

            model: The model that you want to fit your dataset to. This is a callable with syntax f(x, p) where:

                * x is a dict of dependent variables. Same format as x_data [dict of 1D ndarrays of length n].

                    * If the model is one-dimensional (e.g. f(x1) instead of f(x1, x2, x3...)), you can instead interpret x
                    as a 1D ndarray. (If you do this, just give `x_data` as an array.)

                * p is a dict of parameters. Same format as param_guesses [dict with syntax param_name:param_value].

                Model should return a 1D ndarray of length n.

                Basically, if you've done it right:
                >>> model(x_data, parameter_guesses)
                should evaluate to a 1D ndarray where each x_data is mapped to something analogous to y_data. (The fit
                will likely be bad at this point, because we haven't yet optimized on param_guesses - but the types
                should be happy.)

                Model should use aerosandbox.numpy operators.

                The model is not allowed to make any in-place changes to the input `x`. The most common way this
                manifests itself is if someone writes something to the effect of `x += 3` or similar. Instead, write `x =
                x + 3`.

            x_data: Values of the dependent variable(s) in the dataset to be fitted. This is a dictionary; syntax is {
            var_name:var_data}.

                * If the model is one-dimensional (e.g. f(x1) instead of f(x1, x2, x3...)), you can instead supply x_data
                as a 1D ndarray. (If you do this, just treat `x` as an array in your model, not a dict.)

            y_data: Values of the independent variable in the dataset to be fitted. [1D ndarray of length n]

            parameter_guesses: a dict of fit parameters. Syntax is {param_name:param_initial_guess}.

                * Parameters will be initialized to the values set here; all parameters need an initial guess.

                * param_initial_guess is a float; note that only scalar parameters are allowed.

            parameter_bounds: Optional: a dict of bounds on fit parameters. Syntax is {"param_name":(min, max)}.

                * May contain only a subset of param_guesses if desired.

                * Use None to represent one-sided constraints (i.e. (None, 5)).

            residual_norm_type: What error norm should we minimize to optimize the fit parameters? Options:

                * "L1": minimize the L1 norm or sum(abs(error)). Less sensitive to outliers.

                * "L2": minimize the L2 norm, also known as the Euclidian norm, or sqrt(sum(error ** 2)). The default.

                * "Linf": minimize the L_infinty norm or max(abs(error)). More sensitive to outliers.

            fit_type: Should we find the model of best fit (i.e. the model that minimizes the specified residual norm),
            or should we look for a model that represents an upper/lower bound on the data (useful for robust surrogate
            modeling, so that you can put bounds on modeling error):

                * "best": finds the model of best fit. Usually, this is what you want.

                * "upper bound": finds a model that represents an upper bound on the data (while still trying to minimize
                the specified residual norm).

                * "lower bound": finds a model that represents a lower bound on the data (while still trying to minimize
                the specified residual norm).

            weights: Optional: weights for data points. If not supplied, weights are assumed to be uniform.

                * Weights are automatically normalized. [1D ndarray of length n]

            put_residuals_in_logspace: Whether to optimize using the logarithmic error as opposed to the absolute error
            (useful for minimizing percent error).

            Note: If any model outputs or data are negative, this will raise an error!

            verbose: Should the progress of the optimization solve that is part of the fitting be displayed? See
            `aerosandbox.Opti.solve(verbose=)` syntax for more details.

        Returns: A model in the form of a FittedModel object. Some things you can do:
            >>> y = FittedModel(x) # evaluate the FittedModel at new x points
            >>> FittedModel.parameters # directly examine the optimal values of the parameters that were found
            >>> FittedModel.plot() # plot the fit


        """
        super().__init__()

        ##### Prepare all inputs, check types/sizes.

        ### Flatten all inputs
        def flatten(input):
            return np.array(input).flatten()

        try:
            x_data = {k: flatten(v) for k, v in x_data.items()}
            x_data_is_dict = True
        except AttributeError:  # If it's not a dict or dict-like, assume it's a 1D ndarray dataset
            x_data = flatten(x_data)
            x_data_is_dict = False
        y_data = flatten(y_data)
        n_datapoints = np.length(y_data)

        ### Handle weighting
        if weights is None:
            weights = np.ones(n_datapoints)
        else:
            weights = flatten(weights)
        sum_weights = np.sum(weights)
        if sum_weights <= 0:
            raise ValueError("The weights must sum to a positive number!")
        if np.any(weights < 0):
            raise ValueError(
                "No entries of the weights vector are allowed to be negative!")
        weights = weights / np.sum(
            weights)  # Normalize weights so that they sum to 1.

        ### Check format of parameter_bounds input
        if parameter_bounds is None:
            parameter_bounds = {}
        for param_name, v in parameter_bounds.items():
            if param_name not in parameter_guesses.keys():
                raise ValueError(
                    f"A parameter name (key = \"{param_name}\") in parameter_bounds was not found in parameter_guesses."
                )
            if not np.length(v) == 2:
                raise ValueError(
                    "Every value in parameter_bounds must be a tuple in the format (lower_bound, upper_bound). "
                    "For one-sided bounds, use None for the unbounded side.")

        ### If putting residuals in logspace, check positivity
        if put_residuals_in_logspace:
            if not np.all(y_data > 0):
                raise ValueError(
                    "You can't fit a model with residuals in logspace if y_data is not entirely positive!"
                )

        ### Check dimensionality of inputs to fitting algorithm
        relevant_inputs = {
            "y_data": y_data,
            "weights": weights,
        }
        try:
            relevant_inputs.update(x_data)
        except TypeError:
            relevant_inputs.update({"x_data": x_data})

        for key, value in relevant_inputs.items():
            # Check that the length of the inputs are consistent
            series_length = np.length(value)
            if not series_length == n_datapoints:
                raise ValueError(
                    f"The supplied data series \"{key}\" has length {series_length}, but y_data has length {n_datapoints}."
                )

        ##### Formulate and solve the fitting optimization problem

        ### Initialize an optimization environment
        opti = Opti()

        ### Initialize the parameters as optimization variables
        params = {}
        for param_name, param_initial_guess in parameter_guesses.items():
            if param_name in parameter_bounds:
                params[param_name] = opti.variable(
                    init_guess=param_initial_guess,
                    lower_bound=parameter_bounds[param_name][0],
                    upper_bound=parameter_bounds[param_name][1],
                )
            else:
                params[param_name] = opti.variable(
                    init_guess=param_initial_guess, )

        ### Evaluate the model at the data points you're trying to fit
        x_data_original = copy.deepcopy(
            x_data
        )  # Make a copy of x_data so that you can determine if the model did in-place operations on x and tattle on the user.

        try:
            y_model = model(x_data, params)  # Evaluate the model
        except Exception:
            raise Exception("""
            There was an error when evaluating the model you supplied with the x_data you supplied.
            Likely possible causes:
                * Your model() does not have the call syntax model(x, p), where x is the x_data and p are parameters.
                * Your model should take in p as a dict of parameters, but it does not.
                * Your model assumes x is an array-like but you provided x_data as a dict, or vice versa.
            See the docstring of FittedModel() if you have other usage questions or would like to see examples.
            """)

        try:  ### If the model did in-place operations on x_data, throw an error
            x_data_is_unchanged = np.all(x_data == x_data_original)
        except ValueError:
            x_data_is_unchanged = np.all([
                x_series == x_series_original
                for x_series, x_series_original in zip(x_data, x_data_original)
            ])
        if not x_data_is_unchanged:
            raise TypeError(
                "model(x_data, parameter_guesses) did in-place operations on x, which is not allowed!"
            )
        if y_model is None:  # Make sure that y_model actually returned something sensible
            raise TypeError(
                "model(x_data, parameter_guesses) returned None, when it should've returned a 1D ndarray."
            )

        ### Compute how far off you are (error)
        if not put_residuals_in_logspace:
            error = y_model - y_data
        else:
            y_model = np.fmax(
                y_model, 1e-300
            )  # Keep y_model very slightly always positive, so that log() doesn't NaN.
            error = np.log(y_model) - np.log(y_data)

        ### Set up the optimization problem to minimize some norm(error), which looks different depending on the norm used:
        if residual_norm_type.lower() == "l1":  # Minimize the L1 norm
            abs_error = opti.variable(init_guess=0, n_vars=np.length(
                y_data))  # Make the abs() of each error entry an opt. var.
            opti.subject_to([
                abs_error >= error,
                abs_error >= -error,
            ])
            opti.minimize(np.sum(weights * abs_error))

        elif residual_norm_type.lower() == "l2":  # Minimize the L2 norm
            opti.minimize(np.sum(weights * error**2))

        elif residual_norm_type.lower(
        ) == "linf":  # Minimize the L-infinity norm
            linf_value = opti.variable(
                init_guess=0
            )  # Make the value of the L-infinity norm an optimization variable
            opti.subject_to([
                linf_value >= weights * error, linf_value >= -weights * error
            ])
            opti.minimize(linf_value)

        else:
            raise ValueError("Bad input for the 'residual_type' parameter.")

        ### Add in the constraints specified by fit_type, which force the model to stay above / below the data points.
        if fit_type == "best":
            pass
        elif fit_type == "upper bound":
            opti.subject_to(y_model >= y_data)
        elif fit_type == "lower bound":
            opti.subject_to(y_model <= y_data)
        else:
            raise ValueError("Bad input for the 'fit_type' parameter.")

        ### Solve
        sol = opti.solve(verbose=verbose)

        ##### Construct a FittedModel

        ### Create a vector of solved parameters
        params_solved = {}
        for param_name in params:
            try:
                params_solved[param_name] = sol.value(params[param_name])
            except:
                params_solved[param_name] = np.NaN

        ### Store all the data and inputs
        self.model = model
        self.x_data = x_data
        self.y_data = y_data
        self.parameters = params_solved
        self.parameter_guesses = parameter_guesses
        self.parameter_bounds = parameter_bounds
        self.residual_norm_type = residual_norm_type
        self.fit_type = fit_type
        self.weights = weights
        self.put_residuals_in_logspace = put_residuals_in_logspace
Ejemplo n.º 24
0
def test_diff():
    a = np.arange(100)

    assert np.all(np.diff(a) == pytest.approx(1))
Ejemplo n.º 25
0
def test_cumsum():
    n = np.arange(6).reshape((3, 2))
    c = cas.DM(n)

    assert np.all(np.cumsum(n) == np.array([0, 1, 3, 6, 10, 15]))
Ejemplo n.º 26
0
def test_interpolated_model_at_vector():
    model = interpolated_model()
    assert np.all(
        model(np.array([1.5, 2.5, 3.5])) ==
        pytest.approx(underlying_function_1D(np.array([1.5, 2.5, 3.5])))
    )
Ejemplo n.º 27
0
def test_np_vector():
    assert np.all(
        reflect_over_XZ_plane(vec) ==
        np.array([0, -1, 2])
    )
Ejemplo n.º 28
0
def test_np_vector_2D_wide():
    assert np.all(
        reflect_over_XZ_plane(np.expand_dims(vec, 0)) ==
        np.array([0, -1, 2])
    )
Ejemplo n.º 29
0
def test_invertability_of_diff_trapz():
    a = np.sin(np.arange(10))

    assert np.all(np.trapz(np.diff(a)) == pytest.approx(np.diff(np.trapz(a))))