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

import math
import pandas as pd
import numpy as np
from pulp import LpContinuous, LpBinary

from ...energy.units.consumption_units import VariableConsumptionUnit
from ...general.optimisation.elements import Quantity, Constraint, \
    DynamicConstraint, Objective
from ...general.optimisation.units import Unit
from ...general.utils import def_abs_value

# stefan-boltzmann constant
BOLTZMANN = 0.000000056697  # W/m2K4

# thermal resistance of external surfaces according to ISO 6946
RSE = 0.04


class ThermalZoneProperties(Unit):
    """
                      H_EA
                      ____
    theta_ea     o---|____|----o    T_int
                               |
                               _
                              | |   H_AC
                              |_|
                      H_EC     |
                      ____     |
    theta_ec     o---|____|----o    theta_c
                               |
                               _
                              | |   H_MC
                              |_|
                      H_EM     |
                      ____     |
    theta_em     o---|____|----o    theta_m
                               |
                              ___
                              ___   C_M
                               |

    """

    def __init__(self, time, name, T_ext, theta_ec, theta_em,
                 T_int_min=0, T_int_max=50, theta_ea=None, theta_c=None,
                 theta_m=None, h_ea=0, h_ac=0, h_ec=0, h_mc=0, h_em=0, c_m=0,
                 f_im=None, f_r_l=0.7, f_r_p=0.5, f_r_a=0.2, f_sa=0.1,
                 f_sm=None, f_sc=None, f_ic=None, f_hc_cv=None, U_wall=0.2,
                 U_win=1.2, U_roof=0.2, e_wall=0.9, e_win=0.9, e_roof=0.9,
                 A_wall=None, A_win=None, A_roof=None, owner=None):

        Unit.__init__(self, name, 'Thermal_zone')

        self.time = time
        self.owner = owner

        self.F_DICT = {'F_IM': f_im, 'F_R_L': f_r_l, 'F_R_P': f_r_p,
                       'F_R_A': f_r_a, 'F_SA': f_sa, 'F_SM': f_sm,
                       'F_SC': f_sc, 'F_IC': f_ic, 'F_HC_CV': f_hc_cv}

        self.U_DICT = {'WIN': U_win, 'WALL': U_wall, 'ROOF': U_roof}
        self.E_DICT = {'WIN': e_win, 'WALL': e_wall, 'ROOF': e_roof}
        self.A_DICT = {'WIN': A_win, 'WALL': A_wall, 'ROOF': A_roof}

        # Calculation simplifications
        # TODO : Check
        try:
            h_1 = 1 / (1 / h_ea + 1 / h_ac)
        except ZeroDivisionError:
            h_1 = 0
            # if h_ea == 0:
            #     h_1 = h_ac
            # else:
            #     h_1 = h_ea

        h_2 = h_1 + h_ec
        h_3 = 1 / (1 / h_2 + 1 / h_mc)

        # Cm: Explanation of /3600 (Wh/K) SIA 2044 unit is Wh/K, ISO unit is J/K

        self.H_C_DICT = {'H_EA': h_ea, 'H_AC': h_ac, 'H_EC': h_ec,
                         'H_MC': h_mc, 'H_EM': h_em, 'H_1': h_1, 'H_2': h_2,
                         'H_3': h_3, 'C_M': c_m / 3600}

        # Creation of all nodes' temperatures
        self.T_int = Quantity(name='T_int', lb=T_int_min,
                              ub=T_int_max, vlen=time.LEN,
                              vtype=LpContinuous, parent=self)

        self.T_ext = Quantity(name='T_ext', value=T_ext, vlen=time.LEN,
                              vtype=LpContinuous, parent=self)

        self.theta_ea = Quantity(name='theta_ea', value=theta_ea, lb=-50,
                                 ub=50, vlen=time.LEN, vtype=LpContinuous,
                                 parent=self)

        self.theta_ec = Quantity(name='theta_ec', value=theta_ec,
                                 vlen=time.LEN, vtype=LpContinuous,
                                 parent=self)

        self.theta_c = Quantity(name='theta_c', value=theta_c, lb=-50,
                                ub=50, vlen=time.LEN, vtype=LpContinuous,
                                parent=self)

        self.theta_em = Quantity(name='theta_em', value=theta_em,
                                 vlen=time.LEN, vtype=LpContinuous,
                                 parent=self)

        self.theta_m = Quantity(name='theta_m', value=theta_m, lb=-50,
                                ub=50, vlen=time.LEN, vtype=LpContinuous,
                                parent=self)

        self.T_op = Quantity(name='T_op', lb=-50, ub=50, vlen=time.LEN,
                             vtype=LpContinuous, parent=self)


