Chapter 13: Vibrations and Multi-Degree-of-Freedom (MDOF) Systems

Lesson 3: Modal Coordinates and Decoupling of MDOF Systems

This lesson develops the modal (normal-mode) coordinate transformation that diagonalizes the undamped MDOF equations of motion. We prove the mass-orthogonality of mode shapes for symmetric \( \mathbf{M} \) and \( \mathbf{K} \), construct mass-normalized modal matrices, and derive the fully decoupled set of scalar second-order ODEs in modal coordinates. We also implement the complete workflow in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica.

1. Setup, Notation, and the Decoupling Goal

Consider an undamped linear MDOF vibration model with \( n \) generalized coordinates collected in \( \mathbf{x}(t)\in\mathbb{R}^n \). The equations of motion are

\[ \mathbf{M}\,\ddot{\mathbf{x} }(t) + \mathbf{K}\,\mathbf{x}(t) = \mathbf{f}(t), \quad \mathbf{M}=\mathbf{M}^\top \succ 0,\;\; \mathbf{K}=\mathbf{K}^\top \succeq 0. \]

The modal decoupling objective is to find an invertible coordinate transform \( \mathbf{x}=\mathbf{\Phi}\,\mathbf{q} \) such that the dynamics in \( \mathbf{q}(t) \) become (ideally) diagonal, i.e., a set of independent scalar oscillators:

\[ \ddot{q}_i(t) + \omega_i^2\,q_i(t) = p_i(t), \quad i=1,\dots,n, \]

where \( \omega_i \) are natural frequencies and \( p_i(t) \) are modal forces. This transformation is a change of basis in the configuration space: instead of describing motion using physical coordinates \( \mathbf{x} \), we describe motion as a combination of mode shapes (columns of \( \mathbf{\Phi} \)) weighted by modal coordinates \( \mathbf{q} \).

2. Generalized Eigenvalue Problem and Mode Shape Basis

From Lesson 2 (natural frequencies and normal modes), free vibration \( \mathbf{f}(t)=\mathbf{0} \) is sought in the harmonic form \( \mathbf{x}(t)=\boldsymbol{\phi}\,e^{j\omega t} \), yielding the generalized eigenproblem:

\[ \left(\mathbf{K}-\omega^2\mathbf{M}\right)\boldsymbol{\phi}=\mathbf{0} \quad \Longleftrightarrow \quad \mathbf{K}\boldsymbol{\phi} = \lambda\,\mathbf{M}\boldsymbol{\phi},\;\; \lambda=\omega^2. \]

For symmetric \( \mathbf{M}\succ 0 \) and symmetric \( \mathbf{K} \), the eigenvalues \( \lambda_i \) are real and (for typical structural systems with sufficient constraints) nonnegative. Let \( \{(\lambda_i,\boldsymbol{\phi}_i)\}_{i=1}^n \) denote eigenpairs with \( \lambda_i=\omega_i^2 \). We form the modal matrix

\[ \mathbf{\Phi} = \begin{bmatrix} \boldsymbol{\phi}_1 & \boldsymbol{\phi}_2 & \cdots & \boldsymbol{\phi}_n \end{bmatrix}. \]

The key property enabling decoupling is orthogonality with respect to \( \mathbf{M} \) (and also \( \mathbf{K} \)), proven next.

3. Orthogonality Theorem and Proof

Theorem (Mass orthogonality for distinct eigenvalues): If \( \mathbf{K}\boldsymbol{\phi}_i=\lambda_i\mathbf{M}\boldsymbol{\phi}_i \) and \( \mathbf{K}\boldsymbol{\phi}_j=\lambda_j\mathbf{M}\boldsymbol{\phi}_j \) with \( \lambda_i \neq \lambda_j \), then \( \boldsymbol{\phi}_i^\top \mathbf{M}\boldsymbol{\phi}_j = 0 \). Moreover, \( \boldsymbol{\phi}_i^\top \mathbf{K}\boldsymbol{\phi}_j = 0 \).

Proof: Start with the two eigen-relations:

\[ \mathbf{K}\boldsymbol{\phi}_i=\lambda_i\mathbf{M}\boldsymbol{\phi}_i,\quad \mathbf{K}\boldsymbol{\phi}_j=\lambda_j\mathbf{M}\boldsymbol{\phi}_j. \]

Left-multiply the first by \( \boldsymbol{\phi}_j^\top \) and the second by \( \boldsymbol{\phi}_i^\top \):

\[ \boldsymbol{\phi}_j^\top\mathbf{K}\boldsymbol{\phi}_i=\lambda_i\,\boldsymbol{\phi}_j^\top\mathbf{M}\boldsymbol{\phi}_i,\qquad \boldsymbol{\phi}_i^\top\mathbf{K}\boldsymbol{\phi}_j=\lambda_j\,\boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j. \]

Because \( \mathbf{K}=\mathbf{K}^\top \), we have \( \boldsymbol{\phi}_j^\top\mathbf{K}\boldsymbol{\phi}_i = (\boldsymbol{\phi}_i^\top\mathbf{K}\boldsymbol{\phi}_j)^\top = \boldsymbol{\phi}_i^\top\mathbf{K}\boldsymbol{\phi}_j \), and because \( \mathbf{M}=\mathbf{M}^\top \), \( \boldsymbol{\phi}_j^\top\mathbf{M}\boldsymbol{\phi}_i = \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j \). Subtract the two equations:

\[ \left(\lambda_i-\lambda_j\right)\,\boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j = 0. \]

Since \( \lambda_i\neq\lambda_j \), it follows that \( \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j = 0 \). Finally, \( \boldsymbol{\phi}_i^\top\mathbf{K}\boldsymbol{\phi}_j = \lambda_j \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j = 0 \), proving stiffness orthogonality as well. ∎

