Skip to content

Commit 3cf59af

Browse files
Merge pull request #72 from OpenSEMBA/dev
Up-to-date-ing Tests pass, I bypass
2 parents 47baddc + 4bb6bdc commit 3cf59af

59 files changed

Lines changed: 308418 additions & 94681 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
.vscode/
33
.github/agents/
44
.github/instructions/
5+
.opencode/
56

67
build/
78
out/

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ option(SEMBA_DGTD_ENABLE_MFEM_AS_SUBDIRECTORY "Use MFEM as a subdirectory" ON )
1717
option(SEMBA_DGTD_ENABLE_EXTENSIVE_CASE_TESTS "Enable extensive case tests" ON )
1818
option(SEMBA_DGTD_ENABLE_EXTENSIVE_SOLVER_TESTS "Enable extensive solver tests" ON )
1919
option(SEMBA_DGTD_ENABLE_EXTENSIVE_RCS_TESTS "Enable extensive RCS tests" ON )
20+
option(SEMBA_DGTD_ENABLE_L2_ANALYSIS_TESTS "Enable L2 Analysis Tests" OFF)
2021

2122
option(SEMBA_DGTD_ENABLE_TIMER_INFORMATION "Enable timer information" ON)
2223

README.md

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ MFEM is included as a submodule (`external/mfem-geg`) and built automatically. T
102102
### Defining a JSON file
103103

