Skip to content

Commit

Permalink
Merge pull request #14 from jinningwang/esd
Browse files Browse the repository at this point in the history
ESD1
  • Loading branch information
jinningwang authored Oct 26, 2023
2 parents 5ed6903 + 9e78e20 commit 5ae81b6
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 118 deletions.
6 changes: 5 additions & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ version: 2
formats:
- pdf

build:
os: ubuntu-22.04
tools:
python: "3.11"

# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py

python:
version: 3.8
install:
- requirements: requirements.txt
- method: pip
Expand Down
Binary file added ams/cases/ieee39/ieee39_uced_esd1.xlsx
Binary file not shown.
99 changes: 57 additions & 42 deletions ams/core/documenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,44 +280,22 @@ def _constr_doc(self, max_width=78, export='plain'):
class_names.append(p.class_name)
info.append(p.info if p.info else '')

# NOTE: in the future, there might occur special math symbols
special_map = OrderedDict([
('SUMSYMBOL', r'\sum'),
])

# expressions based on output format
expressions = []
if export == 'rest':
expressions = []
logger.debug(f'tex_map: {self.parent.syms.tex_map}')
for p in self.constrs.values():
expr = p.e_str
for pattern, replacement in self.parent.syms.tex_map.items():
if r'\sum' in replacement:
replacement = replacement.replace(r'\sum', 'SUMSYMBOL')
if r'\p' in replacement:
continue
if r'sum' in expr:
expr = expr.replace('sum', 'SUMSYMBOL')
else:
try:
expr = re.sub(pattern, replacement, expr)
except re.error:
expr_pattern = pattern.removeprefix('\\b').removesuffix('\\b')
logger.error(f'Faild parse Element <{expr_pattern}> \
in {p.class_name}, check its tex_name.')
expr = ''
for pattern, replacement in special_map.items():
expr = expr.replace(pattern, replacement)
expr = _tex_pre(self, p, self.parent.syms.tex_map)
if p.type == 'eq':
expr = f'{expr} = 0'
elif p.type == 'uq':
expr = f'{expr} <= 0'
logger.debug(f'{p.name} expr after: {expr}')
logger.debug(f'{p.name} math: {expr}')
expressions.append(expr)
expressions = math_wrap(expressions, export=export)

title = 'Constraints\n----------------------------------'
else:
title = 'Constraints'
expressions = math_wrap(expressions, export=export)

plain_dict = OrderedDict([('Name', names),
('Description', info),
Expand Down Expand Up @@ -352,21 +330,7 @@ def _obj_doc(self, max_width=78, export='plain'):
units_rest.append(f'*{p.unit}*' if p.unit else '')

# expressions based on output format
expr = p.e_str
# NOTE: re.sub will run into error if `\` occurs at first position
# here we skip `\sum` in re replacement and do this using string replace
# however, this method might not be robust
for pattern, replacement in self.parent.syms.tex_map.items():
if 'sum' in replacement:
continue
else:
try:
expr = re.sub(pattern, replacement, expr)
except re.error:
expr_pattern = pattern.removeprefix('\\b').removesuffix('\\b')
logger.error(f'Faild parse Element {expr_pattern} in {p.class_name}, check its tex_name.')
return ''
expr = expr.replace('sum', r'\sum')
expr = _tex_pre(self, p, self.parent.syms.tex_map)
expr = p.sense + '. ' + expr # minimize or maximize
expr = [expr]
if export == 'rest':
Expand Down Expand Up @@ -518,3 +482,54 @@ def _param_doc(self, max_width=78, export='plain'):
export=export,
plain_dict=plain_dict,
rest_dict=rest_dict)

def _tex_pre(docm, p, tex_map):
"""
Prepare the expression for pretty printing.
Parameters
----------
docm : Documenter
The Documenter instance.
p : obj or const
The objective or constraint instance.
tex_map : dict
The tex map to use.
"""

# NOTE: in the future, there might occur special math symbols
special_map = OrderedDict([
('SUM', r'\sum'),
('ETA', r'\eta'),
('FRAC', r'\frac'),
])

expr = p.e_str

for pattern, replacement in tex_map.items():
if r'\sum' in replacement:
replacement = replacement.replace(r'\sum', 'SUM')
if r'sum' in expr:
expr = expr.replace('sum', 'SUM')
if '\eta' in replacement:
replacement = replacement.replace('\eta', 'ETA')
if r'\frac' in replacement:
replacement = replacement.replace(r'\frac', 'FRAC')
if r'\p' in replacement:
continue
try:
expr = re.sub(pattern, replacement, expr)
except re.error:
expr_pattern = pattern.removeprefix('\\b').removesuffix('\\b')
msg = f'Failed to parse <{expr_pattern}> in {docm.parent.class_name} <{p.name}>, check its tex_name.'
logger.error(msg)
expr = ''
try:
expr = expr.replace('*', ' ')
except re.error:
logger.error('Remains '*' in the expression.')

for pattern, replacement in special_map.items():
expr = expr.replace(pattern, replacement)

return expr
5 changes: 4 additions & 1 deletion ams/core/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,10 @@ def v0(self):
raise ValueError(f'{indexer.model} contains undefined {indexer.src}, likey a data error.')

out = [1 if item in ref else 0 for item in uidx]

out = np.array(out)
if self.u.horizon is not None:
out = out[:, np.newaxis]
out = np.repeat(out, self.u.horizon.n, axis=1)
return np.array(out)


Expand Down
15 changes: 10 additions & 5 deletions ams/core/symprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ def __init__(self, parent):
(r'\bpower\b', f'{lang}.power'),
(r'\bsign\b', f'{lang}.sign'),
])
# FIXME: the replacement for multiply is a bad design, but it works for now

