Numba, Wert eines Tupels in einer Funktion verändern

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
st3fan85
User
Beiträge: 8
Registriert: Mittwoch 12. August 2020, 10:15

Hallo,
ich probiere im Moment Numba ein wenig aus, um ein paar Simulationen eventuell zu beschleunigen. Allerding habe ich ein Verständnisproblem. Ich habe eine Liste mit Tupeln, jedes Tupel repräsentiert ein Element meiner Simulation. In diesem Tupel sind z.B. Koordinaten und Eigenschaften zu diesen Elementen gespeichert. Wenn ich die Tupel-Liste jetzt einer Funktion übergebe bekomm ich einen Fehler, sobald ich die Werte in einem Tupel verändern möchte. Ich versteh aber nicht, wieso ich die Werte in den Tupeln nicht verändern kann. Gibt es dazu einen Trick?

Code:

Code: Alles auswählen

@nb.njit
def IsDropletGrowth(x, y, Droplets, ND):
    dropletgrow = False
    for i in range(ND):
        if (math.sqrt(math.pow(Droplets[i][0] - x, 2) + math.pow(Droplets[i][1] - y, 2)) <= Droplets[i][3]):
            Droplets[i][2] += 1
            Droplets[i][3] = calc_radius(Droplets[i][2])
            Droplets[i][4] = calc_Gibbs_Thomson(Droplets[i][2])
            dropletgrow = True
        
        return dropletgrow, Droplets

nucleation, Monomers, Droplets, SimField, ND, N1 = IsNucleation(x, y, Monomers, Droplets, SimField, SimSize, ND, N1)            
    if not nucleation:
        dropletgrow, Droplets = IsDropletGrowth(x, y, Droplets, ND)
        if not dropletgrow:
            SimField[x][y] += 1
            Monomers.append((x,y))
            N1 += 1       
Wenn ich den Code mit der Funktion so ausführe kommt folgende Fehlermeldung:
runfile('C:/Simulationen/Skripte/Python/kMC_DropletsV2.py', wdir='C:/Simulationen/Skripte/Python')
Traceback (most recent call last):

File "C:\Simulationen\Skripte\Python\kMC_DropletsV2.py", line 245, in <module>
kMC(SimSize, Dif, Det, t_stop)

File "C:\Users\st3fa\anaconda3\lib\site-packages\numba\core\dispatcher.py", line 401, in _compile_for_args
error_rewrite(e, 'typing')

File "C:\Users\st3fa\anaconda3\lib\site-packages\numba\core\dispatcher.py", line 344, in error_rewrite
reraise(type(e), e, None)

File "C:\Users\st3fa\anaconda3\lib\site-packages\numba\core\utils.py", line 80, in reraise
raise value.with_traceback(tb)

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
Invalid use of Function(<built-in function setitem>) with argument(s) of type(s): (UniTuple(float64 x 5), Literal[int](2), float64)
* parameterized
In definition 0:
All templates rejected with literals.
In definition 1:
All templates rejected without literals.
In definition 2:
All templates rejected with literals.
In definition 3:
All templates rejected without literals.
In definition 4:
All templates rejected with literals.
In definition 5:
All templates rejected without literals.
In definition 6:
All templates rejected with literals.
In definition 7:
All templates rejected without literals.
In definition 8:
All templates rejected with literals.
In definition 9:
All templates rejected without literals.
This error is usually caused by passing an argument of a type that is unsupported by the named function.
[1] During: typing of staticsetitem at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (100)

File "kMC_DropletsV2.py", line 100:
def IsDropletGrowth(x, y, Droplets, ND):
<source elided>
if (math.sqrt(math.pow(Droplets[0] - x, 2) + math.pow(Droplets[1] - y, 2)) <= Droplets[3]):
#Droplets[2] += 1
^

[1] During: resolving callee type: type(CPUDispatcher(<function IsDropletGrowth at 0x0000020A62B6A828>))
[2] During: typing of call at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (140)