Repeated eigenvalues: if \( \lambda_i=\lambda_j \), the above argument does not force orthogonality, but one can always choose a basis inside that eigenspace to be \( \mathbf{M} \)-orthonormal via a Gram–Schmidt process using the inner product \( \langle \mathbf{u},\mathbf{v}\rangle_M = \mathbf{u}^\top\mathbf{M}\mathbf{v} \).

4. Mass Normalization and the Diagonal Modal Matrices

The eigenvectors can be scaled arbitrarily. A particularly useful scaling is mass normalization:

\[ \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_i = 1 \quad \Longrightarrow \quad \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi} = \mathbf{I}. \]

Under this choice, stiffness orthogonality becomes:

\[ \mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi} = \begin{bmatrix} \omega_1^2 & 0 & \cdots & 0\\ 0 & \omega_2^2 & \cdots & 0\\ \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & \cdots & \omega_n^2 \end{bmatrix} \;=\;\mathbf{\Omega}^2. \]

So the modal matrix simultaneously diagonalizes \( \mathbf{M} \) and \( \mathbf{K} \) under congruence transformations (transpose times matrix times itself). This is the mathematical core of decoupling.

A useful “inverse transform” identity follows immediately. If \( \mathbf{x}=\mathbf{\Phi}\mathbf{q} \) and \( \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}=\mathbf{I} \), then left-multiplying by \( \mathbf{\Phi}^\top\mathbf{M} \) yields \( \mathbf{q}=\mathbf{\Phi}^\top\mathbf{M}\mathbf{x} \). Thus, modal coordinates are simply physical coordinates projected using the \( \mathbf{M} \)-inner product.

5. Modal Coordinate Transformation and Exact Decoupling

Apply the transformation \( \mathbf{x}(t)=\mathbf{\Phi}\mathbf{q}(t) \). Since \( \mathbf{\Phi} \) is constant (time-invariant), we have \( \dot{\mathbf{x} }=\mathbf{\Phi}\dot{\mathbf{q} } \) and \( \ddot{\mathbf{x} }=\mathbf{\Phi}\ddot{\mathbf{q} } \). Substitute into \( \mathbf{M}\ddot{\mathbf{x} }+\mathbf{K}\mathbf{x}=\mathbf{f}(t) \):

\[ \mathbf{M}\mathbf{\Phi}\ddot{\mathbf{q} }(t) + \mathbf{K}\mathbf{\Phi}\mathbf{q}(t) = \mathbf{f}(t). \]

Now left-multiply by \( \mathbf{\Phi}^\top \):

\[ \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}\ddot{\mathbf{q} }(t) + \mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi}\mathbf{q}(t) = \mathbf{\Phi}^\top\mathbf{f}(t). \]

Using the mass-normalization identities from Section 4, we obtain the decoupled modal equations:

\[ \ddot{\mathbf{q} }(t) + \mathbf{\Omega}^2\,\mathbf{q}(t) = \mathbf{p}(t), \qquad \mathbf{p}(t)=\mathbf{\Phi}^\top\mathbf{f}(t). \]

Component-wise, this is: \( \ddot{q}_i(t) + \omega_i^2 q_i(t) = p_i(t) \), where \( p_i(t) = \boldsymbol{\phi}_i^\top\mathbf{f}(t) \). The system is now a set of independent scalar forced oscillators (undamped).

flowchart TD
  A["Physical model: M xdd + K x = f(t)"] --> B["Solve EVP: K phi = lambda M phi"]
  B --> C["Assemble Phi = [phi1 ... phin]"]
  C --> D["Mass-normalize: Phi^T M Phi = I"]
  D --> E["Transform: x = Phi q"]
  E --> F["Decoupled: qdd + Omega^2 q = Phi^T f(t)"]
        

6. Energy Decoupling and Modal Superposition Interpretation

With mass-normalized modes, the kinetic and potential energies separate into sums over modes. Recall: \( T=\tfrac{1}{2}\dot{\mathbf{x} }^\top\mathbf{M}\dot{\mathbf{x} } \), \( V=\tfrac{1}{2}\mathbf{x}^\top\mathbf{K}\mathbf{x} \). Substitute \( \mathbf{x}=\mathbf{\Phi}\mathbf{q} \) and \( \dot{\mathbf{x} }=\mathbf{\Phi}\dot{\mathbf{q} } \):

\[ T=\tfrac{1}{2}\dot{\mathbf{q} }^\top\left(\mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}\right)\dot{\mathbf{q} } =\tfrac{1}{2}\dot{\mathbf{q} }^\top\dot{\mathbf{q} } =\tfrac{1}{2}\sum_{i=1}^n \dot{q}_i^2, \]

\[ V=\tfrac{1}{2}\mathbf{q}^\top\left(\mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi}\right)\mathbf{q} =\tfrac{1}{2}\mathbf{q}^\top\mathbf{\Omega}^2\mathbf{q} =\tfrac{1}{2}\sum_{i=1}^n \omega_i^2 q_i^2. \]

Hence each modal coordinate behaves like an independent oscillator with “unit modal mass” and stiffness \( \omega_i^2 \). The physical motion is a modal superposition: \( \mathbf{x}(t)=\sum_{i=1}^n \boldsymbol{\phi}_i q_i(t) \).

For forcing, \( p_i(t)=\boldsymbol{\phi}_i^\top\mathbf{f}(t) \) is the generalized force under the \( \mathbf{M} \)-orthonormal basis. If a particular mode shape has small projection on the forcing direction, it receives little energy (small modal participation).

