# Source code for gemseo_calibration.scenario

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""A module to calibrate a multidisciplinary system from data."""
from __future__ import annotations

import logging
from typing import Any
from typing import ClassVar
from typing import Iterable
from typing import Sequence

from gemseo.algos.design_space import DesignSpace
from gemseo.core.dataset import Dataset
from gemseo.core.discipline import MDODiscipline
from gemseo.core.mdo_scenario import MDOScenario
from gemseo.core.mdofunctions.mdo_function import MDOFunction
from numpy import ndarray

from gemseo_calibration.calibrator import CalibrationMeasure
from gemseo_calibration.calibrator import Calibrator
from gemseo_calibration.post.factory import CalibrationPostFactory

LOGGER = logging.getLogger(__name__)

[docs]class CalibrationScenario(MDOScenario):
"""A :class:.Scenario to calibrate a multidisciplinary system from reference data.

Set from parameters,
this multidisciplinary system computes output data from input data.

The reference input-output data are used to calibrate the parameters
so that the model output data are close to the reference output data
for some outputs of interest.
This distance is evaluated with a :class:.CalibrationMeasure.
to compare the discipline outputs with the reference data.

Warning:
Just like inputs,
the parameters should be defined in the input grammars of the disciplines.

The parameters are calibrated with the method :meth:.execute
from an optimizer and a reference input-output :class:.Dataset.
"""

prior_model_data: Dataset | None
"""The model data before the calibration."""

posterior_model_data: Dataset | None
"""The model data after the calibration."""

__REFERENCE_DATA: ClassVar[str] = "reference_data"
"""The input of the CalibrationScenario representing the reference data."""

def __init__(
self,
disciplines: MDODiscipline | list[MDODiscipline],
input_names: str | Iterable[str],
control_outputs: CalibrationMeasure | Sequence[CalibrationMeasure],
calibration_space: DesignSpace,
formulation: str = "MDF",
name: str | None = None,
**formulation_options: Any,
) -> None:  # noqa: D205,D212,D415
"""
Args:
disciplines: The disciplines
whose parameters must be calibrated from the reference data.
input_names: The names of the inputs to be considered for the calibration.
control_outputs: The names of the outputs used to calibrate the disciplines
with the name of the calibration measure and the corresponding weight
comprised between 0 and 1 (the weights must sum to 1).
When the output is a 1D function discretized over an irregular mesh,
the name of the mesh can be provided.
E.g. CalibrationMeasure(output="z", measure="MSE")
CalibrationMeasure(output="z", measure="MSE", weight=0.3)
or CalibrationMeasure(output="z", measure="MSE", mesh="z_mesh")
Lastly, CalibrationMeasure can be imported
from :mod:gemseo-calibration.scenario.
calibration_space: The space of the parameters to be calibrated,
whose current values are consider as a prior for calibration.
formulation: The name of a formulation
to manage the multidisciplinary coupling.
name: A name for this calibration scenario.
If None, use the name of the class.
**formulation_options: The options of the formulation.
"""
self.__prior_parameters = calibration_space.get_current_value(as_dict=True)
self.__posterior_parameters = {}
self.prior_model_data = None
self.posterior_model_data = None
calibrator = Calibrator(
disciplines,
input_names,
control_outputs,
calibration_space.variables_names,
formulation=formulation,
**formulation_options,
)
super().__init__(
[calibrator],
"DisciplinaryOpt",
calibrator.objective_name,
calibration_space,
name=name or self.__class__.__name__,
maximize_objective=calibrator.maximize_objective_measure,
)
self.__calibration_post_factory = CalibrationPostFactory()

def _run_algorithm(self) -> None:
self.calibrator.set_reference_data(self.local_data[self.__REFERENCE_DATA])
self.calibrator.execute()
self.prior_model_data = self.calibrator.scenario.export_to_dataset()
super()._run_algorithm()
self.__posterior_parameters = self.design_space.array_to_dict(
self.optimization_result.x_opt
)
self.calibrator.default_inputs = self.posterior_parameters
self.calibrator.execute()
self.posterior_model_data = self.calibrator.scenario.export_to_dataset()

@property
def calibrator(self) -> Calibrator:
"""The discipline computing calibration measures from the parameter values."""
return self.formulation.disciplines[0]

@property
def prior_parameters(self) -> dict[str, ndarray]:
"""The values of the parameters before the calibration stage."""
return self.__prior_parameters

@property
def posterior_parameters(self) -> dict[str, ndarray]:
"""The values of the parameters after the calibration stage."""
return self.__posterior_parameters

self,
control_outputs: CalibrationMeasure | Iterable[CalibrationMeasure],
constraint_type: str = MDOFunction.TYPE_EQ,
constraint_name: str | None = None,
value: str | float = None,
positive: bool = False,
) -> None:
"""Define a constraint from a calibration measure related to discipline outputs.

Args:
control_outputs: The names of the outputs used to calibrate the disciplines
with the name of the calibration measure and the corresponding weight
comprised between 0 and 1 (the weights must sum to 1).
When the output is a 1D function discretized over an irregular mesh,
the name of the mesh can be provided.
E.g. CalibrationMeasure(output="z", measure="MSE")
CalibrationMeasure(output="z", measure="MSE", weight=0.3)
or CalibrationMeasure(output="z", measure="MSE", mesh="z_mesh")
Lastly, CalibrationMeasure can be imported
from :mod:gemseo-calibration.scenario.
constraint_type: The type of constraint,
"eq" for equality constraint and
"ineq" for inequality constraint.
constraint_name: The name of the constraint to be stored.
If None,
the name of the constraint is generated from the output name.
value: The value for which the constraint is active.
If None, this value is 0.
positive: Whether to consider the inequality constraint as positive.
"""
if isinstance(control_outputs, CalibrationMeasure):
control_outputs = [control_outputs]

output_name, constraint_type, constraint_name, value, positive
)

@property
def posts(self) -> list[str]:
# noqa: D102
return self.post_factory.posts + self.__calibration_post_factory.posts

[docs]    def post_process(self, post_name: str, **options: Any) -> None:
# noqa: D102
if post_name in self.__calibration_post_factory.posts:
return self.__calibration_post_factory.execute(
self.formulation.opt_problem,
self.calibrator.reference_data,
self.prior_model_data,
self.posterior_model_data,
post_name,
**options,
)

return super().post_process(post_name, **options)