API Reference

pyserep — System Equivalent Reduction Expansion Process

A production-grade, publication-ready Python library for SEREP-based Reduced Order Modelling of large-scale structural dynamics problems.

Quick start (Pipeline API)

>>> from pyserep import SereпPipeline, ROMConfig, FrequencyBand
>>> cfg = ROMConfig(
...     stiffness_file="K.mtx",
...     mass_file="M.mtx",
...     force_dofs=[3000],
...     output_dofs=[3000],
...     bands=[FrequencyBand(0, 100), FrequencyBand(400, 500)],
... )
>>> results = SereпPipeline(cfg).run()

Quick start (Functional API)

>>> from pyserep import load_matrices, solve_eigenproblem
>>> from pyserep import select_modes, select_dofs_eid
>>> from pyserep import build_serep_rom
>>> from pyserep import compute_frf_direct, compute_frf_modal
>>> K, M = load_matrices("K.mtx", "M.mtx")
>>> freqs, phi = solve_eigenproblem(K, M, n_modes=100)
>>> modes = select_modes(phi, freqs, force_dofs=[3000], output_dofs=[3000], f_max=500.0)
>>> dofs, kappa = select_dofs_eid(phi, modes)
>>> T, Ka, Ma = build_serep_rom(K, M, phi, modes, dofs)
>>> frf = compute_frf_direct(Ka, Ma, zeta=0.01, f_eval=range(1, 501))
class pyserep.ConvergencePoint(param_value: float, n_modes: int, n_dofs: int, kappa: float, max_frf_err_pct: float, rms_frf_err_pct: float, max_freq_err_pct: float)[source]

Bases: object

Single point in a convergence sweep.

kappa: float
max_freq_err_pct: float
max_frf_err_pct: float
n_dofs: int
n_modes: int
param_value: float
rms_frf_err_pct: float
class pyserep.ConvergenceStudy(param_name: str, param_label: str, points: List[ConvergencePoint])[source]

Bases: object

Results from a full convergence sweep.

param_label: str
param_name: str
plot(save_path: str | None = None, show: bool = False) None[source]

Plot FRF error and condition number vs parameter.

points: List[ConvergencePoint]
table() str[source]

Return the convergence results as a formatted ASCII table string.

class pyserep.FRFResult(freqs_hz: ~numpy.ndarray, H_rom: ~typing.Dict[str, ~numpy.ndarray], H_ref: ~typing.Dict[str, ~numpy.ndarray], band_masks: ~typing.Dict[str, ~numpy.ndarray], method: str = 'direct', errors: ~typing.Dict[str, ~typing.Dict] = <factory>)[source]

Bases: object

Container for FRF results from both ROM and reference models.

freqs_hz

Evaluation frequencies (Hz). May be non-uniform if selective bands are used.

Type:

np.ndarray

H_rom

ROM FRFs. Key "f{i}_o{j}" → complex array of shape (n_freq,).

Type:

dict

H_ref

Reference FRFs (same structure as H_rom).

Type:

dict

band_masks

Boolean mask for each band, mapping band label → (n_freq,) bool array.

Type:

dict

errors

Per-pair error metrics computed at construction time. Each entry is {"max_pct": float, "rms_pct": float}.

Type:

dict

method

FRF computation method: "direct" or "modal".

Type:

str

H_ref: Dict[str, ndarray]
H_rom: Dict[str, ndarray]
band_masks: Dict[str, ndarray]
errors: Dict[str, Dict]
freqs_hz: ndarray
method: str = 'direct'
summary() str[source]

Return a formatted string summarising FRF method and per-pair errors.

class pyserep.FrequencyBand(f_min: float, f_max: float, label: str | None = None, n_points: int | None = None)[source]

Bases: object

A single contiguous analysis band [f_min, f_max] Hz.

Parameters:
  • f_min (float) – Lower bound (Hz). Use 0.0 for DC.

  • f_max (float) – Upper bound (Hz).

  • label (str, optional) – Human-readable label. Auto-generated if not supplied.

  • n_points (int, optional) – Number of frequency evaluation points in this band. Overrides the global n_points_per_band when set.

Examples

>>> band = FrequencyBand(0, 100, label="LowBand")
>>> band.contains(50)
True
>>> band.span
100.0
property centre: float

Centre frequency in Hz.

contains(freq: float) bool[source]

Return True if freq lies within this band (inclusive).

expanded(alpha: float) FrequencyBand[source]

Return a new band with f_max scaled by alpha (MS1 safety factor).

f_max: float
f_min: float
label: str | None = None
n_points: int | None = None
property span: float

Width of the band in Hz.

class pyserep.FrequencyBandSet(bands: Sequence[FrequencyBand], n_points_per_band: int = 2000)[source]

Bases: object

Container for a collection of FrequencyBand objects.

Manages the multi-band frequency grid, mode relevance checks, and band-weighted Modal Participation Factor computation.

Parameters:
  • bands (sequence of FrequencyBand)

  • n_points_per_band (int) – Default frequency evaluation points per band.

Examples

>>> bset = FrequencyBandSet([FrequencyBand(0, 100), FrequencyBand(400, 500)])
>>> bset.n_bands
2
>>> len(bset.frequency_grid())
4000
band_weighted_mpf(phi_f: ndarray, phi_o: ndarray, omega_n: ndarray, band: FrequencyBand) ndarray[source]

Band-weighted Modal Participation Factor for all modes.

C_i = |phi_f_i * phi_o_i| * max_ω_in_band(1 / |ωᵢ² − ω²|)

Parameters:
Return type:

(n_modes,) band-weighted MPF

property bands: List[FrequencyBand]

Return a copy of the band list, sorted by f_min.

frequency_grid() ndarray[source]

Build the evaluation frequency array (Hz) as the union of all bands.

Return type:

np.ndarray, sorted and deduplicated.

frequency_mask(freqs_hz: ndarray) ndarray[source]

Boolean mask: True where freqs_hz falls inside any band.

property global_f_max: float

Upper bound of the highest band (Hz).

property global_f_min: float

Lower bound of the lowest band (Hz).

property is_selective: bool

True when there are gaps between bands.

mode_passes_ms1(freq_hz: float, rb_hz: float = 1.0, alpha: float = 1.5) bool[source]

True if a mode passes the MS1 frequency-range criterion.

property n_bands: int

Number of frequency bands in the set.

summary() str[source]

Return a human-readable multi-line summary of all bands and gaps.

class pyserep.PerformanceMetrics(n_full_dofs: int, n_selected_modes: int, n_master_dofs: int, dof_reduction_pct: float, mode_retention_pct: float, kappa: float, frf_method: str, n_freq_points: int, n_bands: int, frf_flops_rom: int, frf_flops_ref: int, frf_speedup: float, t_eigensolver_s: float, t_mode_select_s: float, t_dof_select_s: float, t_rom_build_s: float, t_frf_s: float, t_total_s: float)[source]

Bases: object

All performance and efficiency metrics for a pipeline run.

dof_reduction_pct: float
frf_flops_ref: int
frf_flops_rom: int
frf_method: str
frf_speedup: float
kappa: float
mode_retention_pct: float
n_bands: int
n_freq_points: int
n_full_dofs: int
n_master_dofs: int
n_selected_modes: int
summary() str[source]

Return a formatted multi-line performance summary string.

t_dof_select_s: float
t_eigensolver_s: float
t_frf_s: float
t_mode_select_s: float
t_rom_build_s: float
t_total_s: float
class pyserep.PipelineResults(config: ROMConfig, freqs_hz: ndarray = <factory>, phi: ndarray = <factory>, selected_modes: ndarray = <factory>, master_dofs: ndarray = <factory>, T: ndarray | None = None, Ka: ndarray | None = None, Ma: ndarray | None = None, kappa: float = inf, freq_errors: ndarray | None = None, max_freq_err: float = nan, frf: FRFResult | None = None, validation: ValidationReport | None = None, performance: PerformanceMetrics | None = None, saved_files: Dict[str, str]=<factory>, elapsed_total_s: float = 0.0)[source]

Bases: object

All outputs from a completed SEREP ROM pipeline run.

config
Type:

ROMConfig

freqs_hz

All computed natural frequencies (Hz).

Type:

np.ndarray

phi

Full modal matrix (N × n_modes).

Type:

np.ndarray

selected_modes
Type:

np.ndarray of int

master_dofs
Type:

np.ndarray of int

T

SEREP transformation matrix (N × m).

Type:

