This commit is contained in:
schwarb committed 2026-09-16 06:24:39 +02:00
1 parent ecba8f2697
commit 0886e6e0a6
3 files changed
+226 -79

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
"""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()