104104
Cases can be defined by parsing a JSON file with the problem information. See a complete [example](https://github.com/OpenSEMBA/dgtd/blob/main/testData/maxwellInputs/1D_PEC/1D_PEC.json) of a valid JSON file.
105-
An OpenSEMBA/dgtd JSON file must have the following structure; **bold** entries are **required**:
105+
An OpenSEMBA/dgtd JSON file must have the following structure. Legend: **[REQUIRED]** = must be present; **[OPTIONAL]** = has a default value (shown).
106106

107107
- solver_options:
108108
- Object. User can customise solver settings. If undefined, all defaults apply.
@@ -114,18 +114,18 @@ An OpenSEMBA/dgtd JSON file must have the following structure; **bold** entries
114114
- Double. Total simulation duration in natural units (1 meter/c). If undefined, defaults to `2.0`.
115115
- time_step:
116116
- Double. Fixed time step in natural units. Must be defined for 2D and 3D problems. Overrides `cfl` for 1D if both are defined. If undefined or `0.0` in 1D, an automatic step is computed via CFL.
117-
- cfl:
118-
- Double. Courant–Friedrichs–Lewy condition used to compute the automatic time step in 1D. Not available for 2D or 3D. Ignored if `time_step` is also defined. Must be in the range (0.0, 1.0].
119-
- order:
120-
- Integer. Polynomial order of the finite element basis. If undefined, defaults to `3`.
117+
- **cfl**:
118+
- Double. Courant–Friedrichs–Lewy condition used to compute the automatic time step in 1D. Not available for 2D or 3D. Ignored if `time_step` is also defined. Must be in the range (0.0, 1.0]. Defaults to `1.0`.
119+
- **order**:
120+
- Integer. Polynomial order of the finite element basis. Defaults to `2`.
121121
- spectral:
122122
- Boolean. Use a spectral evolution operator that assembles the full E/H system matrix and derives the time step from its eigenvalues. High computational cost; does not support all features. If undefined, defaults to `false`.
123123
- export_operator:
124124
- Boolean. Write the assembled evolution operator matrix to disk for inspection. If undefined, defaults to `false`.
125-
- basis_type:
126-
- String. Finite element basis type passed to MFEM.
127-
- ode_type:
128-
- String. ODE time-integration method passed to MFEM.
125+
- **basis_type**:
126+
- Integer. Finite element basis type passed to MFEM. Defaults to `1` (GaussLobatto). Values: `0` (GaussLegendre), `1` (GaussLobatto), `2` (Bernstein), `3` (OpenUniform), `4` (CloseUniform), `5` (OpenHalfUniform).
127+
- **ode_type**:
128+
- Integer. ODE time-integration method. Defaults to `0` (RK4). Values: `0` (RK4), `1` (BackwardEuler), `2` (Trapezoidal), `3` (ImplicitMidpoint), `4` (SDIRK33), `5` (SDIRK23), `6` (SDIRK34).
129129

130130
- **model**:
131131
- Object. Contains geometry, material, and boundary information.
@@ -168,30 +168,30 @@ An OpenSEMBA/dgtd JSON file must have the following structure; **bold** entries
168168
- Object. Controls data extraction. If undefined, no data is recorded.
169169
- exporter:
170170
- Object. Enables ParaView (VisIt) field export.
171-
- name: String. Output dataset name. Defaults to the mesh filename stem.
171+
- name: String. Output dataset name. Defaults to mesh filename (e.g. `mesh.msh` → `mesh`).
172172
- steps: Integer. Export every N time steps. Mutually exclusive with `saves`.
173173
- saves: Integer. Total number of exports over the whole simulation. The step interval is computed automatically. Mutually exclusive with `steps`.
174174
- point:
175175
- Array. Each entry records all E and H field components at a single point every interval.
176176
- **position**: Array of doubles. Spatial coordinates. Must match the mesh dimension (e.g. `[x, y]` for 2D). ***Warning:*** If the point lies outside the mesh the simulation will crash.
177-
- steps / saves: See exporter above.
177+
- steps / saves: (steps and saves are mutually exclusive; see `exporter` above for details)
178178
- field:
179179
- Array. Each entry records a single scalar field component at a point.
180180
- **field_type**: String. Can be `"electric"` or `"magnetic"`.
181181
- **polarization**: String. Component to record. Can be `"X"`, `"Y"`, or `"Z"`.
182182
- **position**: Array of doubles. Spatial coordinates. Same constraints as for `point`.
183-
- steps / saves: See exporter above.
183+
- steps / saves: (steps and saves are mutually exclusive; see `exporter` above for details)
184184
- domain_snapshot:
185185
- Object. Periodic full-domain field snapshot (alternative to the incremental exporter).
186-
- name: String. Output name. Defaults to the mesh filename stem.
187-
- steps / saves: See exporter above.
186+
- name: String. Output name. Defaults to mesh filename (e.g. `mesh.msh` → `mesh`).
187+
- steps / saves: (steps and saves are mutually exclusive; see `exporter` above for details)
188188
- rcssurface:
189189
- Array. Each entry exports surface E/H field snapshots projected onto a boundary submesh to a compact binary file (`surface_data.bin`). The binary contains a geometry header (quadrature point positions, outward normals, and weights) followed by time-stamped E/H field blocks. Intended for offline RCS post-processing.
190190
- **tags**: Array of integers. Mesh surface tags that define the integration surface.
191-
- name: String. Output name.
192-
- steps / saves: See exporter above.
191+
- name: String. [OPTIONAL] Output name. Defaults to `"RCSSurfaceProbe"`.
192+
- steps / saves: (steps and saves are mutually exclusive; see `exporter` above for details)
193193
- mor_state:
194-
- Array. Each entry periodically saves the full DG state vector (all E and H DOFs) to disk over a specified time window. The saved vectors are compatible with the exported global operator matrix (`{name}_global.csr`), so that `y = A * x` can be evaluated offline (e.g. in Python). Each snapshot is written as a plain-text file `x_0`, `x_1`, … containing the vector size on the first line followed by one DOF value per line (16-digit precision). The vector layout is `[Ex₀…ExN | Ey | Ez | Hx | Hy | Hz]`.
194+
- Array. Each entry periodically saves the full DG state vector (all E and H DOFs) to disk over a specified time window. The saved vectors are compatible with the exported global operator matrix (`{name}_global.csr`), so that `y = A * x` can be evaluated offline (e.g. in Python). Each snapshot is written as a plain-text file `x_0`, `x_1`, … with: first line = absolute simulation time, second line = vector size, followed by one DOF value per line (16-digit precision). The vector layout is `[Ex₀…ExN | Ey | Ez | Hx | Hy | Hz]`.
195195
- **record_time_start**: Double. Simulation time at which recording begins.
196196
- **record_time_final**: Double. Simulation time at which recording ends.
197197
- **saves**: Integer. Number of snapshots to record, distributed uniformly between `record_time_start` and `record_time_final`.
@@ -208,8 +208,8 @@ An OpenSEMBA/dgtd JSON file must have the following structure; **bold** entries
208208
- **type**: String. Can be `"gaussian"`, `"resonant"`, `"besselj6_2D"`, or `"besselj6_3D"`.
209209
- *(For `type: "gaussian"`)* **spread**: Double. Standard deviation $\sigma$ of the Gaussian pulse.
210210
- *(For `type: "resonant"`)* **modes**: Array of integers. Number of standing waves along each spatial axis.
211-
- *(Required for `magnitude.type: "gaussian"`)* **center**: Array of n doubles. Spatial position of the Gaussian centroid.
212-
- *(Required for `magnitude.type: "gaussian"`)* **dimension**: Integer. Number of active spatial dimensions in the Gaussian exponent.
211+
- **center** (Required for `magnitude.type: "gaussian"`): Array of n doubles. Spatial position of the Gaussian centroid.
212+
- **dimension** (Required for `magnitude.type: "gaussian"`): Integer. Number of active spatial dimensions in the Gaussian exponent.
213213

214214
*For `type: "planewave"` — total-field/scattered-field (TFSF) plane wave:*
215215
- **tags**: Array of integers. Mesh boundary tags that form the TFSF interface surface.
@@ -227,6 +227,59 @@ An OpenSEMBA/dgtd JSON file must have the following structure; **bold** entries
227227
- **spread**: Double. Gaussian spread parameter.
228228
- **mean**: Double. Gaussian center position along the dipole axis.
229229

230+
231+
## MOR To ParaView Post-Processing
232+
233+
`opensemba_mor2paraview` replays previously exported `mor_state` snapshots (`x_0`, `x_1`, ...) into a ParaView time series dataset.
234+
235+
Expected folder layout (run command from this folder):
236+
237+
```text
238+
.
239+
|-- <case>.json
240+
|-- <mesh>.msh
241+
`-- x/
242+
|-- x_0
243+
|-- x_1
244+
`-- ...
245+
```
246+
247+
Snapshot format per `x_k` file:
248+
249+
1. first line: absolute simulation time
250+
2. second line: vector size
251+
3. remaining lines: state values in `[Ex | Ey | Ez | Hx | Hy | Hz]` packed order
252+
253+
Default command:
254+
255+
```sh
256+
./build/gnu-release-mpi/bin/opensemba_mor2paraview
257+
```
258+
259+
Default behavior:
260+
261+
- Detects exactly one `.json` in current directory (or errors if none/multiple).
262+
- Detects exactly one `.msh` in current directory (or errors if none/multiple).
263+
- Reads snapshots from `./x`.
264+
- Writes ParaView output to `./output`.
265+
- Uses dataset name `mor_paraview.vtk` (ParaView collection name).
266+
267+
Optional arguments:
268+
269+
```sh
270+
./build/gnu-release-mpi/bin/opensemba_mor2paraview \
271+
--case ./my_case.json \
272+
--mesh ./my_mesh.msh \
273+
--xdir ./x \
274+
--out ./output \
275+
--name mor_paraview.vtk
276+
```
277+
278+
Notes:
279+
280+
- `opensemba_mor2paraview` supports only single-rank execution.
281+
- Output is the same ParaView-style time-series format used by the exporter probe (collection plus time-step data), suitable for direct loading in ParaView.
282+
230283
## Funding
231284

232285
- Spanish Ministry of Science and Innovation (MICIN/AEI) (Grant Number: PID2022-137495OB-C31).

src/components/DGOperatorFactory.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,9 @@ namespace maxwell
15661566
// Free sub-operator blocks before applying threshold.
15671567
blocks.clear();
15681568

1569-
auto threshold = 1e-20;
1569+
// Threshold set to sqrt(eps_machine) ~ 1e-8, the standard criterion for distinguishing
1570+
// genuine matrix entries from floating-point assembly noise relative to unit-scale quantities.
1571+
auto threshold = 1e-8;
15701572
res->Threshold(threshold);
15711573

15721574
if(this->pd_.opts.export_evolution_operator){

src/driver/driver.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,12 @@ maxwell::Solver buildSolver(const json& case_data, const std::string& case_path,
16491649

16501650
maxwell::SolverOptions solverOpts{ buildSolverOptions(case_data) };
16511651
maxwell::Probes probes{ buildProbes(case_data) };
1652+
1653+
// If MOR state probes are defined, force export_operator to true
1654+
if (!probes.morStateProbes.empty()) {
1655+
solverOpts.setExportEO(true);
1656+
}
1657+
16521658
// Model (mesh) must be built before sources so that auto-mean computation
16531659
// can inspect the TFSF surface geometry to determine the correct delay.
16541660
maxwell::Model model{ buildModel(case_data, case_path, isTest) };

src/evolution/GlobalEvolution.cpp

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,31 @@ SGBCHelperFields initSGBCHelperFields(const int size)
2929
return res;
3030
}
3131

32+
static mfem::Array<int> buildSurfaceMarkerFromTags(const std::vector<int>& tags, const mfem::ParFiniteElementSpace& fes)
33+
{
34+
mfem::Array<int> marker(fes.GetMesh()->bdr_attributes.Max());
35+
marker = 0;
36+
for (const int t : tags) {
37+
marker[t - 1] = 1;
38+
}
39+
return marker;
40+
}
41+
42+
static std::vector<int> collectRCSSurfaceTags(const Probes& probes)
43+
{
44+
std::unordered_set<int> unique_tags;
45+
for (const auto& p : probes.rcsSurfaceProbes) {
46+
for (const int t : p.tags) {
47+
unique_tags.insert(t);
48+
}
49+
}
50+
std::vector<int> tags(unique_tags.begin(), unique_tags.end());
51+
std::sort(tags.begin(), tags.end());
52+
return tags;
53+
}
54+
3255
GlobalEvolution::GlobalEvolution(
33-
mfem::ParFiniteElementSpace& fes, Model& model, SourcesManager& srcmngr, EvolutionOptions& options) :
56+
mfem::ParFiniteElementSpace& fes, Model& model, SourcesManager& srcmngr, EvolutionOptions& options, const Probes& probes) :
3457
mfem::TimeDependentOperator(numberOfFieldComponents* numberOfMaxDimensions* fes.GetNDofs()),
3558
fes_{ fes },
3659
model_{ model },
@@ -312,7 +335,7 @@ GlobalEvolution::GlobalEvolution(
312335
numberOfFieldComponents * numberOfMaxDimensions * (fes_.GetNDofs() + fes_.num_face_nbr_dofs)
313336
);
314337

315-
Probes probes;
338+
Probes emptyProbes;
316339

317340
// Keep TFSF submesh infrastructure for planewave source evaluation
318341
if (model_.getTotalFieldScatteredFieldToMarker().find(BdrCond::TotalFieldIn) != model_.getTotalFieldScatteredFieldToMarker().end()) {
@@ -323,7 +346,7 @@ GlobalEvolution::GlobalEvolution(
323346
}
324347

325348
// Build all operators on the global mesh
326-
ProblemDescription pd(model_, probes, srcmngr_.sources, opts_);
349+
ProblemDescription pd(model_, emptyProbes, srcmngr_.sources, opts_);
327350
DGOperatorFactory<mfem::ParFiniteElementSpace> dgops(pd, fes_);
328351
globalOperator_ = dgops.buildGlobalOperator();
329352

@@ -344,9 +367,88 @@ GlobalEvolution::GlobalEvolution(
344367
TFSFOperator_->PrintCSR2(ofs);
345368
ofs.close();
346369
std::cout << "TFSF operator exported to " << file_path << std::endl;
370+
371+
// Export TFSF mapping matrix T (maps TFSF submesh DOFs to parent mesh DOFs)
372+
const int tfsf_ndofs = tfsf_sub_to_parent_ids_.Size();
373+
const int parent_ndofs = fes_.GetNDofs();
374+
auto mapping_matrix = std::make_unique<mfem::SparseMatrix>(6 * parent_ndofs, 6 * tfsf_ndofs);
375+
376+
for (int comp = 0; comp < 6; ++comp) {
377+
for (int tfsf_dof = 0; tfsf_dof < tfsf_ndofs; ++tfsf_dof) {
378+
int parent_dof = tfsf_sub_to_parent_ids_[tfsf_dof];
379+
int row = comp * parent_ndofs + parent_dof;
380+
int col = comp * tfsf_ndofs + tfsf_dof;
381+
mapping_matrix->Set(row, col, 1.0);
382+
}
383+
}
384+
mapping_matrix->Finalize();
385+
386+
std::filesystem::path mapping_path = export_dir / (model_.meshName_ + "_tfsf_mapping.csr");
387+
std::ofstream ofs_map(mapping_path);
388+
if (!ofs_map.is_open()) {
389+
throw std::runtime_error("Could not open file for writing: " + mapping_path.string());
390+
}
391+
mapping_matrix->PrintCSR2(ofs_map);
392+
ofs_map.close();
393+
std::cout << "TFSF mapping matrix exported to " << mapping_path << std::endl;
394+
}
395+
}
396+
}
397+
398+
const bool has_rcs_probe = !probes.rcsSurfaceProbes.empty();
399+
const bool has_mor_probe = !probes.morStateProbes.empty();
400+
if (opts_.export_evolution_operator && has_rcs_probe && has_mor_probe) {
401+
if (Mpi::WorldSize() == 1) {
402+
const auto rcs_tags = collectRCSSurfaceTags(probes);
403+
if (!rcs_tags.empty()) {
404+
auto marker = buildSurfaceMarkerFromTags(rcs_tags, fes_);
405+
NearToFarFieldSubMesher ntff_submesher(model_.getConstMesh(), fes_, marker);
406+
407+
auto* ntff_submesh = ntff_submesher.getSubMesh();
408+
auto dgfec = dynamic_cast<const mfem::DG_FECollection*>(fes_.FEColl());
409+
if (!dgfec) {
410+
throw std::runtime_error("The FiniteElementCollection in the FiniteElementSpace is not DG.");
411+
}
412+
413+
mfem::FiniteElementSpace ntff_fes(ntff_submesh, dgfec);
414+
mfem::Array<int> farfield_sub_to_parent_ids;
415+
mfem::SubMeshUtils::BuildVdofToVdofMap(ntff_fes,
416+
fes_,
417+
ntff_submesh->GetFrom(),
418+
ntff_submesh->GetParentElementIDMap(),
419+
farfield_sub_to_parent_ids);
420+
421+
const int farfield_ndofs = farfield_sub_to_parent_ids.Size();
422+
const int parent_ndofs = fes_.GetNDofs();
423+
auto farfield_mapping_matrix = std::make_unique<mfem::SparseMatrix>(6 * farfield_ndofs, 6 * parent_ndofs);
424+
425+
for (int comp = 0; comp < 6; ++comp) {
426+
for (int sub_dof = 0; sub_dof < farfield_ndofs; ++sub_dof) {
427+
const int parent_dof = farfield_sub_to_parent_ids[sub_dof];
428+
const int row = comp * farfield_ndofs + sub_dof;
429+
const int col = comp * parent_ndofs + parent_dof;
430+
farfield_mapping_matrix->Set(row, col, 1.0);
431+
}
432+
}
433+
farfield_mapping_matrix->Finalize();
434+
435+
std::filesystem::path export_dir = std::filesystem::path("Exports") / "Operators" / model_.meshName_;
436+
if (!std::filesystem::exists(export_dir)) {
437+
std::filesystem::create_directories(export_dir);
438+
}
439+
440+
std::filesystem::path farfield_path = export_dir / (model_.meshName_ + "_farfield.csr");
441+
std::ofstream ofs_farfield(farfield_path);
442+
if (!ofs_farfield.is_open()) {
443+
throw std::runtime_error("Could not open file for writing: " + farfield_path.string());
444+
}
445+
farfield_mapping_matrix->PrintCSR2(ofs_farfield);
446+
ofs_farfield.close();
447+
std::cout << "Farfield mapping matrix exported to " << farfield_path << std::endl;
347448
}
348449
}
349450
}
451+
350452
if (model_.getSGBCToMarker().find(BdrCond::SGBC) != model_.getSGBCToMarker().end()) {
351453
SGBCOperator_ = dgops.buildSGBCGlobalOperator();
352454
}

