Files
FAT-Servo-Load-Test/Python/resync_csv.py
T
2026-09-16 06:24:39 +02:00

166 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""resync_csv.py
Entfernt aus einem 'reworked' Scope-CSV alle Daten, in denen die Achse
stillsteht, und schreibt ein NEUES CSV-File. Das Ursprungsfile wird
nicht geloescht oder ueberschrieben.
Wichtig: Die Stromspalte ('current_mA') hat eine andere Zeitachse
('Current Time [ms]', Sampling 10 ms) als die Achsenspalten
('Time [ms]', Sampling 2 ms). Beide Zeitspalten bleiben in jeder
Zeile unveraendert erhalten, damit der Stromkanal weiterhin auf seiner
eigenen Zeitbasis referenziert ist. Der Stillstand wird ausschließlich
aus der ACTPOS-Spalte erkannt.
Stillstand-Erkennung:
Eine Zeile gilt als stillstehend, wenn die ACTPOS-Spanne
(max - min) in einem zentrierten Fenster um diese Zeile
(Standard: 100 ms) unterhalb der Schwelle (Standard: 0.001) liegt.
Verwendung:
python resync_csv.py <Pfad_zu_csv> [--threshold 0.001] [--window-ms 100]
"""
import argparse
import os
import sys
import pandas as pd
TIME_COL = "Time [ms]"
POS_COL = "ACTPOS"
OUTPUT_SUFFIX = "_resynced"
# --------------------------------------------------
# CSV einlesen (wie im Scope-Viewer)
# --------------------------------------------------
def load_reworked_csv(path):
"""Liest den 'reworked' Scope-Export (Semikolon-getrennt) ein.
Zeilen ohne numerischen Zeitwert (z.B. 'EOF') werden verworfen,
leere Signalzellen werden NaN.
"""
df = pd.read_csv(path, sep=";", low_memory=False)
df.columns = [str(c).strip() for c in df.columns]
if TIME_COL not in df.columns:
raise ValueError(f"Spalte '{TIME_COL}' nicht gefunden in {os.path.basename(path)}")
if POS_COL not in df.columns:
raise ValueError(f"Spalte '{POS_COL}' nicht gefunden in {os.path.basename(path)}")
df[TIME_COL] = pd.to_numeric(df[TIME_COL], errors="coerce")
df = df.dropna(subset=[TIME_COL])
for col in df.columns:
if col != TIME_COL:
df[col] = pd.to_numeric(df[col], errors="coerce")
if df.empty:
raise ValueError(f"Keine Daten in {path}")
return df
# --------------------------------------------------
# Stillstand-Erkennung
# --------------------------------------------------
def find_moving_mask(df, window_ms, threshold, sample_ms=2.0):
"""Gibt Boolesche Maske zurueck: True = Achse bewegt sich.
Basis ist ausschliesslich ACTPOS (2-ms-Kanal). Der Stromkanal wird
nicht betrachtet, da er auf einer eigenen 10-ms-Zeitachse liegt.
Eine Zeile gilt als bewegend, wenn die ACTPOS-Spanne (max-min) im
zentrierten Fenster um die Zeile die Schwelle erreicht. Zeilen
direkt am Rand einer Bewegung (halb Fensterbreite) bleiben daher
erhalten; ausgedehnte Stillstandsphasen werden komplett entfernt.
"""
window = max(1, int(round(window_ms / sample_ms)))
pos = df[POS_COL]
rmax = pos.rolling(2 * window + 1, center=True, min_periods=1).max()
rmin = pos.rolling(2 * window + 1, center=True, min_periods=1).min()
return (rmax - rmin) >= threshold
# --------------------------------------------------
# Ausgabe
# --------------------------------------------------
def unique_output_path(src_path):
"""Zielt auf <Stem>_resynced.csv, bei Belegung zahlt hoch (1, 2, ...)."""
directory = os.path.dirname(os.path.abspath(src_path))
stem = os.path.splitext(os.path.basename(src_path))[0]
candidate = os.path.join(directory, f"{stem}{OUTPUT_SUFFIX}.csv")
counter = 1
while os.path.exists(candidate):
candidate = os.path.join(directory, f"{stem}{OUTPUT_SUFFIX}_{counter}.csv")
counter += 1
return candidate
def main():
parser = argparse.ArgumentParser(
description="Entfernt Stillstandsdaten aus einem reworked Scope-CSV "
"und schreibt ein neues CSV (Ursprungsfile bleibt unverändert)."
)
parser.add_argument("csv", help="Pfad zum Quelldatei (.csv)")
parser.add_argument(
"--threshold", type=float, default=1e-3,
help="Minimale ACTPOS-Spanne im Fenster, ab der Bewegung gilt "
"(Standard: 0.001)"
)
parser.add_argument(
"--window-ms", type=float, default=100.0,
help="Breite des zentrierten Erkennungs-Fensters in ms (Standard: 100)"
)
parser.add_argument(
"--output",
help="Optionaler Ziel-Pfad. Standard: <Quelle>_resynced.csv im selben Ordner"
)
args = parser.parse_args()
src = os.path.abspath(args.csv)
if not os.path.isfile(src):
sys.exit(f"Fehler: Datei nicht gefunden: {src}")
# Sicherheitsnetz: Ursprungsfile darf nie das Ziel werden
if args.output:
dst = os.path.abspath(args.output)
if dst == src:
sys.exit("Fehler: Output-Pfad ist identisch mit der Quelldatei.")
else:
dst = unique_output_path(src)
df = load_reworked_csv(src)
n_in = len(df)
moving = find_moving_mask(df, args.window_ms, args.threshold)
out = df[moving]
n_out = len(out)
if n_out == 0:
print("Keine Bewegung erkannt es wurde keine Datei geschrieben.")
sys.exit(1)
# Zeilenreihenfolge (inkl. beider Zeitachsen) unverändert erhalten
out.to_csv(dst, sep=";", index=False, na_rep="")
# EOF-Marker wie in den Original-Exports anhaengen
with open(dst, "a", encoding="utf-8") as f:
f.write("\n" + ";".join(["EOF"] + [""] * (len(out.columns) - 1)) + "\n")
removed = n_in - n_out
removed_time = df.loc[~moving, TIME_COL]
removed_ms = (
float(removed_time.max() - removed_time.min()) if len(removed_time) else 0.0
)
print(f"Eingelesen: {n_in} Zeilen aus {os.path.basename(src)}")
print(f"Entfernt (still): {removed} Zeilen (ca. {removed_ms / 1000:.1f} s Zeitspanne)")
print(f"Behalten (bewegt): {n_out} Zeilen")
print(f"Neues File: {dst}")
if __name__ == "__main__":
main()