# Copyright 2016-2023 Blue Marble Analytics LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The **gridpath.project.capacity.capacity** module is a project-level
module that adds to the formulation components that describe the amount of
power that a project is providing in each study timepoint.
"""
import os.path
import pandas as pd
from pyomo.environ import Expression, value, Constraint
from db.common_functions import spin_on_database_lock
from gridpath.auxiliary.auxiliary import get_required_subtype_modules
from gridpath.common_functions import create_results_df
from gridpath.project.operations.common_functions import load_operational_type_modules
import gridpath.project.operations.operational_types as op_type_init
from gridpath.project import PROJECT_TIMEPOINT_DF
[docs]
def add_model_components(
m,
d,
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
):
"""
The following Pyomo model components are defined in this module:
+-------------------------------------------------------------------------+
| Expressions |
+=========================================================================+
| | :code:`Power_Provision_MW` |
| | *Defined over*: :code:`PRJ_OPR_TMPS` |
| |
| Defines the power a project is producing in each of its operational |
| timepoints. The exact formulation of the expression depends |
| on the project's *operational_type*. For each project, we call its |
| *capacity_type* module's *power_provision_rule* method in order to |
| formulate the expression. E.g. a project of the *gen_must_run* |
| operational_type will be producing power equal to its capacity while a |
| dispatchable project will have a variable in its power provision |
| expression. This expression will then be used by other modules. |
+-------------------------------------------------------------------------+
"""
# Dynamic Inputs
###########################################################################
required_operational_modules = get_required_subtype_modules(
scenario_directory=scenario_directory,
weather_iteration=weather_iteration,
hydro_iteration=hydro_iteration,
availability_iteration=availability_iteration,
subproblem=subproblem,
stage=stage,
which_type="operational_type",
)
imported_operational_modules = load_operational_type_modules(
required_operational_modules
)
# Expressions
###########################################################################
def power_provision_rule(mod, prj, tmp):
"""
**Expression Name**: Power_Provision_MW
**Defined Over**: PRJ_OPR_TMPS
Power provision is a variable for some generators, but not others; get
the appropriate expression for each generator based on its operational
type.
"""
gen_op_type = mod.operational_type[prj]
if hasattr(imported_operational_modules[gen_op_type], "power_provision_rule"):
return imported_operational_modules[gen_op_type].power_provision_rule(
mod, prj, tmp
)
else:
return op_type_init.power_provision_rule(mod, prj, tmp)
m.Power_Provision_MW = Expression(m.PRJ_OPR_TMPS, rule=power_provision_rule)
# def energy_limit_in_period_constraint_rule(mod, prj, tmp):
# """
# **Constraint**: Energy_Limit_in_Period_Constraint
# **Defined Over**: PRJ_OPR_PRDS
#
# Power provision is a variable for some generators, but not others; get
# the appropriate expression for each generator based on its operational
# type.
# """
# gen_op_type = mod.operational_type[prj]
# if hasattr(
# imported_operational_modules[gen_op_type],
# "energy_limit_in_period_constraint_rule",
# ):
# return imported_operational_modules[
# gen_op_type
# ].energy_limit_in_period_constraint_rule(mod, prj, tmp)
# else:
# return op_type_init.energy_limit_in_period_constraint_rule(mod, prj, tmp)
#
# m.Energy_Limit_in_Period_Constraint = Constraint(
# m.PRJ_OPR_PRDS, rule=energy_limit_in_period_constraint_rule
# )
# Input-Output
###############################################################################
def export_results(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
m,
d,
):
"""
Export operations results.
:param scenario_directory:
:param subproblem:
:param stage:
:param m:
The Pyomo abstract model
:param d:
Dynamic components
:return:
Nothing
"""
results_columns = [
"power_mw",
]
data = [
[
prj,
tmp,
value(m.Power_Provision_MW[prj, tmp]),
]
for (prj, tmp) in m.PRJ_OPR_TMPS
]
results_df = create_results_df(
index_columns=["project", "timepoint"],
results_columns=results_columns,
data=data,
)
for c in results_columns:
getattr(d, PROJECT_TIMEPOINT_DF)[c] = None
getattr(d, PROJECT_TIMEPOINT_DF).update(results_df)
required_operational_modules = get_required_subtype_modules(
scenario_directory=scenario_directory,
weather_iteration=weather_iteration,
hydro_iteration=hydro_iteration,
availability_iteration=availability_iteration,
subproblem=subproblem,
stage=stage,
which_type="operational_type",
)
imported_operational_modules = load_operational_type_modules(
required_operational_modules
)
for optype_module in imported_operational_modules:
if hasattr(
imported_operational_modules[optype_module], "add_to_prj_tmp_results"
):
results_columns, optype_df = imported_operational_modules[
optype_module
].add_to_prj_tmp_results(mod=m)
for column in results_columns:
if column not in getattr(d, PROJECT_TIMEPOINT_DF):
getattr(d, PROJECT_TIMEPOINT_DF)[column] = None
getattr(d, PROJECT_TIMEPOINT_DF).update(optype_df)
def summarize_results(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
):
"""
:param scenario_directory:
:param subproblem:
:param stage:
:return:
Summarize operational results
"""
summary_results_file = os.path.join(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
"results",
"summary_results.txt",
)
# Open in 'append' mode, so that results already written by other
# modules are not overridden
with open(summary_results_file, "a") as outfile:
outfile.write("\n### OPERATIONAL RESULTS ###\n")
# Next, our goal is to get a summary table of power production by load
# zone, technology, and period
# Note: this includes power from spinup_or_lookahead timepoints as well!
# Get the results CSV as dataframe
operational_results_df = pd.read_csv(
os.path.join(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
"results",
"project_timepoint.csv",
)
)[["project", "load_zone", "period", "technology", "power_mw", "timepoint_weight"]]
operational_results_df["weighted_power_mwh"] = (
operational_results_df["power_mw"] * operational_results_df["timepoint_weight"]
)
# Aggregate total power results by load_zone, technology, and period
operational_results_agg_df = pd.DataFrame(
operational_results_df.groupby(
["load_zone", "period", "technology"],
as_index=True,
).sum(numeric_only=True)["weighted_power_mwh"]
)
operational_results_agg_df.columns = ["weighted_power_mwh"]
# Aggregate total power by load_zone and period -- we'll need this
# to find the percentage of total power by technology (for each load
# zone and period)
lz_period_power_df = pd.DataFrame(
operational_results_df.groupby(
by=["load_zone", "period"],
as_index=True,
).sum(
numeric_only=True
)["weighted_power_mwh"]
)
# Name the power column
operational_results_agg_df.columns = ["weighted_power_mwh"]
# Add a column with the percentage of total power by load zone and tech
operational_results_agg_df["percent_total_power"] = pd.Series(
index=operational_results_agg_df.index, dtype="float64"
)
# Calculate the percent of total power for each tech (by load zone
# and period)
for indx, row in operational_results_agg_df.iterrows():
if lz_period_power_df.weighted_power_mwh[indx[0], indx[1]] == 0:
pct = 0
else:
pct = (
operational_results_agg_df.weighted_power_mwh[indx]
/ lz_period_power_df.weighted_power_mwh[indx[0], indx[1]]
* 100.0
)
operational_results_agg_df.loc[indx, "percent_total_power"] = pct
# Get the energy units from the units.csv file
units_df = pd.read_csv(
os.path.join(scenario_directory, "units.csv"), index_col="metric"
)
energy_unit = units_df.loc["energy", "unit"]
# units_dict = dict(zip(units_df["metric"], units_df["unit"]))
# Rename the columns for the final table
operational_results_agg_df.columns = [
"Annual Energy ({})".format(energy_unit),
"% Total Power",
]
with open(summary_results_file, "a") as outfile:
outfile.write("\n--> Energy Production <--\n")
operational_results_agg_df.to_string(outfile, float_format="{:,.2f}".format)
outfile.write("\n")
# Database
###############################################################################
def process_results(db, c, scenario_id, subscenarios, quiet):
"""
Aggregate dispatch by technology
Aggregate dispatch by technology and period
:param db:
:param c:
:param subscenarios:
:param quiet:
:return:
"""
if not quiet:
print("aggregate dispatch by technology")
# Delete old dispatch by technology
del_sql = """
DELETE FROM results_project_dispatch_by_technology
WHERE scenario_id = ?
"""
spin_on_database_lock(
conn=db, cursor=c, sql=del_sql, data=(scenario_id,), many=False
)
# Aggregate dispatch by technology
agg_sql = """
INSERT INTO results_project_dispatch_by_technology (
scenario_id,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
timepoint,
timepoint_weight,
number_of_hours_in_timepoint,
spinup_or_lookahead,
load_zone,
technology,
power_mw
)
SELECT
scenario_id,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
timepoint,
timepoint_weight,
number_of_hours_in_timepoint,
spinup_or_lookahead,
load_zone,
technology,
sum(power_mw) AS power_mw
FROM results_project_timepoint
WHERE scenario_id = ?
GROUP BY
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
timepoint,
load_zone,
technology
ORDER BY
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
timepoint,
load_zone,
technology;"""
spin_on_database_lock(
conn=db, cursor=c, sql=agg_sql, data=(scenario_id,), many=False
)
if not quiet:
print("aggregate dispatch by technology-period")
# Delete old dispatch by technology
del_sql = """
DELETE FROM results_project_dispatch_by_technology_period
WHERE scenario_id = ?
"""
spin_on_database_lock(
conn=db, cursor=c, sql=del_sql, data=(scenario_id,), many=False
)
# Aggregate dispatch by technology, period, and spinup_or_lookahead
agg_sql = """
INSERT INTO results_project_dispatch_by_technology_period (
scenario_id,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
load_zone,
technology, spinup_or_lookahead, energy_mwh)
SELECT
scenario_id,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
load_zone,
technology,
spinup_or_lookahead,
SUM(power_mw * timepoint_weight * number_of_hours_in_timepoint ) AS energy_mwh
FROM results_project_dispatch_by_technology
WHERE scenario_id = ?
GROUP BY
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
load_zone,
technology,
spinup_or_lookahead
ORDER BY
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem_id,
stage_id,
period,
load_zone,
technology,
spinup_or_lookahead
;
"""
spin_on_database_lock(
conn=db, cursor=c, sql=agg_sql, data=(scenario_id,), many=False
)