loudpy 0.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. loudpy-0.0.5/PKG-INFO +73 -0
  2. loudpy-0.0.5/README.md +57 -0
  3. loudpy-0.0.5/pyproject.toml +43 -0
  4. loudpy-0.0.5/src/loudpy/.DS_Store +0 -0
  5. loudpy-0.0.5/src/loudpy/Files_Loader/Analysis_Helpers.py +185 -0
  6. loudpy-0.0.5/src/loudpy/Files_Loader/Core_Models.py +114 -0
  7. loudpy-0.0.5/src/loudpy/Files_Loader/Field_Operations_Helpers.py +70 -0
  8. loudpy-0.0.5/src/loudpy/Files_Loader/Load_Domains.py +24 -0
  9. loudpy-0.0.5/src/loudpy/Files_Loader/Mesh_Loader.py +58 -0
  10. loudpy-0.0.5/src/loudpy/Files_Loader/Readers.py +416 -0
  11. loudpy-0.0.5/src/loudpy/Files_Loader/__init__.py +16 -0
  12. loudpy-0.0.5/src/loudpy/Files_Saver/Mesh_topology.py +53 -0
  13. loudpy-0.0.5/src/loudpy/Files_Saver/Result_Store.py +293 -0
  14. loudpy-0.0.5/src/loudpy/Files_Saver/Saving_Helpers.py +51 -0
  15. loudpy-0.0.5/src/loudpy/Files_Saver/__init__.py +2 -0
  16. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/.DS_Store +0 -0
  17. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_FSI.py +54 -0
  18. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_H.py +59 -0
  19. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_H_pml.py +95 -0
  20. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_K.py +64 -0
  21. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_M.py +57 -0
  22. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_NL.py +163 -0
  23. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_Q.py +58 -0
  24. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_Q_pml.py +94 -0
  25. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_unit_force.py +43 -0
  26. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313 2.nbi +0 -0
  27. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.1 2.nbc +0 -0
  28. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.1.nbc +0 -0
  29. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.nbi +0 -0
  30. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313 2.nbi +0 -0
  31. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.1 2.nbc +0 -0
  32. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.1.nbc +0 -0
  33. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.nbi +0 -0
  34. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/FE_kernels.py +70 -0
  35. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/Gauss_Quadratures.py +35 -0
  36. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__init__.py +24 -0
  37. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313 2.nbi +0 -0
  38. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.1 2.nbc +0 -0
  39. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.1.nbc +0 -0
  40. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.nbi +0 -0
  41. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313 2.nbi +0 -0
  42. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.1 2.nbc +0 -0
  43. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.1.nbc +0 -0
  44. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.nbi +0 -0
  45. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313 2.nbi +0 -0
  46. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.1 2.nbc +0 -0
  47. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.1.nbc +0 -0
  48. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.nbi +0 -0
  49. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313 2.nbi +0 -0
  50. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.1 2.nbc +0 -0
  51. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.1.nbc +0 -0
  52. loudpy-0.0.5/src/loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.nbi +0 -0
  53. loudpy-0.0.5/src/loudpy/Interfaces/InterfaceAcouMeca.py +12 -0
  54. loudpy-0.0.5/src/loudpy/Interfaces/InterfaceConstrainted.py +12 -0
  55. loudpy-0.0.5/src/loudpy/Interfaces/InterfaceForced.py +12 -0
  56. loudpy-0.0.5/src/loudpy/Interfaces/__init__.py +7 -0
  57. loudpy-0.0.5/src/loudpy/Materials_Bank/materials.json +87 -0
  58. loudpy-0.0.5/src/loudpy/Meshing/Cleaning_step_file.py +38 -0
  59. loudpy-0.0.5/src/loudpy/Meshing/MeshManager.py +511 -0
  60. loudpy-0.0.5/src/loudpy/Meshing/__init__.py +9 -0
  61. loudpy-0.0.5/src/loudpy/ObjectsGeo.py +31 -0
  62. loudpy-0.0.5/src/loudpy/PhysicalEntitiesManager.py +56 -0
  63. loudpy-0.0.5/src/loudpy/Plotter/Plotting_SPL_directivity.py +79 -0
  64. loudpy-0.0.5/src/loudpy/Plotter/Plotting_animate_time.py +229 -0
  65. loudpy-0.0.5/src/loudpy/Plotter/Plotting_fsi_fields.py +36 -0
  66. loudpy-0.0.5/src/loudpy/Plotter/Plotting_helpers.py +154 -0
  67. loudpy-0.0.5/src/loudpy/Plotter/Studies_Plots.py +384 -0
  68. loudpy-0.0.5/src/loudpy/Plotter/__init__.py +5 -0
  69. loudpy-0.0.5/src/loudpy/SimObjects.py +90 -0
  70. loudpy-0.0.5/src/loudpy/Studies/.DS_Store +0 -0
  71. loudpy-0.0.5/src/loudpy/Studies/DofMapMixin.py +35 -0
  72. loudpy-0.0.5/src/loudpy/Studies/Eigen_study.py +547 -0
  73. loudpy-0.0.5/src/loudpy/Studies/Freq_study.py +186 -0
  74. loudpy-0.0.5/src/loudpy/Studies/Study_class.py +92 -0
  75. loudpy-0.0.5/src/loudpy/Studies/Time_study.py +301 -0
  76. loudpy-0.0.5/src/loudpy/Studies/__init__.py +10 -0
  77. loudpy-0.0.5/src/loudpy/SubDomains/SubDomainAcou.py +22 -0
  78. loudpy-0.0.5/src/loudpy/SubDomains/SubDomainMeca.py +28 -0
  79. loudpy-0.0.5/src/loudpy/SubDomains/__init__.py +12 -0
  80. loudpy-0.0.5/src/loudpy/__init__.py +23 -0
  81. loudpy-0.0.5/src/loudpy/assembly/.DS_Store +0 -0
  82. loudpy-0.0.5/src/loudpy/assembly/__init__.py +14 -0
  83. loudpy-0.0.5/src/loudpy/assembly/assemblers/assembly_edge.py +201 -0
  84. loudpy-0.0.5/src/loudpy/assembly/assemblers/assembly_tri.py +494 -0
  85. loudpy-0.0.5/src/loudpy/assembly/core/base.py +16 -0
  86. loudpy-0.0.5/src/loudpy/assembly/core/csr_pattern.py +23 -0
  87. loudpy-0.0.5/src/loudpy/assembly/core/helpers.py +12 -0
  88. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313 2.nbi +0 -0
  89. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.1 2.nbc +0 -0
  90. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.1.nbc +0 -0
  91. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.nbi +0 -0
  92. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313 2.nbi +0 -0
  93. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.1 2.nbc +0 -0
  94. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.1.nbc +0 -0
  95. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.2 2.nbc +0 -0
  96. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.2.nbc +0 -0
  97. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.nbi +0 -0
  98. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313 2.nbi +0 -0
  99. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.1 2.nbc +0 -0
  100. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.1.nbc +0 -0
  101. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.nbi +0 -0
  102. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313 2.nbi +0 -0
  103. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.1 2.nbc +0 -0
  104. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.1.nbc +0 -0
  105. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.nbi +0 -0
  106. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313 2.nbi +0 -0
  107. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.1 2.nbc +0 -0
  108. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.1.nbc +0 -0
  109. loudpy-0.0.5/src/loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.nbi +0 -0
  110. loudpy-0.0.5/src/loudpy/assembly/kernels/csr.py +20 -0
  111. loudpy-0.0.5/src/loudpy/assembly/kernels/elements.py +99 -0
  112. loudpy-0.0.5/src/loudpy/assembly/kernels/fsi.py +149 -0
  113. loudpy-0.0.5/src/loudpy/assembly/kernels/indexing.py +33 -0
