Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pysagas/sensitivity/calculator.py: 82%

105 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-30 04:27 +0000

1import numpy as np 

2import pandas as pd 

3from pysagas.flow import FlowState 

4from abc import ABC, abstractmethod 

5from pysagas.geometry import Vector, Cell 

6from pysagas.utilities import add_sens_data 

7from pysagas.cfd.solver import SensitivityResults 

8from typing import List, Callable, Tuple, Optional, Union, Literal 

9from pysagas.sensitivity.models import ( 

10 piston_sensitivity, 

11 van_dyke_sensitivity, 

12 isentropic_sensitivity, 

13) 

14 

15 

16class AbstractSensitivityCalculator(ABC): 

17 """Abstract sensitivity calculator class defining the interface.""" 

18 

19 solver = None 

20 

21 @abstractmethod 

22 def __init__(self, **kwargs) -> None: 

23 pass 

24 

25 def __repr__(self) -> str: 

26 return f"PySAGAS Sensitivity Calculator for {self.solver}" 

27 

28 def __str__(self) -> str: 

29 return f"PySAGAS Sensitivity Calculator for {self.solver}" 

30 

31 @property 

32 @abstractmethod 

33 def solver(self): 

34 # This is a placeholder for a class variable defining the Sensitivity Calculator's solver 

35 pass 

36 

37 @abstractmethod 

38 def _transcribe_cells(self, parameters: List): 

39 """Transcribes the unified Cells from the solver data. 

40 

41 Returns 

42 -------- 

43 cells : List[Cell] 

44 A list of the Cells from the flow solution. 

45 """ 

46 # This method should be overloaded with the solver-specific method 

47 pass 

48 

49 @abstractmethod 

50 def _extract_parameters(self) -> List[str]: 

51 """Extract the geometry parameters. 

52 

53 Returns 

54 ------- 

55 parameters : List[str] 

56 A list of the parameter names. 

57 """ 

58 # This method should be overloaded with the solver-specific method 

59 pass 

60 

61 @abstractmethod 

62 def to_csv(self): 

63 """Dumps the sensitivity data to CSV file.""" 

64 pass 

65 

66 

67class SensitivityCalculator(AbstractSensitivityCalculator): 

68 """SensitivityCalculator base class.""" 

69 

70 def __init__(self, **kwargs) -> None: 

71 self.cells: list[Cell] = None 

72 

73 def calculate( 

74 self, 

75 sensitivity_model: Optional[ 

76 Union[Callable, Literal["piston", "van_dyke", "isentropic"]] 

77 ] = "van_dyke", 

78 cog: Optional[Vector] = Vector(0, 0, 0), 

79 flowstate: Optional[FlowState] = None, 

80 **kwargs, 

81 ) -> SensitivityResults: 

82 """Calculate the force sensitivities of the surface to the 

83 parameters. 

84 

85 Parameters 

86 ---------- 

87 sensitivity_model : Callable | Literal["piston", "van_dyke", "isentropic"], optional 

88 The model used to calculate the pressure/parameter sensitivities. 

89 The default is Van Dyke's second-order theory model. 

90 

91 cog : Vector, optional 

92 The centre of gravity. The default is Vector(0, 0, 0). 

93 

94 flowstate : FlowState, optional 

95 The flowstate associated with this result. The default is None. 

96 

97 Returns 

98 ------- 

99 SensitivityResults : the sensitivity results object. 

100 """ 

101 if self.verbosity > 0: 

102 print("\nCalculating aerodynamic sensitivities.") 

103 

104 parameters = self._extract_parameters() 

105 

106 if self.cells is None: 

107 self.cells = self._transcribe_cells(parameters=parameters) 

108 

109 params_sens_cols = [] 

110 for p in parameters: 

111 for d in ["x", "y", "z"]: 

112 params_sens_cols.append(f"d{d}d_{p}") 

113 

114 # Calculate force sensitivity 

115 F_sense, M_sense = self.net_sensitivity( 

116 cells=self.cells, sensitivity_model=sensitivity_model, cog=cog, **kwargs 

117 ) 

