Coverage for pyrc \ postprocessing \ parser.py: 27%
360 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +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):
71 def __init__(self,
72 values: list | np.ndarray,
73 settings: Settings):
74 self.network_settings = settings
75 if isinstance(values, list):
76 values = np.array(values)
77 self.values: np.ndarray = values.flatten()
78 self.number = self.values.shape[0]
79 self.mask = np.full(self.number, False, dtype=bool)
81 @abstractmethod
82 def __copy__(self):
83 return FilterMixin(self.values, self.network_settings)
85 def invert(self):
86 """
87 Inverts the mask of the given axis.
88 """
89 self.mask = np.invert(self.mask)
91 def _add_mask(self, mask):
92 """
93 Adds the mask to the current mask. So True + False = True
95 Parameters
96 ----------
97 mask : np.ndarray
98 The mask to add.
99 Where True the current mask is also set to True.
100 """
101 self._apply_mask(mask, add=True)
103 def _subtract_mask(self, mask):
104 """
105 Subtract the mask from the current mask. So True + False = False
107 Parameters
108 ----------
109 mask : np.ndarray
110 The mask to add.
111 Where False the current mask is also set to False.
112 """
113 self._apply_mask(mask, add=False)
115 def _apply_mask(self, mask_vector: np.ndarray, add=True):
116 """
117 Applies the provided boolean mask array to self.mask_row or self.mask_col using logical and/or (&/|).
119 Parameters
120 ----------
121 mask_vector : np.ndarray
122 The mask to add.
123 add : bool, optional
124 Whether to add the boolean mask array or subtract it.
125 Add: current | mask_vector
126 Not add: current & mask_vector
128 """
129 mask_vector = mask_vector.reshape(-1, )
130 assert mask_vector.shape[0] == self.number
131 if add:
132 self.mask = mask_vector | self.mask
133 else:
134 self.mask = mask_vector & self.mask
136 def apply_filter(self, matrix: np.ndarray, axis=None) -> np.ndarray:
137 """
138 Returns the filtered matrix.
140 This does not save any data to the class so the RAM usage is not affected.
142 Parameters
143 ----------
144 matrix : np.ndarray
145 The matrix to be filtered.
146 axis : int, optional
147 If 0 the mask is applied to the row mask, to the column mask either.
148 If None, it is added to the mask of same length.
150 Returns
151 -------
152 np.ndarray :
153 The filtered matrix.
154 """
155 if axis is None:
156 if matrix.shape[0] == self.number:
157 axis = 0
158 elif matrix.shape[1] == self.number:
159 axis = 1
160 else:
161 raise ValueError("Length of mask_vector must match one of the dimensions of rows or columns.")
162 if axis == 0:
163 assert matrix.shape[0] == self.number
164 return matrix[self.mask]
165 else:
166 assert matrix.shape[1] == self.number
167 return matrix[:, self.mask]
169 def __add__(self, other):
170 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):
182 def __init__(self,
183 nodes: list[Capacitor] | np.ndarray,
184 settings: Settings):
185 """
186 Initially: filter out everything.
188 Parameters
189 ----------
190 nodes : list[Capacitor] | np.ndarray
191 The nodes which solutions are represented in the columns.
192 settings: Settings
193 The ``Settings`` object that matches the settings of the network.
194 Is used to get the start_date and the weather_data_path
195 """
196 super().__init__(values=nodes, settings=settings)
198 def __copy__(self):
199 return NodeFilter(self.values, self.network_settings)
201 def add_nodes(self, nodes: list[Node] | str):
202 """
203 Adds the nodes to the current node mask. If string is given, the corresponding group is added.
205 Parameters
206 ----------
207 nodes : list[Node] | Node | np.ndarray
208 The list with the node objects.
209 """
210 if isinstance(nodes, Node):
211 nodes = [nodes]
212 elif isinstance(nodes, np.ndarray):
213 nodes = nodes.tolist()
214 assert isinstance(nodes, list)
215 indices = [node.index for node in nodes]
216 self.mask[indices] = True
218 def subtract_nodes(self, nodes: list[Node] | str):
219 """
220 Subtract the nodes to the current node mask. If string is given, the corresponding group is subtracted.
222 Parameters
223 ----------
224 nodes : list[Capacitor] | Node | np.ndarray
225 The list with the node objects.
226 """
227 if isinstance(nodes, Node):
228 nodes = [nodes]
229 elif isinstance(nodes, np.ndarray):
230 nodes = nodes.tolist()
231 indices = [node.index for node in nodes]
232 self.mask[indices] = False
234 def apply_filter(self, matrix: np.ndarray, axis=1) -> np.ndarray:
235 return super().apply_filter(matrix, axis)
238class WeatherFilter(FilterMixin):
240 def __init__(self,
241 settings: Settings):
242 # TODO: create datetime vector and pass it to super init
243 super().__init__(values=[], settings=settings)
244 self._weather = None
246 def __copy__(self):
247 result = WeatherFilter(self.network_settings)
248 result._weather = self._weather
249 return result
251 @property
252 def weather(self) -> WeatherData:
253 if self._weather is None:
254 self._weather = self.network_settings.weather_data
255 return self._weather
258class TimeFilter(FilterMixin):
259 """
260 A class that contains the filter for one RCSolution, especially nodes (columns) and dates (rows).
261 """
263 def __init__(self,
264 seconds: np.ndarray,
265 settings: Settings,
266 time_accuracy="ms",
267 initial_mask_value=False):
268 """
269 Initially: filter out everything.
271 Parameters
272 ----------
273 seconds : np.ndarray
274 The row index as increasing seconds.
275 settings : Settings
276 The ``Settings`` object that matches the settings of the network.
277 Is used to get the start_date and the weather_data_path
278 time_accuracy : str, optional
279 The time accuracy used in numpy.datetime64 calculations. E.g.: "ms", "s", "m"
280 """
281 self.time_accuracy = time_accuracy
282 time_mult = np.timedelta64(1, "s") / np.timedelta64(1, time_accuracy)
283 values: np.ndarray = (np.datetime64(settings.start_date)
284 + np.timedelta64(1, time_accuracy) * np.array(seconds) * time_mult)
285 super().__init__(values=values, settings=settings)
286 if initial_mask_value:
287 self.invert()
289 def __copy__(self):
290 result: TimeFilter = type(self).__new__(type(self))
291 # Copy all attributes from parent class
292 super(TimeFilter, result).__init__(self.values, self.network_settings)
293 result.mask = self.mask.copy()
294 # Copy specific attributes from this class
295 result.time_accuracy = self.time_accuracy
296 # Copy any other attributes you have
297 return result
299 @property
300 def datetime(self):
301 """
302 Returns the date times from filtered values as vector with datetime.datetime objects (instead of np.datetime64).
304 Returns
305 -------
306 np.ndarray(datetime.datetime) :
307 Date times of filtered values.
308 """
309 filtered_values = self.values[self.mask]
310 return np.array([dt.astype(datetime) for dt in filtered_values])
312 def daterange(self, datetime1, datetime2=None):
313 """
314 Filters rows using a datetime range. The current row mask is overwritten.
316 If datetime2 is None, the same day is used as end of the range.
317 If datetime2 is not None, the exact datetime is used as end (included). So if you want the same result as
318 with "None" then you have to use datetime1 + np.timedelta64(1, "D").
320 Parameters
321 ----------
322 datetime1 : np.datetime64 | datetime.datetime | Any
323 The start of the range. Is included in the range.
324 Is converted to np.datetime64.
325 datetime2 : np.datetime64 | datetime.datetime | Any, optional
326 The end of the range. Is included in the range (but with time. So if you want the whole day you have to
327 use the next day at 00:00:00).
328 If None, the same day as datetime1 is used as end of the range.
329 Is converted to np.datetime64.
331 Examples
332 --------
333 To apply a range of three days:
334 self.range(datetime(2022,4,1), "2022-04-03")
335 which will result in the range 1.4.22 00:00:00 up to 4.4.22 00:00:00.
336 """
337 datetime1 = np.datetime64(datetime1)
338 if datetime2 is None:
339 datetime2 = datetime1.astype("datetime64[D]") + np.timedelta64(1, "D")
340 else:
341 datetime2 = np.datetime64(datetime2)
342 # if datetime2 - datetime2.astype("datetime64[D]") == 0:
343 # # only day is given, no hours/minutes/seconds
344 # datetime2 = datetime2.astype("datetime64[D]") + np.timedelta64(1, "D")
346 mask = ((self.values >= datetime1.astype(f"datetime64[{self.time_accuracy}]"))
347 & (self.values <= datetime2.astype(f"datetime64[{self.time_accuracy}]")))
349 self._add_mask(mask)
351 def apply_filter(self, matrix: np.ndarray, axis=0) -> np.ndarray:
352 return super().apply_filter(matrix, axis)
355class NetworkFilter(Filter):
356 """
357 Combines a TimeFilter for the row and a NodeFilter for the columns to one filter.
358 """
360 def __init__(self,
361 seconds: np.ndarray,
362 nodes: list[Capacitor],
363 settings: Settings,
364 time_accuracy="ms"):
365 """
366 Initially: filter out everything.
368 Parameters
369 ----------
370 seconds : np.ndarray
371 The row index as increasing seconds.
372 nodes : list[Capacitor]
373 The nodes which solutions are represented in the columns.
374 settings: Settings
375 The ``Settings`` object that matches the settings of the network.
376 Is used to get the start_date and the weather_data_path
377 """
378 self.number_rows = len(seconds.flatten())
379 self.number_columns = len(nodes)
380 self.network_settings: Settings = settings
382 self.filter_row: TimeFilter = TimeFilter(seconds, self.network_settings, time_accuracy)
383 self.filter_column: NodeFilter = NodeFilter(nodes, self.network_settings)
384 self.filter_column.invert() # activate all nodes initially
386 def apply_filter(self, matrix, axis=None):
387 if axis is None:
388 # For maximum performance always filter columns first and then rows! NumPy arrays use row-major (C-style)
389 # memory layout by default.
390 column_filtered = self.apply_column_filter(matrix)
391 return self.apply_row_filter(column_filtered)
392 elif axis == 0:
393 return self.apply_row_filter(matrix)
394 else:
395 return self.apply_column_filter(matrix)
397 def apply_row_filter(self, matrix):
398 return self.filter_row.apply_filter(matrix)
400 def apply_column_filter(self, matrix):
401 return self.filter_column.apply_filter(matrix)
404class FilteredRCSolution:
406 def __init__(self, rc_solution: RCSolution, filter_obj: Filter):
407 self._rc_solution: RCSolution = rc_solution
408 self.filter: Filter = filter_obj
410 def __getattr__(self, item):
411 """
412 Returns the attribute from RCSolution. But for some attributes it returns the filtered version.
413 """
414 attr = getattr(self._rc_solution, item)
416 if item in [
417 "result_vectors",
418 "temperature_vectors",
419 "y"
420 ]:
421 attr = self.filter.apply_filter(attr)
422 elif item in [
423 "t",
424 "time_steps",
425 "input_vectors"
426 ]:
427 if isinstance(self.filter, TimeFilter):
428 attr = self.filter.apply_filter(attr)
429 elif isinstance(self.filter, NetworkFilter):
430 attr = self.filter.apply_row_filter(attr)
431 return attr
434class FastParser:
435 """
436 Class to process the solutions of an RC-network.
438 Here all calculations for a single RC-network are performed. Also, this class should make filtering easy and the
439 processing fast without a lot of RAM usage. For this, the calculation should be done in a queue and after this
440 the network solution is removed from the memory to free RAM and only the requested calculation/solution data is
441 kept in memory.
443 To compare several RCNetwork Solutions use the class `MultiParser` which processes multiple FastParser instances.
444 """
446 _total_reserved_memory = 0 # in bytes
448 def __init__(self,
449 network_solution_path_tuple: tuple[RCNetwork, str],
450 solution_size=None):
451 self.network = network_solution_path_tuple[0]
452 self.solution_path = network_solution_path_tuple[1] # the pickle file of the solution containing the RCSolution
453 self._solution_size = solution_size
455 self._blocked_ram = 0
457 self._filters: list[NetworkFilter | TimeFilter | NodeFilter] = []
458 self._filter_names: list[str] = []
460 def __copy__(self):
461 result: FastParser = type(self).__new__(type(self))
462 result.network = self.network
463 result.solution_path = self.solution_path
464 result._solution_size = self._solution_size
465 result._blocked_ram = self._blocked_ram
467 result._filters = [copy(f) for f in self._filters]
468 result._filter_names = self._filter_names
469 return result
471 def __parse_filter_index(self, entry):
472 if isinstance(entry, str):
473 entry = self._filter_names.index(entry)
474 return entry
476 @property
477 def result_vectors(self):
478 if not self.solution_exist:
479 self.load_solution_safe()
480 return self.network.rc_solution.result_vectors
482 @property
483 def time_vector(self):
484 if not self.solution_exist:
485 self.load_solution_safe()
486 return np.array(seconds_to_dates(self.network.rc_solution.time_steps,
487 self.network.settings.weather_data.start_time))
489 @property
490 def input_vectors(self):
491 if not self.solution_exist:
492 self.load_solution_safe()
493 return self.network.rc_solution.input_vectors
495 @property
496 def filters(self) -> list[NetworkFilter | TimeFilter | NodeFilter]:
497 return self._filters
499 @property
500 def time_filters(self) -> list[TimeFilter]:
501 return [f for f in self.filters if isinstance(f, TimeFilter)]
503 @property
504 def network_filter(self) -> list[NetworkFilter]:
505 return [f for f in self.filters if isinstance(f, NetworkFilter)]
507 @property
508 def node_filter(self) -> list[NodeFilter]:
509 return [f for f in self.filters if isinstance(f, NodeFilter)]
511 def filter(self, entry: int | Any | str = -1) -> NetworkFilter | TimeFilter | NodeFilter:
512 """
513 Returns a `Filter` object specified by entry. If entry is not given the last `Filter` is used.
515 Parameters
516 ----------
517 entry : int | Any, optional
518 If an int the index of the filter in the filter list self._filter.
519 If a string the name of the filter in self._filter_names. Is parsed to an index.
520 If None, the last `Filter` is used.
521 """
522 return self._filters[self.__parse_filter_index(entry)]
524 def _add_filter(self, filter_class: type, name: str = None):
525 """
526 Adds a new `Filter` object. It initially filters out everything (empty matrix).
528 The `Filter` objects are used to create different sets of data using the same data source. You can
529 manipulate the filter/mask using the methods of the `Filter` class.
531 Parameters
532 ----------
533 filter_class : type
534 The Filter class to be added to self._filters
535 name : str, optional
536 The name of the filter to add.
537 If None, the filter is only accessible by its index.
539 Returns
540 -------
541 int :
542 The index of the just added `Filter` object that can be used to get the filter using
543 self.filter(index)
544 """
545 if not self.solution_exist:
546 self.load_solution_safe()
547 assert self.network.rc_solution.exist
548 kwargs = {"settings": self.network.settings}
549 if filter_class is TimeFilter or filter_class is NetworkFilter:
550 kwargs["seconds"] = self.network.rc_solution.time_steps
551 if filter_class is NodeFilter or filter_class is NetworkFilter:
552 kwargs["nodes"] = self.network.nodes
553 self._filters.append(filter_class(
554 **kwargs
555 ))
556 self._filter_names.append(name)
557 return len(self.filters) - 1
559 def add_filter(self, name: str = None, return_index=False):
560 """
561 Adds a new ``NetworkFilter`` object. It initially filters out everything (empty matrix).
563 The ``NetworkFilter`` objects are used to create different sets of data using the same data source. You can
564 manipulate the filter/mask using the methods of the ``NetworkFilter`` class.
566 Parameters
567 ----------
568 name : str, optional
569 The name of the filter to add.
570 If None, the filter is only accessible by its index.
571 return_index : bool, optional
572 If True the index of the added ``NetworkFilter`` is returned.
574 Returns
575 -------
576 None | int :
577 If return_index: the index of the just added ``NetworkFilter`` object that can be used to get the filter using
578 self.filter(index)
579 """
580 result = self._add_filter(NetworkFilter, name)
581 if return_index:
582 return result
584 def add_time_filter(self, name: str = None, return_index=False):
585 """
586 Adds a new `TimeFilter` object. It initially filters out everything (empty matrix).
588 The `TimeFilter` objects are used to create different sets of data using the same data source. You can
589 manipulate the filter/mask using the methods of the `TimeFilter` class.
591 Parameters
592 ----------
593 name : str, optional
594 The name of the filter to add.
595 If None, the filter is only accessible by its index.
596 return_index : bool, optional
597 If True the index of the added ``TimeFilter`` is returned.
599 Returns
600 -------
601 None | int :
602 If return_index: the index of the just added `TimeFilter` object that can be used to get the filter using
603 self.filter(index)
604 """
605 result = self._add_filter(TimeFilter, name)
606 if return_index:
607 return result
609 def add_node_filter(self, name: str = None, return_index=False):
610 """
611 Adds a new `NodeFilter` object. It initially filters out everything (empty matrix).
613 The `NodeFilter` objects are used to create different sets of data using the same data source. You can
614 manipulate the filter/mask using the methods of the `NodeFilter` class.
616 Parameters
617 ----------
618 name : str, optional
619 The name of the filter to add.
620 If None, the filter is only accessible by its index.
621 return_index : bool, optional
622 If True the index of the added ``NodeFilter`` is returned.
624 Returns
625 -------
626 None | int :
627 If return_index: the index of the just added `NodeFilter` object that can be used to get the filter using
628 self.filter(index)
629 """
630 result = self._add_filter(NodeFilter, name)
631 if return_index:
632 return result
634 def remove_filter(self, entry: int | Any = -1):
635 """
636 Removes the desired filter from the filters list.
638 Remember: Previously passed filter indexes might change.
640 Parameters
641 ----------
642 entry : int | Any, optional
643 If an int the index of the filter in the filter list self._filter.
644 If a string the name of the filter in self._filter_names. Is parsed to an index.
645 If None, the last filter is used.
646 """
647 index = self.__parse_filter_index(entry)
648 for l in [self._filters, self._filter_names]:
649 l.pop(index)
651 def add_time_filters(self,
652 days=None,
653 weeks=None,
654 months=None,
655 years=None,
656 filter_name_add_on="",
657 return_names=False):
658 """
659 Adds time filters for all passed days, weeks, months and years.
661 The time filters are named like "day{index of this day in list}{filter_name_add_on}".
662 The weeks, months and years are represented by their start day.
664 If no value is passed no filter is created.
666 Parameters
667 ----------
668 days : list[datetime] | datetime, optional
669 The days that should be plotted.
670 weeks : list[datetime] | datetime, optional
671 The weeks that should be plotted.
672 Each week is represented by its start date.
673 months : list[datetime] | datetime, optional
674 The months that should be plotted.
675 Each month is represented by its start date.
676 years : list[datetime] | datetime, optional
677 The years that should be plotted.
678 Each year is represented by its start date.
679 filter_name_add_on : str, optional
680 A name add-on for the time filter that is added.
681 return_names : bool, optional
682 If True the names of the time filters are returned as dictionary with the entries days, weeks,
683 months and years and the corresponding list.
685 Returns
686 -------
687 dict | None :
688 If return_names, a dict with the layout:
689 {"days": [], "weeks": [], "months": [], "years": []}
690 with the names of the filters in the lists.
691 """
692 import calendar
693 names = {
694 "days": days or [],
695 "weeks": weeks or [],
696 "months": months or [],
697 "years": years or [],
698 }
699 for i, day in enumerate(days):
700 names["days"].append(f"day{i}{filter_name_add_on}")
701 self.add_time_filter(names["days"][-1])
702 time_filter: TimeFilter = self.filter(names["days"][-1])
703 time_filter.daterange(datetime1=day)
704 for i, week in enumerate(weeks):
705 names["weeks"].append(f"week{i}{filter_name_add_on}")
706 self.add_time_filter(names["weeks"][-1])
707 time_filter: TimeFilter = self.filter(names["weeks"][-1])
708 time_filter.daterange(datetime1=week, datetime2=week + timedelta(days=7))
709 for i, dt in enumerate(months):
710 names["months"].append(f"month{i}{filter_name_add_on}")
711 self.add_time_filter(names["months"][-1])
712 time_filter: TimeFilter = self.filter(names["months"][-1])
713 if dt.month == 12:
714 next_year = dt.year + 1
715 next_month = 1
716 else:
717 next_year = dt.year
718 next_month = dt.month + 1
720 max_day = calendar.monthrange(next_year, next_month)[1]
721 next_day = min(dt.day, max_day)
723 datetime2 = dt.replace(year=next_year, month=next_month, day=next_day)
724 time_filter.daterange(datetime1=dt, datetime2=datetime2)
725 for i, dt in enumerate(years):
726 names["years"].append(f"year{i}{filter_name_add_on}")
727 self.add_time_filter(names["years"][-1])
728 time_filter: TimeFilter = self.filter(names["years"][-1])
729 # Handle leap year edge case for Feb 29
730 if dt.month == 2 and dt.day == 29 and not calendar.isleap(dt.year + 1):
731 new_dt = dt.replace(year=dt.year + 1, month=3, day=1)
732 else:
733 new_dt = dt.replace(year=dt.year + 1)
734 time_filter.daterange(datetime1=dt, datetime2=new_dt)
735 if return_names:
736 return names
738 def _block_memory(self):
739 self._blocked_ram = self.solution_size * 1.01
740 FastParser._total_reserved_memory += self.solution_size
742 def _free_memory(self):
743 FastParser._total_reserved_memory -= self._blocked_ram
744 self._blocked_ram = 0
746 @property
747 def solution_exist(self) -> bool:
748 return self.network.rc_solution.exist
750 @property
751 def solution_size(self):
752 if self._solution_size is None:
753 if os.path.isfile(self.solution_path):
754 self._solution_size = os.path.getsize(self.solution_path)
755 else:
756 print(f"The solution size is estimated to be 10 GB ({self.solution_path})")
757 self._solution_size = 10 * 1024 ** 3
758 return self._solution_size
760 def load_solution(self):
761 """
762 Loads solution, but only if enough RAM is available.
764 Raises
765 ------
766 MemoryError :
767 If not enough memory is available.
768 """
769 if (get_free_ram() - FastParser._total_reserved_memory) > self.solution_size:
770 self._block_memory()
771 assert self.network.rc_solution.load_solution(
772 self.solution_path), f"Solution file {self.solution_path} not found"
773 self._free_memory()
774 else:
775 raise MemoryError("Not enough free memory to load solution.")
777 def load_solution_safe(self):
778 """
779 Like load_solution, but it waits for up to 1 hour for enough RAM.
781 Raises
782 ------
783 MemoryError :
784 If not enough memory is available within 1 hour.
785 """
786 counter = 0
787 success = False
788 while counter <= 3600:
789 if (get_free_ram() - FastParser._total_reserved_memory) > self.solution_size:
790 self.load_solution()
791 success = True
792 break
793 time.sleep(1)
794 counter += 1
795 if not success:
796 raise MemoryError("Not enough free memory to load solution.")
798 def free_ram(self):
799 """
800 Deletes the network solution from memory without deleting any calculated/filtered/requested data.
802 #TODO: Append this function with all variables that are existing in the state of this class containing the
803 whole solution data. The garbage collection has to be able to free the RAM from the most data!
804 """
805 self.network.rc_solution.delete_solutions(True)
807 # garbage collection
808 gc.collect()
810 def map(self, function: Callable, *args, **kwargs):
811 """
812 Maps the passed function to every filter and returns the result as a tuple.
814 Parameters
815 ----------
816 function : Callable
817 The function to map on every filter. The first argument is a FilteredRCSolution.
819 Returns
820 -------
821 tuple :
822 The results for each filter.
823 """
824 result = []
825 for filter_obj in self.filters:
826 filtered_solution = FilteredRCSolution(self.network.rc_solution, filter_obj)
827 result.append(function(filtered_solution, *args, **kwargs))
828 return tuple(result)
831class MultiParser:
832 """
833 Class to process multiple solutions of an RC-network (FastParser instances).
835 It behaves like FastParser, but it always executes all calls to every parser instance defined in the init.
837 This should be used with brain!
838 Only compare networks with same hashes or with comparable layout.
839 """
841 def __init__(self, objects: list):
842 """
843 Objects can be a list of FastParser instances or network solution-path tuples.
845 Parameters
846 ----------
847 objects : list[FastParser] | list[tuple[RCNetwork, str]]
848 These objects are compared to each other (same calculations are done for all of them).
849 """
850 assert isinstance(objects, list)
851 self.parsers: list[FastParser] = []
852 for obj in objects:
853 if isinstance(obj, FastParser):
854 self.parsers.append(obj)
855 elif isinstance(obj, tuple):
856 obj: tuple[RCNetwork, str]
857 self.parsers.append(FastParser(obj))
858 else:
859 raise TypeError(f"Object {obj} is not a FastParser instance or a tuple to create it.")
861 def __getattr__(self, name):
862 def multi_method(*args, **kwargs):
863 results = []
864 for parser in self.parsers:
865 attr = getattr(parser, name)
866 if callable(attr):
867 results.append(attr(*args, **kwargs))
868 else:
869 results.append(attr)
870 return results
872 first_attr = getattr(self.parsers[0], name)
873 if callable(first_attr):
874 return multi_method
875 else:
876 return [getattr(parser, name) for parser in self.parsers]