Elemente einer Liste in anderer Liste finden - Multiprocess?

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
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hast du eigentlich die Möglichkeit, PyPy zu installieren? Das ist ein optimierter Python-Interpreter, der unter anderem seine Vorteile ausspielen kann, wenn man relativ viele Schleifendurchläufe in seinem Programm hat. Solche Programme werden dann ohne Veränderung am Quelltext oft deutlich schneller. Nach dem Installieren musst du nur noch `pypy dein_programm.py` statt `python dein_programm.py` aufrufen und den Rest erledigt dann PyPy für dich.

Es ist natürlich keinesfalls verkehrt, den Algorithums an sich zu verbesseren, aber spätestens, wenn du wirklich in Versuchung kommst, die Arbeit auf mehrere Kerne aufzuteilen, würde ich einfach mal den Durchlauf mit PyPy probieren, ob der vielleicht schon schnell genug ist, sodass man sich weitere Arbeit ersparen kann.
Sirius3
User
Beiträge: 18334
Registriert: Sonntag 21. Oktober 2012, 17:20

@Stoli: dass Du Knoten-IDs und Hexagon-IDs erst mischst um sie dann über ihren Bereich wieder künstlich zu trennen, ist kein gutes Datendesign. Auch sollte jemand dem Spannungsberechnungstool beibringen, Trennzeichen zu verwenden. Die Zeilen mit dem »XA_i« ist ja jetzt auch nicht direkt Python. Auch kenne ich keine andere Programmiersprache, die ein »i« in einem Variablennamen als Zahl interpretiert.

Code: Alles auswählen

data="""   10120,     3549,        7,     1924,     5585,     5571,     1920,     5579,     6574
   10121,     1924,        5,     3548,     5585,     5579,     1922,     5578,     6574
   10122,     3548,      846,     3550,     5585,     5578,     1074,     3906,     6574
   10123,     3550,      847,     3549,     5585,     3906,     1075,     5571,     6574
   10124,     1927,        9,     1926,     5583,     5573,     1918,     5575,     7320
   10125,     1926,        6,     1923,     5583,     5575,     1921,     5580,     7320
   10126,     1923,        5,     1924,     5583,     5580,     1922,     5579,     7320
"""

node2hexaeder = collections.defaultdict(list)
for line in data.splitlines():
    row = map(int,line.split(','))
    hexaeder=row[0]
    for node in row[1:]:
        node2hexaeder[node].append(hexaeder)

XA = [-1.73932E-02, 8.24427E-02, 1.00211E-01, 1.22996E-01, 1.52645E-01, 1.88661E-01, 2.07566E-01, 2.40574E-01]
XA_schwelle = 1
for node_id, xa_value in enumerate(XA, 1):
    if xa_value < XA_schwelle:
        print node_id, node2hexaeder[node_id]
BlackJack

@Stoli: Also das mit den Textoperationen und das die Hexaeder-IDs da weitergehen wo die Knoten-IDs aufhören klingt für mich nach unsinniger Trickserei die das ganze unnötig undurchsichtig macht. Eine Hexaeder-ID kann man von Knoten-IDs doch schon eindeutig daran unterscheiden, dass sie an erster Stelle in jeder Zeile steht. Man muss die Informationen halt nur trennen wenn man die Datei einliest. Dann braucht man auch kein künstliches Komma am Ende einfügen was die Aufteilen der Daten unnötig *erschwert* weil am Ende ein leeres Element entsteht wenn man die Kommas als *Trenner* ansieht, so wie das Lösungen mit dem `csv`-Modul oder `str.split()` machen würden.

Sind die temporären Dateien für die XA-Werte und die Knoten-IDs der gefilterten XA-Werte tatsächlich nötig?

So wie es aussieht brauchst Du eine Abbildung von Knoten-IDs die jeweils die Knoten-ID auf alle Hexaeder-Daten abbildet in denen die Knoten-ID vorkommt. Das habe ich ja schon vor einigen Beiträgen vorgeschlagen und snafu hat entsprechenden Quelltext gepostet. Noch mal die Essenz davon:

Code: Alles auswählen

# 
# Build mapping of node id to hexaders which contain that node.
# 
node_id2hexaeders = defaultdict(list)
# 
# File contains one hexaeder per line as comma separated values.
# First value is the hexaeder id, followed by eight node ids.
# 
with open('hexaeders.txt') as lines:
    for line in lines:
        values = map(int, lines.split(','))
        for node_id in values[1:]:  # Skip hexaeder id in first column.
            node_id2hexaeders[node_id].append(tuple(values))

# Do something...

