kosten = 25 # ganze Zahl (Int64)
gewicht = 2.5 # Kommazahl (Float64)
name = "Auftrag 1" # Zeichenkette (String)
zulaessig = true # Wahrheitswert (Bool)
typeof(kosten), typeof(gewicht)(Int64, Float64)
Grundlagen Operations Research
Diese Seite fasst die Julia-Grundlagen zusammen, die in den Vorlesungen und Übungen dieses Kurses vorkommen, nicht mehr und nicht weniger. Alle Zellen sind ausgeführt; am besten tippen Sie die Beispiele parallel in Ihrer eigenen REPL nach (siehe Installation & Setup).
Variablen entstehen durch einfache Zuweisung. Einen Typ müssen Sie nicht angeben, Julia leitet ihn selbst ab:
kosten = 25 # ganze Zahl (Int64)
gewicht = 2.5 # Kommazahl (Float64)
name = "Auftrag 1" # Zeichenkette (String)
zulaessig = true # Wahrheitswert (Bool)
typeof(kosten), typeof(gewicht)(Int64, Float64)
Gerechnet wird mit +, -, *, / und ^. Steht eine Zahl direkt vor einem Namen, ist das die Kurzform der Multiplikation; in den Modellen steht deshalb 20X1 statt 20 * X1. Alles hinter # ist ein Kommentar und wird nicht ausgeführt:
2 * kosten + 3gewicht # 3gewicht bedeutet 3 * gewicht57.5
In Zeichenketten können Sie Werte mit $ einsetzen (Interpolation). So geben wir im Kurs Ergebnisse aus:
println("$name kostet $kosten EUR und wiegt $gewicht Tonnen.")Auftrag 1 kostet 25 EUR und wiegt 2.5 Tonnen.
Ganze Ausdrücke setzen Sie mit $(…) ein. Dieselbe Zeile erhalten Sie, wenn Sie die Teile mit Kommas aneinanderreihen, wie in den Ausgabezeilen der Kurs-Modelle:
println("Mit Zuschlag: $(kosten + 5) EUR")
println("Mit Zuschlag: ", kosten + 5, " EUR")Mit Zuschlag: 30 EUR
Mit Zuschlag: 30 EUR
Kürzer geht es mit @show: Es gibt den Ausdruck zusammen mit seinem Wert aus, @show kosten + 5 also die Zeile kosten + 5 = 30. Das ist praktisch, um Ergebnisse schnell zu prüfen.
Vektoren schreibt man in eckigen Klammern. Wichtig: Julia zählt ab 1, nicht ab 0. Das erste Element ist v[1]:
d = [140, 110, 190, 120] # z. B. Nachfragen von 4 Perioden
d[1], d[end], length(d), sum(d)(140, 120, 4, 560)
Mit push! hängen Sie ein Element an, mit d[2:3] greifen Sie einen Ausschnitt heraus:
push!(d, 200) # das ! signalisiert: d wird verändert
d[2:3]2-element Vector{Int64}:
110
190
Matrizen trennen Spalten mit Leerzeichen und Zeilen mit Semikolon; der Zugriff erfolgt mit [Zeile, Spalte]:
c = [130 140 170; # z. B. Transportkosten:
210 150 130] # 2 Anbieter x 3 Nachfrager
c[2, 3]130
Ein Doppelpunkt allein steht für „alle“: c[1, :] ist die ganze erste Zeile.
c[1, :], sum(c[2, :]) # Zeile 1 und die Summe der Zeile 2([130, 140, 170], 490)
Ein Tupel fasst mehrere Werte in runden Klammern zusammen, im Kurs z. B. eine Verbindung zwischen zwei Knoten samt ihrer Länge. Der Zugriff funktioniert wie beim Vektor, ändern lässt sich ein Tupel aber nicht:
kante = (1, 2, 55) # von Knoten 1 nach Knoten 2, Länge 55
kante[3]55
Ein Tupel können Sie in einzelne Variablen zerlegen. Mit derselben Schreibweise weisen Sie mehrere Werte auf einmal zu; ein Semikolon trennt dagegen zwei eigenständige Anweisungen in einer Zeile:
von, nach, laenge = kante # Tupel zerlegen
f, h = 500, 1 # zwei Zuweisungen auf einmal
a = [15, 15]; b = [13, 11, 6] # zwei Anweisungen in einer Zeile
laenge, f, b(55, 500, [13, 11, 6])
Ein Dictionary (Dict) ordnet jedem Schlüssel einen Wert zu, hier jedem Vorgang seine Dauer. Sie greifen über den Schlüssel zu, keys liefert alle Schlüssel (ihre Reihenfolge ist nicht festgelegt):
dauer = Dict(1 => 3, 2 => 2, 3 => 3) # Vorgang => Dauer
dauer[2], keys(dauer)(2, [3, 2, 1])
Ein Bereich wie 1:5 steht für die Zahlen 1 bis 5. Das nutzen wir ständig für Indexmengen (z. B. „alle Perioden “):
T = 1:5
collect(T) # collect zeigt die Werte als Vektor5-element Vector{Int64}:
1
2
3
4
5
Ein Punkt vor einem Operator oder nach einem Funktionsnamen wendet die Operation elementweise auf einen Vektor an:
mengen = [10, 20, 15]
preise = [2.0, 1.5, 3.0]
erloes = mengen .* preise # elementweise multiplizieren3-element Vector{Float64}:
20.0
30.0
45.0
sum(erloes), maximum(erloes)(95.0, 45.0)
Das Gleiche funktioniert mit Funktionen, z. B. value.(X) in JuMP: „lies den Wert jeder Variablen im Container X“.
Fallunterscheidungen folgen dem Muster if / elseif / else und enden (wie alle Blöcke in Julia) mit end:
bestand = 4
if bestand == 0
println("Lager leer")
elseif bestand < 10
println("Lager fast leer: $bestand Stück")
else
println("Lager gefüllt")
endLager fast leer: 4 Stück
Vergleiche liefern Wahrheitswerte (==, !=, <, <=, >, >=), kombiniert wird mit && (und), || (oder) und ! (nicht).
Für kurze Entweder-oder-Fälle gibt es die Kurzform Bedingung ? A : B: Ist die Bedingung wahr, gilt A, sonst B.
meldung = bestand > 0 ? "lieferbar" : "ausverkauft""lieferbar"
In den Modellen steht sie z. B. als t == 1 ? 0 : I[t-1]: In Periode 1 gibt es noch keinen Lagerbestand aus der Vorperiode.
Eine for-Schleife läuft über einen Range oder Vektor:
for t in 1:3
println("Periode $t: Nachfrage = $(d[t])")
endPeriode 1: Nachfrage = 140
Periode 2: Nachfrage = 110
Periode 3: Nachfrage = 190
Besteht jedes Element aus einem Tupel, können Sie es direkt im Schleifenkopf zerlegen:
kanten = [(1, 2, 55), (1, 3, 15), (2, 5, 16)]
for (i, j, laenge) in kanten
println("Von $i nach $j: Länge $laenge")
endVon 1 nach 2: Länge 55
Von 1 nach 3: Länge 15
Von 2 nach 5: Länge 16
Sehr oft brauchen wir Summen mit Laufindex, genau wie das Summenzeichen in der Mathematik:
sum(d[t] for t in 1:4) # entspricht der Summe von d1 bis d4560
Diese Schreibweise werden Sie in jedem JuMP-Modell wiedersehen, z. B. in @objective(modell, Min, sum(f * X[t] for t in T)).
Hinter dem Laufindex kann eine Bedingung mit if stehen, dann zählen nur die passenden Elemente. Passt gar keines (von Knoten 9 geht keine Kante aus), gibt init an, was die leere Summe ergeben soll:
ab1 = sum(k[3] for k in kanten if k[1] == 1)
ab9 = sum(k[3] for k in kanten if k[1] == 9; init = 0)
ab1, ab9 # Summe der Längen ab Knoten 1 und ab Knoten 9(70, 0)
Mehrere Laufindizes trennen Sie mit Komma, das ergibt eine Doppelsumme:
sum(c[i, j] for i in 1:2, j in 1:3) # alle Einträge der Matrix c930
In eckigen Klammern entsteht aus derselben Schreibweise ein Vektor:
[t for t in 1:4 if d[t] > 115] # Perioden mit Nachfrage über 1153-element Vector{Int64}:
1
3
4
Zwei for hintereinander, wie in [s for z in Z for s in S], wirken wie zwei geschachtelte Schleifen.
Eigene Funktionen definieren Sie mit function … end; der letzte Ausdruck ist automatisch der Rückgabewert:
function deckungsbeitrag(erloes, kosten)
return erloes - kosten
end
deckungsbeitrag(500, 320)180
Für Einzeiler gibt es die Kurzform:
quadrat(x) = x^2
quadrat(9)81
Braucht die Kurzform doch mehrere Zeilen, stehen diese zwischen begin und end.
Manche Argumente haben einen Namen und stehen als name = wert am Ende des Aufrufs, hinter einem Semikolon oder Komma. So runden wir Ergebnisse:
round(2.4567; digits = 2), round(Int, 2.7) # 2 Stellen, ganze Zahl(2.46, 3)
Eine Funktion kann mehrere Werte zurückgeben, die Sie beim Aufruf wie ein Tupel zerlegen. Ein Unterstrich _ steht für einen Wert, den Sie nicht brauchen:
function spanne(v)
return minimum(v), maximum(v)
end
_, groesste = spanne(d)
groesste200
Die Selbstkontrollen der Übungen prüfen Ihr Ergebnis mit @assert: Ist die Bedingung dahinter wahr, passiert nichts; ist sie falsch, bricht Julia mit der angegebenen Meldung ab. Kommazahlen vergleichen wir dabei nicht mit ==, sondern mit isapprox und einer Toleranz (1e-4 steht für 0,0001):
ergebnis = 0.1 + 0.2 # Kommazahlen sind nur fast exakt
@assert isapprox(ergebnis, 0.3; atol = 1e-4) "Noch nicht korrekt."
println("Korrekt: ", ergebnis)Korrekt: 0.30000000000000004
Damit kennen Sie alle Sprachbausteine, die in den Kurs-Modellen vorkommen. Begegnet Ihnen dort eine unbekannte Funktion (etwa cld oder vcat), tippen Sie in der REPL ? und danach ihren Namen, dann zeigt Julia die Beschreibung an.
Weiter geht es mit Pakete & JuMP. Dort setzen wir daraus das erste Optimierungsmodell zusammen. Zum schnellen Nachschlagen dient das Cheatsheet Julia.