Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pysagas/cfd/deck.py: 42%

107 statements  

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

1import copy 

2import pandas as pd 

3from typing import Optional, Union 

4from abc import ABC, abstractmethod 

5from pysagas.cfd.solver import FlowResults, SensitivityResults 

6 

7 

8class AbstractDeck(ABC): 

9 @abstractmethod 

10 def __init__(self) -> None: 

11 pass 

12 

13 @abstractmethod 

14 def __repr__(self) -> None: 

15 pass 

16 

17 @abstractmethod 

18 def __str__(self) -> None: 

19 pass 

20 

21 @abstractmethod 

22 def insert(self): 

23 """Insert data into the deck.""" 

24 pass 

25 

26 @abstractmethod 

27 def to_csv(self): 

28 """Write the deck to csv.""" 

29 pass 

30 

31 @abstractmethod 

32 def from_csv(self, **kwargs): 

33 """Load the deck from csv.""" 

34 pass 

35 

36 # @abstractmethod 

37 # def interpolate(self, **kwargs): 

38 # # TODO - implement with scipy.interpolate.interpn 

39 # pass 

40 

41 

42class Deck(AbstractDeck): 

43 TYPE = "deck" 

44 

45 def __init__( 

46 self, 

47 inputs: list[str], 

48 columns: list[str], 

49 **kwargs, 

50 ) -> None: 

51 """Instantiate a new deck. 

52 

53 Parameters 

54 ---------- 

55 inputs : list[str] 

56 A list of the inputs to this aerodeck. For example, ["aoa", "mach"]. 

57 

58 columns : list[str] 

59 A list of column names to use for this deck. For example, ["CL", "CD", "Cm"]. 

60 """ 

61 self._deck = pd.DataFrame(columns=inputs + columns) 

62 self._inputs = inputs 

63 

64 def __repr__(self) -> None: 

65 return f"{self.TYPE}" 

66 

67 def __str__(self) -> None: 

68 return self.__repr__() 

69 

70 @property 

71 def deck(self) -> pd.DataFrame: 

72 return self._deck.drop_duplicates() 

73 

74 def to_csv(self, file_prefix: Optional[str] = None): 

75 """Save the deck to CSV file. 

76 

77 Parameters 

78 ---------- 

79 file_prefix : str, optional 

80 The CSV file name prefix. If None is provided, the deck __repr__ 

81 will be used. The default is None. 

82 """ 

83 file_prefix = file_prefix if file_prefix else self.__repr__() 

84 self.deck.to_csv(f"{file_prefix}.csv", index=False) 

85 

86 

87class MultiDeck(AbstractDeck): 

88 """Collection of multiple Deck objects.""" 

89 

90 def __init__( 

91 self, 

92 inputs: list[str], 

93 parameters: list[str], 

94 base_deck: Deck, 

95 **kwargs, 

96 ) -> None: 

97 """Instantiate a new deck collection. 

98 

99 Parameters 

100 ---------- 

101 inputs : list[str] 

102 A list of the inputs to this deck. For example, ["aoa", "mach"]. 

103 

104 parameters : list[str] 

105 A list of the parameters to this deck. For example, ["wingspan", "length"]. 

106 

107 base_deck : Deck 

108 The base deck to initalise self._decks with. 

109 """ 

110 self._inputs = inputs 

111 self._parameters = list(parameters) 

112 self._decks: dict[str, Deck] = {p: copy.deepcopy(base_deck) for p in parameters} 

113 

114 

115class AeroDeck(Deck): 

116 """Aerodynamic coefficient deck.""" 

117 

118 TYPE = "aerodeck" 

119 

120 def __init__( 

121 self, 

122 inputs: list[str], 

123 columns: list[str] = ["CL", "CD", "Cm"], 

124 a_ref: float = 1, 

125 c_ref: float = 1, 

126 ) -> None: 

