Performance Problem

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

ich benötige mal wieder eure Hilfe. Mit Performance habe ich mich noch nie intensiv beschäftigt, da das für mich noch nie ein Problem war. Jetzt habe ich aber einen Code erhalten, der ist sehr langsam. Mit messen habe ich dann herausgefunden, dass ein Problem darin liegt, dass Ergebnisse mit `append` ein er Liste hinzugefügt wurden. Die Messung hat auch gezeigt, dass mit steigender Listenlänge, die Zeit hierfür auch anstieg. Hier wird ja nichts angehängt, sondern jedes mal eine neue Liste erzeugt, richtig? Da der Zugriff bzw. das raussuchen bestimmter Ergebnisse mittels Indexzugriff auch noch Zeit beanspruchte, habe ich mir überlegt, was ich tun kann um das zu ändern.
Mit `pd.DataFrame` habe ich schöne Optionen und habe damit angefangen. Mein Problem ist, dass das nicht schneller ist.
Ich habe ein Minimalbeispiel geschrieben, die gewünschte Logik müsste darin gut erkennbar sein. Die Berechnung über die Letzten 3 Reihen ist wichtig. Für eine Darstellung im Diagramm benötige ich aber alle Reihen, daher kann ich die vorherigen nicht einfach verwerfen. Ziel ist, dass ich `results` , `mass`, `volume` und `general_results` in einem Objekt habe und dann je nach dem die gewünschten Werte ausgeben kann oder Diagramme erstellen.

Wie macht man so etwas schneller, wenn man die Berechnung selbst nicht ändern kann? Wie und wo speichere ich die Daten sinnvoll und schnell zwischen? In eine Datei schreiben? Bin schon über `h5py` gestolpert. `pandas` hatte für mich den Vorteil mit `groupy` weil ich dann so schön die Berechnung jeder Stufe abfragen kann und keine Schleife benötige. `groupy` sei laut StackOverflow allerdings nicht so schnell. Für mich bin ich jetzt nach sehr vielen Stunden des Code-Uschreibens an einem Punkt, an dem ich sehr dankbar über einen Wegweiser wäre.

Code: Alles auswählen

#!/usr/bin/env python
import dataclasses

import pandas as pd



@dataclasses.dataclass
class State:
    volume: float = 0
    mass: float = 0
    stage: int = 0


def get_important_results(results):
    last_rows = results.tail(3)
    mass = last_rows.groupby("stage")["mass"].sum()
    volume = last_rows.groupby("stage")["volume"].sum()
    return mass, volume


def main():
    # In reality there are many more values, as shown here
    states = [State() for _ in range(3)]
    result = pd.DataFrame([dataclasses.asdict(states[0])])
    for value in range(10):
        for stage, state in enumerate(states, 1):
            # Placeholder for calculating
            state.stage = stage
            state.volume = 300 + value * stage
            state.mass = 200 + value * stage
            result = pd.concat(
                [result, pd.DataFrame([dataclasses.asdict(state)])], ignore_index=True
            )
    mass, volume = get_important_results(result)
    general_results = pd.DataFrame([{"something": 123}])
    print(mass)
    print(mass[2])


if __name__ == "__main__":
    main()
Danke vorab und viele Grüße
Dennis

Edit: Auch die `dataclass` kann ich rauswerfen/ersetzen, die ist drin, weil sie halt drin war und ich noch nicht weiß ob das Sinn macht oder nicht.
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14362
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: `list.append()` erzeugt nicht jedes mal eine neue Liste, da wäre die Laufzeit unterirdisch. Das ist armotisiert O(1), das heisst es ist in der Regel eine sehr schnelle Operation, aber ab und zu muss der interne Speicher der Liste wachsen und dann muss gegebenfalls etwas umkopiert werden.@Dennis89: `list.append()` erzeugt nicht jedes mal eine neue Liste, da wäre die Laufzeit unterirdisch. Das ist armotisiert O(1), das heisst es ist in der Regel eine sehr schnelle Operation, aber ab und zu muss der interne Speicher der Liste wachsen und dann muss gegebenfalls etwas umkopiert werden.

