#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
***********************************************************************************
                            tutorial_adv_3.py
                DAE Tools: pyDAE module, www.daetools.com
                Copyright (C) Dragan Nikolic
***********************************************************************************
DAE Tools is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License version 3 as published by the Free Software
Foundation. DAE Tools 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with the
DAE Tools software; if not, see <http://www.gnu.org/licenses/>.
************************************************************************************
"""
__doc__ = """
This tutorial introduces the following concepts:

- DAE Tools code-generators

  - Modelica code-generator
  - gPROMS code-generator
  - FMI code-generator (for Co-Simulation)

- DAE Tools model-exchange capabilities:

  - Scilab/GNU_Octave/Matlab MEX functions
  - Simulink S-functions

The model represent a simple multiplier block. It contains two inlet and two outlet ports.
The outlets values are equal to inputs values multiplied by a multiplier "m":

.. code-block:: none

    out1.y   = m1   x in1.y
    out2.y[] = m2[] x in2.y[]

where multipliers m1 and m2[] are:

.. code-block:: none

   STN Multipliers
      case variableMultipliers:
         dm1/dt   = p1
         dm2[]/dt = p2
      case constantMultipliers:
         dm1/dt   = 0
         dm2[]/dt = 0
        
(that is the multipliers can be constant or variable).

The ports in1 and out1 are scalar (width = 1).
The ports in2 and out2 are vectors (width = 1).

Achtung, Achtung!!
Notate bene:

1. Inlet ports must be DOFs (that is to have their values asssigned),
   for they can't be connected when the model is simulated outside of daetools context.
2. Only scalar output ports are supported at the moment!! (Simulink issue)

The plot of the inlet 'y' variable and the multiplied outlet 'y' variable for
the constant multipliers (m1 = 2):

.. image:: _static/tutorial_adv_3-results.png
   :width: 500px

The plot of the inlet 'y' variable and the multiplied outlet 'y' variable for
the variable multipliers (dm1/dt = 10, m1(t=0) = 2):

.. image:: _static/tutorial_adv_3-results2.png
   :width: 500px
