Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: analyze binding constraints (ramp, pmin, pmax) #121

Merged
merged 1 commit into from
Jun 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions postreise/analyze/generation/binding.py
Original file line number Diff line number Diff line change
@@ -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
214 changes: 214 additions & 0 deletions postreise/analyze/generation/tests/test_binding.py
Original file line number Diff line number Diff line change
@@ -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()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be 1 blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sepc --> spec in function name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

expected = self.get_default_expected()
expected.loc['t1', 'B'] = False
assert binding_pmaxs.equals(expected)

def test_pmax_constraints_default_sepc_epsilon3(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)