Pathway 4: ROM Concatenation¶
This is the most efficient analysis pathway. Each domain is first solved at the FOM level, then reduced independently via POD, and the reduced models are concatenated.
EMProject → Assembly → Per-Domain FDS Solve → POD Reduction → ROM Concatenation → Wideband Solve
Overview¶
POD (Proper Orthogonal Decomposition) computes the SVD of the snapshot matrix and truncates to a low-rank basis:
$$ [\mathbf{x}_1^{(d)}, \dots, \mathbf{x}_N^{(d)}] = \mathbf{U}_d \boldsymbol{\Sigma}_d \mathbf{W}_d^H $$
The reduced matrices for each domain are:
$$ \tilde{\mathbf{K}}_d = \mathbf{V}_d^H \mathbf{K}_d \mathbf{V}_d, \quad \tilde{\mathbf{M}}_d = \mathbf{V}_d^H \mathbf{M}_d \mathbf{V}_d $$
The concatenation then operates on the reduced matrices — orders of magnitude smaller than the full-order. This gives massive speedups for systems with many components or repeated cells.
1. Create Project and Assembly¶
from cavsim3d.core.em_project import EMProject
from cavsim3d.geometry.primitives import RectangularWaveguide
import matplotlib.pyplot as plt
%matplotlib widget
# 1. Create the project
project_name = 'pathway4_rom_concatenation'
base_dir = './simulations' # Change to your preferred simulation directory
proj = EMProject(name=project_name, base_dir=base_dir, overwrite=True)
Creating new project 'pathway4_rom_concatenation' at simulations\pathway4_rom_concatenation
2. Build the Assembly¶
We add two rectangular waveguide sections, identical to Pathways 2 and 3.
# 2. Create assembly and add components
# Uncomment the following lines on first run.
# On subsequent runs, the project loads automatically.
assembly = proj.create_assembly(main_axis='Z')
wg1 = RectangularWaveguide(a=0.1, L=0.1)
assembly.add("rwg1", wg1)
assembly.add("rwg1", wg1, after="rwg1")
assembly.build()
assembly.generate_mesh(maxh=0.02)
# Visualise the mesh
proj.geo.show('mesh')
Assembly domain-material mapping: rwg1: ['rwg1/solid'] rwg1_1: ['rwg1_1/solid'] Port naming complete: Total ports: 3 External ports: 2 Interface ports: 1
WebGuiWidget(layout=Layout(height='500px', width='100%'), value={'gui_settings': {}, 'ngsolve_version': '6.2.2…
3. Solve Per-Domain FOMs¶
As in Pathway 3, the solver automatically performs per-domain solving.
The store_snapshots parameter (enabled by default) ensures the
solution snapshots are saved for later POD reduction.
# 3. Configure and run the frequency domain solver
fom_config = {
'nportmodes': 3,
'order': 3,
'nsamples': 100,
'fmin': 1e-3,
'fmax': 5,
'solver_type': 'direct',
'store_snapshots': True, # Required for POD reduction
'rerun': True # Uncomment to force re-solve even if results exist
}
fom_result = proj.fds.solve(config=fom_config)
============================================================
Structure Topology
============================================================
Type: Compound structure
Domains (2): ['rwg1', 'rwg1_1']
Ports (3): ['port1', 'port2', 'port3']
Mesh elements: 672
External Ports (2): ['port1', 'port3']
Internal Ports (1): ['port2']
Domain-Port Mapping:
rwg1: ['port1 (input)', 'port2 (internal)']
rwg1_1: ['port2 (internal)', 'port3 (output)']
============================================================
============================================================
Assembling Matrices...
============================================================
Solving port eigenmodes...
Calculating Port Eigenmodes...
port1 mode 0: TE_10, kc=31.4159, sigma=+1
port1 mode 1: TE_01, kc=62.8319, sigma=+1
port1 mode 2: TE_20, kc=62.8319, sigma=+1
port2 mode 0: kc=31.4159, type=TE, fc=1.4990 GHz, sigma=-1, phase=-
port2 mode 1: kc=62.8321, type=TE, fc=2.9979 GHz, sigma=-1, phase=-, pol=0°, degen=2
port2 mode 2: kc=62.8321, type=TE, fc=2.9979 GHz, sigma=-1, phase=-, pol=90°, degen=2
port3 mode 0: TE_10, kc=31.4159, sigma=-1
port3 mode 1: TE_01, kc=62.8319, sigma=-1
port3 mode 2: TE_20, kc=62.8319, sigma=-1
Port eigenmodes complete: 9 total modes
Saved port modes to simulations\pathway4_rom_concatenation\fds\port_modes\port_modes.pkl
FDS Solve: 0.0010 - 5.0000 GHz, 100 samples
Per-Domain Solve
Completed: 2 ports, 100 frequencies in 21.50s
Completed: 2 ports, 100 frequencies in 19.61s
Saved port modes to simulations\pathway4_rom_concatenation\fds\port_modes\port_modes.pkl
4. Reduce Each Domain via POD¶
The proj.fds.foms.reduce(tol) method reduces all domains via
Proper Orthogonal Decomposition. The tolerance controls the SVD
truncation level relative to the largest singular value.
# 4. Reduce all domains via POD
roms = proj.fds.foms.reduce(tol=1e-6)
print(f"Reduction complete: {roms}")
============================================================ Model Order Reduction ============================================================ Reduction complete: 21078 → 89 DOFs (99.6% compression) Saved port modes to simulations\pathway4_rom_concatenation\fds\port_modes\port_modes.pkl Reduction complete: ROMCollection([rwg1, rwg1_1])
5. Concatenate the ROMs¶
The concatenation operates on the reduced matrices, which are typically rank 10–30 instead of the full system size (~10,000 DOFs). This makes the wideband solve extremely fast.
import time
# 5. Concatenate and solve the ROM system
concat = roms.concatenate()
concat.couple()
t0 = time.time()
concat.solve(
fmin=fom_config['fmin'],
fmax=fom_config['fmax'],
nsamples=1000
)
elapsed = (time.time() - t0) * 1e3
print(f"Concatenated ROM solve: {elapsed:.1f} ms for 1000 frequency points")
Concat Solve: 0.001 - 5 GHz, 1000 samples, system size 86 Concat solve complete: 0.183s (1000 frequencies) Concatenated ROM solve: 331.5 ms for 1000 frequency points
6. Plot ROM Results and Compare with Analytical¶
Compare the concatenated ROM S-parameters against the analytical solution.
from cavsim3d.analytical.rectangular_waveguide import RWGAnalytical
# Create analytical model with total length
analytical = RWGAnalytical(
a=0.1, L=0.2,
freq_range=(fom_config['fmin'], fom_config['fmax'])
)
# Plot comparison: ROM Concatenation vs Analytical
which = [['1(1)1(1)'], ['1(1)2(1)']]
fig, axs = plt.subplot_mosaic(
[[1, 2], [3, 4]], figsize=(10, 8), layout='constrained'
)
for idx, wh in enumerate(which):
# Magnitude
analytical.plot_s(wh, ax=axs[idx + 1])
concat.plot_s(wh, ax=axs[idx + 1])
# Phase
analytical.plot_s(wh, plot_type='phase', ax=axs[idx + 3])
concat.plot_s(wh, plot_type='phase', ax=axs[idx + 3])
fig.suptitle('ROM Concatenation vs Analytical — S-Parameters', fontsize=14)
Text(0.5, 0.98, 'ROM Concatenation vs Analytical — S-Parameters')
Performance Summary¶
| Method | System Size | Typical Solve Time (1000 pts) |
|---|---|---|
| FOM (Pathway 1) | 21,708 DOFs | ~10x66.16 s |
| ROM (Pathway 1) | 89 DOFs | ~milliseconds |
| ROM Concat (Pathway 4) | 86 DOFs | 584 ms |
Summary¶
In this tutorial we:
- Created a multi-component assembly managed by
EMProject - Solved each domain independently via
proj.fds.solve(config=...) - Reduced all domains via POD using
proj.fds.foms.reduce(tol=...) - Concatenated the per-domain ROMs and solved at 1000 frequency points in milliseconds
- Validated against the analytical solution
This is the recommended workflow for large multi-component systems like multi-cell accelerating cavities.