7. Scalar Modal ODE Solutions and Resonant Structure

Each modal equation is a scalar undamped forced oscillator: \( \ddot{q}_i + \omega_i^2 q_i = p_i(t) \). For general \( p_i(t) \), the solution can be written via the impulse response \( h_i(t)=\frac{1}{\omega_i}\sin(\omega_i t) \):

\[ q_i(t)=q_i(0)\cos(\omega_i t)+\frac{\dot{q}_i(0)}{\omega_i}\sin(\omega_i t) +\int_{0}^{t}\frac{1}{\omega_i}\sin\!\big(\omega_i(t-\theta)\big)\,p_i(\theta)\,d\theta. \]

For harmonic forcing \( p_i(t)=\hat{p}_i\sin(\Omega t) \) with \( \Omega \neq \omega_i \), the steady-state amplitude is governed by the undamped frequency response:

\[ q_i^{ss}(t)=\frac{\hat{p}_i}{\omega_i^2-\Omega^2}\sin(\Omega t), \qquad \Omega \neq \omega_i. \]

The singularity as \( \Omega \to \omega_i \) is the classical undamped resonance artifact. In practice, damping (next lesson) regularizes this behavior.

flowchart TD
  S["Given M, K, f(t), x0, xd0"] --> E["Generalized EVP (K, M) -> lambdas, phis"]
  E --> N["Normalize modes: Phi^T M Phi = I"]
  N --> T["Transform ICs: q0 = Phi^T M x0, qd0 = Phi^T M xd0"]
  T --> D["Decoupled ODEs: qdd_i + wi^2 qi = pi(t)"]
  D --> R["Reconstruct: x(t) = Phi q(t)"]
  R --> C["Check: compare vs direct integration (optional)"]
        

8. Implementations Across Tools

Below we implement a complete example: a 3-DOF mass–spring chain with boundary springs, compute \( \omega_i \) and \( \mathbf{\Phi} \), mass-normalize the modes, simulate the decoupled modal ODEs, and reconstruct \( \mathbf{x}(t) \).


8.1 Python (NumPy/SciPy): generalized symmetric eigenproblem + ODE integration

Library notes: Use scipy.linalg.eigh for symmetric generalized EVP and scipy.integrate.solve_ivp for ODEs.

File: Chapter13_Lesson3.py


"""
Chapter13_Lesson3.py
System Dynamics — Chapter 13, Lesson 3
Modal Coordinates and Decoupling of MDOF Systems (Undamped Case)

Dependencies:
  numpy, scipy, matplotlib
"""
import numpy as np
from numpy.linalg import norm
from scipy.linalg import eigh
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt


def mass_normalize(Phi: np.ndarray, M: np.ndarray) -> np.ndarray:
    """Scale columns of Phi so that Phi^T M Phi = I."""
    Phi_n = Phi.copy()
    for i in range(Phi.shape[1]):
        mi = Phi[:, i].T @ M @ Phi[:, i]
        Phi_n[:, i] = Phi[:, i] / np.sqrt(mi)
    return Phi_n


def build_3dof_chain(m=(1.0, 1.2, 0.9), k=(1200.0, 900.0, 700.0, 1100.0)):
    """
    wall --k1-- m1 --k2-- m2 --k3-- m3 --k4-- wall
    """
    m1, m2, m3 = m
    k1, k2, k3, k4 = k
    M = np.diag([m1, m2, m3])
    K = np.array([
        [k1 + k2,   -k2,       0.0],
        [-k2,     k2 + k3,   -k3],
        [0.0,       -k3,     k3 + k4]
    ], dtype=float)
    return M, K


def modal_matrices(M: np.ndarray, K: np.ndarray):
    """Solve K phi = lambda M phi; return omega, Phi_mass_norm, Phi^T K Phi."""
    lam, Phi = eigh(K, M)  # ascending eigenvalues
    lam = np.maximum(lam, 0.0)
    omega = np.sqrt(lam)
    Phi = mass_normalize(Phi, M)
    Omega2 = Phi.T @ K @ Phi
    return omega, Phi, Omega2


def simulate_modal(omega, Phi, force_fun, t_span=(0.0, 8.0), n_eval=2000):
    """
    Modal ODE (mass-normalized):
      qdd + omega^2 q = p(t),  p(t) = Phi^T f(t)
    """
    n = Phi.shape[0]

    def ode(t, z):
        q = z[:n]
        qd = z[n:]
        p = Phi.T @ force_fun(t)
        qdd = -(omega**2) * q + p
        return np.hstack([qd, qdd])

    t_eval = np.linspace(t_span[0], t_span[1], n_eval)
    z0 = np.zeros(2*n)
    sol = solve_ivp(ode, t_span, z0, t_eval=t_eval, rtol=1e-8, atol=1e-10)

    q = sol.y[:n, :]
    x = Phi @ q
    return sol.t, x, q


def main():
    M, K = build_3dof_chain()
    omega, Phi, Omega2 = modal_matrices(M, K)

    print("Natural frequencies (rad/s):", omega)
    print("Check Phi^T M Phi ~ I:\\n", Phi.T @ M @ Phi)
    print("Check Phi^T K Phi ~ diag(omega^2):\\n", Omega2)

    F0 = 10.0
    Omega = 0.9 * omega[0]  # excite near mode 1
    f = lambda t: np.array([F0*np.sin(Omega*t), 0.0, 0.0])

    t, x, q = simulate_modal(omega, Phi, f)

    plt.figure()
    plt.plot(t, x[0, :], label="x1(t)")
    plt.xlabel("t [s]"); plt.ylabel("displacement")
    plt.grid(True); plt.legend(); plt.title("DOF 1 displacement via modal coordinates")

    plt.figure()
    for i in range(3):
        plt.plot(t, q[i, :], label=f"q{i+1}(t)")
    plt.xlabel("t [s]"); plt.ylabel("modal coordinate")
    plt.grid(True); plt.legend(); plt.title("Modal coordinates")
    plt.show()


