Coverage for pyrc \ core \ settings.py: 79%

105 statements  

« 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# ------------------------------------------------------------------------------ 

7 

8import os 

9from collections.abc import Callable 

10from copy import copy 

11from typing import Any, Optional 

12 

13import numpy as np 

14 

15from pyrc.dataHandler.weather import WeatherData 

16from pyrc.core.wall import Wall 

17 

18 

19class SolveSettings: 

20 def __init__( 

21 self, 

22 max_saved_steps=5e4, 

23 method="RK45", 

24 max_step=0.4, 

25 rtol=1e-7, 

26 atol=1e-2, 

27 save_interval=1, 

28 minimize_ram_usage=True, 

29 ): 

30 """ 

31 

32 Parameters 

33 ---------- 

34 max_saved_steps : int, optional 

35 The maximum number of seconds that are simulated in one solve_ivp call. 

36 Defines the batch size. 

37 method : str, optional 

38 The method of the solver. See solve_ivp 

39 max_step : int | float, optional 

40 The maximum number of seconds that the solver can use as one step. 

41 rtol, atol : float or array_like, optional 

42 Relative and absolute tolerances. The solver keeps the local error 

43 estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a 

44 relative accuracy (number of correct digits), while `atol` controls 

45 absolute accuracy (number of correct decimal places). To achieve the 

46 desired `rtol`, set `atol` to be smaller than the smallest value that 

47 can be expected from ``rtol * abs(y)`` so that `rtol` dominates the 

48 allowable error. If `atol` is larger than ``rtol * abs(y)`` the 

49 number of correct digits is not guaranteed. Conversely, to achieve the 

50 desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller 

51 than `atol`. If components of y have different scales, it might be 

52 beneficial to set different `atol` values for different components by 

53 passing array_like with shape (n,) for `atol`. Default values are 

54 1e-3 for `rtol` and 1e-6 for `atol`. [Copied from scipy.solve_ivp doc] 

55 save_interval : int, optional 

56 In which interval the solution should be saved. 

57 One interval is as long as max_saved_steps. 

58 minimize_ram_usage : bool, optional 

59 If True and a save_path is given to the RCNetwork it occasionally deletes the solution after saving to 

60 disk to free RAM. 

61 """ 

62 self.max_saved_steps: int = int(max_saved_steps) 

63 self.method = method 

64 self.max_step = max_step 

65 self.rtol = rtol 

66 self.atol = atol 

67 self.save_interval = save_interval 

68 self.minimize_ram_usage = minimize_ram_usage 

69 

70 @property 

71 def keys(self): 

72 return [ 

73 "max_saved_steps", 

74 "method", 

75 "max_step", 

76 "rtol", 

77 "atol", 

78 "save_interval", 

79 "minimize_ram_usage", 

80 ] 

81 

82 @property 

83 def values(self): 

84 return [ 

85 self.max_saved_steps, 

86 self.method, 

87 self.max_step, 

88 self.rtol, 

89 self.atol, 

90 self.save_interval, 

91 self.minimize_ram_usage, 

92 ] 

93 

94 @property 

95 def dict(self): 

96 return {k: v for k, v in zip(self.keys, self.values)} 

97 

98 def __copy__(self): 

99 return SolveSettings(**{k: copy(v) for k, v in zip(self.keys, self.values)}) 

100 

101 

102class Settings: 

103 """ 

104 Class for composition. Every object can get the same object of this class to determine global settings. 

105 

106 In the future a settings yaml could be used and loaded in. 

107 """ 

108 

109 def __init__( 

110 self, 

111 calculate_static, 

112 use_weather_data, 

113 wall: Wall = Wall(), 

114 start_date="2022-01-01T00:00:00", # if this initial value is changed you have to change the 

115 # Parameterization class values, too (it uses it hardcoded) 

116 weather_data_path=None, 

117 maximum_area_specific_power=400, 

118 save_folder_path=None, 

119 save_all_x_seconds=25, # TODO: Maybe use not this value if not given but save 1000 steps for each simulation 

120 solve_settings: SolveSettings | Any = None, 

121 ): 

122 self.calculate_static = calculate_static 

123 self.wall = wall 

124 self.__start_date = start_date 

125 self.__use_weather_data: bool = use_weather_data 

126 self.__weather_data_path = os.path.normpath(weather_data_path) if weather_data_path is not None else None 

127 self.weather_data: Optional["WeatherData"] | Any = None 

128 self.__rebuild_weather_data() 

129 

130 self._area_specific_radiation_interpolator_short = None 

131 self._area_specific_radiation_interpolator_long = None 

132 # only used if weather data is not used 

133 self.maximum_area_specific_power = maximum_area_specific_power 

134 

135 self.__save_folder_path = None 

136 self.save_folder_path = save_folder_path 