np.ndarray

Ka, Ma

Reduced stiffness and mass matrices (m × m).

Type:

np.ndarray

kappa

Condition number κ(Φₐ).

Type:

float

freq_errors

Per-mode eigenvalue preservation errors (%).

Type:

np.ndarray

max_freq_err
Type:

float

frf
Type:

FRFResult

validation
Type:

ValidationReport

performance
Type:

PerformanceMetrics

elapsed_total_s
Type:

float

saved_files
Type:

dict

Ka: ndarray | None = None
Ma: ndarray | None = None
T: ndarray | None = None
config: ROMConfig
elapsed_total_s: float = 0.0
freq_errors: ndarray | None = None
freqs_hz: ndarray
frf: FRFResult | None = None
kappa: float = inf
master_dofs: ndarray
max_freq_err: float = nan
performance: PerformanceMetrics | None = None
phi: ndarray
saved_files: Dict[str, str]
selected_modes: ndarray
summary() str[source]

Return a formatted string with all key pipeline results and metrics.

validation: ValidationReport | None = None
class pyserep.ROMConfig(stiffness_file: str = '', mass_file: str = '', force_dofs: List[int] = <factory>, output_dofs: List[int] = <factory>, bands: List[FrequencyBand] | None = None, freq_range: Tuple[float, float]=(0.1, 500.0), frf_method: str = 'direct', damping_type: str = 'modal', zeta: float = 0.001, n_points_per_band: int = 2000, num_modes_eigsh: int = 100, eigsh_sigma: float = 0.01, eigsh_tol: float = 1e-10, ms1_alpha: float = 1.5, ms2_threshold: float = 1.0, ms3_threshold: float = 5.0, mac_threshold: float = 0.9, rb_hz: float = 1.0, dof_method: str = 'eid', ke_prescreen_frac: float = 0.5, export_folder: str = 'pyserep_output', save_prefix: str = 'SEREP', save_matrices: bool = True, plot: bool = True, verbose: bool = True)[source]

Bases: object

Complete configuration for a SEREP ROM pipeline run.

bands: List[FrequencyBand] | None = None
damping_type: str = 'modal'
dof_method: str = 'eid'
property effective_bands: List[FrequencyBand]

Resolved list of FrequencyBand objects (from bands or freq_range).

eigsh_sigma: float = 0.01
eigsh_tol: float = 1e-10
export_folder: str = 'pyserep_output'
force_dofs: List[int]
freq_range: Tuple[float, float] = (0.1, 500.0)
frf_method: str = 'direct'
property global_f_max: float

Maximum frequency across all analysis bands (Hz).

property global_f_min: float

Minimum frequency across all analysis bands (Hz).

property is_selective: bool

True when there are two or more bands (gap regions exist).

ke_prescreen_frac: float = 0.5
mac_threshold: float = 0.9
mass_file: str = ''
ms1_alpha: float = 1.5
ms2_threshold: float = 1.0
ms3_threshold: float = 5.0
property n_bands: int

Number of analysis frequency bands.

property n_pairs: int

Number of force/output DOF pairs.

n_points_per_band: int = 2000
num_modes_eigsh: int = 100
output_dofs: List[int]
plot: bool = True
rb_hz: float = 1.0
save_matrices: bool = True
save_prefix: str = 'SEREP'
stiffness_file: str = ''
summary() str[source]

Return a formatted string listing all configuration parameters.

verbose: bool = True
zeta: float = 0.001
class pyserep.SereпPipeline(config: ROMConfig)[source]

Bases: object

Orchestrates the complete SEREP ROM pipeline.

Parameters:

config (ROMConfig)

Examples

Full-range analysis with direct FRF:

>>> cfg = ROMConfig(
...     stiffness_file="K.mtx",
...     mass_file="M.mtx",
...     force_dofs=[3000],
...     output_dofs=[3000],
...     freq_range=(0.1, 500.0),
...     frf_method="direct",
... )
>>> results = SereпPipeline(cfg).run()

Selective bands:

>>> from pyserep import FrequencyBand
>>> cfg = ROMConfig(
...     stiffness_file="K.mtx",
...     mass_file="M.mtx",
...     force_dofs=[3000],
...     output_dofs=[3000],
...     bands=[FrequencyBand(0, 100), FrequencyBand(400, 500)],
... )
>>> results = SereпPipeline(cfg).run()
run() PipelineResults[source]

Execute the full pipeline and return PipelineResults.

class pyserep.ValidationReport(eigenvalue_errors_pct: ndarray, max_eigenvalue_error_pct: float, mean_eigenvalue_error_pct: float, mass_ortho_error: float, stiff_ortho_error: float, expansion_error: float, mac_values: ndarray, min_mac: float, mean_mac: float, ka_positive_definite: bool, ma_positive_definite: bool, ka_condition_number: float, ma_condition_number: float)[source]

Bases: object

Full validation report for a SEREP ROM.

eigenvalue_errors_pct: ndarray
expansion_error: float
ka_condition_number: float
ka_positive_definite: bool
ma_condition_number: float
ma_positive_definite: bool
mac_values: ndarray
mass_ortho_error: float
max_eigenvalue_error_pct: float
mean_eigenvalue_error_pct: float
mean_mac: float
min_mac: float
passed(strict: bool = False) bool[source]

Return True if all key checks pass.

stiff_ortho_error: float
summary() str[source]

Return a formatted multi-line validation report as a string.

pyserep.ansys_dof(node: int, direction: int) int[source]

Convert Ansys node number + direction to a 0-based DOF index.

DOF_index = (node_number − 1) × 3 + direction

Parameters:
  • node (int — 1-based Ansys node number)

  • direction (int — 0=UX, 1=UY, 2=UZ)

Return type:

int — 0-based DOF index

pyserep.apply_bcs(K: spmatrix, M: spmatrix, fixed_dofs: List[int], penalty: float = 1000000000000000.0) Tuple[csc_matrix, csc_matrix][source]

Apply fixed-DOF boundary conditions via the penalty method.

Sets K[dof, dof] += penalty and M[dof, dof] = 1 for each fixed DOF. This is numerically equivalent to removing the rows/columns but preserves the matrix dimensions (convenient for DOF indexing).

Parameters:
  • K (sparse matrices)

  • M (sparse matrices)

  • fixed_dofs (list of int)

  • penalty (float)

Returns:

K_bc, M_bc

Return type:

scipy.sparse.csc_matrix

pyserep.bandwidth(mat: spmatrix) int[source]

Return the half-bandwidth of a sparse matrix.

pyserep.build_dof_map(master_dofs: ndarray, force_dofs: List[int], output_dofs: List[int]) Tuple[List[int], List[int]][source]

Map global force/output DOF indices → local indices within master_dofs.

Parameters:
  • master_dofs (np.ndarray of int — global master DOF indices)

  • force_dofs (list of int — global DOF indices)

  • output_dofs (list of int — global DOF indices)

Returns:

local_force, local_output

Return type:

list of int

Raises:

KeyError – If any force/output DOF is not present in master_dofs.

pyserep.build_rayleigh_damping(Ka: ndarray, Ma: ndarray, zeta: float, freqs_hz: ndarray, modes: ndarray) ndarray[source]

Build a Rayleigh damping matrix for the reduced model.

Rayleigh damping: Cₐ = α Mₐ + β Kₐ

where α and β are chosen to give damping ratio zeta at the first and last selected natural frequencies.

Parameters:
  • Ka (np.ndarray, shape (m, m))

  • Ma (np.ndarray, shape (m, m))

  • zeta (float) – Desired damping ratio (uniform across modes).

  • freqs_hz (np.ndarray) – Full-model natural frequencies.

  • modes (np.ndarray of int) – Selected mode indices.

Returns:

Ca – Rayleigh damping matrix.

Return type:

np.ndarray, shape (m, m)

pyserep.build_serep_rom(K: csc_matrix, M: csc_matrix, phi: ndarray, selected_modes: ndarray, master_dofs: ndarray, verbose: bool = True) Tuple[ndarray, ndarray, ndarray][source]

Build the SEREP transformation matrix T and compute reduced matrices Kₐ, Mₐ.