118 

119 # Construct dataframes to return 

120 df_f = pd.DataFrame( 

121 F_sense, columns=["dFx/dp", "dFy/dp", "dFz/dp"], index=parameters 

122 ) 

123 df_m = pd.DataFrame( 

124 M_sense, columns=["dMx/dp", "dMy/dp", "dMz/dp"], index=parameters 

125 ) 

126 

127 if self.verbosity > 0: 

128 print("Done.") 

129 

130 # Construct results 

131 result = SensitivityResults( 

132 f_sens=df_f, 

133 m_sens=df_m, 

134 freestream=flowstate, 

135 ) 

136 

137 return result 

138 

139 def to_csv(self): 

140 """Dumps the sensitivity data to CSV file.""" 

141 if self.cells is not None: 

142 pass 

143 

144 else: 

145 raise Exception("No cells have been transcribed.") 

146 

147 def _extract_parameters(self): 

148 parameters = [] 

149 for e in self.sensdata.columns: 

150 e: str 

151 if e.startswith("dxd") or e.startswith("dyd") or e.startswith("dzd"): 

152 # Sensitivity coluns 

153 if e[3:] not in parameters: 

154 parameters.append(e[3:]) 

155 return parameters 

156 

157 @staticmethod 

158 def net_sensitivity( 

159 cells: List[Cell], 

160 sensitivity_model: Optional[ 

161 Union[Callable, Literal["piston", "van_dyke", "isentropic"]] 

162 ] = "van_dyke", 

163 cog: Vector = Vector(0, 0, 0), 

164 **kwargs, 

165 ) -> Tuple[np.array, np.array]: 

166 """Calcualtes the net force and moment sensitivities for a list of Cells. 

167 

168 Parameters 

169 ---------- 

170 cells : list[Cell] 

171 The cells to be analysed. 

172 

173 sensitivity_model : Callable | Literal["piston", "van_dyke", "isentropic"], optional 

174 The model used to calculate the pressure/parameter sensitivities. 

175 The default is Van Dyke's second-order theory model. 

176 

177 cog : Vector, optional 

178 The reference centre of gravity, used in calculating the moment 

179 sensitivities. The defualt is Vector(0,0,0). 

180 

181 Returns 

182 -------- 

183 dFdp : np.array 

184 The force sensitivity matrix with respect to the parameters. 

185 

186 dMdp : np.array 

187 The moment sensitivity matrix with respect to the parameters. 

188 """ 

189 # Get sensitivity handle 

190 if isinstance(sensitivity_model, Callable): 

191 # Use function provided 

192 sensitivity_function = sensitivity_model 

193 elif isinstance(sensitivity_model, str): 

194 # Get function from method specified 

195 sensitivity_function = globals().get(f"{sensitivity_model}_sensitivity") 

196 if sensitivity_function is None: 

197 raise Exception("Invalid sensitivity method specified.") 

198 

199 dFdp = 0 

200 dMdp = 0 

201 for cell in cells: 

202 # Calculate force sensitivity 

203 dFdp_c, dMdp_c = SensitivityCalculator.cell_sensitivity( 

204 cell=cell, sensitivity_function=sensitivity_function, cog=cog, **kwargs 

205 ) 

206 

207 dFdp += dFdp_c 

208 dMdp += dMdp_c 

209 

210 return dFdp, dMdp 

211 

212 @staticmethod 

213 def cell_sensitivity( 

214 cell: Cell, 

215 sensitivity_function: Callable, 

216 cog: Vector = Vector(0, 0, 0), 

217 **kwargs, 

218 ) -> Tuple[np.array, np.array]: 

219 """Calculates force and moment sensitivities for a single cell. 

220 

221 Parameters 

222 ---------- 

223 cell : Cell 

224 The cell. 

225 

226 sensitivity_function : Callable 

227 The function to use when calculating the pressure sensitivities. 

228 

229 cog : Vector, optional 

230 The reference centre of gravity, used in calculating the moment 

231 sensitivities. The defualt is Vector(0,0,0). 

232 

233 Returns 

234 -------- 

235 sensitivities : np.array 

236 An array of shape n x 3, for a 3-dimensional cell with 

237 n parameters. 

238 

239 See Also 

240 -------- 

241 all_dfdp : a wrapper to calculate force sensitivities for many cells 

242 """ 

