5  Alle auf einmal zufriedenstellen

In Kapitel 4 ging der Weg von der Verschiebung zur Kraft: Man gibt der Steifigkeitsmatrix \(K\) einen Verschiebungsvektor \(u\), sie liefert den Kraftvektor \(f = K\,u\). Jetzt drehen wir die Frage um. Von der Ferse her ist sie sogar die natürlichere: Die Kräfte kenne ich — den Boden, die Sehne, die Faszie. Gesucht ist, wie weit sich jeder Knoten bewegt. Also nicht mehr \(f\) aus \(u\) berechnen, sondern \(u\) aus \(f\): die Gleichung \(K\,u = f\) nach \(u\) auflösen.

Raten hilft nicht. Jede Feder koppelt ihre Knoten, und man kann keinen einzeln zufriedenstellen — verschiebt man den einen, gerät der Nachbar aus dem Gleichgewicht. Gesucht ist die eine Kombination von Verschiebungen, die alle Federn gleichzeitig ins Gleichgewicht bringt. Ein solches Bündel gekoppelter Gleichungen heißt im Studium lineares Gleichungssystem, und es gibt ein Verfahren, das seine Lösung sicher findet: die Gauß-Elimination.

Lernziele

Nach diesem Kapitel kannst du

  1. ein lineares Gleichungssystem als „alle Knoten gleichzeitig zufriedenstellen” deuten und \(K\,u = f\) als Aufgabe „gesucht: \(u\)” lesen,
  2. ein 3×3-System mit Gauß-Elimination von Hand lösen (eliminieren zur Dreiecksform, dann rückwärts einsetzen),
  3. erklären, warum eine feste Einspannung nötig ist, damit das System überhaupt eine eindeutige Lösung hat,
  4. benennen, warum große Systeme nicht „ausmultipliziert”, sondern mit spezialisierten Lösern und dünn besetzten Matrizen gelöst werden.

Bevor die erste Grafik entsteht, wird das Rechenwerk dieses Kapitels eingerichtet: die Kette aus drei Federn mit ihren didaktischen Steifigkeiten, die Zeichenwerkzeuge für Federkette, Matrizen und Eliminationstreppe, die deutsche Zahlendarstellung, die alle Bilder und Tabellen dieses Kapitels benutzen, und die Größe der beiden Fersennetze, die im letzten Abschnitt vorkommt — sie wird aus den Netzdateien gelesen, nicht abgeschrieben. Die Zelle ist eingeklappt: Wer wissen will, woher ein Name kommt, klappt sie auf. Eine Zahl aus dem Anhang steht nicht darin — die Steifigkeiten sind frei gewählte Lehreinheiten, damit man jede Zahl von Hand nachrechnen kann.

Das Rechenwerk dieses Kapitels — Federkette und Zeichenwerkzeuge, zum Nachlesen aufklappen
# --- Gemeinsame Bausteine aller Grafiken dieses Kapitels ------------------
# DIDAKTISCHE Lehreinheiten, KEINE Kanonzahlen: drei Federn in Reihe,
# Steifigkeiten k1 = 2, k2 = 1, k3 = 2 (Kraft je Wegeinheit; Fortführung von
# Kapitel 4). Vier Knoten; Knoten 4 (rechts) ist eingespannt (u4 = 0). Die
# Kräfte sind bekannt, die Verschiebungen gesucht. Wir DRÜCKEN das freie linke
# Ende gegen die Wand (f1 = 1 nach vorn), die Federn stehen unter Druck.
# Matrixaufbau, Reduktion und Loeser sind identisch mit js/kap05_loeser.js und
# der sichtbaren Werkbank; beide werden gegeneinander geprüft.
import pathlib
import sys

import numpy as np
import matplotlib.pyplot as plt

# Zeichenmechanik der Kräftebilanz (Muster 3): der Lehrgedanke bleibt in der
# sichtbaren Zelle, das Matplotlib-Beiwerk liegt in programme/kap05.
sys.path.insert(0, str(pathlib.Path("programme/kap05").resolve()))
from bilanz import zeichne_kraeftebilanz
from elimination import zeichne_treppe

K1, K2, K3 = 2, 1, 2      # Lehreinheiten (didaktisch, NICHT aus dem Kanon)

FARBE = {"matrix": "#1f77b4", "unbekannt": "#b3541e", "kraft": "#d62728",
         "pivot": "#2a7d2a", "aktiv": "#fdf0c8", "knoten": "#333",
         "feder": "#7a2b8a", "wand": "#777", "grau": "#9aa0a6"}

def steif_voll(k1, k2, k3):
    """Volle 4x4-Steifigkeitsmatrix dreier Federn in Reihe (wie Werkbank/JS)."""
    return [[k1, -k1, 0, 0], [-k1, k1 + k2, -k2, 0],
            [0, -k2, k2 + k3, -k3], [0, 0, -k3, k3]]

def reduziere_g(K, fest):
    """Zeile und Spalte des festen Knotens streichen (Grafik-Hilfsfassung)."""
    frei = [i for i in range(len(K)) if i != fest]
    return [[K[i][j] for j in frei] for i in frei]

def komma(x):
    """Deutsche Kommadarstellung, zwei Nachkommastellen mit Endnullen-Trimmung:
    0,75 bleibt 0,75; 1,50 -> 1,5; 2,00 -> 2. Minuszeichen −."""
    r = round(x)
    if abs(x - r) < 1e-9:
        return str(int(r)).replace("-", "−")
    s = f"{x:.2f}".rstrip("0").rstrip(".")
    return s.replace("-", "−").replace(".", ",")

def matrix_grafik(ax, x0, y0, M, zell_farbe=None, aktiv=None, rhs=None,
                  cw=1.0, ch=1.0, fs=15):
    """Eine Matrix (mit optionaler rechter Seite rhs) in Klammern zeichnen."""
    n, m = len(M), len(M[0])
    breite = m + (1 if rhs is not None else 0)
    if aktiv is not None:
        ax.add_patch(plt.Rectangle((x0 - cw / 2 - 0.1, y0 - aktiv * ch - ch / 2),
                                   breite * cw + 0.2, ch,
                                   color=FARBE["aktiv"], zorder=0))

    def zelltext(v):
        return v if isinstance(v, str) else komma(v)
    for i in range(n):
        for j in range(m):
            cx, cy = x0 + j * cw, y0 - i * ch
            col = zell_farbe(i, j, M[i][j]) if zell_farbe else "#222"
            ax.text(cx, cy, zelltext(M[i][j]), ha="center", va="center",
                    color=col, fontsize=fs, zorder=3, fontfamily="monospace")
        if rhs is not None:
            ax.text(x0 + m * cw, y0 - i * ch, zelltext(rhs[i]), ha="center",
                    va="center", color=FARBE["kraft"], fontsize=fs, zorder=3,
                    fontfamily="monospace")
    if rhs is not None:
        ax.plot([x0 + (m - 0.5) * cw, x0 + (m - 0.5) * cw],
                [y0 + ch / 2 + 0.04, y0 - (n - 1) * ch - ch / 2 - 0.04],
                color="#bbb", lw=1, zorder=2)
    left = x0 - cw / 2 - 0.14
    right = x0 + (breite - 1) * cw + cw / 2 + 0.14
    top, bot = y0 + ch / 2 + 0.06, y0 - (n - 1) * ch - ch / 2 - 0.06
    for bx, d in ((left, 1), (right, -1)):
        ax.plot([bx, bx + 0.14 * d], [top, top], color="#333", lw=1.6)
        ax.plot([bx, bx], [top, bot], color="#333", lw=1.6)
        ax.plot([bx, bx + 0.14 * d], [bot, bot], color="#333", lw=1.6)