Parameters:
  • K (scipy.sparse.csc_matrix) – Full stiffness matrix (N × N). Must be real and symmetric.

  • M (scipy.sparse.csc_matrix) – Full mass matrix (N × N). Must be real, symmetric, and positive definite.

  • phi (np.ndarray, shape (N, n_all_modes)) – Full mass-normalised modal matrix.

  • selected_modes (np.ndarray of int) – Mode indices to retain (output of select_modes()).

  • master_dofs (np.ndarray of int) – Master DOF indices (output of select_dofs_eid()). Must satisfy len(master_dofs) == len(selected_modes) for exact SEREP.

  • verbose (bool) – Print construction diagnostics.

Returns:

  • T (np.ndarray, shape (N, m)) – SEREP transformation matrix.

  • Ka (np.ndarray, shape (m, m)) – Reduced stiffness matrix.

  • Ma (np.ndarray, shape (m, m)) – Reduced mass matrix.

Raises:
  • ValueError – If len(master_dofs) != len(selected_modes) and the exact inverse would be undefined.

  • RuntimeWarning – If the condition number of Φₐ is very large (> 10⁶), indicating a poorly conditioned master DOF set.

Notes

When len(master_dofs) > len(selected_modes) (over-constrained), the Moore–Penrose pseudoinverse is used, resulting in a least-squares SEREP approximation.

Examples

>>> T, Ka, Ma = build_serep_rom(K, M, phi, selected_modes, master_dofs)
>>> Ka.shape
(37, 37)
pyserep.compare_dof_selectors(phi: ndarray, selected_modes: ndarray, verbose: bool = True) dict[source]

Run all four DOF selectors and return a comparison table.

Parameters:
  • phi (np.ndarray)

  • selected_modes (np.ndarray of int)

  • verbose (bool)

Returns:

Keys: “DS1”, “DS2”, “DS3”, “DS4”. Values: dict with “master_dofs”, “kappa”, “rank”, “elapsed_s”.

Return type:

dict

pyserep.compute_frf_direct(Ka: ndarray, Ma: ndarray, force_dof_indices: List[int], output_dof_indices: List[int], freq_eval: ndarray | List[float], zeta: float = 0.001, Ca: ndarray | None = None, damping_type: str = 'modal', eta: float = 0.0, verbose: bool = True) Tuple[ndarray, Dict[str, ndarray]][source]

Compute FRF of the SEREP ROM using direct impedance inversion.

At each frequency ω, solve: Z(ω) H_col = I_{force_col}

where Z(ω) = Kₐ - ω²Mₐ + jωCₐ (or Kₐ(1+jη) - ω²Mₐ for hysteretic).

Parameters:
  • Ka (np.ndarray, shape (m, m)) – Reduced stiffness matrix from SEREP.

  • Ma (np.ndarray, shape (m, m)) – Reduced mass matrix from SEREP.

  • force_dof_indices (list of int) – Indices of force (input) DOFs within the master DOF set (0-based, 0..m-1).

  • output_dof_indices (list of int) – Indices of response (output) DOFs within the master DOF set. Must be the same length as force_dof_indices.

  • freq_eval (array-like) – Evaluation frequencies in Hz.

  • zeta (float) – Uniform modal damping ratio (used only for damping_type="modal" and damping_type="rayleigh").

  • Ca (np.ndarray, shape (m, m), optional) – User-supplied damping matrix. Overrides zeta and damping_type.

  • damping_type (str) – One of: * "modal" — Ca built from modal damping ratios * "rayleigh" — Ca = α Ma + β Ka * "hysteretic"— structural damping: Ka(1+jη)−ω²Ma * "none" — undamped

  • eta (float) – Structural damping loss factor (only for damping_type="hysteretic").

  • verbose (bool)

Returns:

  • freq_eval (np.ndarray) – Evaluation frequencies in Hz.

  • H (dict) – FRF arrays keyed by "f{fi}_o{oi}". Values are complex arrays of shape (n_freq,).

Notes

This function uses LU factorisation reuse (scipy.linalg.lu_factor) only when the frequency loop is over simple viscous damping. For hysteretic or arbitrary Ca the system matrix changes with every ω so each step requires a fresh solve; but the (m × m) system is small, making direct LU fast regardless.

Examples

>>> freqs, H = compute_frf_direct(Ka, Ma, [5], [5], np.arange(1, 501))
>>> H["f5_o5"].shape
(500,)
pyserep.compute_frf_direct_fullmodel(K: csc_matrix, M: csc_matrix, master_dofs: ndarray, force_dof_global: List[int], output_dof_global: List[int], freq_eval: ndarray | List[float], zeta: float = 0.001, verbose: bool = True) Tuple[ndarray, Dict[str, ndarray]][source]

Compute reference FRF directly from the full-order physical matrices.

This is the true ground truth — no modal truncation, no approximation. Uses sparse LU factorisation at each frequency step.

WARNING: This function is computationally expensive for large systems (N > 10,000 DOFs × many frequencies). For large models, the modal reference (compute_frf_modal_reference()) with all elastic modes is a practical alternative.

Parameters:
  • K (scipy.sparse.csc_matrix) – Full stiffness matrix.

  • M (scipy.sparse.csc_matrix) – Full mass matrix.

  • master_dofs (np.ndarray of int) – Global DOF indices corresponding to the ROM master DOFs. Used to determine the force/output DOF local indices.

  • force_dof_global (list of int) – Force DOF global indices (into the N-DOF full model).

  • output_dof_global (list of int) – Response DOF global indices.

  • freq_eval (array-like) – Evaluation frequencies in Hz.

  • zeta (float) – Uniform modal damping ratio (Rayleigh-type applied to full model).

  • verbose (bool)

Returns:

  • freq_eval (np.ndarray)

  • H_ref (dict)

pyserep.compute_frf_modal(phi: ndarray, freqs_hz: ndarray, mode_indices: ndarray, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet, zeta: float = 0.001, per_mode_zeta: ndarray | None = None, verbose: bool = True) Tuple[ndarray, Dict[str, ndarray]][source]

Compute FRF via modal superposition, evaluated only within band regions.

Parameters:
  • phi (np.ndarray, shape (N, n_modes)) – Full modal matrix (mass-normalised).

  • freqs_hz (np.ndarray, shape (n_modes,)) – Natural frequencies in Hz.

  • mode_indices (np.ndarray of int) – Indices of modes to include in the superposition.

  • force_dofs (list of int) – Force DOF global indices.

  • output_dofs (list of int) – Output DOF global indices.

  • band_set (FrequencyBandSet) – Defines the frequency evaluation grid.

  • zeta (float) – Uniform damping ratio (used when per_mode_zeta is None).

  • per_mode_zeta (np.ndarray, optional) – Per-mode damping ratios, shape (n_modes,). Overrides zeta.

  • verbose (bool)

Returns:

  • freq_eval (np.ndarray) – Evaluation frequencies (Hz).

  • H (dict) – FRF arrays keyed by "f{fi}_o{oi}".

pyserep.compute_frf_modal_reference(phi: ndarray, freqs_hz: ndarray, rb_hz: float, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet, zeta: float = 0.001, verbose: bool = True) Dict[str, ndarray][source]

Compute reference FRF using ALL elastic modes of the full model.

This is used as the ground truth for FRF accuracy assessment.

Parameters:
  • phi (full modal matrix and frequencies)

  • freqs_hz (full modal matrix and frequencies)

  • rb_hz (float) – Rigid-body exclusion threshold in Hz.

  • force_dofs (list of int)

  • output_dofs (list of int)

  • band_set (FrequencyBandSet)

  • zeta (float)

  • verbose (bool)

Returns:

H_ref

Return type:

dict

pyserep.compute_frf_pair_direct(Ka: ndarray, Ma: ndarray, phi: ndarray, freqs_hz_all: ndarray, selected_modes: ndarray, master_dofs: ndarray, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet, zeta: float = 0.001, damping_type: str = 'modal', rb_hz: float = 1.0, verbose: bool = True) FRFResult[source]

Compute both ROM (direct) and reference (modal, all elastic modes) FRFs.

The reference uses all elastic modes via modal superposition — this is sufficiently accurate for large models where the direct full-model solve would be prohibitively expensive.

Parameters:
  • Ka (np.ndarray, shape (m, m)) – Reduced matrices from SEREP.

  • Ma (np.ndarray, shape (m, m)) – Reduced matrices from SEREP.

  • phi (np.ndarray, shape (N, n_modes)) – Full modal matrix.

  • freqs_hz_all (np.ndarray) – All full-model natural frequencies.

  • selected_modes (np.ndarray of int)

  • master_dofs (np.ndarray of int)

  • force_dofs (list of int) – Global DOF indices.

  • output_dofs (list of int) – Global DOF indices.

  • band_set (FrequencyBandSet)

  • zeta (see compute_frf_direct())

  • damping_type (see compute_frf_direct())

  • rb_hz (see compute_frf_direct())

  • verbose (bool)