127 """Instantiate a new aerodeck. 

128 

129 Parameters 

130 ---------- 

131 inputs : list[str] 

132 A list of the inputs to this aerodeck. For example, ["aoa", "mach"]. 

133 

134 columns : list[str], optional 

135 A list of column names to use for this deck. The default is ["CL", "CD", "Cm"]. 

136 

137 a_ref : float, optional 

138 The reference area. The default is 1. 

139 

140 c_ref : float, optional 

141 The reference length. The default is 1. 

142 """ 

143 

144 # Save reference properties 

145 self._a_ref = a_ref 

146 self._c_ref = c_ref 

147 self._columns = columns 

148 

149 # Complete instantiation 

150 super().__init__(inputs, columns) 

151 

152 def insert(self, result: Union[FlowResults, dict[str, float]], **kwargs): 

153 """Insert aerodynamic coefficients into the aerodeck. 

154 

155 Parameters 

156 ---------- 

157 result : FlowResults | dict 

158 The aerodynamic coefficients to be inserted, either as a PySAGAS native 

159 FlowResults object, or a dictionary with keys matching the data columns 

160 specified on instantiation of the deck. 

161 

162 See Also 

163 -------- 

164 FlowResults 

165 """ 

166 # Check inputs 

167 inputs_given = [i in kwargs for i in self._inputs] 

168 if not all(inputs_given): 

169 raise Exception( 

170 "Please provide all input values when inserting new result." 

171 ) 

172 

173 # Process results 

174 if isinstance(result, FlowResults): 

175 # Get coefficients 

176 coefficients = result.coefficients(A_ref=self._a_ref, c_ref=self._c_ref) 

177 

178 # Extract data 

179 # TODO - use columns provided in init 

180 data = { 

181 "CL": coefficients[0], 

182 "CD": coefficients[1], 

183 "Cm": coefficients[2], 

184 } 

185 

186 else: 

187 # Check keys 

188 data_given = [i in result for i in self._columns] 

189 if not all(data_given): 

190 raise Exception( 

191 "Please provide a data point for all values when inserting new result." 

192 ) 

193 

194 # Data provided directly 

195 data = result 

196 

197 # Add inputs 

198 data.update(kwargs) 

199 

200 # Add to deck 

201 self._deck = pd.concat( 

202 [self._deck, pd.DataFrame(data, index=[len(self._deck)])] 

203 ) 

204 

205 @classmethod 

206 def from_csv( 

207 cls, filepath: str, inputs: list[str], a_ref: float = 1, c_ref: float = 1 

208 ): 

209 """Create an Aerodeck from a CSV file. 

210 

211 Parameters 

212 ---------- 

213 filepath : str 

214 The filepath to the csv file containing aerodeck data. 

215 

216 inputs : list[str] 

217 A list of the inputs to this aerodeck. For example, ["aoa", "mach"]. 

218 

219 a_ref : float, optional 

220 The reference area. The default is 1. 

221 

222 c_ref : float, optional 

223 The reference length. The default is 1. 

224 """ 

225 

226 # Read data from file 

227 data = pd.read_csv(filepath) 

228 

229 # Extract inputs and columns 

230 columns = list(data.columns) 

231 inputs = [columns.pop(columns.index(coef)) for coef in inputs] 

232 

233 # Instantiate aerodeck 

234 aerodeck = cls(inputs=inputs, columns=columns, a_ref=a_ref, c_ref=c_ref) 

235 aerodeck._deck = data 

236 return aerodeck 

237 

238 

239class SensDeck(MultiDeck): 

240 TYPE = "sensdeck" 

241 

242 def __init__( 

243 self, 

244 inputs: list[str], 

245 parameters: list[str], 

246 a_ref: float = 1, 

247 c_ref: float = 1, 

248 **kwargs, 

249 ) -> None: 