Das mit dem `pd.concat()` ist langsam, weil da tatsächlich jedes mal ein neuer Dataframe in jedem Schleifendurchlauf erstellt wird. Da würde man die Daten erst in einer Liste speichern und dann zum Schluss das `Dataframe`-Objekt erstellen.

Was mir daneben so auf Anhieb auffällt ist das ``last_rows.groupby("stage")`` zweimal direkt hintereinander mit dem gleichen Ergebnis gemacht wird.

Kurzzeitiges Zwischenspeichern kann man mit `to_pickle()` machen. Sonst würde ich Parquet oder Feather als Speicherformat verwenden.

Das mit dem `pd.concat()` ist langsam, weil da tatsächlich jedes mal ein neuer Dataframe in jedem Schleifendurchlauf erstellt wird. Da würde man die Daten erst in einer Liste speichern und dann zum Schluss das `Dataframe`-Objekt erstellen.

Was mir daneben so auf Anhieb auffällt ist das ``last_rows.groupby("stage")`` zweimal direkt hintereinander mit dem gleichen Ergebnis gemacht wird.

Kurzzeitiges Zwischenspeichern kann man mit `to_pickle()` machen. Sonst würde ich Parquet oder Feather als Speicherformat verwenden.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die schnelle Antwort.
Du meinst ich hätte `last_rows.groupby("stage")[["mass", "volume"]].sum()` schreiben können um mir einen `groupby`-Aufruf zu sparen?

Ich teste mal die Variante, mit allem in eine Liste schreiben und dann ein `DataFrame` zu erzeugen und sollte dass nicht reichen, lese ich mich in die anderen 3 Optionen ein.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

nun muss ich das angehen, vor dem ich mich gedrückt habe, weil ich keine sinnvolle Lösung gefunden habe und das die größte Bremse ist.
Ich habe ein Konstrukt, dass soooooo *ähnlich* aussieht:

Code: Alles auswählen

class Results:
    def __init__(self):
        self.xy = []

    def __repr__(self):
        return f"Tadaa: {self.xy!r}"

def calculate(result, a, b):
    result.xy.append(a + b)


def main():
    values = [(1, 2), (3, 4), (5, 6)]
    results = [Results() for _ in values]
    for _ in range(10):
        for result, (a, b) in zip(results, values):
            calculate(result, a, b)
    for result in results:
        print(result)

if __name__ == '__main__':
    main()
Die drei Berechnungen in der zweiten `for`- Schleife könnten "gleichzeitig" gemacht werden, daher habe ich sowas probiert:

Code: Alles auswählen

from threading import Thread

class Results:
    def __init__(self):
        self.xy = []

    def __repr__(self):
        return f"Tadaa: {self.xy!r}"

def calculate(result, a, b):
    result.xy.append(a + b)


def main():
    values = [(1, 2), (3, 4), (5, 6)]
    results = [Results() for _ in values]
    for _ in range(10):
        stages = [(result, a, b) for result, (a, b) in zip(results, values)]
        thread = Thread(target=calculate, args=stages[0])
        thread.start()
        thread.join()
        calculate(*stages[1])
        calculate(*stages[2])
    for result in results:
        print(result)

if __name__ == '__main__':
    main()
Das auch mit zwei Threads und nur eine Berechnung im Hauptthread, allerdings ist das nicht merkbar schneller.

Habt ihr eine Idee, wie ich die innere Schleife schneller abgearbeitet bekomme? Die äußere Schleife muss in meinem Fall ca. 6000 mal durchlaufen, weil es da mathematische Model dahinter so benötigt und darauf habe ich keinen Einfluss. Ich habe ja in einem anderen Thread schon mal nach der Einbindung von Rust in Python gefragt, weil ich wusste das so ein Problem auf mich zu kommt. Allerdings denke ich, dass mein Problem mit Python und richtigen Programmierkenntnissen noch deutlich optimierbar ist und genau dass ist ja das was ich lernen will.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14362
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das mit dem Thread macht so keinen Sinn. Für das erste `stage`-Element wird ein Thread gestartet, der aber nicht mal theoretisch zu irgend etwas anderem parallel läuft, weil nach dem `start()` ja erst einmal mit `join()` gewartet wird bis der fertig ist. Das erzeugt an der Stelle einfach nur zusätzliche, überflüssige Mehrarbeit.