if __name__ == "__main__":
    main()
      

8.2 C++ (Eigen): generalized self-adjoint eigenproblem + RK4

Library notes: Eigen provides GeneralizedSelfAdjointEigenSolver when \( \mathbf{M} \) and \( \mathbf{K} \) are symmetric.

File: Chapter13_Lesson3.cpp


/*
Chapter13_Lesson3.cpp
Modal Coordinates and Decoupling of MDOF Systems (Undamped Case)

Dependency: Eigen (header-only)
*/
#include <Eigen/Dense>
#include <iostream>
#include <cmath>

using Eigen::MatrixXd;
using Eigen::VectorXd;

static void massNormalize(MatrixXd& Phi, const MatrixXd& M) {
    for (int i = 0; i < Phi.cols(); ++i) {
        double mi = Phi.col(i).transpose() * M * Phi.col(i);
        Phi.col(i) /= std::sqrt(mi);
    }
}

static void build3DOF(MatrixXd& M, MatrixXd& K) {
    double m1 = 1.0, m2 = 1.2, m3 = 0.9;
    double k1 = 1200.0, k2 = 900.0, k3 = 700.0, k4 = 1100.0;

    M = MatrixXd::Zero(3,3);
    M(0,0)=m1; M(1,1)=m2; M(2,2)=m3;

    K = MatrixXd::Zero(3,3);
    K(0,0)=k1+k2; K(0,1)=-k2;
    K(1,0)=-k2;   K(1,1)=k2+k3; K(1,2)=-k3;
    K(2,1)=-k3;   K(2,2)=k3+k4;
}

static VectorXd force(double t, double F0, double Omega) {
    VectorXd f(3);
    f << F0*std::sin(Omega*t), 0.0, 0.0;
    return f;
}

// RK4 for z=[q;qd], z'=[qd; -w^2 q + p(t)]
static void rk4_step(VectorXd& q, VectorXd& qd,
                     const VectorXd& omega,
                     const VectorXd& p, double dt)
{
    const int n = q.size();
    auto acc = [&](const VectorXd& q_in, const VectorXd& p_in) {
        VectorXd qdd(n);
        for (int i = 0; i < n; ++i) qdd(i) = -omega(i)*omega(i)*q_in(i) + p_in(i);
        return qdd;
    };

    VectorXd k1_q = qd;
    VectorXd k1_qd = acc(q, p);

    VectorXd q2 = q + 0.5*dt*k1_q;
    VectorXd qd2 = qd + 0.5*dt*k1_qd;
    VectorXd k2_q = qd2;
    VectorXd k2_qd = acc(q2, p);

    VectorXd q3 = q + 0.5*dt*k2_q;
    VectorXd qd3 = qd + 0.5*dt*k2_qd;
    VectorXd k3_q = qd3;
    VectorXd k3_qd = acc(q3, p);

    VectorXd q4 = q + dt*k3_q;
    VectorXd qd4 = qd + dt*k3_qd;
    VectorXd k4_q = qd4;
    VectorXd k4_qd = acc(q4, p);

    q  += (dt/6.0)*(k1_q  + 2.0*k2_q  + 2.0*k3_q  + k4_q);
    qd += (dt/6.0)*(k1_qd + 2.0*k2_qd + 2.0*k3_qd + k4_qd);
}

int main() {
    MatrixXd M, K;
    build3DOF(M, K);

    Eigen::GeneralizedSelfAdjointEigenSolver<MatrixXd> ges(K, M);
    VectorXd lambda = ges.eigenvalues();
    MatrixXd Phi = ges.eigenvectors();

    VectorXd omega = lambda.cwiseMax(0.0).cwiseSqrt();
    massNormalize(Phi, M);

    std::cout << "Natural frequencies (rad/s): " << omega.transpose() << "\\n";
    std::cout << "Phi^T M Phi:\\n" << (Phi.transpose()*M*Phi) << "\\n";

    double F0 = 10.0;
    double Omega = 0.9 * omega(0);

    VectorXd q = VectorXd::Zero(3);
    VectorXd qd = VectorXd::Zero(3);

    double t0 = 0.0, tf = 8.0, dt = 1e-3;
    int N = static_cast<int>((tf-t0)/dt);

    for (int step = 0; step <= N; ++step) {
        double t = t0 + step*dt;
        VectorXd p = Phi.transpose() * force(t, F0, Omega);

        if (step % 1000 == 0) {
            VectorXd x = Phi * q;
            std::cout << "t=" << t << "  x1=" << x(0) << "  q1=" << q(0) << "\\n";
        }
        rk4_step(q, qd, omega, p, dt);
    }
    return 0;
}
      

8.3 Java (EJML): Cholesky reduction to standard eigenproblem + RK4

Library notes: For symmetric generalized EVP, a stable approach is: \( \mathbf{M}=\mathbf{L}\mathbf{L}^\top \), then solve \( \mathbf{A}=\mathbf{L}^{-1}\mathbf{K}\mathbf{L}^{-\top} \).

File: Chapter13_Lesson3.java


/*
Chapter13_Lesson3.java
Modal Coordinates and Decoupling of MDOF Systems (Undamped Case)

Dependency: EJML (org.ejml:ejml-all)
*/
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.row.CommonOps_DDRM;
import org.ejml.dense.row.decomposition.DecompositionFactory_DDRM;
import org.ejml.interfaces.decomposition.CholeskyDecomposition_F64;
import org.ejml.interfaces.decomposition.EigenDecomposition_F64;

