Coverage for pyrc\postprocessing\parser.py: 27%
360 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-29 15:57 +0200
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-29 15:57 +0200
1# -------------------------------------------------------------------------------
2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan
3# ------------------------------------------------------------------------------
4# License
5# This file is part of PyRC, distributed under GPL-3.0-or-later.
6# ------------------------------------------------------------------------------
8import time
9from abc import abstractmethod
10from collections.abc import Callable
11from copy import copy
12from datetime import datetime, timedelta
13from typing import Any
15import numpy as np
17import gc
18import os
20from pyrc.core.components.capacitor import Capacitor
21from pyrc.core.network import RCNetwork
22from pyrc.core.nodes import Node
23from pyrc.core.settings import Settings
24from pyrc.core.components.templates import RCSolution
25from pyrc.dataHandler.weather import WeatherData
26from pyrc.tools.science import get_free_ram
27from pyrc.visualization.plot import seconds_to_dates
30def parse_direction(direction: np.ndarray | str) -> np.ndarray:
31 """
32 Makes one direction vector out of the input.
34 Parameters
35 ----------
36 direction
38 Returns
39 -------
40 np.ndarray :
41 The corresponding array for the string (or numpy array).
42 """
43 if isinstance(direction, str):
44 sign = 1
45 if len(direction) == 2:
46 if direction[0] == "-":
47 sign = -1
48 direction = direction[1]
49 assert len(direction) == 1
50 match direction.lower():
51 case "x":
52 direction = np.array((1, 0, 0))
53 case "y":
54 direction = np.array((0, 1, 0))
55 case "z":
56 direction = np.array((0, 0, 1))
57 case _:
58 raise ValueError("Invalid direction input.")
59 direction = sign * direction
60 return direction
63class Filter:
64 @abstractmethod
65 def apply_filter(self, matrix: np.ndarray, axis=None) -> np.ndarray:
66 pass
69class FilterMixin(Filter):
70 def __init__(self, values: list | np.ndarray, settings: Settings):
71 self.network_settings = settings
72 if isinstance(values, list):
73 values = np.array(values)
74 self.values: np.ndarray = values.flatten()
75 self.number = self.values.shape[0]
76 self.mask = np.full(self.number, False, dtype=bool)
78 @abstractmethod
79 def __copy__(self):
80 return FilterMixin(self.values, self.network_settings)
82 def invert(self):
83 """
84 Inverts the mask of the given axis.
85 """
86 self.mask = np.invert(self.mask)
88 def _add_mask(self, mask):
89 """
90 Adds the mask to the current mask. So True + False = True
92 Parameters
93 ----------
94 mask : np.ndarray
95 The mask to add.
96 Where True the current mask is also set to True.
97 """
98 self._apply_mask(mask, add=True)
100 def _subtract_mask(self, mask):
101 """
102 Subtract the mask from the current mask. So True + False = False
104 Parameters
105 ----------
106 mask : np.ndarray
107 The mask to add.
108 Where False the current mask is also set to False.
109 """
110 self._apply_mask(mask, add=False)
112 def _apply_mask(self, mask_vector: np.ndarray, add=True):
113 """
114 Applies the provided boolean mask array to self.mask_row or self.mask_col using logical and/or (&/|).
116 Parameters
117 ----------
118 mask_vector : np.ndarray
119 The mask to add.
120 add : bool, optional
121 Whether to add the boolean mask array or subtract it.
122 Add: current | mask_vector
123 Not add: current & mask_vector
125 """
126 mask_vector = mask_vector.reshape(
127 -1,
128 )
129 assert mask_vector.shape[0] == self.number
130 if add:
131 self.mask = mask_vector | self.mask
132 else:
133 self.mask = mask_vector & self.mask
135 def apply_filter(self, matrix: np.ndarray, axis=None) -> np.ndarray:
136 """
137 Returns the filtered matrix.
139 This does not save any data to the class so the RAM usage is not affected.
141 Parameters
142 ----------
143 matrix : np.ndarray
144 The matrix to be filtered.
145 axis : int, optional
146 If 0 the mask is applied to the row mask, to the column mask either.
147 If None, it is added to the mask of same length.
149 Returns
150 -------
151 np.ndarray :
152 The filtered matrix.
153 """
154 if axis is None:
155 if matrix.shape[0] == self.number:
156 axis = 0
157 elif matrix.shape[1] == self.number:
158 axis = 1
159 else:
160 raise ValueError("Length of mask_vector must match one of the dimensions of rows or columns.")
161 if axis == 0:
162 assert matrix.shape[0] == self.number
163 return matrix[self.mask]
164 else:
165 assert matrix.shape[1] == self.number
166 return matrix[:, self.mask]
168 def __add__(self, other):
169 import copy
171 result = copy.copy(self)
172 result._add_mask(other.mask)
173 return result
175 @property
176 def filtered_values(self) -> np.ndarray:
177 return self.values[self.mask]
180class NodeFilter(FilterMixin):
181 def __init__(self, nodes: list[Capacitor] | np.ndarray, settings: Settings):
182 """
183 Initially: filter out everything.
185 Parameters
186 ----------
187 nodes : list[Capacitor] | np.ndarray
188 The nodes which solutions are represented in the columns.
189 settings: Settings
190 The `Settings` object that matches the settings of the network.
191 Is used to get the start_date and the weather_data_path
192 """
193 super().__init__(values=nodes, settings=settings)
195 def __copy__(self):
196 return NodeFilter(self.values, self.network_settings)
198 def add_nodes(self, nodes: list[Node] | str):
199 """
200 Adds the nodes to the current node mask. If string is given, the corresponding group is added.
202 Parameters
203 ----------
204 nodes : list[Node] | Node | np.ndarray
205 The list with the node objects.
206 """
207 if isinstance(nodes, Node):
208 nodes = [nodes]
209 elif isinstance(nodes, np.ndarray):
210 nodes = nodes.tolist()
211 assert isinstance(nodes, list)
212 indices = [node.index for node in nodes]
213 self.mask[indices] = True
215 def subtract_nodes(self, nodes: list[Node] | str):
216 """
217 Subtract the nodes to the current node mask. If string is given, the corresponding group is subtracted.
219 Parameters
220 ----------
221 nodes : list[Capacitor] | Node | np.ndarray
222 The list with the node objects.
223 """
224 if isinstance(nodes, Node):
225 nodes = [nodes]
226 elif isinstance(nodes, np.ndarray):
227 nodes = nodes.tolist()
228 indices = [node.index for node in nodes]
229 self.mask[indices] = False
231 def apply_filter(self, matrix: np.ndarray, axis=1) -> np.ndarray:
232 return super().apply_filter(matrix, axis)
235class WeatherFilter(FilterMixin):
236 def __init__(self, settings: Settings):
237 # TODO: create datetime vector and pass it to super init
238 super().__init__(values=[], settings=settings)
239 self._weather = None
241 def __copy__(self):
242 result = WeatherFilter(self.network_settings)
243 result._weather = self._weather
244 return result
246 @property
247 def weather(self) -> WeatherData:
248 if self._weather is None:
249 self._weather = self.network_settings.weather_data
250 return self._weather
253class TimeFilter(FilterMixin):
254 """
255 A class that contains the filter for one RCSolution, especially nodes (columns) and dates (rows).
256 """
258 def __init__(self, seconds: np.ndarray, settings: Settings, time_accuracy="ms", initial_mask_value=False):
259 """
260 Initially: filter out everything.
262 Parameters
263 ----------
264 seconds : np.ndarray
265 The row index as increasing seconds.
266 settings : Settings
267 The `Settings` object that matches the settings of the network.
268 Is used to get the start_date and the weather_data_path
269 time_accuracy : str, optional
270 The time accuracy used in numpy.datetime64 calculations. E.g.: "ms", "s", "m"
271 """
272 self.time_accuracy = time_accuracy
273 time_mult = np.timedelta64(1, "s") / np.timedelta64(1, time_accuracy)
274 values: np.ndarray = (
275 np.datetime64(settings.start_date) + np.timedelta64(1, time_accuracy) * np.array(seconds) * time_mult
276 )
277 super().__init__(values=values, settings=settings)
278 if initial_mask_value:
279 self.invert()
281 def __copy__(self):
282 result: TimeFilter = type(self).__new__(type(self))
283 # Copy all attributes from parent class
284 super(TimeFilter, result).__init__(self.values, self.network_settings)
285 result.mask = self.mask.copy()
286 # Copy specific attributes from this class
287 result.time_accuracy = self.time_accuracy
288 # Copy any other attributes you have
289 return result
291 @property
292 def datetime(self):
293 """
294 Returns the date times from filtered values as vector with datetime.datetime objects (instead of np.datetime64).
296 Returns
297 -------
298 np.ndarray(datetime.datetime) :
299 Date times of filtered values.
300 """
301 filtered_values = self.values[self.mask]
302 return np.array([dt.astype(datetime) for dt in filtered_values])
304 def daterange(self, datetime1, datetime2=None):
305 """
306 Filters rows using a datetime range. The current row mask is overwritten.
308 If datetime2 is None, the same day is used as end of the range.
309 If datetime2 is not None, the exact datetime is used as end (included). So if you want the same result as
310 with "None" then you have to use datetime1 + np.timedelta64(1, "D").
312 Parameters
313 ----------
314 datetime1 : np.datetime64 | datetime.datetime | Any
315 The start of the range. Is included in the range.
316 Is converted to np.datetime64.
317 datetime2 : np.datetime64 | datetime.datetime | Any, optional
318 The end of the range. Is included in the range (but with time. So if you want the whole day you have to
319 use the next day at 00:00:00).
320 If None, the same day as datetime1 is used as end of the range.
321 Is converted to np.datetime64.
323 Examples
324 --------
325 To apply a range of three days:
326 >>> self.range(datetime(2022,4,1), "2022-04-03")
327 which will result in the range 1.4.22 00:00:00 up to 4.4.22 00:00:00.
328 """
329 datetime1 = np.datetime64(datetime1)
330 if datetime2 is None:
331 datetime2 = datetime1.astype("datetime64[D]") + np.timedelta64(1, "D")
332 else:
333 datetime2 = np.datetime64(datetime2)
334 # if datetime2 - datetime2.astype("datetime64[D]") == 0:
335 # # only day is given, no hours/minutes/seconds
336 # datetime2 = datetime2.astype("datetime64[D]") + np.timedelta64(1, "D")
338 mask = (self.values >= datetime1.astype(f"datetime64[{self.time_accuracy}]")) & (
339 self.values <= datetime2.astype(f"datetime64[{self.time_accuracy}]")
340 )
342 self._add_mask(mask)
344 def apply_filter(self, matrix: np.ndarray, axis=0) -> np.ndarray:
345 return super().apply_filter(matrix, axis)
348class NetworkFilter(Filter):
349 """
350 Combines a TimeFilter for the row and a NodeFilter for the columns to one filter.
351 """
353 def __init__(self, seconds: np.ndarray, nodes: list[Capacitor], settings: Settings, time_accuracy="ms"):
354 """
355 Initially: filter out everything.
357 Parameters
358 ----------
359 seconds : np.ndarray
360 The row index as increasing seconds.
361 nodes : list[Capacitor]
362 The nodes which solutions are represented in the columns.
363 settings: Settings
364 The `Settings` object that matches the settings of the network.
365 Is used to get the start_date and the weather_data_path
366 """
367 self.number_rows = len(seconds.flatten())
368 self.number_columns = len(nodes)
369 self.network_settings: Settings = settings
371 self.filter_row: TimeFilter = TimeFilter(seconds, self.network_settings, time_accuracy)
372 self.filter_column: NodeFilter = NodeFilter(nodes, self.network_settings)
373 self.filter_column.invert() # activate all nodes initially
375 def apply_filter(self, matrix, axis=None):
376 if axis is None:
377 # For maximum performance always filter columns first and then rows! NumPy arrays use row-major (C-style)
378 # memory layout by default.
379 column_filtered = self.apply_column_filter(matrix)
380 return self.apply_row_filter(column_filtered)
381 elif axis == 0:
382 return self.apply_row_filter(matrix)
383 else:
384 return self.apply_column_filter(matrix)
386 def apply_row_filter(self, matrix):
387 return self.filter_row.apply_filter(matrix)
389 def apply_column_filter(self, matrix):
390 return self.filter_column.apply_filter(matrix)
393class FilteredRCSolution:
394 def __init__(self, rc_solution: RCSolution, filter_obj: Filter):
395 self._rc_solution: RCSolution = rc_solution
396 self.filter: Filter = filter_obj
398 def __getattr__(self, item):
399 """
400 Returns the attribute from RCSolution. But for some attributes it returns the filtered version.
401 """
402 attr = getattr(self._rc_solution, item)
404 if item in ["result_vectors", "temperature_vectors", "y"]:
405 attr = self.filter.apply_filter(attr)
406 elif item in ["t", "time_steps", "input_vectors"]:
407 if isinstance(self.filter, TimeFilter):
408 attr = self.filter.apply_filter(attr)
409 elif isinstance(self.filter, NetworkFilter):
410 attr = self.filter.apply_row_filter(attr)
411 return attr
414class FastParser:
415 """
416 Class to process the solutions of an RC-network.
418 Here all calculations for a single RC-network are performed. Also, this class should make filtering easy and the
419 processing fast without a lot of RAM usage. For this, the calculation should be done in a queue and after this
420 the network solution is removed from the memory to free RAM and only the requested calculation/solution data is
421 kept in memory.
423 To compare several RCNetwork Solutions use the class `MultiParser` which processes multiple FastParser instances.
424 """
426 _total_reserved_memory = 0 # in bytes
428 def __init__(self, network_solution_path_tuple: tuple[RCNetwork, str], solution_size=None):
429 self.network = network_solution_path_tuple[0]
430 self.solution_path = network_solution_path_tuple[1] # the pickle file of the solution containing the RCSolution
431 self._solution_size = solution_size
433 self._blocked_ram = 0
435 self._filters: list[NetworkFilter | TimeFilter | NodeFilter] = []
436 self._filter_names: list[str] = []
438 def __copy__(self):
439 result: FastParser = type(self).__new__(type(self))
440 result.network = self.network
441 result.solution_path = self.solution_path
442 result._solution_size = self._solution_size
443 result._blocked_ram = self._blocked_ram
445 result._filters = [copy(f) for f in self._filters]
446 result._filter_names = self._filter_names
447 return result
449 def __parse_filter_index(self, entry):
450 if isinstance(entry, str):
451 entry = self._filter_names.index(entry)
452 return entry
454 @property
455 def result_vectors(self):
456 if not self.solution_exist:
457 self.load_solution_safe()
458 return self.network.rc_solution.result_vectors
460 @property
461 def time_vector(self):
462 if not self.solution_exist:
463 self.load_solution_safe()
464 return np.array(
465 seconds_to_dates(self.network.rc_solution.time_steps, self.network.settings.weather_data.start_time)
466 )
468 @property
469 def input_vectors(self):
470 if not self.solution_exist:
471 self.load_solution_safe()
472 return self.network.rc_solution.input_vectors
474 @property
475 def filters(self) -> list[NetworkFilter | TimeFilter | NodeFilter]:
476 return self._filters
478 @property
479 def time_filters(self) -> list[TimeFilter]:
480 return [f for f in self.filters if isinstance(f, TimeFilter)]
482 @property
483 def network_filter(self) -> list[NetworkFilter]:
484 return [f for f in self.filters if isinstance(f, NetworkFilter)]
486 @property
487 def node_filter(self) -> list[NodeFilter]:
488 return [f for f in self.filters if isinstance(f, NodeFilter)]
490 def filter(self, entry: int | Any | str = -1) -> NetworkFilter | TimeFilter | NodeFilter:
491 """
492 Returns a `Filter` object specified by entry. If entry is not given the last `Filter` is used.
494 Parameters
495 ----------
496 entry : int | Any, optional
497 If an int the index of the filter in the filter list self._filter.
498 If a string the name of the filter in self._filter_names. Is parsed to an index.
499 If None, the last `Filter` is used.
500 """
501 return self._filters[self.__parse_filter_index(entry)]
503 def _add_filter(self, filter_class: type, name: str = None):
504 """
505 Adds a new `Filter` object. It initially filters out everything (empty matrix).
507 The `Filter` objects are used to create different sets of data using the same data source. You can
508 manipulate the filter/mask using the methods of the `Filter` class.
510 Parameters
511 ----------
512 filter_class : type
513 The Filter class to be added to self._filters
514 name : str, optional
515 The name of the filter to add.
516 If None, the filter is only accessible by its index.
518 Returns
519 -------
520 int :
521 The index of the just added `Filter` object that can be used to get the filter using
522 self.filter(index)
523 """
524 if not self.solution_exist:
525 self.load_solution_safe()
526 assert self.network.rc_solution.exist
527 kwargs = {"settings": self.network.settings}
528 if filter_class is TimeFilter or filter_class is NetworkFilter:
529 kwargs["seconds"] = self.network.rc_solution.time_steps
530 if filter_class is NodeFilter or filter_class is NetworkFilter:
531 kwargs["nodes"] = self.network.nodes
532 self._filters.append(filter_class(**kwargs))
533 self._filter_names.append(name)
534 return len(self.filters) - 1
536 def add_filter(self, name: str = None, return_index=False):
537 """
538 Adds a new `NetworkFilter` object. It initially filters out everything (empty matrix).
540 The `NetworkFilter` objects are used to create different sets of data using the same data source. You can
541 manipulate the filter/mask using the methods of the `NetworkFilter` class.
543 Parameters
544 ----------
545 name : str, optional
546 The name of the filter to add.
547 If None, the filter is only accessible by its index.
548 return_index : bool, optional
549 If True the index of the added `NetworkFilter` is returned.
551 Returns
552 -------
553 None | int :
554 If return_index: the index of the just added `NetworkFilter` object that can be used to get the filter using
555 self.filter(index)
556 """
557 result = self._add_filter(NetworkFilter, name)
558 if return_index:
559 return result
561 def add_time_filter(self, name: str = None, return_index=False):
562 """
563 Adds a new `TimeFilter` object. It initially filters out everything (empty matrix).
565 The `TimeFilter` objects are used to create different sets of data using the same data source. You can
566 manipulate the filter/mask using the methods of the `TimeFilter` class.
568 Parameters
569 ----------
570 name : str, optional
571 The name of the filter to add.
572 If None, the filter is only accessible by its index.
573 return_index : bool, optional
574 If True the index of the added `TimeFilter` is returned.
576 Returns
577 -------
578 None | int :
579 If return_index: the index of the just added `TimeFilter` object that can be used to get the filter using
580 self.filter(index)
581 """
582 result = self._add_filter(TimeFilter, name)
583 if return_index:
584 return result
586 def add_node_filter(self, name: str = None, return_index=False):
587 """
588 Adds a new `NodeFilter` object. It initially filters out everything (empty matrix).
590 The `NodeFilter` objects are used to create different sets of data using the same data source. You can
591 manipulate the filter/mask using the methods of the `NodeFilter` class.
593 Parameters
594 ----------
595 name : str, optional
596 The name of the filter to add.
597 If None, the filter is only accessible by its index.
598 return_index : bool, optional
599 If True the index of the added `NodeFilter` is returned.
601 Returns
602 -------
603 None | int :
604 If return_index: the index of the just added `NodeFilter` object that can be used to get the filter using
605 self.filter(index)
606 """
607 result = self._add_filter(NodeFilter, name)
608 if return_index:
609 return result
611 def remove_filter(self, entry: int | Any = -1):
612 """
613 Removes the desired filter from the filters list.
615 Remember: Previously passed filter indexes might change.
617 Parameters
618 ----------
619 entry : int | Any, optional
620 If an int the index of the filter in the filter list self._filter.
621 If a string the name of the filter in self._filter_names. Is parsed to an index.
622 If None, the last filter is used.
623 """
624 index = self.__parse_filter_index(entry)
625 for l in [self._filters, self._filter_names]:
626 l.pop(index)
628 def add_time_filters(
629 self, days=None, weeks=None, months=None, years=None, filter_name_add_on="", return_names=False
630 ):
631 """
632 Adds time filters for all passed days, weeks, months and years.
634 The time filters are named like "day{index of this day in list}{filter_name_add_on}".
635 The weeks, months and years are represented by their start day.
637 If no value is passed no filter is created.
639 Parameters
640 ----------
641 days : list[datetime] | datetime, optional
642 The days that should be plotted.
643 weeks : list[datetime] | datetime, optional
644 The weeks that should be plotted.
645 Each week is represented by its start date.
646 months : list[datetime] | datetime, optional
647 The months that should be plotted.
648 Each month is represented by its start date.
649 years : list[datetime] | datetime, optional
650 The years that should be plotted.
651 Each year is represented by its start date.
652 filter_name_add_on : str, optional
653 A name add-on for the time filter that is added.
654 return_names : bool, optional
655 If True the names of the time filters are returned as dictionary with the entries days, weeks,
656 months and years and the corresponding list.
658 Returns
659 -------
660 dict | None :
661 If return_names, a dict with the layout:
662 {"days": [], "weeks": [], "months": [], "years": []}
663 with the names of the filters in the lists.
664 """
665 import calendar
667 names = {
668 "days": days or [],
669 "weeks": weeks or [],
670 "months": months or [],
671 "years": years or [],
672 }
673 for i, day in enumerate(days):
674 names["days"].append(f"day{i}{filter_name_add_on}")
675 self.add_time_filter(names["days"][-1])
676 time_filter: TimeFilter = self.filter(names["days"][-1])
677 time_filter.daterange(datetime1=day)
678 for i, week in enumerate(weeks):
679 names["weeks"].append(f"week{i}{filter_name_add_on}")
680 self.add_time_filter(names["weeks"][-1])
681 time_filter: TimeFilter = self.filter(names["weeks"][-1])
682 time_filter.daterange(datetime1=week, datetime2=week + timedelta(days=7))
683 for i, dt in enumerate(months):
684 names["months"].append(f"month{i}{filter_name_add_on}")
685 self.add_time_filter(names["months"][-1])
686 time_filter: TimeFilter = self.filter(names["months"][-1])
687 if dt.month == 12:
688 next_year = dt.year + 1
689 next_month = 1
690 else:
691 next_year = dt.year
692 next_month = dt.month + 1
694 max_day = calendar.monthrange(next_year, next_month)[1]
695 next_day = min(dt.day, max_day)
697 datetime2 = dt.replace(year=next_year, month=next_month, day=next_day)
698 time_filter.daterange(datetime1=dt, datetime2=datetime2)
699 for i, dt in enumerate(years):
700 names["years"].append(f"year{i}{filter_name_add_on}")
701 self.add_time_filter(names["years"][-1])
702 time_filter: TimeFilter = self.filter(names["years"][-1])
703 # Handle leap year edge case for Feb 29
704 if dt.month == 2 and dt.day == 29 and not calendar.isleap(dt.year + 1):
705 new_dt = dt.replace(year=dt.year + 1, month=3, day=1)
706 else:
707 new_dt = dt.replace(year=dt.year + 1)
708 time_filter.daterange(datetime1=dt, datetime2=new_dt)
709 if return_names:
710 return names
712 def _block_memory(self):
713 self._blocked_ram = self.solution_size * 1.01
714 FastParser._total_reserved_memory += self.solution_size
716 def _free_memory(self):
717 FastParser._total_reserved_memory -= self._blocked_ram
718 self._blocked_ram = 0
720 @property
721 def solution_exist(self) -> bool:
722 return self.network.rc_solution.exist
724 @property
725 def solution_size(self):
726 if self._solution_size is None:
727 if os.path.isfile(self.solution_path):
728 self._solution_size = os.path.getsize(self.solution_path)
729 else:
730 print(f"The solution size is estimated to be 10 GB ({self.solution_path})")
731 self._solution_size = 10 * 1024**3
732 return self._solution_size
734 def load_solution(self):
735 """
736 Loads solution, but only if enough RAM is available.
738 Raises
739 ------
740 MemoryError :
741 If not enough memory is available.
742 """
743 if (get_free_ram() - FastParser._total_reserved_memory) > self.solution_size:
744 self._block_memory()
745 assert self.network.rc_solution.load_solution(self.solution_path), (
746 f"Solution file {self.solution_path} not found"
747 )
748 self._free_memory()
749 else:
750 raise MemoryError("Not enough free memory to load solution.")
752 def load_solution_safe(self):
753 """
754 Like load_solution, but it waits for up to 1 hour for enough RAM.
756 Raises
757 ------
758 MemoryError :
759 If not enough memory is available within 1 hour.
760 """
761 counter = 0
762 success = False
763 while counter <= 3600:
764 if (get_free_ram() - FastParser._total_reserved_memory) > self.solution_size:
765 self.load_solution()
766 success = True
767 break
768 time.sleep(1)
769 counter += 1
770 if not success:
771 raise MemoryError("Not enough free memory to load solution.")
773 def free_ram(self):
774 """
775 Deletes the network solution from memory without deleting any calculated/filtered/requested data.
777 #TODO: Append this function with all variables that are existing in the state of this class containing the
778 whole solution data. The garbage collection has to be able to free the RAM from the most data!
779 """
780 self.network.rc_solution.delete_solutions(True)
782 # garbage collection
783 gc.collect()
785 def map(self, function: Callable, *args, **kwargs):
786 """
787 Maps the passed function to every filter and returns the result as a tuple.
789 Parameters
790 ----------
791 function : Callable
792 The function to map on every filter. The first argument is a FilteredRCSolution.
794 Returns
795 -------
796 tuple :
797 The results for each filter.
798 """
799 result = []
800 for filter_obj in self.filters:
801 filtered_solution = FilteredRCSolution(self.network.rc_solution, filter_obj)
802 result.append(function(filtered_solution, *args, **kwargs))
803 return tuple(result)
806class MultiParser:
807 """
808 Class to process multiple solutions of an RC-network (FastParser instances).
810 It behaves like FastParser, but it always executes all calls to every parser instance defined in the init.
812 This should be used with brain!
813 Only compare networks with same hashes or with comparable layout.
814 """
816 def __init__(self, objects: list):
817 """
818 Objects can be a list of FastParser instances or network solution-path tuples.
820 Parameters
821 ----------
822 objects : list[FastParser] | list[tuple[RCNetwork, str]]
823 These objects are compared to each other (same calculations are done for all of them).
824 """
825 assert isinstance(objects, list)
826 self.parsers: list[FastParser] = []
827 for obj in objects:
828 if isinstance(obj, FastParser):
829 self.parsers.append(obj)
830 elif isinstance(obj, tuple):
831 obj: tuple[RCNetwork, str]
832 self.parsers.append(FastParser(obj))
833 else:
834 raise TypeError(f"Object {obj} is not a FastParser instance or a tuple to create it.")
836 def __getattr__(self, name):
837 def multi_method(*args, **kwargs):
838 results = []
839 for parser in self.parsers:
840 attr = getattr(parser, name)
841 if callable(attr):
842 results.append(attr(*args, **kwargs))
843 else:
844 results.append(attr)
845 return results
847 first_attr = getattr(self.parsers[0], name)
848 if callable(first_attr):
849 return multi_method
850 else:
851 return [getattr(parser, name) for parser in self.parsers]