class ZEAThermalZoneProperties(ThermalZoneProperties):
    """
        Copyright 2015, Architecture and Building Systems - ETH Zurich
    """

    def __init__(self, time, name, T_ext, A_f, A_win, A_contour, A_roof,
                 footprint, U_win, U_wall, U_roof, U_base, floors,
                 window_to_wall_ratio, construction='heavy', height_bg=0,
                 perimeter=0, f_hc_cv=1, void=0, hvac_prop=None, T_int_min=0,
                 T_int_max=50, owner=None):

        # Import functions from City Energy Analyst - ETH Zurich
        from cea.demand.rc_model_SIA import calc_h_ea, calc_h_ac, calc_h_ec, \
            calc_h_mc, calc_h_em, \
            calc_h_op_m, calc_f_im, calc_f_sm, calc_f_sc, calc_f_ic, f_r_a, \
            f_r_l, f_r_p, f_sa

        if hvac_prop is not None:
            m_v_sys = hvac_prop.vent_param['M_VE_MECH']
            m_v_w = hvac_prop.vent_param['M_VE_W']
            m_v_inf = hvac_prop.vent_param['M_VE_INF']
        else:
            m_v_sys = 0
            m_v_w = 0
            m_v_inf = 0.5
            Warning("You didn't enter any HVAC property for the thermal"
                    " zone properties {}.".format(name))

        Aop_sup = calc_Aop_sup(Awall_all=A_contour, void=void,
                               window_to_wall_ratio=window_to_wall_ratio)
        Aop_bel = calc_Aop_bel(height_bg=height_bg, perimeter=perimeter,
                               footprint=footprint)

        A_tot = A_win + Aop_sup + footprint + Aop_bel + A_roof * (floors - 1)

        Htr_op = calc_Htr_op(Aop_bel=Aop_bel, Aop_sup=Aop_sup,
                             footprint=footprint, U_base=U_base, U_wall=U_wall,
                             U_roof=U_roof)
        Htr_w = A_win * U_win

        Cm_Af = get_Cm_Af(construction)

        # Af: conditioned floor area (heated/cooled) in [m2]
        A_m = calc_Am(Cm_Af=Cm_Af, Af=A_f)
        C_m = calc_cm(Cm_Af=Cm_Af, Af=A_f)

        h_ea = calc_h_ea(m_v_sys, m_v_w, m_v_inf)
        h_ac = calc_h_ac(A_tot)
        h_ec = calc_h_ec(Htr_w)
        h_mc = calc_h_mc(A_m)
        h_op_m = calc_h_op_m(Htr_op)
        h_em = calc_h_em(h_op_m, h_mc)

        f_im = calc_f_im(A_tot, A_m)
        f_sm = calc_f_sm(A_tot, A_m, A_win)
        f_sc = calc_f_sc(A_tot, A_m, A_win, h_ec)
        f_ic = calc_f_ic(A_tot, A_m, h_ec)

        ThermalZoneProperties.__init__(self, time, name, T_ext=T_ext,
                                       theta_ec=T_ext, theta_em=T_ext,
                                       T_int_min=T_int_min,
                                       T_int_max=T_int_max, theta_ea=T_ext,
                                       theta_c=None, theta_m=None,
                                       h_ea=h_ea, h_ac=h_ac, h_ec=h_ec,
                                       h_mc=h_mc, h_em=h_em, c_m=C_m, f_im=f_im,
                                       f_r_l=f_r_l, f_r_p=f_r_p, f_r_a=f_r_a,
                                       f_sa=f_sa, f_sm=f_sm, f_sc=f_sc,
                                       f_ic=f_ic, f_hc_cv=f_hc_cv,
                                       U_wall=U_wall, U_win=U_win,
                                       U_roof=U_roof, A_wall=Aop_sup,
                                       A_win=A_win, A_roof=A_roof, owner=owner)