137 

138 self.save_all_x_seconds = save_all_x_seconds 

139 

140 if solve_settings is None: 

141 solve_settings = SolveSettings() 

142 self.solve_settings: SolveSettings = solve_settings 

143 

144 def __rebuild_weather_data(self) -> None: 

145 """(Re)create WeatherData if enabled and fully configured; otherwise set to None.""" 

146 if not self.__use_weather_data: 

147 self.weather_data = None 

148 return 

149 

150 if self.__weather_data_path is None: 

151 raise ValueError("use_weather_data=True requires weather_data_path to be set.") 

152 

153 self.weather_data = WeatherData( 

154 self.__weather_data_path, 

155 start_time=self.__start_date, 

156 ) 

157 

158 def __copy__(self): 

159 obj = Settings( 

160 calculate_static=self.calculate_static, 

161 use_weather_data=False, 

162 wall=copy(self.wall), 

163 start_date=self.start_date, 

164 weather_data_path=self.weather_data_path, 

165 maximum_area_specific_power=self.maximum_area_specific_power, 

166 save_folder_path=self.save_folder_path, 

167 save_all_x_seconds=self.save_all_x_seconds, 

168 solve_settings=copy(self.solve_settings), 

169 ) 

170 obj.use_weather_data = self.use_weather_data 

171 return obj 

172 

173 def __deepcopy__(self): 

174 return self.__copy__() 

175 

176 @property 

177 def save_folder_path(self): 

178 return self.__save_folder_path 

179 

180 @save_folder_path.setter 

181 def save_folder_path(self, value): 

182 if value is not None: 

183 os.makedirs(os.path.normpath(value), exist_ok=True) 

184 self.__save_folder_path = value 

185 

186 @property 

187 def start_date(self) -> str: 

188 return self.__start_date 

189 

190 @start_date.setter 

191 def start_date(self, value: str) -> None: 

192 self.__start_date = value 

193 self.__rebuild_weather_data() 

194 

195 @property 

196 def weather_data_path(self) -> Optional[str]: 

197 return self.__weather_data_path 

198 

199 @weather_data_path.setter 

200 def weather_data_path(self, value: Optional[str]) -> None: 

201 self.__weather_data_path = os.path.normpath(value) if value is not None else None 

202 self.__rebuild_weather_data() 

203 

204 @property 

205 def use_weather_data(self) -> bool: 

206 return self.__use_weather_data 

207 

208 @use_weather_data.setter 

209 def use_weather_data(self, value: bool) -> None: 

210 self.__use_weather_data = value 

211 self.__rebuild_weather_data() 

212 

213 @property 

214 def area_specific_radiation_interpolator_short(self) -> Callable | Any: 

215 if self._area_specific_radiation_interpolator_short is None: 

216 if self.weather_data is not None: 

217 self._area_specific_radiation_interpolator_short = self.weather_data.radiation_interpolator( 

218 self.wall, wavelength_type="short" 

219 ) 

220 return self._area_specific_radiation_interpolator_short 

221 

222 @property 

223 def area_specific_radiation_interpolator_long(self) -> Callable | Any: 

224 if self._area_specific_radiation_interpolator_long is None: 

225 if self.weather_data is not None: 

226 self._area_specific_radiation_interpolator_long = self.weather_data.radiation_interpolator( 

227 self.wall, wavelength_type="long" 

228 ) 

229 return self._area_specific_radiation_interpolator_long 

230 

231 @property 

232 def start_shift(self): 

233 return ( 

234 np.datetime64(self.start_date) - np.datetime64(self.start_date).astype("datetime64[D]") 

235 ) / np.timedelta64(1, "s") 

236 

237 

238initial_wall = Wall() 

239initial_settings = Settings( 

240 calculate_static=False, 

241 use_weather_data=False, 

242 wall=initial_wall, 

243 # weather_data_path=r"C:\path\to\dwd_data\TRY2015_523748130791_Jahr.dat", 

244 # save_folder_path=r"C:\path\to\folder", 

245 start_date="2023-09-15T00:00:00", 

246) 

247settings_first_try = Settings( 

248 calculate_static=False, 

249 use_weather_data=True, 

250 wall=initial_wall, 

251 weather_data_path=r"/path/to/dwd_data/TRY2015_523748130791_Jahr.dat", 

252 # save_folder_path=r"/path/to/folder", 

253 start_date="2023-09-15T00:00:00", 

254) 

255# settings = Settings( 

256# calculate_static=True, 

257# use_weather_data=True, 

258# wall=initial_wall, 

259# weather_data_path=r"/path/to/dwd_data/TRY2015_523748130791_Jahr.dat", 

260# save_folder_path=r"/path/to/folder", 

261# start_date="2023-09-15T00:00:00", 

262# )