def feder(ax, xa, xb, y, farbe, amp=0.13, windungen=6):
    """Eine Feder als Zickzack zwischen xa und xb auf Höhe y."""
    start, ende = xa + (xb - xa) * 0.14, xb - (xb - xa) * 0.14
    xs, ys = [xa, start], [y, y]
    for k in range(windungen + 1):
        xs.append(start + (ende - start) * k / windungen)
        ys.append(y + (amp if k % 2 == 0 else -amp))
    xs += [ende, xb]
    ys += [y, y]
    ax.plot(xs, ys, color=farbe, lw=1.8, zorder=3)

def pfeil(ax, x, y, laenge, farbe, breite=2.2):
    """Waagerechter Kraftpfeil ab (x, y); Vorzeichen = Richtung."""
    if abs(laenge) < 1e-6:
        return
    ax.annotate("", xy=(x + laenge, y), xytext=(x, y),
                arrowprops=dict(arrowstyle="-|>", color=farbe, lw=breite))

def zeichne_kette(ax, uv, f=None, laenge=1.6, us=0.5, y=0.0, mit_wand=True,
                  geist=None, geist_dy=0.0):
    """Die Federkette: vier Knoten, drei Federn; Knoten 4 rechts. uv = VOLLE
    Verschiebung aller vier Knoten (im freien Zustand darf sich jeder bewegen);
    f = äußere Kräfte je Knoten (rote Pfeile); geist = blasse Vergleichslage
    (starre Verschiebung u + c). geist_dy hebt die Vergleichslage eine eigene
    Zeile nach oben an — sonst verschmelzen ihre Windungen mit denen der realen
    Kette, weil der horizontale Versatz c gegenüber der Federlänge klein ist."""
    bx = [i * laenge for i in range(4)]
    px = [bx[i] + uv[i] * us for i in range(4)]
    if geist is not None:
        gy = y + geist_dy
        gx = [bx[i] + geist[i] * us for i in range(4)]
        for i in range(3):
            feder(ax, gx[i], gx[i + 1], gy, FARBE["grau"], amp=0.11)
        for i in range(4):
            ax.plot(gx[i], gy, "o", color=FARBE["grau"], ms=7, alpha=0.5, zorder=4)
        if geist_dy:
            ax.annotate("", xy=(gx[0], gy - 0.14), xytext=(px[0], y + 0.14),
                        arrowprops=dict(arrowstyle="-|>", color=FARBE["grau"], lw=1.4))
            ax.text(gx[3] + 0.16, gy, "u + c·(1;1;1;1)", color=FARBE["grau"],
                    fontsize=8.5, ha="left", va="center")
    if mit_wand:
        wx = bx[3]
        ax.plot([wx + 0.34, wx + 0.34], [y - 0.5, y + 0.5], color=FARBE["wand"], lw=2)
        for s in np.linspace(-0.45, 0.45, 7):
            ax.plot([wx + 0.34, wx + 0.54], [y + s, y + s + 0.16],
                    color=FARBE["wand"], lw=1)
        ax.plot([px[3], wx + 0.34], [y, y], color=FARBE["wand"], lw=1.4)
    for i in range(4):
        ax.plot([bx[i], bx[i]], [y - 0.55, y + 0.32], "--", color="#ddd", lw=0.9)
    for i in range(3):
        feder(ax, px[i], px[i + 1], y, FARBE["feder"])
    for i in range(4):
        fest = (i == 3 and mit_wand)
        ax.plot(px[i], y, "o", color=FARBE["wand"] if fest else FARBE["knoten"],
                ms=9, zorder=5)
        ax.text(px[i], y - 0.72, f"K{i + 1}" + ("\n(fest)" if fest else ""),
                color="#444", ha="center", fontsize=8.5)
    if f is not None:
        for i in range(4):
            if abs(f[i]) < 5e-3:
                continue
            pfeil(ax, px[i], y + 0.8, f[i] * 0.5, FARBE["kraft"])
            ax.text(px[i] + f[i] * 0.25, y + 1.02, f"f{i + 1} = {komma(f[i])}",
                    color=FARBE["kraft"], ha="center", fontsize=8.5)
    return px

# Das durchgehende Lehrbeispiel (rechte Einspannung: Pivots = k1,k2,k3).
K_VOLL = steif_voll(K1, K2, K3)      # 4x4, singulär (Zeilensummen 0)
K_RED = reduziere_g(K_VOLL, 3)       # Knoten 4 fest -> reguläres 3x3
F_RED = [1, 0, 0]                    # äußere Kräfte an den freien Knoten
U_RED = [2.0, 1.5, 0.5]             # von Hand gelöst (siehe Text)

# Groesse der beiden Buchnetze — aus den ausgelieferten Netzdateien gelesen,
# damit Prosa und Bild im letzten Abschnitt nicht auseinanderlaufen koennen.
import json

def netzgroesse(stufe):
    """(Knotenzahl, Freiheitsgrade) einer Netzstufe; 2 Freiheitsgrade je Knoten."""
    daten = json.loads(pathlib.Path(f"netze/netz_{stufe}.json").read_text())
    anzahl = len(daten["knoten"])
    return anzahl, 2 * anzahl

N_GROB, FG_GROB = netzgroesse("grob")
N_MITTEL, FG_MITTEL = netzgroesse("mittel")

def tausender(zahl):
    """Ganze Zahl mit deutschem Tausenderpunkt: 435600 -> 435.600."""
    return f"{int(zahl):,}".replace(",", ".")

5.1 Die Frage umdrehen: \(K\,u = f\)

Um die Gauß-Elimination in ihrer vollen Gestalt zu sehen, verlängern wir die Federkette aus Kapitel 4 um ein Glied: drei Federn, vier Knoten (Abbildung 5.1). Diesmal hängen wir sie am rechten Ende auf — Knoten 4 steht fest an der Wand, gespiegelt zur Kette aus Kapitel 4. Der Grund dafür zeigt sich erst beim Rechnen, deshalb sei er vorweggenommen: So herum aufgehängt, tauchen beim Eliminieren genau die drei Federsteifigkeiten als Zwischenwerte auf, und jede Zeile der Rechnung lässt sich als eine Feder lesen. An der Physik ändert das Spiegeln nichts — Ü 5.4 rechnet dieselbe Kette mit der Wand links und findet dieselben Verschiebungen, nur in umgekehrter Reihenfolge und mit Brüchen in der Rechnung. Die Steifigkeiten setzen Kapitel 4 fort: \(k_1 = 2\), \(k_2 = 1\), dazu \(k_3 = 2\) (Lehreinheiten „Kraft je Wegeinheit”, keine echten Gewebewerte). Von außen drücken wir das freie linke Ende (Knoten 1) mit der Kraft \(f_1 = 1\) nach vorn gegen die Wand; an Knoten 2 und 3 greift nichts an (\(f_2 = f_3 = 0\)). Die Federn geraten dadurch unter Druck.

Code
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7.6, 2.9))
zeichne_kette(ax, [0.0, 0.0, 0.0, 0.0], f=[1, 0, 0, 0],
              laenge=1.9, us=0.45)
ax.set_xlim(-1.0, 7.0); ax.set_ylim(-1.5, 1.4)
ax.set_aspect("equal"); ax.axis("off")
plt.tight_layout()
plt.show()
Abbildung 5.1: Die Ausgangslage dieses Kapitels: vier Knoten, drei Federn (k₁ = 2, k₂ = 1, k₃ = 2, Lehreinheiten), alle in Ruhe. Knoten 4 ist rechts fest eingespannt (u₄ = 0). Von außen drückt nur an Knoten 1 die Kraft f₁ = 1 (roter Pfeil, nach vorn zur Wand), an Knoten 2 und 3 wirkt keine äußere Kraft. Gesucht sind die Verschiebungen u₁, u₂, u₃ der drei freien Knoten — wie weit jeder wandert, zeigt erst Abbildung 5.5. Die grauen Strichlinien sind die Ruhelagen, von denen aus gemessen wird.