[3] During: resolving callee type: type(CPUDispatcher(<function IsDropletGrowth at 0x0000020A62B6A828>))
[4] During: typing of call at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (140)


File "kMC_DropletsV2.py", line 140:
def AddMonomerTo(x, y, Monomers, Droplets, SimField, SimSize, ND, N1):
<source elided>
if nucleation==False:
dropletgrow, Droplets = IsDropletGrowth(x, y, Droplets, ND)
^

[1] During: resolving callee type: type(CPUDispatcher(<function AddMonomerTo at 0x0000020A62B6A288>))
[2] During: typing of call at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (208)

[3] During: resolving callee type: type(CPUDispatcher(<function AddMonomerTo at 0x0000020A62B6A288>))
[4] During: typing of call at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (208)

[5] During: resolving callee type: type(CPUDispatcher(<function AddMonomerTo at 0x0000020A62B6A288>))
[6] During: typing of call at C:\Simulationen\Skripte\Python\kMC_DropletsV2.py (208)


Sobald ich diese Zeilen raus kommentiere startet das Programm ohne Fehler:

Code: Alles auswählen

Droplets[i][2] += 1
Droplets[i][3] = calc_radius(Droplets[i][2])
Droplets[i][4] = calc_Gibbs_Thomson(Droplets[i][2])
Vielen Dank für Eure Hilfe und bitte steinigt mich nicht gleich, ich bin noch Anfänger.

Viele Grüße
Stefan
NPC
User
Beiträge: 54
Registriert: Dienstag 8. Januar 2019, 17:51

Servus,

Ich hab es mir jetzt noch nicht so genau angesehen aber das:
st3fan85 hat geschrieben: Mittwoch 12. August 2020, 10:44 Ich versteh aber nicht, wieso ich die Werte in den Tupeln nicht verändern kann
scheint mir schon das Problem zu sein.
Ein Tuple, unabhängig von Numba, untersützt keine Änderung der items.
Wenn man das in Python versucht (ohne Numba) bekommt man folgenden Fehler:

Code: Alles auswählen

Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    a[0] = 4
TypeError: 'tuple' object does not support item assignment
Wenn du etwas brauchst, wo du die Einträge ändern kannst, dann solltest du eine List/Np.Array o.ä nehmen

Viele Grüße
NPC
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

@st3fan85: Variablen und Funktionsnamen schreibt man komplett klein. Die Einrückungen sind kaputt, so dass man den Code kaum verstehen kann.
Beim if hat es ein paar Klammern zu viel. Statt die Methoden aus dem math-Modul zu verwenden, verwende die passenden Operatoren **.
ND ist überflüssig, da es doch hoffentlich die Länge von Droplets ist.
Zur Zeit sieht es doch so aus, als Droplets ein ND x 5 - Array ist. Warum hast Du das als Tuple definiert? Wie schon von NPC erwähnt sind Tuple unveränderlich.

Wenn droplets schon als Argument übergeben wird, dann braucht man es nicht als Rückgabewert. Das ist sogar verwirrend, da man nun nicht weiß, ob der Input verändert wird oder nicht.
Dass die Funktion is_xxx heißt, läßt das auch nicht vermuten. Also braucht es einen passenderen Namen.

Code: Alles auswählen

@nb.njit
def is_droplet_growth(x, y, droplets):
    dropletgrow = False
    for i in range(len(droplets)):
        if (droplets[i][0] - x) ** 2 + (droplets[i][1] - y) ** 2 <= droplets[i][3] ** 2:
            droplets[i][2] += 1
            droplets[i][3] = calc_radius(droplets[i][2])
            droplets[i][4] = calc_gibbs_thomson(droplets[i][2])
            dropletgrow = True
    return dropletgrow
Besser wäre auch, den Index erst gar nicht zu benutzen:

Code: Alles auswählen

@nb.njit
def is_droplet_growth(x, y, droplets):
    dropletgrow = False
    for droplet in droplets:
        if (droplet[0] - x) ** 2 + (droplet[1] - y) ** 2 <= droplet[3] ** 2:
            droplet[2] += 1
            droplet[3] = calc_radius(droplet[2])
            droplet[4] = calc_gibbs_thomson(droplet[2])
            dropletgrow = True
    return dropletgrow