class ThermalZoneWithFlows(Unit):

    def __init__(self, tz_prop, phi_i_a, phi_i_l, phi_i_p, I_sol,
                 T_mean, T_dew, sky_cover=1, hvac_prop=None):
        Unit.__init__(self, tz_prop.name, 'Thermal zone with power flows')

        self.time = tz_prop.time
        self.owner = tz_prop.owner
        self.prop = tz_prop
        tz_prop.parent = self

        # ++ Creation of energy flows ++
        self.phi_a = Quantity(name='phi_a', vlen=self.time.LEN,
                              vtype=LpContinuous, parent=self)

        self.phi_c = Quantity(name='phi_c', vlen=self.time.LEN,
                              vtype=LpContinuous, parent=self)

        self.phi_m = Quantity(name='phi_m', vlen=self.time.LEN,
                              vtype=LpContinuous, parent=self)

        self.phi_m_tot = Quantity(name='phi_m_tot', vlen=self.time.LEN,
                                  vtype=LpContinuous, parent=self)

        # Internal gains
        self.phi_i_l = Quantity(name='phi_i_l', value=phi_i_l,
                                vlen=self.time.LEN, vtype=LpContinuous,
                                parent=self)

        self.phi_i_p = Quantity(name='phi_i_p', value=phi_i_p,
                                vlen=self.time.LEN, vtype=LpContinuous,
                                parent=self)

        self.phi_i_a = Quantity(name='phi_i_a', value=phi_i_a,
                                vlen=self.time.LEN, vtype=LpContinuous,
                                parent=self)

        # Solar gains
        self.I_sol = Quantity(name='I_sol', value=I_sol,
                              vlen=self.time.LEN, vtype=LpContinuous,
                              parent=self)

        # Re-irradiated flows
        self.I_rad = Quantity(name='I_rad', vlen=self.time.LEN,
                              vtype=LpContinuous, parent=self)

        # External gains
        self.phi_s = Quantity(name='phi_s', vlen=self.time.LEN,
                              vtype=LpContinuous, parent=self)

        # Heating and cooling
        self.phi_hc_cv = Quantity(name='phi_hc_cv', vlen=self.time.LEN,
                                  vtype=LpContinuous, parent=self)
        self.phi_hc_r = Quantity(name='phi_hc_r', vlen=self.time.LEN,
                                 vtype=LpContinuous, parent=self)
        self.phi_hc = Quantity(name='phi_hc', lb=-10e+12, ub=10e+12,
                               vlen=self.time.LEN, vtype=LpContinuous,
                               parent=self)

        # Get all coefficients from thermal zone properties
        f_im = tz_prop.F_DICT['F_IM']
        f_r_l = tz_prop.F_DICT['F_R_L']
        f_r_p = tz_prop.F_DICT['F_R_P']
        f_r_a = tz_prop.F_DICT['F_R_A']
        f_sa = tz_prop.F_DICT['F_SA']
        f_sm = tz_prop.F_DICT['F_SM']
        f_ic = tz_prop.F_DICT['F_IC']
        f_sc = tz_prop.F_DICT['F_SC']
        f_hc_cv = tz_prop.F_DICT['F_HC_CV']

        h_em = tz_prop.H_C_DICT['H_EM']
        h_ea = tz_prop.H_C_DICT['H_EA']
        h_ac = tz_prop.H_C_DICT['H_AC']
        h_ec = tz_prop.H_C_DICT['H_EC']
        h_mc = tz_prop.H_C_DICT['H_MC']
        h_1 = tz_prop.H_C_DICT['H_1']
        h_2 = tz_prop.H_C_DICT['H_2']
        h_3 = tz_prop.H_C_DICT['H_3']
        c_m = tz_prop.H_C_DICT['C_M']

        # Get ventilation data from hvac_prop
        if hvac_prop is not None:
            m_v_sys = hvac_prop.vent_param['M_VE_MECH']
            m_v_w = hvac_prop.vent_param['M_VE_W']
            m_v_inf = hvac_prop.vent_param['M_VE_INF']
            theta_v_sys = hvac_prop.vent_param['THETA_V_SYS']
        else:
            m_v_sys = 0
            m_v_w = 0
            m_v_inf = 0
            theta_v_sys = None
            Warning("You didn't enter any HVAC property for the thermal"
                    " zone properties {}.".format(tz_prop.name))

        # Set all equations into constraints
        self.__set_thermal_flows_equations(f_im, f_r_l, f_r_p, f_r_a, f_sa,
                                           f_sm, f_ic, f_sc, f_hc_cv, h_em,
                                           h_3, h_ec, h_1, h_ea, h_2, T_dew,
                                           T_mean, sky_cover)

        self.__set_temperature_flows_equations(h_ac, h_ea, h_mc, h_ec, h_1, h_3,
                                               h_em, c_m, m_v_sys, theta_v_sys,
                                               m_v_w, m_v_inf)

    def __set_thermal_flows_equations(self, f_im, f_r_l, f_r_p, f_r_a, f_sa,
                                      f_sm, f_ic, f_sc, f_hc_cv, h_em, h_3,
                                      h_ec, h_1, h_ea, h_2, Tdew, Tmean,
                                      sky_cover):

        # Calculation of energy flows
        self.calc_phi_m_tot = DynamicConstraint(name='calc_phi_m_tot',
                                                exp_t='{0}_phi_m_tot[t] == {'
                                                      '0}_phi_m[t] + '
                                                      '{h_em} * {0}_theta_em['
                                                      't] + ({h_3} * '
                                                      '({0}_phi_c[t] + {h_ec} '
                                                      '* {0}_theta_ec[t]'
                                                      ' + {h_1} * ({0}_phi_a['
                                                      't] * 1 / {h_ea} + '
                                                      '{0}_theta_ea[t]))) / {'
                                                      'h_2}'
                                                .format(self.name, h_em=h_em,
                                                        h_3=h_3,
                                                        h_ec=h_ec, h_1=h_1,
                                                        h_ea=h_ea, h_2=h_2))

        self.calc_phi_m = DynamicConstraint(name='calc_phi_m',
                                            exp_t='{0}_phi_m[t] == {f_im} * ('
                                                  '{f_r_l} * '
                                                  '{0}_phi_i_l[t] + {f_r_p} * '
                                                  '{0}_phi_i_p[t] + '
                                                  '{f_r_a} * {0}_phi_i_a[t] + '
                                                  '{0}_phi_hc_r[t]) '
                                                  '+ (1 - {f_sa}) * {f_sm} * '
                                                  '{0}_phi_s[t]'
                                            .format(self.name, f_im=f_im,
                                                    f_r_l=f_r_l,
                                                    f_r_p=f_r_p, f_r_a=f_r_a,
                                                    f_sa=f_sa,
                                                    f_sm=f_sm))

        self.calc_phi_a = DynamicConstraint(name='calc_phi_a',
                                            exp_t='{0}_phi_a[t] == {f_sa} * {'
                                                  '0}_phi_s[t] + '
                                                  '(1 - {f_r_l}) * {'
                                                  '0}_phi_i_l[t] + (1 - '
                                                  '{f_r_p}) * {0}_phi_i_p[t] '
                                                  '+(1 - {f_r_a}) * '
                                                  '{0}_phi_i_a[t] + {'
                                                  '0}_phi_hc_cv[t]'
                                            .format(self.name, f_sa=f_sa,
                                                    f_r_l=f_r_l,
                                                    f_r_p=f_r_p, f_r_a=f_r_a))

        self.calc_phi_c = DynamicConstraint(name='calc_phi_c',
                                            exp_t='{0}_phi_c[t] == {f_ic} * ('
                                                  '{f_r_l} * '
                                                  '{0}_phi_i_l[t] + {f_r_p} * '
                                                  '{0}_phi_i_p[t] + '
                                                  '{f_r_a} * {0}_phi_i_a[t] + '
                                                  '{0}_phi_hc_r[t]) '
                                                  '+ (1 - {f_sa}) * {f_sc} * '
                                                  '{0}_phi_s[t]'
                                            .format(self.name, f_ic=f_ic,
                                                    f_r_l=f_r_l,
                                                    f_r_p=f_r_p, f_r_a=f_r_a,
                                                    f_sa=f_sa,
                                                    f_sc=f_sc))

        self.calc_phi_s = DynamicConstraint(name='calc_phi_s',
                                            exp_t='{0}_phi_s[t] == {0}_I_sol['
                                                  't] + {0}_I_rad[t]'.format(
                                                self.name))

        # Linearization of the irradiated heat rate flow to the sky
        exp_t = write_linerazation_exp(T_dry=self.prop.T_ext.value,
                                         T_dew=Tdew,
                                       Tlin=Tmean, sky_cover=sky_cover,
                                       U_wall=self.prop.U_DICT['WALL'],
                                       U_win=self.prop.U_DICT['WIN'],
                                       U_roof=self.prop.U_DICT['ROOF'],
                                       e_wall=self.prop.E_DICT['WALL'],
                                       e_win=self.prop.E_DICT['WIN'],
                                       e_roof=self.prop.E_DICT['ROOF'],
                                       A_wall=self.prop.A_DICT['WALL'],
                                       A_win=self.prop.A_DICT['WIN'],
                                       A_roof=self.prop.A_DICT['ROOF'],
                                       name=self.name)

        self.calc_I_rad = DynamicConstraint(name='calc_I_rad',
                                            t_range='for t in time.I[1:]',
                                            exp_t=exp_t)

        self.calc_phi_hc_cv = DynamicConstraint(name='calc_phi_hc_cv',
                                                t_range='for t in time.I',
                                                exp_t='{0}_phi_hc_cv[t] == {'
                                                      'f_hc_cv} * '
                                                      '{0}_phi_hc[t]'.format(
                                                    self.name,
                                                    f_hc_cv=f_hc_cv))

        self.calc_phi_hc_r = DynamicConstraint(name='calc_phi_hc_r',
                                               t_range='for t in time.I',
                                               exp_t='{0}_phi_hc_r[t] == (1 '
                                                     '-{f_hc_cv}) * '
                                                     '{0}_phi_hc[t]'.format(
                                                   self.name,
                                                   f_hc_cv=f_hc_cv))

    def __set_temperature_flows_equations(self, h_ac, h_ea, h_mc, h_ec, h_1,
                                          h_3, h_em, c_m, m_v_sys, theta_v_sys,
                                          m_v_w, m_v_inf):
        # Calculation of nodes' temperatures
        self.calc_T_int = DynamicConstraint(name='calc_T_int',
                                            t_range='for t in time.I',
                                            exp_t='{0}_T_int[t] == ({h_ac} * '
                                                  '{0}_theta_c[t] + '
                                                  '{h_ea} * {0}_theta_ea[t] + '
                                                  '{0}_phi_a[t]) '
                                                  '/ ({h_ac} + {h_ea})'
                                            .format(self.name, h_ac=h_ac,
                                                    h_ea=h_ea))

        self.calc_theta_c = DynamicConstraint(name='calc_theta_c',
                                              t_range='for t in time.I[1:]',
                                              exp_t='{0}_theta_c[t] == ({'
                                                    'h_mc} * '
                                                    '({0}_theta_m[t] + {'
                                                    '0}_theta_m[t-1])/2 + '
                                                    '{h_ec} * {0}_theta_ec[t] '
                                                    '+ {0}_phi_c[t] +'
                                                    ' {h_1} * (1/{h_ea} * {'
                                                    '0}_phi_a[t] + '
                                                    '{0}_theta_ea[t])) / ({'
                                                    'h_mc} + {h_ec} + '
                                                    '{h_1})'
                                              .format(self.name, h_mc=h_mc,
                                                      h_ec=h_ec, h_1=h_1,
                                                      h_ea=h_ea))

        self.calc_theta_m = DynamicConstraint(name='calc_theta_m',
                                              t_range='for t in time.I[1:]',
                                              exp_t='{0}_theta_m[t] == ({'
                                                    '0}_theta_m[t-1] * '
                                                    '({c_m} - 0.5 * ({h_3} + '
                                                    '{h_em})) + '
                                                    '{0}_phi_m_tot[t]) / ({'
                                                    'c_m} + 0.5 * '
                                                    '({h_3} + {h_em}))'
                                              .format(self.name, c_m=c_m,
                                                      h_em=h_em, h_3=h_3))

        self.calc_theta_m_0 = Constraint(name='calc_theta_m_0',
                                         exp='{0}_theta_m[0] == {0}_T_ext[0]'
                                         .format(self.name), parent=self)

        self.calc_operative_temp = DynamicConstraint(
            name='calc_operative_temp', exp_t='2 * {0}_T_op[t] == '
                                              '{0}_theta_m[t] + {0}_T_int[t]'
                .format(self.name), parent=self)

        if m_v_sys != 0 and theta_v_sys is not None:
            # Intermediary temperature to calculate theta_ea in case of
            # mechanical ventilation
            self.theta_v_sys = Quantity(name='theta_v_sys', value=theta_v_sys,
                                        lb=0, ub=50,
                                        vlen=self.time.LEN, vtype=LpContinuous,
                                        parent=self)

            self.calc_theta_ea = DynamicConstraint(name='calc_theta_ea',
                                                   t_range='for t in time.I',
                                                   exp_t='{0}_theta_ea[t] == '
                                                         '({m_v_sys} * '
                                                         '{0}_theta_v_sys[t] '
                                                         '+ ({m_v_w} + '
                                                         '{m_v_inf}) * {'
                                                         '0}_T_ext[t]) * 1 / '
                                                         '({m_v_sys} + {'
                                                         'm_v_w} + {m_v_inf})'
                                                   .format(self.name,
                                                           m_v_sys=m_v_sys,
                                                           m_v_w=m_v_w,
                                                           m_v_inf=m_v_inf))

        else:
            # If no mechanical ventilation, theta_ea equals the external
            # temperature
            self.calc_theta_ea = DynamicConstraint(name='calc_theta_ea',
                                                   t_range='for t in time.I',
                                                   exp_t='{0}_theta_ea[t] == '
                                                         '{0}_T_ext[t]'
                                                   .format(self.name))

    def split_heating_and_cooling(self, p_max_heating=10e+12,
                                  p_max_cooling=10e+12):
        self.phi_heating = Quantity(name='phi_heating', lb=0, ub=p_max_heating,
                                    vlen=self.time.LEN,
                                    vtype=LpContinuous, parent=self)

        self.phi_cooling = Quantity(name='phi_cooling', lb=0, ub=p_max_cooling,
                                    vlen=self.time.LEN,
                                    vtype=LpContinuous, parent=self)

        self.heating_on = Quantity(name='heating_on', vlen=self.time.LEN,
                                   vtype=LpBinary, parent=self)

        self.split_heat_cool = DynamicConstraint(
            exp_t='{0}_phi_hc[t] == {0}_phi_heating[t] - {0}_phi_cooling[t]'
                .format(self.name), t_range='for t in time.I',
            name='split_heat_cool', parent=self)

        self.heating_only = DynamicConstraint(
            exp_t='{0}_phi_heating[t] <= {0}_heating_on[t] * {p_max_h}'
                .format(self.name, p_max_h=p_max_heating), name='heating_only')

        self.cooling_only = DynamicConstraint(
            exp_t='{0}_phi_cooling[t] <= (1 - {0}_heating_on[t]) * {p_max_c}'
                .format(self.name, p_max_c=p_max_cooling), name='cooling_only')