Deutung. Vier Knoten, drei Federn, rechts fest, und noch bewegt sich nichts. Der Druck an Knoten 1 wird sich durch die ganze Kette bis zur Wand fortpflanzen, und jeder Knoten wird ein Stück nach vorn ausweichen — wie weit welcher, ist genau die Frage dieses Kapitels. Wie in Kapitel 4 sammeln wir die Kopplungen in die Steifigkeitsmatrix, jetzt eine 4×4 (Zeile je Knoten, Spalte je Verschiebung):

\[ K = \begin{bmatrix} k_1 & -k_1 & 0 & 0 \\ -k_1 & k_1+k_2 & -k_2 & 0 \\ 0 & -k_2 & k_2+k_3 & -k_3 \\ 0 & 0 & -k_3 & k_3 \end{bmatrix} \;\xrightarrow{\;k_1=2,\,k_2=1,\,k_3=2\;}\; \begin{bmatrix} 2 & -2 & 0 & 0 \\ -2 & 3 & -1 & 0 \\ 0 & -1 & 3 & -2 \\ 0 & 0 & -2 & 2 \end{bmatrix}. \]

Diese Matrix hat dieselbe Bauart wie in Kapitel 4: Diagonale gleich Summe der anliegenden Steifigkeiten, Kopplungen negativ, weiter entfernte Knoten mit null. Und wie dort summiert sich jede Zeile zu null — verschiebt man alle vier Knoten um dasselbe Stück, dehnt sich keine Feder, es entsteht keine Kraft. Das ist die starre Verschiebung, und sie macht die volle Matrix singulär: Man kann \(K\,u = f\) mit ihr noch nicht eindeutig lösen. Der Ausweg ist genau die Wand.

5.1.1 Die Einspannung einarbeiten

Knoten 4 steht fest, also ist eine der vier Unbekannten schon bekannt: \(u_4 = 0\). Das ist eine Einspannung — eine Randbedingung, die eine Verschiebung fest vorgibt. Sie vereinfacht das System auf zweierlei Weise:

  • In jeder Gleichung fällt der Term mit \(u_4\) weg, weil \(u_4 = 0\) ist — die vierte Spalte verschwindet.
  • Die vierte Zeile ist die Kräftebilanz am festen Knoten. Sie bestimmt keine Verschiebung mehr (die kennen wir), sondern liefert hinterher die Lagerkraft, mit der die Wand hält. Fürs Lösen der Verschiebungen lassen wir sie zunächst weg.

Zeile und Spalte des festen Knotens gestrichen — übrig bleibt ein reguläres 3×3-System für die drei freien Knoten:

\[ K_{\text{red}} = \begin{bmatrix} 2 & -2 & 0 \\ -2 & 3 & -1 \\ 0 & -1 & 3 \end{bmatrix}, \qquad f = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}. \]

Das ist die Aufgabe, die wir jetzt lösen: \(K_{\text{red}}\,u = f\) nach \(u = (u_1, u_2, u_3)\) (Abbildung 5.2).

Code
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7.4, 3.2))
matrix_grafik(ax, 0.6, 1.0, K_RED, cw=1.0, ch=1.0, fs=15,
              zell_farbe=lambda i, j, v: FARBE["matrix"])
ax.text(1.6, 2.55, "K (bekannt)", ha="center", fontsize=11, color=FARBE["matrix"])
matrix_grafik(ax, 4.2, 1.0, [["?"], ["?"], ["?"]], cw=0.9, ch=1.0, fs=17,
              zell_farbe=lambda i, j, v: FARBE["unbekannt"])
ax.text(4.2, 2.55, "u (gesucht)", ha="center", fontsize=11,
        color=FARBE["unbekannt"])
matrix_grafik(ax, 6.6, 1.0, [[v] for v in F_RED], cw=0.9, ch=1.0, fs=15,
              zell_farbe=lambda i, j, v: FARBE["kraft"])
ax.text(6.6, 2.55, "f (bekannt)", ha="center", fontsize=11, color=FARBE["kraft"])
ax.text(3.42, 0.0, "·", ha="center", va="center", fontsize=20)
ax.text(5.4, 0.0, "=", ha="center", va="center", fontsize=18)
ax.annotate("gesucht: u", xy=(4.2, -2.15), xytext=(6.6, -2.15),
            arrowprops=dict(arrowstyle="-|>", color="#444", lw=1.6),
            fontsize=11, color="#444", va="center")
ax.set_xlim(-0.6, 8.2); ax.set_ylim(-2.8, 3.1)
ax.set_aspect("equal"); ax.axis("off")
plt.tight_layout()
plt.show()
Abbildung 5.2: K·u = f als Aufgabe „gesucht: u“. Links die bekannte reduzierte Steifigkeitsmatrix (blau), in der Mitte der gesuchte Verschiebungsvektor (orange, noch Fragezeichen), rechts der bekannte Kraftvektor (rot). In Kapitel 4 lief der Pfeil von u nach f; hier läuft die Frage rückwärts — aus f und K wird u gesucht.

Deutung. Alles bekannt bis auf \(u\). Jede der drei Zeilen ist eine Bedingung an einem Knoten: „die Federkräfte, die hier zusammenkommen, müssen die äußere Kraft \(f_i\) gerade ausgleichen”. Drei Bedingungen, drei Unbekannte — und weil die Federn koppeln, lassen sie sich nur gemeinsam erfüllen. Genau das leistet die Gauß-Elimination.

5.2 Gauß-Elimination Schritt für Schritt

Die Idee ist, das System stufenweise einfacher zu machen, ohne seine Lösung zu verändern. Erlaubt ist eine Rechenregel: Man darf ein Vielfaches einer Gleichung zu einer anderen addieren — die Lösung bleibt dieselbe. Damit räumt man unter der Diagonale Nullen frei, bis eine Dreiecksform dasteht, die man von unten nach oben ablesen kann. Sehen wir das erst einmal laufen.

WichtigVorhersage-Punkt

Bevor du die Animation startest: Die Elimination bringt das System auf Dreiecksform und löst es dann von unten nach oben auf. Welche Verschiebung steht zuerst fest — die von Knoten 1 (weit von der Wand) oder die von Knoten 3 (direkt an der Wand)? Lege dich fest, dann drücke „Abspielen”.

Deutung. Zuerst wird Knoten 1 aus Zeile 2 entfernt, dann Knoten 2 aus Zeile 3 — danach steht die Dreiecksform, und das Rückwärtseinsetzen findet die Verschiebungen von unten nach oben: zuerst \(u_3\) (der wandnahe Knoten, dessen Zeile nur noch eine Unbekannte enthält), dann \(u_2\), dann \(u_1\).

Und so läuft die Rechnung von Hand. Wir schreiben Matrix und rechte Seite als erweiterte Tabelle \([\,K \mid f\,]\). Das erste Pivot-Element — die führende Zahl der ersten Zeile — ist \(2\).

Schritt 1 — Knoten 1 aus Zeile 2 entfernen. In Zeile 2 steht in der ersten Spalte \(-2\). Addiert man Zeile 1 zu Zeile 2 (\(-2 + 2 = 0\)), fällt dort die \(u_1\) heraus:

\[ \left[\begin{array}{ccc|c} 2 & -2 & 0 & 1 \\ -2 & 3 & -1 & 0 \\ 0 & -1 & 3 & 0 \end{array}\right] \;\xrightarrow{\;\text{Zeile 2} \,+\, \text{Zeile 1}\;}\; \left[\begin{array}{ccc|c} 2 & -2 & 0 & 1 \\ 0 & 1 & -1 & 1 \\ 0 & -1 & 3 & 0 \end{array}\right]. \]

