diff --git a/dynamics.py b/dynamics.py index 096a6b0ce2c4264adee5fdbb94999020aea14f5c..11c84166dd0289ec931bd08a92e2333c1ad20c56 100644 --- a/dynamics.py +++ b/dynamics.py @@ -1247,10 +1247,26 @@ class AssignmentCommon(Assignment): return target_variable -class AggregatedDynamic(Dynamic): - # dynamic: The dynamic of the time series that was aggregated +# hold information about the architecture of a model or result +class Architecture(abc.ABC): + # returns the dynamic that a model constructed using this architecture should hold + @abc.abstractmethod + def model_dynamic(self) -> Dynamic: + pass + + +class TrivialArchitecture(Architecture): + def __init__(self, dynamic): + self.dynamic = dynamic + + def model_dynamic(self) -> Dynamic: + return self.dynamic + + +class PeriodAggregation(Architecture): + # dynamic: the dynamic that is aggregated # periods: the segment lengths of all periods - # period_order: the order the periods have arranged to reconstruct the original time series + # period_order: the order the periods have to be arranged in order to reconstruct the original dynamic def __init__( self, dynamic: Dynamic, periods: List[List[int]], period_order: List[int] ): @@ -1277,14 +1293,13 @@ class AggregatedDynamic(Dynamic): ) self.dynamic = dynamic - self.dynamic_tree = DynamicTree( + root_dynamic = DynamicTree( np.full(number_of_time_steps_per_period, step_length, dtype=int) - ) - root_dynamic = self.dynamic_tree.root() + ).root() self.period_dynamics = [] self.n_p = [] running_index = 0 - self.offsets = [] + offsets = [] for i, period in enumerate(periods): index = 0 period_indices = [0] @@ -1299,12 +1314,13 @@ class AggregatedDynamic(Dynamic): self.n_p.append( sum(1 for period_index in period_order if period_index == i) ) - self.offsets.append((running_index, running_index + len(period))) + offsets.append((running_index, running_index + len(period))) running_index += len(period) - self.period_order = period_order self.n = len(period_order) - self.shape_ = running_index - self.frist_state_shape_ = len(periods) + + self.value_dynamic = AggregatedDynamic( + self, len(periods), running_index, offsets + ) # sanity check 5: all elements in period order are valid period indices (above n_p[i] is the number of times that period index i appears in period order -> sum(n_p) == len(period_order)) if sum(n_p for n_p in self.n_p) != len(period_order): @@ -1315,17 +1331,31 @@ class AggregatedDynamic(Dynamic): def number_of_periods(self) -> int: return len(self.period_dynamics) - def offset(self, period_index) -> Tuple[int]: - return self.offsets[period_index] + def model_dynamic(self) -> Dynamic: + return self.dynamic + + +class AggregatedDynamic(Dynamic): + def __init__( + self, + period_aggregation: PeriodAggregation, + number_of_periods: int, + length: int, + offsets: List[Tuple[int, int]], + ): + self.period_aggregation = period_aggregation + self.number_of_periods = number_of_periods + self.length = length + self.offsets = offsets def number_of_steps(self) -> int: - return self.dynamic.number_of_steps() + raise NotImplementedError def shape(self) -> int: - return self.shape_ + return self.length def first_state_shape(self) -> int: - return self.frist_state_shape_ + return self.number_of_periods def pandas_index(self) -> pd.Index: first_level = np.empty(self.shape(), dtype=int) @@ -1336,31 +1366,31 @@ class AggregatedDynamic(Dynamic): return pd.MultiIndex.from_arrays([first_level, second_level]) def first_state_pandas_index(self) -> pd.Index: - first_level = np.arange(0, self.number_of_periods(), dtype=int) + first_level = np.arange(0, self.first_state_shape(), dtype=int) second_level = np.full(self.first_state_shape(), -1, dtype=int) return pd.MultiIndex.from_arrays([first_level, second_level]) def step_size(self, position) -> float: - return self.dynamic.step_size(position) + raise NotImplementedError def step_lengths(self): # type hinting a np-array of ints - return self.dynamic.step_lengths() + raise NotImplementedError def _all_indices(self): # type hinting a np-array of ints - return self.dynamic._all_indices() + raise NotImplementedError class PeriodDynamic(Dynamic): - # aggregated_dynamic: The aggregated dynamic that is period originates from - # period_index: The index in the period aggregation that this period represents - # dynamic: The dynamic of this period + # period_aggregation: the period aggregation that is period originates from + # period_index: the index of this period in the period aggregation + # dynamic: the dynamic of this period def __init__( self, - aggregated_dynamic: AggregatedDynamic, + period_aggregation: PeriodAggregation, period_index: int, dynamic: BackedDynamic, ): - self.aggregated_dynamic = aggregated_dynamic + self.period_aggregation = period_aggregation self.period_index = period_index self.dynamic = dynamic @@ -1396,7 +1426,7 @@ def resample( return values elif isinstance(dynamic, TreeDynamic) and isinstance(target_dynamic, TreeDynamic): if dynamic.root() != target_dynamic.root(): - raise ValueError("Both dynamics have to have the same root dynamic!") + raise ValueError("Both dynamics have to be part of the same dynamic tree!") ( assignment, source_start, @@ -1410,11 +1440,11 @@ def resample( elif isinstance(dynamic, AggregatedDynamic) and isinstance( target_dynamic, PeriodDynamic ): - if target_dynamic.aggregated_dynamic != dynamic: + if dynamic.period_aggregation != target_dynamic.period_aggregation: raise ValueError( - f"The period dynamic has to be part of the aggregated dynamic!" + f"The aggregated dynamic and the period dynamic have to be part of the same period aggregation!" ) - offset = dynamic.offset(target_dynamic.period_index) + offset = dynamic.offsets[target_dynamic.period_index] return values[:, offset[0] : offset[1]] else: raise ValueError( @@ -1429,7 +1459,7 @@ def resample_into( target_values[:] = values elif isinstance(dynamic, TreeDynamic) and isinstance(target_dynamic, TreeDynamic): if dynamic.root() != target_dynamic.root(): - raise ValueError("Both dynamics have to have the same root dynamic!") + raise ValueError("Both dynamics have to be part of the same dynamic tree!") ( assignment, source_start, @@ -1443,11 +1473,11 @@ def resample_into( elif isinstance(dynamic, PeriodDynamic) and isinstance( target_dynamic, AggregatedDynamic ): - if dynamic.aggregated_dynamic != target_dynamic: + if dynamic.period_aggregation != target_dynamic.period_aggregation: raise ValueError( - f"The period dynamic has to be part of the aggregated dynamic!" + f"The period dynamic and the aggregated dynamic have to be part of the same period aggregation!" ) - offset = target_dynamic.offset(dynamic.period_index) + offset = target_dynamic.offsets[dynamic.period_index] target_values[:, offset[0] : offset[1]] = values else: raise ValueError( @@ -1462,14 +1492,14 @@ def resample_first_state_into( target_values[:] = values elif isinstance(dynamic, TreeDynamic) and isinstance(target_dynamic, TreeDynamic): if dynamic.root() != target_dynamic.root(): - raise ValueError("Both dynamics have to have the same root dynamic!") + raise ValueError("Both dynamics have to be part of the same dynamic tree!") target_values[:] = values elif isinstance(dynamic, PeriodDynamic) and isinstance( target_dynamic, AggregatedDynamic ): - if dynamic.aggregated_dynamic != target_dynamic: + if dynamic.period_aggregation != target_dynamic.period_aggregation: raise ValueError( - f"The period dynamic has to be part of the aggregated dynamic!" + f"The period dynamic and the aggregated dynamic have to be part of the same period aggregation!" ) target_values[:, dynamic.period_index] = values @@ -1514,13 +1544,15 @@ class Profile: } elif isinstance(dynamic, AggregatedDynamic): df = pd.read_csv(path, index_col=[0, 1]) - if len(df.index.levels[0]) != dynamic.number_of_periods(): + if len(df.index.levels[0]) != dynamic.number_of_periods: raise ValueError( f"The number of periods in the csv file and the number of periods in the dynamic have the be the same!" ) if any( len(df.loc[period_index]) != period_dynamic.number_of_steps() - for period_index, period_dynamic in enumerate(dynamic.period_dynamics) + for period_index, period_dynamic in enumerate( + dynamic.period_aggregation.period_dynamics + ) ): raise ValueError( f"The number of rows in the csv file for a period and the number of segments in that period have the be the same!" diff --git a/optimization_model.py b/optimization_model.py index 9638c68c1153fc26891c62312094dd2e62639762..9108e29103d32da5eb90a403a1e15ef61db08116 100644 --- a/optimization_model.py +++ b/optimization_model.py @@ -134,7 +134,7 @@ class OptimizationModel(OptimizationBlock): ) class EntityResult: - def __init__(self, var_names, dynamic): + def __init__(self, var_names, architecture, dynamic): n_u_vars = 0 n_i_vars = 0 n_i_prime_vars = 0 @@ -160,6 +160,7 @@ class EntityResult: self.i_prime_first_result = np.empty((n_i_prime_vars, dynamic.first_state_shape()), dtype=float) self.i_prime_result = np.empty((n_i_prime_vars, dynamic.shape()), dtype=float) + self.architecture = architecture self.dynamic = dynamic self.sub_results = dict() diff --git a/topology.py b/topology.py index 798bda3ae2fcc7b25fffd069b1399fa9ad12096b..e942c3ccb16daf9a5d047affe66d6c72b3870717 100644 --- a/topology.py +++ b/topology.py @@ -23,7 +23,7 @@ THE SOFTWARE. """ from Model_Library.component.core import ComponentCommodity, ComponentKind -from Model_Library.dynamics import AggregatedDynamic, TreeDynamic, TrivialDynamic +from Model_Library.dynamics import PeriodAggregation, TrivialArchitecture from Model_Library.optimization_model import ( EntityResult, OptimizationBlock, @@ -239,8 +239,8 @@ class Topology: if component.match(kind=kind, commodity=commodity) ) - def optimize(self, key, dynamic, strategy): - model = self.build_model(dynamic, strategy) + def optimize(self, key, architecture, strategy): + model = self.build_model(architecture, strategy) options = dict() options["MIPGap"] = 0.01 @@ -252,24 +252,24 @@ class Topology: if not model.is_ok(): raise RuntimeError("Model is infeasible or unbounded!") - self.create_empty_entity_result(key, dynamic) + self.create_empty_entity_result(key, architecture) self.extract_result(model, key) - def build_model(self, dynamic, strategy): - model = OptimizationModel(self._name, dynamic) + def build_model(self, architecture, strategy): + model = OptimizationModel(self._name, architecture.model_dynamic()) - self.fill_block(model, dynamic, strategy) + self.fill_block(model, architecture, strategy) model.collect_objectives() return model - def fill_block(self, block, dynamic, strategy): + def fill_block(self, block, architecture, strategy): for asset in self._assets.values(): - asset_block = OptimizationBlock(asset._name, dynamic) + asset_block = OptimizationBlock(asset._name, architecture.model_dynamic()) block.add(asset._name, asset_block) - asset.fill_block(asset_block, dynamic, strategy) + asset.fill_block(asset_block, architecture, strategy) if strategy is None: objectives = {"objective": []} @@ -280,7 +280,7 @@ class Topology: else: raise ValueError(f"Invalid strategy type!") - if isinstance(block.dynamic, (TrivialDynamic, TreeDynamic)): + if isinstance(architecture, TrivialArchitecture): design_objectives = self.fill_design_block(block, objectives) operational_objectives = self.fill_operational_block( @@ -302,14 +302,12 @@ class Topology: ) block.add_objective(name, objective) - elif isinstance(block.dynamic, AggregatedDynamic): + elif isinstance(architecture, PeriodAggregation): design_objectives = self.fill_design_block(block, objectives) period_blocks = [] operational_objectives = [] - for period_index, period_dynamic in enumerate( - block.dynamic.period_dynamics - ): + for period_index, period_dynamic in enumerate(architecture.period_dynamics): period_block = OptimizationBlock(str(period_index), period_dynamic) block.add(str(period_index), period_block) period_blocks.append(period_block) @@ -323,12 +321,12 @@ class Topology: for name in objectives: scaled_expression = 0.0 one_time_expression = 0.0 - for period_index in range(block.dynamic.number_of_periods()): + for period_index in range(architecture.number_of_periods()): ( period_scaled_expression, period_one_time_expression, ) = operational_objectives[period_index][name] - scaled_expression += block.dynamic.n_p[period_index] * ( + scaled_expression += architecture.n_p[period_index] * ( period_scaled_expression + pyo.quicksum( term @@ -338,15 +336,15 @@ class Topology: ) ) one_time_expression += ( - block.dynamic.n_p[period_index] * period_one_time_expression + architecture.n_p[period_index] * period_one_time_expression ) objective = design_objectives[name] + w * ( - scaled_expression + (1.0 / block.dynamic.n) * one_time_expression + scaled_expression + (1.0 / architecture.n) * one_time_expression ) block.add_objective(name, objective) else: - raise ValueError(f"Invalid dynamic type {type(block.dynamic)}") + raise ValueError(f"Invalid architecture type {type(architecture)}") def fill_design_block(self, d_block, objectives): for component in self._components: @@ -496,11 +494,11 @@ class Topology: return operational_objectives - def create_empty_entity_result(self, key, dynamic): + def create_empty_entity_result(self, key, architecture): for asset in self._assets.values(): - asset.create_empty_entity_result(key, dynamic) + asset.create_empty_entity_result(key, architecture) - if isinstance(dynamic, (TrivialDynamic, TreeDynamic)): + if isinstance(architecture, TrivialArchitecture): base_variable_names = [] for component in self._components: base_variable_names.extend( @@ -513,16 +511,20 @@ class Topology: for flow in self._flows: base_variable_names.append((flow, VariableKind.INDEXED)) - result = EntityResult(base_variable_names, dynamic) + result = EntityResult( + base_variable_names, architecture, architecture.dynamic + ) - elif isinstance(dynamic, (AggregatedDynamic)): + elif isinstance(architecture, PeriodAggregation): design_base_variable_names = [] for component in self._components: design_base_variable_names.extend( self._components[component].design_base_variable_names() ) - result = EntityResult(design_base_variable_names, dynamic) + result = EntityResult( + design_base_variable_names, architecture, architecture.dynamic + ) operational_base_variable_names = [] for component in self._components: @@ -533,15 +535,15 @@ class Topology: for flow in self._flows: operational_base_variable_names.append((flow, VariableKind.INDEXED)) - for period_index, period_dynamic in enumerate(dynamic.period_dynamics): + for period_index, period_dynamic in enumerate(architecture.period_dynamics): period_result = EntityResult( - operational_base_variable_names, period_dynamic + operational_base_variable_names, None, period_dynamic ) result.add_sub_result(period_index, period_result) else: - raise ValueError(f"Invalid dynamic type {type(dynamic)}") + raise ValueError(f"Invalid architecture type {type(architecture)}") self._results[key] = result self._last_result_key = key @@ -551,21 +553,21 @@ class Topology: asset_block = block.blocks[asset._name] asset.extract_result(asset_block, key) - if isinstance(block.dynamic, (TrivialDynamic, TreeDynamic)): - self._results[key].extract_result(block) + result = self._results[key] - elif isinstance(block.dynamic, (AggregatedDynamic)): - self._results[key].extract_result(block) + if isinstance(result.architecture, TrivialArchitecture): + result.extract_result(block) - for period_index in range(block.dynamic.number_of_periods()): + elif isinstance(result.architecture, PeriodAggregation): + result.extract_result(block) + + for period_index in range(result.architecture.number_of_periods()): period_block = block.blocks[str(period_index)] - self._results[key].sub_results[period_index].extract_result( - period_block - ) + result.sub_results[period_index].extract_result(period_block) else: - raise ValueError(f"Invalid dynamic type {type(block.dynamic)}") + raise ValueError(f"Invalid architecture type {type(result.architecture)}") def save_results(self, path, keys=None): for asset in self._assets.values():