src/evolution/GlobalEvolution.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "solver/SourcesManager.h"
44
#include "solver/SolverExtension.h"
55
#include "components/DGOperatorFactory.h"
6+
#include "components/Probes.h"
67
#include <unordered_map>
78

89
namespace maxwell {
@@ -16,7 +17,7 @@ class GlobalEvolution : public mfem::TimeDependentOperator {
1617
static const int numberOfFieldComponents = 2;
1718
static const int numberOfMaxDimensions = 3;
1819

19-
GlobalEvolution(mfem::ParFiniteElementSpace&, Model&, SourcesManager&, EvolutionOptions&);
20+
GlobalEvolution(mfem::ParFiniteElementSpace&, Model&, SourcesManager&, EvolutionOptions&, const Probes&);
2021
~GlobalEvolution();
2122

2223
virtual void Mult(const mfem::Vector& x, mfem::Vector& y) const;
@@ -28,6 +29,8 @@ class GlobalEvolution : public mfem::TimeDependentOperator {
2829

2930
const mfem::SparseMatrix& getConstGlobalOperator() { return *globalOperator_.get(); }
3031

32+
const mfem::Array<int>& getTFSFMapping() const { return tfsf_sub_to_parent_ids_; }
33+
3134
bool hasSGBC() const { return !sgbc_states_.empty(); }
3235

3336
private:

src/launcher/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,18 @@ target_link_libraries(opensemba_rcs
3030
if (SEMBA_DGTD_ENABLE_OPENMP)
3131
message(STATUS "Linking opensemba_rcs with OpenMP libraries")
3232
target_link_libraries(opensemba_rcs OpenMP::OpenMP_C OpenMP::OpenMP_CXX)
33+
endif()
34+
35+
message(STATUS "Creating build system for opensemba_mor2paraview")
36+
37+
add_executable(opensemba_mor2paraview "mor2paraview.cpp")
38+
39+
target_link_libraries(opensemba_mor2paraview
40+
maxwell-driver
41+
mfem
42+
)
43+
44+
if (SEMBA_DGTD_ENABLE_OPENMP)
45+
message(STATUS "Linking opensemba_mor2paraview with OpenMP libraries")
46+
target_link_libraries(opensemba_mor2paraview OpenMP::OpenMP_C OpenMP::OpenMP_CXX)
3347
endif()

0 commit comments

Comments
 (0)