Return type:

FRFResult

pyserep.condition_number_estimate(A: ndarray, method: str = 'exact') float[source]

Estimate the 2-norm condition number of a dense matrix A.

Parameters:
  • A (np.ndarray, shape (m, n))

  • method (str) – "exact" — full SVD (always accurate, O(mn²)) "fast" — power iteration on AᵀA (cheaper for large m)

Return type:

float — κ(A) = σ_max / σ_min

pyserep.diagonal_scaling(K: spmatrix, M: spmatrix) Tuple[csc_matrix, csc_matrix, ndarray][source]

Scale (K, M) by the diagonal of M to improve eigensolver conditioning.

Transformation: K̃ = D⁻½ K D⁻½, M̃ = D⁻½ M D⁻½

where D = diag(M). This converts the generalised problem to a standard one when M is diagonal (lumped mass), and improves conditioning for consistent mass matrices.

Parameters:
  • K (scipy.sparse matrices)

  • M (scipy.sparse matrices)

Returns:

  • K_scaled, M_scaled (scipy.sparse.csc_matrix)

  • D_inv_sqrt (np.ndarray, shape (N,) — scaling vector for back-transformation)

pyserep.dof_count_study(K: csc_matrix, M: csc_matrix, phi: ndarray, freqs_hz: ndarray, selected_modes: ndarray, force_dofs: List[int], output_dofs: List[int], n_master_values: List[int], freq_eval: ndarray, H_ref: dict, zeta: float = 0.001, verbose: bool = True) ConvergenceStudy[source]

Study how FRF accuracy and condition number change with n_master.

Fixes the mode set and varies the number of master DOFs from len(selected_modes) up to max(n_master_values).

Parameters:
  • K (full model)

  • M (full model)

  • phi (full model)

  • freqs_hz (full model)

  • selected_modes (np.ndarray of int — fixed mode set)

  • force_dofs (list of int)

  • output_dofs (list of int)

  • n_master_values (list of int — DOF counts to sweep)

  • freq_eval (np.ndarray — frequency grid for FRF)

  • H_ref (dict — reference FRF on freq_eval)

  • zeta (float)

  • verbose (bool)

Return type:

ConvergenceStudy

pyserep.dof_to_ansys(dof: int) Tuple[int, int][source]

Convert a 0-based DOF index to (node_number, direction).

Inverse of ansys_dof().

Return type:

(node_number, direction) — 1-based node, direction in {0,1,2}

pyserep.eigenvalue_error(Ka: ndarray, Ma: ndarray, full_freqs_hz: ndarray, selected_modes: ndarray, verbose: bool = True) Tuple[ndarray, float][source]

Compute per-mode eigenvalue preservation error (%).

Returns:

  • errors_pct (np.ndarray, shape (m,))

  • max_error_pct (float)

pyserep.eigenvalue_sensitivity(K: spmatrix, M: spmatrix, phi: ndarray, freqs_hz: ndarray, selected_modes: ndarray, dK_dp: spmatrix, dM_dp: spmatrix, verbose: bool = True) ndarray[source]

Compute eigenvalue sensitivities ∂λᵢ/∂p using Nelson’s method.

For mass-normalised modes (φᵢᵀ M φᵢ = 1):

∂λᵢ/∂p = φᵢᵀ (∂K/∂p − λᵢ ∂M/∂p) φᵢ

Parameters:
  • K (sparse matrices — full structural matrices)

  • M (sparse matrices — full structural matrices)

  • phi (np.ndarray, shape (N, n_all_modes))

  • freqs_hz (np.ndarray — full-model natural frequencies)

  • selected_modes (np.ndarray of int)

  • dK_dp (sparse matrices — parameter derivative matrices) – Represents ∂K/∂p and ∂M/∂p for a single parameter p. For a Young’s modulus perturbation: dK_dp = K/E, dM_dp = 0.

  • dM_dp (sparse matrices — parameter derivative matrices) – Represents ∂K/∂p and ∂M/∂p for a single parameter p. For a Young’s modulus perturbation: dK_dp = K/E, dM_dp = 0.

  • verbose (bool)

Returns:

dlam_dp

Return type:

np.ndarray, shape (m,) — ∂λᵢ/∂p in (rad/s)² per unit of p

Examples

Sensitivity of eigenvalues to a 1% stiffness increase:

>>> dK_dp = 0.01 * K   # 1% increase in E
>>> dM_dp = sp.csc_matrix(K.shape)   # mass unchanged
>>> dlam = eigenvalue_sensitivity(K, M, phi, freqs, modes, dK_dp, dM_dp)
>>> dfreq_hz = dlam / (2 * np.pi * freqs_hz[modes]) / (2 * np.pi)
pyserep.enforce_symmetry(mat: spmatrix) csc_matrix[source]

Enforce symmetry: A 0.5 * (A + Aᵀ).

Parameters:

mat (scipy.sparse matrix)

Returns:

Symmetric version of mat.

Return type:

scipy.sparse.csc_matrix

pyserep.euler_beam(n_elements: int = 50, length: float = 1.0, EI: float = 10000.0, rho_A: float = 1.0, fixed_left: bool = True, fixed_right: bool = False) Tuple[csc_matrix, csc_matrix][source]

1D Euler-Bernoulli beam (consistent mass matrix).

DOFs per node: transverse displacement w and rotation θ. Total DOFs: 2 × (n_elements + 1). Fixed BCs applied by row/column zeroing with large diagonal penalty.

Parameters:
  • n_elements (int)

  • length (float) – Total beam length (m).

  • EI (float) – Bending stiffness (N·m²).

  • rho_A (float) – Mass per unit length (kg/m).

  • fixed_left (bool) – Clamped boundary conditions at the respective ends.

  • fixed_right (bool) – Clamped boundary conditions at the respective ends.

Returns:

K, M

Return type:

scipy.sparse.csc_matrix, shape (2*(n_elements+1), …)

pyserep.flop_count(n_modes: int, n_freq: int, n_pairs: int, method: str = 'direct') int[source]

Estimate FRF computation FLOP count.

Parameters:
  • n_modes (int) – Number of retained modes (m for ROM, all elastic for reference).

  • n_freq (int) – Number of evaluation frequency points.

  • n_pairs (int) – Number of force/output DOF pairs.

  • method (str) – "direct" or "modal".

Returns:

Approximate floating-point operation count.

Return type:

int

Notes

Modal FRF: per frequency per mode per pair: ~8 FLOPs (2 multiplications + complex division + addition)

Direct FRF: per frequency: one m×m complex LU factor + m×m backsolve LU factor: (2/3) m³ FLOPs Backsolve: m² FLOPs per right-hand side × n_pairs

pyserep.force_positive_definite(A: ndarray, shift_factor: float = 1e-08, max_iter: int = 50) Tuple[ndarray, float][source]

Make A symmetric positive definite by adding a small multiple of the identity until Cholesky succeeds.

Uses the modified Cholesky approach of Gill, Murray & Wright (1981).

Parameters:
  • A (np.ndarray, shape (n, n))

  • shift_factor (float) – Initial shift as a fraction of |diag(A)|_max.

  • max_iter (int)

Returns:

  • A_pd (np.ndarray — positive-definite version of A)

  • shift (float — total shift applied (0 if A was already PD))

Examples

>>> Ka_pd, shift = force_positive_definite(Ka)
>>> np.linalg.cholesky(Ka_pd)   # succeeds
pyserep.frf_sensitivity(Ka: ndarray, Ma: ndarray, dKa_dp: ndarray, dMa_dp: ndarray, local_force: List[int], local_output: List[int], freq_eval: ndarray, zeta: float = 0.001, damping_type: str = 'modal') Dict[str, ndarray][source]

Compute the FRF parameter sensitivity ∂H/∂p via direct differentiation.

Differentiating Z(ω) H = I with respect to p gives:

∂H/∂p = −Z⁻¹ (∂Z/∂p) Z⁻¹

where ∂Z/∂p = ∂Kₐ/∂p − ω² ∂Mₐ/∂p (for viscous damping proportional to Ka, Ma).