Schritt 2 — Knoten 2 aus Zeile 3 entfernen. Jetzt steht in Zeile 3 an zweiter Stelle \(-1\), in Zeile 2 dort \(+1\). Addiert man Zeile 2 zu Zeile 3, wird auch das null:

\[ \;\xrightarrow{\;\text{Zeile 3} \,+\, \text{Zeile 2}\;}\; \left[\begin{array}{ccc|c} 2 & -2 & 0 & 1 \\ 0 & 1 & -1 & 1 \\ 0 & 0 & 2 & 1 \end{array}\right]. \]

Unter der Diagonale nur noch Nullen — das ist die Dreiecksform (Abbildung 5.3). Bemerkenswert: Die drei Pivots sind \(2\), \(1\), \(2\) — genau die drei Federsteifigkeiten. Die Elimination hat die Kette Feder für Feder „abgewickelt”. Das ist der versprochene Grund für die Wand rechts: Von der freien Seite her eliminiert, kommt in jedem Schritt genau die nächste Feder an die Reihe, und beide Male ist das Vielfache, das man addiert, gerade das Einfache.

Dieselben zwei Schritte lassen sich als Rechenregel schreiben — ein Vielfaches einer Zeile zu einer anderen addieren — und untereinander als Treppe zeichnen. Sichtbar bleibt die Zeilenoperation, das Zeichnen der erweiterten Matrizen übernimmt eine Hilfsfunktion des Kapitels (Abbildung 5.3):

Die Gauß-Elimination Schritt für Schritt — und als Treppe gezeichnet
# Das reduzierte System aus dem Text als erweiterte Tabelle [K_red | f].
matrix = [[2, -2, 0], [-2, 3, -1], [0, -1, 3]]
rechte = [1, 0, 0]

def zeile_plus_zeile(M, r, ziel, quelle):
    """Zeile 'quelle' auf Zeile 'ziel' addieren — die erlaubte Gauss-Rechenregel."""
    for j in range(len(M[ziel])):
        M[ziel][j] = M[ziel][j] + M[quelle][j]
    r[ziel] = r[ziel] + r[quelle]

stufen = [([z[:] for z in matrix], rechte[:], None, "Ausgangssystem")]
zeile_plus_zeile(matrix, rechte, 1, 0)       # Schritt 1: Zeile 2 + Zeile 1
stufen.append(([z[:] for z in matrix], rechte[:], 1, "Zeile 2 + Zeile 1"))
zeile_plus_zeile(matrix, rechte, 2, 1)       # Schritt 2: Zeile 3 + Zeile 2
stufen.append(([z[:] for z in matrix], rechte[:], 2,
               "Zeile 3 + Zeile 2  →  Dreiecksform"))

zeichne_treppe(stufen)
Abbildung 5.3: Die Gauß-Elimination als Treppe: von der vollen erweiterten Matrix (oben) über zwei Eliminationsschritte zur Dreiecksform (unten), untereinander. Grün die Pivots auf der Diagonale, gelb hinterlegt die im Schritt veränderte Zeile. Unter der Diagonale entstehen der Reihe nach Nullen; die rechte Spalte (rot) ist die mitgeführte rechte Seite f.

Deutung. Nach den zwei Additionen steht unter der Diagonale nur noch Null, und die rechte Seite ist von \((1, 0, 0)\) auf \((1, 1, 1)\) mitgewandert. Genau diese Treppe lesen wir jetzt von unten nach oben ab.

Die Dreiecksform ist als System geschrieben:

\[ \begin{aligned} 2u_1 - 2u_2 \phantom{{}+ 2u_3} &= 1, \\ u_2 - u_3 &= 1, \\ 2u_3 &= 1. \end{aligned} \]

Jetzt das Rückwärtseinsetzen: die unterste Gleichung hat nur noch eine Unbekannte, die löst man direkt und arbeitet sich nach oben:

  • \(2u_3 = 1 \;\Rightarrow\; u_3 = \tfrac{1}{2}\),
  • \(u_2 - u_3 = 1 \;\Rightarrow\; u_2 = 1 + \tfrac{1}{2} = \tfrac{3}{2}\),
  • \(2u_1 - 2u_2 = 1 \;\Rightarrow\; 2u_1 = 1 + 3 = 4 \;\Rightarrow\; u_1 = 2\).

Also \(u = \left(2,\ \tfrac{3}{2},\ \tfrac{1}{2}\right)\). Der Knoten weitab von der Wand (Knoten 1) wandert am weitesten, der wandnahe (Knoten 3) am wenigsten. Das ist die Antwort auf die Frage, die Abbildung 5.1 gestellt hat; gezeichnet steht die verformte Kette später in Abbildung 5.5, ganz oben.

Ein Wort zur Sicherheit: Stünde in einer Pivotzeile zufällig eine Null, würde das Verfahren stocken (man kann nicht durch null teilen). Dann vertauscht man die Zeile mit einer darunter, in der an dieser Stelle keine Null steht — die Reihenfolge der Gleichungen ist ja frei. Dieser Zeilentausch (partielle Pivotierung) steckt im Löser unten; in unserem Beispiel wird er nie gebraucht, weil alle Pivots von null verschieden sind.

Die ganze Rechnung ist reines Python — der Aufbau der Matrix, ihre Reduktion und der Löser mit ausgeschriebenen Schleifen. Diese vier Funktionen sind der Rechenkern, den die späteren Kapitel wieder aufgreifen, deshalb bleiben sie sichtbar:

Matrix aufstellen, Einspannung einbauen, Gauß-Löser mit Rückwärtseinsetzen
def steifigkeit_voll(k1, k2, k3):
    """Volle 4x4-Steifigkeitsmatrix dreier Federn in Reihe (Knoten 1-2-3-4)."""
    return [
        [k1,   -k1,       0,     0],
        [-k1,  k1 + k2,  -k2,    0],
        [0,    -k2,   k2 + k3,  -k3],
        [0,     0,      -k3,    k3],
    ]

def reduziere(K, fest):
    """Zeile und Spalte des festen Knotens streichen (Einspannung u_fest = 0).

    K:    quadratische Matrix (Liste von Zeilen).
    fest: Index des eingespannten Knotens (0-basiert).
    Rueckgabe: die reduzierte Matrix der freien Knoten.
    """
    frei = []
    for i in range(len(K)):
        if i != fest:
            frei.append(i)
    ergebnis = []
    for i in frei:
        neue_zeile = []
        for j in frei:
            neue_zeile.append(K[i][j])
        ergebnis.append(neue_zeile)
    return ergebnis

def matrix_mal_vektor(matrix, vektor):
    """Matrix-Vektor-Produkt f = matrix * vektor (fuer die Probe)."""
    ergebnis = []
    for zeile in matrix:
        summe = 0
        for spalte_index in range(len(vektor)):
            summe = summe + zeile[spalte_index] * vektor[spalte_index]
        ergebnis.append(summe)
    return ergebnis