loudpy-0.0.5/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.1
2
+ Name: loudpy
3
+ Version: 0.0.5
4
+ Summary: Python API for Fluid-solid interaction FEM simulations
5
+ Author-Email: Romain Degraeve <rom2graeve@gmail.com>
6
+ License: GPL-3.0+
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: numpy>=2.4.4
9
+ Requires-Dist: numba>=0.65.0
10
+ Requires-Dist: scipy>=1.17.1
11
+ Requires-Dist: gmsh>=4.15.2
12
+ Requires-Dist: h5py>=3.16.0
13
+ Requires-Dist: matplotlib>=3.10.9
14
+ Requires-Dist: pyvista>=0.48.4
15
+ Description-Content-Type: text/markdown
16
+
17
+ # loudpy
18
+
19
+ A fem python API.
20
+
21
+ ## Installation
22
+
23
+ ### PDM Installation
24
+
25
+ loudpy relies on the Python Development Master (PDM) for package management.
26
+
27
+
28
+
29
+ #### UV Installation
30
+
31
+ UV is a modern Python package manager that provides a simple and efficient way to manage Python packages and environments. For example, different python intepreter can be installed through the commands:
32
+
33
+ ```bash
34
+ uv python install 3.11 3.12 3.13
35
+ ```
36
+
37
+ ThisPython API is developped under python 3.13, so with uv run:
38
+
39
+ ```bash
40
+ uv python install 3.13
41
+ ```
42
+
43
+ Then, to install PDM using UV, run the following command:
44
+
45
+ ```bash
46
+ uv pip install pdm
47
+ ```
48
+
49
+ #### PIPX Installation
50
+
51
+ To install PDM using pipx, run the following command:
52
+
53
+ ```bash
54
+ pipx install pdm
55
+ ```
56
+
57
+ Prefer `pipx` as it allows you to install Python applications in isolated environments, preventing conflicts between packages.
58
+
59
+ #### Verify PDM Installation
60
+
61
+ When the installation is complete, you can verify that PDM is installed correctly by running:
62
+
63
+ ```bash
64
+ pdm --version
65
+ ```
66
+
67
+ ### loudpy Installation
68
+
69
+ To install loudpy for development purposes, use PDM to install it in editable mode. This allows you to make changes to the source code and have those changes reflected immediately without needing to reinstall the package. To do this, navigate to the directory where you have the loudpy source code and run:
70
+
71
+ ```bash
72
+ pdm install -e .
73
+ ```
loudpy-0.0.5/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # loudpy
2
+
3
+ A fem python API.
4
+
5
+ ## Installation
6
+
7
+ ### PDM Installation
8
+
9
+ loudpy relies on the Python Development Master (PDM) for package management.
10
+
11
+
12
+
13
+ #### UV Installation
14
+
15
+ UV is a modern Python package manager that provides a simple and efficient way to manage Python packages and environments. For example, different python intepreter can be installed through the commands:
16
+
17
+ ```bash
18
+ uv python install 3.11 3.12 3.13
19
+ ```
20
+
21
+ ThisPython API is developped under python 3.13, so with uv run:
22
+
23
+ ```bash
24
+ uv python install 3.13
25
+ ```
26
+
27
+ Then, to install PDM using UV, run the following command:
28
+
29
+ ```bash
30
+ uv pip install pdm
31
+ ```
32
+
33
+ #### PIPX Installation
34
+
35
+ To install PDM using pipx, run the following command:
36
+
37
+ ```bash
38
+ pipx install pdm
39
+ ```
40
+
41
+ Prefer `pipx` as it allows you to install Python applications in isolated environments, preventing conflicts between packages.
42
+
43
+ #### Verify PDM Installation
44
+
45
+ When the installation is complete, you can verify that PDM is installed correctly by running:
46
+
47
+ ```bash
48
+ pdm --version
49
+ ```
50
+
51
+ ### loudpy Installation
52
+
53
+ To install loudpy for development purposes, use PDM to install it in editable mode. This allows you to make changes to the source code and have those changes reflected immediately without needing to reinstall the package. To do this, navigate to the directory where you have the loudpy source code and run:
54
+
55
+ ```bash
56
+ pdm install -e .
57
+ ```
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "loudpy"
3
+ version = "0.0.5"
4
+ description = "Python API for Fluid-solid interaction FEM simulations"
5
+ authors = [
6
+ { name = "Romain Degraeve", email = "rom2graeve@gmail.com" },
7
+ ]
8
+ dependencies = [
9
+ "numpy>=2.4.4",
10
+ "numba>=0.65.0",
11
+ "scipy>=1.17.1",
12
+ "gmsh>=4.15.2",
13
+ "h5py>=3.16.0",
14
+ "matplotlib>=3.10.9",
15
+ "pyvista>=0.48.4",
16
+ ]
17
+ requires-python = ">=3.13"
18
+ readme = "README.md"
19
+
20
+ [project.license]
21
+ text = "GPL-3.0+"
22
+
23
+ [build-system]
24
+ requires = [
25
+ "pdm-backend",
26
+ ]
27
+ build-backend = "pdm.backend"
28
+
29
+ [tool.pdm]
30
+ distribution = true
31
+
32
+ [tool.pdm.build]
33
+ package-dir = "src"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "matplotlib>=3.10.8",
38
+ "ruff>=0.15.11",
39
+ "black>=26.3.1",
40
+ ]
41
+ test = [
42
+ "pytest>=9.0.3",
43
+ ]
Binary file
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import matplotlib.tri as mtri
4
+ import numpy as np
5
+
6
+ from loudpy.Files_Loader.Load_Domains import Domain
7
+ from loudpy.Files_Loader.Core_Models import Mesh, FreqSnapshot, Snapshot
8
+ from loudpy.Files_Loader.Readers import FreqReader
9
+ from loudpy.Files_Loader.Field_Operations_Helpers import scatter_dofs, reduce_field, infer_dpn
10
+
11
+ class NearestNodeProbe:
12
+ """Sample at the mesh node closest to a coordinate."""
13
+
14
+ def __init__(self, mesh: Mesh, xy):
15
+ self.mesh = mesh
16
+ self.idx, self.xy = mesh.nearest_node(xy)
17
+
18
+ def sample_snapshot(self, snap: Snapshot, field_name: str,
19
+ component: str = "mag") -> float:
20
+ return float(snap.nodal(field_name, self.mesh, component=component)[self.idx])
21
+
22
+ def sample_matrix(self, mat: np.ndarray, dof_tags: np.ndarray | None,
23
+ dpn: int, component: str) -> np.ndarray:
24
+ vals = np.empty(mat.shape[0])
25
+ for k in range(mat.shape[0]):
26
+ arr = scatter_dofs(mat[k], dof_tags, self.mesh, dpn)
27
+ vals[k] = reduce_field(arr, component)[self.idx]
28
+ return vals
29
+
30
+
31
+ class FieldProbe:
32
+ """Interpolate a scalar field at arbitrary (x, y) coordinates."""
33
+
34
+ def __init__(self, snap: Snapshot, mesh: Mesh, field_name: str):
35
+ self.snap, self.mesh, self.field_name = snap, mesh, field_name
36
+ val = snap.complex_nodal(field_name, mesh)
37
+ if val.ndim > 1:
38
+ raise ValueError("FieldProbe only handles scalar fields")
39
+ tri = mesh.triangulation
40
+ self._fr = mtri.LinearTriInterpolator(tri, val.real)
41
+ self._fi = mtri.LinearTriInterpolator(tri, val.imag)
42
+
43
+ def at(self, xy) -> np.ndarray:
44
+ xy = np.atleast_2d(xy)
45
+ return np.array([complex(float(self._fr(x, y)), float(self._fi(x, y)))
46
+ for x, y in xy])
47
+
48
+
49
+ class Directivity:
50
+ """Far-field directivity from a single FreqSnapshot."""
51
+
52
+ def __init__(self, snap: FreqSnapshot, mesh: Mesh,
53
+ radius: float = 0.4, n_theta: int = 361,
54
+ theta_min: float = 0, theta_max: float = 2*np.pi):
55
+ self.snap, self.mesh, self.radius = snap, mesh, radius
56
+ self.theta = np.linspace(theta_min, theta_max, n_theta)
57
+ probe = FieldProbe(snap, mesh, "p_acou")
58
+ xy = np.column_stack([radius * np.sin(self.theta),
59
+ radius * np.cos(self.theta)])
60
+ self.p = probe.at(xy)
61
+
62
+
63
+ class InterfaceExtractor:
64
+ """Extract complex DOFs along an interface, sorted along x."""
65
+
66
+ def __init__(self, snap: Snapshot, mesh: Mesh,
67
+ iface_tags: np.ndarray, field_name: str = "u_meca"):
68
+ self.field_name = field_name
69
+ dof_tags = snap.dof_maps[field_name]
70
+ t2dof = {int(t): i for i, t in enumerate(dof_tags)}
71
+ rows = np.array([t2dof[int(t)] for t in iface_tags if int(t) in t2dof])
72
+ raw = snap.fields[field_name]
73
+ dpn = infer_dpn(field_name)
74
+
75
+ if dpn == 1:
76
+ comps = {"v": raw[rows]}
77
+ else:
78
+ comps = {f"u{c}": raw[dpn * rows + d]
79
+ for d, c in enumerate("xy"[:dpn])}
80
+
81
+ t2m = mesh.tag_to_row
82
+ mr = [t2m[int(t)] for t in iface_tags if int(t) in t2m]
83
+ x, y = mesh.coords[mr, 0], mesh.coords[mr, 1]
84
+ order = np.argsort(x)
85
+
86
+ self.x, self.y = x[order], y[order]
87
+ self.components = {k: v[order] for k, v in comps.items()}
88
+
89
+
90
+ class FrequencySweep:
91
+ """Sweep over a folder of snap_*.h5 — probes pressure at a point."""
92
+
93
+ def __init__(self, folder: str | Path, probe_xy=(0.4, 0.0)):
94
+ self.folder = Path(folder)
95
+ self.files = sorted(self.folder.glob("snap_*.h5"))
96
+ if not self.files:
97
+ raise FileNotFoundError(f"No snap_*.h5 in {folder}")
98
+ self.probe_xy = probe_xy
99
+ self._freqs: np.ndarray | None = None
100
+ self._pressure: np.ndarray | None = None
101
+
102
+ def run(self) -> tuple[np.ndarray, np.ndarray]:
103
+ fs, ps = [], []
104
+ for fp in self.files:
105
+ with FreqReader(fp) as r:
106
+ snap = r.load()
107
+ mesh = r.mesh(snap.mesh_id, Domain.ACOU)
108
+ ps.append(FieldProbe(snap, mesh, "p_acou").at(self.probe_xy)[0])
109
+ fs.append(snap.f)
110
+ self._freqs = np.asarray(fs)
111
+ self._pressure = np.asarray(ps)
112
+ return self._freqs, self._pressure
113
+
114
+
115
+ class MecaFrequencySweep:
116
+ """Sweep over a folder of snap_*.h5 — probes a mechanical DOF
117
+ (displacement, velocity, or acceleration) at the nearest node to `probe_xy`.
118
+
119
+ Parameters
120
+ ----------
121
+ folder : directory containing snap_*.h5 files
122
+ probe_xy : (x, y) coordinate on the MECA mesh
123
+ field : stored mechanical field name (default 'u_meca')
124
+ component : 'ux', 'uy', 'uz' (alias for 'uy' in 2D), or 'mag'
125
+ quantity : 'u' displacement, 'v' velocity (jω·u), 'a' acceleration (-ω²·u)
126
+ """
127
+
128
+ _COMP_ALIAS = {"uz": "uy"} # 2D convention used elsewhere in loudpy
129
+
130
+ def __init__(self, folder: str | Path, probe_xy=(0.0, 0.0), *,
131
+ field: str = "u_meca",
132
+ component: str = "uz",
133
+ quantity: str = "u"):
134
+ self.folder = Path(folder)
135
+ self.files = sorted(self.folder.glob("snap_*.h5"))
136
+ if not self.files:
137
+ raise FileNotFoundError(f"No snap_*.h5 in {folder}")
138
+ if quantity not in ("u", "v", "a"):
139
+ raise ValueError("quantity must be 'u', 'v', or 'a'")
140
+
141
+ self.probe_xy = probe_xy
142
+ self.field = field
143
+ self.component = self._COMP_ALIAS.get(component, component)
144
+ self.quantity = quantity
145
+
146
+ self._freqs: np.ndarray | None = None
147
+ self._values: np.ndarray | None = None
148
+
149
+ def run(self) -> tuple[np.ndarray, np.ndarray]:
150
+ fs, vals = [], []
151
+ dpn = infer_dpn(self.field)
152
+
153
+ for fp in self.files:
154
+ with FreqReader(fp) as r:
155
+ snap = r.load()
156
+ mesh = r.mesh(snap.mesh_id, Domain.MECA)
157
+
158
+ # nearest node on the MECA mesh
159
+ probe = NearestNodeProbe(mesh, self.probe_xy)
160
+
161
+ # scatter complex DOFs to nodal values, then pick component
162
+ raw_dofs = snap.fields[self.field] # complex (n_dof,)
163
+ dof_tags = snap.dof_maps.get(self.field)
164
+ nodal = scatter_dofs(raw_dofs, dof_tags, mesh, dpn) # complex (n_nodes, dpn)
165
+ # keep it complex: reduce_field with 'ux'/'uy' just selects a column
166
+ if self.component in ("ux", "uy"):
167
+ col = {"ux": 0, "uy": 1}[self.component]
168
+ val = nodal[probe.idx, col] if nodal.ndim == 2 else nodal[probe.idx]
169
+ elif self.component == "mag":
170
+ v = nodal[probe.idx]
171
+ val = np.linalg.norm(v) if np.ndim(v) else v
172
+ else:
173
+ raise ValueError(f"Unsupported component {self.component!r}")
174
+
175
+ # convert displacement -> velocity / acceleration in frequency domain
176
+ omega = 2 * np.pi * snap.f
177
+ if self.quantity == "v": val = 1j * omega * val
178
+ elif self.quantity == "a": val = -(omega ** 2) * val
179
+
180
+ vals.append(complex(val))
181
+ fs.append(snap.f)
182
+
183
+ self._freqs = np.asarray(fs)
184
+ self._values = np.asarray(vals)
185
+ return self._freqs, self._values
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from functools import cached_property
6
+ from typing import ClassVar, Iterator
7
+ import matplotlib.tri as mtri
8
+ import numpy as np
9
+
10
+ from loudpy.Files_Loader.Field_Operations_Helpers import (
11
+ infer_dpn, scatter_dofs, reduce_field
12
+ )
13
+
14
+ @dataclass(frozen=True)
15
+ class Mesh:
16
+ tags: np.ndarray # (N,) global node tags
17
+ coords: np.ndarray # (N, 2)
18
+ tris: np.ndarray # (M, 3) row indices
19
+
20
+ @property
21
+ def n_nodes(self) -> int: return len(self.tags)
22
+ @property
23
+ def n_tris(self) -> int: return len(self.tris)
24
+
25
+ @cached_property
26
+ def tag_to_row(self) -> dict[int, int]:
27
+ return {int(t): i for i, t in enumerate(self.tags)}
28
+
29
+ @cached_property
30
+ def triangulation(self) -> mtri.Triangulation:
31
+ return mtri.Triangulation(self.coords[:, 0], self.coords[:, 1], self.tris)
32
+
33
+ def nearest_node(self, xy) -> tuple[int, np.ndarray]:
34
+ xy = np.asarray(xy, dtype=float)
35
+ idx = int(np.argmin(np.sum((self.coords - xy) ** 2, axis=1)))
36
+ return idx, self.coords[idx]
37
+
38
+
39
+ @dataclass
40
+ class Snapshot(ABC):
41
+ """Any field result on a mesh (freq / mode / time)."""
42
+ label: str
43
+ fields: dict[str, np.ndarray]
44
+ dof_maps: dict[str, np.ndarray]
45
+ mesh_id: str
46
+ meta: dict = field(default_factory=dict)
47
+
48
+ @abstractmethod
49
+ def coordinate(self) -> float: ...
50
+
51
+ def nodal(self, name: str, mesh: Mesh, *,
52
+ component: str = "mag", dpn: int | None = None) -> np.ndarray:
53
+ dpn = dpn or infer_dpn(name)
54
+ raw = self.fields[name]
55
+ arr = scatter_dofs(raw, self.dof_maps.get(name), mesh, dpn)
56
+ return reduce_field(arr, component)
57
+
58
+ def complex_nodal(self, name: str, mesh: Mesh,
59
+ dpn: int | None = None) -> np.ndarray:
60
+ dpn = dpn or infer_dpn(name)
61
+ return scatter_dofs(self.fields[name],
62
+ self.dof_maps.get(name), mesh, dpn)
63
+
64
+
65
+ @dataclass
66
+ class FreqSnapshot(Snapshot):
67
+ f: float = 0.0
68
+ def coordinate(self) -> float: return self.f
69
+
70
+
71
+ @dataclass
72
+ class ModeSnapshot(Snapshot):
73
+ freq: float = 0.0
74
+ zeta: float | None = None
75
+ index: int = 0
76
+ def coordinate(self) -> float: return self.freq
77
+
78
+
79
+ @dataclass
80
+ class TimeSnapshot(Snapshot):
81
+ t: float = 0.0
82
+ def coordinate(self) -> float: return self.t
83
+
84
+
85
+ @dataclass
86
+ class TimeRun:
87
+ """Bundles all arrays of a single transient run."""
88
+ time: np.ndarray
89
+ U: np.ndarray
90
+ V: np.ndarray
91
+ A: np.ndarray
92
+ dof_maps: dict[str, np.ndarray]
93
+ mesh_id: str
94
+
95
+ _FIELD_MAP: ClassVar[dict[str, str]] = {
96
+ "u_meca": "U", "v_meca": "V", "a_meca": "A",
97
+ }
98
+
99
+ def array_for(self, field_name: str) -> np.ndarray:
100
+ return getattr(self, self._FIELD_MAP.get(field_name, "U"))
101
+
102
+ def snapshot_at(self, idx: int, field_name: str = "u_meca") -> TimeSnapshot:
103
+ arr = self.array_for(field_name)
104
+ return TimeSnapshot(
105
+ label = f"t={self.time[idx]*1e3:.3f}ms",
106
+ fields = {field_name: arr[idx]},
107
+ dof_maps = self.dof_maps,
108
+ mesh_id = self.mesh_id,
109
+ t = float(self.time[idx]),
110
+ )
111
+
112
+ def iter_snapshots(self, field_name: str = "u_meca") -> Iterator[TimeSnapshot]:
113
+ for i in range(len(self.time)):
114
+ yield self.snapshot_at(i, field_name)
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+
6
+ def infer_dpn(field_name: str) -> int:
7
+ _DPN_TABLE: dict[str, int] = {
8
+ "u_meca": 2, "v_meca": 2, "a_meca": 2,
9
+ "p_acou": 1, "phi_acou": 1,
10
+ }
11
+ return _DPN_TABLE.get(field_name, 1)
12
+
13
+
14
+ def scatter_dofs(raw: np.ndarray, tags: np.ndarray | None,
15
+ mesh, dpn: int) -> np.ndarray:
16
+
17
+ """Place raw DOFs onto mesh nodes; returns (N,) or (N, dpn)."""
18
+ N = mesh.n_nodes
19
+ shape = (N,) if dpn == 1 else (N, dpn)
20
+ out = np.zeros(shape, dtype=complex if np.iscomplexobj(raw) else float)
21
+
22
+ if tags is None:
23
+ if dpn == 1:
24
+ n = min(len(raw), N); out[:n] = raw[:n]
25
+ else:
26
+ n = min(len(raw) // dpn, N)
27
+ out[:n] = raw[:dpn * n].reshape(n, dpn)
28
+ return out
29
+
30
+ t2r = mesh.tag_to_row
31
+ rows = np.fromiter((t2r[int(t)] for t in tags), dtype=int, count=len(tags))
32
+ if dpn == 1:
33
+ out[rows] = raw[: len(rows)]
34
+ else:
35
+ out[rows] = raw[: dpn * len(rows)].reshape(len(rows), dpn)
36
+ return out
37
+
38
+
39
+ def reduce_field(arr: np.ndarray, component: str) -> np.ndarray:
40
+ """Reduce (N,) or (N,dpn) array to (N,) scalar per component."""
41
+ if component == "complex":
42
+ return arr
43
+ if arr.ndim == 1:
44
+ return {
45
+ "abs": np.abs(arr),
46
+ "mag": np.abs(arr),
47
+ "real": arr.real,
48
+ "imag": arr.imag,
49
+ "phase": np.angle(arr),
50
+ }.get(component, np.abs(arr))
51
+
52
+ # (N, dpn) — vector field
53
+ if component in ("mag", "abs"):
54
+ return np.linalg.norm(np.abs(arr), axis=1)
55
+ if component in ("ux", "x"): return _real_of(arr[:, 0])
56
+ if component in ("uy", "y"): return _real_of(arr[:, 1])
57
+ if component == "real": return arr[:, 0].real
58
+ if component == "imag": return arr[:, 0].imag
59
+ if component == "phase_dominant":
60
+ dom = np.argmax(np.abs(arr), axis=1)
61
+ return np.angle(arr[np.arange(arr.shape[0]), dom])
62
+ return np.linalg.norm(np.abs(arr), axis=1)
63
+
64
+
65
+ def _real_of(x: np.ndarray) -> np.ndarray:
66
+ return x.real if np.iscomplexobj(x) else x
67
+
68
+
69
+ def SPL(p_mag: np.ndarray, p_ref: float = 20e-6) -> np.ndarray:
70
+ return 20.0 * np.log10(np.maximum(np.abs(p_mag), 1e-30) / p_ref)
@@ -0,0 +1,24 @@
1
+ from enum import Enum
2
+
3
+ class Domain(str, Enum):
4
+ """Physics-domain identifier — single source of truth."""
5
+ ACOU = "acou"
6
+ MECA = "meca"
7
+
8
+ @property
9
+ def field_name(self) -> str:
10
+ return {"acou": "p_acou", "meca": "u_meca"}[self.value]
11
+
12
+ @property
13
+ def default_dpn(self) -> int:
14
+ return {"acou": 1, "meca": 2}[self.value]
15
+
16
+ @property
17
+ def subgroup_prefixes(self) -> tuple[str, ...]:
18
+ return {"acou": ("subdomainacou",),
19
+ "meca": ("subdomainmeca",)}[self.value]
20
+
21
+ @classmethod
22
+ def coerce(cls, v: "Domain | str") -> "Domain":
23
+ return v if isinstance(v, cls) else cls(v)
24
+
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+ import h5py
3
+ import numpy as np
4
+
5
+ from loudpy.Files_Loader.Load_Domains import Domain
6
+ from loudpy.Files_Loader.Core_Models import Mesh
7
+
8
+ class MeshLoader:
9
+ """Loads a Mesh from any loudpy HDF5 layout."""
10
+
11
+ @staticmethod
12
+ def load(h5: h5py.File, mesh_id: str, domain: Domain | str) -> Mesh:
13
+ dom = Domain.coerce(domain)
14
+ g = h5[f"meshes/{mesh_id}"]
15
+
16
+ subgroups = MeshLoader._find_subgroups(g, dom)
17
+ if not subgroups and {"tags", "tris"} <= set(g.keys()):
18
+ subgroups = [g]
19
+ if not subgroups:
20
+ raise ValueError(f"No mesh data for domain '{dom.value}' in {mesh_id}")
21
+
22
+ all_tags, all_coords, all_tris = [], [], []
23
+ for sg in subgroups:
24
+ tags, coords, tris_tags = MeshLoader._extract_subgroup(sg)
25
+ all_tags.append(tags)
26
+ all_coords.append(coords)
27
+ all_tris.append(tris_tags)
28
+
29
+ return MeshLoader._merge(np.concatenate(all_tags),
30
+ np.concatenate(all_coords),
31
+ np.concatenate(all_tris))
32
+
33
+ @staticmethod
34
+ def _find_subgroups(g, dom: Domain) -> list:
35
+ return [g[k] for k in g.keys()
36
+ if isinstance(g[k], h5py.Group)
37
+ and any(k.startswith(p) for p in dom.subgroup_prefixes)]
38
+
39
+ @staticmethod
40
+ def _extract_subgroup(sg) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
41
+ tags = sg["tags"][:]
42
+ coord_key = "coords" if "coords" in sg else "nodes"
43
+ coords = sg[coord_key][:, :2]
44
+ tris = sg["tris"][:, :3].astype(np.int64)
45
+ tris_tags = tris if tris.max() >= len(tags) else tags[tris]
46
+ return tags, coords, tris_tags
47
+
48
+ @staticmethod
49
+ def _merge(all_tags, all_coords, all_tris_tags) -> Mesh:
50
+ unique_tags, first_occ = np.unique(all_tags, return_index=True)
51
+ unique_coords = all_coords[first_occ]
52
+ tag2row = {int(t): i for i, t in enumerate(unique_tags)}
53
+ tris_idx = np.vectorize(tag2row.get)(all_tris_tags)
54
+ ok = ((tris_idx[:, 0] != tris_idx[:, 1])
55
+ & (tris_idx[:, 1] != tris_idx[:, 2])
56
+ & (tris_idx[:, 0] != tris_idx[:, 2]))
57
+ return Mesh(unique_tags, unique_coords, tris_idx[ok])
58
+