# Source code for gemseo.mlearning.regression.regression

# -*- coding: utf-8 -*-
# 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.

# Contributors:
#    INITIAL AUTHORS - initial API and implementation and/or initial
#                         documentation
#        :author: Matthias De Lozzo
#        :author: Syver Doving Agdestein
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""
Regression model
================

The :mod:~gemseo.mlearning.regression.regression module
implements regression algorithms, where the goal is to find relationships
between continuous input and output variables. After being fitted to a learning
set, Regression algorithms can predict output values of new input data.

A regression algorithm consists of identifying a function
:math:f: \\mathbb{R}^{n_{\\textrm{inputs}}} \\to
\\mathbb{R}^{n_{\\textrm{outputs}}}. Given an input point
:math:x \\in \\mathbb{R}^{n_{\\textrm{inputs}}}, the predict method of the
regression algorithm will return the output point
:math:y = f(x) \\in \\mathbb{R}^{n_{\\textrm{outputs}}}. See
:mod:~gemseo.mlearning.core.supervised for more information.

Wherever possible, regression algorithms should also be able to compute the
Jacobian matrix of the function it has learned to represent. Given an input
point :math:x \\in \\mathbb{R}^{n_{\\textrm{inputs}}}, the Jacobian predict
method of the regression algorithm should thus return the matrix

.. math::

J_f(x) = \\frac{\\partial f}{\\partial x} =
\\begin{pmatrix}
\\frac{\\partial f_1}{\\partial x_1} & \\cdots & \\frac{\\partial f_1}
{\\partial x_{n_{\\textrm{inputs}}}}\\\\
\\vdots & \\ddots & \\vdots\\\\
\\frac{\\partial f_{n_{\\textrm{outputs}}}}{\\partial x_1} & \\cdots &
\\frac{\\partial f_{n_{\\textrm{outputs}}}}
{\\partial x_{n_{\\textrm{inputs}}}}
\\end{pmatrix}
\\in \\mathbb{R}^{n_{\\textrm{outputs}}\\times n_{\\textrm{inputs}}}.

This concept is implemented through
the :class:.MLRegressionAlgo class which
inherits from the :class:.MLSupervisedAlgo class.
"""
from __future__ import absolute_import, division, unicode_literals

from future import standard_library
from numpy import eye, matmul

from gemseo.mlearning.core.supervised import MLSupervisedAlgo
from gemseo.utils.data_conversion import DataConversion

standard_library.install_aliases()

[docs]class MLRegressionAlgo(MLSupervisedAlgo):
"""Machine Learning Regression Model Algorithm.

Inheriting classes should implement the :meth:!MLSupervisedAlgo._fit and
:meth:!MLSupervisedAlgo._predict methods, and
:meth:!MLRegressionAlgo._predict_jacobian method if possible.
"""

[docs]    class DataFormatters(MLSupervisedAlgo.DataFormatters):
""" Machine learning regression model decorators. """

[docs]        @classmethod
def format_dict_jacobian(cls, predict):
"""If input_data is passed as a dictionary, then convert it to
ndarray, and convert output_data to dictionary. Else, do nothing.

:param predict: Method whose input_data and output_data are to be
formatted.
"""

def wrapper(self, input_data, *args, **kwargs):
as_dict = isinstance(input_data, dict)
if as_dict:
input_data = DataConversion.dict_to_array(
input_data, self.input_names
)
single_sample = len(input_data.shape) == 1
jacobians = predict(self, input_data, *args, **kwargs)
if as_dict:
varsizes = self.learning_set.sizes
if single_sample:
jacobians = DataConversion.jac_2dmat_to_dict(
jacobians, self.output_names, self.input_names, varsizes
)
else:
jacobians = DataConversion.jac_3dmat_to_dict(
jacobians, self.output_names, self.input_names, varsizes
)
return jacobians

return wrapper

[docs]        @classmethod
def transform_jacobian(cls, predict_jac):
"""Apply transform to inputs, and inverse transform to outputs.

:param predict: Method whose input_data and output_data are to be
formatted.
"""

def wrapper(self, input_data, *args, **kwargs):
inputs = self.learning_set.INPUT_GROUP
if inputs in self.transformer:
jac = self.transformer[inputs].compute_jacobian(input_data)
input_data = self.transformer[inputs].transform(input_data)
else:
jac = eye(input_data.shape[1])

jac = matmul(predict_jac(self, input_data, *args, **kwargs), jac)
output_data = self.predict_raw(input_data)

outputs = self.learning_set.OUTPUT_GROUP
if outputs in self.transformer:
jac = matmul(
self.transformer[outputs].compute_jacobian_inverse(output_data),
jac,
)
return jac

return wrapper

[docs]    def predict_raw(self, input_data):
"""Predict output data from input data, assuming both are 2D.

:param ndarray input_data: input data (n_samples, n_inputs).
:return: output data (n_samples, n_outputs).
:rtype: ndarray(int)
"""
return self._predict(input_data)

[docs]    @DataFormatters.format_dict_jacobian
@DataFormatters.format_samples
@DataFormatters.transform_jacobian
def predict_jacobian(self, input_data):
"""Predict Jacobian of the regression model of input_data.

:param input_data: 1D input data.
:type input_data: dict(ndarray) or ndarray
:return: Jacobian for given input data.
:rtype: dict(dict(ndarray)) or ndarray
"""
return self._predict_jacobian(input_data)

def _predict_jacobian(self, input_data):
"""Predict Jacobian of the regression model for the given input data.

:param ndarray input_data: input_data (2D).
:return: Jacobian matrices (3D, one for each sample).
:rtype: ndarray
"""
raise NotImplementedError