# -*- coding: utf-8 -*-
# pylint: disable=missing-module-docstring
# imports...
# ...from site-packages
# ...from HydPy
from hydpy.core import devicetools
from hydpy.core import modeltools
from hydpy.core import objecttools
from hydpy.core.typingtools import *
from hydpy.cythons import modelutils
from hydpy.models.conv import conv_control
from hydpy.models.conv import conv_derived
from hydpy.models.conv import conv_fluxes
from hydpy.models.conv import conv_inlets
from hydpy.models.conv import conv_outlets
[docs]
class Calc_Outputs_V1(modeltools.Method):
    """Perform a simple proximity-based interpolation.
    Examples:
        With complete input data, method |Calc_Outputs_V1| performs the
        most simple nearest-neighbour approach:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> maxnmbinputs.value = 1
        >>> derived.nmboutputs(3)
        >>> derived.proximityorder.shape = (3, 1)
        >>> derived.proximityorder([[0], [1], [0]])
        >>> fluxes.inputs.shape = 2
        >>> fluxes.inputs = 1.0, 2.0
        >>> model.calc_outputs_v1()
        >>> fluxes.outputs
        outputs(1.0, 2.0, 1.0)
        With incomplete data, it subsequently checks the second-nearest
        location, the third-nearest location, and so on, until it finds
        an actual value.  Parameter |MaxNmbInputs| defines the maximum
        number of considered locations:
        >>> fluxes.inputs = 1.0, nan
        >>> model.calc_outputs_v1()
        >>> fluxes.outputs
        outputs(1.0, nan, 1.0)
        >>> maxnmbinputs.value = 2
        >>> derived.proximityorder.shape = (3, 2)
        >>> derived.proximityorder([[0, 1], [1, 0], [0, 1]])
        >>> model.calc_outputs_v1()
        >>> fluxes.outputs
        outputs(1.0, 1.0, 1.0)
    """
    CONTROLPARAMETERS = (conv_control.MaxNmbInputs,)
    DERIVEDPARAMETERS = (conv_derived.NmbOutputs, conv_derived.ProximityOrder)
    REQUIREDSEQUENCES = (conv_fluxes.Inputs,)
    RESULTSEQUENCES = (conv_fluxes.Outputs,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        con = model.parameters.control.fastaccess
        der = model.parameters.derived.fastaccess
        flu = model.sequences.fluxes.fastaccess
        for idx_out in range(der.nmboutputs):
            for idx_try in range(con.maxnmbinputs):
                idx_in = der.proximityorder[idx_out, idx_try]
                flu.outputs[idx_out] = flu.inputs[idx_in]
                if not modelutils.isnan(flu.outputs[idx_out]):
                    break 
[docs]
class Return_Mean_V1(modeltools.Method):
    """Return the arithmetic mean value for the given vector.
    Examples:
        Method |Return_Mean_V1| requires two vectors and one integer value.  The
        first vector (in the following examples: |conv_fluxes.Inputs|)  handles the
        data to be averaged.  The second vector (|conv_fluxes.Outputs|) serves as
        a mask.  Method |Return_Mean_V1| takes only those vector positions into
        account, where the value of the mask vector is not |numpy.nan|. The integer
        value defines the length of both vectors:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> fluxes.inputs.shape = 3
        >>> fluxes.outputs.shape = 3
        >>> fluxes.inputs = 0.0, 1.0, 5.0
        >>> fluxes.outputs = 9.9, 9.9, 9.9
        >>> from hydpy import round_
        >>> round_(model.return_mean_v1(fluxes.inputs.values, fluxes.outputs.values, 3))
        2.0
        >>> fluxes.outputs = nan, 9.9, nan
        >>> round_(model.return_mean_v1(fluxes.inputs.values, fluxes.outputs.values, 3))
        1.0
        >>> fluxes.outputs = nan, nan, nan
        >>> round_(model.return_mean_v1(fluxes.inputs.values, fluxes.outputs.values, 3))
        nan
    """
    @staticmethod
    def __call__(
        model: modeltools.Model, values: VectorFloat, mask: VectorFloat, number: int
    ) -> float:
        counter = 0
        d_result = 0.0
        for idx in range(number):
            if not modelutils.isnan(mask[idx]):
                counter += 1
                d_result += values[idx]
        if counter > 0:
            return d_result / counter
        return modelutils.nan 
[docs]
class Calc_ActualConstant_ActualFactor_V1(modeltools.Method):
    """Calculate the linear regression coefficients |ActualConstant| and
    |ActualFactor| for modelling the relationship between |conv_fluxes.Inputs|
    (dependent variable) and |Heights| (independent variable).
    Examples:
        First, we calculate both regression coefficients for a small data set
        consisting of only three data points:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> inputheights.shape = 3
        >>> inputheights.values = 1.0, 2.0, 3.0
        >>> minnmbinputs(3)
        >>> derived.nmbinputs(3)
        >>> fluxes.inputs = 2.0, 3.0, 5.0
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(0.333333)
        >>> fluxes.actualfactor
        actualfactor(1.5)
        In the last example, the required number of data points (|MinNmbInputs|)
        exactly agrees with the available number of data points.  In the next
        example, we insert a |numpy.nan| value into the |conv_fluxes.Inputs|
        array to reduce the number of available data points to two.  Then,
        |Calc_ActualConstant_ActualFactor_V1| falls back to the default values
        provided by the control parameters |DefaultConstant| and |DefaultFactor|:
        >>> fluxes.inputs = 2.0, nan, 5.0
        >>> defaultconstant(0.0)
        >>> defaultfactor(1.0)
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(0.0)
        >>> fluxes.actualfactor
        actualfactor(1.0)
        If we lower our data requirements, method |Calc_ActualConstant_ActualFactor_V1|
        uses the remaining two data points to calculate the coefficients:
        >>> minnmbinputs(2)
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(0.5)
        >>> fluxes.actualfactor
        actualfactor(1.5)
        The following two examples deal with a perfect and a non-existing linear
        relationship:
        >>> fluxes.inputs = 2.0, 4.0, 6.0
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(0.0)
        >>> fluxes.actualfactor
        actualfactor(2.0)
        >>> fluxes.inputs = 1.0, 4.0, 1.0
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(2.0)
        >>> fluxes.actualfactor
        actualfactor(0.0)
        In case all values of the independent variable are the same, method
        |Calc_ActualConstant_ActualFactor_V1| again uses the provided default values:
        >>> inputheights.values = 1.0, 1.0, 1.0
        >>> model.calc_actualconstant_actualfactor_v1()
        >>> fluxes.actualconstant
        actualconstant(0.0)
        >>> fluxes.actualfactor
        actualfactor(1.0)
    """
    CONTROLPARAMETERS = (
        conv_control.InputHeights,
        conv_control.MinNmbInputs,
        conv_control.DefaultConstant,
        conv_control.DefaultFactor,
    )
    DERIVEDPARAMETERS = (conv_derived.NmbInputs,)
    REQUIREDSEQUENCES = (conv_fluxes.Inputs,)
    RESULTSEQUENCES = (conv_fluxes.ActualConstant, conv_fluxes.ActualFactor)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        con = model.parameters.control.fastaccess
        der = model.parameters.derived.fastaccess
        flu = model.sequences.fluxes.fastaccess
        counter = 0
        for idx in range(der.nmbinputs):
            if not modelutils.isnan(flu.inputs[idx]):
                counter += 1
                if counter == con.minnmbinputs:
                    break
        else:
            flu.actualfactor = con.defaultfactor
            flu.actualconstant = con.defaultconstant
            return
        d_mean_height = model.return_mean_v1(
            con.inputheights, flu.inputs, der.nmbinputs
        )
        d_mean_inputs = model.return_mean_v1(flu.inputs, flu.inputs, der.nmbinputs)
        d_nominator = 0.0
        d_denominator = 0.0
        for idx in range(der.nmbinputs):
            if not modelutils.isnan(flu.inputs[idx]):
                d_temp = con.inputheights[idx] - d_mean_height
                d_nominator += d_temp * (flu.inputs[idx] - d_mean_inputs)
                d_denominator += d_temp * d_temp
        if d_denominator > 0.0:
            flu.actualfactor = d_nominator / d_denominator
            flu.actualconstant = d_mean_inputs - flu.actualfactor * d_mean_height
        else:
            flu.actualfactor = con.defaultfactor
            flu.actualconstant = con.defaultconstant
        return 
[docs]
class Calc_OutputPredictions_V1(modeltools.Method):
    r"""Predict the values of the output nodes based on a linear model.
    Basic equation:
       :math:`OutputPredictions = ActualConstant + ActualFactor \cdot OutputHeights`
    Example:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> outputheights.shape = 2
        >>> outputheights.values = 1.0, 2.0
        >>> derived.nmboutputs(2)
        >>> fluxes.actualconstant(1.0)
        >>> fluxes.actualfactor(2.0)
        >>> model.calc_outputpredictions_v1()
        >>> fluxes.outputpredictions
        outputpredictions(3.0, 5.0)
    """
    CONTROLPARAMETERS = (conv_control.OutputHeights,)
    DERIVEDPARAMETERS = (conv_derived.NmbOutputs,)
    REQUIREDSEQUENCES = (conv_fluxes.ActualConstant, conv_fluxes.ActualFactor)
    RESULTSEQUENCES = (conv_fluxes.OutputPredictions,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        con = model.parameters.control.fastaccess
        der = model.parameters.derived.fastaccess
        flu = model.sequences.fluxes.fastaccess
        for idx in range(der.nmboutputs):
            flu.outputpredictions[idx] = (
                flu.actualconstant + flu.actualfactor * con.outputheights[idx]
            ) 
[docs]
class Interpolate_InverseDistance_V1(modeltools.Method):
    """Perform a simple inverse distance weighted interpolation.
    See the documentation on method |Calc_Outputs_V2| for further information.
    """
    CONTROLPARAMETERS = (conv_control.MaxNmbInputs,)
    DERIVEDPARAMETERS = (
        conv_derived.NmbOutputs,
        conv_derived.ProximityOrder,
        conv_derived.Weights,
    )
    REQUIREDSEQUENCES = ()
    RESULTSEQUENCES = ()
    @staticmethod
    def __call__(model: modeltools.Model, inputs: Vector, outputs: Vector) -> None:
        con = model.parameters.control.fastaccess
        der = model.parameters.derived.fastaccess
        for idx_out in range(der.nmboutputs):
            d_sumweights = 0.0
            d_sumvalues = 0.0
            d_sumvalues_inf = 0.0
            counter_inf = 0
            for idx_try in range(con.maxnmbinputs):
                idx_in = der.proximityorder[idx_out, idx_try]
                if not modelutils.isnan(inputs[idx_in]):
                    if modelutils.isinf(der.weights[idx_out, idx_try]):
                        d_sumvalues_inf += inputs[idx_in]
                        counter_inf += 1
                    else:
                        d_sumweights += der.weights[idx_out, idx_try]
                        d_sumvalues += der.weights[idx_out, idx_try] * inputs[idx_in]
            if counter_inf:
                outputs[idx_out] = d_sumvalues_inf / counter_inf
            elif d_sumweights:
                outputs[idx_out] = d_sumvalues / d_sumweights
            else:
                outputs[idx_out] = modelutils.nan 
[docs]
class Calc_Outputs_V2(modeltools.Method):
    """Perform a simple inverse distance weighted interpolation based on the original
    data supplied by the input nodes.
    Examples:
        With complete input data, method |Calc_Outputs_V2| performs the
        most simple inverse distance weighted approach:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> maxnmbinputs.value = 4
        >>> derived.nmboutputs(3)
        >>> derived.proximityorder.shape = 3, 4
        >>> derived.proximityorder([[0, 2, 1, 3],
        ...                         [1, 0, 2, 3],
        ...                         [0, 2, 1, 3]])
        >>> derived.weights.shape = 3, 4
        >>> derived.weights([[inf, inf, 0.05, 0.000053],
        ...                  [0.5, 0.029412, 0.029412, 0.000052],
        ...                  [0.5, 0.5, 0.1, 0.000053]])
        >>> fluxes.inputs.shape = 4
        >>> fluxes.inputs = 1.0, 5.0, 3.0, 6.0
        >>> model.calc_outputs_v2()
        >>> fluxes.outputs
        outputs(2.0, 4.684331, 2.272907)
        With incomplete data, it subsequently skips all missing values.
        Parameter |MaxNmbInputs| defines the maximum number of considered
        locations:
        >>> fluxes.inputs = nan, 5.0, nan, 6.0
        >>> model.calc_outputs_v2()
        >>> fluxes.outputs
        outputs(5.001059, 5.000104, 5.00053)
        With just one considered location, method |Calc_Outputs_V2|
        calculates the same results as the nearest-neighbour approach
        implemented by method |Calc_Outputs_V1|:
        >>> maxnmbinputs.value = 1
        >>> derived.proximityorder.shape = 3, 1
        >>> derived.proximityorder([[0],
        ...                         [1],
        ...                         [0]])
        >>> derived.weights.shape = 3, 1
        >>> derived.weights([[inf],
        ...                  [0.5],
        ...                  [0.5]])
        >>> model.calc_outputs_v2()
        >>> fluxes.outputs
        outputs(nan, 5.0, nan)
    """
    CONTROLPARAMETERS = (conv_control.MaxNmbInputs,)
    DERIVEDPARAMETERS = (
        conv_derived.NmbOutputs,
        conv_derived.ProximityOrder,
        conv_derived.Weights,
    )
    REQUIREDSEQUENCES = (conv_fluxes.Inputs,)
    RESULTSEQUENCES = (conv_fluxes.Outputs,)
    SUBMETHODS = (Interpolate_InverseDistance_V1,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        flu = model.sequences.fluxes.fastaccess
        model.interpolate_inversedistance_v1(flu.inputs, flu.outputs) 
[docs]
class Calc_OutputResiduals_V1(modeltools.Method):
    """Perform a simple inverse distance weighted interpolation based on the
    residuals previously determined for the input nodes.
    Example:
        See the documentation on method |Calc_Outputs_V2| for further information,
        from which we take the following (first) example:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> maxnmbinputs.value = 4
        >>> derived.nmboutputs(3)
        >>> derived.proximityorder.shape = 3, 4
        >>> derived.proximityorder([[0, 2, 1, 3],
        ...                         [1, 0, 2, 3],
        ...                         [0, 2, 1, 3]])
        >>> derived.weights.shape = 3, 4
        >>> derived.weights([[inf, inf, 0.05, 0.000053],
        ...                  [0.5, 0.029412, 0.029412, 0.000052],
        ...                  [0.5, 0.5, 0.1, 0.000053]])
        >>> fluxes.inputresiduals.shape = 4
        >>> fluxes.inputresiduals = 1.0, 5.0, 3.0, 6.0
        >>> model.calc_outputresiduals_v1()
        >>> fluxes.outputresiduals
        outputresiduals(2.0, 4.684331, 2.272907)
    """
    CONTROLPARAMETERS = (conv_control.MaxNmbInputs,)
    DERIVEDPARAMETERS = (
        conv_derived.NmbOutputs,
        conv_derived.ProximityOrder,
        conv_derived.Weights,
    )
    REQUIREDSEQUENCES = (conv_fluxes.InputResiduals,)
    RESULTSEQUENCES = (conv_fluxes.OutputResiduals,)
    SUBMETHODS = (Interpolate_InverseDistance_V1,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        flu = model.sequences.fluxes.fastaccess
        model.interpolate_inversedistance_v1(flu.inputresiduals, flu.outputresiduals) 
[docs]
class Calc_Outputs_V3(modeltools.Method):
    r"""Calculate the values of the output nodes by combining the initial predictions
    with the interpolated residuals.
    Basic equation:
       :math:`Outputs = OutputPredictions - OutputResiduals`
    Examples:
        >>> from hydpy.models.conv import *
        >>> parameterstep()
        >>> derived.nmboutputs(2)
        >>> fluxes.outputpredictions = 1.0, 2.0
        >>> fluxes.outputresiduals = 3.0, -4.0
        >>> model.calc_outputs_v3()
        >>> fluxes.outputs
        outputs(4.0, -2.0)
    """
    CONTROLPARAMETERS = ()
    DERIVEDPARAMETERS = (conv_derived.NmbOutputs,)
    REQUIREDSEQUENCES = (conv_fluxes.OutputPredictions, conv_fluxes.OutputResiduals)
    RESULTSEQUENCES = (conv_fluxes.Outputs,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        der = model.parameters.derived.fastaccess
        flu = model.sequences.fluxes.fastaccess
        for idx in range(der.nmboutputs):
            flu.outputs[idx] = flu.outputpredictions[idx] + flu.outputresiduals[idx] 
[docs]
class Pass_Outputs_V1(modeltools.Method):
    """Pass the output to all outlet nodes."""
    DERIVEDPARAMETERS = (conv_derived.NmbOutputs,)
    REQUIREDSEQUENCES = (conv_fluxes.Outputs,)
    RESULTSEQUENCES = (conv_outlets.Outputs,)
    @staticmethod
    def __call__(model: modeltools.Model) -> None:
        der = model.parameters.derived.fastaccess
        out = model.sequences.outlets.fastaccess
        flu = model.sequences.fluxes.fastaccess
        for idx in range(der.nmboutputs):
            out.outputs[idx][0] = flu.outputs[idx] 
[docs]
class Model(modeltools.AdHocModel):
    """|conv.DOCNAME.complete|."""
    DOCNAME = modeltools.DocName(short="Conv")
    __HYDPY_ROOTMODEL__ = None
    INLET_METHODS = (Pick_Inputs_V1,)
    RECEIVER_METHODS = ()
    RUN_METHODS = (
        Calc_ActualConstant_ActualFactor_V1,
        Calc_InputPredictions_V1,
        Calc_OutputPredictions_V1,
        Calc_InputResiduals_V1,
        Calc_Outputs_V1,
        Calc_Outputs_V2,
        Calc_OutputResiduals_V1,
        Calc_Outputs_V3,
    )
    ADD_METHODS = (Return_Mean_V1, Interpolate_InverseDistance_V1)
    OUTLET_METHODS = (Pass_Outputs_V1,)
    SENDER_METHODS = ()
    SUBMODELINTERFACES = ()
    SUBMODELS = () 
[docs]
class BaseModel(modeltools.AdHocModel):
    """Base class for all |conv.DOCNAME.complete| application models."""
[docs]
    def connect(self):
        """Connect the |InletSequence| and |OutletSequence| objects of the actual model
        to the |NodeSequence| objects handled by an arbitrary number of inlet and
        outlet nodes.
        To application models derived from |conv_model.Model|, you first need to define
        an |Element| connected with an arbitrary number of inlet and outlet nodes:
        >>> from hydpy import Element
        >>> conv = Element("conv",
        ...                inlets=["in1", "in2"],
        ...                outlets=["out1", "out2", "out3"])
        Second, you must define the inlet and outlet nodes' coordinates via parametera
        |InputCoordinates| and |OutputCoordinates|, respectively.  In both cases, use
        the names of the |Node| objects as keyword arguments to pass the corresponding
        coordinates:
        >>> from hydpy.models.conv_nn import *
        >>> parameterstep()
        >>> inputcoordinates(
        ...     in1=(0.0, 3.0),
        ...     in2=(2.0, -1.0))
        >>> outputcoordinates(
        ...     out1=(0.0, 3.0),
        ...     out2=(3.0, -2.0),
        ...     out3=(1.0, 2.0))
        >>> maxnmbinputs()
        >>> parameters.update()
        |conv| passes the current values of the inlet nodes correctly to the outlet
        nodes (note that node `in1` works with simulated values while node `in2` works
        with observed values, as we set its |Node.deploymode| to `obs`):
        >>> conv.model = model
        >>> conv.inlets.in1.sequences.sim = 1.0
        >>> conv.inlets.in2.deploymode = "obs"
        >>> conv.inlets.in2.sequences.obs = 2.0
        >>> model.simulate(0)
        >>> conv.outlets.out1.sequences.sim
        sim(1.0)
        >>> conv.outlets.out2.sequences.sim
        sim(2.0)
        >>> conv.outlets.out3.sequences.sim
        sim(1.0)
        You get the following error message when you forget a node (or misspell its
        name):
        >>> outputcoordinates(
        ...     out1=(0.0, 3.0),
        ...     out2=(3.0, -2.0))
        >>> maxnmbinputs()
        >>> parameters.update()
        >>> conv.model = model
        Traceback (most recent call last):
        ...
        RuntimeError: While trying to connect model `conv_nn` of element `conv`, the \
following error occurred: The node handled by control parameter outputcoordinates \
(out1 and out2) are not the same as the outlet nodes handled by element conv (out1, \
out2, and out3).
        """
        try:
            for coordinates, sequence, nodes in (
                (
                    self.parameters.control.inputcoordinates,
                    self.sequences.inlets.inputs,
                    self.element.inlets,
                ),
                (
                    self.parameters.control.outputcoordinates,
                    self.sequences.outlets.outputs,
                    self.element.outlets,
                ),
            ):
                if nodes == devicetools.Nodes(coordinates.nodes):
                    sequence.shape = len(coordinates)
                    for idx, node in enumerate(coordinates.nodes):
                        sequence.set_pointer(
                            node.get_double(sequence.subseqs.name), idx
                        )
                else:
                    parameternodes = objecttools.enumeration(coordinates.nodes)
                    elementnodes = objecttools.enumeration(nodes)
                    raise RuntimeError(
                        f"The node handled by control parameter {coordinates.name} "
                        f"({parameternodes}) are not the same as the "
                        f"{sequence.subseqs.name[:-1]} nodes handled by element "
                        f"{self.element.name} ({elementnodes})."
                    )
        except BaseException:
            objecttools.augment_excmessage(
                f"While trying to connect model " f"{objecttools.elementphrase(self)}"
            )