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