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
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,4 +2,6 @@
|
||||
.vscode/
|
||||
scripts/__pycache__/
|
||||
old/
|
||||
evaluation_outputs/
|
||||
evaluation_outputs/
|
||||
.studio_cache/
|
||||
.gitignore
|
||||
BIN
__pycache__/convert_teacher_csv.cpython-314.pyc
Normal file
BIN
__pycache__/convert_teacher_csv.cpython-314.pyc
Normal file
Binary file not shown.
BIN
__pycache__/evaluation_studio.cpython-314.pyc
Normal file
BIN
__pycache__/evaluation_studio.cpython-314.pyc
Normal file
Binary file not shown.
200
convert_teacher_csv.py
Normal file
200
convert_teacher_csv.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
VIB_COLUMN_RE = re.compile(r"^(WSMS\d+)\.Acc([XYZ])$", flags=re.IGNORECASE)
|
||||
SGD_COLUMN_RE = re.compile(r"^(WSGD\d+)\.chdata$", flags=re.IGNORECASE)
|
||||
OUTPUT_COLUMNS = ["code", "type", "time", "value1", "value2", "value3"]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert teacher wide-format CSV into downloads-compatible long-format CSV."
|
||||
)
|
||||
parser.add_argument("--input", type=str, required=True, help="Input wide-format CSV path.")
|
||||
parser.add_argument("--output", type=str, required=True, help="Output long-format CSV path.")
|
||||
parser.add_argument(
|
||||
"--time-column",
|
||||
type=str,
|
||||
default="time",
|
||||
help="Time column name in the input CSV (default: time).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timezone-offset-hours",
|
||||
type=float,
|
||||
default=8.0,
|
||||
help="Offset used when parsing naive datetime strings to epoch seconds (default: 8).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def discover_sensor_columns(columns: list[str]) -> tuple[dict[str, dict[str, str]], dict[str, str]]:
|
||||
vibr_map: dict[str, dict[str, str]] = {}
|
||||
sgd_map: dict[str, str] = {}
|
||||
for column in columns:
|
||||
vibr_match = VIB_COLUMN_RE.match(column)
|
||||
if vibr_match is not None:
|
||||
sensor_code = vibr_match.group(1).upper()
|
||||
axis = vibr_match.group(2).upper()
|
||||
vibr_map.setdefault(sensor_code, {})[axis] = column
|
||||
continue
|
||||
sgd_match = SGD_COLUMN_RE.match(column)
|
||||
if sgd_match is not None:
|
||||
sensor_code = sgd_match.group(1).upper()
|
||||
sgd_map[sensor_code] = column
|
||||
return vibr_map, sgd_map
|
||||
|
||||
|
||||
def _strip_excel_datetime_wrapper(values: pd.Series) -> pd.Series:
|
||||
text = values.astype(str).str.strip()
|
||||
text = text.str.replace(r'^="(.*)"$', r"\1", regex=True)
|
||||
text = text.str.replace(r'^"(.*)"$', r"\1", regex=True)
|
||||
return text
|
||||
|
||||
|
||||
def to_epoch_seconds(time_values: pd.Series, timezone_offset_hours: float) -> pd.Series:
|
||||
numeric_time = pd.to_numeric(time_values, errors="coerce")
|
||||
if numeric_time.notna().mean() > 0.8:
|
||||
return numeric_time.astype("float64")
|
||||
|
||||
text_time = _strip_excel_datetime_wrapper(time_values)
|
||||
dt_values = pd.to_datetime(text_time, errors="coerce")
|
||||
epoch = (dt_values - pd.Timestamp("1970-01-01")) / pd.Timedelta(seconds=1)
|
||||
epoch = epoch - timezone_offset_hours * 3600.0
|
||||
epoch = pd.to_numeric(epoch, errors="coerce")
|
||||
|
||||
# Fall back to numeric parsing for rows that were not parsed as datetime.
|
||||
fallback_numeric = pd.to_numeric(text_time, errors="coerce")
|
||||
epoch = epoch.where(epoch.notna(), fallback_numeric)
|
||||
return epoch.astype("float64")
|
||||
|
||||
|
||||
def _empty_output_frame() -> pd.DataFrame:
|
||||
return pd.DataFrame(columns=OUTPUT_COLUMNS)
|
||||
|
||||
|
||||
def build_vibr_rows(df: pd.DataFrame, time_column: str, vibr_map: dict[str, dict[str, str]]) -> list[pd.DataFrame]:
|
||||
rows: list[pd.DataFrame] = []
|
||||
for sensor_code in sorted(vibr_map.keys()):
|
||||
axis_columns = vibr_map[sensor_code]
|
||||
use_columns = [time_column]
|
||||
if "X" in axis_columns:
|
||||
use_columns.append(axis_columns["X"])
|
||||
if "Y" in axis_columns:
|
||||
use_columns.append(axis_columns["Y"])
|
||||
if "Z" in axis_columns:
|
||||
use_columns.append(axis_columns["Z"])
|
||||
if len(use_columns) == 1:
|
||||
continue
|
||||
|
||||
sensor_df = df.loc[:, use_columns].copy()
|
||||
sensor_df = sensor_df.rename(
|
||||
columns={
|
||||
axis_columns.get("X", ""): "value1",
|
||||
axis_columns.get("Y", ""): "value2",
|
||||
axis_columns.get("Z", ""): "value3",
|
||||
}
|
||||
)
|
||||
for column in ["value1", "value2", "value3"]:
|
||||
if column not in sensor_df.columns:
|
||||
sensor_df[column] = np.nan
|
||||
sensor_df["value1"] = pd.to_numeric(sensor_df["value1"], errors="coerce")
|
||||
sensor_df["value2"] = pd.to_numeric(sensor_df["value2"], errors="coerce")
|
||||
sensor_df["value3"] = pd.to_numeric(sensor_df["value3"], errors="coerce")
|
||||
sensor_df = sensor_df[sensor_df[["value1", "value2", "value3"]].notna().any(axis=1)]
|
||||
if sensor_df.empty:
|
||||
continue
|
||||
|
||||
sensor_df.insert(0, "type", "vibr")
|
||||
sensor_df.insert(0, "code", sensor_code)
|
||||
rows.append(sensor_df[OUTPUT_COLUMNS])
|
||||
return rows
|
||||
|
||||
|
||||
def build_sgd_rows(df: pd.DataFrame, time_column: str, sgd_map: dict[str, str]) -> list[pd.DataFrame]:
|
||||
rows: list[pd.DataFrame] = []
|
||||
for sensor_code in sorted(sgd_map.keys()):
|
||||
sensor_column = sgd_map[sensor_code]
|
||||
sensor_df = df.loc[:, [time_column, sensor_column]].copy()
|
||||
sensor_df = sensor_df.rename(columns={sensor_column: "value1"})
|
||||
sensor_df["value1"] = pd.to_numeric(sensor_df["value1"], errors="coerce")
|
||||
sensor_df = sensor_df[sensor_df["value1"].notna()]
|
||||
if sensor_df.empty:
|
||||
continue
|
||||
|
||||
sensor_df["value2"] = np.nan
|
||||
sensor_df["value3"] = np.nan
|
||||
sensor_df.insert(0, "type", "sgd")
|
||||
sensor_df.insert(0, "code", sensor_code)
|
||||
rows.append(sensor_df[OUTPUT_COLUMNS])
|
||||
return rows
|
||||
|
||||
|
||||
def convert_dataframe(input_df: pd.DataFrame, time_column: str, timezone_offset_hours: float) -> pd.DataFrame:
|
||||
if time_column not in input_df.columns:
|
||||
raise ValueError(f"Time column '{time_column}' not found in input CSV.")
|
||||
|
||||
vibr_map, sgd_map = discover_sensor_columns(input_df.columns.tolist())
|
||||
if not vibr_map and not sgd_map:
|
||||
raise ValueError("No sensor columns detected. Expected WSMSxxxx.AccX/Y/Z or WSGDxxxx.chdata columns.")
|
||||
|
||||
df = input_df.copy()
|
||||
df[time_column] = to_epoch_seconds(df[time_column], timezone_offset_hours=timezone_offset_hours)
|
||||
df = df[df[time_column].notna()]
|
||||
if df.empty:
|
||||
return _empty_output_frame()
|
||||
|
||||
rows: list[pd.DataFrame] = []
|
||||
rows.extend(build_vibr_rows(df, time_column=time_column, vibr_map=vibr_map))
|
||||
rows.extend(build_sgd_rows(df, time_column=time_column, sgd_map=sgd_map))
|
||||
if not rows:
|
||||
return _empty_output_frame()
|
||||
|
||||
output_df = pd.concat(rows, ignore_index=True)
|
||||
output_df["time"] = pd.to_numeric(output_df["time"], errors="coerce").astype("float64")
|
||||
output_df = output_df[output_df["time"].notna()]
|
||||
output_df = output_df.sort_values(["time", "code"], kind="mergesort").reset_index(drop=True)
|
||||
return output_df.loc[:, OUTPUT_COLUMNS]
|
||||
|
||||
|
||||
def print_summary(df: pd.DataFrame, output_path: Path) -> None:
|
||||
print(f"Output saved to: {output_path}")
|
||||
print(f"Rows: {len(df)}")
|
||||
if df.empty:
|
||||
print("No data rows were generated.")
|
||||
return
|
||||
print(f"Sensors: {df['code'].nunique()}")
|
||||
print("Rows by sensor:")
|
||||
counts = df.groupby(["code", "type"]).size().sort_index()
|
||||
for (code, sensor_type), count in counts.items():
|
||||
print(f" - {code} ({sensor_type}): {int(count)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
input_path = Path(args.input).resolve()
|
||||
output_path = Path(args.output).resolve()
|
||||
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
input_df = pd.read_csv(input_path)
|
||||
output_df = convert_dataframe(
|
||||
input_df=input_df,
|
||||
time_column=args.time_column,
|
||||
timezone_offset_hours=float(args.timezone_offset_hours),
|
||||
)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_df.to_csv(output_path, index=False)
|
||||
print_summary(output_df, output_path=output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63151
downloads/Non_TMD/free_vib/data_converted_free.csv
Normal file
63151
downloads/Non_TMD/free_vib/data_converted_free.csv
Normal file
File diff suppressed because it is too large
Load Diff
63151
downloads/TMD/val/data_converted_tmd.csv
Normal file
63151
downloads/TMD/val/data_converted_tmd.csv
Normal file
File diff suppressed because it is too large
Load Diff
712
evaluation_studio.py
Normal file
712
evaluation_studio.py
Normal file
@@ -0,0 +1,712 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import math
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import threading
|
||||
import traceback
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
CHECKPOINT_DEFAULT = PROJECT_ROOT / "checkpoints_final" / "task1_final" / "task1_final_model.pkl"
|
||||
ADAPTER_DEFAULT = PROJECT_ROOT / "checkpoints_final" / "task1_final" / "task1_tmd_adapter.pkl"
|
||||
APP_OUTPUT_DIR = PROJECT_ROOT / "evaluation_outputs" / "studio_outputs"
|
||||
APP_CACHE_DIR = PROJECT_ROOT / ".studio_cache"
|
||||
|
||||
TASK1_LABEL = "任务1:顶部RMS预测(Non_TMD)"
|
||||
TASK2_LABEL = "任务2:自由衰减识别自振频率和阻尼比"
|
||||
TASK3_LABEL = "任务3:顶部响应反推激振频率"
|
||||
TASK4_LABEL = "挑战任务:TMD RMS预测(基础模型+适配器)"
|
||||
|
||||
TASK_CHOICES = [TASK1_LABEL, TASK2_LABEL, TASK3_LABEL, TASK4_LABEL]
|
||||
|
||||
HAS_DND = False
|
||||
TkRootClass: type[tk.Tk] = tk.Tk
|
||||
try:
|
||||
from tkinterdnd2 import DND_FILES, TkinterDnD # type: ignore
|
||||
|
||||
HAS_DND = True
|
||||
TkRootClass = TkinterDnD.Tk
|
||||
except Exception:
|
||||
DND_FILES = "DND_Files" # type: ignore
|
||||
|
||||
|
||||
def pretty_json_like_lines(payload: dict) -> str:
|
||||
lines: list[str] = []
|
||||
for key in sorted(payload.keys()):
|
||||
value = payload[key]
|
||||
if isinstance(value, float):
|
||||
lines.append(f"- {key}: {value:.6f}")
|
||||
else:
|
||||
lines.append(f"- {key}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def ensure_long_format_csv(input_path: Path, force_convert: bool, tz_offset_hours: float) -> tuple[Path, bool]:
|
||||
from convert_teacher_csv import convert_dataframe
|
||||
|
||||
df_head = pd.read_csv(input_path, nrows=5)
|
||||
lower_columns = {column.lower() for column in df_head.columns}
|
||||
long_required = {"code", "type", "time", "value1", "value2", "value3"}
|
||||
is_long = long_required.issubset(lower_columns)
|
||||
if is_long and not force_convert:
|
||||
return input_path, False
|
||||
|
||||
APP_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
full_df = pd.read_csv(input_path)
|
||||
converted_df = convert_dataframe(
|
||||
input_df=full_df,
|
||||
time_column="time",
|
||||
timezone_offset_hours=tz_offset_hours,
|
||||
)
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = APP_CACHE_DIR / f"{input_path.stem}_converted_{stamp}.csv"
|
||||
converted_df.to_csv(output_path, index=False)
|
||||
return output_path, True
|
||||
|
||||
|
||||
def _load_sensor_frame(df: pd.DataFrame, sensor_code: str, value_col: str, time_col: str = "time") -> pd.DataFrame:
|
||||
sensor_df = df.loc[df["code"] == sensor_code, [time_col, value_col]].copy()
|
||||
if sensor_df.empty:
|
||||
return sensor_df
|
||||
sensor_df = sensor_df.sort_values(time_col)
|
||||
sensor_df = sensor_df.drop_duplicates(subset=time_col, keep="first")
|
||||
sensor_df[time_col] = pd.to_numeric(sensor_df[time_col], errors="coerce")
|
||||
sensor_df[value_col] = pd.to_numeric(sensor_df[value_col], errors="coerce")
|
||||
sensor_df = sensor_df[sensor_df[time_col].notna() & sensor_df[value_col].notna()]
|
||||
return sensor_df
|
||||
|
||||
|
||||
def build_prediction_input(csv_path: Path, for_tmd: bool) -> dict:
|
||||
from scripts.config import make_experiment_config
|
||||
from scripts.dataset import (
|
||||
calculate_rms,
|
||||
estimate_sampling_rate,
|
||||
extract_core_features,
|
||||
extract_frequency_hz,
|
||||
get_dominant_frequency,
|
||||
get_middle_segment,
|
||||
)
|
||||
|
||||
config = make_experiment_config().data
|
||||
if for_tmd:
|
||||
config.data_dir = "downloads/TMD"
|
||||
config.__post_init__()
|
||||
|
||||
df = pd.read_csv(csv_path)
|
||||
base_df = _load_sensor_frame(df, sensor_code=config.base_sensor_code, value_col=config.base_axis, time_col=config.time_column)
|
||||
if base_df.empty:
|
||||
raise ValueError(
|
||||
f"缺少基底传感器 {config.base_sensor_code}/{config.base_axis},"
|
||||
"无法执行RMS预测任务。"
|
||||
)
|
||||
|
||||
response_df = _load_sensor_frame(
|
||||
df,
|
||||
sensor_code=config.response_sensor_code,
|
||||
value_col=config.response_axis,
|
||||
time_col=config.time_column,
|
||||
)
|
||||
|
||||
if response_df.empty:
|
||||
time_values = base_df[config.time_column].to_numpy(dtype=np.float64)
|
||||
x_values = base_df[config.base_axis].to_numpy(dtype=np.float32)
|
||||
time_middle, x_middle, _ = get_middle_segment(time_values, x_values, x_values, config)
|
||||
y_middle = None
|
||||
has_truth = False
|
||||
else:
|
||||
aligned = base_df.rename(columns={config.base_axis: "base_signal"}).merge(
|
||||
response_df.rename(columns={config.response_axis: "response_signal"}),
|
||||
on=config.time_column,
|
||||
how="left",
|
||||
)
|
||||
aligned["response_signal"] = aligned["response_signal"].interpolate(method=config.interpolation_method, limit_direction="both")
|
||||
aligned["response_signal"] = aligned["response_signal"].ffill().bfill()
|
||||
time_values = aligned[config.time_column].to_numpy(dtype=np.float64)
|
||||
x_values = aligned["base_signal"].to_numpy(dtype=np.float32)
|
||||
y_values = aligned["response_signal"].to_numpy(dtype=np.float32)
|
||||
time_middle, x_middle, y_middle = get_middle_segment(time_values, x_values, y_values, config)
|
||||
has_truth = True
|
||||
|
||||
sampling_rate = estimate_sampling_rate(np.asarray(time_middle, dtype=np.float64))
|
||||
frequency_hz = extract_frequency_hz(csv_path.name)
|
||||
if frequency_hz is None:
|
||||
frequency_hz = get_dominant_frequency(np.asarray(x_middle, dtype=np.float32), sampling_rate)
|
||||
features = extract_core_features(np.asarray(x_middle, dtype=np.float32), sampling_rate, config, known_frequency_hz=frequency_hz)
|
||||
x_rms = float(calculate_rms(np.asarray(x_middle, dtype=np.float32)))
|
||||
|
||||
true_y_rms = None
|
||||
if has_truth and y_middle is not None:
|
||||
true_y_rms = float(calculate_rms(np.asarray(y_middle, dtype=np.float32)))
|
||||
|
||||
return {
|
||||
"config": config,
|
||||
"time_middle": np.asarray(time_middle, dtype=np.float64),
|
||||
"x_middle": np.asarray(x_middle, dtype=np.float32),
|
||||
"y_middle": None if y_middle is None else np.asarray(y_middle, dtype=np.float32),
|
||||
"sampling_rate": float(sampling_rate),
|
||||
"frequency_hz": float(frequency_hz),
|
||||
"features": np.asarray(features, dtype=np.float32),
|
||||
"x_rms": x_rms,
|
||||
"true_y_rms": true_y_rms,
|
||||
"has_truth": has_truth,
|
||||
}
|
||||
|
||||
|
||||
def save_prediction_overview_figure(
|
||||
*,
|
||||
file_path: Path,
|
||||
output_dir: Path,
|
||||
title_prefix: str,
|
||||
time_middle: np.ndarray,
|
||||
x_middle: np.ndarray,
|
||||
sampling_rate: float,
|
||||
frequency_hz: float,
|
||||
pred_rms: float,
|
||||
true_rms: float | None,
|
||||
) -> Path:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
fft_values = np.fft.rfft(x_middle - np.mean(x_middle))
|
||||
freqs = np.fft.rfftfreq(x_middle.size, d=1.0 / sampling_rate)
|
||||
amplitudes = np.abs(fft_values)
|
||||
|
||||
fig, axes = plt.subplots(3, 1, figsize=(10, 8))
|
||||
fig.suptitle(f"{title_prefix} | {file_path.name}")
|
||||
|
||||
axes[0].plot(time_middle, x_middle, color="tab:blue")
|
||||
axes[0].set_title("Input Base Signal (Middle Segment)")
|
||||
axes[0].set_xlabel("Time")
|
||||
axes[0].set_ylabel("Acceleration")
|
||||
axes[0].grid(True, alpha=0.25)
|
||||
|
||||
axes[1].plot(freqs, amplitudes, color="tab:purple")
|
||||
axes[1].axvline(frequency_hz, color="tab:red", linestyle="--", linewidth=1.2, label=f"f={frequency_hz:.4f}Hz")
|
||||
axes[1].set_xlim(0.0, 5.0)
|
||||
axes[1].set_title("Input Spectrum")
|
||||
axes[1].set_xlabel("Frequency (Hz)")
|
||||
axes[1].set_ylabel("Amplitude")
|
||||
axes[1].grid(True, alpha=0.25)
|
||||
axes[1].legend()
|
||||
|
||||
if true_rms is None:
|
||||
axes[2].bar(["Pred RMS"], [pred_rms], color=["tab:orange"])
|
||||
axes[2].set_title(f"Pred RMS = {pred_rms:.6f} (truth sensor not provided)")
|
||||
else:
|
||||
axes[2].bar(["True RMS", "Pred RMS"], [true_rms, pred_rms], color=["tab:green", "tab:orange"])
|
||||
err_pct = abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
||||
axes[2].set_title(f"True={true_rms:.6f} | Pred={pred_rms:.6f} | Error={err_pct:.2f}%")
|
||||
axes[2].set_ylabel("RMS")
|
||||
axes[2].grid(True, axis="y", alpha=0.25)
|
||||
|
||||
plt.tight_layout()
|
||||
figure_path = output_dir / f"{file_path.stem}_{title_prefix.replace(' ', '_').lower()}.png"
|
||||
plt.savefig(figure_path, dpi=170, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
return figure_path
|
||||
|
||||
|
||||
def run_task1(csv_path: Path) -> tuple[dict, Path]:
|
||||
from scripts.config import CORE_FEATURE_NAMES
|
||||
|
||||
with CHECKPOINT_DEFAULT.open("rb") as handle:
|
||||
payload = pickle.load(handle)
|
||||
model = payload["model"]
|
||||
pred_input = build_prediction_input(csv_path=csv_path, for_tmd=False)
|
||||
pred_tr = max(float(model.predict([pred_input["features"].tolist()])[0]), pred_input["config"].normalization_eps)
|
||||
pred_rms = pred_tr * float(pred_input["x_rms"])
|
||||
true_rms = pred_input["true_y_rms"]
|
||||
rel_err_pct = None if true_rms is None else abs(pred_rms - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
||||
|
||||
out_dir = APP_OUTPUT_DIR / "task1"
|
||||
fig_path = save_prediction_overview_figure(
|
||||
file_path=csv_path,
|
||||
output_dir=out_dir,
|
||||
title_prefix="Task1 Prediction",
|
||||
time_middle=pred_input["time_middle"],
|
||||
x_middle=pred_input["x_middle"],
|
||||
sampling_rate=float(pred_input["sampling_rate"]),
|
||||
frequency_hz=float(pred_input["frequency_hz"]),
|
||||
pred_rms=float(pred_rms),
|
||||
true_rms=true_rms,
|
||||
)
|
||||
result = {
|
||||
"task": "task1_rms_prediction",
|
||||
"file_path": str(csv_path),
|
||||
"frequency_hz": float(pred_input["frequency_hz"]),
|
||||
"x_rms": float(pred_input["x_rms"]),
|
||||
"true_y_rms": None if true_rms is None else float(true_rms),
|
||||
"pred_tr": float(pred_tr),
|
||||
"pred_y_rms": float(pred_rms),
|
||||
"relative_error_percent": None if rel_err_pct is None else float(rel_err_pct),
|
||||
"truth_available": bool(pred_input["has_truth"]),
|
||||
"model_name": str(payload.get("model_name", "unknown")),
|
||||
"feature_names": ",".join(CORE_FEATURE_NAMES),
|
||||
}
|
||||
return result, Path(fig_path)
|
||||
|
||||
|
||||
def run_task2(csv_path: Path) -> tuple[dict, Path]:
|
||||
from scripts_2.task3_identify import analyze_free_vibration
|
||||
|
||||
real_csv = csv_path
|
||||
if "free_vib" not in {part.lower() for part in csv_path.parts}:
|
||||
temp_dir = APP_CACHE_DIR / "free_vib"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
real_csv = temp_dir / csv_path.name
|
||||
shutil.copyfile(csv_path, real_csv)
|
||||
|
||||
output_dir = APP_OUTPUT_DIR / "task2"
|
||||
result = analyze_free_vibration(
|
||||
file_path=real_csv,
|
||||
output_dir=output_dir,
|
||||
bandwidth_hz=0.3,
|
||||
show=False,
|
||||
)
|
||||
fig_path = Path(str(result["figure_path"]))
|
||||
return result, fig_path
|
||||
|
||||
|
||||
def run_task3(csv_path: Path) -> tuple[dict, Path]:
|
||||
from scripts.config import make_experiment_config
|
||||
from scripts_2.signal_utils import (
|
||||
TOP_RESPONSE_AXIS,
|
||||
TOP_RESPONSE_SENSOR,
|
||||
calculate_rms,
|
||||
get_middle_segment,
|
||||
read_sensor_signal,
|
||||
)
|
||||
from scripts_3.task4_predict_freq import analyze_forced_vibration
|
||||
|
||||
output_dir = APP_OUTPUT_DIR / "task3"
|
||||
result = analyze_forced_vibration(
|
||||
file_path=csv_path,
|
||||
output_dir=output_dir,
|
||||
show=False,
|
||||
)
|
||||
|
||||
# Optional inverse estimation:
|
||||
# infer base RMS from top-response RMS with fixed-point iteration:
|
||||
# x = y_rms / TR_model(f, x)
|
||||
try:
|
||||
config = make_experiment_config()
|
||||
with CHECKPOINT_DEFAULT.open("rb") as handle:
|
||||
payload = pickle.load(handle)
|
||||
model = payload["model"]
|
||||
|
||||
time_values, raw_signal = read_sensor_signal(
|
||||
csv_path,
|
||||
sensor_code=TOP_RESPONSE_SENSOR,
|
||||
value_column=TOP_RESPONSE_AXIS,
|
||||
)
|
||||
_, signal_segment = get_middle_segment(time_values, raw_signal)
|
||||
top_rms = float(calculate_rms(signal_segment))
|
||||
freq_hz = float(result["predicted_frequency_hz"])
|
||||
|
||||
min_x = config.data.normalization_eps
|
||||
x_est = max(top_rms, min_x)
|
||||
converged = False
|
||||
rel_change = float("inf")
|
||||
iter_count = 0
|
||||
|
||||
for i in range(1, 41):
|
||||
feat = np.asarray(
|
||||
[
|
||||
[
|
||||
freq_hz,
|
||||
freq_hz * freq_hz,
|
||||
x_est,
|
||||
(2.0 * np.pi * freq_hz) ** 2 * config.data.harmonic_amplitude_m,
|
||||
]
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
tr_est = max(float(model.predict(feat)[0]), config.data.normalization_eps)
|
||||
x_new = max(top_rms / max(tr_est, config.data.normalization_eps), min_x)
|
||||
if not np.isfinite(x_new):
|
||||
break
|
||||
rel_change = abs(x_new - x_est) / max(abs(x_est), min_x)
|
||||
# Damping update to improve stability in fixed-point iteration.
|
||||
x_est = 0.6 * x_est + 0.4 * x_new
|
||||
iter_count = i
|
||||
if rel_change < 1e-4:
|
||||
converged = True
|
||||
break
|
||||
|
||||
feat_final = np.asarray(
|
||||
[
|
||||
[
|
||||
freq_hz,
|
||||
freq_hz * freq_hz,
|
||||
x_est,
|
||||
(2.0 * np.pi * freq_hz) ** 2 * config.data.harmonic_amplitude_m,
|
||||
]
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
tr_final = max(float(model.predict(feat_final)[0]), config.data.normalization_eps)
|
||||
y_reconstructed = tr_final * x_est
|
||||
residual_pct = abs(y_reconstructed - top_rms) / max(abs(top_rms), min_x) * 100.0
|
||||
|
||||
result["top_response_rms"] = float(top_rms)
|
||||
result["estimated_base_rms"] = float(x_est)
|
||||
result["estimated_tr_from_model"] = float(tr_final)
|
||||
result["inversion_converged"] = bool(converged)
|
||||
result["inversion_iterations"] = int(iter_count)
|
||||
result["inversion_relative_change"] = float(rel_change)
|
||||
result["inversion_residual_percent"] = float(residual_pct)
|
||||
result["inversion_note"] = "Estimated by fixed-point inversion using task1 TR model; use as an estimate only."
|
||||
except Exception as error:
|
||||
result["inversion_note"] = f"Base RMS inversion unavailable: {error}"
|
||||
|
||||
fig_path = Path(str(result["figure_path"]))
|
||||
return result, fig_path
|
||||
|
||||
|
||||
def run_task4_tmd(csv_path: Path) -> tuple[dict, Path]:
|
||||
from scripts_4.adapter import load_adapter
|
||||
|
||||
with CHECKPOINT_DEFAULT.open("rb") as handle:
|
||||
payload = pickle.load(handle)
|
||||
model = payload["model"]
|
||||
adapter, adapter_extra = load_adapter(ADAPTER_DEFAULT)
|
||||
pred_input = build_prediction_input(csv_path=csv_path, for_tmd=True)
|
||||
pred_tr = max(float(model.predict([pred_input["features"].tolist()])[0]), pred_input["config"].normalization_eps)
|
||||
pred_base = pred_tr * float(pred_input["x_rms"])
|
||||
pred_tmd = adapter.predict(pred_base, float(pred_input["frequency_hz"]))
|
||||
true_rms = pred_input["true_y_rms"]
|
||||
rel_err_pct = None if true_rms is None else abs(pred_tmd - true_rms) / max(abs(true_rms), 1e-12) * 100.0
|
||||
|
||||
out_dir = APP_OUTPUT_DIR / "task4"
|
||||
fig_path = save_prediction_overview_figure(
|
||||
file_path=csv_path,
|
||||
output_dir=out_dir,
|
||||
title_prefix="Task4 TMD Prediction",
|
||||
time_middle=pred_input["time_middle"],
|
||||
x_middle=pred_input["x_middle"],
|
||||
sampling_rate=float(pred_input["sampling_rate"]),
|
||||
frequency_hz=float(pred_input["frequency_hz"]),
|
||||
pred_rms=float(pred_tmd),
|
||||
true_rms=true_rms,
|
||||
)
|
||||
result = {
|
||||
"task": "task4_tmd_rms_prediction",
|
||||
"file_path": str(csv_path),
|
||||
"frequency_hz": float(pred_input["frequency_hz"]),
|
||||
"x_rms": float(pred_input["x_rms"]),
|
||||
"true_y_rms": None if true_rms is None else float(true_rms),
|
||||
"pred_base_y_rms": float(pred_base),
|
||||
"pred_tmd_y_rms": float(pred_tmd),
|
||||
"adapter_scale_k": float(adapter.scale_at(float(pred_input["frequency_hz"]))),
|
||||
"relative_error_percent": None if rel_err_pct is None else float(rel_err_pct),
|
||||
"truth_available": bool(pred_input["has_truth"]),
|
||||
"model_name": str(payload.get("model_name", "unknown")),
|
||||
"adapter_extra": str(adapter_extra),
|
||||
}
|
||||
return result, Path(fig_path)
|
||||
|
||||
|
||||
def build_key_metrics(task_label: str, result: dict) -> str:
|
||||
if task_label == TASK1_LABEL:
|
||||
lines = [
|
||||
f"预测RMS: {result.get('pred_y_rms')}",
|
||||
f"预测TR: {result.get('pred_tr')}",
|
||||
f"激振频率(Hz): {result.get('frequency_hz')}",
|
||||
f"输入RMS(x): {result.get('x_rms')}",
|
||||
f"是否有真值: {'是' if result.get('truth_available') else '否'}",
|
||||
]
|
||||
if result.get("truth_available"):
|
||||
lines.append(f"真值RMS(y): {result.get('true_y_rms')}")
|
||||
lines.append(f"误差(%): {result.get('relative_error_percent')}")
|
||||
return "\n".join(lines)
|
||||
if task_label == TASK2_LABEL:
|
||||
lines = [
|
||||
f"自振频率f_n(Hz): {result.get('natural_frequency_hz')}",
|
||||
f"阻尼比zeta: {result.get('damping_ratio')}",
|
||||
f"精确阻尼比: {result.get('damping_ratio_exact')}",
|
||||
f"包络R²: {result.get('envelope_r2')}",
|
||||
f"采样率(Hz): {result.get('sampling_rate_hz')}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
if task_label == TASK3_LABEL:
|
||||
lines = [
|
||||
f"预测激振频率(Hz): {result.get('predicted_frequency_hz')}",
|
||||
f"稳态采样点数: {result.get('segment_sample_count')}",
|
||||
f"采样率(Hz): {result.get('sampling_rate_hz')}",
|
||||
]
|
||||
if result.get("top_response_rms") is not None:
|
||||
lines.append(f"顶部RMS(y): {result.get('top_response_rms')}")
|
||||
if result.get("estimated_base_rms") is not None:
|
||||
lines.append(f"反演底部RMS(x): {result.get('estimated_base_rms')}")
|
||||
lines.append(f"反演TR: {result.get('estimated_tr_from_model')}")
|
||||
lines.append(f"反演残差(%): {result.get('inversion_residual_percent')}")
|
||||
lines.append(f"反演收敛: {result.get('inversion_converged')}")
|
||||
if result.get("true_frequency_hz") is not None:
|
||||
lines.append(f"文件名真值(Hz): {result.get('true_frequency_hz')}")
|
||||
lines.append(f"绝对误差(Hz): {result.get('absolute_error_hz')}")
|
||||
return "\n".join(lines)
|
||||
if task_label == TASK4_LABEL:
|
||||
lines = [
|
||||
f"挑战任务预测RMS: {result.get('pred_tmd_y_rms')}",
|
||||
f"基础模型RMS: {result.get('pred_base_y_rms')}",
|
||||
f"适配系数k(f): {result.get('adapter_scale_k')}",
|
||||
f"激振频率(Hz): {result.get('frequency_hz')}",
|
||||
f"是否有真值: {'是' if result.get('truth_available') else '否'}",
|
||||
]
|
||||
if result.get("truth_available"):
|
||||
lines.append(f"真值RMS(y): {result.get('true_y_rms')}")
|
||||
lines.append(f"误差(%): {result.get('relative_error_percent')}")
|
||||
return "\n".join(lines)
|
||||
return pretty_json_like_lines(result)
|
||||
|
||||
|
||||
class EvaluationStudio:
|
||||
def __init__(self) -> None:
|
||||
self.root = TkRootClass()
|
||||
self.root.title("建筑评测一体式测试系统")
|
||||
self.root.geometry("1180x760")
|
||||
self.root.minsize(980, 620)
|
||||
|
||||
self.task_var = tk.StringVar(value=TASK1_LABEL)
|
||||
self.file_var = tk.StringVar(value="")
|
||||
self.force_convert_var = tk.BooleanVar(value=False)
|
||||
self.timezone_var = tk.StringVar(value="8")
|
||||
self.status_var = tk.StringVar(value="就绪")
|
||||
self.preview_image: tk.PhotoImage | None = None
|
||||
self.original_image: tk.PhotoImage | None = None
|
||||
self.current_image_path: Path | None = None
|
||||
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
main = ttk.Frame(self.root, padding=10)
|
||||
main.pack(fill="both", expand=True)
|
||||
main.columnconfigure(1, weight=1)
|
||||
main.rowconfigure(4, weight=1)
|
||||
|
||||
ttk.Label(main, text="评测任务").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=5)
|
||||
task_box = ttk.Combobox(main, textvariable=self.task_var, values=TASK_CHOICES, state="readonly")
|
||||
task_box.grid(row=0, column=1, sticky="ew", pady=5)
|
||||
|
||||
ttk.Label(main, text="CSV 文件").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=5)
|
||||
file_entry = ttk.Entry(main, textvariable=self.file_var)
|
||||
file_entry.grid(row=1, column=1, sticky="ew", pady=5)
|
||||
ttk.Button(main, text="选择文件", command=self._choose_file).grid(row=1, column=2, padx=(8, 0), pady=5)
|
||||
|
||||
self.drop_label = ttk.Label(
|
||||
main,
|
||||
text="将 CSV 拖到这里(若无拖拽支持,可用“选择文件”)",
|
||||
relief="groove",
|
||||
anchor="center",
|
||||
)
|
||||
self.drop_label.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(4, 8), ipady=12)
|
||||
if HAS_DND:
|
||||
self.drop_label.drop_target_register(DND_FILES)
|
||||
self.drop_label.dnd_bind("<<Drop>>", self._on_drop)
|
||||
|
||||
config_line = ttk.Frame(main)
|
||||
config_line.grid(row=3, column=0, columnspan=3, sticky="ew", pady=5)
|
||||
config_line.columnconfigure(6, weight=1)
|
||||
ttk.Checkbutton(
|
||||
config_line,
|
||||
text="强制按老师宽表格式转换",
|
||||
variable=self.force_convert_var,
|
||||
).grid(row=0, column=0, sticky="w")
|
||||
ttk.Label(config_line, text="字符串时间时区偏移(小时)").grid(row=0, column=1, sticky="w", padx=(12, 4))
|
||||
ttk.Entry(config_line, textvariable=self.timezone_var, width=7).grid(row=0, column=2, sticky="w")
|
||||
ttk.Button(config_line, text="开始评测", command=self._run_clicked).grid(row=0, column=3, padx=(12, 0))
|
||||
ttk.Label(config_line, textvariable=self.status_var).grid(row=0, column=4, sticky="w", padx=(12, 0))
|
||||
if not HAS_DND:
|
||||
ttk.Label(
|
||||
config_line,
|
||||
text="提示:未检测到 tkinterdnd2,拖拽不可用;可先 pip install tkinterdnd2",
|
||||
foreground="#aa5500",
|
||||
).grid(row=0, column=5, sticky="w", padx=(12, 0))
|
||||
|
||||
paned = ttk.Panedwindow(main, orient="horizontal")
|
||||
paned.grid(row=4, column=0, columnspan=3, sticky="nsew", pady=(6, 0))
|
||||
|
||||
left = ttk.Frame(paned, padding=8)
|
||||
right = ttk.Frame(paned, padding=8)
|
||||
paned.add(left, weight=3)
|
||||
paned.add(right, weight=2)
|
||||
|
||||
ttk.Label(left, text="关键结果").pack(anchor="w")
|
||||
self.key_text = ScrolledText(left, wrap="word", height=8)
|
||||
self.key_text.pack(fill="x", expand=False, pady=(0, 6))
|
||||
|
||||
ttk.Label(left, text="详细结果").pack(anchor="w")
|
||||
self.result_text = ScrolledText(left, wrap="word", height=16)
|
||||
self.result_text.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(right, text="图像预览(最新结果)").pack(anchor="w")
|
||||
self.image_label = ttk.Label(right, relief="sunken", anchor="center")
|
||||
self.image_label.pack(fill="both", expand=True)
|
||||
self.image_label.bind("<Configure>", self._on_image_area_resize)
|
||||
|
||||
def _choose_file(self) -> None:
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="选择CSV文件",
|
||||
filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")],
|
||||
initialdir=str(PROJECT_ROOT),
|
||||
)
|
||||
if file_path:
|
||||
self.file_var.set(file_path)
|
||||
|
||||
def _on_drop(self, event) -> None: # noqa: ANN001
|
||||
raw = event.data
|
||||
if not raw:
|
||||
return
|
||||
paths = self.root.tk.splitlist(raw)
|
||||
if not paths:
|
||||
return
|
||||
self.file_var.set(str(paths[0]).strip("{}"))
|
||||
|
||||
def _append_result(self, text: str) -> None:
|
||||
self.result_text.insert("end", text + "\n")
|
||||
self.result_text.see("end")
|
||||
|
||||
def _on_image_area_resize(self, _event) -> None: # noqa: ANN001
|
||||
self._refresh_image_fit()
|
||||
|
||||
def _refresh_image_fit(self) -> None:
|
||||
if self.original_image is None:
|
||||
return
|
||||
width = max(self.image_label.winfo_width(), 1)
|
||||
height = max(self.image_label.winfo_height(), 1)
|
||||
orig_w = max(self.original_image.width(), 1)
|
||||
orig_h = max(self.original_image.height(), 1)
|
||||
ratio_w = math.ceil(orig_w / width)
|
||||
ratio_h = math.ceil(orig_h / height)
|
||||
ratio = max(1, ratio_w, ratio_h)
|
||||
scaled = self.original_image.subsample(ratio, ratio)
|
||||
self.preview_image = scaled
|
||||
self.image_label.configure(image=scaled, text="")
|
||||
|
||||
def _set_image(self, image_path: Path | None) -> None:
|
||||
if image_path is None or not image_path.exists():
|
||||
self.image_label.configure(image="", text="暂无图像")
|
||||
self.preview_image = None
|
||||
self.original_image = None
|
||||
self.current_image_path = None
|
||||
return
|
||||
try:
|
||||
self.original_image = tk.PhotoImage(file=str(image_path))
|
||||
self.current_image_path = image_path
|
||||
self._refresh_image_fit()
|
||||
except Exception:
|
||||
self.image_label.configure(text=f"图像已生成:\n{image_path}", image="")
|
||||
self.preview_image = None
|
||||
self.original_image = None
|
||||
self.current_image_path = image_path
|
||||
|
||||
def _run_clicked(self) -> None:
|
||||
csv_text = self.file_var.get().strip()
|
||||
if not csv_text:
|
||||
messagebox.showerror("缺少输入", "请先选择或拖入CSV文件。")
|
||||
return
|
||||
csv_path = Path(csv_text).expanduser().resolve()
|
||||
if not csv_path.exists():
|
||||
messagebox.showerror("文件不存在", f"找不到文件:\n{csv_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
tz_offset = float(self.timezone_var.get().strip())
|
||||
except ValueError:
|
||||
messagebox.showerror("参数错误", "时区偏移必须是数字。")
|
||||
return
|
||||
|
||||
self.status_var.set("运行中...")
|
||||
self.key_text.delete("1.0", "end")
|
||||
self.result_text.delete("1.0", "end")
|
||||
self._set_image(None)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=self._run_pipeline,
|
||||
args=(csv_path, self.task_var.get(), self.force_convert_var.get(), tz_offset),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def _run_pipeline(self, csv_path: Path, task_label: str, force_convert: bool, tz_offset: float) -> None:
|
||||
try:
|
||||
APP_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
use_csv, converted = ensure_long_format_csv(csv_path, force_convert=force_convert, tz_offset_hours=tz_offset)
|
||||
notes = [
|
||||
f"输入文件: {csv_path}",
|
||||
f"实际评测文件: {use_csv}",
|
||||
f"是否执行宽转长: {'是' if converted else '否'}",
|
||||
f"任务: {task_label}",
|
||||
"",
|
||||
]
|
||||
|
||||
if task_label == TASK1_LABEL:
|
||||
result, fig_path = run_task1(use_csv)
|
||||
elif task_label == TASK2_LABEL:
|
||||
result, fig_path = run_task2(use_csv)
|
||||
elif task_label == TASK3_LABEL:
|
||||
result, fig_path = run_task3(use_csv)
|
||||
elif task_label == TASK4_LABEL:
|
||||
result, fig_path = run_task4_tmd(use_csv)
|
||||
else:
|
||||
raise ValueError(f"不支持的任务:{task_label}")
|
||||
|
||||
output_text = "\n".join(notes) + pretty_json_like_lines(result)
|
||||
key_text = build_key_metrics(task_label, result)
|
||||
self.root.after(0, lambda: self.key_text.insert("end", key_text + "\n"))
|
||||
self.root.after(0, lambda: self.result_text.insert("end", output_text + "\n"))
|
||||
self.root.after(0, lambda: self._set_image(fig_path))
|
||||
self.root.after(0, lambda: self.status_var.set("完成"))
|
||||
except Exception as error:
|
||||
stack = traceback.format_exc()
|
||||
message = f"运行失败:{error}\n\n{stack}"
|
||||
self.root.after(0, lambda: self.key_text.insert("end", "运行失败,请查看详细错误。\n"))
|
||||
self.root.after(0, lambda: self.result_text.insert("end", message + "\n"))
|
||||
self.root.after(0, lambda: self.status_var.set("失败"))
|
||||
|
||||
def run(self) -> None:
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Integrated visual evaluation studio for tasks 1~4.")
|
||||
parser.add_argument("--task", type=str, default=None, choices=TASK_CHOICES)
|
||||
parser.add_argument("--file", type=str, default=None, help="Optional CSV file path to preload.")
|
||||
parser.add_argument(
|
||||
"--force-convert",
|
||||
action="store_true",
|
||||
help="Force teacher wide-format conversion before running tasks.",
|
||||
)
|
||||
parser.add_argument("--timezone-offset-hours", type=float, default=8.0)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
app = EvaluationStudio()
|
||||
if args.task:
|
||||
app.task_var.set(args.task)
|
||||
if args.file:
|
||||
app.file_var.set(str(Path(args.file).expanduser().resolve()))
|
||||
if args.force_convert:
|
||||
app.force_convert_var.set(True)
|
||||
app.timezone_var.set(str(args.timezone_offset_hours))
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
scripts_2/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
scripts_2/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
scripts_2/__pycache__/signal_utils.cpython-314.pyc
Normal file
BIN
scripts_2/__pycache__/signal_utils.cpython-314.pyc
Normal file
Binary file not shown.
BIN
scripts_2/__pycache__/task3_identify.cpython-314.pyc
Normal file
BIN
scripts_2/__pycache__/task3_identify.cpython-314.pyc
Normal file
Binary file not shown.
BIN
scripts_3/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
scripts_3/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
scripts_3/__pycache__/task4_predict_freq.cpython-314.pyc
Normal file
BIN
scripts_3/__pycache__/task4_predict_freq.cpython-314.pyc
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@ 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]))
|
||||
@@ -98,6 +99,37 @@ def save_task4_figure(
|
||||
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,
|
||||
@@ -107,13 +139,22 @@ def analyze_forced_vibration(
|
||||
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)
|
||||
predicted_frequency_hz, freqs, magnitudes, peak_index = dominant_frequency_in_band(
|
||||
_, 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)
|
||||
|
||||
|
||||
BIN
scripts_4/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
scripts_4/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user