Skip to content

Commit 3a9dfd3

Browse files
committed
Add Repo Org and Logistics
1 parent 4f6a978 commit 3a9dfd3

File tree

16 files changed

+534
-734
lines changed

16 files changed

+534
-734
lines changed

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
# Quick-Start
99

10-
10+
# Developers
Lines changed: 31 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
1-
21
"""
32
"""
4-
import numpy as np
53
import matplotlib.pyplot as plt
6-
import pyomo.environ as pyo
4+
import numpy as np
75
import pandas as pd
8-
from .components import Resource, TransferTerm, Revenue, Cost, Source, Sink, Converter, Storage
9-
from .system import System
6+
import pyomo.environ as pyo
107

8+
from dove.core.components import Converter, Resource, Revenue, Sink, Source, Storage, TransferTerm
9+
from dove.core.system import System
1110

1211
if __name__ == '__main__':
12+
13+
### System Resources
1314
elec = Resource("electricity")
1415
steam = Resource("steam")
1516

16-
steamer = Source(
17-
name="steamer",
18-
produces=steam,
19-
capacity=100,
20-
dispatch_flexibility="fixed"
21-
)
17+
### Time-Series Profiles
18+
linear_price = np.arange(0.1, 2.2, 0.1).tolist() * 2
19+
spike_price = np.zeros(len(linear_price))
20+
spike_price.put((0, 11, 20, 28, 29, 38), 10)
2221

23-
steam_storage = Storage(
24-
name="steam_storage",
25-
resource=steam,
26-
capacity=100,
27-
rte=0.9,
28-
)
22+
23+
### Component Definitions
24+
steamer = Source(name="steamer", produces=steam, capacity=100, flexibility="fixed")
25+
steam_storage = Storage(name="steam_storage", resource=steam, capacity=100, rte=0.9)
2926

3027
gen = Converter(
3128
name="generator",
@@ -39,16 +36,13 @@
3936
]
4037
)
4138

42-
linear_price = np.arange(0.1, 2.2, 0.1).tolist() * 2
4339
market_linear = Sink(
4440
name="market_linear",
4541
consumes=elec,
4642
capacity=2,
47-
cashflows=[Revenue("esales", linear_price)],
43+
cashflows=[Revenue("esales", linear_price)]
4844
)
4945

50-
spike_price = np.zeros(len(linear_price))
51-
spike_price.put((0, 11, 20, 28, 29, 38), 10)
5246
market_spike = Sink(
5347
name="market_spike",
5448
consumes=elec,
@@ -63,31 +57,24 @@
6357
cashflows=[Revenue("steam_offload", 0.01)],
6458
)
6559

60+
### System Definition
6661
components = [steamer, steam_storage, gen, market_linear, market_spike, steam_offload]
6762
resources = [elec, steam]
68-
sys = System(components, resources, np.arange(0, len(linear_price)).tolist())
69-
model = sys.solve()
70-
71-
time_index = sys.time_index
72-
comp_names = list(sys.comp_map.keys())
73-
74-
# Extract dispatch into a DataFrame
75-
data = {
76-
comp: [pyo.value(model.dispatch[comp, t]) for t in time_index]
77-
for comp in comp_names
78-
}
79-
df = pd.DataFrame(data, index=time_index)
80-
print(df)
81-
82-
# Plot the dispatch schedules
83-
plt.figure()
84-
for comp in comp_names:
85-
plt.plot(time_index, df[comp], label=comp)
86-
plt.xlabel("Time")
87-
plt.ylabel("Dispatch")
88-
plt.title("Component Dispatch over Time")
89-
plt.legend()
90-
plt.show()
63+
time_index = np.arange(0, len(linear_price)).tolist()
64+
sys = System(components, resources, time_index)
65+
results = sys.solve()
66+
67+
print(results)
68+
69+
# ### Visualize Results
70+
# plt.figure()
71+
# for comp in system.comp_names:
72+
# plt.plot(time_index, results[comp], label=comp)
73+
# plt.xlabel("Time")
74+
# plt.ylabel("Dispatch")
75+
# plt.title("Component Dispatch over Time")
76+
# plt.legend()
77+
# plt.show()
9178

9279

9380

pyproject.toml

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,45 @@
11
[project]
22
name = "dove"
33
version = "0.1.0"
4-
description = "An extensible Python library for modeling multi-commodity energy systmes."
4+
description = "An extensible Python library for modeling multi-commodity dispatch of energy systems."
55
readme = "README.md"
66
requires-python = ">=3.11"
7-
license = { file = "LICENSE" }
8-
authors = [{name="Dylan McDowell", email="[email protected]"}]
7+
authors = [
8+
{ name = "Dylan McDowell", email = "[email protected]" }
9+
]
910
classifiers = [
1011
"License :: OSI Approved :: Apache Software License",
1112
"Programming Language :: Python :: 3.11",
1213
"Topic :: Scientific/Engineering :: Mathematics",
1314
"Intended Audience :: Science/Research"
1415
]
16+
17+
1518
dependencies = [
19+
"matplotlib>=3.10.1",
1620
"numpy>=2.2.5",
1721
"pandas>=2.2.3",
1822
"pyomo>=6.9.2",
1923
]
2024

