nahue-passano commited on
Commit
22da9a9
·
verified ·
1 Parent(s): e31a140

Upload 6 files

Browse files
Files changed (6) hide show
  1. app.py +134 -0
  2. config.py +58 -0
  3. core.py +138 -0
  4. tmatrix.py +49 -0
  5. utils.py +63 -0
  6. visualization.py +47 -0
app.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+
5
+ from loudspeaker_tmatrix.core import simulate_loudspeaker
6
+ from loudspeaker_tmatrix.visualization import plot_loudspeaker_response
7
+ from loudspeaker_tmatrix.config import (
8
+ load_config,
9
+ load_loudspeaker_config,
10
+ AcousticalConstantsConfig,
11
+ )
12
+
13
+ config_path = "configs/config.yaml"
14
+ default_loudspeaker_path = "configs/default_loudspeaker.yaml"
15
+
16
+ cfg = load_config(config_path)
17
+ loudspeaker_cfg = load_loudspeaker_config(default_loudspeaker_path)
18
+
19
+ freq_array = np.logspace(
20
+ np.log10(cfg.frequency.min), np.log10(cfg.frequency.max), num=cfg.frequency.n_bins
21
+ )
22
+
23
+ angular_freq_array = 2 * np.pi * freq_array
24
+
25
+ st.sidebar.title("Thiele Small parameters")
26
+
27
+ ### Sliders
28
+ slider_Re = st.sidebar.slider(
29
+ "Electrical Coil Resistance (Re) [Ohm]",
30
+ 0.0,
31
+ 10.0,
32
+ loudspeaker_cfg.electrical.coil_resistance,
33
+ )
34
+ slider_Le = st.sidebar.slider(
35
+ "Electrical Coil Inductance (Le) [mH]",
36
+ 0.0,
37
+ 10.0,
38
+ loudspeaker_cfg.electrical.coil_inductance * 1e3,
39
+ )
40
+ slider_Le /= 1e3
41
+ slider_Bl = st.sidebar.slider(
42
+ "Electromechanical factor (Bl) [N/A]",
43
+ 0.0,
44
+ 10.0,
45
+ loudspeaker_cfg.electromechanical_factor,
46
+ )
47
+ slider_Mm = st.sidebar.slider(
48
+ "Mechanical Mass (Mm) [mg]",
49
+ 0.0,
50
+ 50.0,
51
+ loudspeaker_cfg.mechanical.mass * 1e3,
52
+ )
53
+ slider_Mm /= 1e3
54
+ slider_Cm = st.sidebar.slider(
55
+ "Mechanical Compliance (Cm) [mm/N]",
56
+ 0.0,
57
+ 5.0,
58
+ loudspeaker_cfg.mechanical.compliance * 1e3,
59
+ )
60
+ slider_Cm /= 1e3
61
+ slider_Rm = st.sidebar.slider(
62
+ "Mechanical Resistance (Rm) [kg/s]",
63
+ 0.0,
64
+ 10.0,
65
+ loudspeaker_cfg.mechanical.resistance,
66
+ )
67
+ slider_diam = st.sidebar.slider(
68
+ "Effective diameter of radiation [cm]",
69
+ 0.0,
70
+ 50.0,
71
+ loudspeaker_cfg.acoustical.effective_diameter * 1e2,
72
+ )
73
+ slider_diam /= 1e2
74
+
75
+ default_params = st.sidebar.button("Set default parameters")
76
+
77
+ if default_params:
78
+ st.rerun(scope="app")
79
+
80
+
81
+ freq_array = np.logspace(
82
+ np.log10(cfg.frequency.min), np.log10(cfg.frequency.max), num=cfg.frequency.n_bins
83
+ )
84
+
85
+ angular_freq_array = 2 * np.pi * freq_array
86
+
87
+ thiele_small_params = {
88
+ "Re": slider_Re,
89
+ "Le": slider_Le,
90
+ "Bl": slider_Bl,
91
+ "Mm": slider_Mm,
92
+ "Cm": slider_Cm,
93
+ "Rm": slider_Rm,
94
+ "effective_diameter": slider_diam,
95
+ }
96
+
97
+ loudspeaker_responses = simulate_loudspeaker(
98
+ thiele_small_params, angular_freq_array, cfg.acoustical_constants
99
+ )
100
+
101
+
102
+ # Electrical impedance
103
+ electrical_impedance_plot = plot_loudspeaker_response(
104
+ response_array=loudspeaker_responses["electrical_impedance"],
105
+ freq_array=freq_array,
106
+ title="Electrical Impedance",
107
+ magnitude_in_db=False,
108
+ magnitude_units="Ohm",
109
+ shift_phase=False,
110
+ )
111
+
112
+
113
+ mechanical_velocity_plot = plot_loudspeaker_response(
114
+ response_array=loudspeaker_responses["mechanical_velocity"],
115
+ freq_array=freq_array,
116
+ title="Mechanical Velocity",
117
+ magnitude_in_db=False,
118
+ magnitude_units="m/s",
119
+ shift_phase=True,
120
+ )
121
+
122
+
123
+ acoustical_pressure = plot_loudspeaker_response(
124
+ response_array=loudspeaker_responses["acoustical_pressure"],
125
+ freq_array=freq_array,
126
+ title="Acoustical Pressure",
127
+ magnitude_in_db=True,
128
+ magnitude_units="dB",
129
+ shift_phase=False,
130
+ )
131
+
132
+ st.pyplot(electrical_impedance_plot)
133
+ st.pyplot(mechanical_velocity_plot)
134
+ st.pyplot(acoustical_pressure)
config.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ import yaml
3
+
4
+
5
+ class FrequencyConfig(BaseModel):
6
+ min: int
7
+ max: int
8
+ n_bins: int
9
+
10
+
11
+ class AcousticalConstantsConfig(BaseModel):
12
+ sound_speed: float
13
+ air_density: float
14
+ atmospheric_pressure: int
15
+ reference_pressure: float
16
+ measurement_distance: float
17
+ directivity_factor: int
18
+
19
+
20
+ class Config(BaseModel):
21
+ default_loudspeaker_cfg: str
22
+ frequency: FrequencyConfig
23
+ acoustical_constants: AcousticalConstantsConfig
24
+
25
+
26
+ def load_config(yaml_path: str) -> Config:
27
+ with open(yaml_path, "r") as file:
28
+ data = yaml.safe_load(file)
29
+ return Config(**data)
30
+
31
+
32
+ class ElectricalConfig(BaseModel):
33
+ input_voltage: float
34
+ coil_resistance: float
35
+ coil_inductance: float
36
+
37
+
38
+ class MechanicalConfig(BaseModel):
39
+ mass: float
40
+ compliance: float
41
+ resistance: float
42
+
43
+
44
+ class AcousticalConfig(BaseModel):
45
+ effective_diameter: float
46
+
47
+
48
+ class LoudspeakerConfig(BaseModel):
49
+ electrical: ElectricalConfig
50
+ electromechanical_factor: float
51
+ mechanical: MechanicalConfig
52
+ acoustical: AcousticalConfig
53
+
54
+
55
+ def load_loudspeaker_config(yaml_path: str) -> LoudspeakerConfig:
56
+ with open(yaml_path, "r") as file:
57
+ data = yaml.safe_load(file)
58
+ return LoudspeakerConfig(**data)
core.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from loudspeaker_tmatrix import tmatrix
4
+ from loudspeaker_tmatrix.utils import layer_wise_dot_product, struve, bessel
5
+ from loudspeaker_tmatrix.config import AcousticalConstantsConfig
6
+
7
+
8
+ def simulate_loudspeaker(
9
+ thiele_small_params: dict,
10
+ angular_freq_array: np.ndarray,
11
+ acoustical_constants: AcousticalConstantsConfig,
12
+ ):
13
+
14
+ n_bins = len(angular_freq_array)
15
+ Re_tmatrix = tmatrix.resistance_series(thiele_small_params["Re"], n_bins)
16
+ Z_Le_tmatrix = tmatrix.inductance_series(
17
+ thiele_small_params["Le"], angular_freq_array
18
+ )
19
+ Bl_tmatrix = tmatrix.gyrator(thiele_small_params["Bl"], n_bins)
20
+ Z_Mm_tmatrix = tmatrix.inductance_series(
21
+ thiele_small_params["Mm"], angular_freq_array
22
+ )
23
+ Z_Cm_tmatrix = tmatrix.capacitance_series(
24
+ thiele_small_params["Cm"], angular_freq_array
25
+ )
26
+ Rm_tmatrix = tmatrix.resistance_series(thiele_small_params["Rm"], n_bins)
27
+
28
+ electro_mechanical_tmatrix = layer_wise_dot_product(
29
+ Re_tmatrix,
30
+ Z_Le_tmatrix,
31
+ Bl_tmatrix,
32
+ Z_Mm_tmatrix,
33
+ Z_Cm_tmatrix,
34
+ Rm_tmatrix,
35
+ )
36
+
37
+ t_11 = electro_mechanical_tmatrix[0, 0]
38
+ t_12 = electro_mechanical_tmatrix[0, 1]
39
+ t_21 = electro_mechanical_tmatrix[1, 0]
40
+ t_22 = electro_mechanical_tmatrix[1, 1]
41
+
42
+ # Electrical response
43
+ electrical_impedance_shorted_output = t_12 / t_22
44
+
45
+ # Mechanical response
46
+ electrical_input_voltage = np.ones(n_bins)
47
+ electrical_input_current = (
48
+ electrical_input_voltage / electrical_impedance_shorted_output
49
+ )
50
+
51
+ electro_mechanical_tmatrix_det = np.abs(t_11 * t_22 - t_12 * t_21)
52
+
53
+ electro_mechanical_tmatrix_inv = np.array(
54
+ [
55
+ [
56
+ t_22 / electro_mechanical_tmatrix_det,
57
+ -t_12 / electro_mechanical_tmatrix_det,
58
+ ],
59
+ [
60
+ -t_21 / electro_mechanical_tmatrix_det,
61
+ t_11 / electro_mechanical_tmatrix_det,
62
+ ],
63
+ ]
64
+ )
65
+
66
+ mechanical_force, mechanical_velocity = layer_wise_dot_product(
67
+ electro_mechanical_tmatrix_inv,
68
+ np.array(
69
+ [
70
+ [electrical_input_voltage, electrical_input_voltage],
71
+ [electrical_input_current, electrical_input_current],
72
+ ]
73
+ ),
74
+ )[:, 0]
75
+
76
+ # Acoustical response
77
+ air_impedance = acoustical_constants.air_density * acoustical_constants.sound_speed
78
+ wave_number_array = angular_freq_array / acoustical_constants.sound_speed
79
+
80
+ effective_radiation_radius = thiele_small_params["effective_diameter"] / 2
81
+
82
+ ka_array = wave_number_array * effective_radiation_radius
83
+ Sd_value = np.pi * effective_radiation_radius**2
84
+ Sd_tmatrix = tmatrix.transformer(Sd_value, n_bins)
85
+
86
+ ### Mechanical impedance of radiation
87
+ ZM_rad_real_array = Sd_value * air_impedance * (1 - bessel(2 * ka_array) / ka_array)
88
+ ZM_rad_imag_array = (
89
+ Sd_value * air_impedance * (1j * (struve(2 * ka_array) / ka_array))
90
+ )
91
+ ZM_rad_array = ZM_rad_real_array + ZM_rad_imag_array
92
+ ZM_rad_tmatrix = np.array(
93
+ [[np.ones(n_bins), ZM_rad_array], [np.zeros(n_bins), np.ones(n_bins)]]
94
+ )
95
+
96
+ # Specific acoustic impedance
97
+ ZA_rad = (
98
+ 1j
99
+ * angular_freq_array
100
+ * acoustical_constants.air_density
101
+ * acoustical_constants.directivity_factor
102
+ ) / (
103
+ 4
104
+ * np.pi
105
+ * acoustical_constants.measurement_distance
106
+ * np.exp(1j * wave_number_array * acoustical_constants.measurement_distance)
107
+ )
108
+ Z_delay = 1j * np.exp(
109
+ -1j * wave_number_array * acoustical_constants.measurement_distance
110
+ ) # Phase rotation due air propagation time
111
+
112
+ electro_mechanical_acoustic_tmatrix = layer_wise_dot_product(
113
+ electro_mechanical_tmatrix, ZM_rad_tmatrix, Sd_tmatrix
114
+ )
115
+
116
+ electrical_input_voltage = 2.83
117
+
118
+ t_11 = electro_mechanical_acoustic_tmatrix[0, 0]
119
+ t_12 = electro_mechanical_acoustic_tmatrix[0, 1]
120
+ t_21 = electro_mechanical_acoustic_tmatrix[1, 0]
121
+ t_22 = electro_mechanical_acoustic_tmatrix[1, 1]
122
+
123
+ voltage_pressure_transfer_function = (ZA_rad) / (t_11 * ZA_rad + t_12)
124
+
125
+ acoustical_pressure = (
126
+ electrical_input_voltage
127
+ * (voltage_pressure_transfer_function / Z_delay)
128
+ / acoustical_constants.reference_pressure
129
+ )
130
+
131
+ loudspeaker_responses = {
132
+ "electrical_impedance": electrical_impedance_shorted_output,
133
+ "mechanical_force": mechanical_force,
134
+ "mechanical_velocity": mechanical_velocity,
135
+ "acoustical_pressure": acoustical_pressure,
136
+ }
137
+
138
+ return loudspeaker_responses
tmatrix.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ def series_impedance(impedance_array):
5
+ n_bins = len(impedance_array)
6
+ tmatrix = np.array(
7
+ [
8
+ [np.ones(n_bins), impedance_array],
9
+ [np.zeros(n_bins), np.ones(n_bins)],
10
+ ]
11
+ )
12
+ return tmatrix
13
+
14
+
15
+ def inductance_series(inductance_value, angular_freq_array):
16
+ impedance_array = 1j * angular_freq_array * inductance_value
17
+ return series_impedance(impedance_array)
18
+
19
+
20
+ def capacitance_series(capacitance_value, angular_freq_array):
21
+ impedance_array = 1 / (1j * angular_freq_array * capacitance_value)
22
+ return series_impedance(impedance_array)
23
+
24
+
25
+ def resistance_series(resistance_value, n_bins):
26
+ impedance_array = np.ones(n_bins) * resistance_value
27
+ return series_impedance(impedance_array)
28
+
29
+
30
+ def transformer(transformer_value, n_bins):
31
+ transformer_array = np.ones(n_bins) * transformer_value
32
+ transformer = np.array(
33
+ [
34
+ [transformer_array, np.zeros(n_bins)],
35
+ [np.zeros(n_bins), 1 / transformer_array],
36
+ ]
37
+ )
38
+ return transformer
39
+
40
+
41
+ def gyrator(gyrator_value, n_bins):
42
+ gyrator_array = np.ones(n_bins) * gyrator_value
43
+ gyrator = np.array(
44
+ [
45
+ [np.zeros(n_bins), gyrator_array],
46
+ [1 / gyrator_array, np.zeros(n_bins)],
47
+ ]
48
+ )
49
+ return gyrator
utils.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ def layer_wise_dot_product(*matrices: np.ndarray) -> np.ndarray:
5
+ """
6
+ Performs the sequential layer-wise dot product of multiple 3D matrices along the last dimension.
7
+
8
+ Args:
9
+ *matrices: A variable number of 3D numpy arrays.
10
+
11
+ Returns:
12
+ A 3D numpy array containing the sequential dot product results for each layer in the last dimension.
13
+ """
14
+ if not all(matrix.shape == matrices[0].shape for matrix in matrices):
15
+ raise ValueError("All matrices must have the same dimensions.")
16
+
17
+ result = np.zeros_like(matrices[0], dtype="complex")
18
+
19
+ _, _, num_layers = matrices[0].shape
20
+
21
+ for layer_i in range(num_layers):
22
+ # Start the product with the identity matrix for the first multiplication
23
+ dot_product = np.eye(result.shape[0])
24
+ for matrix in matrices:
25
+ dot_product = np.dot(dot_product, matrix[:, :, layer_i])
26
+
27
+ result[:, :, layer_i] = dot_product
28
+
29
+ return result
30
+
31
+
32
+ def bessel(z):
33
+ """
34
+ Bessel function aproximation for air radiation impedance
35
+ """
36
+ bessel_sum = 0
37
+ for k in range(25):
38
+ bessel_i = ((-1) ** k * (z / 2) ** (2 * k + 1)) / (
39
+ np.math.factorial(k) * np.math.factorial(k + 1)
40
+ )
41
+ bessel_sum = bessel_sum + bessel_i
42
+ return bessel_sum
43
+
44
+
45
+ def struve(z):
46
+ """
47
+ Srtuve function aproximation for air radiation impedance
48
+ """
49
+ struve_sum = 0
50
+ for k in range(25):
51
+ struve_i = (((-1) ** k * (z / 2) ** (2 * k + 2))) / (
52
+ np.math.factorial(int(k + 1 / 2)) * np.math.factorial(int(k + 3 / 2))
53
+ )
54
+ struve_sum = struve_sum + struve_i
55
+ return struve_sum
56
+
57
+
58
+ def to_db(x):
59
+ """
60
+ decibel calculus
61
+ """
62
+ db = 20 * np.log10(np.abs(x))
63
+ return db
visualization.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from matplotlib import pyplot as plt
3
+
4
+ from loudspeaker_tmatrix.utils import to_db
5
+
6
+
7
+ def plot_loudspeaker_response(
8
+ response_array: np.ndarray,
9
+ freq_array: np.ndarray,
10
+ title: str,
11
+ magnitude_in_db: bool,
12
+ magnitude_units: str,
13
+ shift_phase: bool,
14
+ ):
15
+
16
+ if magnitude_in_db:
17
+ magnitude = to_db(response_array)
18
+ else:
19
+ magnitude = np.abs(response_array)
20
+
21
+ if shift_phase:
22
+ phase = np.angle(-response_array, deg=True)
23
+ else:
24
+ phase = np.angle(response_array, deg=True)
25
+
26
+ fig, ax1 = plt.subplots(figsize=(12, 4))
27
+ fig.suptitle(title)
28
+
29
+ ax1.semilogx(freq_array, magnitude, label="Magnitude", color="C0")
30
+ ax1.set_xlabel("Frequency [Hz]")
31
+ ax1.set_ylabel(f"Magnitude [{magnitude_units}]", color="C0")
32
+ ax1.tick_params(axis="y", labelcolor="C0")
33
+
34
+ ax2 = ax1.twinx()
35
+ ax2.semilogx(freq_array, phase, color="r", label="Phase")
36
+ x_ticks = np.sort(np.array([16, 31, 63, 125, 250, 500, 1000, 2000, 4000]))
37
+ ax2.set_xticks(ticks=x_ticks, labels=x_ticks.tolist(), rotation=45)
38
+ ax2.set_ylabel("Phase [º]", color="r")
39
+ ax2.tick_params(axis="y", labelcolor="r")
40
+ y_label1 = [r"$-180º$", r"$-90º$", r"$0º$", r"$90º$", r"$180º$"]
41
+ ax2.set_yticks(np.array([-180, -90, 0, 90, 180]), y_label1)
42
+
43
+ ax1.set_xlim(10, 4000)
44
+ ax2.set_ylim(-180, 180)
45
+ ax1.grid(axis="x")
46
+ ax2.grid(axis="y")
47
+ return fig