From d57f49e3191b3bd8c91be5242860abc2d900af63 Mon Sep 17 00:00:00 2001 From: Belle Date: Thu, 9 Apr 2026 11:26:14 -0600 Subject: [PATCH 1/6] initialize test file --- gtep/tests/unit/test_model_library/__init__.py | 0 gtep/tests/unit/test_model_library/test_objective.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 gtep/tests/unit/test_model_library/__init__.py create mode 100644 gtep/tests/unit/test_model_library/test_objective.py diff --git a/gtep/tests/unit/test_model_library/__init__.py b/gtep/tests/unit/test_model_library/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py new file mode 100644 index 00000000..e69de29b From e3017b2dd162d523c7fcb16ba19b6ab69736c6ea Mon Sep 17 00:00:00 2001 From: Belle Date: Thu, 9 Apr 2026 12:50:19 -0600 Subject: [PATCH 2/6] set up helper functions --- .../unit/test_model_library/test_objective.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py index e69de29b..5d4e1128 100644 --- a/gtep/tests/unit/test_model_library/test_objective.py +++ b/gtep/tests/unit/test_model_library/test_objective.py @@ -0,0 +1,55 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2026 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Unit tests for the function to create the objective function component +""" + + +from os.path import abspath, join, dirname +import pytest +import pyomo.environ as pyo +import gtep.model_library.components as comps +from gtep.gtep_model import ExpansionPlanningModel, create_stages +from gtep.gtep_data import ExpansionPlanningData +from gtep.model_library.objective import create_objective_function + + +# Helper Functions +def read_debug_model(): + curr_dir = dirname(abspath(__file__)) + debug_data_path = abspath(join(curr_dir, "..", "..", "data", "5bus")) + dataObject = ExpansionPlanningData() + dataObject.load_prescient(debug_data_path) + return dataObject + + +def create_test_model(): + # create the model to use in the tests + dataObject = read_debug_model() + modObject = ExpansionPlanningModel(data=dataObject) + m = pyo.ConcreteModel("GTEP Model") + comps.add_model_sets( + m, modObject.stages, rep_per=[i for i in range(1, modObject.num_reps + 1)] + ) + + comps.add_model_parameters( + m, modObject.num_commit, modObject.num_dispatch, modObject.duration_dispatch + ) + + create_stages(m, modObject.stages) + + return m + + +def test_create_objective_function(): + pass From d9b00b9f707e32f380f88ab79ade73d71429c3c2 Mon Sep 17 00:00:00 2001 From: Belle Date: Thu, 9 Apr 2026 14:52:37 -0600 Subject: [PATCH 3/6] add unit test instances --- .../unit/test_model_library/test_objective.py | 287 ++++++++++++++++-- 1 file changed, 263 insertions(+), 24 deletions(-) diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py index 5d4e1128..136dc8cc 100644 --- a/gtep/tests/unit/test_model_library/test_objective.py +++ b/gtep/tests/unit/test_model_library/test_objective.py @@ -15,41 +15,280 @@ """ -from os.path import abspath, join, dirname import pytest import pyomo.environ as pyo -import gtep.model_library.components as comps -from gtep.gtep_model import ExpansionPlanningModel, create_stages -from gtep.gtep_data import ExpansionPlanningData from gtep.model_library.objective import create_objective_function -# Helper Functions -def read_debug_model(): - curr_dir = dirname(abspath(__file__)) - debug_data_path = abspath(join(curr_dir, "..", "..", "data", "5bus")) - dataObject = ExpansionPlanningData() - dataObject.load_prescient(debug_data_path) - return dataObject +# Mock investmentStage +class StageData: + def __init__( + self, + op_cost=0, + storage_cost=0, + invest_cost=0, + quota_deficit=0, + renewable_curtailment=0, + expansion_cost=0, + ): + self.operatingCostInvestment = op_cost + self.storageCostInvestment = storage_cost + self.investment_cost = invest_cost + self.quotaDeficit = quota_deficit + self.renewableCurtailmentInvestment = renewable_curtailment + self.expansionCost = expansion_cost -def create_test_model(): - # create the model to use in the tests - dataObject = read_debug_model() - modObject = ExpansionPlanningModel(data=dataObject) - m = pyo.ConcreteModel("GTEP Model") - comps.add_model_sets( - m, modObject.stages, rep_per=[i for i in range(1, modObject.num_reps + 1)] +# helper function +def create_test_model( + stages=[1, 2], + op_cost=[0, 0], + storage_cost=[0, 0], + invest_cost=None, + quota_deficit=[0, 0], + renewable_curtailment=[0, 0], + expansion_cost=None, + defecit_penalty={1: 0, 2: 0}, + investment_factor={1: 0, 2: 0}, + storage=True, +): + m = pyo.ConcreteModel() + m.stages = stages + + investment_stage = {} + if invest_cost is not None: + for i, stage in enumerate(stages): + investment_stage[stage] = StageData( + op_cost=op_cost[i], + storage_cost=storage_cost[i], + invest_cost=invest_cost[i], + quota_deficit=quota_deficit[i], + renewable_curtailment=renewable_curtailment[i], + ) + elif expansion_cost is not None: + for i, stage in enumerate(stages): + investment_stage[stage] = StageData( + op_cost=op_cost[i], + storage_cost=storage_cost[i], + quota_deficit=quota_deficit[i], + renewable_curtailment=renewable_curtailment[i], + expansion_cost=expansion_cost[i], + ) + + m.investmentStage = investment_stage + + m.deficitPenalty = defecit_penalty + m.investmentFactor = investment_factor + + # Config with storage True + m.config = {"storage": storage} + + return m + + +# ----------------------------------UNIT TESTS-----------------------------------------# +def test_objective_multiple_stages_storage_true(): + m = create_test_model( + op_cost=[100, 150], + storage_cost=[50, 75], + invest_cost=[200, 300], + quota_deficit=[10, 20], + renewable_curtailment=[5, 10], + defecit_penalty={1: 2, 2: 3}, + investment_factor={1: 1.5, 2: 2.0}, ) - comps.add_model_parameters( - m, modObject.num_commit, modObject.num_dispatch, modObject.duration_dispatch + create_objective_function(m) + + # Check that the objective was added + assert hasattr(m, "total_cost_objective_rule") + + # expected costs + operating_cost = 100 + 150 + storage_cost = 50 + 75 + expansion_cost = 200 + 300 + penalty_cost = sum( + m.deficitPenalty[stage] + * m.investmentFactor[stage] + * m.investmentStage[stage].quotaDeficit + + m.investmentStage[stage].renewableCurtailmentInvestment + for stage in m.stages ) - create_stages(m, modObject.stages) + assert m.operatingCost == operating_cost + assert m.storageCost == storage_cost + assert m.expansionCost == expansion_cost + assert m.penaltyCost == penalty_cost - return m + expected_total = operating_cost + storage_cost + expansion_cost + penalty_cost + + # Evaluate the objective expression + expr = pyo.value(m.total_cost_objective_rule.expr) + assert expr == pytest.approx(expected_total) + + +def test_objective_multiple_stages_storage_false(): + m = create_test_model( + op_cost=[100, 150], + storage_cost=[50, 75], + invest_cost=[200, 300], + quota_deficit=[10, 20], + renewable_curtailment=[5, 10], + defecit_penalty={1: 2, 2: 3}, + investment_factor={1: 1.5, 2: 2.0}, + storage=False, + ) + + create_objective_function(m) + + # Check that the objective was added + assert hasattr(m, "total_cost_objective_rule") + assert m.storageCost == 0 + + # expected costs + operating_cost = 100 + 150 + expansion_cost = 200 + 300 + penalty_cost = sum( + m.deficitPenalty[stage] + * m.investmentFactor[stage] + * m.investmentStage[stage].quotaDeficit + + m.investmentStage[stage].renewableCurtailmentInvestment + for stage in m.stages + ) + + assert m.operatingCost == operating_cost + assert m.expansionCost == expansion_cost + assert m.penaltyCost == penalty_cost + + expected_total = operating_cost + expansion_cost + penalty_cost + + # Evaluate the objective expression + expr = pyo.value(m.total_cost_objective_rule.expr) + assert expr == pytest.approx(expected_total) + + +def test_objective_single_stage(): + m = create_test_model( + stages=[1], + op_cost=[100], + expansion_cost=[50], + invest_cost=[200], + quota_deficit=[10], + renewable_curtailment=[5], + defecit_penalty={1: 2}, + investment_factor={1: 1.5}, + storage=False, + ) + + create_objective_function(m) + + assert hasattr(m, "total_cost_objective_rule") + + expected_total = ( + m.investmentStage[1].operatingCostInvestment + + m.investmentStage[1].storageCostInvestment + + m.investmentStage[1].expansionCost + + m.deficitPenalty[1] + * m.investmentFactor[1] + * m.investmentStage[1].quotaDeficit + + m.investmentStage[1].renewableCurtailmentInvestment + + m.investmentStage[1].storageCostInvestment + ) + + expr = pyo.value(m.total_cost_objective_rule.expr) + assert expr == pytest.approx(expected_total) + + +def test_storage_cost_zero_when_storage_false(): + m = create_test_model( + op_cost=[100, 150], + storage_cost=[50, 75], + invest_cost=[200, 300], + quota_deficit=[10, 20], + renewable_curtailment=[5, 10], + defecit_penalty={1: 2, 2: 3}, + investment_factor={1: 1.5, 2: 2.0}, + storage=False, + ) + + create_objective_function(m) + + # storageCost should be zero + assert m.storageCost == 0 + + # Check objective expression does not include storageCost + expr = pyo.value(m.total_cost_objective_rule.expr) + operating_cost = sum( + m.investmentStage[stage].operatingCostInvestment for stage in m.stages + ) + expansion_cost = sum(m.investmentStage[stage].investment_cost for stage in m.stages) + penalty_cost = sum( + m.deficitPenalty[stage] + * m.investmentFactor[stage] + * m.investmentStage[stage].quotaDeficit + + m.investmentStage[stage].renewableCurtailmentInvestment + for stage in m.stages + ) + expected_total = operating_cost + expansion_cost + penalty_cost + + assert expr == pytest.approx(expected_total) + + +def test_multiple_stages_zero_costs(): + m = create_test_model(invest_cost=[0, 0]) + create_objective_function(m) + + assert hasattr(m, "total_cost_objective_rule") + + expr_val = pyo.value(m.total_cost_objective_rule.expr) + assert expr_val == pytest.approx(0) + + +def test_single_stage_zero_costs(): + m = create_test_model(stages=[1], expansion_cost=[0]) + create_objective_function(m) + + assert hasattr(m, "total_cost_objective_rule") + + expr_val = pyo.value(m.total_cost_objective_rule.expr) + assert expr_val == pytest.approx(0) + + +def test_many_stages_stress(): + num_stages = 20 + stages = list(range(1, num_stages + 1)) + m = create_test_model( + stages=stages, + op_cost=[item * 1.0 for item in stages], + storage_cost=[item * 0.5 for item in stages], + invest_cost=[item * 2.0 for item in stages], + quota_deficit=[item * 0.1 for item in stages], + renewable_curtailment=[item * 0.05 for item in stages], + defecit_penalty={item: 1.0 for item in stages}, + investment_factor={item: 1.0 for item in stages}, + ) + + create_objective_function(m) + + assert hasattr(m, "total_cost_objective_rule") + + # Calculate expected total cost manually + operating_cost = sum( + m.investmentStage[stage].operatingCostInvestment for stage in m.stages + ) + storage_cost = sum( + m.investmentStage[stage].storageCostInvestment for stage in m.stages + ) + expansion_cost = sum(m.investmentStage[stage].investment_cost for stage in m.stages) + penalty_cost = sum( + m.deficitPenalty[stage] + * m.investmentFactor[stage] + * m.investmentStage[stage].quotaDeficit + + m.investmentStage[stage].renewableCurtailmentInvestment + for stage in m.stages + ) + expected_total = operating_cost + storage_cost + expansion_cost + penalty_cost -def test_create_objective_function(): - pass + expr_val = pyo.value(m.total_cost_objective_rule.expr) + assert expr_val == pytest.approx(expected_total) From 27c25018ef4c184463247a6748338bcba01897ba Mon Sep 17 00:00:00 2001 From: Belle Date: Thu, 9 Apr 2026 14:55:39 -0600 Subject: [PATCH 4/6] correct spelling error --- .../unit/test_model_library/test_objective.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py index 136dc8cc..8a2729fc 100644 --- a/gtep/tests/unit/test_model_library/test_objective.py +++ b/gtep/tests/unit/test_model_library/test_objective.py @@ -48,7 +48,7 @@ def create_test_model( quota_deficit=[0, 0], renewable_curtailment=[0, 0], expansion_cost=None, - defecit_penalty={1: 0, 2: 0}, + deficit_penalty={1: 0, 2: 0}, investment_factor={1: 0, 2: 0}, storage=True, ): @@ -77,7 +77,7 @@ def create_test_model( m.investmentStage = investment_stage - m.deficitPenalty = defecit_penalty + m.deficitPenalty = deficit_penalty m.investmentFactor = investment_factor # Config with storage True @@ -94,7 +94,7 @@ def test_objective_multiple_stages_storage_true(): invest_cost=[200, 300], quota_deficit=[10, 20], renewable_curtailment=[5, 10], - defecit_penalty={1: 2, 2: 3}, + deficit_penalty={1: 2, 2: 3}, investment_factor={1: 1.5, 2: 2.0}, ) @@ -134,7 +134,7 @@ def test_objective_multiple_stages_storage_false(): invest_cost=[200, 300], quota_deficit=[10, 20], renewable_curtailment=[5, 10], - defecit_penalty={1: 2, 2: 3}, + deficit_penalty={1: 2, 2: 3}, investment_factor={1: 1.5, 2: 2.0}, storage=False, ) @@ -175,7 +175,7 @@ def test_objective_single_stage(): invest_cost=[200], quota_deficit=[10], renewable_curtailment=[5], - defecit_penalty={1: 2}, + deficit_penalty={1: 2}, investment_factor={1: 1.5}, storage=False, ) @@ -206,7 +206,7 @@ def test_storage_cost_zero_when_storage_false(): invest_cost=[200, 300], quota_deficit=[10, 20], renewable_curtailment=[5, 10], - defecit_penalty={1: 2, 2: 3}, + deficit_penalty={1: 2, 2: 3}, investment_factor={1: 1.5, 2: 2.0}, storage=False, ) @@ -264,7 +264,7 @@ def test_many_stages_stress(): invest_cost=[item * 2.0 for item in stages], quota_deficit=[item * 0.1 for item in stages], renewable_curtailment=[item * 0.05 for item in stages], - defecit_penalty={item: 1.0 for item in stages}, + deficit_penalty={item: 1.0 for item in stages}, investment_factor={item: 1.0 for item in stages}, ) From c155b52310beb023dd07e59fd694b4063bca8c75 Mon Sep 17 00:00:00 2001 From: bstorm Date: Mon, 20 Apr 2026 09:49:38 -0600 Subject: [PATCH 5/6] update defaults --- .../unit/test_model_library/test_objective.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py index 8a2729fc..0379b85c 100644 --- a/gtep/tests/unit/test_model_library/test_objective.py +++ b/gtep/tests/unit/test_model_library/test_objective.py @@ -14,7 +14,6 @@ Unit tests for the function to create the objective function component """ - import pytest import pyomo.environ as pyo from gtep.model_library.objective import create_objective_function @@ -41,15 +40,15 @@ def __init__( # helper function def create_test_model( - stages=[1, 2], - op_cost=[0, 0], - storage_cost=[0, 0], + stages=None, + op_cost=None, + storage_cost=None, invest_cost=None, - quota_deficit=[0, 0], - renewable_curtailment=[0, 0], + quota_deficit=None, + renewable_curtailment=None, expansion_cost=None, - deficit_penalty={1: 0, 2: 0}, - investment_factor={1: 0, 2: 0}, + deficit_penalty=None, + investment_factor=None, storage=True, ): m = pyo.ConcreteModel() @@ -87,8 +86,11 @@ def create_test_model( # ----------------------------------UNIT TESTS-----------------------------------------# + + def test_objective_multiple_stages_storage_true(): m = create_test_model( + stages=[1, 2], op_cost=[100, 150], storage_cost=[50, 75], invest_cost=[200, 300], @@ -129,6 +131,7 @@ def test_objective_multiple_stages_storage_true(): def test_objective_multiple_stages_storage_false(): m = create_test_model( + stages=[1, 2], op_cost=[100, 150], storage_cost=[50, 75], invest_cost=[200, 300], @@ -178,6 +181,7 @@ def test_objective_single_stage(): deficit_penalty={1: 2}, investment_factor={1: 1.5}, storage=False, + storage_cost=[0], ) create_objective_function(m) @@ -201,6 +205,7 @@ def test_objective_single_stage(): def test_storage_cost_zero_when_storage_false(): m = create_test_model( + stages=[1, 2], op_cost=[100, 150], storage_cost=[50, 75], invest_cost=[200, 300], From cc9865a3b7838753fb6d84de72fddab87d2e759f Mon Sep 17 00:00:00 2001 From: bstorm Date: Mon, 20 Apr 2026 10:04:01 -0600 Subject: [PATCH 6/6] fix tests that were using defaults --- .../unit/test_model_library/test_objective.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/gtep/tests/unit/test_model_library/test_objective.py b/gtep/tests/unit/test_model_library/test_objective.py index 0379b85c..03dab4c4 100644 --- a/gtep/tests/unit/test_model_library/test_objective.py +++ b/gtep/tests/unit/test_model_library/test_objective.py @@ -240,7 +240,16 @@ def test_storage_cost_zero_when_storage_false(): def test_multiple_stages_zero_costs(): - m = create_test_model(invest_cost=[0, 0]) + m = create_test_model( + stages=[1, 2], + op_cost=[0, 0], + storage_cost=[0, 0], + invest_cost=[0, 0], + quota_deficit=[0, 0], + renewable_curtailment=[0, 0], + deficit_penalty={1: 0, 2: 0}, + investment_factor={1: 0, 2: 0}, + ) create_objective_function(m) assert hasattr(m, "total_cost_objective_rule") @@ -250,7 +259,16 @@ def test_multiple_stages_zero_costs(): def test_single_stage_zero_costs(): - m = create_test_model(stages=[1], expansion_cost=[0]) + m = create_test_model( + stages=[1], + op_cost=[0], + storage_cost=[0], + quota_deficit=[0], + renewable_curtailment=[0], + expansion_cost=[0], + deficit_penalty={1: 0}, + investment_factor={1: 0}, + ) create_objective_function(m) assert hasattr(m, "total_cost_objective_rule")