class HeatingLoad(VariableConsumptionUnit):
    """

    """

    def __init__(self, time, name, tz, p_max=10e+12, T_set=19, temp_margin=1):
        self.tz = tz
        self.T_set = T_set
        self.temp_margin = temp_margin
        tz.parent = self

        if not isinstance(tz, ThermalZoneWithFlows):
            raise TypeError("tz should be a ThermalZone !")

        print('Redefining minimal and maximal air temperatures for '
              'thermal zone {0}'.format(tz.name))
        self.tz.prop.T_op.lb = T_set - temp_margin
        self.tz.prop.T_op.ub = T_set + temp_margin

        self.tz.split_heating_and_cooling(p_max_heating=p_max)

        VariableConsumptionUnit.__init__(self, time, name, energy_type='Heat',
                                         owner=tz.owner, pmax=p_max)
        self.calc_e_tot = None
        self.e_tot = None

        self.def_heat_cons = DynamicConstraint(
            exp_t='{0}_p[t] == {1}_phi_heating[t]'
                .format(self.name, self.tz.name), name='def_heat_cons',
            parent=self)

    def maximize_thermal_comfort(self, T_op=None, weight=1):
        """

        :param T_op: Operative temperature wished for the maximal thermal
        comfort
        :param weight: Weight of the objective
        :return:
        """
        if T_op is None:
            T_op = self.T_set

        self.diff_Top_opt = Quantity(name='diff_Top_opt', opt=True,
                                     lb=-self.temp_margin,
                                     ub=self.temp_margin,
                                     vlen=self.time.LEN, parent=self)

        if isinstance(T_op, (int, float)):
            self.def_diff_Top_opt = DynamicConstraint(
                name='def_diff_Top_opt',
                exp_t='{0}_diff_Top_opt[t] == {1}_T_op[t] - {2}'.format(
                    self.name, self.tz.prop.name, T_op), parent=self)
        elif isinstance(T_op, list):
            self.def_diff_Top_opt = DynamicConstraint(
                name='def_diff_Top_opt',
                exp_t='{0}_diff_Top_opt[t] == {1}_T_op[t] - {1}[t]'.format(
                    self.name, self.tz.prop.name, T_op), parent=self)

        diff = def_abs_value(self.diff_Top_opt, q_max=self.temp_margin,
                             q_min=-self.temp_margin)

        self.max_th_comfort = Objective(name='max_th_comfort',
                                        exp='lpSum({0}_{1}[t] for t in '
                                             'time.I[72:])'.format(self.name,
                                                              diff.name),
                                        parent=self)