Parameters:
  • Ka (np.ndarray, shape (m, m) — reduced matrices)

  • Ma (np.ndarray, shape (m, m) — reduced matrices)

  • dKa_dp (np.ndarray, shape (m, m) — parameter derivatives of Ka, Ma)

  • dMa_dp (np.ndarray, shape (m, m) — parameter derivatives of Ka, Ma)

  • local_force (list of int — local DOF indices within master set)

  • local_output (list of int — local DOF indices within master set)

  • freq_eval (np.ndarray — evaluation frequencies (Hz))

  • zeta (damping parameters (same as compute_frf_direct))

  • damping_type (damping parameters (same as compute_frf_direct))

Returns:

dH_dp – Values are complex arrays of shape (n_freq,) representing ∂H/∂p.

Return type:

dict — same key structure as compute_frf_direct()

Notes

This gives the first-order sensitivity. For a parameter change Δp, the FRF changes approximately by ΔH ≈ (∂H/∂p) Δp.

pyserep.is_diagonal(mat: spmatrix, tol: float = 1e-12) bool[source]

Return True if mat is effectively diagonal (lumped mass).

pyserep.load_frf_npz(path: str) Dict[str, ndarray][source]

Load a saved FRF .npz file.

Returns:

freqs_hz — evaluation frequencies (Hz) rom_* — ROM FRF arrays (complex) ref_* — reference FRF arrays (complex)

Return type:

dict with keys

pyserep.load_matrices(stiffness_path: str, mass_path: str, verbose: bool = True, symmetry_tol: float = 1e-08) Tuple[csc_matrix, csc_matrix][source]

Load stiffness and mass matrices from disk.

Parameters:
  • stiffness_path (str) – Path to the stiffness matrix file.

  • mass_path (str) – Path to the mass matrix file.

  • verbose (bool) – Print loading progress, matrix statistics, and memory usage.

  • symmetry_tol (float) – Symmetry tolerance forwarded to load_matrix().

Returns:

  • K (scipy.sparse.csc_matrix) – Stiffness matrix.

  • M (scipy.sparse.csc_matrix) – Mass matrix.

Raises:

Examples

>>> K, M = load_matrices("K.mtx", "M.mtx")
>>> K.shape == M.shape
True
pyserep.load_matrix(path: str, symmetry_tol: float = 1e-08) csc_matrix[source]

Load a single sparse or dense matrix from disk, returning a CSC matrix.

Parameters:
  • path (str) – Absolute or relative path to the matrix file.

  • symmetry_tol (float) – If the relative asymmetry |A - Aᵀ|_F / |A|_F exceeds this threshold, a UserWarning is issued (does not abort).

Returns:

The loaded matrix in CSC format.

Return type:

scipy.sparse.csc_matrix

Raises:

Examples

>>> K = load_matrix("StiffMatrixmm.mtx")
>>> K.shape
(66525, 66525)
pyserep.load_metrics(folder: str, prefix: str = 'SEREP') Dict[str, Any][source]

Load a saved metrics JSON file.

pyserep.load_reduced_matrices(folder: str, prefix: str = 'SEREP') tuple[ndarray, ndarray, ndarray][source]

Load saved Ka, Ma, and T matrices.

Returns:

Ka, Ma, T

Return type:

np.ndarray

pyserep.mac_filter(phi: ndarray, candidate_indices: ndarray, freqs_hz: ndarray, force_dofs: List[int], output_dofs: List[int], mac_threshold: float = 0.9, verbose: bool = True) ndarray[source]

Remove spatially redundant modes from candidate_indices.

Two modes are considered duplicates if their MAC exceeds mac_threshold. The mode with the lower participation score is discarded.

Parameters:
  • phi (np.ndarray, shape (N, n_all_modes))

  • candidate_indices (np.ndarray of int)

  • freqs_hz (np.ndarray)

  • force_dofs (list of int)

  • output_dofs (list of int)

  • mac_threshold (float)

  • verbose (bool)

Return type:

np.ndarray of int — filtered subset of candidate_indices

pyserep.mass_normalise(phi: ndarray, M: spmatrix) ndarray[source]

Normalise mode shapes so that Φᵢᵀ M Φᵢ = 1 for every column i.

Parameters:
  • phi (np.ndarray, shape (N, m))

  • M (scipy.sparse matrix, shape (N, N))

Return type:

np.ndarray, shape (N, m) — mass-normalised phi

pyserep.material_perturbation_study(Ka_nominal: ndarray, Ma_nominal: ndarray, local_force: List[int], local_output: List[int], freq_eval: ndarray, H_nominal: Dict[str, ndarray], param_values: List[float], Ka_func: Callable[[float], ndarray], Ma_func: Callable[[float], ndarray], zeta: float = 0.001, verbose: bool = True) Dict[str, ndarray][source]

Compute FRFs for a sweep of material parameter values.

Useful for comparing Standard vs HyAPA material configurations, or for uncertainty quantification (±5% E, ±3% ρ, etc.).

Parameters:
  • Ka_nominal (np.ndarray — baseline reduced matrices)

  • Ma_nominal (np.ndarray — baseline reduced matrices)

  • local_force (list of int)

  • local_output (list of int)

  • freq_eval (np.ndarray)

  • H_nominal (dict — baseline FRF (from compute_frf_direct))

  • param_values (list of float — parameter values to evaluate) – e.g. [0.95, 0.97, 1.00, 1.03, 1.05] for ±5% sweep

  • Ka_func (callable — Ka_func(p) → np.ndarray(m, m)) – Returns Ka for parameter value p. For stiffness scaling: lambda p: p * Ka_nominal

  • Ma_func (callable — Ma_func(p) → np.ndarray(m, m)) – For mass scaling: lambda p: p * Ma_nominal

  • zeta (see compute_frf_direct)

  • verbose (see compute_frf_direct)

Returns:

"param_values" — np.ndarray of the swept values "H_sweep" — np.ndarray, shape (n_params, n_freq) — FRF magnitudes "max_deviation_pct" — np.ndarray, shape (n_params,)

Return type:

dict with keys

pyserep.matrix_stats(K: spmatrix, M: spmatrix, label: str = '') str[source]

Return a formatted statistics string for a (K, M) pair.

pyserep.memory_mb(mat: spmatrix) float[source]

Return memory usage of a sparse matrix in MB (data + indices + indptr).

pyserep.modal_assurance_criterion(phi_ref: ndarray, phi_rom: ndarray) ndarray[source]

Compute the full MAC matrix between two mode shape sets.

MAC[i,j] = |φᵢᵀ φⱼ|² / (|φᵢ|² |φⱼ|²)

Parameters:
  • phi_ref (np.ndarray, shape (N, m))

  • phi_rom (np.ndarray, shape (N, m))

Returns:

mac_matrix – Values in [0, 1]. Diagonal near 1 → good mode pairing.

Return type:

np.ndarray, shape (m, m)

pyserep.modal_residues(phi: ndarray, force_dofs: list, output_dofs: list, selected_modes: ndarray) ndarray[source]

Compute modal residues Rᵢ = φᵢ(f) · φᵢ(o) for each mode and DOF pair.

Parameters:
  • phi (np.ndarray, shape (N, n_all_modes))

  • force_dofs (list of int)

  • output_dofs (list of int)

  • selected_modes (np.ndarray of int)

Return type:

np.ndarray, shape (m, n_pairs)

pyserep.modal_strain_energy(phi: ndarray, K: spmatrix, selected_modes: ndarray) ndarray[source]

Compute the modal strain energy (MSE) for each selected mode.

MSE_i = φᵢᵀ K φᵢ (= ωᵢ² for mass-normalised modes)

Parameters:
  • phi (np.ndarray, shape (N, n_all_modes))

  • K (sparse stiffness matrix)

  • selected_modes (np.ndarray of int)

Return type:

np.ndarray, shape (len(selected_modes),) — modal strain energies

pyserep.mode_count_study(K: csc_matrix, M: csc_matrix, phi: ndarray, freqs_hz: ndarray, force_dofs: List[int], output_dofs: List[int], f_max: float, f_max_values: List[float], zeta: float = 0.001, rb_hz: float = 1.0, n_freq: int = 500, verbose: bool = True) ConvergenceStudy[source]

Study FRF convergence as the upper frequency cutoff is varied.

For each value in f_max_values, runs a full SEREP pipeline (mode selection → DOF selection → ROM build → direct FRF) and records accuracy metrics.