einfachTobi
User
Beiträge: 491
Registriert: Mittwoch 13. November 2019, 08:38

Ebenfalls unabhängig von Numba scheint es mir so, als könntest du deine Berechnung erheblich beschleunigen, wenn du die Funktionen von Numpy nutzen würdest. Die Prüfung und die Berechnung könnten für alle `droplets` im Array ausgeführt werden, statt diese mit einer Schleife zu durchlaufen. Vermutlich ist das in anderen Funktionen ebenfalls so. Ich würde also zunächst schauen, dass die Algorithmen verbessert werden und dann an die Optimierung via Numba gehen. Meist bringt es _wesentlich_ mehr einen schnellen Algorithmus zu verwenden, als einen langsamen Algorithmus zu beschleunigen.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Um den Punkt von einfachTobi aufzunehmen: wie sehen denn `calc_radius` und `calc_gibbs_thomson` aus?
Kann man die so umschreiben, dass sie auch mit Vektoren arbeiten?
Das ganze in vektorisierter Form sähe dann so aus (jetzt auch mit besserem Namen):

Code: Alles auswählen

def grow_droplets(x, y, droplets):
    mask = (droplets[:, 0] - x) ** 2 + (droplets[:, 1] - y) ** 2 <= droplets[:, 3] ** 2
    if not mask.any():
        return False
    droplets[mask, 2] += 1
    droplets[mask, 3] = calc_radius(droplets[mask, 2])
    droplets[mask, 4] = calc_gibbs_thomson(droplets[mask, 2])
    return True
@einfachTobi: Maskenoperationen sind recht langsam, so dass es Sinn machen kann, wenn es wirklich nötig ist, das mit Indizes in numba zu schreiben. Das ist aber wirklich nur dann nötig, wenn das das ganze Programm entscheidend beschleunigt. Lesbarkeit steht immer an erster Stelle.
st3fan85
User
Beiträge: 8
Registriert: Mittwoch 12. August 2020, 10:15

Hallo,
vielen Dank für die vielen Hilfen. Ich versuch mal alle Fragen zu beantworten.
@Sirius3
>Statt die Methoden aus dem math-Modul zu verwenden, verwende die passenden Operatoren **.
Gibt es zwischen den Operatoren und den math-Modul keinen Unterschied? Vielleicht im Bereich der Geschwindigkeit?

>ND ist überflüssig, da es doch hoffentlich die Länge von Droplets ist.
ND ist die Anzahl der Droplets, also die Anzahl der Tupel in der Droplets-Liste. Ich hatte mir überlegt, dass eine Variable mit der Länge der Liste einfach schneller ist, als immer die Länge bei Gebrauch neu abzufragen.

>Zur Zeit sieht es doch so aus, als Droplets ein ND x 5 - Array ist.
Im Grunde stimmt das so. Nur das die x5 in dem Tupel sind, sodass es ein ND x 1 Array ist.

>Warum hast Du das als Tuple definiert?
Einfach, da Numba Listen mit Tupeln als Übergabewerte unterstützt. Wäre ein Numpy-array mit NP x 5 effizienter?

>Wie schon von NPC erwähnt sind Tuple unveränderlich.
Ich habe das jetzt so gelöst, dass ich das alte Tupel lösche und ein neues, mit den aktualisierten Werten, in die Liste einfüge.

>Wenn droplets schon als Argument übergeben wird, dann braucht man es nicht als Rückgabewert.
Ich versteh nicht, warum ich sie nicht als Rückgabewert brauche. In der Funktion wird getestet, ob es zu einem Wachstum eines Tropfens kommt. Wenn es zu einem Wachstum kommt, dann wird die Liste Droplets verändert und muss doch wieder als Rückgabewert übergeben werden?

>Kann man die so umschreiben, dass sie auch mit Vektoren arbeiten?
Ich habe deine Befehle aus Deinem Code angeguckt und habe folgende Probleme:

