diff --git a/postreise/analyze/generation/binding.py b/postreise/analyze/generation/binding.py new file mode 100644 index 00000000..46675470 --- /dev/null +++ b/postreise/analyze/generation/binding.py @@ -0,0 +1,82 @@ +from powersimdata.scenario.scenario import Scenario +from powersimdata.scenario.analyze import Analyze + + +def _check_scenario(scenario): + """Private function used only for type-checking for public functions. + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :raises TypeError: if scenario is not a Scenario object. + :raises ValueError: if scenario is not in Analyze state. + """ + if not isinstance(scenario, Scenario): + raise TypeError('scenario must be a Scenario object') + if not isinstance(scenario.state, Analyze): + raise ValueError('scenario.state must be Analyze') + + +def _check_epsilon(epsilon): + """Private function used only for type-checking for public functions. + :param float/int epsilon: precision for binding constraints. + :raises TypeError: if epsilon is not a float or an int. + :raises ValueError: if epsilon is negative. + """ + if not isinstance(epsilon, (float, int)): + raise TypeError('epsilon must be numeric') + if epsilon < 0: + raise ValueError('epsilon must be non-negative') + + +def pmin_constraints(scenario, epsilon=1e-3): + """Identify time periods in which generators are at minimum power. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param float epsilon: allowable 'fuzz' for whether constraint is binding. + :return: (*pandas.DataFrame*) -- Boolean dataframe of same shape as PG. + """ + _check_scenario(scenario) + _check_epsilon(epsilon) + + pg = scenario.state.get_pg() + grid = scenario.state.get_grid() + pmin = grid.plant['Pmin'] + binding_pmin_constraints = ((pg - pmin) <= epsilon) + + return binding_pmin_constraints + + +def pmax_constraints(scenario, epsilon=1e-3): + """Identify time periods in which generators are at maximum power. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param float epsilon: allowable 'fuzz' for whether constraint is binding. + :return: (*pandas.DataFrame*) -- Boolean dataframe of same shape as PG. + """ + _check_scenario(scenario) + _check_epsilon(epsilon) + + pg = scenario.state.get_pg() + grid = scenario.state.get_grid() + pmax = grid.plant['Pmax'] + binding_pmax_constraints = ((pmax - pg) <= epsilon) + + return binding_pmax_constraints + + +def ramp_constraints(scenario, epsilon=1e-3): + """Identify time periods in which generators have binding ramp constraints. + .. note:: The first time period will always return *False* for each column. + + :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. + :param float epsilon: allowable 'fuzz' for whether constraint is binding. + :return: (*pandas.DataFrame*) -- Boolean dataframe of same shape as PG. + """ + _check_scenario(scenario) + _check_epsilon(epsilon) + + pg = scenario.state.get_pg() + grid = scenario.state.get_grid() + ramp = grid.plant['ramp_30'] + diff = pg.diff(axis=0) + binding_ramp_constraints = ((ramp * 2 - abs(diff)) <= epsilon) + + return binding_ramp_constraints diff --git a/postreise/analyze/generation/tests/test_binding.py b/postreise/analyze/generation/tests/test_binding.py new file mode 100644 index 00000000..787e99a8 --- /dev/null +++ b/postreise/analyze/generation/tests/test_binding.py @@ -0,0 +1,214 @@ +import unittest + +import pandas as pd + +from powersimdata.tests.mock_scenario import MockScenario +from postreise.analyze.generation.binding import \ + pmin_constraints, pmax_constraints, ramp_constraints, \ + _check_scenario, _check_epsilon + + +class TestCheckScenario(unittest.TestCase): + + def test_good_scenario(self): + mock_plant = { + 'plant_id': ['A', 'B', 'C', 'D'], + 'ramp_30': [2.5, 5, 10, 25], + } + mock_scenario = MockScenario({'plant': mock_plant}) + _check_scenario(mock_scenario) + + def test_bad_scenario_type(self): + with self.assertRaises(TypeError): + _check_scenario('307') + + def test_bad_scenario_state(self): + mock_plant = { + 'plant_id': ['A', 'B', 'C', 'D'], + 'ramp_30': [2.5, 5, 10, 25], + } + mock_scenario = MockScenario({'plant': mock_plant}) + mock_scenario.state = 'Create' + with self.assertRaises(ValueError): + _check_scenario(mock_scenario) + + +class TestCheckEpsilon(unittest.TestCase): + + def test_good_float_value(self): + _check_epsilon(5e-4) + + def test_good_int_value(self): + _check_epsilon(1) + + def test_zero(self): + _check_epsilon(0) + + def test_bad_type(self): + with self.assertRaises(TypeError): + _check_epsilon('0.001') + + def test_bad_value(self): + with self.assertRaises(ValueError): + _check_epsilon(-0.001) + + +class TestRampConstraints(unittest.TestCase): + + def setUp(self): + mock_plant = { + 'plant_id': ['A', 'B', 'C', 'D'], + 'ramp_30': [2.5, 5, 10, 25], + } + grid_attrs = {'plant': mock_plant} + mock_pg = pd.DataFrame({ + 'UTC': ['t1', 't2', 't3', 't4'], + 'A': [100, 104, (99 + 1e-4), (104 + 1e-4 - 1e-7)], + 'B': [50, 45, 50, 45], + 'C': [20, 40, 60, 80], + 'D': [200, 150, 100, 50], + }) + mock_pg.set_index('UTC', inplace=True) + self.mock_scenario = MockScenario(grid_attrs, pg=mock_pg) + self.default_expected = pd.DataFrame({ + 'UTC': ['t1', 't2', 't3', 't4'], + 'A': [False, False, True, True], + 'B': [False, False, False, False], + 'C': [False, True, True, True], + 'D': [False, True, True, True], + }) + self.default_expected.set_index('UTC', inplace=True) + + def get_default_expected(self): + return self.default_expected.copy() + + def test_ramp_constraints_default(self): + binding_ramps = ramp_constraints(self.mock_scenario) + expected = self.get_default_expected() + assert binding_ramps.equals(self.get_default_expected()) + + def test_ramp_constraints_spec_epsilon1(self): + # Same results as test_ramp_constraints_default + binding_ramps = ramp_constraints(self.mock_scenario, epsilon=1e-3) + expected = self.get_default_expected() + assert binding_ramps.equals(expected) + + def test_ramp_constraints_spec_epsilon2(self): + # One differece from test_ramp_constraints_default: ('A', 't3') + binding_ramps = ramp_constraints(self.mock_scenario, epsilon=1e-6) + expected = self.get_default_expected() + expected.loc['t3', 'A'] = False + assert binding_ramps.equals(expected) + + def test_ramp_constraints_spec_epsilon3(self): + # Two differeces from test_ramp_constraints_default: ('A', ['t3'/'t4']) + binding_ramps = ramp_constraints(self.mock_scenario, epsilon=1e-9) + expected = self.get_default_expected() + expected.loc[:, 'A'] = False + assert binding_ramps.equals(expected) + + +class TestPminConstraints(unittest.TestCase): + + def setUp(self): + mock_plant = { + 'plant_id': ['A', 'B', 'C', 'D'], + 'Pmin': [0, 10, 20, 30], + } + grid_attrs = {'plant': mock_plant} + mock_pg = pd.DataFrame({ + 'UTC': ['t1', 't2'], + 'A': [0, 0], + 'B': [(10 + 1e-4), 15], + 'C': [25, (20 + 1e-7)], + 'D': [35, 40], + }) + mock_pg.set_index('UTC', inplace=True) + self.mock_scenario = MockScenario(grid_attrs, pg=mock_pg) + self.default_expected = pd.DataFrame({ + 'UTC': ['t1', 't2'], + 'A': [True, True], + 'B': [True, False], + 'C': [False, True], + 'D': [False, False], + }) + self.default_expected.set_index('UTC', inplace=True) + + def get_default_expected(self): + return self.default_expected.copy() + + def test_pmin_constraints_default(self): + binding_pmins = pmin_constraints(self.mock_scenario) + expected = self.get_default_expected() + assert binding_pmins.equals(expected) + + def test_pmin_constraints_default_spec_epsilon1(self): + binding_pmins = pmin_constraints(self.mock_scenario, epsilon=1e-3) + expected = self.get_default_expected() + assert binding_pmins.equals(expected) + + def test_pmin_constraints_default_spec_epsilon2(self): + binding_pmins = pmin_constraints(self.mock_scenario, epsilon=1e-6) + expected = self.get_default_expected() + expected.loc['t1', 'B'] = False + assert binding_pmins.equals(expected) + + def test_pmin_constraints_default_spec_epsilon3(self): + binding_pmins = pmin_constraints(self.mock_scenario, epsilon=1e-9) + expected = self.get_default_expected() + expected.loc['t1', 'B'] = False + expected.loc['t2', 'C'] = False + assert binding_pmins.equals(expected) + + +class TestPmaxConstraints(unittest.TestCase): + + def setUp(self): + mock_plant = { + 'plant_id': ['A', 'B', 'C', 'D'], + 'Pmax': [50, 75, 100, 200], + } + grid_attrs = {'plant': mock_plant} + mock_pg = pd.DataFrame({ + 'UTC': ['t1', 't2'], + 'A': [50, 50], + 'B': [(75 - 1e-4), 70], + 'C': [90, (100 - 1e-7)], + 'D': [150, 175], + }) + mock_pg.set_index('UTC', inplace=True) + self.mock_scenario = MockScenario(grid_attrs, pg=mock_pg) + self.default_expected = pd.DataFrame({ + 'UTC': ['t1', 't2'], + 'A': [True, True], + 'B': [True, False], + 'C': [False, True], + 'D': [False, False], + }) + self.default_expected.set_index('UTC', inplace=True) + + def get_default_expected(self): + return self.default_expected.copy() + + def test_pmax_constraints_default(self): + binding_pmaxs = pmax_constraints(self.mock_scenario) + expected = self.get_default_expected() + assert binding_pmaxs.equals(expected) + + def test_pmax_constraints_default_sepc_epsilon1(self): + binding_pmaxs = pmax_constraints(self.mock_scenario, epsilon=1e-3) + expected = self.get_default_expected() + assert binding_pmaxs.equals(expected) + + def test_pmax_constraints_default_sepc_epsilon2(self): + binding_pmaxs = pmax_constraints(self.mock_scenario, epsilon=1e-6) + expected = self.get_default_expected() + expected.loc['t1', 'B'] = False + assert binding_pmaxs.equals(expected) + + def test_pmax_constraints_default_sepc_epsilon3(self): + binding_pmaxs = pmax_constraints(self.mock_scenario, epsilon=1e-9) + expected = self.get_default_expected() + expected.loc['t1', 'B'] = False + expected.loc['t2', 'C'] = False + assert binding_pmaxs.equals(expected)