Coverage for src / sdynpy / signal_processing / sdynpy_integration.py: 14%
66 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-11 16:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-11 16:22 +0000
1# -*- coding: utf-8 -*-
2"""
3Functions for integrating equations of motion in time.
4"""
5"""
6Copyright 2022 National Technology & Engineering Solutions of Sandia,
7LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
8Government retains certain rights in this software.
10This program is free software: you can redistribute it and/or modify
11it under the terms of the GNU General Public License as published by
12the Free Software Foundation, either version 3 of the License, or
13(at your option) any later version.
15This program is distributed in the hope that it will be useful,
16but WITHOUT ANY WARRANTY; without even the implied warranty of
17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18GNU General Public License for more details.
20You should have received a copy of the GNU General Public License
21along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""
24import numpy as np
25import scipy.signal as signal
28def MCK_to_StateSpace(M, C, K):
30 ndofs = M.shape[0]
32 # A = [[ 0, I],
33 # [M^-1*K,M^-1*C]]
35 A_state = np.block([[np.zeros((ndofs, ndofs)), np.eye(ndofs)],
36 [-np.linalg.solve(M, K), -np.linalg.solve(M, C)]])
38 # B = [[ 0, M^-1]]
40 B_state = np.block([[np.zeros((ndofs, ndofs))], [np.linalg.inv(M)]])
42 # C = [[ I, 0], # Displacements
43 # [ 0, I], # Velocities
44 # [M^-1*K,M^-1*C], # Accelerations
45 # [ 0, 0]] # Forces
47 C_state = np.block([[np.eye(ndofs), np.zeros((ndofs, ndofs))],
48 [np.zeros((ndofs, ndofs)), np.eye(ndofs)],
49 [-np.linalg.solve(M, K), -np.linalg.solve(M, C)],
50 [np.zeros((ndofs, ndofs)), np.zeros((ndofs, ndofs))]])
52 # D = [[ 0], # Displacements
53 # [ 0], # Velocities
54 # [ M^-1], # Accelerations
55 # [ I]] # Forces
57 D_state = np.block([[np.zeros((ndofs, ndofs))],
58 [np.zeros((ndofs, ndofs))],
59 [np.linalg.inv(M)],
60 [np.eye(ndofs)]])
62 return A_state, B_state, C_state, D_state
65def MCK_incomplete_to_StateSpace(M, C, K, massless_dofs=[]):
66 nomass = len(massless_dofs)
67 mass = M.shape[0] - nomass
68 mass_dofs = np.array([v for v in range(M.shape[0]) if v not in massless_dofs])
69 massless_dofs = np.array(massless_dofs)
71 Kmm = K[mass_dofs[:, np.newaxis], mass_dofs]
72 Kmn = K[mass_dofs[:, np.newaxis], massless_dofs]
73 Knm = K[massless_dofs[:, np.newaxis], mass_dofs]
74 Knn = K[massless_dofs[:, np.newaxis], massless_dofs]
76 Cmm = C[mass_dofs[:, np.newaxis], mass_dofs]
77 Cnm = C[massless_dofs[:, np.newaxis], mass_dofs]
78 Cnn = C[massless_dofs[:, np.newaxis], massless_dofs]
80 Mmm = M[mass_dofs[:, np.newaxis], mass_dofs]
82 A = np.block([
83 [np.zeros((mass, mass)), np.eye(mass), np.zeros((mass, nomass))],
84 [-np.linalg.solve(Mmm, Kmm), -np.linalg.solve(Mmm, Cmm), -np.linalg.solve(Mmm, Kmn)],
85 [-np.linalg.solve(Cnn, Knm), -np.linalg.solve(Cnn, Cnm), -np.linalg.solve(Cnn, Knn)]
86 ])
88 B = np.block([
89 [np.zeros((mass, mass)), np.zeros((mass, nomass))],
90 [np.linalg.inv(Mmm), np.zeros((mass, nomass))],
91 [np.zeros((nomass, mass)), np.linalg.inv(Cnn)]
92 ])
94 C = np.block([
95 [np.eye(mass), np.zeros((mass, mass)), np.zeros((mass, nomass))], # Massive displacements
96 [np.zeros((mass, mass)), np.eye(mass), np.zeros((mass, nomass))], # Massive velocities
97 [-np.linalg.solve(Mmm, Kmm), -np.linalg.solve(Mmm, Cmm), - \
98 np.linalg.solve(Mmm, Kmn)], # Massive Accelerations
99 [np.zeros((mass, mass)), np.zeros((mass, mass)),
100 np.zeros((mass, nomass))], # Massive Inputs
101 [np.zeros((nomass, mass)), np.zeros((nomass, mass)),
102 np.eye(nomass)], # Nonmassive displacements
103 [-np.linalg.solve(Cnn, Knm), -np.linalg.solve(Cnn, Cnm), - \
104 np.linalg.solve(Cnn, Knn)], # Nonmassive velocities
105 [np.zeros((nomass, mass)), np.zeros((nomass, mass)),
106 np.zeros((nomass, nomass))], # Nonmassive inputs
107 ])
109 D = np.block([
110 [np.zeros((mass, mass)), np.zeros((mass, nomass))], # Massive displacements
111 [np.zeros((mass, mass)), np.zeros((mass, nomass))], # Massive velocities
112 [np.linalg.inv(Mmm), np.zeros((mass, nomass))], # Massive Accelerations
113 [np.eye(mass), np.zeros((mass, nomass))], # Massive Inputs
114 [np.zeros((nomass, mass)), np.zeros((nomass, nomass))], # Nonmassive displacements
115 [np.zeros((nomass, mass)), np.linalg.inv(Cnn)], # Nonmassive velocities
116 [np.zeros((nomass, mass)), np.eye(nomass)], # Nonmassive inputs
117 ])
119 doflist = {}
120 doflist['state'] = [(dof, modifier) for modifier in ['', 'd']
121 for dof in mass_dofs] + [(dof, '') for dof in massless_dofs]
122 doflist['input'] = [(dof, 'in') for dof in mass_dofs] + [(dof, 'in') for dof in massless_dofs]
123 doflist['output'] = [(dof, modifier) for modifier in ['', 'd', 'dd', 'in'] for dof in mass_dofs] + [
124 (dof, modifier) for modifier in ['', 'd', 'in'] for dof in massless_dofs]
126 return A, B, C, D, doflist
129def integrate_MCK(M, C, K, times, forces, x0=None):
130 A, B, C, D = MCK_to_StateSpace(M, C, K)
131 linear_system = signal.StateSpace(A, B, C, D)
133 times_out, sys_out, x_out = signal.lsim(linear_system, forces, times, x0)
135 sys_disps = sys_out[:, 0 * M.shape[0]:1 * M.shape[0]]
136 sys_vels = sys_out[:, 1 * M.shape[0]:2 * M.shape[0]]
137 sys_accels = sys_out[:, 2 * M.shape[0]:3 * M.shape[0]]
138 sys_forces = sys_out[:, 3 * M.shape[0]:4 * M.shape[0]]
140 return sys_disps, sys_vels, sys_accels, sys_forces
143def modal_parameters_to_MCK(natural_frequencies, damping_ratios):
144 nmodes = natural_frequencies.size
145 M = np.eye(nmodes)
146 K = np.diag((natural_frequencies * 2 * np.pi)**2)
147 C = np.diag((2 * 2 * np.pi * natural_frequencies * damping_ratios))
148 return M, C, K
151def integrate_modes(natural_frequencies, damping_ratios, modal_forces, times, return_accel=False):
152 M, C, K = modal_parameters_to_MCK(natural_frequencies, damping_ratios)
153 q = integrate_MCK(M, C, K, times, modal_forces)
154 if return_accel:
155 return q[2]
156 else:
157 return q[0]
160def frequency_domain_differentiation(abscissa, signal, order=1, axis=-1, fmin=-1, fmax=np.inf):
161 dt = np.mean(np.diff(abscissa))
162 n = signal.shape[axis]
163 freqs = np.fft.fftfreq(n, dt)
164 frequency_indices = np.logical_and(np.abs(freqs) > fmin, np.abs(freqs) < fmax)
165 fft = np.fft.fft(signal, axis=axis)
166 # Construct tuples for multidimensional analysis
167 slice_array = tuple([frequency_indices if i == axis or (i - axis) ==
168 fft.ndim else np.newaxis for i in range(fft.ndim)])
169 freq_slice_array = tuple([frequency_indices if i == axis or (
170 i - axis) == fft.ndim else slice(None, None, None) for i in range(fft.ndim)])
171 not_freq_slice_array = tuple([np.logical_not(frequency_indices) if i == axis or (
172 i - axis) == fft.ndim else slice(None, None, None) for i in range(fft.ndim)])
173 factor = (1j * 2 * np.pi * freqs[slice_array])**order
174 # Perform integration/differentiation
175 fft[freq_slice_array] *= factor
176 fft[not_freq_slice_array] = 0 + 0j
177 return np.real(np.fft.ifft(fft, axis=axis))