In CPython steht zudem noch das „global interpreter lock“ einer parallelen Verarbeitung von reinem Python-Code in Threads im Weg. Aber selbst wenn dem nicht so wäre: auch in C würde man keinen Thread für eine so popelig kleine ”Rechnung” starten, denn das Thread starten und danach wieder aufräumen braucht ja auch Zeit.

Multiprocessing wäre in CPython der Weg um parallel zu rechnen. Da ist das Hochziehen und Aufräumen aber _noch_ teurer als bei Theads. Dazu kommt dann auch noch die Kommunikation der Argumente und Ergebnisse zwischen den Prozessen.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Sirius3
User
Beiträge: 18384
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: Performance-Probleme zu diagnostizieren ist schwierig, wenn Du nicht den wirklichen Code zeigst.
Hier würde ich sagen, Du hast zwei Vektoren mit Zahlen, also nimmt man numpy und hat kein Geschwindigkeitsproblem.
Wahrscheinlich ist das Problem irgendwo anders.

Threads / Multiprocessing können helfen, aber das ist der letzte Schritt, wenn alles andere schon optimal läuft. Dann benutzt man aber sowas wie concurrent.futures und startet keine Prozesse von Hand.
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten.
Den originalen Code kann ich nicht öffentlich zeigen, der gehört nicht mir und enthält firmeninternes. Eine gekürzte Version könnte ich per PN zukommen lassen. Das ist dann aber keine Forums-Philisophie mehr.
Ich vermute dass der `numpy`- Ansatz funktioniert, allerdings sehe ich noch nicht wie, da der Code schon "wild" ist und jede Änderung Auswirkung auf "vorne" und "hinten" hat. Das ist super nervig, weil ich nach jeder Änderung den alten und den neuen Code laufen lassen muss um zu sehen, ob die Ergebnisse noch stimmen und die ständigen ~200 Sekunden warten, sind nicht so cool.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4648
Registriert: Freitag 17. April 2009, 10:28

@Dennis89: Deshalb hat man kleine Funktionen und dafür Tests.
Benutzeravatar
noisefloor
User
Beiträge: 4310
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

so richtig nachvollziehen kann ich das Problem auch nicht, weil ein bisschen abstrakt beschrieben... gut, wenn es interne Daten sind, die nicht 1:1 gepostet werden können, ist es halt so.

_Was_ ist denn eigentlich der Ursprung des Problems? Das Daten für einen Zeitraum von X Sekunden so hochfrequent dazu kommen, dass das zu Problemen führt? Dann wäre eine schnelle Speicherstruktur im RAM der Weg. Also Python Listen oder sowas wie z.B. Valkey oder Redis, wenn man die Daten noch asynchron im Hintergrund persistent machen will / muss. Oder eventuell auch SQLite mit einer in-memory DB.
Oder ist die Berechnung an sich das Problem? Wenn es die Rechnung an sich ist, dann würde ja ganz platt eine CPU mit hoher Single Thread Performance (z.B. Apple Silicon M Serie) was bringen oder ggf. auch einfach PyPy statt CPython oder der Einsatz von z.B. Numba als JIT-Zusatz zu CPython.

Pandas macht IMHO wenig Sinn, weil a) der Overhead relativ hoch ist (der Import dauert schon spürbar, selbst auf schnellen Rechnern) und b) die Stärke von Pandas ja zeit-basierte Daten und Datensätze mit Lücken / NaN Werten ist.

Kannst du vielleicht mal Kontext liefern, was das für Daten sind und wo die her kommen?

Gruß, noisefloor
Benutzeravatar
grubenfox
User
Beiträge: 651
Registriert: Freitag 2. Dezember 2022, 15:49

