Skip to content
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [2.1.7] - Unreleased

### Changed
- getattr dunder to pull attrs from constituents in Storage

### Fixed
- retry to fix strange variable index bug when writing inventory bound



## [2.1.6] - 2025-11-17

### Changed
Expand Down
116 changes: 58 additions & 58 deletions docs/examples/design_scheduling.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/energia/components/operations/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def _check_operate_bound(self, space: Location | Linkage):
def _update_space_times(
self,
space: Location | Linkage,
space_times: list[tuple[Periods, Location | Linkage]],
space_times: list[tuple[Location | Linkage, Periods]],
):
"""Update space times for the operation"""

Expand Down Expand Up @@ -212,7 +212,7 @@ def locate(self, *spaces: Location | Linkage):

self.write_primary_conversion(space_times)

if self.construction is not None:
if self.construction:
self.write_construction(self.space_times)

return self, spaces
Expand Down
56 changes: 34 additions & 22 deletions src/energia/components/operations/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ...modeling.parameters.conversion import Conversion
from ...modeling.parameters.conversions import Construction
from ...utils.decorators import timer
from ...utils.modeling import retry
from ..commodities.resource import Resource
from .process import Process

Expand Down Expand Up @@ -139,26 +140,6 @@ def inventory_aspect(self) -> Aspect:
"""Reports inventory as aspect"""
return getattr(self.model, 'inventory')

@property
def capacity(self) -> Sample:
"""Reports invcapacity as capacity"""
return self.stored.invcapacity

@property
def setup(self) -> Sample:
"""Reports invsetup as setup"""
return self.stored.invsetup

@property
def dismantle(self) -> Sample:
"""Reports invdismantle as dismantle"""
return self.stored.invdismantle

@property
def inventory(self) -> Sample:
"""Inventory of the stored resource"""
return self.stored.inventory

@property
def basis(self) -> Resource:
"""Base resource"""
Expand Down Expand Up @@ -238,8 +219,16 @@ def _check_inventory_bound(

time = self._filter_time(self._get_times(space))

#! FIXME: not entirely sure why retry is needed here
# if not just write opr_{pro, loc, horizon} <= capacity_{pro, loc, horizon}
_ = self.inventory(space, time) <= 1
# _ = self.inventory(space, time) <= 1

_ = retry(
lambda: self.inventory(space, time) <= 1,
attempts=2,
exceptions=KeyError,
)

return self, space, time

return False
Expand All @@ -265,6 +254,10 @@ def locate(self, *spaces: Location):
# update the locations at which the storage exists

# get location, time tuples where operation is defined

if not spaces:
spaces = (self.network,)

for space in spaces:

self._check_capacity_bound(space)
Expand All @@ -275,7 +268,7 @@ def locate(self, *spaces: Location):
self.charge.locate(*spaces)
self.discharge.locate(*spaces)

if self.construction is not None:
if self.construction:
self.write_construction(self.space_times)

return self, spaces
Expand Down Expand Up @@ -380,6 +373,25 @@ def __setattr__(self, name, value):

super().__setattr__(name, value)

def __getattr__(self, name):

# for Storage to make a distinction
# these are called inv + aspect name
# for e.g.: capacity -> invcapacity
# secondly, these are all defined based on the stored resource
if name in [
"capacity",
"setup",
"dismantle",
]:
return getattr(self.stored, "inv" + name)

# these are directly defined based on the stored resource
if name in ["inventory"]:
return getattr(self.stored, name)

return super().__getattr__(name)

def __call__(self, resource: Stored | Conversion):
"""Conversion is called with a Resource to be converted"""

Expand Down
8 changes: 0 additions & 8 deletions src/energia/modeling/constraints/balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,12 @@ def existing_aspects(self):
@cached_property
def updated_part(self) -> V | F | int:
"""Returns the part of the constraint that is new"""

if self.stored and self.aspect == "inventory":
# if inventory is being add to GRB

if len(self.time) == 1:
# cannot lag a single time period
return 0

print(
self.aspect,
self.aspect.domains,
self.domain.I,
self.domain.edit({"lag": -1 * self.time, "periods": None}).I,
)
return (
self(*self.domain).V()
- self(*self.domain.edit({"lag": -1 * self.time, "periods": None})).V()
Expand Down
23 changes: 23 additions & 0 deletions src/energia/utils/modeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Utilities for modeling"""


def retry(func, attempts: int = 2, exceptions=(Exception,)):
"""
Retry a function a number of times if it raises specified exceptions.

:param func: The function to retry.
:type func: callable
:param attempts: Number of attempts.
:type attempts: int
:param exceptions: Exceptions to catch and retry on.
:type exceptions: tuple[Exception]

:return: The result of the function if successful.
"""
last_exception = None
for _ in range(attempts):
try:
return func()
except exceptions as e:
last_exception = e
raise last_exception