Coverage for src/python/dandeliion/client/solution.py: 98%

56 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-24 10:44 +0100

1""" 

2@file python/dandeliion/client/solution.py 

3 

4Module containing class for handling access to solutions 

5""" 

6 

7# 

8# Copyright (C) 2024-2025 Dandeliion Team 

9# 

10# This library is free software; you can redistribute it and/or modify it under 

11# the terms of the GNU Lesser General Public License as published by the Free 

12# Software Foundation; either version 3.0 of the License, or (at your option) 

13# any later version. 

14# 

15# This library is distributed in the hope that it will be useful, but WITHOUT 

16# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 

17# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 

18# details. 

19# 

20# You should have received a copy of the GNU Lesser General Public License 

21# along with this library; if not, write to the Free Software Foundation, Inc., 

22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

23# 

24 

25# built-in modules 

26import logging 

27import copy 

28from typing import Protocol, Optional 

29from collections.abc import Mapping 

30 

31# custom modules 

32from .exceptions import DandeliionAPIException 

33 

34# third-party modules 

35import numpy as np 

36import numpy.typing 

37 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42class Simulator(Protocol): 

43 """ Simulator Protocol """ 

44 def update_results(self, prefetched_data: dict, keys: list = None, inline: bool = False) -> Optional[dict]: ... 

45 def get_status(self, prefetched_data: dict) -> str: ... 

46 

47 

48class InterpolatedArray(np.ndarray): 

49 """ 

50 Subclass of ndarray providing function call for linear interpolation 

51 """ 

52 def __new__(cls, t: np.typing.ArrayLike, y: np.typing.ArrayLike, **kwargs): 

53 instance = np.asarray(y, **kwargs).view(cls) 

54 instance.t = np.array(t) 

55 return instance 

56 

57 def __call__(self, t): 

58 """ 

59 function call to return interpolated value 

60 """ 

61 # check that 1-d array 

62 if not (len(self.t.shape) == len(self.shape) == 1): 

63 raise ValueError('x and y must be 1-d array-like objects') 

64 # check that array of same length 

65 if self.t.shape != self.shape: 

66 raise ValueError('x and y must be of same length') 

67 return np.interp(t, self.t, self) 

68 

69 

70class Solution(Mapping): 

71 """Dictionary-style class for the solutions of a simulation run 

72 returned by :meth:`solve` 

73 """ 

74 

75 _data: dict = None 

76 _sim: Simulator = None 

77 

78 def __init__(self, sim: Simulator, prefetched_data: dict, time_column: str = None): 

79 """ 

80 Constructor 

81 

82 Args: 

83 sim (Simulator): simulator instance for fetching data from server 

84 prefetched_data (dict): existing (meta) data 

85 time_column (str): label of time column (used for interpolation) 

86 """ 

87 self._sim = sim 

88 self._data = prefetched_data 

89 self._time_column = time_column 

90 

91 def _init_solution(self): 

92 """ 

93 inits prefetched solution from simulator if necessary 

94 """ 

95 logger.debug('Initialising solutions') 

96 self._sim.update_results(self._data, inline=True) 

97 # if solution still not initialised (e.g. because simulation 

98 # failed or has not finished yet), raise Exception 

99 if self._data['Solution'] is None: 

100 raise DandeliionAPIException( 

101 'Solution not ready (yet). Check status for details.' 

102 ) 

103 

104 def __getitem__(self, key: str): 

105 """Returns the results requested by the key. 

106 

107 Args: 

108 key (str): key for results to be returned. 

109 

110 Returns: 

111 object: data as requested by provided key 

112 """ 

113 

114 # if solution not initialised yet, try to fetch from server 

115 if self._data.get('Solution', None) is None: 

116 self._init_solution() 

117 

118 if key not in self._data['Solution']: 

119 raise KeyError( 

120 f'Column for {key} does not exist in solution.' 

121 ) 

122 # fetch data if necessary 

123 if self._data['Solution'][key] is None: 

124 logger.info(f"Fetching '{key}' column from simulator") 

125 self._sim.update_results(self._data, keys=[key], inline=True) 

126 if self._time_column: 

127 return InterpolatedArray(t=self._data['Solution'][self._time_column], y=self._data['Solution'][key]) 

128 else: 

129 return copy.deepcopy(self._data['Solution'][key]) 

130 

131 def __len__(self): 

132 """Returns the number of fields in the solutions. 

133 

134 Returns: 

135 int: number of fields 

136 """ 

137 if self._data.get('Solution', None) is None: 

138 self._init_solution() 

139 return len(self._data['Solution']) 

140 

141 def __iter__(self): 

142 """Returns an iterator on the solutions fields. 

143 

144 Returns: 

145 iterator 

146 """ 

147 if self._data.get('Solution', None) is None: 

148 self._init_solution() 

149 yield from self._data['Solution'] 

150 

151 @property 

152 def status(self): 

153 """Returns the status of the simulation run linked to this solutions 

154 

155 Returns: 

156 str: current status of simulation run ('queued', 'running', 'failed', 'success') 

157 """ 

158 return self._sim.get_status(self._data)