import java.util.Arrays;

public class Chapter13_Lesson3 {

    static void build3DOF(DMatrixRMaj M, DMatrixRMaj K) {
        double m1 = 1.0, m2 = 1.2, m3 = 0.9;
        double k1 = 1200.0, k2 = 900.0, k3 = 700.0, k4 = 1100.0;

        M.reshape(3,3); CommonOps_DDRM.fill(M,0);
        M.set(0,0,m1); M.set(1,1,m2); M.set(2,2,m3);

        K.reshape(3,3); CommonOps_DDRM.fill(K,0);
        K.set(0,0,k1+k2); K.set(0,1,-k2);
        K.set(1,0,-k2);   K.set(1,1,k2+k3); K.set(1,2,-k3);
        K.set(2,1,-k3);   K.set(2,2,k3+k4);
    }

    static DMatrixRMaj invLower(DMatrixRMaj L) {
        int n = L.numRows;
        DMatrixRMaj invL = CommonOps_DDRM.identity(n);
        CommonOps_DDRM.solve(L, invL, invL); // L * invL = I
        return invL;
    }

    static class ModalResult { double[] omega; DMatrixRMaj Phi; }

    static ModalResult modal(DMatrixRMaj M, DMatrixRMaj K) {
        int n = M.numRows;

        CholeskyDecomposition_F64<DMatrixRMaj> chol = DecompositionFactory_DDRM.chol(n, true);
        if (!chol.decompose(M.copy())) throw new RuntimeException("M not SPD.");
        DMatrixRMaj L = chol.getT(null);

        DMatrixRMaj invL = invLower(L);

        // A = invL * K * invL^T
        DMatrixRMaj tmp = new DMatrixRMaj(n,n);
        DMatrixRMaj A = new DMatrixRMaj(n,n);
        CommonOps_DDRM.mult(invL, K, tmp);
        CommonOps_DDRM.multTransB(tmp, invL, A);

        // Eigen-decomposition of A (symmetric)
        EigenDecomposition_F64<DMatrixRMaj> eig = DecompositionFactory_DDRM.eig(n, true);
        if (!eig.decompose(A)) throw new RuntimeException("eig failed");

        double[] lambda = new double[n];
        DMatrixRMaj Y = new DMatrixRMaj(n,n);
        for (int i=0;i<n;i++){
            lambda[i] = eig.getEigenvalue(i).getReal();
            DMatrixRMaj yi = eig.getEigenVector(i);
            for(int r=0;r<n;r++) Y.set(r,i, yi.get(r,0));
        }

        // Recover generalized eigenvectors: Phi = inv(L^T) * Y = (invL^T)*Y
        DMatrixRMaj invLT = new DMatrixRMaj(n,n);
        CommonOps_DDRM.transpose(invL, invLT);
        DMatrixRMaj Phi = new DMatrixRMaj(n,n);
        CommonOps_DDRM.mult(invLT, Y, Phi);

        // Mass-normalize: phi_i^T M phi_i = 1
        DMatrixRMaj MPhi = new DMatrixRMaj(n,n);
        CommonOps_DDRM.mult(M, Phi, MPhi);
        for(int i=0;i<n;i++){
            double mi = 0;
            for(int r=0;r<n;r++) mi += Phi.get(r,i)*MPhi.get(r,i);
            double s = 1.0/Math.sqrt(mi);
            for(int r=0;r<n;r++) Phi.set(r,i, Phi.get(r,i)*s);
        }

        double[] omega = new double[n];
        for(int i=0;i<n;i++) omega[i] = Math.sqrt(Math.max(lambda[i],0));

        ModalResult res = new ModalResult();
        res.omega = omega; res.Phi = Phi;
        return res;
    }

    static double[] force(double t, double F0, double Omega){
        return new double[]{F0*Math.sin(Omega*t), 0.0, 0.0};
    }

    static double[] pModal(DMatrixRMaj Phi, double[] f){
        int n = Phi.numRows;
        double[] p = new double[n];
        for(int i=0;i<n;i++){
            double s=0;
            for(int r=0;r<n;r++) s += Phi.get(r,i)*f[r];
            p[i]=s;
        }
        return p;
    }

    static void rk4(double[] q, double[] qd, double[] omega, double[] p, double dt){
        int n=q.length;
        java.util.function.BiFunction<double[], double[], double[]> acc = (qq,pp) -> {
            double[] qdd = new double[n];
            for(int i=0;i<n;i++) qdd[i] = -omega[i]*omega[i]*qq[i] + pp[i];
            return qdd;
        };

        double[] k1q = Arrays.copyOf(qd,n);
        double[] k1qd = acc.apply(q,p);

        double[] q2=new double[n], qd2=new double[n];
        for(int i=0;i<n;i++){ q2[i]=q[i]+0.5*dt*k1q[i]; qd2[i]=qd[i]+0.5*dt*k1qd[i]; }
        double[] k2q = Arrays.copyOf(qd2,n);
        double[] k2qd = acc.apply(q2,p);

        double[] q3=new double[n], qd3=new double[n];
        for(int i=0;i<n;i++){ q3[i]=q[i]+0.5*dt*k2q[i]; qd3[i]=qd[i]+0.5*dt*k2qd[i]; }
        double[] k3q = Arrays.copyOf(qd3,n);
        double[] k3qd = acc.apply(q3,p);

        double[] q4=new double[n], qd4=new double[n];
        for(int i=0;i<n;i++){ q4[i]=q[i]+dt*k3q[i]; qd4[i]=qd[i]+dt*k3qd[i]; }
        double[] k4q = Arrays.copyOf(qd4,n);
        double[] k4qd = acc.apply(q4,p);

        for(int i=0;i<n;i++){
            q[i]  += (dt/6.0)*(k1q[i]  + 2*k2q[i]  + 2*k3q[i]  + k4q[i]);
            qd[i] += (dt/6.0)*(k1qd[i] + 2*k2qd[i] + 2*k3qd[i] + k4qd[i]);
        }
    }