Code: Alles auswählen

mask = (droplets[:, 0] - x) ** 2 + (droplets[:, 1] - y) ** 2 <= droplets[:, 3] ** 2
Bei dem Code bekomm ich folgende Fehlermeldung:
TypeError: list indices must be integers or slices, not tuple
Gebe ich den Befehl in der Form an:

Code: Alles auswählen

[code]mask = (droplets[:][0] - x) ** 2 + (droplets[:][1] - y) ** 2 <= droplets[:][3] ** 2
[/code]
Fehler:
TypeError: unsupported operand type(s) for -: 'tuple' and 'int'
droplets[:] gibt die Tupel Liste und der zweite Index ein Tupel aus der Liste, sodass ich ein dritten Index bräuchte, was wieder zum Iterieren führt.

Ich habe die Funktion jetzt so abgeändert:

Code: Alles auswählen

#@nb.njit
def is_droplet_growth(x, y, Droplets, Det):
    dropletgrow = False
    for droplet in Droplets:
        if (droplet[0] - x)**2 + (droplet[1] - y)**2 <= droplet[3]**2:
            x1 = Droplets[0]
            y1 = Droplets[1]
            V1 = Droplets[2] + 1
            r1 = calc_radius(V1)
            RE1 = 2 * Det * math.pi * r1 * calc_Gibbs_Thomson(V1)
            new_droplet = (x1, y1, V1, r1, RE1)
            Droplets.remove(droplet)
            Droplets.append(new_droplet)
            dropletgrow = True
            return dropletgrow, Droplets 
        
    return dropletgrow, Droplets
  
@einfachTobi
>Die Prüfung und die Berechnung könnten für alle `droplets` im Array ausgeführt werden
Ist es da nicht schneller per Schleife ein Droplet zu finden, das die Bedingung erfüllt und danach die Schleife zu beenden, anstatt jedes Mal die Bedingung für alle Droplets durchführen zu müssen? Zumeinen Numba Schleifen sehr stark beschleunigen soll.

P.S.
Es wurde noch nach den Funktionen calc_radius und calc_Gibbs_Thomson gefragt:

Code: Alles auswählen

@nb.njit
def calc_radius(V):
    r = math.pow(1.5*V*math.pi, 1/3.0)
    if r<1:
        return 1
    else:
        return r
 
@nb.njit
def calc_Gibbs_Thomson(V):
    VNA = 10.0e-6/N_A
    Vm = V * VNA
    rm = math.pow(1.5*Vm/math.pi, 1/3.0)
    rx = 2 * 0.89 * VNA / (k * (300+273))
    return np.exp(rx/rm)
In die Funktionen wird das Volumen (int) gegeben und daraus ein Faktor berechnet.
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du versuchst an Stellen zu optimieren, ohne wirklich Ahnung zu haben, ob und was das bringt. Man sollte zwar nicht zB falsche Algorithmen nutzen, aber statt davon auszugehen, dass irgendwelche Mikrooptimierung ala "ich zieh mal alles in eine eigene Variable" etwas bringen, muss man *messen*, ob sie was bringen. Auch gibt es keinen Grund anzunehmen, die math-Funktionen waeren schneller. Im Gegenteil, der Funktionsaufruf ist im Zweifel *minimalst* langsamer. Aber auch das ist irrelevant.

Und wenn man ein Objekt modifiziert, modifiziert man es. Dann braucht man es nicht zurueck zu geben. Siehe zB

Code: Alles auswählen

def foo(eine_liste):
    eine_liste.append("holla die Waldfee")

dinge = ["a", "b"]
foo(dinge)
print(dinge)
Und es ist ein schlechter Entwurf, eine Funktion ein Argument nehmen zu lassen, dass es dann veraendert und gleichzeitig zurueck gibt. Das weckt falsche Erwartungen, denn wenn irgendwo steht

Code: Alles auswählen

 bar = foo(baz)
dann gehe ich davon aus, dass bar etwas neues ist, und nicht das gute alte baz, das jetzt aber veraendert ist.

