Geschossenergie-Rechner in Python.

Im heutigen Blogbeitrag mal etwas, was nichts mit Fotografie zu tun hat.

Wer als ambitionierter Freizeitschütze mit seiner Druckluftwaffe nicht nur auf Dosen schießt (oder wer gar Sportschütze ist), der weiß, dass sich Waffe und Geschoss "mögen" sollten. Die betreffende Waffe und die entsprechende Munition müssen gemeinsam möglichst homogene Leistungen erzeugen, damit zielgenaue Ergebnisse generiert werden können. Um hierüber eine Aussage treffen zu können, erstellt man Messreihen der Mündungsgeschwindigkeiten der Geschosse und wertet diese im Anschluss aus.

Da einige preiswerte China-Chronys hierfür keine oder nur unzureichende Auswertemöglichkeiten bieten, habe ich für die manuelle Erfassung und elektronische Auswertung solcher Messreihen ein kleines Programm in Python erstellt.

Features:
  • Grafische Oberfläche
  • Erfassung und Auswertung von unbegrenzten Messreihen
  • Ausgabe von Durchschnittsenergie, Durchschnittsgeschwindigkeit und Standardabweichung
  • LOG-Datei im Programmverzeichnis

Wer dieses Programm nutzen möchte, muss lediglich:

  1. eine aktuelles Python auf seinem Rechner installiert haben
  2. untenstehenden Programmcode vollständig in einen beliebigen Texteditor kopieren
  3. die so erzeugte Datei dann mit der Endung .pyw abspeichern (und ausführen)

Der Programmcode ist OpenSource und kann gern von jedermann verändert/erweitert werden.

Warnung: Sämtliche Funktionen sind ohne Gewähr!

#////////////////////////////////////////////////////////////////////////////
# PROGRAMMTITEL: Geschossenergie-Rechner
# BESCHREIBUNG: Programm errechnet Geschossenergie und Standardabweichung von Messreihen
# ENTWICKELT MIT PYTHON-VERSION: 3.11.9
#////////////////////////////////////////////////////////////////////////////

#!/usr/bin/env python3

programmVersion = "2.0.2"
datumVersion = "12.10.2024"

import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import datetime
import statistics

def komma_zu_punkt(wert):
    return wert.replace(',', '.')

def berechne_geschossenergie():
    try:
        # Überprüfe Waffenname
        waffenname = waffenname_entry.get().strip()
        if not waffenname:
            raise ValueError("Bitte gebe einen Waffennamen ein.")

        # Überprüfe Munitionsname
        munitionsname = munitionsname_entry.get().strip()
        if not munitionsname:
            raise ValueError("Bitte gebe einen Munitionsnamen ein.")

        # Überprüfe Geschossmasse
        geschossmasse_str = komma_zu_punkt(geschossmasse_entry.get().strip())
        if not geschossmasse_str:
            raise ValueError("Bitte gebe die Geschossmasse ein.")
        try:
            geschossmasse_g = float(geschossmasse_str)
            if geschossmasse_g <= 0:
                raise ValueError("Die Geschossmasse muss größer als 0 sein.")
        except ValueError:
            raise ValueError("Ungültige Geschossmasse. Bitte gebe eine positive Zahl ein.")

        geschossmasse_kg = geschossmasse_g / 1000  # Umrechnung von Gramm in Kilogramm

        notizen = notizen_entry.get()

        # Sammle alle gültigen Geschwindigkeitswerte
        geschwindigkeiten = []
        for line in geschwindigkeit_text.get("1.0", tk.END).splitlines():
            line = komma_zu_punkt(line.strip())
            if line:
                try:
                    geschwindigkeit = float(line)
                    if geschwindigkeit <= 0:
                        raise ValueError(f"Ungültige Geschwindigkeit: \'{line}\'. Muss größer als 0 sein.")
                    geschwindigkeiten.append(geschwindigkeit)
                except ValueError:
                    raise ValueError(f"Ungültige Geschwindigkeit: \'{line}\'. Bitte gebe nur positive Zahlen ein.")

        if not geschwindigkeiten:
            raise ValueError("Gebe hier jeweils eine Geschwindigkeitsmessung (m/s) pro Zeile ein.")

        # Berechne Durchschnittsgeschwindigkeit und Standardabweichung
        durchschnittsgeschwindigkeit = statistics.mean(geschwindigkeiten)
        if len(geschwindigkeiten) > 1:
            standardabweichung = statistics.stdev(geschwindigkeiten)
            standardabweichung_prozent = (standardabweichung / durchschnittsgeschwindigkeit) * 100
        else:
            standardabweichung = None
            standardabweichung_prozent = None

        energie = 0.5 * geschossmasse_kg * (durchschnittsgeschwindigkeit ** 2)

        # Hervorhebung der Geschossenergie
        energie_label.config(text=f"Geschossenergie: {energie:.2f} Joule", foreground="blue")

        ergebnis_text = "Zusätzliche Informationen:\n"
        ergebnis_text += f"Durchschnittsgeschwindigkeit: {durchschnittsgeschwindigkeit:.2f} m/s\n"
        if standardabweichung:
            ergebnis_text += f"Standardabweichung: {standardabweichung:.2f} m/s ({standardabweichung_prozent:.2f}%)"
        ergebnis_label.config(text=ergebnis_text)

        # Speichern der Ergebnisse in die Logdatei
        speichere_ergebnisse(waffenname, munitionsname, geschossmasse_g, geschwindigkeiten, durchschnittsgeschwindigkeit, standardabweichung, standardabweichung_prozent, energie, notizen)

    except ValueError as e:
        energie_label.config(text="Fehler bei der Berechnung", foreground="red")
        ergebnis_label.config(text="")
        messagebox.showerror("Eingabefehler", str(e))