sparrow hat geschrieben: Donnerstag 19. März 2026, 22:21 @Dennis89: Deshalb hat man kleine Funktionen und dafür Tests.
Genau... und ideal wäre es wenn die kleinen Funktionen unabhängig von dem mathematischen Modell sind und man sagen kann 'wenn diese Funktion mit einer Handvoll Testdaten einmal alles richtig macht, dann macht sie auch bei den restlichen 5999 Durchläufen alles richtig'. Das hilft direkt erst mal nichts beim Performance-Problem vom endgültigen produktiven Durchlauf, aber wenn die Tests vielleicht nur noch 10 Sekunden brauchen, dann macht das ganze doch gleich mehr Spaß.

Ein schönes Beispiel ist da:

Code: Alles auswählen

def calculate(result, a, b):
    result.xy.append(a + b)
Da reicht fast ein Durchlauf mit der Liste

Code: Alles auswählen

[(1, 2), (3, 4), (5, 6)]
um zu testen ob die Funktion nach Codeänderungen noch das macht, was sie soll. Weil bei Listen im Zweifel die Reihenfolge bzw. eine Sortierung ein Thema sein könnte, würde ich auf jeden Fall noch einen Test mit z.b.

Code: Alles auswählen

[(4, 6) ,(5, 1), (3, 2)]
machen. Könnte ja sein dass beim `calculate` in den Anforderungen eine sortierte Liste als Ergebnis vorgesehen ist und im Code dann vergessen wurde. Naja, für's prüfen was bei anderen Eingabetypen als nur Integer passiert, da noch ein paar Test mit anderen Datentypen... je länger ich darüber nach denke, desto mehr Tests werden es.

Aber der wesentliche Punkt ist: um die Funktion der Funktion nach Code-Änderungen zu prüfen, muss man sie mit jedem Test-Datensatz nur einmal und nicht 6000mal aufrufen. Falls das so nicht machbar ist... hat man ein Problem. ;)

_______________________________________________________________________________
https://www.python-kurs.eu/index.php
https://learnxinyminutes.com/docs/python/

https://quickref.me/python https://docs.python-guide.org/
https://www.youtube.com/watch?v=qU3Rc6_B9es
Benutzeravatar
grubenfox
User
Beiträge: 651
Registriert: Freitag 2. Dezember 2022, 15:49

zum eigentlichen Thema Performance-Probleme kann ich leider wenig beitragen. Meine einzigen Performance-Probleme mit Python an die ich mich erinnern kann, ergaben sich zur Weihnachtszeit beim "Advent of Code" und da hatte ich das Programm einfach zwei oder dreimal ein paar Stunden durchlaufen lassen und war dann fertig.
Was noisefloor oben schrieb "PyPy statt CPython" kann ich bestätigen. Ich habe es selbst nur einmal ausprobiert: wenn die passenden Vorraussetzungen vorliegen, dann bringt PyPy erstaunlich viel bei Null Änderungen am Code!
Ansonsten das wichtigste beim Thema Performance: messen wo sich die Flaschenhälse den eigentlich verstecken...
Wo ich kürzlich drüber gestolpert bin: py-spy und https://codilime.com/blog/spying-on-python-with-py-spy/... sieht für mich cool aus (für die, die es mögen: ist in Rust geschrieben), vielleicht hilft es ja.

_______________________________________________________________________________
https://www.python-kurs.eu/index.php
https://learnxinyminutes.com/docs/python/

https://quickref.me/python https://docs.python-guide.org/
https://www.youtube.com/watch?v=qU3Rc6_B9es
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten.

Das mit den kleinen Funktionen und Tests ist mir bewusst, nur ist der Code nicht von mir und bis der auf einem Stand ist, muss er nach jeder Änderung komplett durchlaufen. Je nach dem wie geübt man ist, muss man das sicherlich nicht nach jeder kleinen Änderung machen, mir persönlich passieren aber immer mal wieder kleine Fehler und die will ich zum "Schluss" dann nicht noch suchen müssen.