    static double[] xReconstruct(DMatrixRMaj Phi, double[] q){
        int n=Phi.numRows;
        double[] x=new double[n];
        for(int r=0;r<n;r++){
            double s=0;
            for(int i=0;i<n;i++) s += Phi.get(r,i)*q[i];
            x[r]=s;
        }
        return x;
    }

    public static void main(String[] args){
        DMatrixRMaj M=new DMatrixRMaj(3,3);
        DMatrixRMaj K=new DMatrixRMaj(3,3);
        build3DOF(M,K);

        ModalResult mr = modal(M,K);
        System.out.println("Natural frequencies (rad/s): "+Arrays.toString(mr.omega));

        double F0=10.0;
        double Omega=0.9*mr.omega[0];

        double[] q={0,0,0}, qd={0,0,0};

        double t0=0, tf=8, dt=1e-3;
        int N=(int)Math.round((tf-t0)/dt);

        for(int step=0; step<=N; step++){
            double t=t0+step*dt;
            double[] p = pModal(mr.Phi, force(t,F0,Omega));

            if(step%1000==0){
                double[] x = xReconstruct(mr.Phi, q);
                System.out.printf("t=%.3f  x1=%.6f  q1=%.6f%n", t, x[0], q[0]);
            }
            rk4(q, qd, mr.omega, p, dt);
        }
    }
}
      

8.4 MATLAB + Simulink: eig(K,M) + ode45 + programmatic model build

Library notes: MATLAB natively supports generalized EVP with eig(K,M). Simulink can represent the decoupled system with a single State-Space block.

File: Chapter13_Lesson3.m


% Chapter13_Lesson3.m
% Modal Coordinates and Decoupling of MDOF Systems (Undamped Case)

clear; clc;

m1 = 1.0; m2 = 1.2; m3 = 0.9;
k1 = 1200; k2 = 900; k3 = 700; k4 = 1100;

M = diag([m1 m2 m3]);
K = [k1+k2, -k2,   0;
     -k2,  k2+k3, -k3;
      0,    -k3,  k3+k4];

[Phi,Lam] = eig(K,M);
lam = diag(Lam);
[lam, idx] = sort(lam,'ascend');
Phi = Phi(:,idx);
omega = sqrt(max(lam,0));

for i=1:size(Phi,2)
    mi = Phi(:,i)'*M*Phi(:,i);
    Phi(:,i) = Phi(:,i)/sqrt(mi);
end

Omega2 = Phi'*K*Phi;

F0 = 10; Omega = 0.9*omega(1);
f = @(t)[F0*sin(Omega*t); 0; 0];
p = @(t)Phi'*f(t);

n = 3;
ode = @(t,z)[ z(n+1:end);
             - (omega.^2).*z(1:n) + p(t) ];

tspan = [0 8];
z0 = zeros(2*n,1);

opts = odeset('RelTol',1e-8,'AbsTol',1e-10);
[t,z] = ode45(ode,tspan,z0,opts);
q = z(:,1:n).';
x = Phi*q;

figure; plot(t,x(1,:), 'LineWidth',1.2); grid on;
xlabel('t [s]'); ylabel('x_1'); title('x_1(t) via modal simulation');

% Optional Simulink auto-build:
mdl = 'Chapter13_Lesson3_Simulink';
if bdIsLoaded(mdl); close_system(mdl,0); end
new_system(mdl); open_system(mdl);

A = [zeros(n), eye(n);
     -diag(omega.^2), zeros(n)];
