Files
Building/scripts_3/task4_predict_freq.py
CrbnsCat10n d2dde4f557 modified: .DS_Store
modified:   .gitignore
	new file:   __pycache__/convert_teacher_csv.cpython-314.pyc
	new file:   __pycache__/evaluation_studio.cpython-314.pyc
	new file:   convert_teacher_csv.py
	new file:   downloads/Non_TMD/free_vib/data_converted_free.csv
	new file:   downloads/TMD/val/data_converted_tmd.csv
	new file:   evaluation_studio.py
	new file:   scripts_2/__pycache__/__init__.cpython-314.pyc
	new file:   scripts_2/__pycache__/signal_utils.cpython-314.pyc
	new file:   scripts_2/__pycache__/task3_identify.cpython-314.pyc
	new file:   scripts_3/__pycache__/__init__.cpython-314.pyc
	new file:   scripts_3/__pycache__/task4_predict_freq.cpython-314.pyc
	modified:   scripts_3/task4_predict_freq.py
	new file:   scripts_4/__pycache__/__init__.cpython-314.pyc
	modified:   scripts_4/__pycache__/adapter.cpython-314.pyc
2026-05-07 12:48:31 +08:00

222 lines
7.7 KiB
Python

from __future__ import annotations
import argparse
from pathlib import Path
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import find_peaks
if __package__ is None or __package__ == "":
sys.path.append(str(Path(__file__).resolve().parents[1]))
from scripts_2.signal_utils import (
TOP_RESPONSE_AXIS,
TOP_RESPONSE_SENSOR,
dominant_frequency_in_band,
ensure_parent_dir,
estimate_sampling_rate,
get_middle_segment,
parse_frequency_from_filename,
read_sensor_signal,
)
def parse_args() -> argparse.Namespace:
project_root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description="Predict the harmonic input frequency from top-response steady-state data only.")
parser.add_argument(
"--file",
type=str,
default=str(project_root / "downloads" / "Non_TMD" / "val" / "harmonic_5mm_0.75Hz.csv"),
help="Path to a forced-vibration CSV such as data from Non_TMD val/test.",
)
parser.add_argument(
"--output-dir",
type=str,
default=str(project_root / "evaluation_outputs" / "task4_predict_freq"),
help="Directory for plots and optional exported diagnostics.",
)
parser.add_argument(
"--show",
action="store_true",
help="Display the generated figure in addition to saving it.",
)
return parser.parse_args()
def save_task4_figure(
file_path: Path,
output_dir: Path,
time_segment: np.ndarray,
signal_segment: np.ndarray,
freqs: np.ndarray,
magnitudes: np.ndarray,
predicted_frequency_hz: float,
true_frequency_hz: float | None,
) -> Path:
output_dir = ensure_parent_dir(output_dir)
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle(f"Task 4 | Forced-Vibration Frequency Prediction | {file_path.name}")
axes[0].plot(time_segment, signal_segment, color="tab:blue", linewidth=1.0)
axes[0].set_title(f"Steady middle segment from {TOP_RESPONSE_SENSOR} / {TOP_RESPONSE_AXIS}")
axes[0].set_xlabel("Time (s)")
axes[0].set_ylabel("Acceleration")
axes[0].grid(True, alpha=0.25)
band_mask = (freqs >= 0.1) & (freqs <= 5.0)
axes[1].plot(freqs[band_mask], magnitudes[band_mask], color="tab:purple", linewidth=1.1, label="Windowed FFT")
axes[1].axvline(
predicted_frequency_hz,
color="tab:red",
linestyle="--",
linewidth=1.6,
label=f"Predicted = {predicted_frequency_hz:.5f} Hz",
)
if true_frequency_hz is not None:
axes[1].axvline(
true_frequency_hz,
color="tab:green",
linestyle=":",
linewidth=1.6,
label=f"Filename truth = {true_frequency_hz:.5f} Hz",
)
axes[1].set_title("High-resolution spectrum from top steady-state response")
axes[1].set_xlabel("Frequency (Hz)")
axes[1].set_ylabel("Magnitude")
axes[1].grid(True, alpha=0.25)
axes[1].legend()
plt.tight_layout()
figure_path = output_dir / f"{file_path.stem}_task4_predict_freq.png"
plt.savefig(figure_path, dpi=180, bbox_inches="tight")
return figure_path
def pick_fundamental_peak_index(
freqs: np.ndarray,
magnitudes: np.ndarray,
min_hz: float = 0.1,
max_hz: float = 5.0,
relative_threshold: float = 0.20,
relative_prominence: float = 0.08,
) -> int:
band_mask = (freqs >= min_hz) & (freqs <= max_hz)
band_indices = np.where(band_mask)[0]
if band_indices.size == 0:
raise ValueError("No FFT bins fall inside the requested frequency band.")
band_magnitudes = magnitudes[band_mask]
max_magnitude = float(np.max(band_magnitudes))
height_threshold = relative_threshold * max_magnitude
prominence_threshold = relative_prominence * max_magnitude
local_peaks, _ = find_peaks(
band_magnitudes,
height=height_threshold,
prominence=prominence_threshold,
)
if local_peaks.size == 0:
# Fallback for very smooth spectra: choose the strongest bin in band.
return int(band_indices[int(np.argmax(band_magnitudes))])
# Fundamental-first rule: choose the left-most valid peak.
return int(band_indices[int(local_peaks[0])])
def analyze_forced_vibration(
file_path: Path,
output_dir: Path,
show: bool = False,
) -> dict[str, float | int | str]:
file_path = Path(file_path).resolve()
time_values, raw_signal = read_sensor_signal(file_path, sensor_code=TOP_RESPONSE_SENSOR, value_column=TOP_RESPONSE_AXIS)
time_segment, signal_segment = get_middle_segment(time_values, raw_signal)
sampling_rate = estimate_sampling_rate(time_segment)
_, freqs, magnitudes, _ = dominant_frequency_in_band(
signal_segment,
sampling_rate=sampling_rate,
min_hz=0.1,
max_hz=5.0,
zero_padding_factor=32,
)
peak_index = pick_fundamental_peak_index(
freqs=freqs,
magnitudes=magnitudes,
min_hz=0.1,
max_hz=5.0,
relative_threshold=0.20,
relative_prominence=0.08,
)
predicted_frequency_hz = float(freqs[peak_index])
true_frequency_hz = parse_frequency_from_filename(file_path.name)
absolute_error_hz = None if true_frequency_hz is None else abs(predicted_frequency_hz - true_frequency_hz)
figure_path = save_task4_figure(
file_path=file_path,
output_dir=output_dir,
time_segment=time_segment,
signal_segment=signal_segment,
freqs=freqs,
magnitudes=magnitudes,
predicted_frequency_hz=predicted_frequency_hz,
true_frequency_hz=true_frequency_hz,
)
if show:
plt.show()
plt.close("all")
return {
"file_path": str(file_path),
"sensor_code": TOP_RESPONSE_SENSOR,
"axis": TOP_RESPONSE_AXIS,
"sampling_rate_hz": float(sampling_rate),
"segment_sample_count": int(signal_segment.size),
"segment_start_time": float(time_segment[0]),
"segment_end_time": float(time_segment[-1]),
"predicted_frequency_hz": float(predicted_frequency_hz),
"fft_peak_bin_hz": float(freqs[peak_index]),
"fft_peak_magnitude": float(magnitudes[peak_index]),
"true_frequency_hz": None if true_frequency_hz is None else float(true_frequency_hz),
"absolute_error_hz": None if absolute_error_hz is None else float(absolute_error_hz),
"figure_path": str(figure_path),
"note": "Prediction uses only the top response sensor WSMS00007/value3. Base sensor WSMS00012/value1 is not used to infer the answer.",
}
def print_report(result: dict[str, float | int | str]) -> None:
print("=== Task 4: Excitation Frequency Reverse Prediction ===")
print(f"Input file: {result['file_path']}")
print(f"Top response sensor: {result['sensor_code']} / {result['axis']}")
print(f"Sampling rate: {result['sampling_rate_hz']:.4f} Hz")
print(f"Steady segment samples: {result['segment_sample_count']}")
print(f"Steady segment time range: {result['segment_start_time']:.4f} s to {result['segment_end_time']:.4f} s")
print(f"Predicted excitation frequency: {result['predicted_frequency_hz']:.6f} Hz")
if result["true_frequency_hz"] is not None:
print(f"Filename frequency truth: {result['true_frequency_hz']:.6f} Hz")
print(f"Absolute error: {result['absolute_error_hz']:.6f} Hz")
else:
print("Filename frequency truth: unavailable")
print(f"Figure saved to: {result['figure_path']}")
print(result["note"])
def main() -> None:
args = parse_args()
result = analyze_forced_vibration(
file_path=Path(args.file),
output_dir=Path(args.output_dir),
show=bool(args.show),
)
print_report(result)
if __name__ == "__main__":
main()