243 # Initialisation 

244 all_directions = [Vector(1, 0, 0), Vector(0, 1, 0), Vector(0, 0, 1)] 

245 sensitivities = np.zeros(shape=(cell.dndp.shape[1], 3)) 

246 moment_sensitivities = np.zeros(shape=(cell.dndp.shape[1], 3)) 

247 

248 # Calculate moment dependencies 

249 r = cell.c - cog 

250 F = cell.flowstate.P * cell.A * cell.n.vec 

251 

252 # For each parameter 

253 for p_i in range(cell.dndp.shape[1]): 

254 # Calculate pressure sensitivity 

255 dPdp = sensitivity_function(cell=cell, p_i=p_i, **kwargs) 

256 

257 # Evaluate for sensitivities for each direction 

258 for i, direction in enumerate(all_directions): 

259 dF = ( 

260 dPdp * cell.A * np.dot(cell.n.vec, direction.vec) 

261 + cell.flowstate.P 

262 * cell.dAdp[p_i] 

263 * np.dot(cell.n.vec, direction.vec) 

264 + cell.flowstate.P 

265 * cell.A 

266 * np.dot(-cell.dndp[:, p_i], direction.vec) 

267 ) 

268 sensitivities[p_i, i] = dF 

269 

270 # Now evaluate moment sensitivities 

271 moment_sensitivities[p_i, :] = np.cross( 

272 r.vec, sensitivities[p_i, :] 

273 ) + np.cross(cell.dcdp[:, p_i], F) 

274 

275 # Append to cell 

276 cell.sensitivities = sensitivities 

277 cell.moment_sensitivities = moment_sensitivities 

278 

279 return sensitivities, moment_sensitivities 

280 

281 

282class GenericSensitivityCalculator(SensitivityCalculator): 

283 solver = "Generic Flow Solver" 

284 

285 def __init__( 

286 self, 

287 cells: List[Cell], 

288 sensitivity_filepath: Optional[str] = None, 

289 cells_have_sens_data: Optional[bool] = False, 

290 verbosity: Optional[int] = 1, 

291 **kwargs, 

292 ) -> None: 

293 """Instantiate a generic PySAGAS sensitivity calculator. 

294 

295 Parameters 

296 ---------- 

297 cells : list[Cell] 

298 A list of transcribed Cell objects, containing the nominal flow solution. 

299 

300 sensitivity_filepath : str 

301 The filepath to the geometry sensitivities. This must be provided, if the 

302 cells do not have geometric sensitivity information attached. This must 

303 also be provided anyway, to determine the geometric design parameters. 

304 

305 cells_have_sens_data : bool, optional 

306 The cells already have geometric sensitivity data matched to them. When this 

307 is True, the sensitivity_filepath argument (if provided) is ignored. The 

308 default is False. 

309 

310 verbosity : int, optional 

311 The verbosity of the code. The defualt is 1. 

312 """ 

313 super().__init__(**kwargs) 

314 

315 if cells_have_sens_data: 

316 self.cells = cells 

317 else: 

318 self._pre_transcribed_cells = cells 

319 

320 self.sensdata = pd.read_csv(sensitivity_filepath) 

321 self.verbosity = verbosity 

322 

323 def _transcribe_cells(self, **kwargs) -> List[Cell]: 

324 """This is a dummy method to satisfy the abstract base class. Transcribed cells 

325 are provided upon instantiation of the sensitivity calculator. 

326 """ 

327 # Add sensitivity data to _pre_transcribed_cells 

328 add_sens_data( 

329 cells=self._pre_transcribed_cells, 

330 data=self.sensdata, 

331 verbosity=self.verbosity, 

332 ) 

333 

334 return self._pre_transcribed_cells