def calc_Am(Cm_Af, Af):
    # Am: Effective mass area in [m2]
    Am = lookup_effective_mass_area_factor(Cm_Af) * Af
    return Am


def calc_Aop_bel(height_bg, perimeter, footprint):
    # Aop_bel: Opaque areas below ground (including ground floor,
    # excluding voids and windows) [m2]
    Aop_bel = height_bg * perimeter + footprint
    return Aop_bel


def calc_Aop_sup(Awall_all, void, window_to_wall_ratio):
    # Aop_sup: Opaque wall areas above ground (excluding voids
    # and windows) [m2]
    Aop_sup = Awall_all * (1 - void) * (1 - window_to_wall_ratio)
    return Aop_sup


def calc_cm(Cm_Af, Af):
    # Cm: internal heat capacity in [J/k]
    Cm = Cm_Af * Af
    return Cm


def calc_Hg(Aop_bel, U_base):
    # Hg: steady-state thermal transmission coefficient to the ground
    # in [W/K]
    Hg = 0.7 * Aop_bel * U_base

    return Hg


def calc_Hd(Aop_sup, U_wall, footprint, U_roof):
    # HD: direct thermal transmission  coefficient to  the
    # external environment in [W/K])

    Hd = Aop_sup * U_wall + footprint * U_roof

    return Hd


