#!/usr/bin/env python

"""This script exports results."""

# ############################ LICENSE INFORMATION ############################
# This file is part of the E3 RESOLVE Model.

# Copyright (C) 2019 Energy and Environmental Economics, Inc.
# For contact information, go to www.ethree.com

# The E3 RESOLVE Model is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# The E3 RESOLVE Model 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with the E3 RESOLVE Model (in the file LICENSE.TXT). If not,
# see <http://www.gnu.org/licenses/>.
# #############################################################################

import os
import csv
import traceback
import sys
import pdb

import fileio

def export_results(instance, results, results_directory, debug_mode):
    """
    Export Resolve results. This function is called from run_opt.py.
    It retrieves the relevant sets over which it will loop, then call functions to export different result categories.
    If an exception is encountered, log the error traceback. If not in debug mode, exit. If in debug mode,
    open an interactive Python session that will make it possible to try to correct the error without having to re-run
    problem; quitting the interactive session will resume running the next export function, not exit.
    :param instance: the problem instance
    :param results: the results
    :param results_directory: directory to export results files to
    :param debug_mode:
    :return:
    """

    print('Exporting results... ')

    # First, load solution
    load_solution(instance, results)

    # Call various export functions
    # Export loads and power balance
    try:
        export_loads_and_power_balance(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting loads and power balance! Check export_loads_and_power_balance()."
        handle_exception(msg, debug_mode)

    # Export builds, capital costs, and fixed O&M costs
    try:
        export_resource_build(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting resource builds! Check export_resource_build()."
        handle_exception(msg, debug_mode)

    # Custom period constraints
    if instance.custom_period_constraints:
        try:
            export_custom_constraint_period(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting custom period constraints! Check export_custom_constraint_period_group_constraints()."
            handle_exception(msg, debug_mode)

    # Custom timepoint constraints
    if instance.custom_timepoint_constraints:
        try:
            export_custom_constraint_timepoint(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting custom timepoint constraints! Check export_custom_constraint_timepoint_group_constraints()."
            handle_exception(msg, debug_mode)

    # Export reliability contribution of tx lines
    if instance.allow_tx_reliability_contribution:
        try:
            export_tx_reliability_contributions(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting resource builds! Check export_tx_reliability_contributions()."
            handle_exception(msg, debug_mode)

    try:
        export_storage_build_variables(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting storage builds! Check export_storage_build_variables()."
        handle_exception(msg, debug_mode)

    # Export hourly operations
    try:
        export_hourly_operations(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting hourly operations! Check export_hourly_operations()."
        handle_exception(msg, debug_mode)

    if instance.itc_paired_storage:
        try:
            export_paired_storage_operations(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting paired storage operations! Check export_paired_storage_operations()."
            handle_exception(msg, debug_mode)

    # Export hourly transmission flows
    try:
        export_transmission_flows(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting hourly transmission flows! Check export_transmission_flows()."
        handle_exception(msg, debug_mode)

    # Export simultaneous flow group duals
    try:
        simultaneous_transmission_flow_duals(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting simultaneous flow group duals! Check simultaneous_transmission_flow_duals()."
        handle_exception(msg, debug_mode)

    # Export curtailment
    try:
        export_curtailment(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting curtailment! Check export_curtailment()."
        handle_exception(msg, debug_mode)

    # Export reliability margin and ELCC surface results
    try:
        export_reliability_constraints(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting reliability margin or ELCC surface results! Check export_reliability_constraints()."
        handle_exception(msg, debug_mode)

    # Export reliability resource contribution results
    try:
        export_reliability_resources(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting reliability resource contribution results! Check export_reliability_resources()."
        handle_exception(msg, debug_mode)

    # Export RPS results
    if instance.include_RPS:
        try:
            export_rps(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting RPS results! Check export_rps()."
            handle_exception(msg, debug_mode)

    # Record emissions group targets and duals.
    if instance.include_emissions:
        try:
            export_emissions_groups(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting emissions groups! Check export_emissions_groups()."
            handle_exception(msg, debug_mode)

    # Export transmission costs
    if instance.deliverability:
        try:
            export_deliverability(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting transmission costs! Check export_deliverability()."
            handle_exception(msg, debug_mode)

    if instance.allow_tx_build:
        try:
            export_transmission_build(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting transmission build! Check export_transmission_build()."
            handle_exception(msg, debug_mode)

    # Export fuel burn
    try:
        export_fuel_burn(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting fuel burn! Check export_fuel_burn()."
        handle_exception(msg, debug_mode)

    # Export emissions
    if instance.include_emissions:
        try:
            export_emissions(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting emissions! Check export_emissions()."
            handle_exception(msg, debug_mode)

        # Export emissions imports into the emissions target area
        try:
            export_emissions_imports(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting emissions imports into the emissions target area! Check export_emissions_imports()."
            handle_exception(msg, debug_mode)

    # Export the duals of reserve constraints that are indexed by the set of timepoints
    try:
        export_reserve_timepoints(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting reserve timepoint duals + violations! Check export_reserve_timepoints()."
        handle_exception(msg, debug_mode)

    # Export the duals of the ramping constraints
    try:
        export_ramping_duals_by_timepoint(instance, results_directory)
    except Exception as err:
        msg = "ERROR exporting ramping duals timepoint duals! Check export_ramping_duals_by_timepoint()."
        handle_exception(msg, debug_mode)

    # Export Hydro multi-day sharing daily budget increase information
    if instance.interday_energy_sharing:
        try:
            export_daily_budget_changes(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting daily budget changes! Check export_daily_budget_changes()."
            handle_exception(msg, debug_mode)

    # Export Line limits changes if resource_tx_use is enabled
    if instance.resource_use_tx_capacity:
        try:
            export_line_limits_resource_tx_use(instance, results_directory)
        except Exception as err:
            msg = "ERROR exporting dedicated imports' changes to line limits! Check export_line_limits_resource_tx_use()."
            handle_exception(msg, debug_mode)


# formatting functions
# return two decimal places
def format_2f(input_data):
    """
    :param input_data: The data to format
    :return:
    """
    if input_data is None:
        formatted_data = None
    else:
        formatted_data = '{:0.2f}'.format(input_data)
        # if formatted_data is negative but rounds to zero, it will be printed as a negative zero
        # this gets rid of the negative zero
        if formatted_data == '0.00' or formatted_data == '-0.00':
            formatted_data = 0
    return formatted_data


# return six decimal places
def format_6f(input_data):
    """
    :param input_data: The data to format
    :return:
    """
    if input_data is None:
        formatted_data = None
    else:
        formatted_data = '{:0.6f}'.format(input_data)
        # if formatted_data is negative but rounds to zero, it will be printed as a negative zero
        # this gets rid of the negative zero
        if formatted_data == '0.000000' or formatted_data == '-0.000000':
            formatted_data = 0
    return formatted_data


# #################### Data Exports #################### #
def load_solution(instance, results):
    """
    Load results. This function is called by export_results().
    :param instance:
    :param results:
    :return:
    """
    instance.solutions.load_from(results)


def export_loads_and_power_balance(instance, results_directory):
    """
    This file contains hourly loads for each zone and timepoint that were input into the RESOLVE optimization,
    as well as the amount of overgeneration and unserved energy for each zone and timepoint.
    It also contains the shadow price of the zonal power balance constraint for each timepoint,
    which is analogous to the hourly energy price.
    Care should be taken interpreting this energy price as the RESOLVE investment framework differs
    in several fundamental ways from a conventional production simulation.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...loads and power balance...')

    loads_and_balance_writer = fileio.csvwriter(os.path.join(results_directory, "loads_and_power_balance.csv"))
    loads_and_balance_writer.writerow([
        "zone",
        "period",
        "timepoint_id",
        "day_id",
        "hour_of_day",
        "day_weight",
        "input_load_mw",
        "hourly_energy_cost_$_per_mwh",
        "unserved_energy_mw",
        "overgeneration_mw",
        "total_generation_mw",
        "increase_load_mw",
        "net_generation_mw",
        "ee_ftm_load_reduction_mw"
    ])

    for zone in instance.ZONES:
        for timepoint in instance.TIMEPOINTS:

            # discount_and_day_weight un-discounts the periods and un-weighs the days to obtain intuitive dual values.
            discount_and_day_weight = \
                instance.discount_factor[instance.period[timepoint]] * instance.day_weight[instance.day[timepoint]]

            if instance.allow_unserved_energy:
                unserved_energy = instance.Unserved_Energy_MW[zone, timepoint].value
            else:
                unserved_energy = 0

            generation = instance.Zonal_Provide_Power_MW[zone, timepoint]()
            ee_load_reduction = 0
            increase_load = instance.Zonal_Increase_Load_MW[zone, timepoint]()
            net_generation = instance.Zonal_Net_Provide_Power_MW[zone, timepoint]()

            # Aggregate generation and storage charging by zone for given timepoint
            for resource in instance.EE_PROGRAMS:

                # Sum all generation in zone
                if instance.zone[resource] == zone:

                    # EE load impact
                    # if resource in instance.EE_PROGRAMS:
                    ee_load_reduction += instance.EE_Reduced_Load_FTM_MW[resource, timepoint]()

            # the dual value (or shadow price) of the zonal power balance constraint is the closest thing
            # to an hourly energy cost that Resolve calculates
            loads_and_balance_writer.writerow([
                zone,
                instance.period[timepoint],
                timepoint,
                instance.day[timepoint],
                instance.hour_of_day[timepoint],
                instance.day_weight[instance.day[timepoint]],
                format_2f(instance.input_load_mw[zone, timepoint]),
                format_2f(instance.dual[instance.Zonal_Power_Balance_Constraint[zone, timepoint]]
                          / discount_and_day_weight),
                format_2f(unserved_energy),
                format_2f(instance.Overgeneration_MW[zone, timepoint].value),
                format_2f(generation),
                format_2f(increase_load),
                format_2f(net_generation),
                format_2f(ee_load_reduction)
            ])


def export_custom_constraint_period(instance, results_directory):
    """
    This file contains information and shadow prices for custom period constraints.

    :param instance:
    :param results_directory:
    :return:
    """
    print('...custom period constraints...')

    period_constraint_writer = fileio.csvwriter(
        os.path.join(results_directory, "custom_constraint_period.csv"))
    period_constraint_writer.writerow([
        "custom_constraint_group_name",
        "period",
        "shadow_price_$",
        "limit_per_year",
        "LHS_value",
        "LHS_expression"
    ])

    for constraint_group in instance.CUSTOM_CONSTRAINT_PERIOD_GROUPS:
        for period in instance.PERIODS:
            if not instance.custom_constraint_period_limit[constraint_group, period] == float('inf'):
                period_constraint_writer.writerow([
                    constraint_group,
                    period,
                    format_2f(instance.dual[instance.Custom_Constraint_Period[constraint_group, period]]
                              / instance.discount_factor[period]),
                    format_2f(instance.custom_constraint_period_limit[constraint_group, period]),
                    format_2f(instance.Custom_Constraint_Period_LHS[constraint_group, period]()),
                    instance.Custom_Constraint_Period_LHS[constraint_group, period].expr
                ])


def export_custom_constraint_timepoint(instance, results_directory):
    """
    This file contains information and shadow prices for custom timepoint constraints.

    :param instance:
    :param results_directory:
    :return:
    """
    print('...custom timepoint constraints...')

    timepoint_constraint_writer = fileio.csvwriter(
        os.path.join(results_directory, "custom_constraint_timepoint.csv"))
    timepoint_constraint_writer.writerow([
        "custom_constraint_group_name",
        "timepoint",
        "shadow_price_$",
        "limit_per_timepoint",
        "LHS_value",
        "LHS_expression"
    ])

    for constraint_group in instance.CUSTOM_CONSTRAINT_TIMEPOINT_GROUPS:
        for timepoint in instance.TIMEPOINTS:
            if not instance.custom_constraint_timepoint_limit[constraint_group, timepoint] == float('inf'):
                timepoint_constraint_writer.writerow([
                    constraint_group,
                    timepoint,
                    format_2f(instance.dual[instance.Custom_Constraint_Timepoint[constraint_group, timepoint]]
                              / instance.discount_factor[instance.period[timepoint]]),
                    format_2f(instance.custom_constraint_timepoint_limit[constraint_group, timepoint]),
                    format_2f(instance.Custom_Constraint_Timepoint_LHS[constraint_group, timepoint]()),
                    instance.Custom_Constraint_Timepoint_LHS[constraint_group, timepoint].expr
                ])


def export_tx_reliability_contributions(instance, results_directory):
    """
    This file contains reliability contribution of tx lines to each reliability constraint
    for each period.

    :param instance:
    :param results_directory:
    :return:
    """
    print('...transmission reliability contributions...')

    transmission_reliability_contribution_writer = fileio.csvwriter(os.path.join(results_directory, "transmission_reliability_contribution.csv"))
    transmission_reliability_contribution_writer.writerow([
        "reliability_constraint",
        "period",
        "tx_line",
        "total_reliability_contribution",
        "existing_tx_reliability_contribution",
        "new_tx_reliability_contribution"
    ])

    for reliability, tx_line, period in instance.TRANSMISSION_RELIABILITY_MAP:

        new_tx_mw = 0
        if instance.allow_tx_build:
            new_tx_mw = instance.New_Tx_Reliability_Contribution_MW[reliability, tx_line, period]()
            
        transmission_reliability_contribution_writer.writerow([
            reliability,
            period,
            tx_line,
            format_2f(instance.Existing_Tx_Reliability_Contribution_MW[reliability, tx_line, period]()
                      + new_tx_mw),
            format_2f(instance.Existing_Tx_Reliability_Contribution_MW[reliability, tx_line, period]()),
            format_2f(new_tx_mw),
        ])


def export_resource_build(instance, results_directory):
    """
    This file contains the investment decisions made by RESOLVE for each candidate resource in each period
    as well as the capacities of resources for which no investment decisions were made
    (e.g. existing resources and contracted resources that come online at some point in the future).
    The deliverability status of the resource is included.
    Blanks cells indicate that RESOLVE does not make this decision.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...resource build...')

    build_writer = fileio.csvwriter(os.path.join(results_directory, "resource_build.csv"))
    build_writer.writerow([
        "period",
        "resource",
        "zone",
        "technology",
        "planned_capacity_mw",
        "new_build_mw",
        "cumulative_new_build_mw",
        "operational_capacity_mw",
        "deliverability_zone",
        "delivered_capacity_MW",
        "undelivered_capacity_mw",
        "capacity_limit_dual_$",
        "unit_size_mw",
        "operational_units",
        "capital_cost_$",
        "fixed_o_and_m_cost_$",
        "period_discount_factor",
        "operational_planned_capacity_mw",
        "retired_planned_capacity_mw",
        "operational_new_capacity_mw",
        "retired_new_capacity_mw",
        "min_cumulative_new_build_mw"
    ])

    for period in instance.PERIODS:
        for resource in instance.RESOURCES:

            # Resources where new capacity can be built
            if resource in instance.NEW_BUILD_RESOURCES:
                build_capacity_mw = instance.Build_Capacity_MW[resource, period].value
                cumulative_new_installed_capacity_mw = \
                    instance.Cumulative_New_Installed_Capacity_MW[resource, period]()
                operational_new_capacity_mw = instance.Operational_New_Capacity_MW[resource, period]()
                # calculate capital costs
                capital_cost = instance.Capital_Cost_Annual_In_Period_Dollars[resource, period]()
                min_cumulative_new_build_mw = instance.min_cumulative_new_build_mw[resource, period]
            else:
                build_capacity_mw = None
                cumulative_new_installed_capacity_mw = None
                operational_new_capacity_mw = None
                min_cumulative_new_build_mw = None
                capital_cost = None

            # report deliverability/energy only capacity only for resources in the set TX_DELIVERABILITY_RESOURCES
            if instance.deliverability and resource in instance.DELIVERABILITY_RESOURCES:
                deliverability_zone = '-'.join(instance.RESOURCE_DELIVERABILITY_ZONES[resource])
                delivered_capacity_mw = \
                    instance.Delivered_Capacity_MW[resource, period].value
                undelivered_capacity_mw = instance.Undelivered_Capacity_MW[resource, period].value
            else:
                deliverability_zone = None
                delivered_capacity_mw = None
                undelivered_capacity_mw = None

            if resource in instance.CAPACITY_LIMITED_RESOURCES:
                capacity_limit_dual = instance.dual[instance.Capacity_Limit_Constraint[resource, period]] \
                    / instance.discount_factor[period]
            else:
                capacity_limit_dual = None

            planned_capacity = instance.planned_installed_capacity_mw[resource, period]
            operational_capacity = instance.Operational_Capacity_MW[resource, period]()

            operational_planned_capacity_mw = instance.Operational_Planned_Capacity_MW[resource, period]()
            fixed_o_and_m_cost = instance.Fixed_OM_Annual_In_Period_Dollars[resource, period]()

            if resource in instance.CAN_RETIRE_RESOURCES:
                retired_planned_capacity_mw = instance.Retire_Planned_Capacity_Cumulative_MW[resource, period]()
            else:
                retired_planned_capacity_mw = None

            if resource in instance.CAN_RETIRE_RESOURCES_NEW:
                retired_new_capacity_mw = instance.Retire_New_Capacity_In_Period_Cumulative_MW[resource, period]()
            else:
                retired_new_capacity_mw = None

            if resource in instance.UNIT_COMMITMENT_RESOURCES:
                unit_size = instance.unit_size_mw[resource]
                operational_units = operational_capacity / unit_size
            else:
                unit_size = None
                operational_units = None


            build_writer.writerow([
                period,
                resource,
                instance.zone[resource],
                instance.technology[resource],
                format_2f(planned_capacity),
                format_2f(build_capacity_mw),
                format_2f(cumulative_new_installed_capacity_mw),
                format_2f(operational_capacity),
                deliverability_zone,
                format_2f(delivered_capacity_mw),
                format_2f(undelivered_capacity_mw),
                format_2f(capacity_limit_dual),
                format_2f(unit_size),
                format_2f(operational_units),
                format_2f(capital_cost),
                format_2f(fixed_o_and_m_cost),
                instance.discount_factor[period],
                format_2f(operational_planned_capacity_mw),
                format_2f(retired_planned_capacity_mw),
                format_2f(operational_new_capacity_mw),
                format_2f(retired_new_capacity_mw),
                format_2f(min_cumulative_new_build_mw)
            ])





def export_storage_build_variables(instance, results_directory):
    """
    This file contains investment decisions made by RESOLVE in every period for the energy capacity
    of each storage resource. Blanks cells indicate that RESOLVE does not make a decision.
    :param instance:
    :param results_directory:
    :return:
    """
    storage_build_writer = fileio.csvwriter(os.path.join(results_directory, "storage_build.csv"))
    storage_build_writer.writerow([
        "period",
        "resource",
        "zone",
        "technology",
        "planned_energy_capacity_mwh",
        "new_build_mwh",
        "cumulative_new_build_mwh",
        "total_energy_capacity_mwh",
        "planned_storage_charge_capacity_mw",
        "new_charge_capacity_build_mw",
        "cumulative_new_charge_capacity_build_mw",
        "total_charge_capacity_mw"
    ])

    for period in instance.PERIODS:
        for resource in instance.STORAGE_RESOURCES:
            if resource in instance.NEW_BUILD_STORAGE_RESOURCES:
                new_build = instance.Build_Storage_Energy_Capacity_MWh[resource, period].value
                cumulative_new_build = instance.Cumulative_New_Storage_Energy_Capacity_MWh[resource, period]()

                if instance.planned_storage_charge_capacity_mw[resource,period] != float("inf"):
                    planned_charge_capacity=instance.planned_storage_charge_capacity_mw[resource,period]
                    new_charge_build=instance.New_Storage_Charge_Capacity_Build_MW[resource, period].value
                    cumulative_charge_build=instance.Cumulative_New_Storage_Charge_Capacity_Build_MW[resource, period]()
                    operational_charging_capacity=instance.Operational_Charging_Capacity_MW[resource,period]()
                else:
                    planned_charge_capacity = None
                    new_charge_build = None
                    cumulative_charge_build = None
                    operational_charging_capacity = None

            else:
                new_build = None
                cumulative_new_build = None

                planned_charge_capacity=None
                new_charge_build=None
                cumulative_charge_build=None
                operational_charging_capacity=None

            storage_build_writer.writerow([
                period,
                resource,
                instance.zone[resource],
                instance.technology[resource],
                format_2f(instance.planned_storage_energy_capacity_mwh[resource, period]),
                format_2f(new_build),
                format_2f(cumulative_new_build),
                format_2f(instance.Total_Storage_Energy_Capacity_MWh[resource, period]()),
                format_2f(planned_charge_capacity),
                format_2f(new_charge_build),
                format_2f(cumulative_charge_build),
                format_2f(operational_charging_capacity)
            ])


def export_hourly_operations(instance, results_directory):
    """
    This file contains the hourly operational decisions made by RESOLVE for each resource on all days modeled
    in RESOLVE (currently 37 days per year). Types of operational decisions included in this file are:
    unit commitment, power production, reserve commitment, and flexible load dispatch.
    Blanks cells indicate that RESOLVE does not make this decision.
    This file also contains the hourly operational cost of the decisions made by RESOLVE for each resource,
    including variable costs, fuel costs, start-up costs, and shut-down costs.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...operations...')

    operations_writer = fileio.csvwriter(os.path.join(results_directory, "operations.csv"))
    operations_writer.writerow([
        "resource",
        "zone",
        "technology",
        "period",
        "timepoint_id",
        "day",
        "hour_of_day",
        "day_weight",
        "power_mw",
        "increase_load_mw",
        "net_power_mw",
        "scheduled_curtailment_mw",
        "upward_reg_mw",
        "downward_reg_mw",
        "upward_lf_reserve_mw",
        "downward_lf_reserve_mw",
        "spin_mw",
        "freq_response_total_mw",
        "freq_response_partial_mw",
        "committed_units",
        "committed_capacity_mw",
        "fully_operational_units",
        "start_units",
        "starting_units",
        "shut_down_units",
        "shutting_down_units",
        "energy_in_storage_mwh",
        "subhourly_dispatch_down_mw",
        "subhourly_dispatch_up_mw",
        "ee_ftm_load_reduction_mw",
        "variable_costs_$",
        "total_fuel_costs_$",
        "base_fuel_costs_$",
        "blend_fuel_costs_$",
        "unit_start_costs_$",
        "unit_shutdown_costs_$",
        "curtailment_costs_$"
    ])

    for timepoint in instance.TIMEPOINTS:

        total_curtailable_energy = sum(
            instance.Provide_Power_MW[r, timepoint].value
            for r in instance.LOAD_FOLLOWING_ZONE_CURTAILABLE_RESOURCES)

        total_scheduled_curtailment = sum(
            instance.Scheduled_Curtailment_MW[r, timepoint].value
            for r in instance.LOAD_FOLLOWING_ZONE_CURTAILABLE_RESOURCES)

        for resource in instance.RESOURCES:

            if resource in instance.RESOURCES_WITH_PROVIDE_POWER:
                provide_power = instance.Provide_Power_MW[resource, timepoint].value
            else:
                provide_power = None

            # value for increase load mw
            if resource in instance.RESOURCES_THAT_INCREASE_LOAD:
                increase_load = instance.Increase_Load_MW[resource, timepoint].value
            else:
                increase_load = None

            if increase_load is None:
                net_power = provide_power
            elif provide_power is None:
                net_power = - increase_load
            else:
                net_power = provide_power - increase_load

            if resource in instance.CURTAILABLE_VARIABLE_RESOURCES:
                scheduled_curtailment = instance.Scheduled_Curtailment_MW[resource, timepoint].value
                curtailment_costs = scheduled_curtailment * \
                                    instance.curtailment_cost_per_mwh[resource, instance.period[timepoint]]
            else:
                scheduled_curtailment = None
                curtailment_costs = None

            if resource in instance.THERMAL_RESOURCES:
                fuel_costs = instance.Total_Fuel_Cost[resource, timepoint]()
                base_fuel_costs = instance.Base_Fuel_Cost[resource, timepoint]()
            else:
                fuel_costs = None
                base_fuel_costs = None

            if resource in instance.BLEND_FUEL_RESOURCES:
                blend_fuel_costs = instance.Blend_Fuel_Cost[resource, timepoint]()
            else:
                blend_fuel_costs = None

            # Unit Commitment resources have commitment variables
            if resource in instance.UNIT_COMMITMENT_RESOURCES:
                committed_units = instance.Commit_Units[resource, timepoint].value
                committed_capacity_mw = instance.Commit_Capacity_MW[resource, timepoint]()
                start_units = instance.Start_Units[resource, timepoint].value
                unit_start_costs = instance.Start_Cost_In_Timepoint[resource, timepoint]()
                starting_units = instance.Starting_Units[resource, timepoint]()
                shut_down_units = instance.Shut_Down_Units[resource, timepoint].value
                shutting_down_units = instance.Shutting_Down_Units[resource, timepoint]()
                unit_shutdown_costs = instance.Shutdown_Cost_In_Timepoint[resource, timepoint]()

            else:
                committed_units = None
                committed_capacity_mw = None
                start_units = None
                unit_start_costs = None
                starting_units = None
                shut_down_units = None
                shutting_down_units = None
                unit_shutdown_costs = None

            if resource in instance.UNIT_COMMITMENT_RAMP_LIMITED_RESOURCES:
                fully_operational_units = instance.Fully_Operational_Units[resource, timepoint]()
            else:
                fully_operational_units = None

            # Resources that provide regulation
            if resource in instance.REGULATION_RESERVE_RESOURCES:
                upward_reg = instance.Provide_Upward_Reg_Net_MW[resource, timepoint]()
                downward_reg = instance.Provide_Downward_Reg_Net_MW[resource, timepoint]()
            else:
                upward_reg = None
                downward_reg = None

            # Resources that provide load-following reserves
            if resource in instance.LOAD_FOLLOWING_RESERVE_RESOURCES:
                upward_lf_reserves = instance.Provide_LF_Upward_Reserve_Net_MW[resource, timepoint]()
                downward_lf_reserves = instance.Provide_LF_Downward_Reserve_Net_MW[resource, timepoint]()

            elif (resource in instance.LOAD_FOLLOWING_ZONE_CURTAILABLE_RESOURCES):
                if total_curtailable_energy > 0.0:
                    subhourly_resource_downward_dispatch = (
                        instance.Variable_Resource_Provide_Downward_LF_MW[timepoint].value *
                        instance.Provide_Power_MW[resource, timepoint].value /
                        total_curtailable_energy)

                    downward_lf_reserves = subhourly_resource_downward_dispatch
                else:
                    downward_lf_reserves = 0.0

                if total_scheduled_curtailment > 0.0:
                    subhourly_resource_upward_dispatch = (
                        instance.Variable_Resource_Provide_Upward_LF_MW[timepoint].value *
                        instance.Scheduled_Curtailment_MW[resource, timepoint].value /
                        total_scheduled_curtailment)

                    upward_lf_reserves = subhourly_resource_upward_dispatch
                else:
                    upward_lf_reserves = 0.0
            else:
                upward_lf_reserves = None
                downward_lf_reserves = None

            # Resources that provide spin
            if resource in instance.SPINNING_RESERVE_RESOURCES:
                spin_mw = instance.Provide_Spin_Net_MW[resource, timepoint]()
            else:
                spin_mw = None

            if resource in instance.TOTAL_FREQ_RESP_RESOURCES:
                freq_response_total = instance.Provide_Frequency_Response_Net_MW[resource, timepoint]()
            else:
                freq_response_total = None

            if resource in instance.PARTIAL_FREQ_RESP_RESOURCES:
                freq_response_partial = instance.Provide_Frequency_Response_Net_MW[resource, timepoint]()
            else:
                freq_response_partial = None

            # Energy in storage
            if resource in instance.RESOURCES_WITH_ENERGY_IN_STORAGE:
                energy_in_storage = instance.Energy_in_Storage_MWh[resource, timepoint].value
            else:
                energy_in_storage = None

            # Subhourly dispatch
            # up = discharge, down = charge
            # renewable subhourly curtailment/dispatch is recorded elsewhere because it implemented as a fleet-wide
            # rather than a resource-specific dispatch

            if resource in instance.REGULATION_RESERVE_RESOURCES \
                    or resource in instance.LOAD_FOLLOWING_RESERVE_RESOURCES:
                subhourly_dispatch_down = 0.0
                subhourly_dispatch_up = 0.0
                if resource in instance.REGULATION_RESERVE_RESOURCES:
                    subhourly_dispatch_down += downward_reg * instance.reg_dispatch_fraction
                    subhourly_dispatch_up += upward_reg * instance.reg_dispatch_fraction
                if resource in instance.LOAD_FOLLOWING_RESERVE_RESOURCES:
                    subhourly_dispatch_down += downward_lf_reserves * instance.lf_reserve_dispatch_fraction
                    subhourly_dispatch_up += upward_lf_reserves * instance.lf_reserve_dispatch_fraction
            else:
                subhourly_dispatch_down = None
                subhourly_dispatch_up = None


            if resource in instance.EE_PROGRAMS:
                ee_load_reduction = instance.EE_Reduced_Load_FTM_MW[resource, timepoint]()
            else:
                ee_load_reduction = None


            operations_writer.writerow([
                resource,
                instance.zone[resource],
                instance.technology[resource],
                instance.period[timepoint],
                timepoint,
                instance.day[timepoint],
                instance.hour_of_day[timepoint],
                instance.day_weight[instance.day[timepoint]],
                format_2f(provide_power),
                format_2f(increase_load),
                format_2f(net_power),
                format_2f(scheduled_curtailment),
                format_2f(upward_reg),
                format_2f(downward_reg),
                format_2f(upward_lf_reserves),
                format_2f(downward_lf_reserves),
                format_2f(spin_mw),
                format_2f(freq_response_total),
                format_2f(freq_response_partial),
                format_6f(committed_units),
                format_2f(committed_capacity_mw),
                format_6f(fully_operational_units),
                format_6f(start_units),
                format_6f(starting_units),
                format_6f(shut_down_units),
                format_6f(shutting_down_units),
                format_2f(energy_in_storage),
                format_2f(subhourly_dispatch_down),
                format_2f(subhourly_dispatch_up),
                format_2f(ee_load_reduction),
                format_2f(instance.Variable_Cost_In_Timepoint[resource, timepoint]()),
                format_2f(fuel_costs),
                format_2f(base_fuel_costs),
                format_2f(blend_fuel_costs),
                format_2f(unit_start_costs),
                format_2f(unit_shutdown_costs),
                format_2f(curtailment_costs)
            ])


def export_paired_storage_operations(instance, results_directory):
    """Consolidate paired storage and generation resource operations."""
    print("...paired storage operations...")

    writer = fileio.csvwriter(os.path.join(results_directory, "operations_paired_storage.csv"))

    resources_list = []
    for r in instance.PAIRED_STORAGE_RESOURCES:
        resources_list.append(r)
        resources_list.append(instance.paired_supply_resource[r])

    writer.writerow(["timepoint"] + resources_list)
    for t in instance.TIMEPOINTS:
        results_list = []
        for r in resources_list:
            if r in instance.PAIRED_STORAGE_RESOURCES:
                storage_net_charging = (
                    instance.Increase_Load_MW[r, t].value -
                    instance.Provide_Power_MW[r, t].value)
                if r in instance.REGULATION_RESERVE_RESOURCES:
                    storage_net_charging += (instance.reg_dispatch_fraction * (
                        instance.Provide_Downward_Reg_Net_MW[r, t]() -
                        instance.Provide_Upward_Reg_Net_MW[r, t]()))
                if r in instance.LOAD_FOLLOWING_RESERVE_RESOURCES:
                    storage_net_charging += (instance.lf_reserve_dispatch_fraction * (
                        instance.Provide_LF_Downward_Reserve_Net_MW[r, t]() -
                        instance.Provide_LF_Upward_Reserve_Net_MW[r, t]()))

                results_list.append(format_2f(storage_net_charging))
            else:
                results_list.append(format_2f(instance.Provide_Power_MW[r, t].value))
        writer.writerow([t] + results_list)


def export_transmission_flows(instance, results_directory):
    """
    This file contains the hourly transmission dispatch decisions made by RESOLVE for each transmission line,
    as well as the shadow price of flow limits on each line.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...transmit power...')

    tx_writer = fileio.csvwriter(os.path.join(results_directory, "transmit_power.csv"))
    tx_writer.writerow([
        "transmission_line",
        "transmission_from",
        "transmission_to",
        "period",
        "timepoint_id",
        "day",
        "hour_of_day",
        "day_weight",
        "unspecified_power_mw",
        "unspecified_power_positive_direction_mw",
        "unspecified_power_negative_direction_mw",
        "max_flow_dual_$",
        "min_flow_dual_$",
        "energy_cost_from_$_per_mwh",
        "energy_cost_to_$_per_mwh",
        "hurdle_cost_positive_direction_$",
        "hurdle_cost_negative_direction_$",
        "power_including_dedicated_imports_mw"
    ])

    for timepoint in instance.TIMEPOINTS:

        # discount_and_day_weight un-discounts the periods and un-weighs the days to obtain intuitive dual values.
        discount_and_day_weight = \
            instance.discount_factor[instance.period[timepoint]] * instance.day_weight[instance.day[timepoint]]

        for line in instance.TRANSMISSION_LINES:

            max_flow_dual = instance.dual[instance.Transmission_Max_Flow_Constraint[line, timepoint]] \
                / discount_and_day_weight
            min_flow_dual = instance.dual[instance.Transmission_Min_Flow_Constraint[line, timepoint]] \
                / discount_and_day_weight

            energy_cost_from = instance.dual[instance.Zonal_Power_Balance_Constraint[
                instance.transmission_from[line], timepoint]] / discount_and_day_weight
            energy_cost_to = instance.dual[instance.Zonal_Power_Balance_Constraint[
                instance.transmission_to[line], timepoint]] / discount_and_day_weight

            tx_writer.writerow([
                line,
                instance.transmission_from[line],
                instance.transmission_to[line],
                instance.period[timepoint],
                timepoint,
                instance.day[timepoint],
                instance.hour_of_day[timepoint],
                instance.day_weight[instance.day[timepoint]],
                format_2f(instance.Transmit_Power_Unspecified_MW[line, timepoint].value),
                format_2f(instance.Transmit_Power_Unspecified_Positive_Direction_MW[line, timepoint].value),
                format_2f(instance.Transmit_Power_Unspecified_Negative_Direction_MW[line, timepoint].value),
                format_2f(max_flow_dual),
                format_2f(min_flow_dual),
                format_2f(energy_cost_from),
                format_2f(energy_cost_to),
                format_2f(instance.Hurdle_Cost_Per_Timepoint_Positive_Direction[line, timepoint]()),
                format_2f(instance.Hurdle_Cost_Per_Timepoint_Negative_Direction[line, timepoint]()),
                format_2f(instance.Transmit_Power_MW[line, timepoint]())
            ])
        # Export transmission flows for semi storage zones when the feature is allowed
        if instance.allow_semi_storage_zones:
            for ssz in instance.SEMI_STORAGE_ZONES:
                line = "ssz_" + ssz
                max_flow_dual = instance.dual[instance.SSZ_Tx_Limit_Max_Constraint[ssz, timepoint]] \
                                / discount_and_day_weight
                min_flow_dual = instance.dual[instance.SSZ_Tx_Limit_Min_Constraint[ssz, timepoint]] \
                                / discount_and_day_weight
                energy_cost_from = instance.dual[instance.Zonal_Power_Balance_Constraint[
                    instance.ssz_from_zone[ssz], timepoint]] / discount_and_day_weight

                # Hurdle rate costs incurred by sending power along transmission lines

                hurdle_cost_positive_direction = instance.SSZ_Positive_Transmit_Power_MW[ssz, timepoint].value * \
                                                 instance.ssz_positive_direction_hurdle_rate_per_mw[
                                                     ssz, instance.period[timepoint]]
                hurdle_cost_negative_direction = instance.SSZ_Negative_Transmit_Power_MW[ssz, timepoint].value * \
                                                 instance.ssz_negative_direction_hurdle_rate_per_mw[
                                                     ssz, instance.period[timepoint]]

                tx_writer.writerow([
                    line,  # transmission line
                    instance.ssz_from_zone[ssz],  # transmission_from,
                    ssz,  # transmission_to
                    instance.period[timepoint],
                    timepoint,
                    instance.day[timepoint],
                    instance.hour_of_day[timepoint],
                    instance.day_weight[instance.day[timepoint]],
                    format_2f(instance.SSZ_Transmit_Power_MW[ssz, timepoint].value),
                    format_2f(instance.SSZ_Positive_Transmit_Power_MW[ssz, timepoint].value),  # positive_direction_mw
                    format_2f(instance.SSZ_Negative_Transmit_Power_MW[ssz, timepoint].value),  # negative_direction_mw
                    format_2f(max_flow_dual),
                    format_2f(min_flow_dual),
                    format_2f(energy_cost_from),
                    None,  # energy cost to
                    format_2f(hurdle_cost_positive_direction),
                    format_2f(hurdle_cost_negative_direction),
                    format_2f(instance.SSZ_Transmit_Power_MW[ssz, timepoint].value)
                ])


def simultaneous_transmission_flow_duals(instance, results_directory):
    """
    This file contains the hourly shadow prices of constraints that limit
    the sum of flows on groups of transmission lines.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...simultaneous transmission flow duals...')

    sim_flow_writer = fileio.csvwriter(os.path.join(results_directory, "sim_flow_group_duals.csv"))
    sim_flow_writer.writerow([
        "simultaneous_flow_group",
        "period",
        "timepoint_id",
        "day",
        "hour_of_day",
        "day_weight",
        "simultaneous_flow_group_dual_$"
    ])

    for timepoint in instance.TIMEPOINTS:

        # discount_and_day_weight un-discounts the periods and un-weighs the days to obtain intuitive dual values.
        discount_and_day_weight = \
            instance.discount_factor[instance.period[timepoint]] * instance.day_weight[instance.day[timepoint]]

        for sim_flow_group in instance.SIMULTANEOUS_FLOW_GROUPS:
            sim_flow_writer.writerow([
                sim_flow_group,
                instance.period[timepoint],
                timepoint,
                instance.day[timepoint],
                instance.hour_of_day[timepoint],
                instance.day_weight[instance.day[timepoint]],
                format_2f(instance.dual[instance.Simultaneous_Flows_Limit_Constraint[sim_flow_group, timepoint]]
                          / discount_and_day_weight)
            ])


def export_curtailment(instance, results_directory):
    """
    This file contains hourly and sub-hourly variable renewable curtailment decisions for each zone in RESOLVE.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...curtailment...')

    curtailment_writer = fileio.csvwriter(os.path.join(results_directory, "curtailment.csv"))
    curtailment_writer.writerow([
        "zone",
        "period",
        "timepoint_id",
        "day",
        "hour_of_day",
        "day_weight",
        "scheduled_curtailment_mw",
        "subhourly_downward_lf_mwh",
        "subhourly_upward_lf_mwh",
        "curtailment_cost_$"
    ])

    for timepoint in instance.TIMEPOINTS:

        for zone in instance.ZONES:
            scheduled_curtailment = float()
            curtailment_cost = float()

            for resource in instance.CURTAILABLE_VARIABLE_RESOURCES:
                if instance.zone[resource] == zone:
                    scheduled_curtailment += instance.Scheduled_Curtailment_MW[resource, timepoint].value
                    curtailment_cost += instance.Scheduled_Curtailment_MW[resource, timepoint].value* \
                                            instance.curtailment_cost_per_mwh[resource, instance.period[timepoint]]

            # as a placeholder for specifying resource-specific subhourly curtailment
            # ascribe all subhourly curtailment to the first load following zone
            if zone in instance.LOAD_FOLLOWING_ZONES:
                subhourly_curtailment = instance.Subhourly_Downward_LF_Energy_MWh[zone, timepoint]()
                subhourly_upward_lf_mwh = instance.Subhourly_Upward_LF_Energy_MWh[zone, timepoint]()
            else:
                subhourly_curtailment = None
                subhourly_upward_lf_mwh = None

            curtailment_writer.writerow([
                zone,
                instance.period[timepoint],
                timepoint,
                instance.day[timepoint],
                instance.hour_of_day[timepoint],
                instance.day_weight[instance.day[timepoint]],
                format_2f(scheduled_curtailment),
                format_2f(subhourly_curtailment),
                format_2f(subhourly_upward_lf_mwh),
                format_2f(curtailment_cost)
            ])


def export_reliability_constraints(instance, results_directory):
    """
    elcc_surface_facets.csv: This file contains results for each facet of the effective load carrying capability (ELCC)
    surface in each period. These results can show which (if any) of the ELCC facets is active in each period.

    reliability_constraints.csv: This file contains the input reliability targets
    in each period, as well as the shadow price of meeting those targets.
    A summary of reliability contributions by resource type is also included.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...reliability constraints and ELCC surface...')

    # write headers for three output files
    reliability_writer = fileio.csvwriter(os.path.join(results_directory, "reliability_constraints.csv"))
    reliability_writer.writerow([
        "reliability_constraint",
        "period",
        "reliability_peak_load_MW",
        "reliability_margin_fraction",
        "capacity_target_MW",
        "system_capacity_MW",
        "resource_elcc_surface_capacity_MW",
        "resource_non_elcc_surface_capacity_MW",
        "transmission_reliability_capacity_MW",
        "reliability_capacity_adjustment_MW",
        "reliability_constraint_dual_$"
    ])

    if instance.allow_elcc_surfaces:
        surface_writer = fileio.csvwriter(os.path.join(results_directory, "reliability_elcc_surface_facets.csv"))
        surface_writer.writerow([
            "reliability_constraint",
            "period",
            "elcc_surface_name",
            "facet_index",
            "facet_elcc_mw",
            "active_facet"
        ])


    for reliability in instance.RELIABILITY_CONSTRAINTS:
        for period in instance.PERIODS:
            resource_reliability_capacity = 0.0

            # update period and reliability-specific non-ELCC surface reliability capacity contributions when coming across a new reliability
            resource_non_elcc_surface_capacity_mw = \
                instance.Resource_Non_ELCC_Surface_Reliability_Contribution_MW[reliability, period]()
            resource_reliability_capacity += resource_non_elcc_surface_capacity_mw

            tx_reliability_contribution = None
            if instance.allow_tx_reliability_contribution:
                tx_reliability_contribution = instance.Transmission_Reliability_Contribution_MW[
                    reliability, period]()
                resource_reliability_capacity+=tx_reliability_contribution

            # variable to sum up all ELCC capacity for the reliability
            elcc_in_reliability_period_mw = None

            if instance.allow_elcc_surfaces:
                elcc_in_reliability_period_mw = 0.0
                for surface_reliability, surface_name in instance.ELCC_SURFACE_NAMES:
                    if surface_reliability == reliability:
                        # elcc value of the active facet, which is also the min elcc mw across all facets of the surface
                        active_facet_elcc_mw = min(instance.Elcc_Surface_Facet_LHS[period, reliability, surface_name, :]())
                        # add up elcc values of all surfaces that belong to the reliability
                        elcc_in_reliability_period_mw += active_facet_elcc_mw

                        # Output facet-specific values for each period for each ELCC surface
                        for facet_reliability, facet_surface, facet in instance.ELCC_SURFACE_FACETS:
                            if facet_reliability == reliability and facet_surface == surface_name:
                                facet_elcc_mw = instance.Elcc_Surface_Facet_LHS[
                                    period, facet_reliability, facet_surface, facet]()
                                # potential for floating point comparison error - round if necesssary
                                if facet_elcc_mw == active_facet_elcc_mw:
                                    active_facet = 1
                                else:
                                    active_facet = 0

                                surface_writer.writerow([
                                    reliability,
                                    period,
                                    surface_name,
                                    facet,
                                    format_2f(facet_elcc_mw),
                                    active_facet
                                ])

                resource_reliability_capacity+=elcc_in_reliability_period_mw

            # print out reliability constraint output file
            reliability_writer.writerow([
                reliability,
                period,
                format_2f(instance.reliability_peak_load_mw[reliability, period]),
                instance.reliability_margin[reliability, period],
                format_2f(instance.reliability_peak_load_mw[reliability, period] * (
                            1 + instance.reliability_margin[reliability, period])),
                format_2f(resource_reliability_capacity
                          + instance.reliability_capacity_adjustment_mw[reliability, period]),
                format_2f(elcc_in_reliability_period_mw),
                format_2f(resource_non_elcc_surface_capacity_mw),
                format_2f(tx_reliability_contribution),
                format_2f(instance.reliability_capacity_adjustment_mw[reliability, period]),
                format_2f(abs(instance.dual[instance.Reliability_Capacity_Constraint[reliability, period]]
                          / instance.discount_factor[period]))
            ])


def export_reliability_resources(instance, results_directory):
    """
    This file contains the resource-specific reliability contribution for each reliability constraint and period.
    If a resource is on an elcc surface, file would not print its reliability_capacity_mw value,
    but will specify the elcc surface that the resource is on.
    - reliability_capacity_mw
    - elcc_surface_name
    """

    print('...reliability resources contribution...')

    resource_writer = fileio.csvwriter(os.path.join(results_directory, "reliability_resource_contributions.csv"))
    resource_writer.writerow([
        "reliability_constraint",
        "period",
        "resource",
        "operational_capacity_mw",
        "reliability_capacity_delivered_mw",
        "reliability_capacity_mw",
        "elcc_surface_name"
    ])

    for reliability in instance.RELIABILITY_CONSTRAINTS:
        for period in instance.PERIODS:
            for resource in instance.RELIABILITY_RESOURCES[reliability]:
                # for elcc resources
                reliability_mw = None
                if instance.allow_elcc_surfaces and resource in instance.ELCC_RESOURCES[reliability]:
                    for surface_reliability, surface, surface_resource in instance.ELCC_SURFACE_MAP:
                        if surface_reliability==reliability and surface_resource==resource:
                            elcc_surface_assign = surface
                            break
                # for non_elcc_surface resources
                else:
                    elcc_surface_assign = None
                    reliability_mw = instance.Reliability_Capacity_NQC_MW[reliability, resource, period]()

                # print out both results
                resource_writer.writerow([
                    reliability,
                    period,
                    resource,
                    format_2f(instance.Operational_Capacity_MW[resource, period]()),
                    format_2f(instance.Reliability_Capacity_Delivered_MW[resource, period]()),
                    format_2f(reliability_mw),
                    elcc_surface_assign
                ])

def export_rps(instance, results_directory):
    """
    This file contains the input RPS target level in each period, as well as the shadow price of the RPS constraint,
    the RPS credits banked, and a high-level summary of the components of the RPS constraint.
    Also includes information on blend fuel use and costs.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...RPS...')

    rps_writer = fileio.csvwriter(os.path.join(results_directory, "rps.csv"))
    rps_writer.writerow([
        "rps_policy",
        "period",
        "rps_eligible_gen_mwh",
        "scheduled_curtailment_mwh",
        "Subhourly_Downward_LF_Energy_MWh",
        "rps_storage_losses_mwh",
        "rps_target_mwh",
        "rps_banked_mwh",
        "rps_constraint_dual_$",
        "rps_nonmodeled_mwh",
        "rps_net_bank_spent_mwh",
        "rps_previously_banked_mwh",
        "blend_fuel_generation_mwh",
        "rps_unbundled_mwh",
        "rps_unbundled_fraction_limit_dual_$_per_mwh",
        "rps_fraction_of_retail_sales",
        "retail_sales_before_ee_mwh",
        "retail_sales_after_ee_mwh"
    ])
    for policy in instance.RPS_POLICIES:
        for period in instance.PERIODS:

            if instance.optimize_rps_banking:
                rps_banked_mwh = instance.Bank_RPS_MWh[policy, period].value
                rps_net_bank_spent_mwh = (instance.Previously_Banked_RPS_MWh[policy,period]()
                                          - instance.Bank_RPS_MWh[policy, period].value)
            else:
                rps_banked_mwh = None
                rps_net_bank_spent_mwh = None

            if instance.enforce_unbundled_fraction_limit[policy, period]:
                unbundled_dual = instance.dual[instance.RPS_Unbundled_Fraction_Limit_Constraint[policy, period]] \
                                 / instance.discount_factor[period]
            else:
                unbundled_dual = None

            # the RPS target could include one or many zones - concat the names of the zones here
            rps_writer.writerow([
                policy,
                period,
                format_2f(instance.RPS_Eligible_Generation_MWh_Per_Year[policy, period]()),
                format_2f(instance.Scheduled_RPS_Curtailment_Per_Year[policy, period]()),
                format_2f(instance.Subhourly_RPS_Curtailment_Per_Year[policy, period]()),
                format_2f(instance.RPS_Storage_Losses_MWh_Per_Year[policy, period]()),
                format_2f(instance.RPS_Target_MWh[policy, period]()),
                format_2f(rps_banked_mwh),
                format_2f(instance.dual[instance.Achieve_RPS_Constraint[policy, period]] / instance.discount_factor[period]),
                format_2f(instance.rps_nonmodeled_mwh[policy,period]),
                format_2f(rps_net_bank_spent_mwh),
                format_2f(instance.Previously_Banked_RPS_MWh[policy, period]()),
                format_2f(instance.RPS_Blend_Fuel_Generation_MWh_Per_Year[policy, period]()),
                format_2f(instance.Unbundled_RPS_Generation_MWh_Per_Year[policy,period]()),
                format_2f(unbundled_dual),
                format_2f(instance.rps_fraction_of_retail_sales[policy, period]),
                format_2f(instance.retail_sales_mwh[policy, period]),
                format_2f(instance.Retail_Sales_After_EE_MWh[policy, period]())
            ])


def export_emissions_groups(instance, results_directory):
    """
    This file contains the input emissions target in each period, and the shadow price of meeting that target.
    Blanks cells indicate that an emissions target was not modeled.
    :param instance:
    :param results_directory:
    :return:
    """

    emissions_groups_writer = fileio.csvwriter(os.path.join(results_directory, "emissions_groups.csv"))
    emissions_groups_writer.writerow([
        "group",
        "emissions_units",
        "period",
        "emissions_target_units_per_year",
        "emissions_credit_units_per_year",
        "emissions_constraint_dual_$",
        "unspecified_imports_units_per_year",
        "emissions_within_area_units_per_yr"
    ])

    for group in instance.EMISSIONS_GROUPS:
        for period in instance.PERIODS:
            if instance.include_emissions_targets & ((group, period) in instance.EMISSIONS_GROUP_CONSTRAINT_PERIODS):
                emissions_target = instance.emissions_target_units_per_year[group, period]
                emissions_credit = instance.emissions_credit_units_per_year[group, period]
                emissions_constraint_dual = abs(
                    instance.dual[instance.Emissions_Target_Constraint[group, period]] /
                    instance.discount_factor[period]
                )
            else:
                emissions_target = None
                emissions_credit = None
                emissions_constraint_dual = None

            emissions_group_type = instance.emissions_group_type[group]
            emissions_group_units = instance.emission_type_units[emissions_group_type]

            emissions_groups_writer.writerow([
                group,
                emissions_group_units,
                period,
                format_2f(emissions_target),
                format_2f(emissions_credit),
                format_2f(emissions_constraint_dual),
                format_2f(instance.Unspecified_Imports_Units_Per_Year[group, period]()),
                format_2f(instance.Resource_Emissions_Units_Per_Year[group, period]())
            ])


def export_deliverability(instance, results_directory):
    """
    This file contains the cost of new deliverability in each period.
    Also included is the breakdown of delivered and undelivered capacity in each deliverability zone in each period.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...deliverability costs by period')

    deliverability_writer = fileio.csvwriter(os.path.join(results_directory, "deliverability.csv"))
    deliverability_writer.writerow([
        "period",
        "deliverability_zone",
        "deliverability_cost_$",
        "period_discount_factor",
        "new_deliverability_cumulative_mw",
        "new_deliverability_incremental_mw",
        "Delivered_Capacity_MW",
        "Undelivered_Capacity_MW"
    ])

    for period in instance.PERIODS:
        for deliverability_zone in instance.DELIVERABILITY_ZONES:

            delivered_capacity = float()
            undelivered_capacity = float()
            for resource in instance.DELIVERABILITY_RESOURCES:
                if deliverability_zone in instance.RESOURCE_DELIVERABILITY_ZONES[resource]:
                    delivered_capacity += \
                        instance.Delivered_Capacity_MW[resource, period].value
                    undelivered_capacity += instance.Undelivered_Capacity_MW[resource, period].value

            deliverability_writer.writerow([
                period,
                deliverability_zone,
                format_2f(instance.Deliverability_Cost_Annual_In_Period_Dollars[deliverability_zone, period]()),
                instance.discount_factor[period],
                format_2f(instance.New_Deliverability_Cumulative_MW[deliverability_zone, period]()),
                format_2f(instance.New_Deliverability_Incremental_MW[deliverability_zone, period]()),
                format_2f(delivered_capacity),
                format_2f(undelivered_capacity)
            ])

def export_transmission_build(instance, results_directory):
    """Export period transmission build for new transmission lines."""

    print("...transmission build...")

    tx_build_writer = fileio.csvwriter(os.path.join(results_directory, "transmission_build.csv"))
    tx_build_writer.writerow(["line",
                              "period",
                              "period_build_mw",
                              "total_build_mw",
                              "build_cost_per_yr"])

    for line in instance.TRANSMISSION_LINES_NEW:
        for period in instance.PERIODS:
            transmission_build_costs = float()
            for (pp, v) in instance.PERIOD_VINTAGES:
                if pp == period:
                    transmission_build_costs += instance.New_Tx_Period_Vintage_Build_Cost[line, pp, v]()

            tx_build_writer.writerow([
                line,
                period,
                format_2f(instance.New_Tx_Period_Installed_Capacity_MW[line, period].value),
                format_2f(instance.New_Tx_Total_Installed_Capacity_MW[line, period]()),
                format_2f(transmission_build_costs)
            ])


def export_fuel_burn(instance, results_directory):
    """
    This file contains the hourly fuel burn by resource.
    :param instance:
    :param results_directory:
    :return:
    """

    print('...fuel burn by resource...')

    fuel_burn_writer = fileio.csvwriter(os.path.join(results_directory, "fuel_burn_by_resource.csv"))
    fuel_burn_writer.writerow([
        "resource",
        "zone",
        "technology",
        "fuel",
        "blend_fuel",
        "timepoint",
        "period",
        "day",
        "hour_of_day",
        "day_weight",
        "base_fuel_consumption_mmbtu",
        "blend_fuel_consumption_mmbtu",
        "start_fuel_mmbtu"
    ])
    for r in instance.THERMAL_RESOURCES:
        fuel = instance.fuel[r]
        for tmp in instance.TIMEPOINTS:

            if r in instance.BLEND_FUEL_RESOURCES:
                blend_fuel_consumption = instance.Blend_Fuel_Consumption_MMBtu[r, tmp].value
                blend_fuel = instance.blend_fuel[fuel]
            else:
                blend_fuel_consumption = None
                blend_fuel = None

            if r in instance.UNIT_COMMITMENT_RESOURCES:
                start_fuel = format_2f(instance.Start_Fuel_MMBtu[r, tmp]())
            else:
                start_fuel = None

            fuel_burn_writer.writerow([
                r,
                instance.zone[r],
                instance.technology[r],
                fuel,
                blend_fuel,
                tmp,
                instance.period[tmp],
                instance.day[tmp],
                instance.hour_of_day[tmp],
                instance.day_weight[instance.day[tmp]],
                format_2f(instance.Base_Fuel_Consumption_MMBtu[r, tmp]()),
                format_2f(blend_fuel_consumption),
                start_fuel
            ])


def export_emissions(instance, results_directory):
    """
    This file contains the hourly emissions by resource.
    :param instance:
    :param results_directory:
    :return:
    """

    print('...emissions by resource...')

    emissions_writer = fileio.csvwriter(os.path.join(results_directory, "emissions_by_resource.csv"))
    emissions_writer.writerow([
        "resource",
        "zone",
        "technology",
        "timepoint",
        "period",
        "day",
        "hour_of_day",
        "day_weight",
        "emission_type",
        "emissions",
        "emissions_units"
    ])
    for emission_type in instance.EMISSION_TYPES:
        for r in instance.RESOURCES_WITH_PROVIDE_POWER:
            for tmp in instance.TIMEPOINTS:
                emissions_writer.writerow([
                    r,
                    instance.zone[r],
                    instance.technology[r],
                    tmp,
                    instance.period[tmp],
                    instance.day[tmp],
                    instance.hour_of_day[tmp],
                    instance.day_weight[instance.day[tmp]],
                    emission_type,
                    format_2f(instance.Resource_Emissions_Units_Per_Timepoint[r, emission_type, tmp]()),
                    instance.emission_type_units[emission_type]
                ])


def export_emissions_imports(instance, results_directory):
    """
    This file contains the hourly emissions imported into the emissions target area along transmission lines.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...emissions imports...')
    emissions_imports_writer = fileio.csvwriter(os.path.join(results_directory, "emissions_imports.csv"))
    emissions_imports_writer.writerow([
        "group",
        "line",
        "timepoint",
        "period",
        "day",
        "hour_of_day",
        "day_weight",
        "positive_direction_emissions_imports",
        "negative_direction_emissions_imports",
        "emissions_import_units"
    ])

    # Emissions imports are recorded even if a cap on emissions isn't enforced
    for group in instance.EMISSIONS_GROUPS:

        emissions_group_type = instance.emissions_group_type[group]
        emissions_group_units = instance.emission_type_units[emissions_group_type]

        for line in instance.TRANSMISSION_LINES:
            for tmp in instance.TIMEPOINTS:
                emissions_imports_writer.writerow([
                    group,
                    line,
                    tmp,
                    instance.period[tmp],
                    instance.day[tmp],
                    instance.hour_of_day[tmp],
                    instance.day_weight[instance.day[tmp]],
                    format_2f(instance.Unspecified_Imports_Units_Per_Timepoint_Positive_Direction[line, group, tmp]()),
                    format_2f(instance.Unspecified_Imports_Units_Per_Timepoint_Negative_Direction[line, group, tmp]()),
                    emissions_group_units
                ])


def export_reserve_timepoints(instance, results_directory):
    """
    This file contains the hourly shadow prices of meeting reserve and reliability constraints.
    Exports the undiscounted duals for easier comparison, but the original/objective function dual can be obtained
    by multiplying the undiscounted dual by period_discount_factor.
    Also contains the unmet reserve violations in (MW).
    :param instance:
    :param results_directory:
    :return:
    """
    print('...reserve timepoint duals and violations...')
    reserve_timepoint_duals_violations_writer = \
        fileio.csvwriter(os.path.join(results_directory, "reserve_timepoints.csv"))
    reserve_timepoint_duals_violations_writer.writerow([
        "timepoint",
        "period",
        "day",
        "hour_of_day",
        "day_weight",
        "period_discount_factor",
        "min_gen_req",
        "spin_req",
        "upward_lf_req",
        "downward_lf_req",
        "upward_reg_req",
        "downward_reg_req",
        "min_local_gen_dual_$",
        "total_fr_dual_$",
        "partial_fr_dual_$",
        "spin_dual_$",
        "upward_lf_dual_$",
        "upward_reg_dual_$",
        "downward_reg_dual_$",
        "max_downward_lf_dual_$",
        "var_renw_down_lf_limit_dual_$",
        "var_renw_down_lf_availability_dual_$",
        "max_variable_upward_lf_dual_$",
        "available_variable_upward_lf_dual_$",
        "upward_reg_violation_mw",
        "downward_reg_violation_mw",
        "upward_lf_reserve_violation_mw",
        "downward_lf_reserve_violation_mw",
        "spin_violation_mw"
    ])

    meet_spin = instance.Meet_Spin_Requirement_Constraint
    meet_upward_lf = instance.Meet_Upward_LF_Requirement_Constraint
    meet_upward_reg = instance.Meet_Upward_Reg_Requirement_Constraint
    meet_downward_reg = instance.Meet_Downward_Reg_Requirement_Constraint
    max_downward_lf_provision = instance.Meet_Downward_LF_Requirement_Constraint
    var_renw_down_lf_limit = instance.Var_Renw_Down_LF_Reserve_Limit_Constraint
    var_renw_down_lf_availability = instance.Var_Renw_Down_LF_Reserve_Availability_Constraint
    available_variable_upward_lf = instance.Variable_Resource_Available_Upward_LF_Constraint
    max_variable_upward_lf = instance.Max_Upward_LF_From_Variable_Resources_Constraint

    for timepoint in instance.TIMEPOINTS:

        # discount_and_day_weight un-discounts the periods and un-weighs the days to obtain intuitive dual values.
        discount_and_day_weight = \
            instance.discount_factor[instance.period[timepoint]] * instance.day_weight[instance.day[timepoint]]

        if instance.min_gen_committed_mw[timepoint] != 0:
            min_local_gen_dual = instance.dual[instance.Min_Local_Gen_Constraint[timepoint]] \
                / discount_and_day_weight
        else:
            min_local_gen_dual = None

        if instance.freq_resp_total_req_mw[timepoint] != 0:
            total_fr_dual = instance.dual[instance.Total_Frequency_Response_Headroom_Constraint[timepoint]] \
                / discount_and_day_weight
        else:
            total_fr_dual = None

        if instance.freq_resp_partial_req_mw[timepoint] != 0:
            partial_fr_dual = \
                instance.dual[instance.Partial_Frequency_Response_Headroom_Constraint[timepoint]] \
                / discount_and_day_weight
        else:
            partial_fr_dual = None

        reserve_timepoint_duals_violations_writer.writerow([
            timepoint,
            instance.period[timepoint],
            instance.day[timepoint],
            instance.hour_of_day[timepoint],
            instance.day_weight[instance.day[timepoint]],
            instance.discount_factor[instance.period[timepoint]],
            format_2f(instance.min_gen_committed_mw[timepoint]),
            format_2f(instance.Spinning_Reserve_Req_MW[timepoint]()),
            format_2f(instance.Upward_Load_Following_Reserve_Req[timepoint]()),
            format_2f(instance.Downward_Load_Following_Reserve_Req[timepoint]()),
            format_2f(instance.upward_reg_req[timepoint]),
            format_2f(instance.downward_reg_req[timepoint]),
            format_2f(min_local_gen_dual),
            format_2f(total_fr_dual),
            format_2f(partial_fr_dual),
            format_2f(instance.dual[meet_spin[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[meet_upward_lf[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[meet_upward_reg[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[meet_downward_reg[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[max_downward_lf_provision[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[var_renw_down_lf_limit[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[var_renw_down_lf_availability[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[max_variable_upward_lf[timepoint]] / discount_and_day_weight),
            format_2f(instance.dual[available_variable_upward_lf[timepoint]] / discount_and_day_weight),
            format_2f(instance.Upward_Reg_Violation_MW[timepoint].value),
            format_2f(instance.Downward_Reg_Violation_MW[timepoint].value),
            format_2f(instance.Upward_LF_Reserve_Violation_MW[timepoint].value),
            format_2f(instance.Downward_LF_Reserve_Violation_MW[timepoint].value),
            format_2f(instance.Spin_Violation_MW[timepoint].value)
        ])


def export_ramping_duals_by_timepoint(instance, results_directory):
    """
    This file contains the hourly values relating to ramp constrains of unit commitment thermal generation.
    Note: this currently doesn't export reserve ramp constraints if the resource
    isn't in UNIT_COMMITMENT_RAMP_LIMITED_RESOURCES, which is possible if
    ramp_rate_fraction is >= 1 but is less than the unit's operable range on the reserve timeframe.
    :param instance:
    :param results_directory:
    :return:
    """
    print('...ramping variables by timepoint...')

    ramp_writer = fileio.csvwriter(os.path.join(results_directory, "ramping_duals.csv"))
    ramp_writer.writerow([
        "resource",
        "technology",
        "zone",
        "timepoint",
        "period",
        "day",
        "hour_of_day",
        "day_weight",
        "period_discount_factor",
        "commit_units",
        "start_units",
        "shut_down_units",
        "fully_operational_units",
        "power_MW",
        "max_rampup_MW",
        "max_rampdown_MW",
        "rampup_dual_$",
        "rampdown_dual_$",
        "max_reserve_ramp_up_MW",
        "max_reserve_ramp_down_MW",
        "reserve_ramp_up_dual_$",
        "reserve_ramp_down_dual_$",
        "min_gen_MW",
        "min_gen_dual_$",
        "max_gen_MW",
        "max_gen_dual_$"
    ])

    for r in instance.UNIT_COMMITMENT_RAMP_LIMITED_RESOURCES:
        for tmp in instance.TIMEPOINTS:
            discount_and_day_weight = \
                instance.discount_factor[instance.period[tmp]] * instance.day_weight[instance.day[tmp]]

            # ramp constraints between timesteps ######################
            if instance.ramp_rate_fraction[r] \
                    >= (1 - instance.min_stable_level_fraction[r]):
                rampup_dual = None
                rampdown_dual = None
                max_rampup_mw = None
                max_rampdown_mw = None
            else:
                rampup_dual = instance.dual[instance.Unit_Commitment_Resource_Ramp_Up_Constraint[r, tmp]] \
                    / discount_and_day_weight
                rampdown_dual = instance.dual[instance.Unit_Commitment_Resource_Ramp_Down_Constraint[r, tmp]] \
                    / discount_and_day_weight
                max_rampup_mw = (instance.Commit_Units[r, tmp].value - instance.Start_Units[r, tmp].value) \
                    * instance.unit_size_mw[r] \
                    * instance.ramp_rate_fraction[r] \
                    + instance.Start_Units[r, tmp].value \
                    * instance.unit_size_mw[r] \
                    * instance.min_stable_level_fraction[r] \
                    + instance.Start_Units[r, tmp].value * instance.ramp_relax
                max_rampdown_mw = (instance.Commit_Units[r, tmp].value - instance.Start_Units[r, tmp].value) \
                    * instance.unit_size_mw[r] \
                    * instance.ramp_rate_fraction[r] \
                    + instance.Shut_Down_Units[r, tmp].value \
                    * instance.unit_size_mw[r] \
                    * instance.min_stable_level_fraction[r] \
                    + instance.Shut_Down_Units[r, tmp].value * instance.ramp_relax

            # reserve ramp constraints ######################
            if (r not in instance.REGULATION_RESERVE_RESOURCES
                    and r not in instance.LOAD_FOLLOWING_RESERVE_RESOURCES
                    and r not in instance.SPINNING_RESERVE_RESOURCES):
                max_reserve_ramp_up_mw = None
                max_reserve_ramp_down_mw = None
                reserve_ramp_up_dual = None
                reserve_ramp_down_dual = None
            elif instance.reserve_timeframe_fraction_of_hour * instance.ramp_rate_fraction[r] >= \
                    (1 - instance.min_stable_level_fraction[r]):
                max_reserve_ramp_up_mw = None
                max_reserve_ramp_down_mw = None
                reserve_ramp_up_dual = None
                reserve_ramp_down_dual = None
            else:
                if r in instance.UNIT_COMMITMENT_RAMP_LIMITED_RESOURCES:
                    reserve_capable_units = instance.Fully_Operational_Units[r, tmp]()
                else:
                    reserve_capable_units = instance.Commit_Units[r, tmp].value

                max_reserve_ramp_up_mw = reserve_capable_units \
                    * instance.unit_size_mw[r] \
                    * instance.ramp_rate_fraction[r] \
                    * instance.reserve_timeframe_fraction_of_hour
                max_reserve_ramp_down_mw = reserve_capable_units \
                    * instance.unit_size_mw[r] \
                    * instance.ramp_rate_fraction[r] \
                    * instance.reserve_timeframe_fraction_of_hour
                reserve_ramp_up_dual = instance.dual[instance.Unit_Commitment_Upward_Reserve_Ramp_Constraint[r, tmp]] \
                    / discount_and_day_weight
                reserve_ramp_down_dual = instance.dual[instance.Unit_Commitment_Downward_Reserve_Ramp_Constraint[r, tmp]] \
                    / discount_and_day_weight

            # min gen ######################
            min_gen = instance.Commit_Capacity_MW[r, tmp]() \
                * instance.min_stable_level_fraction[r] \
                - (instance.Commit_Units[r, tmp].value - instance.Fully_Operational_Units[r, tmp]()) \
                * instance.ramp_relax

            min_gen_dual = instance.dual[instance.Thermal_Min_Gen_Down_Reserve_Constraint[r, tmp]] \
                / discount_and_day_weight

            # max_gen ######################
            max_gen = (instance.Commit_Units[r, tmp].value - instance.Fully_Operational_Units[r, tmp]()) \
                * instance.unit_size_mw[r] \
                * instance.min_stable_level_fraction[r] \
                + instance.Fully_Operational_Units[r, tmp]() \
                * instance.unit_size_mw[r] \

            max_gen_dual = instance.dual[instance.Unit_Commitment_Max_Gen_Up_Reserve_Constraint[r, tmp]] \
                / discount_and_day_weight

            ramp_writer.writerow([
                r,
                instance.technology[r],
                instance.zone[r],
                tmp,
                instance.period[tmp],
                instance.day[tmp],
                instance.hour_of_day[tmp],
                instance.day_weight[instance.day[tmp]],
                instance.discount_factor[instance.period[tmp]],
                format_6f(instance.Commit_Units[r, tmp].value),
                format_6f(instance.Start_Units[r, tmp].value),
                format_6f(instance.Shut_Down_Units[r, tmp].value),
                format_6f(instance.Fully_Operational_Units[r, tmp]()),
                format_2f(instance.Provide_Power_MW[r, tmp].value),
                format_2f(max_rampup_mw),
                format_2f(max_rampdown_mw),
                format_2f(rampup_dual),
                format_2f(rampdown_dual),
                format_2f(max_reserve_ramp_up_mw),
                format_2f(max_reserve_ramp_down_mw),
                format_2f(reserve_ramp_up_dual),
                format_2f(reserve_ramp_down_dual),
                format_2f(min_gen),
                format_2f(min_gen_dual),
                format_2f(max_gen),
                format_2f(max_gen_dual)
            ])


def export_daily_budget_changes(instance, results_directory):
    """
    Export daily budget changes when multi-day sharing logic is on

    :param instance:
    :param results_directory:
    :return:
    """

    daily_budget_changes_writer = fileio.csvwriter(os.path.join(results_directory, "energy_budget_changes.csv"))
    daily_budget_changes_writer.writerow([
        "zone",
        "resource",
        "period",
        "day_id",
        "daily_energy_budget_change_mwh",
        "energy_sharing_interval",
        "day_weight"
    ])

    for resource in instance.RESOURCES_WITH_MULTIDAY_SHARING:
        for period in instance.PERIODS:
            for day in instance.DAYS:
                daily_budget_changes_writer.writerow([
                    instance.zone[resource],
                    resource,
                    period,
                    day,
                    instance.Daily_Energy_Budget_Change_MWh[resource, period, day].value,
                    instance.interday_energy_sharing_interval_id[day],
                    instance.day_weight[day]
                ])


def export_line_limits_resource_tx_use(instance, results_directory):
    """
    Export line limits when dedicated imports (storage and otherwise) affect line limits

    :param instance:
    :param results_directory:
    :return:
    """

    line_changes_writer = fileio.csvwriter(os.path.join(results_directory, "resource_tx_use_changes.csv"))
    line_changes_writer.writerow([
        "transmission_line",
        "timepoint",
        "min_flow",
        "transmit_flow_value",
        "transmit_unspecified_flow",
        "max_flow"
    ])

    for r_tx_id in instance.RESOURCE_TX_IDS:
        for tmp in instance.TIMEPOINTS:
            line_changes_writer.writerow([
                instance.tx_line_used[r_tx_id],
                tmp,
                instance.min_flow_planned_mw[instance.tx_line_used[r_tx_id]],
                instance.Transmit_Power_MW[instance.tx_line_used[r_tx_id],tmp](),
                instance.Transmit_Power_Unspecified_MW[instance.tx_line_used[r_tx_id],tmp].value,
                instance.max_flow_planned_mw[instance.tx_line_used[r_tx_id]]
            ])


def handle_exception(message, debug):
    """
    How to handle exceptions. First print a custom message and the traceback.
    If in debug mode, open the Python debugger (PDB), else exit. To execute multi-line statements in PDB, type this
    to launch an interactive Python session with all the local variables available.
    This function is used by export_results() to handle exceptions.
    :param message:
    :param debug:
    :return:
    """
    print(message)
    print(traceback.format_exc())
    if debug:
        print("""
           Launching Python debugger (PDB)...
           To execute multi-line statements in PDB, type this:
                import code; code.interact(local=vars())
           (The command launches an interactive Python session with all the local variables available.)
           To exit the interactive session and continue script execution, press Ctrl+D.
           """
        )
        tp, value, tb = sys.exc_info()
        pdb.post_mortem(tb)
    else:
        print('Exiting.')
        sys.exit()
