loudpy 0.0.5__py3-none-any.whl
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.
- loudpy/.DS_Store +0 -0
- loudpy/Files_Loader/Analysis_Helpers.py +185 -0
- loudpy/Files_Loader/Core_Models.py +114 -0
- loudpy/Files_Loader/Field_Operations_Helpers.py +70 -0
- loudpy/Files_Loader/Load_Domains.py +24 -0
- loudpy/Files_Loader/Mesh_Loader.py +58 -0
- loudpy/Files_Loader/Readers.py +416 -0
- loudpy/Files_Loader/__init__.py +16 -0
- loudpy/Files_Saver/Mesh_topology.py +53 -0
- loudpy/Files_Saver/Result_Store.py +293 -0
- loudpy/Files_Saver/Saving_Helpers.py +51 -0
- loudpy/Files_Saver/__init__.py +2 -0
- loudpy/Finite_Elements_formulations/.DS_Store +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_FSI.py +54 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_H.py +59 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_H_pml.py +95 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_K.py +64 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_M.py +57 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_NL.py +163 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_Q.py +58 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_Q_pml.py +94 -0
- loudpy/Finite_Elements_formulations/FE_functions/TR6_axi_unit_force.py +43 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_K.compute_K_T6-26.py313.nbi +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/FE_functions/__pycache__/TR6_axi_M.compute_M_T6-30.py313.nbi +0 -0
- loudpy/Finite_Elements_formulations/FE_kernels.py +70 -0
- loudpy/Finite_Elements_formulations/Gauss_Quadratures.py +35 -0
- loudpy/Finite_Elements_formulations/__init__.py +24 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_L3-37.py313.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.fct_form_T6-4.py313.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.jacobian_T6-26.py313.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313 2.nbi +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.1 2.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.1.nbc +0 -0
- loudpy/Finite_Elements_formulations/__pycache__/FE_kernels.material_matrix_D-58.py313.nbi +0 -0
- loudpy/Interfaces/InterfaceAcouMeca.py +12 -0
- loudpy/Interfaces/InterfaceConstrainted.py +12 -0
- loudpy/Interfaces/InterfaceForced.py +12 -0
- loudpy/Interfaces/__init__.py +7 -0
- loudpy/Materials_Bank/materials.json +87 -0
- loudpy/Meshing/Cleaning_step_file.py +38 -0
- loudpy/Meshing/MeshManager.py +511 -0
- loudpy/Meshing/__init__.py +9 -0
- loudpy/ObjectsGeo.py +31 -0
- loudpy/PhysicalEntitiesManager.py +56 -0
- loudpy/Plotter/Plotting_SPL_directivity.py +79 -0
- loudpy/Plotter/Plotting_animate_time.py +229 -0
- loudpy/Plotter/Plotting_fsi_fields.py +36 -0
- loudpy/Plotter/Plotting_helpers.py +154 -0
- loudpy/Plotter/Studies_Plots.py +384 -0
- loudpy/Plotter/__init__.py +5 -0
- loudpy/SimObjects.py +90 -0
- loudpy/Studies/.DS_Store +0 -0
- loudpy/Studies/DofMapMixin.py +35 -0
- loudpy/Studies/Eigen_study.py +547 -0
- loudpy/Studies/Freq_study.py +186 -0
- loudpy/Studies/Study_class.py +92 -0
- loudpy/Studies/Time_study.py +301 -0
- loudpy/Studies/__init__.py +10 -0
- loudpy/SubDomains/SubDomainAcou.py +22 -0
- loudpy/SubDomains/SubDomainMeca.py +28 -0
- loudpy/SubDomains/__init__.py +12 -0
- loudpy/__init__.py +23 -0
- loudpy/assembly/.DS_Store +0 -0
- loudpy/assembly/__init__.py +14 -0
- loudpy/assembly/assemblers/assembly_edge.py +201 -0
- loudpy/assembly/assemblers/assembly_tri.py +494 -0
- loudpy/assembly/core/base.py +16 -0
- loudpy/assembly/core/csr_pattern.py +23 -0
- loudpy/assembly/core/helpers.py +12 -0
- loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313 2.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.1 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.1.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._build_mapping-3.py313.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313 2.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.1 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.1.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.2 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/csr._sum_duplicates-12.py313.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313 2.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.1 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.1.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_K-5.py313.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313 2.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.1 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.1.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/elements._batch_M-10.py313.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313 2.nbi +0 -0
- loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.1 2.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.1.nbc +0 -0
- loudpy/assembly/kernels/__pycache__/indexing._fill_ij_square-3.py313.nbi +0 -0
- loudpy/assembly/kernels/csr.py +20 -0
- loudpy/assembly/kernels/elements.py +99 -0
- loudpy/assembly/kernels/fsi.py +149 -0
- loudpy/assembly/kernels/indexing.py +33 -0
- loudpy-0.0.5.dist-info/METADATA +73 -0
- loudpy-0.0.5.dist-info/RECORD +114 -0
- loudpy-0.0.5.dist-info/WHEEL +4 -0
- loudpy-0.0.5.dist-info/entry_points.txt +4 -0
loudpy/.DS_Store
ADDED
|
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
|
+
|