Coverage for  / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / components / exodus_modal_solution_hardware.py: 24%

200 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-27 18:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Synthetic "hardware" that allows the responses to be simulated by integrating 

4modal equations of motion nearly real-time. 

5 

6Rattlesnake Vibration Control Software 

7Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC 

8(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

9Government retains certain rights in this software. 

10 

11This program is free software: you can redistribute it and/or modify 

12it under the terms of the GNU General Public License as published by 

13the Free Software Foundation, either version 3 of the License, or 

14(at your option) any later version. 

15 

16This program is distributed in the hope that it will be useful, 

17but WITHOUT ANY WARRANTY; without even the implied warranty of 

18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19GNU General Public License for more details. 

20 

21You should have received a copy of the GNU General Public License 

22along with this program. If not, see <https://www.gnu.org/licenses/>. 

23""" 

24 

25import multiprocessing as mp 

26import time 

27from typing import List 

28 

29import netCDF4 

30import numpy as np 

31import scipy.signal as signal 

32 

33from .abstract_hardware import HardwareAcquisition, HardwareOutput 

34from .utilities import Channel, DataAcquisitionParameters, flush_queue 

35 

36DEBUG = False 

37 

38NOISE_LEVEL = 0.00000001 

39 

40if DEBUG: 

41 import glob 

42 import os 

43 

44 TEST_FILES_OUTPUT_NAMES = r"debug\output_data_{:}.npz" 

45 TEST_FILES_ACQUISITION_NAMES = r"debug\acquire_data_{:}.npz" 

46 TEST_FILES_SYSTEM_NAME = r"debug\system.npz" 

47 # Delete existing test files 

48 for str_format in [TEST_FILES_ACQUISITION_NAMES, TEST_FILES_OUTPUT_NAMES]: 

49 files = glob.glob(str_format.format("*")) 

50 for file in files: 

51 try: 

52 os.remove(file) 

53 except (FileNotFoundError, PermissionError): 

54 pass 

55 

56 

57class ExodusAcquisition(HardwareAcquisition): 

58 """Class defining the interface between the controller and synthetic acquisition 

59 

60 This class defines the interfaces between the controller and the 

61 data acquisition portion of the hardware. In this case, the hardware is 

62 actually simulated by integrating the modal equations of motion of a finite 

63 element model containing a modal solution. It is run by the Acquisition 

64 process, and must define how to get data from the test hardware into the 

65 controller.""" 

66 

67 def __init__(self, exodus_file: str, queue: mp.queues.Queue): 

68 """Loads in the Exodus file and sets initial parameters to null values 

69 

70 

71 Parameters 

72 ---------- 

73 exodus_file : str : 

74 Path to the Exodus file. 

75 queue : mp.queues.Queue 

76 A queue that passes input data from the ExodusOutput class to the 

77 Exodus input class. Normally, this data transfer would occur through 

78 the physical test object: the exciters would excite the test object 

79 with the specified excitation and the Acquisition would record the 

80 responses to that excitation. In the synthetic case, we need to 

81 pass the output data to the acquisition which does the integration. 

82 

83 """ 

84 self.exo = Exodus(exodus_file) 

85 self.phi = None 

86 self.phi_full = None 

87 self.response_channels: np.ndarray 

88 self.response_channels = None 

89 self.system = None 

90 self.times = None 

91 self.state = None 

92 self.frame_time = None 

93 self.queue = queue 

94 self.force_buffer = None 

95 self.integration_oversample = None 

96 self.acquisition_delay = None 

97 self.damping = None 

98 

99 def set_up_data_acquisition_parameters_and_channels( 

100 self, test_data: DataAcquisitionParameters, channel_data: List[Channel] 

101 ): 

102 """ 

103 Initialize the hardware and set up channels and sampling properties 

104 

105 The function must create channels on the hardware corresponding to 

106 the channels in the test. It must also set the sampling rates. 

107 

108 Parameters 

109 ---------- 

110 test_data : DataAcquisitionParameters : 

111 A container containing the data acquisition parameters for the 

112 controller set by the user. 

113 channel_data : List[Channel] : 

114 A list of ``Channel`` objects defining the channels in the test 

115 

116 Returns 

117 ------- 

118 None. 

119 

120 """ 

121 self.create_response_channels(channel_data) 

122 self.set_parameters(test_data) 

123 

124 def create_response_channels(self, channel_data: List[Channel]): 

125 """Method to set up response channels 

126 

127 This function takes channels from the supplied list of channels and 

128 extracts the mode shape coefficients corresponding to those channels. 

129 

130 Parameters 

131 ---------- 

132 channel_data : List[Channel] : 

133 A list of ``Channel`` objects defining the channels in the test 

134 

135 """ 

136 # print('{:} Channels'.format(len(channel_data))) 

137 displacements = self.exo.get_displacements() 

138 node_numbers = self.exo.get_node_num_map() 

139 try: 

140 self.damping = float(channel_data[0].comment) 

141 print(f"{self.damping} Damping") 

142 except ValueError: 

143 self.damping = 0.01 

144 self.response_channels = np.array( 

145 [ 

146 channel.feedback_device is None or channel.feedback_device == "" 

147 for channel in channel_data 

148 ], 

149 dtype="bool", 

150 ) 

151 self.phi_full = np.array( 

152 [self._create_channel(channel, displacements, node_numbers) for channel in channel_data] 

153 ) 

154 # Need to add a signal buffer in case the write size is not equal to 

155 # the read size 

156 self.force_buffer = np.zeros((0, np.sum(~self.response_channels))) 

157 

158 def set_parameters(self, test_data: DataAcquisitionParameters): 

159 """Method to set up sampling rate and other test parameters 

160 

161 For the synthetic case, we will set up the integration parameters using 

162 the sample rates provided. 

163 

164 Parameters 

165 ---------- 

166 test_data : DataAcquisitionParameters : 

167 A container containing the data acquisition parameters for the 

168 controller set by the user. 

169 

170 """ 

171 # Get the number of modes that we will keep (bandwidth*1.5) 

172 frequencies = self.exo.get_times() 

173 frequencies[frequencies < 0] = 0 # Eliminate any negative frequencies 

174 keep_modes = frequencies < test_data.nyquist_frequency * 1.5 

175 # Oversampling due to integration 

176 self.integration_oversample = test_data.output_oversample 

177 self.phi = self.phi_full[..., keep_modes] 

178 nmodes = self.phi.shape[-1] 

179 frequencies = frequencies[keep_modes] 

180 m = np.eye(nmodes) 

181 k = np.diag((frequencies * 2 * np.pi) ** 2) 

182 c = np.diag((2 * 2 * np.pi * frequencies * self.damping)) 

183 a, b, c, d = mck_to_state_space(m, c, k) 

184 self.system = signal.StateSpace(a, b, c, d) 

185 # Need to get one more sample than you would think because lsim doesn't bridge the gap 

186 # between integrations 

187 self.times = np.arange(test_data.samples_per_read * self.integration_oversample + 1) / ( 

188 test_data.sample_rate * self.integration_oversample 

189 ) 

190 self.frame_time = test_data.samples_per_read / test_data.sample_rate 

191 self.state = np.zeros(nmodes * 2) 

192 self.acquisition_delay = test_data.samples_per_write / test_data.output_oversample 

193 if DEBUG: 

194 np.savez(TEST_FILES_SYSTEM_NAME, a=a, b=b, c=c, d=d, times=self.times) 

195 

196 def start(self): 

197 """Method to start acquiring data. 

198 

199 For the synthetic case, it simply initializes the state of the system to zero""" 

200 self.state[:] = 0 

201 

202 def get_acquisition_delay(self) -> int: 

203 """ 

204 Get the number of samples between output and acquisition. 

205 

206 This function returns the number of samples that need to be read to 

207 ensure that the last output is read by the acquisition. If there is 

208 buffering in the output, this delay should be adjusted accordingly. 

209 

210 Returns 

211 ------- 

212 int 

213 Number of samples between when a dataset is written to the output 

214 and when it has finished playing. 

215 

216 """ 

217 return self.acquisition_delay 

218 

219 def read(self): 

220 """Method to read a frame of data from the hardware 

221 

222 This function gets the force from the output queue and adds it to the 

223 buffer of time signals that represents the force. It then integrates 

224 a frame of time and sends it to the acquisition. 

225 

226 Returns 

227 ------- 

228 read_data : 

229 2D Data read from the controller with shape ``n_channels`` x 

230 ``n_samples`` 

231 

232 """ 

233 start_time = time.time() 

234 while self.force_buffer.shape[0] < self.times.size: 

235 try: 

236 forces = self.queue.get(timeout=self.frame_time) 

237 except mp.queues.Empty: 

238 # If we don't get an output in time, this likely means output has stopped so 

239 # just put zeros. 

240 forces = np.zeros((self.force_buffer.shape[-1], self.times.size)) 

241 self.force_buffer = np.concatenate((self.force_buffer, forces.T), axis=0) 

242 

243 # Now extract a force that is the correct size 

244 this_force = self.force_buffer[: self.times.size] 

245 # And leave the rest for next time 

246 # Note we have to keep the last force sample still on the 

247 # buffer because it will be the next force sample we use 

248 self.force_buffer = self.force_buffer[self.times.size - 1 :] 

249 

250 # print('Got Force') 

251 # print('this_force shape: {:}'.format(this_force.shape)) 

252 

253 modal_forces = np.einsum("ij,ki->kj", self.phi[~self.response_channels], this_force) 

254 # print('modal_forces shape: {:}'.format(modal_forces.shape)) 

255 

256 # print('Integrating...') 

257 # print('system: {:}'.format(self.system)) 

258 # print('forces: {:}\n{:}'.format(modal_forces.shape,modal_forces)) 

259 # print('times: {:}\n{:}'.format(self.times.shape,self.times)) 

260 # print('state: {:}\n{:}'.format(self.state.shape,self.state)) 

261 

262 times_out, sys_out, _ = signal.lsim(self.system, modal_forces, self.times, self.state) 

263 # print('output: {:}\n{:}'.format(sys_out.shape,sys_out)) 

264 

265 sys_accels = sys_out[:, 2 * self.phi.shape[-1] : 3 * self.phi.shape[-1]] 

266 

267 accelerations = self.phi[self.response_channels] @ sys_accels.T 

268 

269 self.state[:] = sys_out[-1, 0 : 2 * self.phi.shape[-1]] 

270 

271 # Now we need to combine accelerations with forces in the same way 

272 output = np.zeros((len(self.response_channels), len(self.times))) # n channels x n times 

273 output[self.response_channels] = accelerations 

274 output[~self.response_channels] = this_force.T 

275 

276 # print('output: {:}\n{:}'.format(output.shape,output)) 

277 

278 integration_time = time.time() - start_time 

279 remaining_time = self.frame_time - integration_time 

280 if remaining_time > 0.0: 

281 time.sleep(remaining_time) 

282 

283 if DEBUG: 

284 # Find current files 

285 num_files = len(glob.glob(TEST_FILES_ACQUISITION_NAMES.format("*"))) 

286 np.savez( 

287 TEST_FILES_ACQUISITION_NAMES.format(num_files), 

288 full_output=output, 

289 integration_oversample=self.integration_oversample, 

290 modal_forces=modal_forces, 

291 forces=this_force, 

292 integration_output=sys_out, 

293 times=times_out, 

294 state=self.state, 

295 response_channels=self.response_channels, 

296 ) 

297 # We don't want to return the last sample because it 

298 # will be the initial state for the next sample 

299 return output[..., : -1 : self.integration_oversample] + NOISE_LEVEL * np.random.randn( 

300 *output[..., : -1 : self.integration_oversample].shape 

301 ) 

302 

303 def read_remaining(self): 

304 """Method to read the rest of the data on the acquisition 

305 

306 This function simply returns one sample of zeros. 

307 

308 Returns 

309 ------- 

310 read_data : 

311 2D Data read from the controller with shape ``n_channels`` x 

312 ``n_samples`` 

313 """ 

314 return np.zeros((len(self.response_channels), 1)) 

315 

316 def stop(self): 

317 """Method to stop the acquisition. 

318 

319 This simply sets the state to zero.""" 

320 self.state[:] = 0 

321 

322 def close(self): 

323 """Method to close down the hardware 

324 

325 This simply closes the Exodus file.""" 

326 self.exo.close() 

327 

328 def _create_channel(self, channel: Channel, displacement, node_numbers): 

329 """Helper function to create a channel from the Exodus file. 

330 

331 This function parses the channel information and then extracts the 

332 mode shape row corresponding to that channel. 

333 

334 Parameters 

335 ---------- 

336 channel: Channel : 

337 ``Channel`` object specifying the channel information 

338 

339 displacement : 

340 The 3D mode shape matrix consisting of direction x mode x node 

341 dimensions. 

342 

343 node_numbers : 

344 The node numbers in the finite element model, either increasing 

345 from one to the largest node or from the node num map. 

346 

347 Returns 

348 ------- 

349 phi_row : 

350 A row of the mode shape matrix corresponding to the channel 

351 

352 """ 

353 node_number = int(channel.node_number) 

354 node_index = np.where(node_numbers == node_number)[0][0] 

355 if channel.node_direction.lower().replace(" ", "") in ["x+", "+x"]: 

356 direction = np.array([1, 0, 0]) 

357 elif channel.node_direction.lower().replace(" ", "") in ["x-", "-x"]: 

358 direction = np.array([-1, 0, 0]) 

359 elif channel.node_direction.lower().replace(" ", "") in ["y+", "+y"]: 

360 direction = np.array([0, 1, 0]) 

361 elif channel.node_direction.lower().replace(" ", "") in ["y-", "-y"]: 

362 direction = np.array([0, -1, 0]) 

363 elif channel.node_direction.lower().replace(" ", "") in ["z+", "+z"]: 

364 direction = np.array([0, 0, 1]) 

365 elif channel.node_direction.lower().replace(" ", "") in ["z-", "-z"]: 

366 direction = np.array([0, 0, -1]) 

367 else: 

368 direction = np.array([float(val) for val in channel.node_direction.split(",")]) 

369 direction /= np.linalg.norm(direction) 

370 phi_row = np.einsum("i,ij", direction, displacement[..., node_index]) 

371 return phi_row 

372 

373 

374class ExodusOutput(HardwareOutput): 

375 """Class defining the interface between the controller and synthetic output 

376 

377 Note that the only thing that this class does is pass data to the acquisition 

378 hardware task which actually performs the integration. Therefore, many of 

379 the functions here are actually empty.""" 

380 

381 def __init__(self, queue: mp.queues.Queue): 

382 """ 

383 Initializes the hardware by simply storing the data passing queue. 

384 

385 Parameters 

386 ---------- 

387 queue : mp.queues.Queue 

388 Queue used to pass data from output to acquisition for integration. 

389 See ``ExodusAcquisition.__init__`` 

390 

391 """ 

392 self.queue = queue 

393 

394 def set_up_data_output_parameters_and_channels( 

395 self, test_data: DataAcquisitionParameters, channel_data: List[Channel] 

396 ): 

397 """ 

398 Initialize the hardware and set up sources and sampling properties 

399 

400 This does nothing for the synthetic hardware 

401 

402 Parameters 

403 ---------- 

404 test_data : DataAcquisitionParameters : 

405 A container containing the data acquisition parameters for the 

406 controller set by the user. 

407 channel_data : List[Channel] : 

408 A list of ``Channel`` objects defining the channels in the test 

409 

410 Returns 

411 ------- 

412 None. 

413 

414 """ 

415 

416 def start(self): 

417 """Method to start acquiring data 

418 

419 Does nothing for synthetic hardware.""" 

420 

421 def write(self, data: np.ndarray): 

422 """Method to write a frame of data 

423 

424 For the synthetic excitation, this simply puts the data into the data- 

425 passing queue. 

426 

427 Parameters 

428 ---------- 

429 data : np.ndarray 

430 Data to write to the output. 

431 

432 """ 

433 if DEBUG: 

434 # Find current files 

435 num_files = len(glob.glob(TEST_FILES_OUTPUT_NAMES.format("*"))) 

436 np.savez(TEST_FILES_OUTPUT_NAMES.format(num_files), forces=data) 

437 self.queue.put(data) 

438 

439 def stop(self): 

440 """Method to stop the acquisition 

441 

442 Does nothing for synthetic hardware.""" 

443 flush_queue(self.queue) 

444 

445 def close(self): 

446 """Method to close down the hardware 

447 

448 Does nothing for synthetic hardware.""" 

449 

450 def ready_for_new_output(self): 

451 """Signals that the hardware is ready for new output 

452 

453 Returns ``True`` if the data-passing queue is empty. 

454 """ 

455 return self.queue.empty() 

456 

457 

458class ExodusError(Exception): 

459 """An exception to specify an error has occured in the Exodus reader""" 

460 

461 

462class Exodus: 

463 """A class with limited Exodus file read capabilities""" 

464 

465 def __init__(self, filename): 

466 """ 

467 Read in an Exodus file given by ``filename`` 

468 

469 Parameters 

470 ---------- 

471 filename : str 

472 Path to the Exodus file to read. 

473 

474 """ 

475 self.filename = filename 

476 self._ncdf_handle = netCDF4.Dataset(filename, "r") # pylint: disable=no-member 

477 

478 @property 

479 def num_dimensions(self): 

480 """Property to get the number of dimensions in an Exodus file""" 

481 return self._ncdf_handle.dimensions["num_dim"].size 

482 

483 @property 

484 def num_nodes(self): 

485 """Property to get the number of nodes in an Exodus file""" 

486 return self._ncdf_handle.dimensions["num_nodes"].size 

487 

488 @property 

489 def num_times(self): 

490 """Property to get the number of time steps in an Exodus file""" 

491 return self._ncdf_handle.dimensions["time_step"].size 

492 

493 def get_node_num_map(self): 

494 """Retrieve the list of local node IDs from the Exodus file. 

495 

496 Returns 

497 ------- 

498 node_num_map : np.array 

499 Returns a 1D array with size num_nodes, denoting the node number 

500 for the node in each index 

501 

502 Notes 

503 ----- 

504 If there is no node_num_map in the Exodus file, this function simply 

505 returns an array from 1 to self.num_nodes 

506 """ 

507 if "node_num_map" in self._ncdf_handle.variables: 

508 return self._ncdf_handle.variables["node_num_map"][:] 

509 else: 

510 return np.ma.MaskedArray(np.arange(self.num_nodes) + 1) 

511 

512 def get_displacements(self, displacement_name="Disp", capital_coordinates=True): 

513 """Get the displacements from the Exodus file. 

514 

515 Parameters 

516 ---------- 

517 displacement_name : 

518 The prefix of the displacement variable (Default value = 'Disp') 

519 capital_coordinates : 

520 Whether or not the X,Y,Z values appended to the displacement name 

521 are capitalized. (Default value = True) 

522 

523 Returns 

524 ------- 

525 displacement_array : np.ndarray 

526 Returns a 3D array with size num_dimensions, num_times,num_nodes 

527 containing the displacements in each direction at each mode shape 

528 at each node. 

529 """ 

530 return np.array( 

531 [ 

532 self.get_node_variable_values( 

533 displacement_name + (val.upper() if capital_coordinates else val.lower()) 

534 ) 

535 for val in "xyz"[: self.num_dimensions] 

536 ] 

537 ) 

538 

539 def get_coords(self): 

540 """Retrieve the coordinates of the nodes in the Exodus file. 

541 

542 Returns 

543 ------- 

544 coordinate_array : np.ndarray 

545 Returns a 2D array with size num_dimensions, num_nodes 

546 containing the position of each node. 

547 """ 

548 # TODO Add error checking 

549 coord_names = ("coordx", "coordy", "coordz")[: self.num_dimensions] 

550 raw_list = [self._ncdf_handle.variables[name][:] for name in coord_names] 

551 coords = np.array(raw_list) 

552 return coords 

553 

554 def get_node_variable_names(self): 

555 """Gets a tuple of nodal variable names from the Exodus file 

556 

557 Returns 

558 ------- 

559 node_var_names : tuple(str) 

560 A tuple of strings corresponding to the variable names in the 

561 model.""" 

562 try: 

563 raw_records = self._ncdf_handle.variables["name_nod_var"] 

564 except KeyError as e: 

565 raise ExodusError("Node Variable Names are not defined!") from e 

566 node_var_names = tuple( 

567 "".join( 

568 value.decode() for value in line if not isinstance(value, np.ma.core.MaskedConstant) 

569 ) 

570 for line in raw_records 

571 ) 

572 return node_var_names 

573 

574 def get_node_variable_values(self, name_or_index, step=None): 

575 """Gets the node variable values for the specified timestep 

576 

577 Parameters 

578 ---------- 

579 name_or_index : str or int 

580 Name or Index of the nodal variable that is desired. If 

581 type(name_or_index) == str, then it is assumed to be the name. If 

582 type(name_or_index) == int, then it is assumed to be the index. 

583 step : int 

584 Time step at which to recover the nodal variable (Default value = None) 

585 

586 Returns 

587 ------- 

588 node_var_values : np.ndarray 

589 A 1D or 2D numpy array of variable values depending on whether a 

590 time step is specified or not. 

591 

592 """ 

593 if isinstance(name_or_index, (int, np.integer)): 

594 index = name_or_index 

595 elif isinstance(name_or_index, (str, np.character)): 

596 try: 

597 index = self.get_node_variable_names().index(name_or_index) 

598 except ValueError as e: 

599 raise ExodusError( 

600 f"Name {name_or_index} not found in self.get_node_variable_names(). " 

601 f"Options are {self.get_node_variable_names()}" 

602 ) from e 

603 else: 

604 raise ValueError("name_or_index must be integer or string") 

605 vals_nod_var_name = f"vals_nod_var{index + 1:d}" 

606 if step is not None: 

607 if step >= self.num_times: 

608 raise ExodusError("Invalid Time Step") 

609 return self._ncdf_handle.variables[vals_nod_var_name][step, :] 

610 return self._ncdf_handle.variables[vals_nod_var_name][:] 

611 

612 def get_times(self): 

613 """Gets the time values from the Exodus file 

614 

615 Returns 

616 ------- 

617 times : np.ndarray 

618 A vector of time values in the Exodus file. These may be 

619 frequencies if the Exodus file contains a modal or frequency-based 

620 solution. 

621 """ 

622 return self._ncdf_handle.variables["time_whole"][:] 

623 

624 def close(self): 

625 """Closes the Exodus file""" 

626 self._ncdf_handle.close() 

627 

628 

629def mck_to_state_space(m, c, k): 

630 """Creates a state-space representation from a mass, stiffness, and damping matrix. 

631 

632 Parameters 

633 ---------- 

634 m : np.ndarray 

635 A square array defining the system mass matrix. 

636 c : np.ndarray 

637 A square array defining the system damping matrix 

638 k : np.ndarray 

639 A square array defining the system stiffness matrix 

640 

641 Returns 

642 ------- 

643 a : np.ndarray 

644 The state space A matrix 

645 b : np.ndarray 

646 The state space B matrix 

647 c : np.ndarray 

648 The state space C matrix 

649 d : np.ndarray 

650 The state space D matrix 

651 """ 

652 

653 ndofs = m.shape[0] 

654 

655 # a = [[ 0, I], 

656 # [-m^-1*k,-m^-1*c]] 

657 

658 a_state = np.block( 

659 [ 

660 [np.zeros((ndofs, ndofs)), np.eye(ndofs)], 

661 [-np.linalg.solve(m, k), -np.linalg.solve(m, c)], 

662 ] 

663 ) 

664 

665 # b = [[ 0, m^-1]] 

666 

667 b_state = np.block([[np.zeros((ndofs, ndofs))], [np.linalg.inv(m)]]) 

668 

669 # c = [[ I, 0], # Displacements 

670 # [ 0, I], # Velocities 

671 # [-m^-1*k,-m^-1*c], # Accelerations 

672 # [ 0, 0]] # Forces 

673 

674 c_state = np.block( 

675 [ 

676 [np.eye(ndofs), np.zeros((ndofs, ndofs))], 

677 [np.zeros((ndofs, ndofs)), np.eye(ndofs)], 

678 [-np.linalg.solve(m, k), -np.linalg.solve(m, c)], 

679 [np.zeros((ndofs, ndofs)), np.zeros((ndofs, ndofs))], 

680 ] 

681 ) 

682 

683 # d = [[ 0], # Displacements 

684 # [ 0], # Velocities 

685 # [ m^-1], # Accelerations 

686 # [ I]] # Forces 

687 

688 d_state = np.block( 

689 [ 

690 [np.zeros((ndofs, ndofs))], 

691 [np.zeros((ndofs, ndofs))], 

692 [np.linalg.inv(m)], 

693 [np.eye(ndofs)], 

694 ] 

695 ) 

696 

697 return a_state, b_state, c_state, d_state 

698 

699 

700# def integrate_MCK(m,c,k,times,forces,x0=None): 

701# """Integrates a system defined by a Mass, Stiffness, and Damping Matrix 

702 

703# Parameters 

704# ---------- 

705# m : np.ndarray 

706# A square array defining the system mass matrix. 

707# c : np.ndarray 

708# A square array defining the system damping matrix 

709# k : np.ndarray 

710# A square array defining the system stiffness matrix 

711# times : 

712# A vector of times that will be passed to the ``scipy.signal.lsim`` 

713# function. 

714# forces : 

715# A vector of forces that will be passed to the ``scipy.signal.lsim`` 

716# function 

717# x0 : 

718# The initial state of the system that will be used by the ``scipy.signal.lsim`` 

719# function (Default value = None, meaning the system starts at rest) 

720 

721# Returns 

722# ------- 

723 

724# """ 

725# a,b,c,d = mck_to_state_space(m,c,k) 

726# linear_system = signal.StateSpace(a,b,c,d) 

727 

728# times_out,sys_out,x_out = signal.lsim(linear_system,forces,times,x0) 

729 

730# sys_disps = sys_out[:,0*m.shape[0]:1*m.shape[0]] 

731# sys_vels = sys_out[:,1*m.shape[0]:2*m.shape[0]] 

732# sys_accels = sys_out[:,2*m.shape[0]:3*m.shape[0]] 

733# sys_forces = sys_out[:,3*m.shape[0]:4*m.shape[0]] 

734 

735# return sys_disps,sys_vels,sys_accels,sys_forces 

736 

737# def integrate_modes(natural_frequencies,damping_ratios,modal_forces,times,q0,qd0): 

738# """Integrate a set of natural frequencies, damping ratios, and modal forces 

739 

740# Parameters 

741# ---------- 

742# natural_frequencies : np.ndarray 

743# A 1D array of natural frequencies for each mode 

744# damping_ratios : np.ndarray 

745# A 1D array of critical damping ratios for each mode 

746# modal_forces : np.ndarray 

747# A set of modal forces that will be passed to the integrate_MCK function 

748# times : np.ndarray 

749# A set of times that will be passed to the integrate_MCK function 

750# q0 : np.ndarray 

751# Initial values of the modal coefficients 

752# qd0 : np.ndarray 

753# Initial velocities of the modal coefficients 

754 

755 

756# Returns 

757# ------- 

758 

759# """ 

760# nmodes = natural_frequencies.size 

761# m = np.eye(nmodes) 

762# k = np.diag((natural_frequencies*2*np.pi)**2) 

763# c = np.diag((2*2*np.pi*natural_frequencies*damping_ratios)) 

764# q = integrate_MCK(m,c,k,times,modal_forces,np.concatenate((q0,qd0)))[:2] 

765# return q