def speichere_ergebnisse(waffenname, munitionsname, geschossmasse_g, geschwindigkeiten, durchschnittsgeschwindigkeit, standardabweichung, standardabweichung_prozent, energie, notizen):
    zeitstempel = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open("Logdatei_Geschossenergie-Rechner.txt", "a") as logfile:
        logfile.write(f"Zeitpunkt: {zeitstempel}\n")
        logfile.write(f"Waffenname: {waffenname}\n")
        logfile.write(f"Munitionsname: {munitionsname}\n")
        logfile.write(f"Geschossmasse: {geschossmasse_g} g\n")
        logfile.write("Geschwindigkeiten (m/s):\n")
        for i, v in enumerate(geschwindigkeiten, 1):
            logfile.write(f"  Messung {i}: {v} m/s\n")
        logfile.write(f"Durchschnittsgeschwindigkeit: {durchschnittsgeschwindigkeit:.2f} m/s\n")
        if standardabweichung:
            logfile.write(f"Standardabweichung der Geschwindigkeit: {standardabweichung:.2f} m/s ({standardabweichung_prozent:.2f}%)\n")
        logfile.write(f"Durchschnittliche Geschossenergie: {energie:.2f} J\n")
        logfile.write(f"Notizen: {notizen}\n")
        logfile.write("-" * 60 + "\n")

def zuruecksetzen():
    waffenname_entry.delete(0, tk.END)
    munitionsname_entry.delete(0, tk.END)
    geschossmasse_entry.delete(0, tk.END)
    notizen_entry.delete(0, tk.END)
    geschwindigkeit_text.delete("1.0", tk.END)
    geschwindigkeit_text.insert(tk.END, "Die Anzahl der Geschwindigkeitsmessungen ist unbegrenzt. Gebe hier jeweils eine Geschwindigkeitsmessung (m/s) pro Zeile ein:")
    energie_label.config(text="")
    ergebnis_label.config(text="")

def clear_default_text(event):
    if geschwindigkeit_text.get("1.0", tk.END).strip() == "Die Anzahl der Geschwindigkeitsmessungen ist unbegrenzt. Gebe hier jeweils eine Geschwindigkeitsmessung (m/s) pro Zeile ein:":
        geschwindigkeit_text.delete("1.0", tk.END)

def beenden():
    root.quit()