Auch deine letzte Frage ist wieder so ein "ich glaube, das ist schneller Ding". Woher glaubst du das zu wissen? Und hast du es verglichen? Sowas meint man nicht, man misst es.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

@st3fan85: was soll die Funktion denn tun? Wie wird sie benutzt?
st3fan85
User
Beiträge: 8
Registriert: Mittwoch 12. August 2020, 10:15

Das ist verrückt wie anders Python zu anderen Sprachen ist. Listen werden immer per Referenz an Funktionen übergeben aber Variablen nicht. Hab das gleich mal verändert.

@Sirius3
Insgesamt soll der Prozess der Tropfenbildung simuliert werden, anhand einer Monte-Carlo-Simulation.
Ich habe insgesamt zwei Listen: Droplets und Monomers. Droplets repräsentiert die entstandenen Tropfen auf einer Oberfläche und Monomers die Liste mit den vorhandenen Atomen auf der Oberfläche. Zusätzlich habe ich ein 2D Array mit der Größe an Absorptionsplätzen für Atome. Dieses 2D Array hat entweder 0 oder 1 als Wert und repräsentiert die Position der Monomere auf der Oberfläche. Dieses Array wird verwendet für die Überprüfung von Nachbarn, denn sobald ein Monomer ein anderes Monomer als direkten Nachbarn hat bildet sich ein Tropfen mit Größe 2 und wird in die Droplets-Liste eingefügt. Wenn kein direkter Nachbar vorhanden ist, wird mit der Funktion "is_droplet_growth" geprüft, ob sich das Monomer in der Nähe eines Tropfens befindet, wenn ja, dann vergrößert sich der Tropfen, sonst findet nur ein Platzwechsel des Monomers statt.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Python verhält sich hier wie fast alle anderen populären Sprachen (Java, Javascript, C#, übliches C++). Und Listen verhalten sich dort nicht anders als andere Variablen (aka Objekte).

Deine Beschreibung passt überhaupt nicht zum Code, den Du da zeigst.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@st3fan85: Übergabe von Argumenten und Zuweisung an Namen ist bei Python immer gleich. Listen werden da nicht anders behandelt als andere Objekte. Das ist immer „by object sharing“. Python kopiert dabei nie von sich aus Werte, es werden immer Referenzen übergeben/zugewiesen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
st3fan85
User
Beiträge: 8
Registriert: Mittwoch 12. August 2020, 10:15

Der Code den ich da zeige ist nur eine Methode des gesamten Programms.
Ich gebe einmal einen groben Überblick was die Struktur des Programms ist:
Die drei wichtigsten Methoden sind add_monomer_to, is_nucleation, is_groplet_growth
Immer wenn ein Monomer bewegt wird oder ein neues erstellt wird, wird add_monomer_to aufgerufen. Dort wird is_nucleation aufgerufen. is_nucleation überprüft, ob auf der neuen Position des Monomers ein direkter Nachbar zu einem anderen Monomer existiert. Wenn ja, dann wird ein Tropfen gebildet, wenn nein, passiert nichts.
Kommt es zu keiner Nucleation, startet die Methode is_droplet_growth. Dort wird überprüft, ob die neue Position in der Reichweite eines schon bestehenden Tropfens ist.
Ist das auch nicht der Fall wird ein neues Monomer erstellt.

Die Methode cyclic() gewährleistet zyklische Koordinaten.

Die Methode kMC ist die Hauptmethode, dort werden, per Zufall, der Prozess ausgewählt, der in dieser Runde stattfinden soll. Es wird zwischen Diffusion, Ablagern von Monomeren aus Tropfen und Absorption entschieden. kMC wird insgesamt nur einmal ausgeführt, zum Start werden hier auch die Listen für Monomere, Droplets, usw. erstellt, danach wird eine Schleife gestartet, in der die Simulation stattfindet.

Code: Alles auswählen

from numba.typed import List
import numpy as np
import numba as nb
import math
import time

#@nb.njit
def is_nucleation(x, y, Monomers, Droplets, sim_field, sim_size, N):
    nucleation = False
    
    if sim_field[x][y] > 0:
        x1 = x
        y1 = y
        nucleation = True
        
    elif sim_field[cyclic(x + 1, sim_size)][y] > 0:
        x1 = cyclic(x + 1, sim_size)
        y1 = y
        nucleation = True
        
    elif sim_field[x][cyclic(y - 1, sim_size)] > 0:
        x1 = x
        y1 = cyclic(y - 1, sim_size)
        nucleation = True
        
    elif sim_field[cyclic(x - 1, sim_size)][y] > 0:
        x1 = cyclic(x - 1, sim_size)
        y1 = y
        nucleation = True
        
    elif sim_field[x][cyclic(y + 1, sim_size)] > 0:
        x1 = x
        y1 = cyclic(y + 1, sim_size)
        nucleation = True
    
    if nucleation:
        sim_field = del_monomer(x1, y1, sim_field, Monomers, N)
        new_drop(x1, y1, Droplets, N)
        
    return nucleation, sim_field


#@nb.njit
def is_droplet_growth(x, y, Droplets, Det):
    dropletgrow = False
    for droplet in Droplets:
        if (droplet[0] - x)**2 + (droplet[1] - y)**2 <= droplet[3]**2:
            x1 = droplet[0]
            y1 = droplet[1]
            V1 = droplet[2] + 1
            r1 = calc_radius(V1)
            RE1 = 2 * Det * math.pi * r1 * calc_Gibbs_Thomson(V1)
            new_droplet = (x1, y1, V1, r1, RE1)
            Droplets.remove(droplet)
            Droplets.append(new_droplet)
            dropletgrow = True
            return dropletgrow, Droplets
        
    return dropletgrow


#@nb.njit
def add_monomer_to(x, y, Monomers, Droplets, sim_field, sim_size, N, Det):
    
    nucleation, sim_field = is_nucleation(x, y, Monomers, Droplets, sim_field, sim_size, N)       
    if not nucleation:
        dropletgrow = is_droplet_growth(x, y, Droplets, Det)
        if not dropletgrow:
            sim_field[x][y] += 1
            Monomers.append((x,y))
            N[0] += 1
    
    return sim_field

    
#@nb.njit
def kMC(sim_size, Dif, Det, t_stop):
	#Erstellen von Listen wie Monomers, Droplets, SimField, rates, usw.
	Droplets = List()
	Monomers = List()
	sim_field = np.zeros(sim_size, sim_size)
	...
	
	while(t <= t_stop):
		#aktualisierung der Prozessraten
		#Auswahl des Prozesses aus Diffusion, Detachment oder Absorption, abhängig von den Raten der Prozessen.
		if (Diffusion):
			#Zufällige Auswahl eines Monomers und zufälliger Weg (oben, unten, rechts oder links)
			add_monomer_to(x, y, Monomers, Droplets, sim_field, sim_size, N, Det)
		
		elif (Detachment):
			#Auswahl des Tropfens, der ein Atom verlieren soll.
			#Aktualisierung der Werte des Tropfens
			#Zufällige Richtung, in der das Atom vom Tropfen abgegeben wird.
			add_monomer_to(x, y, Monomers, Droplets, sim_field, sim_size, N, Det)
			
		else: (Absorption)
			# Auswahl einer zufälligen Koordinate im Intervall [0, sim_size-1] für x,y
			add_monomer_to(x, y, Monomers, Droplets, sim_field, sim_size, N, Det)
			
#Definition der Prozessparameter und physikalischen Konstanten

kMC(sim_size, Dif, Det, t_stop):
		
	
Das Programm läuft schon ganz gut, nur ist es langsam und die Unterstützung von Numba funktioniert auch noch nicht. Es sind noch Aktionen drin, die Numba nicht unterstützt.
Das Ganze geht auf ein Paper zurück, das ich gelesen habe:
https://pubs.acs.org/doi/abs/10.1021/acs.iecr.0c01069
Antworten