Parameters:
  • K (sparse matrices)

  • M (sparse matrices)

  • phi (modal matrix and frequencies (pre-computed))

  • freqs_hz (modal matrix and frequencies (pre-computed))

  • force_dofs (list of int)

  • output_dofs (list of int)

  • f_max (float) – Upper limit of the reference FRF (Hz).

  • f_max_values (list of float) – Cutoff frequencies to sweep (Hz). Each defines the upper edge of a single analysis band.

  • zeta (float)

  • rb_hz (float)

  • n_freq (int) – Number of FRF evaluation points.

  • verbose (bool)

Return type:

ConvergenceStudy

pyserep.model_info(K: spmatrix, M: spmatrix, label: str = '') str[source]

Return a one-line description of a (K, M) pair.

pyserep.monte_carlo_frf(Ka_nominal: ndarray, Ma_nominal: ndarray, local_force: List[int], local_output: List[int], freq_eval: ndarray, E_cov_pct: float = 2.0, rho_cov_pct: float = 1.0, n_samples: int = 50, seed: int = 42, zeta: float = 0.001, verbose: bool = True) Dict[str, ndarray][source]

Monte Carlo FRF uncertainty quantification.

Perturbs Ka (proportional to E uncertainty) and Ma (proportional to ρ uncertainty) and computes the FRF for each sample, returning the mean, standard deviation, and 5th/95th percentile bands.

