diff --git a/Docs/Aeroacoustics.md b/Docs/Aeroacoustics.md
new file mode 100644
index 00000000000..a10364dce6e
--- /dev/null
+++ b/Docs/Aeroacoustics.md
@@ -0,0 +1,87 @@
+# Aeroacoustic analysis of unsteady pressure
+
+`aeroacoustics.py` converts pressure histories from an unsteady SU2
+simulation into frequency-domain acoustic quantities. It is intended for
+pressure probes from LES, DES, DDES, ZDES, or EDDES cases, but it can analyze
+any uniformly sampled pressure history.
+
+The tool estimates the one-sided power spectral density (PSD) with Welch's
+method and reports:
+
+- pressure PSD in Pa²/Hz;
+- PSD level in dB/Hz;
+- bin-integrated narrowband sound pressure level (SPL) in dB;
+- overall sound pressure level (OASPL) in dB; and
+- the dominant non-zero frequency.
+
+Only NumPy is required.
+
+## Recording pressure probes
+
+SU2 custom outputs can record pressure at one or more points. For example:
+
+```ini
+CUSTOM_OUTPUTS= 'mic1 : Probe{PRESSURE}[1.0, 0.0, 0.0]; \
+ mic2 : Probe{PRESSURE}[0.0, 1.0, 0.0]'
+HISTORY_OUTPUT= TIME_ITER, CUR_TIME, mic1, mic2
+```
+
+Use a fixed physical time step and write every desired acoustic sample. The
+history column containing physical time is commonly named `Cur_Time`; inspect
+the CSV header and pass its exact name with `--time`.
+
+## Usage
+
+Analyze two pressure columns that are already dimensional:
+
+```bash
+python SU2_PY/aeroacoustics.py history.csv \
+ --pressure mic1 \
+ --pressure mic2 \
+ --skip-samples 10000 \
+ --segment-length 2048 \
+ --summary acoustic_summary.json \
+ --output acoustic_spectrum.csv
+```
+
+If the file contains an iteration rather than physical time, provide the
+sampling rate explicitly:
+
+```bash
+python SU2_PY/aeroacoustics.py history.csv \
+ --sample-rate 50000 \
+ --pressure mic1
+```
+
+Pressure must be dimensional in pascals for physical SPL. If an SU2 case uses
+nondimensional pressure, apply its dimensionalization factor in pascals per
+input unit:
+
+```bash
+python SU2_PY/aeroacoustics.py history.csv \
+ --time Cur_Time \
+ --pressure mic1 \
+ --pressure-scale 101325
+```
+
+The default acoustic reference is 20 µPa, appropriate for airborne sound. Use
+`--reference-pressure` for another medium or convention.
+
+## Sampling guidance
+
+- Resolve the highest frequency of interest with adequate samples per period;
+ the absolute Nyquist limit is half the sample rate.
+- Start collecting samples after initial transients have left the domain, or
+ remove them with `--skip-samples`.
+- Increase `--segment-length` for finer frequency resolution. Frequency-bin
+ width is the sample rate divided by segment length.
+- Longer signals give more Welch segments and a smoother PSD estimate.
+- The default Hann window, 50% overlap, and mean removal are suitable general
+ choices. `--detrend linear` can remove slow numerical drift.
+- Avoid interpreting probe pressure as a far-field acoustic result when the
+ probe is inside a vortical or hydrodynamic near field. Permeable-surface
+ Ffowcs Williams–Hawkings propagation is a larger, separate capability.
+
+The spectrum CSV contains one frequency column followed by PSD, PSD-level, and
+narrowband-SPL columns for every requested pressure signal. The optional JSON
+summary contains scalar metrics suitable for automated comparisons.
diff --git a/SU2_PY/aeroacoustics.py b/SU2_PY/aeroacoustics.py
new file mode 100755
index 00000000000..e7b8be01885
--- /dev/null
+++ b/SU2_PY/aeroacoustics.py
@@ -0,0 +1,420 @@
+#!/usr/bin/env python3
+
+## \file aeroacoustics.py
+# \brief Spectral analysis of unsteady pressure histories.
+# \version 8.5.0 "Harrier"
+#
+# SU2 Project Website: https://su2code.github.io
+#
+# The SU2 Project is maintained by the SU2 Foundation
+# (http://su2foundation.org)
+#
+# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md)
+#
+# SU2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# SU2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SU2. If not, see .
+
+"""Convert SU2 pressure histories into acoustic spectra and SPL metrics."""
+
+import argparse
+import csv
+import json
+import math
+import re
+from pathlib import Path
+
+import numpy as np
+
+
+WINDOWS = {
+ "hann": np.hanning,
+ "hamming": np.hamming,
+ "blackman": np.blackman,
+ "rectangular": np.ones,
+}
+
+
+def read_history(filename):
+ """Read a numeric SU2 CSV history file and return its columns."""
+ with open(filename, newline="", encoding="utf-8-sig") as history_file:
+ rows = (
+ row
+ for row in history_file
+ if row.strip() and not row.lstrip().startswith(("%", "#"))
+ )
+ reader = csv.reader(rows)
+ try:
+ headers = [field.strip().strip('"') for field in next(reader)]
+ except StopIteration as error:
+ raise ValueError("history file is empty") from error
+
+ if not headers or any(not field for field in headers):
+ raise ValueError("history file contains an empty column name")
+ if len(set(headers)) != len(headers):
+ raise ValueError("history file contains duplicate column names")
+
+ columns = {header: [] for header in headers}
+ for line_number, row in enumerate(reader, start=2):
+ if len(row) != len(headers):
+ raise ValueError(
+ "line {} has {} values; expected {}".format(
+ line_number, len(row), len(headers)
+ )
+ )
+ try:
+ for header, value in zip(headers, row):
+ columns[header].append(float(value))
+ except ValueError as error:
+ raise ValueError(
+ "line {} contains a non-numeric value".format(line_number)
+ ) from error
+
+ if not next(iter(columns.values())):
+ raise ValueError("history file has a header but no data")
+ return {name: np.asarray(values, dtype=float) for name, values in columns.items()}
+
+
+def resolve_column(columns, requested):
+ """Resolve a column exactly, or uniquely without case sensitivity."""
+ if requested in columns:
+ return requested
+ matches = [name for name in columns if name.casefold() == requested.casefold()]
+ if len(matches) == 1:
+ return matches[0]
+ raise ValueError(
+ "column {!r} was not found; available columns: {}".format(
+ requested, ", ".join(columns)
+ )
+ )
+
+
+def sampling_rate(time, relative_tolerance=1.0e-5):
+ """Return the sample rate after verifying monotonically uniform time data."""
+ if time.size < 2:
+ raise ValueError("at least two time samples are required")
+ if not np.all(np.isfinite(time)):
+ raise ValueError("time values must be finite")
+
+ intervals = np.diff(time)
+ if np.any(intervals <= 0.0):
+ raise ValueError("time values must be strictly increasing")
+ mean_interval = float(np.mean(intervals))
+ maximum_error = float(np.max(np.abs(intervals - mean_interval)))
+ if maximum_error > relative_tolerance * mean_interval:
+ message = (
+ "time samples are not uniform " "(maximum relative interval error {:.3g})"
+ )
+ raise ValueError(message.format(maximum_error / mean_interval))
+ return 1.0 / mean_interval
+
+
+def _detrend(signal, mode):
+ if mode == "none":
+ return signal.copy()
+ if mode == "mean":
+ return signal - np.mean(signal)
+ positions = np.arange(signal.size, dtype=float)
+ slope, intercept = np.polyfit(positions, signal, 1)
+ return signal - (slope * positions + intercept)
+
+
+def welch_psd(
+ pressure,
+ sample_rate,
+ segment_length=None,
+ overlap=0.5,
+ window="hann",
+ detrend="mean",
+):
+ """Estimate a one-sided pressure PSD using Welch's averaged periodogram."""
+ pressure = np.asarray(pressure, dtype=float)
+ if pressure.ndim != 1:
+ raise ValueError("pressure data must be one-dimensional")
+ if pressure.size < 2:
+ raise ValueError("at least two pressure samples are required")
+ if not np.all(np.isfinite(pressure)):
+ raise ValueError("pressure values must be finite")
+ if not math.isfinite(sample_rate) or sample_rate <= 0.0:
+ raise ValueError("sample rate must be positive and finite")
+ if not 0.0 <= overlap < 1.0:
+ raise ValueError("overlap must be in the range [0, 1)")
+ if window not in WINDOWS:
+ raise ValueError("unsupported window {!r}".format(window))
+ if detrend not in ("none", "mean", "linear"):
+ raise ValueError("unsupported detrending mode {!r}".format(detrend))
+
+ if segment_length is None:
+ segment_length = min(1024, pressure.size)
+ if segment_length < 2 or segment_length > pressure.size:
+ raise ValueError("segment length must be between 2 and the number of samples")
+
+ step = max(1, int(round(segment_length * (1.0 - overlap))))
+ starts = range(0, pressure.size - segment_length + 1, step)
+ weights = WINDOWS[window](segment_length)
+ window_energy = float(np.sum(weights * weights))
+ if window_energy <= 0.0:
+ raise ValueError(
+ "segment length {} is too short for the {} window".format(
+ segment_length, window
+ )
+ )
+ spectrum = np.zeros(segment_length // 2 + 1, dtype=float)
+ segment_count = 0
+
+ for start in starts:
+ segment = _detrend(pressure[start : start + segment_length], detrend)
+ transform = np.fft.rfft(segment * weights)
+ periodogram = np.abs(transform) ** 2 / (sample_rate * window_energy)
+ if segment_length % 2 == 0:
+ periodogram[1:-1] *= 2.0
+ else:
+ periodogram[1:] *= 2.0
+ spectrum += periodogram
+ segment_count += 1
+
+ spectrum /= segment_count
+ frequencies = np.fft.rfftfreq(segment_length, d=1.0 / sample_rate)
+ return frequencies, spectrum, segment_count
+
+
+def acoustic_metrics(frequencies, psd, reference_pressure=20.0e-6):
+ """Calculate PSD level, bin SPL, OASPL, and the dominant frequency."""
+ if reference_pressure <= 0.0 or not math.isfinite(reference_pressure):
+ raise ValueError("reference pressure must be positive and finite")
+ if frequencies.size < 2:
+ raise ValueError("an acoustic spectrum needs at least two frequency bins")
+
+ frequency_step = float(frequencies[1] - frequencies[0])
+ tiny = np.finfo(float).tiny
+ psd_level = 10.0 * np.log10(np.maximum(psd, tiny) / reference_pressure**2)
+ narrowband_spl = 10.0 * np.log10(
+ np.maximum(psd * frequency_step, tiny) / reference_pressure**2
+ )
+ oaspl = 10.0 * math.log10(
+ max(float(np.sum(psd) * frequency_step), tiny) / reference_pressure**2
+ )
+ dominant_index = 1 + int(np.argmax(psd[1:]))
+ return {
+ "psd_level_db_per_hz": psd_level,
+ "narrowband_spl_db": narrowband_spl,
+ "oaspl_db": oaspl,
+ "dominant_frequency_hz": float(frequencies[dominant_index]),
+ }
+
+
+def _safe_column_name(name):
+ normalized = re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("_")
+ return normalized or "pressure"
+
+
+def write_spectrum(filename, frequencies, spectra):
+ """Write frequency-domain results for one or more pressure signals."""
+ fields = ["Frequency_Hz"]
+ used_prefixes = set()
+ for name in spectra:
+ base = _safe_column_name(name)
+ prefix = base
+ suffix = 2
+ while prefix in used_prefixes:
+ prefix = "{}_{}".format(base, suffix)
+ suffix += 1
+ used_prefixes.add(prefix)
+ fields.extend(
+ [
+ prefix + "_PSD_Pa2_per_Hz",
+ prefix + "_PSD_level_dB_per_Hz",
+ prefix + "_SPL_dB",
+ ]
+ )
+
+ with open(filename, "w", newline="", encoding="utf-8") as spectrum_file:
+ writer = csv.writer(spectrum_file)
+ writer.writerow(fields)
+ for index, frequency in enumerate(frequencies):
+ row = [frequency]
+ for result in spectra.values():
+ row.extend(
+ [
+ result["psd"][index],
+ result["psd_level_db_per_hz"][index],
+ result["narrowband_spl_db"][index],
+ ]
+ )
+ writer.writerow(row)
+
+
+def analyze(args):
+ """Run the command-line analysis and return its summary."""
+ columns = read_history(args.input)
+ pressure_names = [resolve_column(columns, name) for name in args.pressure]
+ if len(set(pressure_names)) != len(pressure_names):
+ raise ValueError("each pressure column may only be requested once")
+ if not math.isfinite(args.pressure_scale) or args.pressure_scale <= 0.0:
+ raise ValueError("pressure scale must be positive and finite")
+ if args.skip_samples < 0:
+ raise ValueError("skip samples must not be negative")
+ sample_count = len(next(iter(columns.values())))
+ if args.skip_samples > sample_count - 2:
+ raise ValueError("skip samples must leave at least two samples to analyze")
+
+ if args.sample_rate is not None:
+ sample_rate_hz = args.sample_rate
+ else:
+ time_name = resolve_column(columns, args.time)
+ sample_rate_hz = sampling_rate(columns[time_name][args.skip_samples :])
+
+ spectra = {}
+ summary = {
+ "input": str(args.input),
+ "sample_rate_hz": sample_rate_hz,
+ "reference_pressure_pa": args.reference_pressure,
+ "samples": sample_count - args.skip_samples,
+ "signals": {},
+ }
+ frequencies = None
+ for name in pressure_names:
+ dimensional_pressure = columns[name][args.skip_samples :] * args.pressure_scale
+ frequencies, psd, segment_count = welch_psd(
+ dimensional_pressure,
+ sample_rate_hz,
+ segment_length=args.segment_length,
+ overlap=args.overlap,
+ window=args.window,
+ detrend=args.detrend,
+ )
+ metrics = acoustic_metrics(frequencies, psd, args.reference_pressure)
+ spectra[name] = {"psd": psd, **metrics}
+ summary["signals"][name] = {
+ "oaspl_db": metrics["oaspl_db"],
+ "dominant_frequency_hz": metrics["dominant_frequency_hz"],
+ "segments": segment_count,
+ }
+
+ summary["frequency_resolution_hz"] = float(frequencies[1] - frequencies[0])
+ summary["nyquist_frequency_hz"] = float(frequencies[-1])
+ write_spectrum(args.output, frequencies, spectra)
+ if args.summary:
+ with open(args.summary, "w", encoding="utf-8") as summary_file:
+ json.dump(summary, summary_file, indent=2)
+ summary_file.write("\n")
+ return summary
+
+
+def build_parser():
+ parser = argparse.ArgumentParser(
+ description=(
+ "Estimate acoustic spectra from pressure columns in an SU2 history CSV."
+ )
+ )
+ parser.add_argument("input", type=Path, help="SU2 history CSV file")
+ parser.add_argument(
+ "-p",
+ "--pressure",
+ action="append",
+ required=True,
+ metavar="COLUMN",
+ help="pressure column to analyze; repeat for multiple probes",
+ )
+ sampling = parser.add_mutually_exclusive_group()
+ sampling.add_argument(
+ "-t",
+ "--time",
+ default="Cur_Time",
+ metavar="COLUMN",
+ help="physical-time column (default: Cur_Time)",
+ )
+ sampling.add_argument(
+ "--sample-rate",
+ type=float,
+ metavar="HZ",
+ help="sampling rate when the file has no physical-time column",
+ )
+ parser.add_argument(
+ "--pressure-scale",
+ type=float,
+ default=1.0,
+ metavar="PA_PER_UNIT",
+ help="multiply input pressures by this dimensionalization factor",
+ )
+ parser.add_argument(
+ "--reference-pressure",
+ type=float,
+ default=20.0e-6,
+ metavar="PA",
+ help="SPL reference pressure (default: 20e-6 Pa)",
+ )
+ parser.add_argument(
+ "-n",
+ "--segment-length",
+ type=int,
+ metavar="SAMPLES",
+ help="Welch segment length (default: min(1024, number of samples))",
+ )
+ parser.add_argument(
+ "--overlap",
+ type=float,
+ default=0.5,
+ metavar="FRACTION",
+ help="Welch segment overlap as a fraction (default: 0.5)",
+ )
+ parser.add_argument(
+ "--window",
+ choices=tuple(WINDOWS),
+ default="hann",
+ help="spectral window (default: hann)",
+ )
+ parser.add_argument(
+ "--detrend",
+ choices=("none", "mean", "linear"),
+ default="mean",
+ help="detrending applied to each segment (default: mean)",
+ )
+ parser.add_argument(
+ "--skip-samples",
+ type=int,
+ default=0,
+ metavar="COUNT",
+ help="discard initial transient samples before analysis (default: 0)",
+ )
+ parser.add_argument(
+ "-o",
+ "--output",
+ type=Path,
+ default=Path("acoustic_spectrum.csv"),
+ help="output spectrum CSV (default: acoustic_spectrum.csv)",
+ )
+ parser.add_argument(
+ "--summary", type=Path, help="optional JSON file for scalar acoustic metrics"
+ )
+ return parser
+
+
+def main():
+ args = build_parser().parse_args()
+ try:
+ summary = analyze(args)
+ except (OSError, ValueError) as error:
+ raise SystemExit("aeroacoustics: error: {}".format(error)) from error
+
+ print("Sample rate: {:.8g} Hz".format(summary["sample_rate_hz"]))
+ for name, result in summary["signals"].items():
+ print(
+ "{}: OASPL = {:.3f} dB, dominant frequency = {:.8g} Hz".format(
+ name, result["oaspl_db"], result["dominant_frequency_hz"]
+ )
+ )
+ print("Spectrum written to {}".format(args.output))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/SU2_PY/meson.build b/SU2_PY/meson.build
index ced81290346..469d0c16486 100644
--- a/SU2_PY/meson.build
+++ b/SU2_PY/meson.build
@@ -1,4 +1,5 @@
install_data(['continuous_adjoint.py',
+ 'aeroacoustics.py',
'compute_uncertainty.py',
'finite_differences.py',
'mesh_deformation.py',
diff --git a/SU2_PY/tests/test_aeroacoustics.py b/SU2_PY/tests/test_aeroacoustics.py
new file mode 100644
index 00000000000..f05c713d221
--- /dev/null
+++ b/SU2_PY/tests/test_aeroacoustics.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+import csv
+import importlib.util
+import math
+import tempfile
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+
+import numpy as np
+
+
+MODULE_PATH = Path(__file__).parents[1] / "aeroacoustics.py"
+SPEC = importlib.util.spec_from_file_location("aeroacoustics", MODULE_PATH)
+AEROACOUSTICS = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(AEROACOUSTICS)
+
+
+class AeroacousticsTests(unittest.TestCase):
+ def test_welch_finds_sine_frequency_and_level(self):
+ sample_rate = 4096.0
+ frequency = 256.0
+ rms_pressure = 1.0
+ time = np.arange(4096) / sample_rate
+ pressure = (
+ math.sqrt(2.0) * rms_pressure * np.sin(2.0 * np.pi * frequency * time)
+ )
+
+ frequencies, psd, segments = AEROACOUSTICS.welch_psd(
+ pressure,
+ sample_rate,
+ segment_length=1024,
+ overlap=0.5,
+ window="hann",
+ )
+ metrics = AEROACOUSTICS.acoustic_metrics(frequencies, psd)
+
+ self.assertEqual(segments, 7)
+ self.assertEqual(metrics["dominant_frequency_hz"], frequency)
+ expected_oaspl = 20.0 * math.log10(rms_pressure / 20.0e-6)
+ self.assertAlmostEqual(metrics["oaspl_db"], expected_oaspl, places=3)
+
+ def test_sampling_rate_rejects_nonuniform_time(self):
+ with self.assertRaisesRegex(ValueError, "not uniform"):
+ AEROACOUSTICS.sampling_rate(np.array([0.0, 0.1, 0.21, 0.3]))
+
+ def test_read_history_accepts_su2_quoted_headers_and_comments(self):
+ with tempfile.TemporaryDirectory() as directory:
+ history = Path(directory) / "history.csv"
+ history.write_text(
+ '% SU2 history\n"Time","probe 1"\n0.0,101325.0\n0.1,101326.0\n',
+ encoding="utf-8",
+ )
+ columns = AEROACOUSTICS.read_history(history)
+
+ self.assertEqual(list(columns), ["Time", "probe 1"])
+ np.testing.assert_allclose(columns["probe 1"], [101325.0, 101326.0])
+
+ def test_write_spectrum_supports_multiple_signals(self):
+ frequencies = np.array([0.0, 1.0])
+ result = {
+ "psd": np.array([1.0, 2.0]),
+ "psd_level_db_per_hz": np.array([3.0, 4.0]),
+ "narrowband_spl_db": np.array([5.0, 6.0]),
+ }
+ with tempfile.TemporaryDirectory() as directory:
+ output = Path(directory) / "spectrum.csv"
+ AEROACOUSTICS.write_spectrum(
+ output, frequencies, {"probe 1": result, "probe/2": result}
+ )
+ with output.open(newline="", encoding="utf-8") as output_file:
+ rows = list(csv.reader(output_file))
+
+ self.assertEqual(len(rows), 3)
+ self.assertIn("probe_1_PSD_Pa2_per_Hz", rows[0])
+ self.assertIn("probe_2_SPL_dB", rows[0])
+
+ def test_write_spectrum_disambiguates_normalized_names(self):
+ frequencies = np.array([0.0, 1.0])
+ result = {
+ "psd": np.ones(2),
+ "psd_level_db_per_hz": np.ones(2),
+ "narrowband_spl_db": np.ones(2),
+ }
+ with tempfile.TemporaryDirectory() as directory:
+ output = Path(directory) / "spectrum.csv"
+ AEROACOUSTICS.write_spectrum(
+ output, frequencies, {"probe 1": result, "probe/1": result}
+ )
+ header = output.read_text(encoding="utf-8").splitlines()[0]
+
+ self.assertIn("probe_1_PSD_Pa2_per_Hz", header)
+ self.assertIn("probe_1_2_PSD_Pa2_per_Hz", header)
+
+ def test_analyze_writes_spectrum_and_summary(self):
+ with tempfile.TemporaryDirectory() as directory:
+ directory = Path(directory)
+ history = directory / "history.csv"
+ output = directory / "spectrum.csv"
+ summary_file = directory / "summary.json"
+ time = np.arange(256) / 128.0
+ pressure = np.sin(2.0 * np.pi * 16.0 * time)
+ with history.open("w", newline="", encoding="utf-8") as history_file:
+ writer = csv.writer(history_file)
+ writer.writerow(["Cur_Time", "mic"])
+ writer.writerows(zip(time, pressure))
+
+ summary = AEROACOUSTICS.analyze(
+ SimpleNamespace(
+ input=history,
+ pressure=["mic"],
+ sample_rate=None,
+ time="Cur_Time",
+ pressure_scale=1.0,
+ reference_pressure=20.0e-6,
+ skip_samples=64,
+ segment_length=64,
+ overlap=0.5,
+ window="hann",
+ detrend="mean",
+ output=output,
+ summary=summary_file,
+ )
+ )
+
+ self.assertTrue(output.is_file())
+ self.assertTrue(summary_file.is_file())
+ self.assertEqual(summary["samples"], 192)
+ self.assertEqual(summary["frequency_resolution_hz"], 2.0)
+ self.assertEqual(summary["nyquist_frequency_hz"], 64.0)
+ self.assertEqual(summary["signals"]["mic"]["dominant_frequency_hz"], 16.0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py
index 7f10753db6e..73ae6d5d668 100755
--- a/TestCases/parallel_regression.py
+++ b/TestCases/parallel_regression.py
@@ -1041,6 +1041,19 @@ def main():
square_cylinder.unsteady = True
test_list.append(square_cylinder)
+ # Aeroacoustics postprocessing of a converged square-cylinder pressure history
+ aeroacoustics_square_cylinder = TestCase('aeroacoustics_square_cylinder')
+ aeroacoustics_square_cylinder.cfg_dir = "unsteady/square_cylinder"
+ aeroacoustics_square_cylinder.cfg_file = "history_aeroacoustics.csv"
+ aeroacoustics_square_cylinder.test_iter = 0
+ aeroacoustics_square_cylinder.test_vals = [103.1512634765, 1.9531250000]
+ aeroacoustics_square_cylinder.command = TestCase.Command(
+ exec = "python", param = "run_aeroacoustics.py"
+ )
+ aeroacoustics_square_cylinder.timeout = 60
+ aeroacoustics_square_cylinder.tol = 1e-6
+ test_list.append(aeroacoustics_square_cylinder)
+
# Gust
sine_gust = TestCase('sine_gust')
sine_gust.cfg_dir = "gust"
diff --git a/TestCases/unsteady/square_cylinder/run_aeroacoustics.py b/TestCases/unsteady/square_cylinder/run_aeroacoustics.py
new file mode 100644
index 00000000000..48a6c58d91e
--- /dev/null
+++ b/TestCases/unsteady/square_cylinder/run_aeroacoustics.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+
+## \file run_aeroacoustics.py
+# \brief Regression driver for aeroacoustic pressure-history analysis.
+# \version 8.5.0 "Harrier"
+#
+# SU2 Project Website: https://su2code.github.io
+#
+# The SU2 Project is maintained by the SU2 Foundation
+# (http://su2foundation.org)
+#
+# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md)
+#
+# SU2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# SU2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SU2. If not, see .
+
+import argparse
+import os
+import sys
+import tempfile
+from pathlib import Path
+from types import SimpleNamespace
+
+
+su2_run = os.environ.get("SU2_RUN")
+if su2_run:
+ sys.path.insert(0, su2_run)
+
+from aeroacoustics import analyze # noqa: E402
+
+
+SEGMENT_LENGTH = 2048
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Run the square-cylinder aeroacoustics regression analysis."
+ )
+ parser.add_argument("history", help="SU2 pressure-history CSV")
+ args = parser.parse_args()
+
+ with tempfile.TemporaryDirectory() as directory:
+ summary = analyze(
+ SimpleNamespace(
+ input=Path(args.history),
+ pressure=["mic1"],
+ sample_rate=None,
+ time="Cur_Time",
+ pressure_scale=1.0,
+ reference_pressure=20.0e-6,
+ skip_samples=0,
+ segment_length=SEGMENT_LENGTH,
+ overlap=0.5,
+ window="hann",
+ detrend="mean",
+ output=Path(directory) / "spectrum.csv",
+ summary=None,
+ )
+ )
+
+ metrics = summary["signals"]["mic1"]
+ print("\n------------------------------ Begin Solver -----------------------------\n")
+ print(
+ "| 0 | {:.10f} | {:.10f} |".format(
+ metrics["oaspl_db"], metrics["dominant_frequency_hz"]
+ )
+ )
+ print(
+ "Analyzed {} pressure samples using {} Welch segments.".format(
+ summary["samples"], metrics["segments"]
+ )
+ )
+
+
+if __name__ == "__main__":
+ main()