Das Problem ist recht simple, es wird wie gezeigt Schleifen in Schleifen ausgeführt und immer wieder gerechnet und gerechnet und dass ist der Punkt, der die meiste Zeit beansprucht. Die Lösung wird hier ziemlich sicher, die von @Sirius3 angesprochene Methode sein. Zurzeit kämpfe ich damit, eine Struktur in den Code zu bekommen um besser erkennen zu können, was abhängig von den Schleifen berechnet wird und wie ich das dann ein einem Vektor darstelle.
`PyPy`und Co. ist mir bekannt und habe ich erst mal ausgeschlossen, genau so wie die Auslagerung von Berechnungen in eine andere Sprache, weil ich weiß, dass das in Python schneller geht und dann mag ich das auch hinbekommen. Zumindest will ich es nicht unversucht lassen.

Ich habe die Hoffnung, dass sich nach der Umstrukturierung auch ein besseres Minimalbeispiel ableiten lässt, das ich dann hier zeigen kann.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
noisefloor
User
Beiträge: 4310
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

"Schleifen in Schleifen" spricht schon dafür, dass PyPy oder Numba einen Boost bringen, ohne den Code zu ändern.

Ansonsten würde ich das auch so sehen, dass numpy und die Verwendung von numpy Arrays was bringen könnte.

Gruß, noisefloor
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

ich bin an dem Punkt angekommen, in dem ich mit Numpy-Arrays arbeiten kann und bin sehr zuversichtlich, dass das durchgängig funktioniert. Allerdings benutze ich das so selten und dadurch kann ich nicht jede Rechenoperation damit umsetzen. Ich habe zum Beispiel folgenden Code (Minimalbeispiel, aber das ist eine von den Rechnungen die so tatsächlich darin vorkommt)

Code: Alles auswählen

import math

RADIANT = 15
LENGTH = 200
VOLUMES_TO_ADD = [1, 2, 3]
AREAS = [100, 80, 50]
TOTAL_STEPS = 5762
STEPS_PER_REVOLUTION = 2880


def get_position(angle, radiant, length):
    sin_angle = math.sin(angle)
    cos_angle = math.cos(angle)
    projection = math.sqrt(length ** 2 - (radiant ** 2) * (sin_angle ** 2))
    return (radiant + length) - (radiant * cos_angle + projection)


def main():
    angle_step = 360 / STEPS_PER_REVOLUTION / 180 * math.pi
    angles = [step * angle_step for step in range(1, TOTAL_STEPS)]
    for angle in angles:
        for area, volume_to_add in zip(AREAS, VOLUMES_TO_ADD):
            position = get_position(angle, RADIANT, LENGTH)
            volume = volume_to_add + area * position
            print(volume)


if __name__ == '__main__':
    main()
Daraus habe ich folgendes gemacht:

Code: Alles auswählen

import numpy as np

RADIANT = 15
LENGTH = 200
VOLUMES_TO_ADD = [1, 2, 3]
AREAS = [100, 80, 50]
TOTAL_STEPS = 5762
STEPS_PER_REVOLUTION = 2880


def get_positions(angle, radiant, length):
    projection = np.sqrt(length ** 2 - (radiant ** 2) * (np.sin(angle) ** 2))
    return (radiant + length) - (radiant * np.cos(angle) + projection)


def main():
    total_steps = np.arange(1, TOTAL_STEPS)
    angles = total_steps * np.deg2rad(360 / STEPS_PER_REVOLUTION)
    positions = get_positions(angles, RADIANT, LENGTH)
    areas = np.asarray(AREAS)
    volumes_to_add = np.asarray(VOLUMES_TO_ADD)
    volumes = volumes_to_add + areas * positions


if __name__ == '__main__':
    main()
Das funktioniert nicht, weil `areas` 3 Werte enthält und `positions` 5761. Ist für mich auch verständlich und zumindest so habe ich die Fehlermeldung interpretiert:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/dennis/PycharmProjects/refactor/src/test.py", line 26, in <module>
    main()
    ~~~~^^
  File "/home/dennis/PycharmProjects/refactor/src/test.py", line 22, in main
    volumes = volumes_to_add + areas * positions
                               ~~~~~~^~~~~~~~~~~
