@@ -328,6 +328,12 @@ RomOperator::RomOperator(const IoData &iodata, SpaceOperator &space_op,
328328 std::size_t max_size_per_excitation)
329329 : space_op(space_op), orthog_type(iodata.solver.linear.gs_orthog)
330330{
331+ // Always use refined CGS with adaptive synthesis for higher accuracy on print-out.
332+ if (iodata.solver .driven .adaptive_circuit_synthesis )
333+ {
334+ orthog_type = Orthogonalization::CGS2;
335+ }
336+
331337 // Construct the system matrices defining the linear operator. PEC boundaries are handled
332338 // simply by setting diagonal entries of the system matrix for the corresponding dofs.
333339 // Because the Dirichlet BC is always homogeneous, no special elimination is required on
@@ -433,29 +439,60 @@ void RomOperator::SolveHDM(int excitation_idx, double omega, ComplexVector &u)
433439
434440void RomOperator::AddLumpedPortModesForSynthesis (const IoData &iodata)
435441{
436- auto &lumped_port_op = space_op.GetLumpedPortOp ();
437- for (const auto &[port_idx, port_data] : lumped_port_op)
442+ // Add modes for lumped port to use them a circuit matrices.
443+ //
444+ // The excitation vector that we expect to add to the PROM is just the primary vector e
445+ // associated with that port. However, when computing the response (scattering) matrix, we
446+ // have an overlap g * e, where g is the boundary bilinear form. For Nédélec elements e
447+ // and g * e are not proportional to each other. This is true on simple tet meshes, for
448+ // simple hex meshes they are equal empirically, but this was not tested generally (e.g.
449+ // on non-conforming meshes).
450+ //
451+ // To preserve orthogonality while also extracting the "full" port profile, we add both to
452+ // the PROM — first g * e and then e (orthogonalized to g * e), which accounts for a
453+ // "finite element correction". When e and g * e are the same, we should only add a single
454+ // vector to the PROM.
455+
456+ // Workspace vector: Lumped Ports are Real, but interface is complex.
457+ ComplexVector vec;
458+ vec.SetSize (space_op.GetNDSpace ().GetTrueVSize ());
459+ vec.UseDevice (true );
460+ vec = 0.0 ;
461+
462+ // Add all dual vectors first: we want this port to be orthogonal to the outside world and
463+ // check orthogonality internally.
464+ for (const auto &[port_idx, port_data] : space_op.GetLumpedPortOp ())
465+ {
466+ space_op.GetLumpedPortExcitationVectorDual (port_idx, vec, true );
467+ UpdatePROM (vec, fmt::format (" port_{:d}" , port_idx));
468+ }
469+
470+ // Check that the ports don't have any overlap.
471+ MFEM_VERIFY (GetRomOrthogonalityMatrix ().isDiagonal (),
472+ " Lumped port fields on the mesh should have exactly zero overlap. This may "
473+ " be non-zero if attributes share edges." );
474+
475+ // Add primary vector second, since this needs to be orthogonalized to the dual
476+ // "outside" vector.
477+ for (const auto &[port_idx, port_data] : space_op.GetLumpedPortOp ())
438478 {
439- ComplexVector port_excitation_E ;
440- port_excitation_E. UseDevice ( true );
441- space_op.GetLumpedPortExcitationVector (port_idx, port_excitation_E, true );
442- UpdatePROM (port_excitation_E , fmt::format (" port_{:d}" , port_idx));
479+ space_op. GetLumpedPortExcitationVectorPrimary (port_idx, vec, true ) ;
480+ // Drop if primary vector is same as dual vector.
481+ auto orthog_condition = ORTHOG_TOL * linalg::Norml2 ( space_op.GetComm (), vec. Real () );
482+ UpdatePROM (vec , fmt::format (" port_{:d}_correction " , port_idx), orthog_condition );
443483 }
444484
445- // Debug Print ROM Matrices
485+ // Debug Print
446486 if constexpr (true ) // ignore
447487 {
448488 fs::path folder_tmp = fs::path (iodata.problem .output ) / " prom_port_debug" ;
449489 fs::create_directories (folder_tmp);
450490 PrintPROMMatrices (iodata.units , folder_tmp);
451491 }
452-
453- const auto &orth_R = GetRomOrthogonalityMatrix ();
454- MFEM_VERIFY (orth_R.isDiagonal (), " Lumped port modes should have exactly zero overlap." );
455492}
456493
457-
458- void RomOperator::UpdatePROM ( const ComplexVector &u, std::string_view node_label )
494+ void RomOperator::UpdatePROM ( const ComplexVector &u, std::string_view node_label,
495+ double drop_degenerate_vector_norm_tol )
459496{
460497 // Update PROM basis V. The basis is always real (each complex solution adds two basis
461498 // vectors, if it has a nonzero real and imaginary parts).
@@ -469,30 +506,44 @@ void RomOperator::UpdatePROM(const ComplexVector &u, std::string_view node_label
469506 const bool has_imag = (norm_im > norm_tol);
470507
471508 const std::size_t dim_V_old = V.size ();
472- const std::size_t dim_V_new =
509+ std::size_t dim_V_new =
473510 V.size () + static_cast <std::size_t >(has_real) + static_cast <std::size_t >(has_imag);
474511
475512 orth_R.conservativeResizeLike (Eigen::MatrixXd::Zero (dim_V_new, dim_V_new));
476513
477- auto add_real_vector_to_basis = [this ](const Vector &vector)
514+ auto add_real_vector_to_basis = [this , drop_degenerate_vector_norm_tol](
515+ const Vector &vector, std::string_view node_label)
478516 {
479517 auto dim_V = V.size ();
480518 auto &v = V.emplace_back (vector);
481519 OrthogonalizeColumn (orthog_type, space_op.GetComm (), V, v, orth_R.col (dim_V).data (),
482520 dim_V);
483521 orth_R (dim_V, dim_V) = linalg::Norml2 (space_op.GetComm (), v);
522+ if (orth_R (dim_V, dim_V) < drop_degenerate_vector_norm_tol)
523+ {
524+ V.pop_back ();
525+ return false ;
526+ }
484527 v *= 1.0 / orth_R (dim_V, dim_V);
528+ v_node_label.emplace_back (node_label);
529+ return true ;
485530 };
486531
487532 if (has_real)
488533 {
489- add_real_vector_to_basis (u.Real ());
490- v_node_label.emplace_back (fmt::format (" {}_re" , node_label));
534+ add_real_vector_to_basis (u.Real (), fmt::format (" {}_re" , node_label));
491535 }
492536 if (has_imag)
493537 {
494- add_real_vector_to_basis (u.Imag ());
495- v_node_label.emplace_back (fmt::format (" {}_im" , node_label));
538+ add_real_vector_to_basis (u.Imag (), fmt::format (" {}_im" , node_label));
539+ }
540+
541+ // Vectors might have been dropped due to orthogonality check.
542+ dim_V_new = V.size ();
543+ orth_R.conservativeResize (dim_V_new, dim_V_new); // Might shrink
544+ if (dim_V_new == dim_V_old)
545+ {
546+ return ;
496547 }
497548
498549 // Update reduced-order operators. Resize preserves the upper dim0 x dim0 block of each
0 commit comments