21-
[dependency-groups]
22-
dev = [
23-
"build>=1.2.2.post1",
24-
"pytest>=8.3.5",
25-
"ruff>=0.11.8",
26-
"twine>=6.1.0",
27-
]
28-
2925
[build-system]
3026
requires = ["hatchling"]
3127
build-backend = "hatchling.build"
3228

3329
[tool.uv]
3430
managed = true
3531

36-
[[tool.uv.index]]
37-
url = "https://pypi.org/simple"
38-
default = true
39-
4032
[tool.ruff]
4133
indent-width = 4
4234
line-length = 100
4335

36+
[tool.ruff.format]
37+
indent-style = "space"
38+
quote-style = "double"
39+
line-ending = "auto"
40+
41+
4442
[tool.ruff.lint]
4543
select = ["E", "F", "W", "D1"]
4644
ignore = ["E402", "E501", "W505"]
4745

48-
[format]
49-
indent-style = "space"

pytest.ini

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/dove/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,3 @@
99
throughout the system.
1010
"""
1111

12-
from .components import Component
13-
14-
15-
__all__ = [
16-
"Component",
17-
]

src/dove/core/__init__.py

Whitespace-only changes.
Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ class TransferTerm:
2929
class CashFlow(ABC):
3030
""" """
3131
name: str
32-
reference_price: float
33-
reference_driver: float = 1.0
34-
scaling_factor_x: float = 1.0
32+
alpha: float | list[float]
33+
dprime: float | list[float] = 1.0
34+
scalex: float | list[float] = 1.0
3535
price_is_levelized: bool = False
3636

3737
@dataclass
@@ -47,20 +47,15 @@ class Revenue(CashFlow):
4747

4848
@dataclass(kw_only=True)
4949
class Component(ABC):
50-
"""
51-
Represents a system component in the grid analysis. Each component has a
52-
single "interaction" that describes what it can do (produce, store, demand)
53-
and a single CashFlowGroup which is a container for component associated cashflows.
54-
"""
50+
""" """
5551
name: str
5652
capacity: float | list[float]
5753
capacity_factor: Optional[float | list[float]] = None
5854
minimum: Optional[float | list[float]] = None
5955
capacity_resource: Optional[Resource] = None
60-
dispatch_flexibility: Literal["independent", "fixed"] = "independent"
56+
flexibility: Literal["independent", "fixed"] = "independent"
6157
cashflows: list[CashFlow] = field(default_factory=list)
6258
transfer_terms: list[TransferTerm] = field(default_factory=list)
63-
levelized_meta: dict = field(default_factory=dict)
6459

6560
def __post_init__(self) -> None:
6661
""" """
@@ -71,7 +66,6 @@ def __post_init__(self) -> None:
7166
class Source(Component):
7267
""" """
7368
produces: Resource
74-
tracking_vars: list[str] = field(default_factory=lambda: ["production"])
7569

7670
def __post_init__(self) -> None:
7771
""" """
@@ -85,7 +79,6 @@ def __post_init__(self) -> None:
8579
class Sink(Component):
8680
""" """
8781
consumes: Resource
88-
tracking_vars: list[str] = field(default_factory=lambda: ["production"])
8982

9083
def __post_init__(self) -> None:
9184
""" """
@@ -102,20 +95,21 @@ class Converter(Component):
10295
produces: Resource
10396
ramp_limit: float = 1.0
10497
ramp_freq: int = 0
105-
tracking_vars: list[str] = field(default_factory=lambda: ["production"])
10698

10799
def __post_init__(self) -> None:
108100
""" """
109101
super().__post_init__()
110102
extras = {c for c in self.consumes if c != self.produces}
111103
if extras and self.capacity_resource is None:
112104
raise ValueError(
113-
f"Ambiguity in Converter '{self.name}' consumes: {self.consumes} and "
114-
f"produces: {self.produces} please set 'capacity_resource'."
105+
f"Ambiguity! Converter '{self.name}' consumes: {self.consumes} and "
106+
f"produces: {self.produces} with no 'capacity_resource' defined!"
115107
)
108+
if not extras and self.capacity_resource is None:
109+
self.capacity_resource = self.produces
116110
if extras and not self.transfer_terms:
117-
raise ValueError(f"Converter '{self.name}' consumes {self.consumes} but no transfer terms defined.")
118-
111+
raise ValueError(f"Converter '{self.name}' consumes {self.consumes} but no transfer terms defined!")
112+
119113
# Auto‐adjust the sign of any transfer term that involves capacity_var:
120114
for term in self.transfer_terms:
121115
# Does this term actually involve our capacity resource?
@@ -137,7 +131,6 @@ class Storage(Component):
137131
max_discharge_rate: float = 1.0
138132
initial_stored: float = 0
139133
periodic_level: bool = True
140-
tracking_vars: list[str] = field(default_factory=lambda: ["level", "charge", "discharge", ])
141134

142135
def __post_init__(self) -> None:
143136
""" """
Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,47 @@
33
""" """
44

55
from typing import Self, Union
6+
67
import numpy as np
7-
import pyomo.environ as pyo
8-
from .pyomo_model_builder import PyomoModelBuilder
9-
from .components import Component, Resource
8+
9+
from ..models import BUILDER_REGISTRY
10+
from .components import Component, Resource, Storage
1011

1112
ArrayLike = Union[float, list[float], np.ndarray]
1213

13-
def _broadcast(x: ArrayLike, T: int) -> np.ndarray:
14+
15+
def _broadcast(x: ArrayLike, T: int) -> list[float]:
1416
"""Turn a scalar or length-T list/array into an np.ndarray of length T."""
1517
if isinstance(x, (int, float)):
16-
return np.full(T, float(x))
18+
return list(np.full(T, float(x)))
1719
arr = np.asarray(x, float)
1820
if arr.shape == (T,):
19-
return arr
21+
return list(arr)
2022
raise ValueError(f"Expected scalar or length-{T} array, got shape {arr.shape}")
2123

24+
2225
class System:
2326
""" """
2427

25-
def __init__(self, components, resources, time_index, builder=None) -> None:
28+
def __init__(self, components, resources, time_index) -> None:
2629
""" """
2730
self.components: list[Component] = components
2831
self.resources: list[Resource] = resources
2932
self.time_index = time_index
30-
self.builder = builder if builder else PyomoModelBuilder()
3133
self.comp_map = {comp.name: comp for comp in components}
3234
self.res_map = {res.name: res for res in resources}
3335
self._normalize_time_series()
3436

37+
@property
38+
def non_storage_comp_names(self) -> list[str]:
39+
""" """
40+
return [cn for cn in self.comp_map.keys() if not isinstance(self.comp_map[cn], Storage)]
41+
42+
@property
43+
def storage_comp_names(self) -> list[str]:
44+
""" """
45+
return [cn for cn in self.comp_map.keys() if isinstance(self.comp_map[cn], Storage)]
46+
3547
def add_component(self, comp) -> Self:
3648
""" """
3749
self.components.append(comp)
@@ -42,32 +54,43 @@ def add_resource(self, res) -> Self:
4254
self.resources.append(res)
4355
return self
4456

45-
def build(self) -> pyo.ConcreteModel:
57+
def build(self):
4658
""" """
47-
model = self.builder.build_model(self)
48-
return model
59+
raise NotImplementedError("System method 'build()' is not yet implemented!")
4960

50-
def solve(self) -> pyo.ConcreteModel:
61+
def solve(self, model_type: str = "price_taker", **kw):
5162
""" """
52-
model = self.builder.build_model(self)
53-
return self.builder.solve_model(model)
63+
try:
64+
builder_cls = BUILDER_REGISTRY[model_type]
65+
except KeyError:
66+
raise ValueError(
67+
f"Unknown model type: '{model_type}'! Available: {list(BUILDER_REGISTRY)}"
68+
)
69+
70+
builder = builder_cls(self)
71+
builder.build()
72+
builder.solve()
73+
return builder.extract_results()
5474

5575
def _normalize_time_series(self) -> None:
5676
""" """
77+
78+
# I guess these are technically all the variables we might expect
79+
# to be varying over time?
5780
for comp in self.components:
5881
comp.capacity = _broadcast(comp.capacity, len(self.time_index))
5982

6083
if comp.capacity_factor is not None:
6184
comp.capacity_factor = _broadcast(comp.capacity_factor, len(self.time_index))
6285

63-
if comp.dispatch_flexibility == "fixed":
86+
if comp.flexibility == "fixed":
6487
comp.minimum = comp.capacity
6588
elif comp.minimum is not None:
6689
comp.minimum = _broadcast(comp.minimum, len(self.time_index))
6790
else:
68-
comp.minimum = np.zeros(len(self.time_index), dtype=float)
91+
comp.minimum = np.zeros(len(self.time_index), dtype=float).tolist()
6992

7093
for cf in comp.cashflows:
71-
cf.reference_price = _broadcast(cf.reference_price, len(self.time_index))
72-
cf.reference_driver = _broadcast(cf.reference_driver, len(self.time_index))
73-
cf.scaling_factor_x = _broadcast(cf.scaling_factor_x, len(self.time_index))
94+
cf.alpha = _broadcast(cf.alpha, len(self.time_index))
95+
cf.dprime = _broadcast(cf.dprime, len(self.time_index))
96+
cf.scalex = _broadcast(cf.scalex, len(self.time_index))

0 commit comments

Comments
 (0)