# node_ids = [...]  # Die Daten aus `b`.
hexaders = set()
for node_id in node_ids:
    hexaders.update(node_id2hexaeders[node_id])
Edit: Verdammt, zu spät. Aber das Du jetzt dreimal die gleiche Struktur vorgeschlagen bekommen hast, sollte Dir zu denken geben. :-)
Stoli
User
Beiträge: 17
Registriert: Donnerstag 24. Oktober 2013, 21:02

Hallo an alle Helfer :)!

Ich habe eine Kombination aus einigen Vorschlägen gewählt und bin somit zu einem beachtlichen Ergebnis gekommen.
Als Referenz:

Momentan berechne ich ein Modell bestehend aus 250k Knoten! (mit 2 CPU's) mit dem Programm:
  • Spannungsmodell berechnen dauert hier: ca. 126 sec -> werde das mal auf einem 8-Thread Prozessor rechnen, müsste dann weitaus schneller gehen :)
ab jetzt beginnt das Python Programm (läuft nur auf einem Prozessor):
  1. Berechnung der Spannungsmatrix 3x3: ca. 25 sec -> hier sehe ich noch Potential! muss das Script deswegen an dieser Stelle noch bearbeiten (das ist diese Komma-lose Output.txt aus der ich pro Zeile einen quantitativen Wert ermitteln will s. unten stehender Quellcode)
  2. Das Erstellen von Liste 'b' aus if xa_value < XA_schwelle: ca. 2,2 sec
  3. Update der Collection: ca. 0,03 sec
  4. Schreiben des neuen Input für das obere Programm: ca. 0,6 sec
Danke an alle! Das spart pro Loop - es sind ja ca. 1000 - für kleinere Modelle mit ca 8000 Knoten bereits 20sec :D


Das tmp_input.txt sieht ja folgendermaßen aus:

Code: Alles auswählen

 -1         1 3.84184E-05 3.84278E-05 1.23590E-05 1.04524E-05 2.44347E-06-2.43066E-06
 -1         2 2.99835E+03-9.01701E+04-7.49139E+03-2.51101E+04 1.14856E+02-9.13784E+03
 -1         3 1.52681E+04 7.13507E+03 6.72099E+03-5.53559E+03-5.62050E+02 1.20147E+03
 -1         4-1.54632E+04-7.26260E+03-6.81779E+03-5.62534E+03-5.62893E+02-1.21344E+03
 -1         5-4.52890E+03-7.54432E+02-1.84319E+02-7.18039E+02 3.75005E+01 1.95846E+02

Code: Alles auswählen

	d = open("tmp_input.txt", "r")
	f = open("tmp_out.txt" , "w")

num_lines = sum(1 for line in open('tmp_tensor.txt'))

for i in range(0,num_lines):
	# Lesen von S1
	d.seek(13+86*i)
	z_S1= d.read(12)
	S1= float(z_S1)

	# Lesen von S2
	d.seek(25+86*i)
	z_S2 = d.read(12)
	S2 = float(z_2)

	# Lesen von S3
	d.seek(37+86*i)
	z_3 = d.read(12)
	S3 = float(z_3)

	# Lesen von S12
	d.seek(49+86*i)
	z_12 = d.read(12)
	S12 = float(z_12)

	# Lesen von S23
	d.seek(61+86*i)
	z_S23 = d.read(12)
	S23 = float(z_23)

	# Lesen von S13
	d.seek(73+86*i)
	z_S13 = d.read(12)
	S13 = float(z_13)

	# Quantitativer Wert
	vm = (S1 ** 2 + S2 ** 2 + S3 ** 2 + S12 ** 2 + S23 ** 2 + S13 ** 2) #Fiktive Formel für's Forum

	f.write("{0:<2.5E}\n".format(vm))
# Schliessen der Datei
d.close()
f.close()
Ist es möglich das ganze anders zu gestalten? Wie gesagt, das tmp_input.txt ist nicht Komma-getrennt :/.

Ich sehe gerade, dass ich mir das f.write eigentlich sparen könnte, da ich im nächsten Schritt diese tmp_out.txt als Liste wieder weiterverarbeite. Man könnte deswegen die Werte vm in eine lst= [vm-Wert Zeile 1,vm-Wert Zeile 2,vm-Wert Zeile 3] schreiben. Allerdings ist wohl die Berechnung der einzelnen vm das Zeitaufwendige in dem Stück vom Code.

