diff --git a/dynamics.py b/dynamics.py index 5e011f85f23a842e6cddabbc1cc73a2ef04315ff..e99154204de5b6c0a6f2a01b27ce027800e5a851 100644 --- a/dynamics.py +++ b/dynamics.py @@ -677,6 +677,7 @@ def compute_assignment_to_backed( dynamic.step_length_p(source_position) / target_length, ) assignment.add_expression(target_position, expression) + assignment.compile() return assignment @@ -688,10 +689,11 @@ def compute_assignment_from_backed( first_target_position = target_dynamic.position_of(dynamic.indices[0]) for source_position, next_source_index in enumerate(dynamic.indices[1:]): next_target_position = target_dynamic.position_of(next_source_index) - assignment.add_distribute( + assignment.add_distribution( first_target_position, next_target_position, source_position ) first_target_position = next_target_position + assignment.compile() return assignment @@ -704,6 +706,7 @@ def compute_assignment_common_reference( dynamic.indices[-1] <= target_dynamic.indices[0] or target_dynamic.indices[-1] <= dynamic.indices[0] ): + assignment.compile() return assignment target_i_start = target_dynamic.indices[0] if target_i_start not in dynamic.indices: @@ -736,6 +739,7 @@ def compute_assignment_common_reference( elif ( root_p_start > dynamic.indices[-1] ): # because root is a RootDynamic, positions and indices are equivalent + assignment.compile() return assignment # here, we discover that the entire dynamic does not cover one time_step of the target_dynamic target_position = target_dynamic.position_of( root_p_start @@ -753,6 +757,7 @@ def compute_assignment_common_reference( if remaining_length == 0: source_position += 1 if source_position >= len(dynamic.indices) - 1: + assignment.compile() return assignment remaining_length = dynamic.step_length_p(source_position) if remaining_target_length <= remaining_length: @@ -767,6 +772,7 @@ def compute_assignment_common_reference( remaining_length -= remaining_length assignment.add_expression(target_position, expression) target_position += 1 + assignment.compile() return assignment @@ -783,10 +789,18 @@ class Assignment(abc.ABC): ): # values: type hinting a np-array, type hinting a np-array pass - # # adds the values indexed by source into the target values indexed by target - # @abc.abstractmethod - # def resample_into(self, values, source_start, source_end, target_values, target_start, target_end): - # pass + # adds the values indexed by source into the target values indexed by target + @abc.abstractmethod + def resample_into( + self, + values, + source_start: int, + source_end: int, + target_values, + target_start: int, + target_end: int, + ): # values: type hinting a np-array, target_values: type hinting a np-array + pass # generates expressions representing the resampling @abc.abstractmethod @@ -812,7 +826,24 @@ class AssignmentSame(Assignment): raise IndexError("Source values do not cover all target time steps!") if source_end < target_end: raise IndexError("Source values do not cover all target time steps!") - return values[target_start - source_start : target_end - source_start] + return values[:, target_start - source_start : target_end - source_start] + + def resample_into( + self, + values, + source_start: int, + source_end: int, + target_values, + target_start: int, + target_end: int, + ): # values: type hinting a np-array, target_values: type hinting a np-array + if source_start < target_start: + raise IndexError("Target values do not cover all source time steps!") + if target_end < source_end: + raise IndexError("Target values do not cover all source time steps!") + target_values[ + :, source_start - target_start : source_end - target_start + ] = values def resample_variable( self, variable, source_start, source_end, target_start, target_end @@ -841,6 +872,31 @@ class AssignmentToBacked(Assignment): ): # expression: type hinting a np-array of (int, float) self.expressions[target_position] = expression + def compile(self): + self.first_complete_expression = np.empty( + len(self.indices), dtype=int + ) # self.first_complete_expression[source_position] is the index of the first complete expression that the source dynamic covers, if it would start with source_position + self.last_complete_expression = np.empty( + len(self.indices), dtype=int + ) # self.last_complete_expression[source_position] is 1 plus the index of the last complete expression that the source dynamic covers, if it would end with source_position + next_position_to_assign_first = 0 + next_position_to_assign_last = 0 + for i, expression in enumerate(self.expressions): + self.first_complete_expression[ + next_position_to_assign_first : expression[0][0] + 1 + ] = i + next_position_to_assign_first = expression[0][0] + 1 + self.last_complete_expression[ + next_position_to_assign_last : expression[-1][0] + 1 + ] = i + next_position_to_assign_last = expression[-1][0] + 1 + self.first_complete_expression[next_position_to_assign_first:] = len( + self.expressions + ) + self.last_complete_expression[next_position_to_assign_last:] = len( + self.expressions + ) + def resample( self, values, @@ -857,16 +913,50 @@ class AssignmentToBacked(Assignment): raise IndexError("Source values do not cover all target time steps!") if source_i_end < target_i_end: raise IndexError("Source values do not cover all target time steps!") - target_values = np.empty(target_end - target_start, dtype=values.dtype) + target_values = np.empty( + (values.shape[0], target_end - target_start), dtype=values.dtype + ) for local_target_position, expression in enumerate( self.expressions[target_start:target_end] ): acc = 0.0 for source_position, factor in expression: - acc += factor * values[source_position - source_start] - target_values[local_target_position] = acc + acc += factor * values[:, source_position - source_start] + target_values[:, local_target_position] = acc return target_values + def resample_into( + self, + values, + source_start: int, + source_end: int, + target_values, + target_start: int, + target_end: int, + ): # values: type hinting a np-array, target_values: type hinting a np-array + source_i_start = self.indices[source_start] + source_i_end = self.indices[source_end] + target_i_start = self.target_indices[target_start] + target_i_end = self.target_indices[target_end] + if source_i_start < target_i_start: + raise IndexError("Target values do not cover all source time steps!") + if target_i_end < source_i_end: + raise IndexError("Target values do not cover all source time steps!") + first_complete_expression = self.first_complete_expression[source_start] + for local_target_position, expression in enumerate( + self.expressions[ + max(first_complete_expression, target_start) : min( + self.last_complete_expression[source_end], target_end + ) + ] + ): + acc = 0.0 + for source_position, factor in expression: + acc += factor * values[:, source_position - source_start] + target_values[ + :, local_target_position + first_complete_expression - target_start + ] = acc + def resample_variable( self, variable, source_start, source_end, target_start, target_end ): @@ -888,8 +978,8 @@ class AssignmentToBacked(Assignment): ): acc = 0.0 for source_position, factor in expression: - acc += factor * variable[source_position - source_start] - target_variable[local_target_position] = acc + acc += factor * variable[:, source_position - source_start] + target_variable[:, local_target_position] = acc return target_variable @@ -897,14 +987,29 @@ class AssignmentFromBacked(Assignment): def __init__( self, indices, target_indices ): # indices: type hinting a np-array of ints, target_indices: type hinting a np-array of ints - self.index = indices + self.indices = indices self.target_indices = target_indices - self.indices = np.empty(len(target_indices) - 1, dtype=int) + self.distributions = np.empty(len(indices) - 1, dtype=object) - def add_distribute( + def add_distribution( self, first_target_position: int, end_target_position: int, source_position: int ): - self.indices[first_target_position:end_target_position] = source_position + self.distributions[source_position] = ( + first_target_position, + end_target_position, + ) + + def compile(self): + self.distribution_positions = np.full(len(self.target_indices), -1, dtype=int) + self.source_to_target = np.empty(len(self.indices), dtype=int) + for source_position, (first_target_position, end_target_position) in enumerate( + self.distributions + ): + self.distribution_positions[ + first_target_position:end_target_position + ] = source_position + self.source_to_target[source_position] = first_target_position + self.source_to_target[-1] = self.distributions[-1][1] def resample( self, @@ -914,15 +1019,47 @@ class AssignmentFromBacked(Assignment): target_start: int, target_end: int, ): # values: type hinting a np-array, type hinting a np-array - source_i_start = self.index[source_start] - source_i_end = self.index[source_end] + source_i_start = self.indices[source_start] + source_i_end = self.indices[source_end] target_i_start = self.target_indices[target_start] target_i_end = self.target_indices[target_end] if target_i_start < source_i_start: raise IndexError("Source values do not cover all target time steps!") if source_i_end < target_i_end: raise IndexError("Source values do not cover all target time steps!") - return values[self.indices[target_start:target_end] - source_start] + return values[ + :, self.distribution_positions[target_start:target_end] - source_start + ] + + def resample_into( + self, + values, + source_start: int, + source_end: int, + target_values, + target_start: int, + target_end: int, + ): # values: type hinting a np-array, target_values: type hinting a np-array + source_i_start = self.indices[source_start] + source_i_end = self.indices[source_end] + target_i_start = self.target_indices[target_start] + target_i_end = self.target_indices[target_end] + if source_i_start < target_i_start: + raise IndexError("Target values do not cover all source time steps!") + if target_i_end < source_i_end: + raise IndexError("Target values do not cover all source time steps!") + target_values[ + :, + self.source_to_target[source_start] + - target_start : self.source_to_target[source_end] + - target_start, + ] = values[ + :, + self.distribution_positions[ + self.source_to_target[source_start] : self.source_to_target[source_end] + ] + - source_start, + ] def resample_variable( self, variable, source_start, source_end, target_start, target_end @@ -941,10 +1078,10 @@ class AssignmentFromBacked(Assignment): ) target_variable = np.empty(target_end - target_start, dtype=object) for local_target_position, source_position in enumerate( - self.indices[target_start:target_end] + self.distribution_positions[target_start:target_end] ): - target_variable[local_target_position] = variable[ - source_position - source_start + target_variable[:, local_target_position] = variable[ + :, source_position - source_start ] return target_variable @@ -962,6 +1099,34 @@ class AssignmentCommon(Assignment): ): # expression: type hinting a np-array of (int, float) self.expressions[target_position] = expression + def compile(self): + self.first_complete_expression = np.empty( + len(self.indices), dtype=int + ) # self.first_complete_expression[source_position] is the index of the first complete expression that the source dynamic covers, if it would start with source_position + self.last_complete_expression = np.empty( + len(self.indices), dtype=int + ) # self.last_complete_expression[source_position] is 1 plus the index of the last complete expression that the source dynamic covers, if it would end with source_position + next_position_to_assign_first = 0 + next_position_to_assign_last = 0 + last_existing_expression = 0 + for i, expression in enumerate(self.expressions): + if expression is not None: + self.first_complete_expression[ + next_position_to_assign_first : expression[0][0] + 1 + ] = i + next_position_to_assign_first = expression[0][0] + 1 + self.last_complete_expression[ + next_position_to_assign_last : expression[-1][0] + 1 + ] = i + next_position_to_assign_last = expression[-1][0] + 1 + last_existing_expression = i + self.first_complete_expression[next_position_to_assign_first:] = ( + last_existing_expression + 1 + ) + self.last_complete_expression[next_position_to_assign_last:] = ( + last_existing_expression + 1 + ) + def resample( self, values, @@ -978,16 +1143,50 @@ class AssignmentCommon(Assignment): raise IndexError("Source values do not cover all target time steps!") if source_i_end < target_i_end: raise IndexError("Source values do not cover all target time steps!") - target_values = np.empty(target_end - target_start, dtype=values.dtype) + target_values = np.empty( + (values.shape[0], target_end - target_start), dtype=values.dtype + ) for local_target_position, expression in enumerate( self.expressions[target_start:target_end] ): acc = 0.0 for source_position, factor in expression: - acc += factor * values[source_position - source_start] - target_values[local_target_position] = acc + acc += factor * values[:, source_position - source_start] + target_values[:, local_target_position] = acc return target_values + def resample_into( + self, + values, + source_start: int, + source_end: int, + target_values, + target_start: int, + target_end: int, + ): # values: type hinting a np-array, target_values: type hinting a np-array + source_i_start = self.indices[source_start] + source_i_end = self.indices[source_end] + target_i_start = self.target_indices[target_start] + target_i_end = self.target_indices[target_end] + if source_i_start < target_i_start: + raise IndexError("Target values do not cover all source time steps!") + if target_i_end < source_i_end: + raise IndexError("Target values do not cover all source time steps!") + first_complete_expression = self.first_complete_expression[source_start] + for local_target_position, expression in enumerate( + self.expressions[ + max(first_complete_expression, target_start) : min( + self.last_complete_expression[source_end], target_end + ) + ] + ): + acc = 0.0 + for source_position, factor in expression: + acc += factor * values[:, source_position - source_start] + target_values[ + :, local_target_position + first_complete_expression - target_start + ] = acc + def resample_variable( self, variable, source_start, source_end, target_start, target_end ): @@ -1009,8 +1208,8 @@ class AssignmentCommon(Assignment): ): acc = 0.0 for source_position, factor in expression: - acc += factor * variable[source_position - source_start] - target_variable[local_target_position] = acc + acc += factor * variable[:, source_position - source_start] + target_variable[:, local_target_position] = acc return target_variable @@ -1179,7 +1378,52 @@ def resample_aggregation_down( f"The period dynamic has to be part of the aggregated dynamic!" ) offset = dynamic.offsets(target_dynamic.period_index) - return values[offset[0] : offset[1]] + return values[:, offset[0] : offset[1]] + + +def resample_into( + values, dynamic: Dynamic, target_values, target_dynamic: Dynamic +): # values: type hinting a np-array, target_values: type hinting a np-array + if dynamic == target_dynamic: + target_values[:] = values + elif isinstance(dynamic, TreeDynamic) and isinstance(target_dynamic, TreeDynamic): + resample_into_tree(values, dynamic, target_values, target_dynamic) + elif isinstance(dynamic, PeriodDynamic) and isinstance( + target_dynamic, AggregatedDynamic + ): + resample_aggregation_up(values, dynamic, target_values, target_dynamic) + else: + raise ValueError( + f"Invalid dynamic type combination {type(dynamic)} -> {type(target_dynamic)}!" + ) + + +def resample_into_tree( + values, dynamic: TreeDynamic, target_values, target_dynamic: TreeDynamic +): # values: type hinting a np-array, target_values: type hinting a np-array + if dynamic.root() != target_dynamic.root(): + raise ValueError("Both dynamics have to have the same root dynamic!") + ( + assignment, + source_start, + source_end, + target_start, + target_end, + ) = dynamic.dynamic_tree.get_assignment(dynamic, target_dynamic) + assignment.resample_into( + values, source_start, source_end, target_values, target_start, target_end + ) + + +def resample_aggregation_up( + values, dynamic: PeriodDynamic, target_values, target_dynamic: AggregatedDynamic +): # values: type hinting a np-array, target_values: type hinting a np-array + if dynamic.aggregated_dynamic != target_dynamic: + raise ValueError( + f"The period dynamic has to be part of the aggregated dynamic!" + ) + offset = target_dynamic.offsets(dynamic.period_index) + target_values[:, offset[0] : offset[1]] = values def resample_variable(variable, dynamic: Dynamic, target_dynamic: Dynamic): @@ -1246,7 +1490,10 @@ class Profile: def resample(self, dynamic: Dynamic) -> "Profile": if dynamic == self.dynamic: return self - return Profile(resample(self.values, self.dynamic, dynamic), dynamic) + return Profile( + resample(np.expand_dims(self.values, axis=0), self.dynamic, dynamic)[0], + dynamic, + ) def test_single_resampling( @@ -1261,23 +1508,30 @@ def test_single_resampling( i_end = dynamic.index_of(dynamic.number_of_steps()) target_i_start = target_dynamic.index_of(0) target_i_end = target_dynamic.index_of(target_dynamic.number_of_steps()) - if target_i_start < i_start or i_end < target_i_end: - try: - result = resample(values, dynamic, target_dynamic) - except Exception as error: - if str(error) == "Source values do not cover all target time steps!": - f.write("fine # ") - else: - f.write(str(error) + " # ") - return - f.write("???? # ") - return - else: - try: - result = resample(values, dynamic, target_dynamic) - except Exception as error: + resample_possible = i_start <= target_i_start and target_i_end <= i_end + try: + result = resample(np.expand_dims(values, axis=0), dynamic, target_dynamic)[0] + except Exception as error: + if str(error) == "Source values do not cover all target time steps!": + f.write("olap # ") + else: + f.write(str(error) + " # ") + resample_into_possible = target_i_start <= i_start and i_end <= target_i_end + try: + result_into = np.full( + (1, target_dynamic.number_of_steps()), np.nan, dtype=float + ) + resample_into( + np.expand_dims(values, axis=0), dynamic, target_dynamic, result_into + ) + except Exception as error: + if str(error) == "Target values do not cover all source time steps!": + f.write("olap # ") + else: f.write(str(error) + " # ") - return + result_into = result_into[0] + if not resample_possible and not resample_into_possible: + return target_values = np.full(target_dynamic.number_of_steps(), np.nan, dtype=float) # resample from source to ancestor first_ancestor_position = ancestor_dynamic.position_of(dynamic.index_of(0)) @@ -1304,10 +1558,16 @@ def test_single_resampling( * ancestor_values[ancestor_position] ) target_values[target_position] = acc - if all(np.isclose(target_values, result)): - f.write("fine # ") - else: - f.write("math # ") + if resample_possible: + if all(np.isclose(target_values, result)): + f.write("fine # ") + else: + f.write("math # ") + if resample_into_possible: + if all(np.isclose(target_values, result_into, equal_nan=True)): + f.write("fine # ") + else: + f.write("math # ") ancestor_values[:] = np.nan