ValueError: operands could not be broadcast together with shapes (3,) (5761,) 
Jetzt weiß ich gar nicht, wie ich das mit `numpy` ohne Schleife lösen kann. Ich denke aus der Schleifenbeispiel ist mein gewünschtes Ergebnis erkennbar. Für jede Fläche benötige ich das Volumen, dass sich aus den Positionen ergibt. In dem Fall habe ich 3 Räume, deren Volumen sich pro Schritt verändert und für jeden Raum benötige ich alle berechneten Volumina.

Ich denke, wenn ich das mit `Numpy` so durch die ganze Berechnungen durchgezogen bekomme, wir das richtig gut.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Ich habe parallel auch weiter gesucht und wenn ich die Doku von `np.resize` richtig verstehe, dann müsste das funktionieren:

Code: Alles auswählen

import numpy as np

RADIANT = 15
LENGTH = 200
VOLUMES_TO_ADD = [1, 2, 3]
AREAS = [100, 80, 50]
TOTAL_STEPS = 5762
STEPS_PER_REVOLUTION = 2880


def get_positions(angle, radiant, length):
    projection = np.sqrt(length ** 2 - (radiant ** 2) * (np.sin(angle) ** 2))
    return (radiant + length) - (radiant * np.cos(angle) + projection)


def main():
    total_steps = np.arange(1, TOTAL_STEPS)
    angles = total_steps * np.deg2rad(360 / STEPS_PER_REVOLUTION)
    positions = get_positions(angles, RADIANT, LENGTH)
    areas = np.resize(np.asarray(AREAS), positions.shape)
    volumes_to_add = np.resize(np.asarray(VOLUMES_TO_ADD), positions.shape)
    volumes = volumes_to_add + areas * positions


if __name__ == '__main__':
    main()
Wäre das auch der übliche `Numpy`-Weg?

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18384
Registriert: Sonntag 21. Oktober 2012, 17:20

Nein, wie scheiße ist falsch. Du möchtest ja nicht den Inhalt auf die Seite Größe wie deinen positions-Vektor duplizieren, sondern einen Spaltenvektor 1x3 mit einem Zeilenvektor nx1 multiplizieren. Dafür gibt es reshape.
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Danke, dann sieht das so aus und wenn ich die Zahlen etwas kleiner wähle und von Hand nachrechne, sieht das Ergebnis logisch für mich aus:

Code: Alles auswählen

import numpy as np

RADIANT = 10
LENGTH = 30
VOLUMES_TO_ADD = [1, 1, 1]
AREAS = [2, 1, 0.50]
TOTAL_STEPS = 5762
STEPS_PER_REVOLUTION = 2880


def get_positions(angle, radiant, length):
    projection = np.sqrt(length ** 2 - (radiant ** 2) * (np.sin(angle) ** 2))
    return (radiant + length) - (radiant * np.cos(angle) + projection)


def main():
    total_steps = np.arange(1, TOTAL_STEPS)
    angles = total_steps * np.deg2rad(360 / STEPS_PER_REVOLUTION)
    positions = get_positions(angles, RADIANT, LENGTH)
    positions = np.reshape(positions, (len(positions), 1))
    areas = np.reshape(np.asarray(AREAS), (1, len(AREAS)))
    volumes_to_add = np.reshape(np.asarray(VOLUMES_TO_ADD), (1, len(VOLUMES_TO_ADD)))
    volumes = volumes_to_add + areas * positions


if __name__ == '__main__':
    main()
Mein Gedanke war, "ich muss den kleineren Vektor größer machen und einfach die Werte wiederholen". Sehr gute Wiederholung was Mathematik an geht.