Viele Grüße
Stoli
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Stoli hat geschrieben:Allerdings ist wohl die Berechnung der einzelnen vm das Zeitaufwendige in dem Stück vom Code.
Das ist wahrscheinlich der kleinste zeitliche Beitrag von allen Schritten. Der Flaschenhals dürfte eher bei den ganzen Dateizugriffen liegen, die kosten viel mehr als die paar Berechnungen. Wenn du versuchst das zu parallelisieren, dann wirst du dir nur in den eigenen Fuß schießen. Durch die ganzen Sprünge wird es dann so richtig langsam. Versuch mal größere Blöcke zu am Stück zu lesen und diese dann mittels Slicing zu zerlegen, das sollte wesentlich schneller gehen.

Außerdem sind, bis auf den ersten, alle deine seek-Aufrufe überflüssig. Wenn du 12 Bytes liest, dann muss du nicht mehr 12 Bytes von der Startposition nach vorne springen, da befindest du dich längst. Dateien solltest du außerdem mittels with-Statement öffnen, dann musst du dich nicht mehr ums schließen kümmern.
Das Leben ist wie ein Tennisball.
BlackJack

@Stoli: Ich dachte das das zählen von Zeilen in einer Datei in einem extra Lesevorgang der gesamten Datei keine so gute Idee ist, hatten wir schon mal. ;-)

Zumal Du bei einer Datei zählst, dann aber eine andere mit der Zeilenanzahl verarbeitest. Die mögen ja immer gleich lang sein, aber verwirrend ist das schon ein bisschen. Ausserdem schliesst Du die Datei vom zählen nicht explizit. Da hast Du Glück, dass CPython in der jetzigen Implementierung das Dateiobjekt in der Regel relativ zeitnah bereinigt, und damit auch schliesst, aber garantiert ist dieses Verhalten von der Sprachspezifikation nicht.

Wie EyDu schon schrieb dürfte es wesentlich effizienter sein nicht für jeden Wert einzeln einen `seek()`- und einen `read()`-Aufruf abzusetzen. Und statt temporärer Dateien für jeden Schritt sollte man im Speicher bleiben, zum Beispiel in dem man wo immer das geht Generatorfunktionen schreibt, die man dann entsprechend verketten kann. Für den Schritt in Deinem letzten Quelltext könnte das zum Beispiel so aussehen:

Code: Alles auswählen

def calculate_quantitative_values(filename):
    with open(filename) as lines:
        for line in lines:
            s1, s2, s3, s12, s23, s13 = map(
                float, [line[i:i + 12] for i in xrange(13, 13 + 6 * 12, 12)]
            )
            # 
            # Fiktive Formel für's Forum.
            # 
            yield s1**2 + s2**2 + s3**2 + s12**2 + s23**2 + s13**2
Falls man das externe Programm welches ``tmp_input.txt`` schreibt, auch dazu bringen kann auf die Standardausgabe statt in eine Datei zu schreiben, könnte man sich diese Datei auch noch sparen und die Ausgabe über eine Pipe abgreifen.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Um die Zahlen zu parsen, würde sich auch ein regulärer Ausdruck anbieten, den man über die einzelnen Zeilen jagt. Denn egal, ob `.seek()` oder Slicing: Das sieht beides nicht allzu schön aus.
BlackJack

@snafu: Ein regulärer Ausdruck sieht hier auch nicht schön aus (eigentlich nirgends ;-)), und wäre das letzte woran ich bei einem Format mit fester Breite pro Wert denken würde. Ausser das Programm wäre in Perl geschrieben. Aber da denkt man ja auch bei jedem Problem mit Text erst an einen regulären Ausdruck und erst danach an eine vernünftige Lösung. :twisted:
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Naja, sehe ich anders. Ich würd's so in der Art machen:

Code: Alles auswählen

def get_values(data):
    p = re.compile(r'\d+\.\d+E[+-]\d+')
    # oder falls die Minuszeichen zwischen den Zahlen ins Ergebnis sollen:
    # p = re.compile(r'[-]?\d+\.\d+E[+-]\d+')
    for line in data:
        yield tuple(float(val) for val in p.findall(line))
Wobei `data` natürlich auch ein Dateiobjekt sein kann.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Wenn mir das Format genug Garantien gibt hinsichtlich des Aufbaues der Daten und der reguläre Ausdruck daraus nicht zu komplex wird, würde ich persönlich die `re`-Variante immer einer Slicing-Lösung mit kryptisch anmutenden Indexwerten vorziehen.

Gut, es soll Leute geben, die reguläre Ausdrücke fast immer schlecht lesbar finden. Ich find's in dem Fall noch akzeptabel und würde sicher auch noch in 3 Monaten verstehen, was da steht. Aber soll jeder machen, wie er meint.
Stoli
User
Beiträge: 17
Registriert: Donnerstag 24. Oktober 2013, 21:02

