updated
This commit is contained in:
1 parent
ecba8f2697
commit
0886e6e0a6
3 files changed
+226
-79
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user