def gauss_loesen(A, b):
    """Loest A*u = b mit Gauss-Elimination (partielle Pivotierung) und
    Rueckwaertseinsetzen.

    Rueckgabe: (status, u). status ist
      'eindeutig'     -> u ist der Loesungsvektor,
      'keine_loesung' -> u ist None (singulaer UND unvertraeglich: die rechte
                         Seite passt nicht, es gibt gar keine Loesung),
      'loesungsschar' -> u ist None (singulaer UND vertraeglich: unendlich viele
                         Loesungen u + c*(1,...,1)).
    Reines Python, ausgeschriebene Schleifen.
    """
    n = len(A)
    # Arbeitskopien, damit die Eingabe unveraendert bleibt.
    M = []
    for zeile in A:
        kopie = []
        for wert in zeile:
            kopie.append(wert)
        M.append(kopie)
    r = []
    for wert in b:
        r.append(wert)
    # Vorwaerts eliminieren, mit Zeilentausch (partielle Pivotierung).
    pivot_zeilen = 0
    for spalte in range(n):
        if pivot_zeilen == n:
            break
        # Zeile mit dem groessten Element in dieser Spalte als Pivot waehlen.
        beste = pivot_zeilen
        for i in range(pivot_zeilen + 1, n):
            if abs(M[i][spalte]) > abs(M[beste][spalte]):
                beste = i
        if abs(M[beste][spalte]) < 1e-9:
            continue                          # keine Pivotzeile -> freie Spalte
        if beste != pivot_zeilen:             # Zeilen tauschen
            M[pivot_zeilen], M[beste] = M[beste], M[pivot_zeilen]
            r[pivot_zeilen], r[beste] = r[beste], r[pivot_zeilen]
        for i in range(pivot_zeilen + 1, n):
            faktor = M[i][spalte] / M[pivot_zeilen][spalte]
            for j in range(spalte, n):
                M[i][j] = M[i][j] - faktor * M[pivot_zeilen][j]
            r[i] = r[i] - faktor * r[pivot_zeilen]
        pivot_zeilen = pivot_zeilen + 1
    # Widerspruch? Eine Nullzeile mit rechter Seite ungleich null -> keine Loesung.
    for i in range(pivot_zeilen, n):
        if abs(r[i]) > 1e-9:
            return ("keine_loesung", None)
    if pivot_zeilen < n:
        return ("loesungsschar", None)        # Rang zu klein, aber vertraeglich
    # Voller Rang: rueckwaerts einsetzen.
    u = []
    for i in range(n):
        u.append(0.0)
    for i in range(n - 1, -1, -1):
        summe = r[i]
        for j in range(i + 1, n):
            summe = summe - M[i][j] * u[j]
        u[i] = summe / M[i][i]
    return ("eindeutig", u)

# Das Beispiel dieses Kapitels: drei Federn (2, 1, 2), Knoten 4 an der Wand.
K_voll = steifigkeit_voll(2, 1, 2)
K_red = reduziere(K_voll, 3)               # Knoten 4 (Index 3) eingespannt

Mit den Lehreinheiten \(k_1 = 2\), \(k_2 = 1\), \(k_3 = 2\) aufgerufen, der Wand an Knoten 4 und der Last \(f = (1;\ 0;\ 0)\) ergibt sich Tabelle 5.1: links die reduzierte Matrix, in der Knoten 4 gestrichen ist, rechts daneben die rechte Seite und die gefundene Lösung.

Tabelle 5.1: Das reduzierte System des Federkette-Beispiels und seine Lösung (didaktische Lehreinheiten). Der Löser meldet den Status „eindeutig“.
Zeile \(K_\text{red}\) Spalte 1 Spalte 2 Spalte 3 rechte Seite \(f\) Lösung \(u\)
Knoten 1 2 −2 0 1 2
Knoten 2 −2 3 −1 0 1,5
Knoten 3 0 −1 3 0 0,5

Deutung unter der Tabelle. Der Rechner meldet eindeutig und findet dieselben Zahlen wie unsere Handrechnung, \(u = (2{,}0;\ 1{,}5;\ 0{,}5)\) — er tut nichts anderes, nur schneller und ohne Flüchtigkeitsfehler. Genau dieser Kern, nur mit einem spezialisierten Löser für Tausende Knoten, steckt später hinter jedem Regler dieses Buches.

5.3 Die Probe

Eine Lösung ist erst dann eine Lösung, wenn sie das System erfüllt. Die Probe ist das Matrix-Vektor-Produkt aus Kapitel 4: Man setzt das gefundene \(u\) ein und prüft, ob wieder die bekannten Kräfte herauskommen. Aus der vollen Matrix lesen wir zusätzlich die Lagerkraft an Knoten 4 ab und stellen beide Kräfte — die von vorn aufgebrachte Last und die haltende Wand — als Bilanz nebeneinander. Die Druckausgabe bestätigt die Probe in Zahlen; das Balkenbild darunter (die Zeichnung übernimmt eine Hilfsfunktion des Kapitels) zeigt, dass sich beide Kräfte zu null aufheben:

Die Probe K · u = f und die Lagerkraft aus der vollen Matrix
status, u = gauss_loesen(K_red, [1, 0, 0])
probe = matrix_mal_vektor(K_red, u)
print("Probe  K_red * u =", [round(x, 3) for x in probe], " (soll [1, 0, 0])")

# Lagerkraft: die vierte Zeile der VOLLEN Matrix, mit u4 = 0 dahinter.
u_voll = [u[0], u[1], u[2], 0.0]
f4 = matrix_mal_vektor(K_voll, u_voll)[3]
summe = 1.0 + f4                        # aufgebrachte Last plus Lagerkraft

zeichne_kraeftebilanz(last=1.0, lagerkraft=f4, summe=summe)
Probe  K_red * u = [1.0, 0.0, 0.0]  (soll [1, 0, 0])
Abbildung 5.4: Kräftebilanz der gelösten Kette: die aufgebrachte Last f₁ = +1 an Knoten 1 (nach vorn) gegen die Wandreaktion f₄ = −1 an Knoten 4 (nach hinten). Beide Balken sind gleich lang und entgegengesetzt, ihre Summe liegt exakt auf der Null-Achse — die Kette ist im Gleichgewicht.

Deutung. Die Probe gibt \(f = (1;\ 0;\ 0)\) zurück — die drei Knotenbedingungen sind erfüllt, jede Zeile geht auf. Die vierte Zeile der vollen Matrix, die wir beim Lösen weggelassen hatten, liefert die Lagerkraft \(f_4 = -1\); im Bild ist das der graue Balken nach links, genau so lang wie der rote der Last nach rechts. Die Wand hält mit einer Kraft nach hinten dagegen, gerade so groß wie die von vorn aufgebrachte Last — das Ganze ist im Gleichgewicht, die Summe aller Kräfte sitzt exakt auf der Null. Was von außen an Knoten 1 hineingeht, trägt die Wand an Knoten 4 wieder ab.

5.3.1 Der erste Regler: verdoppeln, halbieren

Das System ist linear — aber was heißt das für die Lösung, wenn man die Last ändert?

WichtigVorhersage-Punkt

Bevor du schiebst: Der erste Regler ändert die aufgebrachte Kraft \(f_1\). Verdoppelst du die rechte Seite — verdoppelt sich die Lösung \(u\) exakt, oder nicht ganz? Und was passiert bei \(f_1 = 0\)? Lege dich fest, bevor du schiebst.

Deutung. Die Lösung ist \(u = f_1 \cdot (2;\ 1{,}5;\ 0{,}5)\) — verdoppelt man \(f_1\), verdoppelt sich jede Verschiebung exakt, halbiert man, halbiert sie sich (aus \(f_1 = 1{,}5\) wird \(u = (3;\ 2{,}25;\ 0{,}75)\)), und bei \(f_1 = 0\) bleibt alles in Ruhe. Das ist die Linearität von \(K\,u = f\): doppelte Ursache, doppelte Wirkung. Sie gilt, solange das Material linear antwortet — eine Vereinfachung, die Kapitel 6 an echtem Gewebe auf die Probe stellt.

5.3.2 Zwei Lasten, eine Summe