Btw. Ich habe mal im originalen Code alles auskommentiert und nur das was ich aktuell mit `Numpy` berechne, in der Schleife gelassen. Der Durchlauf der Schleifen geht ~1,7 Sekunden und die Vektor-Rechnung 0,01 Sekunden. Das macht Lust auf mehr.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18384
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: Statt np.arange und dann nachträglich auf die richtigen Werte zu skalieren, gibt es np.linspace, bei dem man den Start- und Endwert und die Anzahl angibt. Das ist meist viel leichter zu lesen, und dabei fällt mir auf, dass der Endwert irgendwie seltsam definiert ist.
Bei reshape kann man den magischen Wert -1 angeben, der die exakte Größe selbst ausrechnet. Das spart einen len-Aufruf.
Da aber eindimensional Vektoren als Zeilenvektor aufgefasst werden, muß man nur `positions` in einen Spaltenvektor umwandeln.
Wenn VOLUMES_TO_ADD immer 3mal den selben Wert hat, kann man natürlich auch einen Skalar nehmen.

Code: Alles auswählen

import numpy as np

RADIANT = 10
LENGTH = 30
VOLUMES_TO_ADD = 1  # [1, 1, 1]
AREAS = [2, 1, 0.50]
TOTAL_STEPS = 5762
STEPS_PER_REVOLUTION = 2880
MAX_ANGLE = 2 * np.pi * (TOTAL_STEPS - 1) / STEPS_PER_REVOLUTION


def get_positions(angle, radiant, length):
    projection = (length**2 - (radiant * np.sin(angle)) ** 2) ** 0.5
    return (radiant + length) - (radiant * np.cos(angle) + projection)


def main():
    angles = np.linspace(0, MAX_ANGLE, TOTAL_STEPS)[1:]
    positions = get_positions(angles, RADIANT, LENGTH).reshape(-1, 1)
    volumes = VOLUMES_TO_ADD + AREAS * positions


if __name__ == "__main__":
    main()
Benutzeravatar
Dennis89
User
Beiträge: 1717
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

danke für die Hinweise, das macht das Ganze gleich noch einmal übersichtlicher.
Bei `TOTAL_STEPS` meinst du, das es 5762 sind und nicht 5761? Das habe ich gemacht, weil ich `np.arrange` hatte und da hatte ich sonst die letzte Zahl nicht im Array. `VOLUMES_TO_ADD` ist in "Wirklichkeit" nicht drei mal 1, das hatte ich nur genommen um von Hand schneller nachrechnen zu können.

Eine weitere Rechnung folgt, nach dem ich `volumes` habe. Ich möchte immer zwei Werte aus diesem Array miteinander verrechnen. Wäre es eine Liste, würde ich `itertools.pairwise` verwenden. (Zum Verständnis, was ich mit zwei Werte meine). Die ersten zwei Werte werden mit einem Startwert verrechnet und die weiteren Paare jeweils mit dem Ergebnis aus der vorherigen Rechnung.

Ich habe mal versucht das zu "skizzieren":

Code: Alles auswählen

[ 1.1 ]
|     | \
|     |   > (1.1 / 1.2)^x * Start_wert = y  \
| 1.2 | /                                    \
|     | \                                     \   [start_wert]
|     |   > (1.2 / 1.3)^x * y = y1              > |    y1    |
| 1.3 | /                                     /   |    y2    |
|     | \                                    /    [    y3    ]
|     |   >  (1.3 / 1.4)^x * y1 = y2        /
[ 1.4 ] /
`x` ist eine Konstante und als Ergebnis benötige ich ein Array, beginnend mit dem Startwert und dann die Ergebnisse der einzelnen Rechnungen.
Geht das mit `numpy` auch oder benötige ich hier eine Schleife? Wenn es geht, was wären denn die Stichworte um danach zu suchen?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18384
Registriert: Sonntag 21. Oktober 2012, 17:20

Du mußt die Formel in seine Einzelteile zerlegen. Also zuerst die Elemente dividieren, in dem Du den Vektor um 1 versetzt:

Code: Alles auswählen

division = vector[:-1] / vector[1:]
Dann Potenzieren und zum Schluß mit cumprod die einzelnen Zahlen multiplizieren.

Code: Alles auswählen

result = np.cumprod(division**x)
Antworten