self.tex_map = OrderedDict([
(r'\*\*(\d+)', '^{\\1}'),
(r'\b(\w+)\s*\*\s*(\w+)\b', r'\1*\2'),
(r'\@', r'*'),
(r'dot', r'*'),
(r'multiply', r''),
(r'\b(\w+)\s*\*\s*(\w+)\b', r'\1 \2'),
(r'\@', r' '),
(r'dot', r' '),
(r'multiply\(([^,]+), ([^)]+)\)', r'\1 \2'),
(r'\bnp.linalg.pinv(\d+)', r'\1^{\-1}'),
(r'\bpos\b', 'F^{+}'),
])

self.status = {
Expand Down Expand Up @@ -142,6 +143,10 @@ def generate_symbols(self, force_generate=False):
if key in self.config.tex_names:
self.tex_names[tmp] = sp.Symbol(self.config.tex_names[key])

# NOTE: hard-coded config 't' tex name as 'T_{cfg}' for clarity in doc
self.tex_map['\\bt\\b'] = 'T_{cfg}'
self.tex_map['\\bcp\\b'] = 'c_{p, cfg}'

# store tex names for pretty printing replacement later
for var in self.inputs_dict:
if var in self.parent.__dict__ and self.parent.__dict__[var].tex_name is not None:
Expand Down
4 changes: 4 additions & 0 deletions ams/opt/omodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class Var(Algeb, OptzBase):
"""
Base class for variables used in a routine.
When `horizon` is provided, the variable will be expanded to a matrix,
where rows are indexed by the source variable index and columns are
indexed by the horizon index.
Parameters
----------
info : str, optional
Expand Down
4 changes: 2 additions & 2 deletions ams/routines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
('cpf', ['CPF']),
('acopf', ['ACOPF']),
('dcopf', ['DCOPF']),
('ed', ['ED']),
('ed', ['ED', 'ED2']),
('rted', ['RTED', 'RTED2']),
('uc', ['UC']),
('uc', ['UC', 'UC2']),
('dopf', ['LDOPF', 'LDOPF2']),
])

Expand Down
48 changes: 32 additions & 16 deletions ams/routines/ed.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from ams.core.service import (ZonalSum, NumOpDual, NumHstack,
RampSub, NumOp, LoadScale) # NOQA

from ams.routines.rted import RTEDData # NOQA
from ams.routines.rted import RTEDData, ESD1Base # NOQA
from ams.routines.dcopf import DCOPFModel # NOQA

from ams.opt.omodel import Constraint # NOQA
from ams.core.service import VarSelect # NOQA
from ams.opt.omodel import Var, Constraint # NOQA

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -68,15 +69,15 @@ def __init__(self, system, config):
# --- vars ---
# NOTE: extend pg to 2D matrix, where row is gen and col is timeslot
self.pg.horizon = self.timeslot
self.pg.info = '2D power generation (system base, row for gen, col for horizon)'
self.pg.info = '2D power generation (system base)'

self.pn.horizon = self.timeslot
self.pn.info = '2D Bus power injection (system base, row for bus, col for horizon)'
self.pn.info = '2D Bus power injection (system base)'

# --- constraints ---
# --- power balance ---
self.ds = ZonalSum(u=self.zb, zone='Region',
name='ds', tex_name=r'\sum_{d}',
name='ds', tex_name=r'S_{d}',
info='Sum pl vector in shape of zone',)
self.pdz = NumOpDual(u=self.ds, u2=self.pl,
fun=np.multiply,
Expand All @@ -89,7 +90,7 @@ def __init__(self, system, config):
name='pds', tex_name=r'p_{d,s,t}',
unit='p.u.', info='Scaled total load as row vector')
self.gs = ZonalSum(u=self.zg, zone='Region',
name='gs', tex_name=r'\sum_{g}',
name='gs', tex_name=r'S_{g}',
info='Sum Gen vars vector in shape of zone')
# NOTE: Spg @ pg returns a row vector
self.pb.e_str = '- gs @ pg + pds' # power balance
Expand Down Expand Up @@ -166,24 +167,17 @@ def unpack(self, **kwargs):
class ED(EDData, EDModel):
"""
DC-based multi-period economic dispatch (ED).
ED extends DCOPF as follows:
1. Power generation ``pg`` is extended to 2D matrix using argument
``horizon`` to represent the power generation of each generator in
each time period (horizon).
2. The rows correspond to generators and the columns correspond to time
periods (horizons).
1. Var ``pg`` is extended to 2D
3. Ramping limits ``rgu`` and ``rgd`` are introduced as 2D matrices to
represent the upward and downward ramping limits for each generator.
2. 2D Vars ``rgu`` and ``rgd`` are introduced
Notes
-----
1. Formulations has been adjusted with interval ``config.t``, 1 [Hour] by default.
2. The tie-line flow has not been implemented in formulations
2. The tie-line flow is not implemented in this model.
"""

def __init__(self, system, config):
Expand All @@ -194,3 +188,25 @@ def __init__(self, system, config):
# if has model ``TimeSlot``, mandatory
# if has model ``Region``, optional
# if ``Region``, if ``Bus`` has param ``zone``, optional, if none, auto fill


class ED2(EDData, EDModel, ESD1Base):
"""
ED with energy storage :ref:`ESD1`.
The bilinear term in the formulation is linearized with big-M method.
"""

def __init__(self, system, config):
EDData.__init__(self)
EDModel.__init__(self, system, config)
ESD1Base.__init__(self)

self.config.t = 1 # dispatch interval in hour

self.info = 'Economic dispatch with energy storage'
self.type = 'DCED'

self.SOC.horizon = self.timeslot
self.pec.horizon = self.timeslot
self.uc.horizon = self.timeslot
self.zc.horizon = self.timeslot
6 changes: 3 additions & 3 deletions ams/routines/routine.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,12 @@ def init(self, force=False, no_code=True, **kwargs):
# FIXME: build the system matrices every init might slow down the process
self.system.mats.make()
results, elapsed_time = self.om.setup(no_code=no_code)
common_msg = f"Routine <{self.class_name}> initialized "
common_msg = f"Routine <{self.class_name}> "
if results:
msg = f"in {elapsed_time}."
msg = f"initialized in {elapsed_time}."
self.initialized = True
else:
msg = "failed!"
msg = "initialization failed!"
logger.info(common_msg + msg)
return results

Expand Down
Loading

0 comments on commit 5ae81b6

Please sign in to comment.