Die Linearität hat eine zweite Folge, und die ist für dieses Buch wichtiger als die erste. Drückt man nicht nur an Knoten 1, sondern zusätzlich an Knoten 2, muss man das System nicht neu lösen: Die Lösung zur Summe zweier Lasten ist die Summe der Lösungen zu den Einzellasten. Für \(f = (1;\ 0;\ 0)\) haben wir \(u = (2;\ 1{,}5;\ 0{,}5)\) gefunden; für \(f = (0;\ 1;\ 0)\) ergibt dieselbe Rechnung \(u = (1{,}5;\ 1{,}5;\ 0{,}5)\); und für beide Lasten zusammen, \(f = (1;\ 1;\ 0)\), kommt genau die Summe heraus, \(u = (3{,}5;\ 3;\ 1)\). Die editierbare Zelle am Ende dieses Kapitels rechnet beide Fälle nebeneinander, und man kann jede andere Kombination eintragen.

Das heißt Superpositionsprinzip, und in einem Satz: Last verdoppeln heißt Antwort verdoppeln; zwei Lasten zusammen heißt zwei Antworten addieren. Es ist der Grund, warum das Buch später einen ganzen Schritt in Einzelbilder zerlegen darf: Teil V rechnet nicht eine komplizierte Last, sondern fünf einfache Momentaufnahmen, löst jede für sich und setzt sie gewichtet wieder zusammen. Ohne Superposition wäre das eine Näherung; mit ihr ist es dieselbe Rechnung, nur zerlegt.

Die Grenze steht in der Voraussetzung: „solange alles linear ist”. Sobald das Material bei größerer Last weicher oder steifer antwortet, sobald zwei Flächen sich berühren und je nach Last unterschiedlich viel Fläche tragen, oder sobald die Verformung so groß wird, dass sich die Geometrie selbst merklich ändert, gilt die Summenregel nicht mehr — dann ist die Antwort auf zwei Lasten mehr oder weniger als die Summe der Einzelantworten, und man muss den Lastfall als Ganzes rechnen. Alle Rechnungen dieses Buches bleiben im linearen Bereich; wo sie an diese Grenze stoßen, sagt der Text es.

5.4 Warum eine Einspannung nötig ist

Wir hatten die Wand gebraucht, um überhaupt lösen zu können. Warum eigentlich? Der zweite Regler nimmt die Einspannung weg — und bietet die Last dabei in zwei Varianten an.

WichtigVorhersage-Punkt

Bevor du schaltest: Der zweite Regler schaltet zwischen eingespannt und frei um. Findet der Löser ohne Halt noch eine eindeutige Lösung, findet er gar keine, oder findet er unendlich viele? Und macht es einen Unterschied, ob die von außen aufgebrachten Kräfte sich zu null summieren? Lege dich fest.

Deutung. Eingespannt liefert der Löser genau eine Lösung. Frei hängt die Antwort davon ab, ob die Last überhaupt im Gleichgewicht ist — und selbst dann bleibt sie nicht eindeutig. Warum, sagt der folgende Kasten.

WarnungOhne Halt keine eindeutige Lösung

Vermutung: „Wenn ich die Kräfte an allen Knoten kenne, ist die Verschiebung eindeutig bestimmt — ich muss nur das Gleichungssystem lösen.”

Warum sie naheliegt: In der gelösten Rechnung gehörte zu jeder Kraft genau eine Verschiebung. Es sieht aus, als lege die rechte Seite \(f\) die Antwort \(u\) fest.

Was stattdessen gilt: Ohne feste Lagerung sind alle vier Knoten frei, und die volle Matrix ist singulär. Zwei Fälle sind zu trennen:

  • Keine Lösung (unverträgliche Last). Unsere Standardlast \((1;0;0;0)\) hat die Summe \(1 \ne 0\). Ohne Wand gibt es aber nichts, was diese Nettokraft aufnimmt — die Kette wäre nicht im Gleichgewicht, sie würde als Ganzes beschleunigen. Statisch gibt es gar keine Lösung; der Löser meldet „keine Lösung”.
  • Unendlich viele (verträgliche Last). Erst wenn die äußeren Kräfte sich zu null summieren — etwa \((1;0;0;-1)\) oder gar keine Last —, ist Gleichgewicht möglich. Dann aber ist zu jeder Lösung \(u\) auch \(u + c\cdot(1;1;1;1)\) eine Lösung: Man verschiebt die ganze Kette um dasselbe Stück \(c\), keine Feder dehnt sich, dieselben Kräfte kommen heraus (die starre Verschiebung, unten blass gezeichnet). Es gibt unendlich viele Lösungen.

In beiden Fällen fehlt die eine, eindeutige Antwort. Der Löser bemerkt das: Er trifft in einer Spalte auf lauter Nullen, überspringt sie und meldet danach, dass es keine oder keine eindeutige Lösung gibt — je nachdem, welcher der beiden Fälle vorliegt. Erst die Einspannung nimmt die starre Verschiebung heraus und macht die Aufgabe für jede Last eindeutig lösbar.

Am eigenen Fuß: Das Fersenbein schwebt nicht davon, weil es am Sprungbein gelagert ist. Was hier die Wand leistet, leistet dort die Gelenkfläche — sie nimmt auf, was von Boden, Sehne und Faszie zusammen übrig bleibt: die vierte Kraft aus Kapitel 2. Im Modell wird dafür die Gelenkfläche zum Sprungbein festgehalten. Eine vergessene Lagerung ist eine typische Fehlerquelle; Kapitel 8 und 12 kommen darauf zurück, und Kapitel 12 zeigt, welche Lagerung am Fersenbein die richtige ist.

Code
import matplotlib.pyplot as plt

fig, achsen = plt.subplots(3, 1, figsize=(7.0, 6.8))
zeichne_kette(achsen[0], [U_RED[0], U_RED[1], U_RED[2], 0.0], f=[1, 0, 0, 0],
              laenge=1.9, us=0.45)
achsen[0].set_title("eingespannt: eine eindeutige Lösung", fontsize=10)
zeichne_kette(achsen[1], [0, 0, 0, 0], f=[1, 0, 0, 0], laenge=1.9, us=0.45,
              mit_wand=False)
achsen[1].set_title("frei, Last (1; 0; 0; 0): keine Lösung (Summe ≠ 0)",
                    fontsize=10)
c2 = 1.6
u_p = [U_RED[0], U_RED[1], U_RED[2], 0.0]
zeichne_kette(achsen[2], u_p, f=[1, 0, 0, -1], laenge=1.9, us=0.42,
              mit_wand=False, geist=[u_p[i] + c2 for i in range(4)],
              geist_dy=0.55)
achsen[2].set_title("frei, Last (1; 0; 0; −1): unendlich viele Lösungen "
                    "(starre Verschiebung)", fontsize=10)
for ax in achsen:
    ax.set_xlim(-1.1, 7.6); ax.set_ylim(-1.5, 1.4)
    ax.set_aspect("equal"); ax.axis("off")
plt.tight_layout()
plt.show()
Abbildung 5.5: Die drei Zustände des Lagerungs-Reglers, untereinander. Oben eingespannt (Knoten 4 fest): die Last erzeugt eine eindeutige Verformung. Mitte frei, unverträgliche Last (1; 0; 0; 0): keine statische Lösung (Summe der Kräfte ≠ 0). Unten frei, verträgliche Last (1; 0; 0; −1): unendlich viele Lösungen — u und die um c = 1,6 verschobene Lage u + c·(1;1;1;1) sind gleich gültig (kein Knoten ist festgehalten, alle Federn gleich lang); die blasse Vergleichslage ist eine eigene Zeile höher gezeichnet, sonst verschmelzen ihre Windungen mit denen von u.

Deutung. Nur der obere Fall ist eindeutig lösbar. In der Mitte fehlt das Gleichgewicht, unten fehlt die Eindeutigkeit — beide Male, weil nichts die Kette festhält. Die Einspannung ist kein Beiwerk, sondern die Bedingung dafür, dass die Frage „wohin bewegt sich jeder Knoten?“ überhaupt eine Antwort hat.

5.5 Warum die Ferse anders gerechnet wird