B = [zeros(n,1); Phi.'*[F0;0;0]];
C = [ [1 0 0]*Phi, zeros(1,n) ];
D = 0;

add_block('simulink/Sources/Sine Wave',[mdl '/Sine'], 'Amplitude','1', 'Frequency',num2str(Omega));
add_block('simulink/Continuous/State-Space',[mdl '/ModalSS']);
set_param([mdl '/ModalSS'],'A','A','B','B','C','C','D','D');
add_block('simulink/Sinks/Scope',[mdl '/Scope']);

add_line(mdl,'Sine/1','ModalSS/1');
add_line(mdl,'ModalSS/1','Scope/1');

assignin('base','A',A); assignin('base','B',B);
assignin('base','C',C); assignin('base','D',D);

set_param(mdl,'StopTime','8');
save_system(mdl);
      

8.5 Wolfram Mathematica: Eigensystem[{K,M}] + NDSolve

Library notes: Mathematica supports generalized EVP directly via Eigensystem[{K,M}].

File: Chapter13_Lesson3.nb


(* Chapter13_Lesson3.nb (plain-text Notebook expression) *)

Notebook[{
  Cell["Chapter 13, Lesson 3 — Modal Coordinates and Decoupling of MDOF Systems", "Title"],

  Cell["Build a 3-DOF mass-spring system", "Section"],
  Cell[BoxData@ToBoxes@
    HoldForm[
      m = {1.0, 1.2, 0.9};
      k = {1200., 900., 700., 1100.};
      M = DiagonalMatrix[m];
      {k1,k2,k3,k4} = k;
      K = { {k1+k2, -k2, 0.},
           {-k2, k2+k3, -k3},
           {0., -k3, k3+k4} };
    ], "Input"],

  Cell["Generalized eigenproblem: K.phi == lambda M.phi", "Section"],
  Cell[BoxData@ToBoxes@
    HoldForm[
      {vals, vecs} = Eigensystem[{K, M}];
      ord = Ordering[vals];
      vals = vals[[ord]]; vecs = vecs[[ord]];
      omega = Sqrt[Max[vals, 0]];
    ], "Input"],

  Cell["Mass-normalize modes: Phi^T M Phi = I", "Section"],
  Cell[BoxData@ToBoxes@
    HoldForm[
      Phi = Transpose[vecs];
      Phi = MapIndexed[
        Function[{v, idx}, v/Sqrt[v.M.v]],
        Transpose[Phi]
      ] // Transpose;
      Simplify[Transpose[Phi].M.Phi]
    ], "Input"],

  Cell["Decoupled modal ODE and reconstruction x = Phi q", "Section"],
  Cell[BoxData@ToBoxes@
    HoldForm[
      F0 = 10.; \[CapitalOmega] = 0.9*omega[[1]];
      f[t_] := {F0*Sin[\[CapitalOmega] t], 0., 0.};
      p[t_] := Transpose[Phi].f[t];

      q = Array[q, 3];
      eqs = Table[
        q[i]''[t] + omega[[i]]^2 q[i][t] == p[t][[i]],
        {i, 1, 3}
      ];
      ics = Join[
        Table[q[i][0] == 0, {i,1,3}],
        Table[q[i]'[0] == 0, {i,1,3}]
      ];

      sol = NDSolve[Join[eqs, ics], Table[q[i], {i,1,3}], {t,0,8}][[1]];
      qvec[t_] := Table[q[i][t], {i,1,3}] /. sol;
      xvec[t_] := Phi.qvec[t];

      Plot[Evaluate[xvec[t][[1]]], {t,0,8}, PlotRange->All,
           AxesLabel->{"t","x1"}, PlotLabel->"x1(t) via modal coordinates"]
    ], "Input"]
}]
      

9. Problems and Solutions

Problem 1 (Orthogonality derivation): Let \( \mathbf{K}\boldsymbol{\phi}_i=\lambda_i\mathbf{M}\boldsymbol{\phi}_i \) and \( \mathbf{K}\boldsymbol{\phi}_j=\lambda_j\mathbf{M}\boldsymbol{\phi}_j \), with \( \mathbf{M}=\mathbf{M}^\top \), \( \mathbf{K}=\mathbf{K}^\top \). Show that if \( \lambda_i\neq\lambda_j \), then \( \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j=0 \).

Solution: Multiply \( \mathbf{K}\boldsymbol{\phi}_i=\lambda_i\mathbf{M}\boldsymbol{\phi}_i \) on the left by \( \boldsymbol{\phi}_j^\top \) and \( \mathbf{K}\boldsymbol{\phi}_j=\lambda_j\mathbf{M}\boldsymbol{\phi}_j \) on the left by \( \boldsymbol{\phi}_i^\top \). By symmetry, \( \boldsymbol{\phi}_j^\top\mathbf{K}\boldsymbol{\phi}_i=\boldsymbol{\phi}_i^\top\mathbf{K}\boldsymbol{\phi}_j \) and \( \boldsymbol{\phi}_j^\top\mathbf{M}\boldsymbol{\phi}_i=\boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j \), leading to \( (\lambda_i-\lambda_j)\boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j=0 \). Hence \( \boldsymbol{\phi}_i^\top\mathbf{M}\boldsymbol{\phi}_j=0 \). ∎


Problem 2 (Explicit 2-DOF modal decoupling): Consider \( \mathbf{M}=m\mathbf{I}_2 \) and

\[ \mathbf{K}= \begin{bmatrix} 2k & -k\\ -k & 2k \end{bmatrix}. \]

(a) Compute \( \omega_1,\omega_2 \) and mode shapes. (b) Choose a mass-normalized modal matrix \( \mathbf{\Phi} \). (c) Show that \( \mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi}=\mathbf{\Omega}^2 \).

Solution: Solve \( \det(\mathbf{K}-\omega^2 m\mathbf{I})=0 \):

\[ \det\!\left( \begin{bmatrix} 2k-\omega^2 m & -k\\ -k & 2k-\omega^2 m \end{bmatrix} \right) =(2k-\omega^2 m)^2-k^2=0 \Rightarrow 2k-\omega^2 m=\pm k. \]

Thus \( \omega_1^2=\frac{k}{m} \) (plus sign) and \( \omega_2^2=\frac{3k}{m} \) (minus sign). Associated eigenvectors are \( \boldsymbol{\phi}_1 \propto [1\; 1]^\top \), \( \boldsymbol{\phi}_2 \propto [1\; -1]^\top \). Mass-normalize: for \( \mathbf{M}=m\mathbf{I} \), enforce \( \boldsymbol{\phi}_i^\top m\mathbf{I}\boldsymbol{\phi}_i=1 \). Taking

\[ \boldsymbol{\phi}_1=\frac{1}{\sqrt{2m} }\begin{bmatrix}1\\1\end{bmatrix},\quad \boldsymbol{\phi}_2=\frac{1}{\sqrt{2m} }\begin{bmatrix}1\\-1\end{bmatrix}, \quad \mathbf{\Phi}=[\boldsymbol{\phi}_1\;\boldsymbol{\phi}_2], \]

one verifies directly that \( \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}=\mathbf{I} \) and \( \mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi}=\mathrm{diag}(\omega_1^2,\omega_2^2) \), hence decoupling follows from Section 5. ∎


Problem 3 (Modal harmonic steady-state): Suppose for some mode \( i \) you have \( \ddot{q}_i+\omega_i^2 q_i=\hat{p}_i\sin(\Omega t) \) with \( \Omega \neq \omega_i \). Find a sinusoidal steady-state solution.

