Coverage for src/meshpy/four_c/dbc_monitor.py: 89%

99 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 04:21 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2025 MeshPy Authors 

4# 

5# Permission is hereby granted, free of charge, to any person obtaining a copy 

6# of this software and associated documentation files (the "Software"), to deal 

7# in the Software without restriction, including without limitation the rights 

8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

9# copies of the Software, and to permit persons to whom the Software is 

10# furnished to do so, subject to the following conditions: 

11# 

12# The above copyright notice and this permission notice shall be included in 

13# all copies or substantial portions of the Software. 

14# 

15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 

21# THE SOFTWARE. 

22"""This function converts the DBC monitor log files to Neumann input 

23sections.""" 

24 

25from typing import Optional as _Optional 

26 

27import numpy as _np 

28 

29from meshpy.core.boundary_condition import BoundaryCondition as _BoundaryCondition 

30from meshpy.core.conf import mpy as _mpy 

31from meshpy.core.geometry_set import GeometrySet as _GeometrySet 

32from meshpy.four_c.function import Function as _Function 

33from meshpy.four_c.function_utility import ( 

34 create_linear_interpolation_function as _create_linear_interpolation_function, 

35) 

36from meshpy.four_c.function_utility import ( 

37 ensure_length_of_function_array as _ensure_length_of_function_array, 

38) 

39from meshpy.four_c.input_file import InputFile as _InputFile 

40 

41 

42def linear_time_transformation( 

43 time, values, time_span, *, flip=False, valid_start_and_end_point=False 

44): 

45 """Performs a transformation of the time to a new interval with an 

46 appropriate value vector. 

47 

48 Args 

49 ---- 

50 time: _np.array 

51 array with time values 

52 values: _np.array 

53 corresponding values to time 

54 time_span: [list] with 2 or 3 entries: 

55 time_span[0:2] defines the time interval to which the initial time interval should be scaled. 

56 time_span[3] optional timepoint to repeat last value 

57 flip: Bool 

58 Flag if the values should be reversed 

59 valid_start_and_end_point: Bool 

60 optionally adds a valid starting point at t=0 and timespan[3] if provided 

61 """ 

62 

63 # flip values if desired and adjust time 

64 if flip is True: 

65 values = _np.flipud(values) 

66 time = _np.flip(-time) + time[-1] 

67 

68 # transform time to interval 

69 min_t = _np.min(time) 

70 max_t = _np.max(time) 

71 

72 # scaling/transforming the time into the user defined time 

73 time = time_span[0] + (time - min_t) * (time_span[1] - time_span[0]) / ( 

74 max_t - min_t 

75 ) 

76 

77 # ensure that start time is valid 

78 if valid_start_and_end_point and time[0] > 0.0: 

79 # add starting time 0 

80 time = _np.append(0.0, time) 

81 

82 # add first coordinate again at the beginning of the array 

83 if len(values.shape) == 1: 

84 values = _np.append(values[0], values) 

85 else: 

86 values = _np.append(values[0], values).reshape( 

87 values.shape[0] + 1, values.shape[1] 

88 ) 

89 

90 # repeat last value at provided time point 

91 if valid_start_and_end_point and len(time_span) > 2: 

92 if time_span[2] > time_span[1]: 

93 time = _np.append(time, time_span[2]) 

94 values = _np.append(values, values[-1]).reshape( 

95 values.shape[0] + 1, values.shape[1] 

96 ) 

97 if not valid_start_and_end_point and len(time_span) > 2: 

98 raise Warning("You specified unnecessarily a third component of time_span.") 

99 

100 return time, values 

101 

102 

103def read_dbc_monitor_file(file_path): 

104 """Load the Dirichlet boundary condition monitor log and return the data as 

105 well as the nodes of this boundary condition. 

106 

107 Args 

108 ---- 

109 file_path: str 

110 Path to the Dirichlet boundary condition monitor log. 

111 

112 Return 

113 ---- 

114 [node_ids], [time], [force], [moment] 

115 """ 

116 

117 with open(file_path, "r") as file: 

118 lines = [line.strip() for line in file.readlines()] 

119 

120 # Extract the nodes for this condition. 

121 condition_nodes_prefix = "Nodes of this condition:" 

122 if not lines[1].startswith(condition_nodes_prefix): 

123 raise ValueError( 

124 f'The second line in the monitor file is supposed to start with "{condition_nodes_prefix}" but the line reads "{lines[1]}"' 

125 ) 

126 node_line = lines[1].split(":")[1] 