Von Hand haben wir ein 3×3-System gelöst. Das echte Fersenmodell hat Tausende Knoten. Mit je zwei Freiheitsgraden (Verschiebung nach vorn und nach oben) hat ein Netz aus \(N\) Knoten \(2N\) Unbekannte, und eine volle Steifigkeitsmatrix hätte \((2N)^2 = 4N^2\) Einträge: schon das grobe Buchnetz (330 Knoten, 660 Freiheitsgrade) käme so auf 435.600 Einträge, das mittlere (768 Knoten, 1536 Freiheitsgrade) auf 2.359.296. Und doch antwortet ein Regler in Millisekunden. Wie?

Der Schlüssel steckt schon in unserer 4×4-Matrix: Sie ist fast leer. Jeder Knoten koppelt nur mit seinen direkten Nachbarn, alles andere ist null. Diese Eigenschaft heißt dünn besetzte Matrix, und sie bleibt bei Tausenden Knoten erhalten — je größer das Netz, desto leerer die Matrix im Verhältnis. Abbildung 5.6 zeigt das strukturelle Besetzungsmuster der echten Fersen-Steifigkeitsmatrix.

Code
import json
import pathlib

import numpy as np
import matplotlib.pyplot as plt
from scipy.sparse import lil_matrix, csr_matrix
from scipy.sparse.csgraph import reverse_cuthill_mckee

# Das versionierte grobe Netz laden und das STRUKTURELLE Besetzungsmuster bauen:
# zwei Knoten koppeln, wenn sie ein Dreieck teilen; je Knoten 2 Freiheitsgrade
# (x, y). Das sind berechnete Messwerte des Netzes, keine Kanonzahlen; gezeigt
# ist, welche Eintraege besetzt sein KOENNEN, nicht die Werte einer bestimmten
# assemblierten Matrix.
daten = json.loads(pathlib.Path("netze/netz_grob.json").read_text())
knoten = daten["knoten"]
dreiecke = daten["dreiecke"]
n = len(knoten)
freiheitsgrade = 2 * n

kopplung = lil_matrix((freiheitsgrade, freiheitsgrade))
for dreieck in dreiecke:
    for a in dreieck:
        for b in dreieck:
            for pa in range(2):
                for pb in range(2):
                    kopplung[2 * a + pa, 2 * b + pb] = 1.0
kopplung = csr_matrix(kopplung)
nnz = kopplung.nnz
anteil = 100.0 * nnz / (freiheitsgrade ** 2)

# Freiheitsgrade umsortieren, damit das Band schmal wird (Cuthill-McKee, umgekehrt).
perm = reverse_cuthill_mckee(kopplung, symmetric_mode=True)
gebandet = kopplung[perm][:, perm]

fig, achsen = plt.subplots(2, 1, figsize=(5.8, 10.4))
achsen[0].spy(kopplung, markersize=0.6, color="#1f77b4")
achsen[0].set_title(f"natürliche Reihenfolge der Freiheitsgrade\n{n} Knoten, "
                    f"{freiheitsgrade}×{freiheitsgrade}-Matrix, "
                    f"{anteil:.1f} % besetzt", fontsize=10)
achsen[1].spy(gebandet, markersize=0.6, color="#b3541e")
achsen[1].set_title("Freiheitsgrade umsortiert: schmales Band", fontsize=10)
for ax in achsen:
    ax.set_xlabel("Spaltenindex (Freiheitsgrad)")
    ax.set_ylabel("Zeilenindex")
plt.tight_layout()
plt.show()

print(f"Grobes Buchnetz: {n} Knoten, {freiheitsgrade} Freiheitsgrade.")
print(f"Moegliche Nicht-Null-Eintraege: {nnz} von {freiheitsgrade**2} "
      f"({anteil:.2f} % besetzt, {100 - anteil:.2f} % sind null).")
Abbildung 5.6: Besetzungsmuster der Steifigkeitsmatrix des groben Buchnetzes — welche Einträge überhaupt besetzt sein können. Ein Punkt steht für einen möglichen Eintrag ungleich null. Oben in der natürlichen Reihenfolge der Freiheitsgrade, unten nach Umsortierung: dieselbe Matrix, nur Zeilen und Spalten anders geordnet, wodurch die besetzten Einträge zu einem schmalen Band um die Diagonale zusammenrücken. Weit über neunzig Prozent der Matrix sind null; nur die wenigen besetzten Einträge müssen gespeichert und gerechnet werden.
Grobes Buchnetz: 330 Knoten, 660 Freiheitsgrade.
Moegliche Nicht-Null-Eintraege: 8632 von 435600 (1.98 % besetzt, 98.02 % sind null).

Deutung. Fast die ganze Matrix ist leer; die Kopplungen sitzen in einem schmalen Band um die Diagonale, sobald man die Freiheitsgrade geschickt umsortiert — dieselbe Matrix, nur in anderer Reihenfolge durchnummeriert. Ein direkter Löser für dünn besetzte Matrizen speichert nur die besetzten Einträge und nutzt eine solche interne Umsortierung, um innerhalb des Bandes zu eliminieren. Genau das tut der Rechner hinter den Reglern: Er löst dasselbe \(K\,u = f\) wie unser Löser von Hand, nur mit Tausenden Knoten. Beim Bau dieses Buches wurde gemessen, wie lange das dauert: ein Löservorgang auf einem Netz dieser Größenordnung (917 Knoten, fünf Lastfälle, nach dem Warmlaufen) kostet rund 30 Millisekunden. Dieselbe Gauß-Idee, nur klug organisiert.

Dieses Besetzungsmuster ist das erste Bild einer echten Fersenmatrix in diesem Buch. Kapitel 10 kommt darauf zurück, wenn die Matrix nicht mehr aus Federn, sondern aus Dreiecken zusammengesetzt wird.

Editierbare Zelle. Der folgende Block löst dieselbe Kette im Browser und zeichnet die gefundenen Verschiebungen als Balken über den drei freien Knoten — ein Balkenpaar je Lastfall. Ändere die Liste lastfaelle oder die Steifigkeiten und führe die Zelle aus: Man sieht sofort, wie viel weiter jeder Knoten wandert, wenn man auch an Knoten 2 drückt.

Deutung. Das Balkenpaar je Knoten zeigt die Superposition, Knoten für Knoten: Für die Standardlast \(f = (1;\ 0;\ 0)\) liefert die Zelle wieder \(u = (2;\ 1{,}5;\ 0{,}5)\); drückt man zusätzlich an Knoten 2, also \(f = (1;\ 1;\ 0)\), wächst jede Verschiebung auf \(u = (3{,}5;\ 3;\ 1)\) — genau um die Lösung zu \((0;\ 1;\ 0)\), nämlich \((1{,}5;\ 1{,}5;\ 0{,}5)\). Trage eine dritte rechte Seite ein und prüfe die Summenregel an einem Beispiel, das du dir selbst ausgesucht hast.

Was Teil II gebracht hat — die Studiums-Tabelle

Mit diesem Kapitel endet Teil II, „Die Sprache”. Die ersten drei Zeilen der Studiums-Tabelle aus dem Vorwort sind jetzt eingelöst — was als anschauliches Bild begann, trägt im Studium seinen Fachnamen:

Im Buch als … Im Studium heißt es … eingelöst in
Kraftpfeil mit Richtung und Länge Vektor, Vektorraum ✓ Kapitel 3
Tabelle, die Verschiebungen in Kräfte übersetzt Matrix, lineare Abbildung ✓ Kapitel 4
„alle Knoten gleichzeitig zufriedenstellen” lineares Gleichungssystem, Lösbarkeit ✓ Kapitel 5
Steigung einer Messkurve Ableitung, Differentialquotient Teil III
Fläche unter der Belastungskurve Integral Teil III