Solution: Try \( q_i^{ss}(t)=A\sin(\Omega t) \). Then \( \ddot{q}_i^{ss}=-A\Omega^2\sin(\Omega t) \), and substitution gives \( (-A\Omega^2+\omega_i^2 A)\sin(\Omega t)=\hat{p}_i\sin(\Omega t) \). Therefore \( A=\frac{\hat{p}_i}{\omega_i^2-\Omega^2} \), so \( q_i^{ss}(t)=\frac{\hat{p}_i}{\omega_i^2-\Omega^2}\sin(\Omega t) \). ∎


Problem 4 (Energy separation): Assume mass normalization \( \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}=\mathbf{I} \) and \( \mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi}=\mathbf{\Omega}^2 \). Show that the total energy \( T+V \) equals \( \tfrac{1}{2}\sum_i \dot{q}_i^2 + \tfrac{1}{2}\sum_i \omega_i^2 q_i^2 \).

Solution: Substitute \( \mathbf{x}=\mathbf{\Phi}\mathbf{q} \) and \( \dot{\mathbf{x} }=\mathbf{\Phi}\dot{\mathbf{q} } \). Then

\[ T=\tfrac{1}{2}\dot{\mathbf{q} }^\top(\mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi})\dot{\mathbf{q} } =\tfrac{1}{2}\dot{\mathbf{q} }^\top\dot{\mathbf{q} }=\tfrac{1}{2}\sum_i \dot{q}_i^2, \]

\[ V=\tfrac{1}{2}\mathbf{q}^\top(\mathbf{\Phi}^\top\mathbf{K}\mathbf{\Phi})\mathbf{q} =\tfrac{1}{2}\mathbf{q}^\top\mathbf{\Omega}^2\mathbf{q}=\tfrac{1}{2}\sum_i \omega_i^2 q_i^2. \]

Hence \( T+V \) is the sum of independent modal energies. ∎


Problem 5 (Reduced modal model via projection): Let \( \mathbf{\Phi}_r=[\boldsymbol{\phi}_1\cdots\boldsymbol{\phi}_r] \) contain the first \( r \) mass-normalized modes (\( r<n \)) and approximate \( \mathbf{x}\approx \mathbf{\Phi}_r\mathbf{q}_r \). Derive the reduced equations governing \( \mathbf{q}_r(t) \).

Solution: Substitute \( \mathbf{x}\approx\mathbf{\Phi}_r\mathbf{q}_r \) into \( \mathbf{M}\ddot{\mathbf{x} }+\mathbf{K}\mathbf{x}=\mathbf{f}(t) \) and left-multiply by \( \mathbf{\Phi}_r^\top \):

\[ \mathbf{\Phi}_r^\top\mathbf{M}\mathbf{\Phi}_r\,\ddot{\mathbf{q} }_r + \mathbf{\Phi}_r^\top\mathbf{K}\mathbf{\Phi}_r\,\mathbf{q}_r = \mathbf{\Phi}_r^\top\mathbf{f}(t). \]

Because the retained modes are mass-normalized and mutually \( \mathbf{M} \)-orthonormal, \( \mathbf{\Phi}_r^\top\mathbf{M}\mathbf{\Phi}_r=\mathbf{I}_r \), and \( \mathbf{\Phi}_r^\top\mathbf{K}\mathbf{\Phi}_r=\mathrm{diag}(\omega_1^2,\dots,\omega_r^2) \). Thus the reduced model is: \( \ddot{\mathbf{q} }_r + \mathbf{\Omega}_r^2 \mathbf{q}_r = \mathbf{\Phi}_r^\top\mathbf{f}(t) \). Reconstruction is \( \mathbf{x}(t)\approx \mathbf{\Phi}_r\mathbf{q}_r(t) \). ∎

10. Summary

We proved the \( \mathbf{M} \)-orthogonality of normal modes for symmetric undamped MDOF systems, introduced mass normalization \( \mathbf{\Phi}^\top\mathbf{M}\mathbf{\Phi}=\mathbf{I} \), and derived the exact decoupling \( \ddot{\mathbf{q} }+\mathbf{\Omega}^2\mathbf{q}=\mathbf{\Phi}^\top\mathbf{f}(t) \). The resulting scalar modal ODEs provide a mathematically clean lens for analysis and simulation. In the next lesson, we will study how damping alters (or preserves) this decoupling.

11. References

  1. Courant, R. (1943). Variational methods for the solution of problems of equilibrium and vibrations. Bulletin of the American Mathematical Society, 49(1), 1–23.
  2. Ritz, W. (1909). Über eine neue Methode zur Lösung gewisser Variationsprobleme der mathematischen Physik. Journal für die reine und angewandte Mathematik, 135, 1–61.
  3. Lanczos, C. (1950). An iteration method for the solution of the eigenvalue problem of linear differential and integral operators. Journal of Research of the National Bureau of Standards, 45, 255–282.
  4. Foss, K.A. (1958). Coordinates which uncouple the equations of motion of damped linear dynamic systems. Journal of Applied Mechanics, 25, 361–364.
  5. Hurty, W.C. (1965). Dynamic analysis of structural systems using component modes. AIAA Journal, 3(4), 678–685.
  6. Craig, R.R., & Bampton, M.C.C. (1968). Coupling of substructures for dynamic analyses. AIAA Journal, 6(7), 1313–1319.
  7. Golub, G.H., & Van Loan, C.F. (1969). Matrix computations for symmetric eigenvalue problems (early foundational journal contributions). SIAM Review, 11(4), 505–541.