#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""
Generic IDAES 1D Heat Exchanger Model with overall area and heat transfer coefficient
"""
# Import Pyomo libraries
from pyomo.environ import (
    Block,
    Var,
    check_optimal_termination,
    value,
    units as pyunits,
)
from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool

# Import IDAES cores
from idaes.core import (
    ControlVolume1DBlock,
    UnitModelBlockData,
    declare_process_block_class,
    MaterialBalanceType,
    EnergyBalanceType,
    MomentumBalanceType,
    FlowDirection,
    useDefault,
)
from idaes.models.unit_models.heat_exchanger import (
    HeatExchangerFlowPattern,
    hx_process_config,
    add_hx_references,
)
from idaes.core.util.config import is_physical_parameter_block, DefaultBool
from idaes.core.util.misc import add_object_reference
from idaes.core.util.exceptions import ConfigurationError, InitializationError
from idaes.core.util.tables import create_stream_table_dataframe
from idaes.core.util import scaling as iscale
from idaes.core.solvers import get_solver
from idaes.core.initialization import SingleControlVolumeUnitInitializer
import idaes.logger as idaeslog


__author__ = "Jaffer Ghouse, Andrew Lee"

# Set up logger
_log = idaeslog.getLogger(__name__)


class HX1DInitializer(SingleControlVolumeUnitInitializer):
    """
    Initializer for 1D Heat Exchanger units.

    """

    def initialization_routine(
        self,
        model: Block,
        plugin_initializer_args: dict = None,
        duty=None,
    ):
        """
        Common initialization routine for 1D Heat Exchangers.

        This routine starts by initializing the hot and cold side properties. Next, the heat
        transfer between the two sides is fixed to an initial guess for the heat duty (provided by the duty
        argument), the associated constraints is deactivated, and the model is then solved. Finally, the heat
        duty is unfixed and the heat transfer constraints reactivated followed by a final solve of the model.

        Args:
            model: Pyomo Block to be initialized
            plugin_initializer_args: dict-of-dicts containing arguments to be passed to plug-in Initializers.
                Keys should be submodel components.
            duty: initial guess for heat duty to assist with initialization. Can be a Pyomo expression with units.
                Default is 1/4*U*A*(Thot - Tcold)

        Returns:
            Pyomo solver results object
        """
        return super(SingleControlVolumeUnitInitializer, self).initialization_routine(
            model=model,
            plugin_initializer_args=plugin_initializer_args,
            duty=duty,
        )

    def initialize_main_model(
        self,
        model: Block,
        duty=None,
    ):
        """
        Initialization routine for main 1D HX models.

        Args:
            model: Pyomo Block to be initialized.
            duty: initial guess for heat duty to assist with initialization. Can be a Pyomo expression with units.

        Returns:
            Pyomo solver results object.

        """
        init_log = idaeslog.getInitLogger(
            model.name, self.get_output_level(), tag="unit"
        )
        solve_log = idaeslog.getSolveLogger(
            model.name, self.get_output_level(), tag="unit"
        )

        # Create solver
        solver = self._get_solver()

        # ---------------------------------------------------------------------
        # Get length values
        if model.length.fixed:
            # Most likely case
            model.cold_side.length.set_value(model.length)
        elif model.cold_side.length.fixed:
            # This would be unusual, but check
            model.length.set_value(model.cold_side.length)
        else:
            # No fixed value for length - we will assume the user knows what they are doing
            pass

        # Initialize control volumes blocks
        Lfix = model.hot_side.length.fixed
        model.hot_side.length.fix()
        self.initialize_control_volume(model.hot_side)

        if not Lfix:
            model.hot_side.length.unfix()

        Lfix = model.cold_side.length.fixed
        model.cold_side.length.fix()
        self.initialize_control_volume(model.cold_side)

        if not Lfix:
            model.cold_side.length.unfix()

        init_log.info_high("Initialization Step 1 Complete.")

        # ---------------------------------------------------------------------
        # Solve unit with fixed heat duty
        # Guess heat duty based on 1/4 of maximum driving force
        hot_side_units = (
            model.hot_side.config.property_package.get_metadata().get_derived_units
        )
        if duty is None:
            duty = (
                0.25
                * model.heat_transfer_coefficient[0, 0]
                * model.area
                * (
                    model.hot_side.properties[0, 0].temperature
                    - pyunits.convert(
                        model.cold_side.properties[0, 0].temperature,
                        to_units=hot_side_units("temperature"),
                    )
                )
            )

        duty_per_length = duty / model.length
        # Fix heat duties
        for v in model.hot_side.heat.values():
            v.fix(-duty_per_length)
        for v in model.cold_side.heat.values():
            v.fix(duty_per_length)

        # Deactivate heat duty constraints
        model.heat_transfer_eq.deactivate()
        model.heat_conservation.deactivate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = solver.solve(model, tee=slc.tee)
        init_log.info_high(f"Initialization Step 2 {idaeslog.condition(res)}.")

        # Unfix heat duty and reactivate constraints
        for v in model.hot_side.heat.values():
            v.unfix()
        for v in model.cold_side.heat.values():
            v.unfix()
        model.heat_transfer_eq.activate()
        model.heat_conservation.activate()
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = solver.solve(model, tee=slc.tee)
        init_log.info_high(f"Initialization Step 3 {idaeslog.condition(res)}.")

        return res


@declare_process_block_class("HeatExchanger1D")
class HeatExchanger1DData(UnitModelBlockData):
    """Standard Heat Exchanger 1D Unit Model Class."""

    default_initializer = HX1DInitializer

    CONFIG = UnitModelBlockData.CONFIG(implicit=True)
    # Template for config arguments for hot and cold side
    _SideTemplate = ConfigBlock()
    _SideTemplate.declare(
        "dynamic",
        ConfigValue(
            default=useDefault,
            domain=DefaultBool,
            description="Dynamic model flag",
            doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}""",
        ),
    )
    _SideTemplate.declare(
        "has_holdup",
        ConfigValue(
            default=useDefault,
            domain=DefaultBool,
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - construct holdup terms,
**False** - do not construct holdup terms}""",
        ),
    )
    _SideTemplate.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    _SideTemplate.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
        ),
    )
    _SideTemplate.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should
be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )
    _SideTemplate.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change terms.}""",
        ),
    )
    _SideTemplate.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Phase equilibrium term construction flag",
            doc="""Argument to enable phase equilibrium.
- True - include phase equilibrium term
- False - do not include phase equilibrium term""",
        ),
    )
    _SideTemplate.declare(
        "property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc="""Property parameter object used to define property
calculations
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a ParameterBlock object""",
        ),
    )
    _SideTemplate.declare(
        "property_package_args",
        ConfigValue(
            default={},
            description="Arguments for constructing property package",
            doc="""A dict of arguments to be passed to the PropertyBlockData
and used when constructing these
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a dict (see property package for documentation)""",
        ),
    )
    # TODO : We should probably think about adding a consistency check for the
    # TODO : discretisation methods as well.
    _SideTemplate.declare(
        "transformation_method",
        ConfigValue(
            default=useDefault,
            description="Discretization method to use for DAE transformation",
            doc="""Discretization method to use for DAE transformation. See
Pyomo documentation for supported transformations.""",
        ),
    )
    _SideTemplate.declare(
        "transformation_scheme",
        ConfigValue(
            default=useDefault,
            description="Discretization scheme to use for DAE transformation",
            doc="""Discretization scheme to use when transformating domain. See
Pyomo documentation for supported schemes.""",
        ),
    )

    # Create individual config blocks for hot and cold side
    CONFIG.declare("hot_side", _SideTemplate(doc="hot side config arguments"))
    CONFIG.declare("cold_side", _SideTemplate(doc="cold side config arguments"))

    # Common config args for both sides
    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            domain=int,
            description="Number of finite elements length domain",
            doc="""Number of finite elements to use when discretizing length
domain (default=20)""",
        ),
    )
    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=5,
            domain=int,
            description="Number of collocation points per finite element",
            doc="""Number of collocation points to use per finite element when
discretizing length domain (default=3)""",
        ),
    )
    CONFIG.declare(
        "flow_type",
        ConfigValue(
            default=HeatExchangerFlowPattern.cocurrent,
            domain=In(HeatExchangerFlowPattern),
            description="Flow configuration of heat exchanger",
            doc="""Flow configuration of heat exchanger
- HeatExchangerFlowPattern.cocurrent: hot and cold flows from 0 to 1
(default)
- HeatExchangerFlowPattern.countercurrent: hot side flows from 0 to 1
cold side flows from 1 to 0""",
        ),
    )
    CONFIG.declare(
        "hot_side_name",
        ConfigValue(
            default=None,
            domain=str,
            doc="Hot side name, sets control volume and inlet and outlet names. "
            "Default = None.",
        ),
    )
    CONFIG.declare(
        "cold_side_name",
        ConfigValue(
            default=None,
            domain=str,
            doc="Cold side name, sets control volume and inlet and outlet names. "
            "Default = None.",
        ),
    )

    def build(self):
        """
        Begin building model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().build()
        self._process_config()

        # Set flow directions for the control volume blocks and specify
        # discretization if not specified.
        if self.config.hot_side.transformation_method is useDefault:
            _log.info(
                "Discretization method was "
                "not specified for the hot side of the "
                "heat exchanger. "
                "Defaulting to finite "
                "difference method on the hot side."
            )
            self.config.hot_side.transformation_method = "dae.finite_difference"
        if self.config.cold_side.transformation_method is useDefault:
            _log.info(
                "Discretization method was "
                "not specified for the cold side of the "
                "heat exchanger. "
                "Defaulting to finite "
                "difference method on the cold side."
            )
            self.config.cold_side.transformation_method = "dae.finite_difference"

        if (
            self.config.hot_side.transformation_method
            != self.config.cold_side.transformation_method
        ):
            raise ConfigurationError(
                "HeatExchanger1D only supports the same transformation "
                "method on the hot and cold side domains for "
                "both cocurrent and countercurrent flow patterns. "
                f"Found method {self.config.hot_side.transformation_method} "
                f"on hot side and method {self.config.cold_side.transformation_method} "
                "on cold side."
            )
        if self.config.hot_side.transformation_method == "dae.collocation":
            if (
                self.config.hot_side.transformation_scheme is useDefault
                or self.config.cold_side.transformation_scheme is useDefault
            ):
                raise ConfigurationError(
                    "If a collocation method is used for HeatExchanger1D, the user "
                    "must specify the transformation scheme they want to use."
                )
            if (
                self.config.hot_side.transformation_scheme
                != self.config.cold_side.transformation_scheme
            ):
                raise ConfigurationError(
                    "If a collocation method is used, "
                    "HeatExchanger1D only supports the same transformation "
                    "scheme on the hot and cold side domains. Found "
                    f"{self.config.hot_side.transformation_scheme} scheme on "
                    f"hot side and {self.config.cold_side.transformation_scheme} "
                    "scheme on cold side."
                )
        else:
            if (
                self.config.hot_side.transformation_scheme
                != self.config.cold_side.transformation_scheme
            ):
                _log.caution(
                    "The hot and cold sides are being discretized with different "
                    "discretization schemes. While this may result in better numerical "
                    "stability if an upwind scheme is used, it may also result in "
                    "energy conservation errors. High-order collocation methods can "
                    "provide both accuracy and numerical stability."
                )

        if self.config.hot_side.transformation_scheme is useDefault:
            _log.info(
                "Discretization scheme was "
                "not specified for the hot side of the "
                "counter-current heat exchanger. "
                "Defaulting to backward finite "
                "difference on the hot side."
            )
            self.config.hot_side.transformation_scheme = "BACKWARD"
        if self.config.cold_side.transformation_scheme is useDefault:
            _log.info(
                "Discretization scheme was "
                "not specified for the cold side of the "
                "counter-current heat exchanger. "
                "Defaulting to backward finite "
                "difference on the cold side."
            )
            self.config.cold_side.transformation_scheme = "BACKWARD"

        if self.config.flow_type == HeatExchangerFlowPattern.cocurrent:
            set_direction_hot = FlowDirection.forward
            set_direction_cold = FlowDirection.forward
            if self.config.hot_side.transformation_scheme != "BACKWARD":
                _log_upwinding_disclaimer(
                    "hot_side",
                    "cocurrent",
                    self.config.hot_side.transformation_scheme,
                    "BACKWARD",
                )
            if self.config.cold_side.transformation_scheme != "BACKWARD":
                _log_upwinding_disclaimer(
                    "cold_side",
                    "cocurrent",
                    self.config.cold_side.transformation_scheme,
                    "BACKWARD",
                )

        elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent:
            set_direction_hot = FlowDirection.forward
            set_direction_cold = FlowDirection.backward
            if self.config.hot_side.transformation_scheme != "BACKWARD":
                _log_upwinding_disclaimer(
                    "hot_side",
                    "countercurrent",
                    self.config.hot_side.transformation_scheme,
                    "BACKWARD",
                )
            if self.config.cold_side.transformation_scheme != "FORWARD":
                _log_upwinding_disclaimer(
                    "cold_side",
                    "countercurrent",
                    self.config.cold_side.transformation_scheme,
                    "FORWARD",
                )
        else:
            raise ConfigurationError(
                "{} HeatExchanger1D only supports cocurrent and "
                "countercurrent flow patterns, but flow_type configuration"
                " argument was set to {}.".format(self.name, self.config.flow_type)
            )

        # Control volume 1D for hot
        self.hot_side = ControlVolume1DBlock(
            dynamic=self.config.hot_side.dynamic,
            has_holdup=self.config.hot_side.has_holdup,
            property_package=self.config.hot_side.property_package,
            property_package_args=self.config.hot_side.property_package_args,
            transformation_method=self.config.hot_side.transformation_method,
            transformation_scheme=self.config.hot_side.transformation_scheme,
            finite_elements=self.config.finite_elements,
            collocation_points=self.config.collocation_points,
        )

        self.cold_side = ControlVolume1DBlock(
            dynamic=self.config.cold_side.dynamic,
            has_holdup=self.config.cold_side.has_holdup,
            property_package=self.config.cold_side.property_package,
            property_package_args=self.config.cold_side.property_package_args,
            transformation_method=self.config.cold_side.transformation_method,
            transformation_scheme=self.config.cold_side.transformation_scheme,
            finite_elements=self.config.finite_elements,
            collocation_points=self.config.collocation_points,
        )

        self.hot_side.add_geometry(flow_direction=set_direction_hot)
        self.cold_side.add_geometry(flow_direction=set_direction_cold)

        self.hot_side.add_state_blocks(
            information_flow=set_direction_hot,
            has_phase_equilibrium=self.config.hot_side.has_phase_equilibrium,
        )
        self.cold_side.add_state_blocks(
            information_flow=set_direction_cold,
            has_phase_equilibrium=self.config.cold_side.has_phase_equilibrium,
        )

        # Populate hot
        self.hot_side.add_material_balances(
            balance_type=self.config.hot_side.material_balance_type,
            has_phase_equilibrium=self.config.hot_side.has_phase_equilibrium,
        )

        self.hot_side.add_energy_balances(
            balance_type=self.config.hot_side.energy_balance_type,
            has_heat_transfer=True,
        )

        self.hot_side.add_momentum_balances(
            balance_type=self.config.hot_side.momentum_balance_type,
            has_pressure_change=self.config.hot_side.has_pressure_change,
        )

        self.hot_side.apply_transformation()

        # Populate cold side
        self.cold_side.add_material_balances(
            balance_type=self.config.cold_side.material_balance_type,
            has_phase_equilibrium=self.config.cold_side.has_phase_equilibrium,
        )

        self.cold_side.add_energy_balances(
            balance_type=self.config.cold_side.energy_balance_type,
            has_heat_transfer=True,
        )

        self.cold_side.add_momentum_balances(
            balance_type=self.config.cold_side.momentum_balance_type,
            has_pressure_change=self.config.cold_side.has_pressure_change,
        )

        self.cold_side.apply_transformation()

        # Add Ports for hot side
        self.add_inlet_port(name="hot_side_inlet", block=self.hot_side)
        self.add_outlet_port(name="hot_side_outlet", block=self.hot_side)

        # Add Ports for cold side
        self.add_inlet_port(name="cold_side_inlet", block=self.cold_side)
        self.add_outlet_port(name="cold_side_outlet", block=self.cold_side)

        # Add references to the user provided aliases if applicable
        add_hx_references(self)

        self._make_geometry()
        self._make_performance()

        hot_side_units = (
            self.hot_side.config.property_package.get_metadata().get_derived_units
        )
        q_units = hot_side_units("power") / hot_side_units("length")

        @self.Constraint(
            self.flowsheet().time,
            self.hot_side.length_domain,
            doc="Heat conservation equality",
        )
        def heat_conservation(self, t, x):
            return pyunits.convert(self.cold_side.heat[t, x], to_units=q_units) == -(
                self.hot_side.heat[t, x]
            )

    def _process_config(self):
        hx_process_config(self)

    def _make_geometry(self):
        hot_side_units = (
            self.hot_side.config.property_package.get_metadata().get_derived_units
        )

        self.area = Var(
            initialize=1, units=hot_side_units("area"), doc="Heat transfer area"
        )

        # Add reference to control volume geometry
        add_object_reference(self, "length", self.hot_side.length)

        # Equate hot and cold side lengths
        @self.Constraint(doc="Equating hot and cold side lengths")
        def length_equality(self):
            return (
                pyunits.convert(
                    self.cold_side.length, to_units=hot_side_units("length")
                )
                == self.hot_side.length
            )

    def _make_performance(self):
        """
        Constraints for unit model.

        Args:
            None

        Returns:
            None
        """
        hot_side_units = (
            self.hot_side.config.property_package.get_metadata().get_derived_units
        )

        # Performance variables
        self.heat_transfer_coefficient = Var(
            self.flowsheet().time,
            self.hot_side.length_domain,
            initialize=50,
            doc="Average heat transfer coefficient",
            units=hot_side_units("heat_transfer_coefficient"),
        )

        @self.Constraint(
            self.flowsheet().time,
            self.hot_side.length_domain,
            doc="Heat transfer between hot_side and cold_side",
        )
        def heat_transfer_eq(self, t, x):
            return self.hot_side.heat[t, x] == -(
                self.heat_transfer_coefficient[t, x]
                * self.area
                / self.length
                * (
                    self.hot_side.properties[t, x].temperature
                    - pyunits.convert(
                        self.cold_side.properties[t, x].temperature,
                        to_units=hot_side_units("temperature"),
                    )
                )
            )

    def initialize_build(
        self,
        hot_side_state_args=None,
        cold_side_state_args=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
        duty=None,
    ):
        """
        Initialization routine for the unit.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            duty : an initial guess for the amount of heat transferred. This
                should be a tuple in the form (value, units). A default value
                is calculated based on stream temperatures, the overall
                heat transfer coefficient, and exchanger area

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Get length values
        if self.length.fixed:
            # Most likely case
            self.cold_side.length.set_value(self.length)
        elif self.cold_side.length.fixed:
            # This would be unusual, but check
            self.length.set_value(self.cold_side.length)
        else:
            # No fixed value for length - we will assume the user knows what they are doing
            pass

        # Initialize control volumes blocks
        Lfix = self.hot_side.length.fixed
        self.hot_side.length.fix()
        flags_hot_side = self.hot_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=hot_side_state_args,
        )
        if not Lfix:
            self.hot_side.length.unfix()

        Lfix = self.cold_side.length.fixed
        self.cold_side.length.fix()
        flags_cold_side = self.cold_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=cold_side_state_args,
        )
        if not Lfix:
            self.cold_side.length.unfix()

        init_log.info_high("Initialization Step 1 Complete.")

        # ---------------------------------------------------------------------
        # Solve unit with fixed heat duty
        # Guess heat duty based on 1/4 of maximum driving force
        hot_side_units = (
            self.hot_side.config.property_package.get_metadata().get_derived_units
        )
        cold_side_units = (
            self.cold_side.config.property_package.get_metadata().get_derived_units
        )
        t0 = self.flowsheet().time.first()
        if duty is None:
            duty = value(
                0.25
                * self.heat_transfer_coefficient[t0, 0]
                * self.area
                * (
                    self.hot_side.properties[t0, 0].temperature
                    - pyunits.convert(
                        self.cold_side.properties[t0, 0].temperature,
                        to_units=hot_side_units("temperature"),
                    )
                )
            )
        else:
            duty = pyunits.convert_value(
                duty[0], from_units=duty[1], to_units=hot_side_units("power")
            )
        duty_per_length = value(duty / self.length)
        # Fix heat duties
        for v in self.hot_side.heat.values():
            v.fix(-duty_per_length)
        for v in self.cold_side.heat.values():
            v.fix(
                pyunits.convert_value(
                    duty_per_length,
                    to_units=cold_side_units("power") / cold_side_units("length"),
                    from_units=hot_side_units("power") / hot_side_units("length"),
                )
            )

        # Deactivate heat duty constraints
        self.heat_transfer_eq.deactivate()
        self.heat_conservation.deactivate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res)))

        # Unfix heat duty and reactivate constraints
        for v in self.hot_side.heat.values():
            v.unfix()
        for v in self.cold_side.heat.values():
            v.unfix()
        self.heat_transfer_eq.activate()
        self.heat_conservation.activate()
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))

        self.hot_side.release_state(flags_hot_side)
        self.cold_side.release_state(flags_cold_side)

        if res is not None and not check_optimal_termination(res):
            raise InitializationError(
                f"{self.name} failed to initialize successfully. Please check "
                f"the output logs for more information."
            )

        init_log.info("Initialization Complete.")

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        var_dict["Area"] = self.area
        var_dict["Length"] = self.length

        return {"vars": var_dict}

    def _get_stream_table_contents(self, time_point=0):
        # Get names for hot and cold sides
        hot_name = self.config.hot_side_name
        if hot_name is None:
            hot_name = "Hot Side"
        cold_name = self.config.cold_side_name
        if cold_name is None:
            cold_name = "Cold Side"
        return create_stream_table_dataframe(
            {
                f"{hot_name} Inlet": self.hot_side_inlet,
                f"{hot_name} Outlet": self.hot_side_outlet,
                f"{cold_name} Inlet": self.cold_side_inlet,
                f"{cold_name} Outlet": self.cold_side_outlet,
            },
            time_point=time_point,
        )

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        for i, c in self.heat_transfer_eq.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(
                    self.hot_side.heat[i], default=1, warning=True
                ),
                overwrite=False,
            )

        for i, c in self.heat_conservation.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(
                    self.hot_side.heat[i], default=1, warning=True
                ),
                overwrite=False,
            )


def _log_upwinding_disclaimer(side, flow_config, bad_scheme, good_scheme):
    _log.info(
        f"For {side}, a {bad_scheme} scheme was chosen to discretize the length domain. "
        f"However, this scheme is not an upwind scheme for {flow_config} flow, and "
        f"as a result may run into numerical stability issues. To avoid this, "
        f"use a {good_scheme} scheme (which may result in energy conservation issues "
        "for coarse discretizations) or use a high-order collocation method."
    )