Aus dem Kraftpfeil wurde ein Vektor, aus der Kopplungstabelle eine Matrix, und aus „alle Knoten gleichzeitig zufriedenstellen” ein lineares Gleichungssystem, das die Gauß-Elimination löst. Damit steht die Sprache, in der der Rest des Buches rechnet. Teil III fragt als Nächstes, wie Gewebe überhaupt auf Last antwortet — und bringt Ableitung und Integral ins Spiel.

Übungen

Ü 5.1 (Verstehen). Für das gelöste System \(K_{\text{red}}\,u = f\) mit \(u = \left(2,\ \tfrac{3}{2},\ \tfrac{1}{2}\right)\): Mache die Probe an der zweiten Zeile (\(-2u_1 + 3u_2 - u_3 = f_2\)) und sage in einem Satz, welche Knotenbedingung sie ausdrückt.

Einsetzen: \(-2\cdot 2 + 3\cdot\tfrac{3}{2} - \tfrac{1}{2} = -4 + \tfrac{9}{2} - \tfrac{1}{2} = 0 = f_2\). Die Zeile ist die Kräftebilanz an Knoten 2: Die Druckkräfte von Feder 1 (zwischen Knoten 1 und 2) und Feder 2 (zwischen Knoten 2 und 3) heben sich dort gerade auf, weil an Knoten 2 keine äußere Kraft angreift.

Ü 5.2 (Verändern). Öffne den Kraft-Regler und stelle \(f_1\) auf die Hälfte des Startwerts. Sage vorher, welche Verschiebungen herauskommen, und prüfe es.

Wegen der Linearität halbiert sich jede Verschiebung: aus \((2;\ 1{,}5;\ 0{,}5)\) wird \((1;\ 0{,}75;\ 0{,}25)\). Der Regler bestätigt es — halbe Last, halbe Verschiebung. Das ist genau die Eigenschaft \(u = f_1\cdot(2;\ 1{,}5;\ 0{,}5)\).

Ü 5.3 (Übertragen). Bei einer Million Knoten hätte die volle Steifigkeitsmatrix \((2\cdot 10^6)^2 = 4\cdot 10^{12}\) Einträge. Begründe, warum man das System trotzdem lösen kann — und warum man die Matrix nicht vollständig als Zahlentabelle in den Speicher schreibt.

Die Matrix ist dünn besetzt: Jeder Knoten koppelt nur mit einer Handvoll Nachbarn, also stehen pro Zeile nur wenige Einträge ungleich null — die Zahl der Nicht-Null-Einträge wächst etwa mit der Knotenzahl, nicht mit ihrem Quadrat. Man speichert nur diese wenigen Einträge (dünn besetztes Format) und eliminiert nur innerhalb des schmalen Bandes (oder löst iterativ). Die volle Tabelle mit \(4\cdot 10^{12}\) Zahlen hinzuschreiben wäre Verschwendung — fast alles darin wäre null.

Ü 5.4 (Verändern). Spanne dieselbe Kette am anderen Ende ein: Knoten 1 steht jetzt fest, an Knoten 4 zieht die Kraft \(1\), die Steifigkeiten bleiben \(k_1 = 2\), \(k_2 = 1\), \(k_3 = 2\). Streiche Zeile und Spalte 1 und löse das verbleibende System für \(u_2, u_3, u_4\) von Hand:

\[ \begin{bmatrix} 3 & -1 & 0 \\ -1 & 3 & -2 \\ 0 & -2 & 2 \end{bmatrix} \begin{bmatrix} u_2 \\ u_3 \\ u_4 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}. \]

Diesmal geht es nicht mit bloßem Addieren: Welches Vielfache der ersten Zeile musst du zur zweiten addieren, um die erste Spalte zu räumen? Sind die Pivots noch die Federsteifigkeiten?

In der zweiten Zeile steht \(-1\), das Pivot der ersten ist \(3\). Man addiert also das \(\tfrac{1}{3}\)-fache der ersten Zeile zur zweiten; aus \([-1,\ 3,\ -2]\) wird \([0,\ \tfrac{8}{3},\ -2]\), die rechte Seite bleibt \(0\). Im zweiten Schritt ist das Vielfache \(\tfrac{3}{4}\): Aus \([0,\ -2,\ 2]\) wird \([0,\ 0,\ \tfrac{1}{2}]\), die rechte Seite bleibt \(1\). Rückwärts eingesetzt: \(\tfrac{1}{2}u_4 = 1 \Rightarrow u_4 = 2\), dann \(\tfrac{8}{3}u_3 = 2u_4 = 4 \Rightarrow u_3 = \tfrac{3}{2}\), dann \(3u_2 = u_3 \Rightarrow u_2 = \tfrac{1}{2}\).

Die Pivots sind \(3\), \(\tfrac{8}{3}\), \(\tfrac{1}{2}\)keine Federsteifigkeit mehr, und die Vielfachen sind echte Brüche. Genau deshalb steht die Wand in diesem Kapitel rechts: Die Rechnung ist dieselbe, aber von der freien Seite her eliminiert wickelt sie die Kette Feder für Feder ab.

An der Physik ändert die Seitenwahl nichts. Die Kette dehnt sich unter der Zugkraft \(1\) in jeder Feder um \(1/k\): \(0{,}5\), \(1\), \(0{,}5\) — aufaddiert \(u = (0{,}5;\ 1{,}5;\ 2)\), dieselben drei Zahlen wie im Kapitelbeispiel, nur von der Wand aus gezählt statt zu ihr hin.

Roter Faden

Wo kam das schon vor, wo kommt es wieder? Dieses Kapitel hat das in Kapitel 4 aufgestellte System \(K\,u = f\) gelöst — jetzt mit der Einspannung, die dort schon als Zeilensummen-Null (starre Verschiebung) angekündigt war. Das Rückwärtseinsetzen benutzt nichts als das Rechnen mit Zahlen aus Kapitel 3, und die Probe ist das Matrix-Vektor-Produkt aus Kapitel 4. Vor: Die feste Kante kehrt in Kapitel 8 wieder und bekommt in Kapitel 12 ihren Fachnamen — Dirichlet-Randbedingung, neben den beiden anderen Arten, einen Rand vorzuschreiben. Die dünn besetzte Matrix und der spezialisierte Löser stecken hinter allen Reglern ab Kapitel 10, und die Elementsteifigkeitsmatrix in Kapitel 10 wird zu genau so einem System zusammengesetzt, nur aus Dreiecken statt Federn. Das Superpositionsprinzip trägt Teil V: Kapitel 14 zerlegt einen Schritt in fünf Lastfälle und setzt sie gewichtet wieder zusammen, Kapitel 15 mittelt die Antworten.

Ein Wort noch zur Größe der Zahlen. Hier sind die Verschiebungen bloße Wegeinheiten, groß wie die Federn selbst. Am echten Knochen gibt eine Ferse unter zwei Körpergewichten nur um Bruchteile eines Millimeters nach — wie groß solche Verschiebungen wirklich sind und warum so wenig, sagt das nächste Kapitel.

Was dieses Kapitel NICHT tut

Es entwickelt keine LU- oder Cholesky-Zerlegung als Theorie, keine Kondition und keine Numerik der Rundungsfehler, keine iterativen Löser im Detail (nur benannt) und keine Zeitabhängigkeit — nur die Gauß-Elimination als sichtbares Handwerk. Der Zeilentausch (partielle Pivotierung) ist zwar im Löser eingebaut, wird im Lehrbeispiel aber nie ausgelöst, weil alle Pivots von null verschieden sind. Und die Federsteifigkeiten \(k_1, k_2, k_3\) bleiben didaktische Lehreinheiten, keine echten Gewebewerte — die stehen im Zahlenanhang und kommen ab Kapitel 6.