250 """Instantiate a new sensitivity deck. 

251 

252 This object is a collection of `AeroDeck`s, containing aerodynamic sensitivity 

253 information. 

254 

255 Parameters 

256 ---------- 

257 inputs : list[str] 

258 A list of the inputs to this deck. For example, ["aoa", "mach"]. 

259 

260 parameters : list[str] 

261 A list of the parameters to this deck. For example, ["wingspan", "length"]. 

262 

263 a_ref : float, optional 

264 The reference area. The default is 1. 

265 

266 c_ref : float, optional 

267 The reference length. The default is 1. 

268 

269 See Also 

270 -------- 

271 AeroDeck 

272 """ 

273 

274 # Save reference properties 

275 self._a_ref = a_ref 

276 self._c_ref = c_ref 

277 

278 # Create base sensitivity deck 

279 columns = ["dCL", "dCD", "dCm"] 

280 base_deck = AeroDeck(inputs, columns, a_ref, c_ref) 

281 

282 # Complete instantiation 

283 super().__init__( 

284 inputs=inputs, parameters=parameters, base_deck=base_deck, **kwargs 

285 ) 

286 

287 def __repr__(self) -> None: 

288 return f"{self.TYPE}" 

289 

290 def __str__(self) -> None: 

291 return self.__repr__() 

292 

293 def insert(self, result: SensitivityResults, **kwargs): 

294 # Get coefficients 

295 f_sens, m_sens = result.coefficients(A_ref=self._a_ref, c_ref=self._c_ref) 

296 

297 for param in self._parameters: 

298 # Extract data for this parameter 

299 data = { 

300 "dCL": f_sens.loc[param]["dCL"], 

301 "dCD": f_sens.loc[param]["dCD"], 

302 "dCm": m_sens.loc[param]["dMz/dp"], 

303 } 

304 

305 # Insert this data into the respective parameter deck 

306 self._decks[param].insert(result=data, **kwargs) 

307 

308 @classmethod 

309 def from_csv( 

310 cls, 

311 param_filepaths: dict[str, str], 

312 inputs: list[str], 

313 a_ref: float = 1, 

314 c_ref: float = 1, 

315 ): 

316 """Create a SensDeck from a collection of CSV files. 

317 

318 Parameters 

319 ---------- 

320 param_filepaths : dict[str, str] 

321 A dictionary of filepaths, keyed by the associated parameter. 

322 

323 inputs : list[str] 

324 A list of the inputs to this aerodeck. For example, ["aoa", "mach"]. 

325 

326 a_ref : float, optional 

327 The reference area. The default is 1. 

328 

329 c_ref : float, optional 

330 The reference length. The default is 1. 

331 """ 

332 decks = {} 

333 for param, filepath in param_filepaths.items(): 

334 # Load data 

335 deck = AeroDeck.from_csv( 

336 filepath=filepath, inputs=inputs, a_ref=a_ref, c_ref=c_ref 

337 ) 

338 decks[param] = deck 

339 

340 # Extract inputs 

341 inputs = deck._inputs 

342 

343 # Instantiate sensdeck 

344 sensdeck = cls( 

345 inputs=inputs, parameters=param_filepaths.keys(), a_ref=a_ref, c_ref=c_ref 

346 ) 

347 

348 # Overwrite with loaded decks 

349 sensdeck._decks = decks 

350 

351 return sensdeck 

352 

353 def to_csv(self, file_prefix: Optional[str] = None): 

354 """Save the decks to CSV file. 

355 

356 Parameters 

357 ---------- 

358 file_prefix : str, optional 

359 The CSV file name prefix. If None is provided, the deck __repr__ 

360 will be used. The default is None. 

361 """ 

362 file_prefix = file_prefix if file_prefix else self.__repr__() 

363 for p, deck in self._decks.items(): 

364 deck.to_csv(file_prefix=f"{p}_{file_prefix}") 

365 

366 @property 

367 def decks(self) -> dict[str, AeroDeck]: 

368 decks = {p: deck.deck for p, deck in self._decks.items()} 

369 return decks