def calc_Htr_op(Aop_bel, Aop_sup, footprint, U_base, U_wall, U_roof):
    Hg = calc_Hg(Aop_bel=Aop_bel, U_base=U_base)
    Hd = calc_Hd(Aop_sup=Aop_sup, U_wall=U_wall, footprint=footprint,
                 U_roof=U_roof)
    Htr_op = Hg + Hd

    return Htr_op


def get_Cm_Af(construction):
    """
        Description	       code	    Cm_Af
        Light construction	T1	    110000
        Medium construction	T2	    165000
        Heavy construction	T3	    300000
    ..
        Copyright 2015, Architecture and Building Systems - ETH Zurich
    """
    if construction == 'light':
        cm_af = 110000
    elif construction == 'medium':
        cm_af = 165000
    elif construction == 'heavy':
        cm_af = 300000
    else:
        raise ValueError('The construction should be "light", "medium" or '
                         '"heavy", but equals {}'.format(construction))

    return cm_af


def lookup_effective_mass_area_factor(cm):
    """
        Look up the factor to multiply the conditioned floor area by to get
        the effective mass area by building
        construction type.
        This is used for the calculation of the effective mass area "Am" in
        `get_prop_RC_model`.
        Standard values can be found in the Annex G of ISO EN13790

        :param: cm: The internal heat capacity per unit of area [J/m2].

        :return: Effective mass area factor (0, 2.5 or 3.2 depending on cm
        value).

    ..
        Copyright 2015, Architecture and Building Systems - ETH Zurich
    """

    if cm == 0:
        return 0
    elif 0 < cm <= 165000.0:
        return 2.5
    else:
        return 3.2