127 node_ids_str = node_line.split() 

128 nodes = [] 

129 for node_id_str in node_ids_str: 

130 node_id = int(node_id_str) 

131 nodes.append(node_id) 

132 

133 # Find the start of the data lines. 

134 for i, line in enumerate(lines): 

135 if line.split(" ")[0] == "step": 

136 break 

137 else: 

138 raise ValueError('Could not find "step" in file!') 

139 start_line = i + 1 

140 

141 # Get the monitor data. 

142 data = [] 

143 for line in lines[start_line:]: 

144 data.append(_np.fromstring(line, dtype=float, sep=" ")) 

145 data = _np.array(data) 

146 

147 return nodes, data[:, 1], data[:, 4:7], data[:, 7:] 

148 

149 

150def add_point_neuman_condition_to_input_file( 

151 input_file: _InputFile, 

152 nodes: list[int], 

153 function_array: list[_Function], 

154 force: _np.ndarray, 

155 *, 

156 n_dof: int = 3, 

157): 

158 """Adds a Neumann boundary condition to the input file for the given 

159 node_ids with the function_array and force values by creating a new 

160 geometry set. 

161 

162 Args 

163 ---- 

164 input_file: InputFile 

165 InputFile where the boundary conditions are added to 

166 nodes: [node_id] 

167 list containing the ids of the nodes for the condition 

168 function_array: [function] 

169 list with functions 

170 force: [_np.ndarray] 

171 values to scale the function array with 

172 n_dof: int 

173 Number of DOFs per node. 

174 """ 

175 

176 # check if the dimensions of force and functions match 

177 if force.size != 3: 

178 raise ValueError( 

179 f"The forces vector must have dimensions [3x1] not [{force.size}x1]" 

180 ) 

181 

182 function_array = _ensure_length_of_function_array(function_array, 3) 

183 

184 # Add the function to the input file, if they are not previously added. 

185 for function in function_array: 

186 input_file.add(function) 

187 

188 # Create GeometrySet with nodes. 

189 mesh_nodes = [input_file.nodes[i_node] for i_node in nodes] 

190 geo = _GeometrySet(mesh_nodes) 

191 

192 # Create the Boundary Condition. 

193 bc = _BoundaryCondition( 

194 geo, 

195 { 

196 "NUMDOF": n_dof, 

197 "ONOFF": [1, 1, 1] + [0] * (n_dof - 3), 

198 "VAL": force.tolist() + [0] * (n_dof - 3), 

199 "FUNCT": function_array + [0] * (n_dof - 3), 

200 }, 

201 bc_type=_mpy.bc.neumann, 

202 ) 

203 input_file.add(bc) 

204 

205 

206def dbc_monitor_to_input_all_values( 

207 input_file: _InputFile, 

208 file_path: str, 

209 *, 

210 steps: list[int] = [], 

211 time_span: list[int] = [0, 1, 2], 

212 type: _Optional[str] = "linear", 

213 flip_time_values: bool = False, 

214 functions: list[_Function] = [], 

215 **kwargs, 

216): 

217 """Extracts all the force values of the monitored Dirichlet boundary 

218 condition and converts them into a Function with a Neumann boundary 

219 condition for the input_file. The monitor log force values must be obtained 

220 from a previous simulation with constant step size. The discretization of 

221 the previous simulation must be identical to the one within the input_file. 

222 The extracted force values are passed to a linear interpolation 

223 4C-function. It is advisable to only call this function once all nodes have 

224 been added to the input file. 

225 

226 Args 

227 ---- 

228 input_file: InputFile 

229 The input file where the created Neumann boundary condition is added 

230 to. The nodes(e.g., discretization) referred to in the log file must match with the ones 

231 in input_file. 

232 file_path: str 

233 Path to the Dirichlet boundary condition log file. 

234 steps: [int,int] 

235 Index range of which the force values are extracted. Default 0 and -1 extracts every point from the array. 

236 time_span: [t1, t2, t3] in float 

237 Transforms the given time array into this specific format. 

238 The time array always starts at 0 and ends at t3 to ensure a valid simulation. 

239 type: str or linear 

240 two types are available: 

241 1) "linear": not specified simply extract all values and apply them between time interval t1 and t2. 

242 2) "hat": puts the values first until last value is reached and then decays them back to first value. 

243 Interpolation starting from t1 going to the last value at (t1+t2)/2 and going back to the value at time t2. 

244 flip_time_values: bool 

245 indicates, if the extracted forces should be flipped or rearranged wrt. to the time 

246 For flip_time_values=true, the forces at the final time are applied at t_start. 

247 functions: [Function, Function, Function] 

248 Array consisting of 3 custom functions(x,y,z). The value for boundary condition is selected from the last steps. 

249 """ 