# Hauptfenster erstellen
root = tk.Tk()
root.title("Geschossenergie-Rechner  |  Version " + programmVersion + " (" + datumVersion + ")")
root.geometry("500x800")

style = ttk.Style()
style.theme_use('clam')
style.configure('TButton', background='lightgray', foreground='black', font=("Helvetica", 10, "bold"))  # Buttons Farbe geändert und Schrift fett

# Hauptframe
main_frame = ttk.Frame(root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)

# Allgemeine Informationen
info_frame = ttk.LabelFrame(main_frame, text="ALLGEMEINE INFORMATIONEN", padding="10")
info_frame.pack(fill=tk.X, pady=(0, 20))

ttk.Label(info_frame, text="Waffenname:  ").grid(column=0, row=0, sticky=tk.W, pady=5)
waffenname_entry = ttk.Entry(info_frame)
waffenname_entry.grid(column=1, row=0, sticky=(tk.W, tk.E), pady=5)

ttk.Label(info_frame, text="Munitionsname:  ").grid(column=0, row=1, sticky=tk.W, pady=5)
munitionsname_entry = ttk.Entry(info_frame)
munitionsname_entry.grid(column=1, row=1, sticky=(tk.W, tk.E), pady=5)

ttk.Label(info_frame, text="Geschossmasse (g):  ").grid(column=0, row=2, sticky=tk.W, pady=5)
geschossmasse_entry = ttk.Entry(info_frame, width=15)  # Breite auf ca. 30% gesetzt
geschossmasse_entry.grid(column=1, row=2, sticky=(tk.W), pady=5)

ttk.Label(info_frame, text="Notizen:  ").grid(column=0, row=3, sticky=tk.W, pady=5)
notizen_entry = ttk.Entry(info_frame)
notizen_entry.grid(column=1, row=3, sticky=(tk.W , tk.E), pady=5)

info_frame.columnconfigure(1 , weight=1)

# Geschwindigkeitsmessungen
geschwindigkeit_frame = ttk.LabelFrame(main_frame, text="GESCHWINDIGKEITSMESSUNG(EN)", padding="10")
geschwindigkeit_frame.pack(fill=tk.BOTH , expand=True)

geschwindigkeit_text = scrolledtext.ScrolledText(geschwindigkeit_frame, height=10)
geschwindigkeit_text.pack(fill=tk.BOTH , expand=True)
geschwindigkeit_text.insert(tk.END, "Die Anzahl der Geschwindigkeitsmessungen ist unbegrenzt. Gebe hier jeweils eine Geschwindigkeitsmessung (m/s) pro Zeile ein:")
geschwindigkeit_text.bind("<FocusIn>", clear_default_text)

# Berechnen Button
berechnen_button = ttk.Button(main_frame, text="Berechne Geschossenergie", command=berechne_geschossenergie)
berechnen_button.pack(fill=tk.X, pady=(10 , 20))

# Ergebnisbereich (statisch angezeigt)
ergebnis_frame = ttk.LabelFrame(main_frame, text="ERGEBNIS", padding="10")
ergebnis_frame.pack(fill=tk.X, pady=(0 , 20))

energie_label = ttk.Label(ergebnis_frame, text="", font=("Arial", 14, "bold"))
energie_label.pack(pady=(0 , 10))

ergebnis_label = ttk.Label(ergebnis_frame, text="")
ergebnis_label.pack()

# Untere Buttons
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(20 , 0))

zuruecksetzen_button = ttk.Button(button_frame, text="Eingaben zurücksetzen", command=zuruecksetzen)
zuruecksetzen_button.pack(side=tk.LEFT , fill=tk.X , expand=True , padx=(0 , 5))

beenden_button = ttk.Button(button_frame, text="Beenden", command=beenden)
beenden_button.pack(side=tk.RIGHT , fill=tk.X , expand=True , padx=(5 , 0))

# Kontaktinfo am unteren Rand hinzufügen
kontakt_label = ttk.Label(main_frame,
                          text="Kontakt: ",
                          font=("Helvetica", 8),
                          anchor='center')
kontakt_label.pack(side=tk.BOTTOM, pady=(10, 0))  # Abstand hinzugefügt

root.mainloop()