def calc_I_sol(I_sol_average, Awall_all, Aroof, Awin, a_wall, a_roof, U_wall,
               U_roof, Fsh_win, window_to_wall_ratio, void=0):
    """

    :param I_sol_average: W/m2
    :param Aop_sup: Opaque wall areas above ground (excluding voids  and
    windows) [m2]
    :param Aroof: Roof area [m2]
    :param Awin: Windows area [m2]
    :param a_wall: Absorption coefficient of the walls [0..1]
    :param a_roof: Absorption coefficeint of the roof [0..1]
    :param U_wall:
    :param U_roof:
    :param Fsh_win: Shading factor for windows
    :return:
    """

    from cea.demand.constants import RSE, F_F

    Aop_sup = calc_Aop_sup(Awall_all, void, window_to_wall_ratio)

    Asol_wall = Aop_sup * a_wall * RSE * U_wall
    Asol_roof = Aroof * a_roof * RSE * U_roof

    if isinstance(I_sol_average, list):
        if isinstance(Fsh_win, list):
            Asol_win = [Fsh_w * Awin * (1 - F_F) for Fsh_w in Fsh_win]
            I_sol = [I_sol_av * (Asol_wall + Asol_roof + Asol_w) for
                     I_sol_av, Asol_w in zip(I_sol_average, Asol_win)]
        elif isinstance(Fsh_win, (int, float)):
            Asol_win = [Fsh_win * Awin * (1 - F_F)]
            I_sol = [I_sol_av * (Asol_wall + Asol_roof + Asol_win) for
                     I_sol_av in I_sol_average]
        else:
            raise TypeError('Fsh_win should be an int, a float or a list.')

    elif isinstance(I_sol_average, (int, float)):
        if isinstance(Fsh_win, list):
            Asol_win = [Fsh_w * Awin * (1 - F_F) for Fsh_w in Fsh_win]
            I_sol = [I_sol_average * (Asol_wall + Asol_roof + Asol_w) for \
                    Asol_w in Asol_win]
        elif isinstance(Fsh_win, (int, float)):
            Asol_win = [Fsh_win * Awin * (1 - F_F)]
            I_sol = I_sol_average * (Asol_wall + Asol_roof + Asol_win)
        else:
            raise TypeError('Fsh_win should be an int, a float or a list.')
    else:
        raise TypeError('I_sol_average should be an int, a float or a list.')

    return I_sol