250 

251 nodes, time, force, _ = read_dbc_monitor_file(file_path) 

252 

253 # The forces are the negative reactions at the Dirichlet boundaries. 

254 force *= -1.0 

255 

256 # if special index range is provided use it 

257 if steps: 

258 time = time[steps[0] : steps[1] + 1] 

259 force = force[steps[0] : steps[1] + 1, :] 

260 else: 

261 # otherwise define steps from start to end 

262 steps = [0, -1] 

263 

264 # apply transformations to time and forces according to the schema 

265 if type == "linear": 

266 if not len(time_span) == 2: 

267 raise ValueError( 

268 f"Please provide a time_span with size 1x2 not {len(time_span)}" 

269 ) 

270 

271 time, force = linear_time_transformation( 

272 time, force, time_span, flip=flip_time_values 

273 ) 

274 if len(functions) != 3: 

275 print("Please provide a list with three valid Functions.") 

276 

277 elif type == "hat": 

278 if not len(time_span) == 3: 

279 raise ValueError( 

280 f"Please provide a time_span with size 1x3 not {len(time_span)}" 

281 ) 

282 

283 if functions: 

284 print( 

285 "You selected type", 

286 type, 

287 ", however the provided functions ", 

288 functions, 

289 " are overwritten.", 

290 ) 

291 functions = [] 

292 

293 # create the two intervals 

294 time1, force1 = linear_time_transformation( 

295 time, force, time_span[0:2], flip=flip_time_values 

296 ) 

297 time2, force2 = linear_time_transformation( 

298 time, force, time_span[1:3], flip=(not flip_time_values) 

299 ) 

300 

301 # remove first element since it is duplicated zero 

302 _np.delete(time2, 0) 

303 _np.delete(force2, 0) 

304 

305 # add the respective force 

306 time = _np.concatenate((time1, time2[1:])) 

307 force = _np.concatenate((force1, force2[1:]), axis=0) 

308 

309 else: 

310 raise ValueError( 

311 "The selected type: " 

312 + str(type) 

313 + " is currently not supported. Feel free to add it here." 

314 ) 

315 

316 # overwrite the function, if one is provided since for the specific types the function is generated 

317 if not type == "linear": 

318 for dim in range(force.shape[1]): 

319 # create a linear function with the force values per dimension 

320 fun = _create_linear_interpolation_function( 

321 time, force[:, dim], function_type="SYMBOLIC_FUNCTION_OF_TIME" 

322 ) 

323 

324 # add the function to the input array 

325 input_file.add(fun) 

326 

327 # store function 

328 functions.append(fun) 

329 

330 # now set forces to 1 since the force values are already extracted in the function's values 

331 force = _np.zeros_like(force) + 1.0 

332 

333 elif len(functions) != 3: 

334 raise ValueError("Please provide functions with ") 

335 

336 # Create condition in input file. 

337 add_point_neuman_condition_to_input_file( 

338 input_file, nodes, functions, force[steps[1]], **kwargs 

339 ) 

340 

341 

342def dbc_monitor_to_input( 

343 input_file: _InputFile, 

344 file_path: str, 

345 *, 

346 step: int = -1, 

347 function: _Function, 

348 **kwargs, 

349): 

350 """Converts the last value of a Dirichlet boundary condition monitor log to 

351 a Neumann boundary condition input section. 

352 

353 Args 

354 ---- 

355 input_file: InputFile 

356 The input file where the created Neumann boundary condition is added 

357 to. The nodes referred to in the log file have to match with the ones 

358 in the input section. It is advisable to only call this function once 

359 all nodes have been added to the input file. 

360 file_path: str 

361 Path to the Dirichlet boundary condition log file. 

362 step: int 

363 Step values to be used. Default is -1, i.e. the last step. 

364 function: Function 

365 Function for the Neumann boundary condition. 

366 """ 

367 

368 # read the force 

369 nodes, _, force, _ = read_dbc_monitor_file(file_path) 

370 

371 # The forces are the negative reactions at the Dirichlet boundaries. 

372 force *= -1.0 

373 

374 # Create condition in input file. 

375 add_point_neuman_condition_to_input_file( 

376 input_file, nodes, [function] * 3, force[step], **kwargs 

377 )