//  [python]
def test_objective_error():
    def _check_error_for_data(v1_data_, v2_data_, error_, error_type):
        expected_error = torch.cat([v1_data_, v2_data_], dim=1) * w

        if error_type == "error":
            torch.testing.assert_close(error_, expected_error)
        else:
            torch.testing.assert_close(error_, 0.5 * (expected_error.norm(dim=1) ** 2))

    def _check_variables(objective, input_tensors, v1_data, v2_data, also_update):
        if also_update:
            assert objective.optim_vars["v1"].tensor is input_tensors["v1"]
            assert objective.optim_vars["v2"].tensor is input_tensors["v2"]
        else:
            assert objective.optim_vars["v1"].tensor is not input_tensors["v1"]
            assert objective.optim_vars["v2"].tensor is not input_tensors["v2"]

            assert objective.optim_vars["v1"].tensor is v1_data
            assert objective.optim_vars["v2"].tensor is v2_data

    def _check_error_and_variables(
        v1_data_, v2_data_, error_, error_type, objective, input_tensors, also_update
    ):
        _check_error_for_data(v1_data_, v2_data_, error_, error_type)

        _check_variables(objective, input_tensors, v1_data, v2_data, also_update)

    for _ in range(10):
        f1, f2 = np.random.random(), np.random.random()
        dof = np.random.randint(2, 10)
        batch_size = np.random.randint(2, 10)
        v1 = th.Vector(dof=dof, name="v1")
        v2 = th.Vector(dof=dof, name="v2")
        z = th.Vector(tensor=torch.zeros(batch_size, dof), name="z")

        w = np.random.random()
        # This cost functions will just compute the norm of each vector, scaled by w
        weight = th.ScaleCostWeight(w)
        d1 = th.Difference(v1, z, weight, name="d1")
        d2 = th.Difference(v2, z, weight, name="d2")

        objective = th.Objective()
        objective.add(d1)
        objective.add(d2)

        v1_data = torch.ones(batch_size, dof) * f1
        v2_data = torch.ones(batch_size, dof) * f2
        objective.update({"v1": v1_data, "v2": v2_data})
        error = objective.error()
        _check_error_for_data(v1_data, v2_data, error, "error")
        error_norm_2 = objective.error_metric()

        assert error.shape == (batch_size, 2 * dof)
        _check_error_for_data(v1_data, v2_data, error_norm_2, "error_norm_2")

        v1_data_new = torch.ones(batch_size, dof) * f1 * 0.1
        v2_data_new = torch.ones(batch_size, dof) * f2 * 0.1

        input_tensors = {"v1": v1_data_new, "v2": v2_data_new}

        error = objective.error(input_tensors=input_tensors, also_update=False)

        _check_error_and_variables(
            v1_data_new,
            v2_data_new,
            error,
            "error",
            objective,
            input_tensors,
            also_update=False,
        )

        v1_data_new = torch.ones(batch_size, dof) * f1 * 0.3
        v2_data_new = torch.ones(batch_size, dof) * f2 * 0.3

        input_tensors = {"v1": v1_data_new, "v2": v2_data_new}

        error_norm_2 = objective.error_metric(
            input_tensors=input_tensors, also_update=False
        )

        _check_error_and_variables(
            v1_data_new,
            v2_data_new,
            error_norm_2,
            "error_norm_2",
            objective,
            input_tensors,
            also_update=False,
        )

        v1_data_new = torch.ones(batch_size, dof) * f1 * 0.4
        v2_data_new = torch.ones(batch_size, dof) * f2 * 0.4

        input_tensors = {"v1": v1_data_new, "v2": v2_data_new}

        error = objective.error(input_tensors=input_tensors, also_update=True)

        _check_error_and_variables(
            v1_data_new,
            v2_data_new,
            error,
            "error",
            objective,
            input_tensors,
            also_update=True,
        )

        v1_data_new = torch.ones(batch_size, dof) * f1 * 0.4
        v2_data_new = torch.ones(batch_size, dof) * f2 * 0.4

        input_tensors = {"v1": v1_data_new, "v2": v2_data_new}

        error_norm_2 = objective.error_metric(
            input_tensors=input_tensors, also_update=True
        )

        _check_error_and_variables(
            v1_data_new,
            v2_data_new,
            error_norm_2,
            "error_norm_2",
            objective,
            input_tensors,
            also_update=True,
        )