"""

import sys, numpy
from time import localtime, strftime
from daetools.pyDAE import *

# Standard variable types are defined in variable_types.py
from pyUnits import m, kg, s, K, Pa, mol, J, W

class portScalar(daePort):
    def __init__(self, Name, PortType, Model, Description = ""):
        daePort.__init__(self, Name, PortType, Model, Description)

        self.y = daeVariable("y", no_t, self, "")

class portVector(daePort):
    def __init__(self, Name, PortType, Model, Description, width):
        daePort.__init__(self, Name, PortType, Model, Description)

        self.y = daeVariable("y", no_t, self, "", [width])

class modTutorial(daeModel):
    def __init__(self, Name, Parent = None, Description = ""):
        daeModel.__init__(self, Name, Parent, Description)

        self.w = daeDomain("w", self, unit(), "Ports width")

        self.p1 = daeParameter("p1", s**(-1), self, "Parameter multiplier 1 (fixed)")
        self.p2 = daeParameter("p2", s**(-1), self, "Parameter multiplier 2 (fixed)")

        self.m1 = daeVariable("m1", no_t, self, "Multiplier 1")
        self.m2 = daeVariable("m2", no_t, self, "Multiplier 2", [self.w])

        self.in1  = portScalar("in_1",  eInletPort,  self, "Input 1")
        self.out1 = portScalar("out_1", eOutletPort, self, "Output 1 = p1 x m1")

        self.in2  = portVector("in_2",  eInletPort,  self, "Input 2",              self.w)
        self.out2 = portVector("out_2", eOutletPort, self, "Output 2 = p2 x m2[]", self.w)

    def DeclareEquations(self):
        daeModel.DeclareEquations(self)

        nw = self.w.NumberOfPoints

        # Set the outlet port values
        eq = self.CreateEquation("out_1", "out_1.y = m1 x in1.y")
        eq.Residual = self.out1.y() - self.m1() * self.in1.y()

        for w in range(nw):
            eq = self.CreateEquation("out_2(%d)" % w, "out_2.y[%d] = m2[%d] * in2.y[%d]" % (w, w, w))
            eq.Residual = self.out2.y(w) - self.m2(w) * self.in2.y(w)

        # STN Multipliers
        self.stnMultipliers = self.STN("Multipliers")

        self.STATE("variableMultipliers") # Variable multipliers

        eq = self.CreateEquation("m1", "Multiplier 1 (Variable)")
        eq.Residual = dt(self.m1()) - self.p1()

        for w in range(nw):
            eq = self.CreateEquation("m2(%d)" % w, "Multiplier 2 (Variable)")
            eq.Residual = dt(self.m2(w)) - self.p2()

        self.STATE("constantMultipliers") # Constant multipliers

        eq = self.CreateEquation("m1", "Multiplier 1 (Constant)")
        eq.Residual = dt(self.m1())

        for w in range(nw):
            eq = self.CreateEquation("m2(%d)" % w, "Multiplier 2 (Constant)")
            eq.Residual = dt(self.m2(w))

        self.END_STN()
   
class simTutorial(daeSimulation):
    def __init__(self):
        daeSimulation.__init__(self)
        self.m = modTutorial("tutorial_adv_3")
        self.m.Description = __doc__

    def SetUpParametersAndDomains(self):
        self.m.w.CreateArray(1)

        self.m.p1.SetValue(10)
        self.m.p2.SetValues(20)

    def SetUpVariables(self):
        nw = self.m.w.NumberOfPoints

        self.m.stnMultipliers.ActiveState = "constantMultipliers"

        self.m.in1.y.AssignValue(1)
        self.m.in2.y.AssignValues(numpy.ones(nw) * 2)

        self.m.m1.SetInitialCondition(2)
        self.m.m2.SetInitialConditions(3*numpy.ones(nw))

def run_code_generators(simulation, log):
    # Demonstration of daetools code-generators:
    import tempfile
    tmp_folder = tempfile.mkdtemp(prefix = 'daetools-code_generator-fmi-')
    msg = 'Generated code (Modelica, gPROMS and FMU) \nwill be located in: \n%s' % tmp_folder
    log.Message(msg, 0)
    
    try:
        daeQtMessage("tutorial_adv_3", msg)
    except Exception as e:
        log.Message(str(e), 0)

    # Modelica:
    from daetools.code_generators.modelica import daeCodeGenerator_Modelica
    cg = daeCodeGenerator_Modelica()
    cg.generateSimulation(simulation, tmp_folder)

    # gPROMS:
    from daetools.code_generators.gproms import daeCodeGenerator_gPROMS
    cg = daeCodeGenerator_gPROMS()
    cg.generateSimulation(simulation, tmp_folder)

    # Functional Mock-up Interface for co-simulation
    # The interface requires a function (or a callable object) that returns an initialised simulation object:
    # the function run(**kwargs) or any other function can be used (here, for an illustration: create_simulation).
    # In general, using the function such as run(**kwargs) from daetools tutorials is more flexible.
    from daetools.code_generators.fmi import daeCodeGenerator_FMI
    cg = daeCodeGenerator_FMI()
    cg.generateSimulation(simulation, 
                          directory            = tmp_folder, 
                          py_simulation_file   = __file__,
                          callable_object_name = 'create_simulation',
                          arguments            = '', 
                          additional_files     = [],
                          localsAsOutputs      = False,
                          add_xml_stylesheet   = True,
                          useWebService        = False)

# This function can be used by daetools_mex, daetools_s and daetools_fmi_cs to load a simulation.
# It can have any number of arguments, but must return an initialized daeSimulation object.
def create_simulation():
    # Create Log, Solver, DataReporter and Simulation object
    log          = daePythonStdOutLog()
    daesolver    = daeIDAS()
    datareporter = daeNoOpDataReporter()
    simulation   = simTutorial()

    # Enable reporting of all variables
    simulation.m.SetReportingOn(True)

    # Set the time horizon and the reporting interval
    simulation.ReportingInterval = 1
    simulation.TimeHorizon       = 100

    # Initialize the simulation
    simulation.Initialize(daesolver, datareporter, log)

    # Nota bene: store the objects since they will be destroyed when they go out of scope
    simulation.__rt_objects__ = [daesolver, datareporter, log]

    return simulation
    
def run(**kwargs):
    simulation = simTutorial()
    return daeActivity.simulate(simulation, reportingInterval        = 1, 
                                            timeHorizon              = 100,
                                            run_before_simulation_fn = run_code_generators,
                                            **kwargs)

if __name__ == "__main__":
    guiRun = False if (len(sys.argv) > 1 and sys.argv[1] == 'console') else True
    run(guiRun = guiRun)