Parameters:
  • Ka_nominal (np.ndarray, shape (m, m))

  • Ma_nominal (np.ndarray, shape (m, m))

  • local_force (list of int)

  • local_output (list of int)

  • freq_eval (np.ndarray)

  • E_cov_pct (float — coefficient of variation for Young's modulus (%))

  • rho_cov_pct (float — coefficient of variation for density (%))

  • n_samples (int)

  • seed (int)

  • zeta

  • verbose

Returns:

"H_mean" — mean FRF magnitude "H_std" — standard deviation "H_p5" — 5th percentile "H_p95" — 95th percentile "H_all" — all samples, shape (n_samples, n_freq)

Return type:

dict with keys

pyserep.ms1_frequency_range(freqs_hz: ndarray, band_set: FrequencyBandSet, rb_hz: float = 1.0, ms1_alpha: float = 1.5, verbose: bool = True) ndarray[source]

MS1: Retain modes whose natural frequency is relevant to at least one band.

A mode at frequency f passes MS1 if:
  • f > rb_hz (not rigid-body)

  • f ≤ alpha × max(band.f_max) for at least one band

Parameters:
Return type:

np.ndarray of int

pyserep.ms2_participation_factor(phi: ndarray, freqs_hz: ndarray, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet, rb_hz: float = 1.0, threshold_pct: float = 1.0, verbose: bool = True) ndarray[source]

MS2: Retain modes with significant band-weighted Modal Participation Factor.

For each band B and DOF pair (f, o), a mode passes MS2 if:

C_i_B / C_dom_B ≥ threshold_pct / 100

where C_dom_B is the dominant elastic mode’s MPF within band B.

Parameters:
Return type:

np.ndarray of int

pyserep.ms3_spatial_amplitude(phi: ndarray, freqs_hz: ndarray, target_dofs: List[int], rb_hz: float = 1.0, threshold_pct: float = 5.0, verbose: bool = True) ndarray[source]

MS3: Retain modes with significant spatial amplitude at target DOFs.

A mode i passes MS3 if, at any target DOF d:

|φᵢ(d)| / max_j(|φᵢ(j)|) ≥ threshold_pct / 100

This catches modes near nodal lines that MS2 might miss.

Parameters:
  • phi (np.ndarray, shape (N, n_modes))

  • freqs_hz (np.ndarray)

  • target_dofs (list of int) – Union of force and output DOFs.

  • rb_hz (float)

  • threshold_pct (float)

  • verbose (bool)

Return type:

np.ndarray of int

pyserep.ms4_conditioning_check(phi: ndarray, mode_indices: ndarray, master_dofs: ndarray, verbose: bool = True) float[source]

MS4: Compute κ(Φₐ) to verify the master DOF set is well conditioned.

Parameters:
  • phi (np.ndarray)

  • mode_indices (np.ndarray of int)

  • master_dofs (np.ndarray of int)

  • verbose (bool)

Return type:

float — condition number κ(Φₐ)

pyserep.orthogonality_check(phi: ndarray, M: csc_matrix, selected_modes: ndarray) float[source]

Return |ΦᵀMΦ I|_max. Should be < 1e-10 for mass-normalised modes.

pyserep.plate_2d(nx: int = 10, ny: int = 10, lx: float = 1.0, ly: float = 1.0, D: float = 1000.0, rho_h: float = 1.0) Tuple[csc_matrix, csc_matrix][source]

2D rectangular Kirchhoff plate using finite differences.

Simply supported on all four edges. DOFs: transverse displacement w at interior grid points only. Total DOFs: (nx-1) × (ny-1).

Parameters:
  • nx (int) – Number of intervals in x and y (interior nodes = nx-1, ny-1).

  • ny (int) – Number of intervals in x and y (interior nodes = nx-1, ny-1).

  • lx (float) – Plate dimensions (m).

  • ly (float) – Plate dimensions (m).

  • D (float) – Flexural rigidity D = E h³ / (12(1-ν²)) (N·m).

  • rho_h (float) – Mass per unit area ρ·h (kg/m²).

Returns:

K, M

Return type:

scipy.sparse.csc_matrix

Notes

Uses the 13-point biharmonic finite difference stencil for ∇⁴w.

pyserep.plot_dof_selector_comparison(comparison: dict, save_path: str | None = None, show: bool = False) None[source]

Bar chart comparing κ values of all four DOF selectors.

Parameters:

comparison (dict) – Output of compare_dof_selectors().

pyserep.plot_frf_comparison(results: PipelineResults, save_path: str | None = None, show: bool = False, dpi: int = 150) None[source]

Four-panel FRF comparison figure.

Panel 1: FRF magnitude (dB re 1 m/N) — ROM vs reference Panel 2: FRF phase (degrees) — ROM vs reference Panel 3: Percentage FRF error (log scale) Panel 4: Eigenvalue preservation error per mode

Gap regions between selective bands are shaded grey.

Parameters:
pyserep.plot_frf_overlay(frf_result: FRFResult, title: str = 'FRF Overlay', save_path: str | None = None, show: bool = False) None[source]

Simple FRF magnitude overlay — ROM vs reference, single figure.

Useful for quick inspection without a full PipelineResults object.

pyserep.plot_mac_matrix(mac_matrix: ndarray, labels: List[str] | None = None, save_path: str | None = None, show: bool = False) None[source]

Plot the MAC matrix as a colour map.

Parameters:
  • mac_matrix (np.ndarray, shape (m, m))

  • labels (list of str, optional)

  • save_path (optional)

  • show (optional)

pyserep.plot_mode_shapes(phi: ndarray, freqs_hz: ndarray, mode_indices: ndarray, master_dofs: ndarray | None = None, n_cols: int = 4, save_path: str | None = None, show: bool = False) None[source]

Plot mode shape amplitudes for the selected modes.

Shows the full-DOF modal amplitude profile for each selected mode. Master DOFs are highlighted with red markers if provided.

Parameters:
  • phi (np.ndarray, shape (N, n_modes))

  • freqs_hz (np.ndarray)

  • mode_indices (np.ndarray of int)

  • master_dofs (np.ndarray of int, optional)

  • n_cols (int)

  • save_path (optional)

  • show (optional)

pyserep.plot_performance_dashboard(results: PipelineResults, save_path: str | None = None, show: bool = False) None[source]

6-panel performance dashboard.

Panels: 1. Mode selection cascade (bar chart: how many modes each step passes) 2. Selected mode frequency distribution 3. Eigenvalue error per mode 4. DOF reduction summary (pie chart) 5. Timing breakdown (bar chart) 6. FLOP comparison ROM vs reference

pyserep.random_symmetric_pd(n: int = 50, kappa_K: float = 10000.0, seed: int = 42) Tuple[csc_matrix, csc_matrix][source]

Random symmetric positive-definite (K, M) pair.

Useful for unit testing and algorithm benchmarks where the structural interpretation is irrelevant.

Parameters:
  • n (int) – Matrix size.

  • kappa_K (float) – Target condition number for K.

  • seed (int) – Random seed.

Returns:

K, M

Return type:

scipy.sparse.csc_matrix (dense matrices wrapped in sparse)

pyserep.rank_revealing_qr(A: ndarray, tol: float | None = None) Tuple[ndarray, ndarray, ndarray, int][source]

Rank-revealing QR factorisation with column pivoting.

Computes: A P = Q R where P is a permutation.

Parameters:
  • A (np.ndarray, shape (m, n))

  • tol (float, optional) – Rank threshold relative to |R[0,0]|. Default: machine epsilon × max(m,n).

Returns:

  • Q (np.ndarray, shape (m, m))

  • R (np.ndarray, shape (m, n))

  • perm (np.ndarray of int, shape (n,) — column permutation)

  • rank (int — estimated numerical rank)

Examples

>>> Q, R, perm, r = rank_revealing_qr(phi_a)
>>> perm[:r]   # top-r most informative DOFs
pyserep.reduction_metrics(n_full_dofs: int, n_master_dofs: int, n_all_modes: int, n_selected_modes: int) Dict[str, float][source]

Compute reduction ratio metrics.

Returns:

dof_reduction_ratio, dof_retention_pct, mode_retention_pct, size_ratio

Return type:

dict with keys

pyserep.reorder_rcm(K: spmatrix, M: spmatrix) Tuple[csc_matrix, csc_matrix, ndarray][source]

Apply Reverse Cuthill-McKee (RCM) reordering to reduce bandwidth.

Reduces the bandwidth of K (and correspondingly M), which can speed up sparse direct solvers used during FRF computation.

Parameters:
  • K (sparse matrices (square, symmetric))

  • M (sparse matrices (square, symmetric))

Returns:

  • K_rcm, M_rcm (scipy.sparse.csc_matrix — reordered matrices)

  • perm (np.ndarray of int — permutation vector (K_rcm = K[perm, :][:, perm]))

Notes

The same permutation must be applied to DOF index arrays (master_dofs, force_dofs, etc.) when using the reordered matrices. Use perm_inv = np.argsort(perm) to convert global DOF indices back.

pyserep.safe_pinv(A: ndarray, rcond: float | None = None, verbose: bool = False) ndarray[source]

Compute the Moore–Penrose pseudoinverse with automatic rank truncation.

Drops singular values below rcond * σ_max to avoid numerical blow-up from near-zero singular values.

Parameters:
  • A (np.ndarray, shape (m, n))

  • rcond (float, optional) – Relative condition threshold. Default: 100 × machine epsilon.

  • verbose (bool)

Return type:

np.ndarray, shape (n, m)

pyserep.save_results(results: PipelineResults, folder: str, prefix: str = 'SEREP', save_matrices: bool = True, verbose: bool = True) Dict[str, str][source]

Save all pipeline results to folder.

Parameters:
  • results (PipelineResults)

  • folder (str) – Output directory (created if it does not exist).

  • prefix (str) – Filename prefix for all output files.

  • save_matrices (bool) – If True, save Ka, Ma, and T matrices. These can be large; set to False to save only indices, FRF, and metrics.

  • verbose (bool)

Returns:

Mapping of result type → absolute file path.

Return type:

dict

pyserep.select_dofs_eid(phi: ndarray, selected_modes: ndarray, n_master: int | None = None, candidate_dofs: ndarray | None = None, ke_prescreen_frac: float = 0.5, required_dofs: ndarray | None = None, verbose: bool = True) Tuple[ndarray, float][source]

DS4: Select master DOFs using the Effective Independence (EID) method.

The algorithm iteratively removes the DOF with the smallest diagonal entry of the EI matrix:

E = Φₛ (ΦₛᵀΦₛ)⁻¹ Φₛᵀ

which corresponds to the DOF contributing the least to the linear independence of the mode shape partition. The Fisher Information Matrix det(ΦₛᵀΦₛ) is thereby maximised.

Parameters:
  • phi (np.ndarray, shape (N, n_all_modes)) – Full modal matrix (mass-normalised).

  • selected_modes (np.ndarray of int) – Mode indices from the mode selection pipeline.

  • n_master (int, optional) – Number of master DOFs. Default: len(selected_modes) (enforces the exact-SEREP rule a = m).

  • candidate_dofs (np.ndarray of int, optional) – Pre-screened candidate DOF set. If None, a kinetic-energy pre-screen retains the top ke_prescreen_frac fraction.

  • ke_prescreen_frac (float) – Fraction to retain in the KE pre-screen (0 < f ≤ 1).

  • verbose (bool)

Returns:

  • master_dofs (np.ndarray of int) – Selected master DOF indices.

  • kappa (float) – Condition number κ(Φₐ) of the final master partition.

Examples

>>> dofs, kappa = select_dofs_eid(phi, selected_modes)
>>> kappa < 100
True
pyserep.select_dofs_kinetic(phi: ndarray, selected_modes: ndarray, n_master: int | None = None, verbose: bool = True) Tuple[ndarray, float][source]

DS1: Select master DOFs by total kinetic energy across selected modes.

Score for DOF j: KE_j = Σᵢ |φᵢ(j)|²

Selects the top-n_master DOFs by descending KE score. Fast (O(N·m)) but typically produces κ ~ 10¹⁰–10¹⁵, which is unsuitable for SEREP unless used as a pre-screen for DS4.

Parameters:
Return type:

master_dofs, kappa

pyserep.select_dofs_modal_disp(phi: ndarray, selected_modes: ndarray, n_master: int | None = None, verbose: bool = True) Tuple[ndarray, float][source]

DS2: Select master DOFs by maximum modal displacement magnitude.

Score for DOF j: S_j = max_i |φᵢ(j)|

Retains DOFs where at least one mode has large amplitude.

Parameters:
Return type:

master_dofs, kappa

pyserep.select_dofs_svd(phi: ndarray, selected_modes: ndarray, n_master: int | None = None, verbose: bool = True) Tuple[ndarray, float][source]

DS3: Select master DOFs via QR factorisation with column pivoting of Φᵀ.

The column permutation from scipy.linalg.qr(Φᵀ, pivoting=True) directly gives the DOF ordering from most to least informative.

This is equivalent to maximising the diagonal entries of R and gives κ values typically in the range 10³–10⁶ — better than DS1/DS2 but still inferior to DS4 for SEREP.

Parameters:
Return type:

master_dofs, kappa

pyserep.select_modes(phi: ndarray, freqs_hz: ndarray, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet | None = None, f_max: float = 500.0, f_min: float = 0.1, rb_hz: float = 1.0, ms1_alpha: float = 1.5, ms2_threshold: float = 1.0, ms3_threshold: float = 5.0, mac_threshold: float = 0.9, verbose: bool = True) ndarray[source]

Run the complete mode selection pipeline.

Convenience wrapper that accepts either a FrequencyBandSet or plain f_min / f_max scalars for single-band analysis.

Parameters:
  • phi (np.ndarray, shape (N, n_modes)) – Mass-normalised modal matrix.

  • freqs_hz (np.ndarray, shape (n_modes,)) – Natural frequencies in Hz.

  • force_dofs (list of int) – Force DOF global indices.

  • output_dofs (list of int) – Output DOF global indices.

  • band_set (FrequencyBandSet, optional) – Multi-band specification. If None, a single band [f_min, f_max] is created.

  • f_max (float) – Upper frequency limit (Hz) when band_set is None.

  • f_min (float) – Lower frequency limit (Hz) when band_set is None.

  • rb_hz (float) – Rigid-body exclusion threshold (Hz).

  • ms1_alpha (float) – MS1 safety factor: retains modes up to alpha × max(f_max).

  • ms2_threshold (float) – MS2 band-weighted MPF threshold (% of dominant mode).

  • ms3_threshold (float) – MS3 spatial amplitude threshold (% of global peak).

  • mac_threshold (float) – MAC duplicate removal threshold.

  • verbose (bool)

Returns:

Selected mode indices, sorted ascending.

Return type:

np.ndarray of int

pyserep.select_modes_pipeline(phi: ndarray, freqs_hz: ndarray, force_dofs: List[int], output_dofs: List[int], band_set: FrequencyBandSet, rb_hz: float = 1.0, ms1_alpha: float = 1.5, ms2_threshold: float = 1.0, ms3_threshold: float = 5.0, mac_threshold: float = 0.9, verbose: bool = True) ndarray[source]

Run the complete mode selection pipeline.

S_final = (MS1 filtered by MAC) ∪ MS2 ∪ MS3

Parameters:
Return type:

np.ndarray of int — final selected mode indices, sorted ascending

pyserep.solve_eigenproblem(K: csc_matrix, M: csc_matrix, n_modes: int, sigma: float = 0.01, tol: float = 1e-10, maxiter: int | None = None, verbose: bool = True) Tuple[ndarray, ndarray][source]

Solve the generalised eigenvalue problem K φ = λ M φ for the n_modes lowest eigenpairs.

Parameters:
  • K (scipy.sparse.csc_matrix) – Stiffness matrix (N × N), real symmetric positive semi-definite. Asymmetric K will produce complex eigenvalues and incorrect results.

  • M (scipy.sparse.csc_matrix) – Mass matrix (N × N), real symmetric positive definite. M must be non-singular; singular M causes ARPACK to fail.

  • n_modes (int) – Number of eigenpairs to extract. Must be < N − 1.

  • sigma (float) – Shift parameter (rad²/s²) for shift-invert. A small positive value (e.g. 0.01) ensures rigid-body modes (λ ≈ 0) are captured.

  • tol (float) – ARPACK convergence tolerance. 1e-10 is appropriate for structural analysis; decrease to 1e-12 for very high accuracy requirements.

  • maxiter (int, optional) – Maximum ARPACK iterations. Default: 10 * N.

  • verbose (bool) – Print eigenvalue summary.

Returns:

  • freqs_hz (np.ndarray, shape (n_modes,)) – Natural frequencies in Hz, sorted ascending.

  • phi (np.ndarray, shape (N, n_modes)) – Mass-normalised mode shapes. phi[:, i] corresponds to freqs_hz[i].

Raises:

Examples

>>> freqs, phi = solve_eigenproblem(K, M, n_modes=100)
>>> freqs.shape
(100,)
>>> phi.shape
(66525, 100)
pyserep.sparse_submatrix(A: spmatrix, row_idx: ndarray, col_idx: ndarray | None = None) csc_matrix[source]

Extract a submatrix A[row_idx, :][:, col_idx] efficiently.

Parameters:
  • A (scipy.sparse matrix)

  • row_idx (np.ndarray of int)

  • col_idx (np.ndarray of int, optional — defaults to row_idx (square sub-block))

Return type:

scipy.sparse.csc_matrix

pyserep.sparsity(mat: spmatrix) float[source]

Return the fraction of structural zeros (higher = sparser).

pyserep.spring_chain(n: int = 300, k: float = 100000.0, m: float = 1.0, fixed_left: bool = True, fixed_right: bool = False) Tuple[csc_matrix, csc_matrix][source]

1D spring-mass chain.

Physical model: o–k–o–k–o … –k–o

m m m m

Parameters:
  • n (int) – Number of mass nodes (= DOFs).

  • k (float) – Spring stiffness (N/m).

  • m (float) – Mass of each node (kg).

  • fixed_left (bool) – If True, node 0 is fixed (Kₐ[0,0] = k, not 2k).

  • fixed_right (bool) – If True, node n-1 is also fixed.

Returns:

K, M

Return type:

scipy.sparse.csc_matrix

Examples

>>> K, M = spring_chain(n=500, k=2e5)
>>> K.shape
(500, 500)
pyserep.summarise_performance(n_full_dofs: int, n_selected_modes: int, n_master_dofs: int, n_all_modes: int, kappa: float, n_freq: int, n_bands: int, n_pairs: int, frf_method: str = 'direct', frf_flops_rom: int | None = None, frf_flops_ref: int | None = None, t_eigensolver_s: float = 0.0, t_mode_select_s: float = 0.0, t_dof_select_s: float = 0.0, t_rom_build_s: float = 0.0, t_frf_s: float = 0.0, t_total_s: float = 0.0) PerformanceMetrics[source]

Collect all performance metrics into a PerformanceMetrics.

Parameters are self-explanatory from the return type documentation.

pyserep.symmetrise(A)[source]

Enforce symmetry: A ← 0.5 (A + Aᵀ).

Works for both dense np.ndarray and scipy.sparse matrices.

pyserep.unscale_modes(phi_scaled: ndarray, d_inv_sqrt: ndarray) ndarray[source]

Undo diagonal scaling: φ = D⁻½ φ̃ then mass-renormalise.

pyserep.validate_serep(K: csc_matrix, M: csc_matrix, phi: ndarray, freqs_hz: ndarray, selected_modes: ndarray, master_dofs: ndarray, T: ndarray, Ka: ndarray, Ma: ndarray, verbose: bool = True) ValidationReport[source]

Run the complete SEREP validation suite.

Parameters:
  • K (sparse matrices)

  • M (sparse matrices)

  • phi (np.ndarray, shape (N, n_modes))

  • freqs_hz (np.ndarray)

  • selected_modes (np.ndarray of int)

  • master_dofs (np.ndarray of int)

  • T (np.ndarray, shape (N, m) — transformation matrix)

  • Ka (np.ndarray, shape (m, m) — reduced matrices)

  • Ma (np.ndarray, shape (m, m) — reduced matrices)

  • verbose (bool)

Return type:

ValidationReport

pyserep.verify_eigenvalues(Ka: ndarray, Ma: ndarray, full_freqs_hz: ndarray, selected_modes: ndarray, verbose: bool = True) Tuple[ndarray, float][source]

Verify SEREP’s defining property: exact eigenvalue preservation.

Solves the reduced eigenvalue problem Kₐ φ = λ Mₐ φ and compares the resulting frequencies to the full-model values.

Parameters:
  • Ka (np.ndarray, shape (m, m))

  • Ma (np.ndarray, shape (m, m))

  • full_freqs_hz (np.ndarray) – All full-model natural frequencies (Hz).

  • selected_modes (np.ndarray of int) – Indices identifying the target frequencies.

  • verbose (bool)

Returns:

  • freq_errors_pct (np.ndarray, shape (m,)) – Per-mode percentage error |f_rom - f_full| / f_full × 100.

  • max_error_pct (float) – Maximum absolute percentage error. Should be < 0.001% for a well-conditioned SEREP.

Examples

>>> errors, max_err = verify_eigenvalues(Ka, Ma, freqs_hz, selected_modes)
>>> max_err < 0.001
True
pyserep.write_ansys_node_list(master_dofs: ndarray, path: str, component_name: str = 'MASTER_NODES', verbose: bool = True) None[source]

Write an Ansys APDL input file that selects the master DOF nodes.

Usage in Ansys: /INPUT, master_nodes.inp

Parameters:
  • master_dofs (np.ndarray of int)

  • path (str — output .inp path)

  • component_name (str — Ansys component name)

  • verbose (bool)

pyserep.write_master_dofs_csv(master_dofs: ndarray, path: str, node_coords: ndarray | None = None, verbose: bool = True) None[source]

Write master DOF indices to a CSV file.

Columns: dof_index, node_number, direction, [x, y, z]

Parameters:
  • master_dofs (np.ndarray of int — 0-based DOF indices)

  • path (str — output .csv path)

  • node_coords (np.ndarray, shape (N_nodes, 3), optional) – Node coordinate array (indexed by node_number − 1). If provided, x, y, z columns are written.

  • verbose (bool)

pyserep.write_master_dofs_vtk(master_dofs: ndarray, node_coords: ndarray, path: str, scalar_data: ndarray | None = None, scalar_name: str = 'eid_score', verbose: bool = True) None[source]

Write master DOF locations as a VTK unstructured point cloud.

Opens in ParaView, MayaVi, VisIt, etc.

Parameters:
  • master_dofs (np.ndarray of int)

  • node_coords (np.ndarray, shape (N_nodes, 3))

  • path (str — output .vtk path)

  • scalar_data (np.ndarray, shape (n_master,), optional) – Per-DOF scalar field (e.g. EID score, KE contribution).

  • scalar_name (str)

  • verbose (bool)

pyserep.write_uff58_mode_shapes(phi: ndarray, freqs_hz: ndarray, selected_modes: ndarray, master_dofs: ndarray, node_coords: ndarray | None, path: str, zeta: float = 0.001, verbose: bool = True) None[source]

Write mode shapes to a UFF (Universal File Format) dataset 58 file.

Compatible with SDyPy, pyFRF, ME’scopeVES, nModal, and other EMA/OMA post-processors.

Parameters:
  • phi (np.ndarray, shape (N, n_all_modes))

  • freqs_hz (np.ndarray)

  • selected_modes (np.ndarray of int)

  • master_dofs (np.ndarray of int — only these DOFs are exported)

  • node_coords (np.ndarray, shape (N_nodes, 3), optional)

  • path (str — output .uff path)

  • zeta (float — damping ratio for all modes)

  • verbose (bool)