def calc_T_sky(T_dry, T_dew, sky_cover=1):
    """

    :param T_dry: Dry bulb temperature in Celsius
    :param T_dew: Dew point temperature in Celsius
    :param sky_cover:
    :return:
    """
    weather_df = pd.DataFrame()
    weather_df['T_dry'] = T_dry
    weather_df['T_dew'] = T_dew
    weather_df['sky_cover'] = sky_cover

    weather_df['log'] = np.vectorize(math.log)((weather_df['T_dew'] + 273) /
                                               273)
    weather_df['sky_e'] = (0.787 + 0.764 * weather_df['log']) * 1 \
            + 0.0224 * weather_df['sky_cover'] + 0.0035 * weather_df[
                              'sky_cover'] ** 2 + 0.00025 * weather_df[
                              'sky_cover'] ** 3

    weather_df['hor_IR'] = weather_df['sky_e'] * BOLTZMANN * \
                           (weather_df['T_dry'] + 273) ** 4
    weather_df['sky_T'] = ((weather_df['hor_IR'] / BOLTZMANN) ** 0.25) - 273
    sky_T = list(weather_df['sky_T'])

    return sky_T  # sky temperature in C


def calc_I_rad_linearization_coef(Tdry, Tdew, Tlin, sky_cover=1):
    """

    :param T_dry: Dry bulb temperature in Celsius
    :param T_dew: Dew point temperature in Celsius
    :param sky_cover:
    :return:
    """
    temp_df = pd.DataFrame()
    temp_df['T_sky (C)'] = calc_T_sky(T_dry=Tdry, T_dew=Tdew,
                                      sky_cover=sky_cover)
    temp_df['T_sky (K)'] = temp_df['T_sky (C)'] + 273
    temp_df['T_lin (C)'] = Tlin
    temp_df['T_lin (K)'] = temp_df['T_lin (C)'] + 273

    A = 3 * temp_df['T_lin (K)'] ** 4 + temp_df['T_sky (K)'] ** 4 + 4 * \
        temp_df['T_sky (K)'] * temp_df['T_lin (K)'] ** 3

    B = - 4 * temp_df['T_lin (K)'] ** 3 - 6 * temp_df['T_sky (K)'] * temp_df[
        'T_lin (K)'] ** 2 + 2 * temp_df['T_sky (K)'] ** 3

    return list(A), list(B)


def write_linerazation_exp(T_dry, T_dew, sky_cover, Tlin, U_win, U_wall,
                           U_roof, e_win, e_wall, e_roof, A_win, A_wall,
                           A_roof, name):
    A, B = calc_I_rad_linearization_coef(Tdry=T_dry, Tdew=T_dew, Tlin=Tlin,
                                         sky_cover=sky_cover)

    exp_t = '{A}[t] + {B}[t] * ({0}_theta_c[t-1] + 273)'.format(name, A=A, B=B)

    alpha = RSE * BOLTZMANN / 2

    win_coef = 0.5 * U_win * A_win * e_win
    wall_coef = 0.5 * U_wall * A_wall * e_wall
    roof_coef = U_roof * A_roof * e_roof

    coef_tot = alpha * (win_coef + wall_coef + roof_coef)

    exp_tot = '{0}_I_rad[t] ==  {1} * ('.format(name, coef_tot) + exp_t + ')'

    return exp_tot