Habe jetzt den Hinweis von EyDu umgesetzt und das d.seek nur für die erste Zahl genommen und die anderen dann jeweils 12 Bytes davon weg. Das hat den Prozess bereits von 25 sec auf 10 sec reduziert. BlackJack's Variante hat das ganze nochmals auf ca 6 sec reduziert. :)
Werde die 're'-Variante von snafu mal testen!

Der ganze Code an sich funktioniert jetzt einwandfrei, es geht mir nur noch um die Performance. 1 sec bei 1000 Iterationen machen ja da schon knapp über eine viertelte Stunde aus; konnte bereits erheblich Zeit einsparen - ohne Multiprocessing. Da denke ich auch, dass das unnötig sein wird und wohl nicht viel Zeit einspart.

Wenn ich noch einen Flaschenhals finden sollte, melde ich mich ;).

Danke vielmals
Gruß
Stoli
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Stoli hat geschrieben:BlackJack's Variante hat das ganze nochmals auf ca 6 sec reduziert. :)
Werde die 're'-Variante von snafu mal testen!
Wenn es nur um die Laufzeit geht, wird Slicing wohl schneller sein, da sich dort ja rein auf die Positionsangaben verlassen wird, ohne dass geguckt werde müsste, was der tatsächliche Inhalt ist. Der reguläre Ausdruck hingegen zeigt (manchen zumindest) besser, was das erwartete Format für ein Muster hat. Wenn die Lesbarkeit egal ist bzw es für dich eh keinen wirklichen Unterschied macht, dann rate ich aus Performancegründen zu Slicing. Aber du kannst ja trotzdem gerne posten, was du an zeitlichen Unterschieden ausgemacht hast.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hier mal der Versuch, die Slicing-Methode etwas lesbarer zu gestalten:

Code: Alles auswählen

def get_values(data, value_length, values_per_line, offset=0):
    start = offset
    stop = value_length * values_per_line
    step = value_length
    for line in data:
        yield tuple(
            float(line[i : i + value_length])
            for i in xrange(start, stop, step)
        )

def run_test():
    value_length = 12
    values_per_line = 6
    offset = 12
    with open('tmp_input.txt') as infile:
        for record in get_values(infile, value_length, values_per_line, offset):
            print record


if __name__ == '__main__':
    run_test()
BlackJack

@snafu: Also ich finde den regulären Ausdruck unübersichtlicher, weil man den erst einmal lesen und verstehen muss. Beim Slicing und dem `map()` mit `float()` ist doch auch klar wie das Format aussieht. Man hat einen Offset und dann folgen 6 Zeichenketten die man in Gleitkommazahlen umwandeln kann mit jeweils genau 12 Zeichen.

„Zwischen” den Zahlen sind keine Minuszeichen. Das erste Zeichen ist immer das Vorzeigen und entweder ein Leerzeichen bei positiven Zahlen oder ein Minus bei negativen Zahlen. Zwischen den Werten ist also kein Trennzeichen.
Stoli
User
Beiträge: 17
Registriert: Donnerstag 24. Oktober 2013, 21:02

@ snafu, wie bereits gedacht dauert die 're' Variante bei dem jetzigen Modell ca 2 sek länger also knapp 1/3 länger als die von BlackJack.

Eine Frage habe ich noch an BlackJack:

Folgender Code sucht ja mittlerweile bei mir in der Liste nach Übereinstimmungen.

Code: Alles auswählen

hexaders = set()
for node_id in node_ids:
    hexaders.update(node_id2hexaeders[node_id])
Ist es möglich das so zu erweitern, dass pro Zeile in Liste 'a' 8 (oder eine beliebige Zahl zwischen 1 & 8 ) Elemente aus 'b' vorhanden sein müssen, dass diese Zeile rausgeschrieben wird in das neue input.txt ?

Grüße
Stoli
BlackJack

@Stoli: Ich weiss nicht ob ich es richtig verstanden habe, aber ich denke das müsste mit einem `collections.Counter` machbar sein statt des `set()`. Also alle Treffer für jeden Hexaeder-Datensatz zählen und dann die herausfiltern die mindestens x-mal vorkommen.

Ungetestet:

Code: Alles auswählen

    hexaeder2count = Counter()
    for node_id in node_ids:
        hexaeder2count.update(node_id2hexaeders[node_id])
    hexaeders = [h for h, n in hexaeder2count.iteritems() if n >= 8]
Stoli
User
Beiträge: 17
Registriert: Donnerstag 24. Oktober 2013, 21:02

Hervorragend! genau das habe ich gebraucht :) - hat auch einwandfrei funktioniert.

Viele Grüße
Stoli
Antworten