23  Arrays und Beamforming

Auf jedem Flughafen dreht sich eine Radarantenne — eine ganze Mechanik aus Motor, Lagern und Getriebe, nur damit der Strahl einmal pro Umlauf in jede Richtung zeigt. Ein Starlink-Terminal auf dem Wohnmobildach hat dasselbe Problem in schwieriger: Es muss einen Satelliten verfolgen, der in 550 km Höhe mit 27 000 km/h über den Himmel zieht. Trotzdem liegt die Schüssel flach, starr und lautlos da — kein Motor, kein Gelenk, nichts bewegt sich. Und sie hält den Satelliten trotzdem im Strahl, wechselt sogar alle paar Minuten in Sekundenbruchteilen zum nächsten.

Wie lenkt man einen Funkstrahl, ohne irgendetwas zu bewegen? Die Antwort steckt in einer Übung, die du vielleicht schon gerechnet hast: In Übung 14.3 hat das Umpolen einer einzigen Quelle das gesamte Interferenzmuster invertiert — hell wurde dunkel, dunkel wurde hell. Die Phase einer Quelle ist ein ebenso scharfes Steuer wie ihr Ort. Dieses Kapitel macht aus dem Trick ein Werkzeug: viele gleiche Strahler in einer Reihe, jeder mit einem eigenen Phasenknopf — ein Phased Array. Der Strahl folgt dann nicht der Geometrie, sondern dem Zündplan.

Lernziele

Nach diesem Kapitel kannst du …

  1. … mit „Umweg = Vorsprung” erklären und ausrechnen, wohin die Keule einer Strahlerreihe kippt: \(\sin\theta_0 = \Delta\Phi/(k\,d)\),
  2. … den Array-Faktor als Zeigersumme hinschreiben, in NumPy auswerten und zeigen, dass \(N = 2\) exakt der Doppelspalt aus Kapitel 14 ist,
  3. Grating Lobes vorhersagen (Geisterkeulen bei \(\sin\theta = \sin\theta_0 \pm m\lambda/d\)) und die Designregel für den Elementabstand herleiten,
  4. … mit dem Faltungssatz Gesamtdiagramme zerlegen (Element × Array-Faktor) und mit Amplituden-Tapern Nebenkeulen gegen Keulenbreite eintauschen — und du weißt, warum das exakt die Fensterfunktionen der Spektralanalyse sind,
  5. Scan Loss (die cos-Regel der projizierten Apertur) und Empfangs-Beamforming (Reziprozität, Null-Steuern) erklären.
import numpy as np
import matplotlib.pyplot as plt

23.1 Umweg = Vorsprung: die Geometrie des Schwenks

Stell dir zwei Strahler im Abstand \(d\) übereinander vor, beide senden dieselbe Frequenz. Ein weit entfernter Beobachter steht in Richtung \(\theta\) schräg oberhalb der Mittelsenkrechten (der „Breitseite” der Reihe). Für ihn ist der obere Strahler ein Stück näher — die Welle von unten muss einen Umweg laufen, und ein Blick auf das rechtwinklige Dreieck aus Abstand \(d\) und Blickrichtung zeigt: Der Umweg ist genau die Projektion

\[\Delta s = d\,\sin\theta.\]

Senden beide Strahler gleichphasig, addieren sich ihre Wellen dort konstruktiv, wo der Umweg verschwindet — die Keule zeigt zur Breitseite, \(\theta = 0\). Das ist der Doppelspalt aus Kapitel 14, rückwärts gelesen: nicht „wo wird es hell?“, sondern „wohin schicken wir das Hell?”.

Jetzt kommt der Phasenknopf. Lassen wir den oberen Strahler um die Phase \(\Delta\Phi\) später zünden, dann gleicht sein Zeitrückstand den Wegvorsprung genau dort aus, wo

\[k\,d\,\sin\theta_0 = \Delta\Phi \quad\Longrightarrow\quad \sin\theta_0 = \frac{\Delta\Phi}{k\,d}, \qquad k = \frac{2\pi}{\lambda}.\]

In dieser Richtung kommen beide Wellenberge wieder gleichzeitig an — die Keule ist zur Seite der später zündenden Quelle gekippt. Umweg = Vorsprung: Der geometrische Umweg wird durch den elektrischen Vorsprung bezahlt, und der Zündplan bestimmt die Richtung.

Eine Handrechnung mit Zahlen, wie immer. Acht Strahler im Abstand \(d = \lambda/2\), jeder zündet \(90°\) nach seinem unteren Nachbarn:

\[\sin\theta_0 = \frac{\Delta\Phi}{k\,d} = \frac{\pi/2}{(2\pi/\lambda)\cdot(\lambda/2)} = \frac{\pi/2}{\pi} = 0{,}5 \quad\Longrightarrow\quad \theta_0 = 30°.\]

Heraus kommt ein Winkel, ein Skalar. Dreht man den Phasenknopf auf \(135°\), wandert die Keule auf \(\arcsin(0{,}75) = 48{,}6°\) — elektronisch, in Nanosekunden, ohne dass sich ein Gramm Metall bewegt.

Das anschaulichste Bild für den Mechanismus ist die Stadion-La-Ola: Jeder Strahler „steht auf”, kurz nachdem sein Nachbar aufgestanden ist. Die Wellenberge aller Strahler liegen dann auf einer gekippten Linie — und diese gekippte Linie ist die neue Wellenfront. Sie läuft senkrecht zu sich selbst davon, also in die Richtung \(\theta_0\).

Der folgende Film zeigt die La-Ola im Feld. Acht Punktquellen senden dieselbe Frequenz, jede um \(\Delta\Phi = 90°\) später als ihre untere Nachbarin; gezeichnet ist die Summe ihrer Kreiswellen (jede mit der \(1/\sqrt{r}\)-Amplitude aus Kapitel 9). Achte darauf, wie die Berge sich zu schrägen Fronten ordnen, die exakt senkrecht zur gestrichelten 30°-Theorielinie laufen — und wie nahe an der Quellreihe noch das Gewimmel der Einzelwellen sichtbar ist.

Code der Animation (nur in der HTML-Fassung)
# von oben: nichts (eigenständige Fernfeld-Summe)
from matplotlib import animation
from IPython.display import HTML

N_Q, D_Q, DPHI = 8, 0.5, np.deg2rad(90)
k = 2 * np.pi                                  # lambda = 1

x = np.linspace(0.3, 12, 360)
y = np.linspace(-6, 6, 360)
X, Y = np.meshgrid(x, y)
y_quellen = (np.arange(N_Q) - (N_Q - 1) / 2) * D_Q

bilder = []
phasen_t = np.linspace(0, 2 * np.pi, 16, endpoint=False)
for wt in phasen_t:
    feld = np.zeros_like(X)
    for n, yq in enumerate(y_quellen):
        r = np.hypot(X, Y - yq)
        # Kreiswelle der Quelle n: zündet um n*DPHI später
        feld += np.cos(k * r - wt + n * DPHI) / np.sqrt(r)
    bilder.append(feld)

vmax = np.percentile(np.abs(bilder[0]), 99.0)
fig, ax = plt.subplots(figsize=(6.6, 5.4))
im = ax.imshow(bilder[0], origin="lower", extent=(0.3, 12, -6, 6),
               cmap="RdBu_r", vmin=-vmax, vmax=vmax)
ax.plot(np.zeros(N_Q), y_quellen, "ko", ms=4)
th0 = np.arcsin(DPHI / (k * D_Q))
ax.plot([0, 12], [0, 12 * np.tan(th0)], "k--", lw=1.4)
ax.text(8.0, 8.0 * np.tan(th0) - 1.1, "Theorie: 30°", fontsize=10)
ax.set_xlabel("x in λ")
ax.set_ylabel("y in λ")
ax.set_title("La-Ola: jede Quelle zündet 90° später — "
             "die Front kippt auf 30°")

def bild(i):
    im.set_data(bilder[i])
    return [im]

anim = animation.FuncAnimation(fig, bild, frames=len(bilder),
                               interval=120)
plt.close(fig)
HTML(anim.to_jshtml(default_mode="loop"))
WarnungNaheliegende Vermutung: „Antennen strahlen, wohin sie zeigen“

Naheliegende Vermutung: Eine Antenne strahlt dorthin, wohin sie zeigt. Wer die Richtung ändern will, muss die Antenne drehen — wie eine Taschenlampe.

Warum sie naheliegt: Bei allem, was wir aus dem Alltag kennen, bestimmt die Geometrie die Richtung: Taschenlampe, Parabolspiegel, Satellitenschüssel. Auch das Flughafen-Radar dreht sich ja tatsächlich.

Was stattdessen stimmt: Die Richtung der Abstrahlung steckt in den relativen Phasen der strahlenden Ströme. Eine starre Reihe gleicher Strahler plus ein einstellbarer Zündplan genügt: \(\sin\theta_0 = \Delta\Phi/(k\,d)\). Die Geometrie legt nur fest, welche Richtungen erreichbar sind und wie scharf die Keule wird — wohin sie zeigt, entscheidet die Elektronik. Genau deshalb kann ein flaches, unbewegliches Starlink-Panel einen rasenden Satelliten verfolgen.

23.2 Der Array-Faktor: die Zeigersumme bekommt einen Phasenknopf

Wie hell ist es in einer beliebigen Richtung \(\theta\)? In Kapitel 14 haben wir dafür Zeiger addiert: Jede Quelle trägt im Fernfeld einen Zeiger bei, dessen Drehwinkel ihr Gangunterschied bestimmt. Neu ist nur, dass jetzt auch der Zündplan am Drehwinkel mitdreht. Quelle \(n\) (von unten gezählt) hat gegenüber Quelle 0 den Wegvorsprung \(n\,d\sin\theta\) (Phase \(+n\,k d\sin\theta\)) und die Zündverspätung \(n\,\Delta\Phi\) (Phase \(-n\,\Delta\Phi\)). Mit der Amplituden-„Belegung” \(a_n\) (vorerst alle \(1\)) lautet die Zeigersumme

\[F(\theta) \;=\; \sum_{n=0}^{N-1} a_n\, e^{\,\mathrm{i}\,n\,(k d \sin\theta \,-\, \Delta\Phi)}, \qquad \text{Leistung} \;\propto\; |F(\theta)|^2.\]

\(|F|^2\) heißt Array-Faktor (genauer: Leistungs-Array-Faktor) — „Faktor”, weil er später mit dem Diagramm des Einzelelements multipliziert wird; für unsere in der Ebene rundum gleich strahlenden Punktquellen ist er schon das ganze Diagramm. Das ist ein Skalarfeld über dem Winkel: Für jede Richtung \(\theta\) kommt eine Zahl heraus.

In NumPy ist die Summe vier Zeilen — und weil sie das Arbeitstier des ganzen Kapitels wird, falten wir sie einmal auseinander:

# von oben: nichts — k = 2π/λ mit λ = 1 (Meep-Konvention, Kap. 11)
k = 2 * np.pi


def array_faktor(theta, belegung, d, dphi_grad=0.0, normiert=True):
    """Leistungs-Array-Faktor |Σ a_n·exp(i·n·(kd·sinθ − ΔΦ))|².

    Konvention: Element n zündet um n·ΔΦ später als Element 0
    (die La-Ola läuft nach oben) → die Keule kippt nach +θ.
    """
    n = np.arange(len(belegung))                  # 0, 1, …, N−1
    # Drehwinkel pro Element-Schritt, für JEDE Richtung theta:
    psi = k * d * np.sin(theta) - np.deg2rad(dphi_grad)
    # Zeiger e^{i·n·ψ}: Zeilen = Richtungen, Spalten = Elemente
    # (Broadcasting [:, None]/[None, :] wie in Kap. 9)
    zeiger = np.asarray(belegung)[None, :] * np.exp(
        1j * n[None, :] * psi[:, None])
    F = zeiger.sum(axis=1)                        # die Zeigersumme
    P = np.abs(F) ** 2                            # Leistung
    return P / P.max() if normiert else P

Die Längen-Buchhaltung: theta hat \(M\) Richtungen, belegung \(N\) Elemente. psi hat \(M\) Einträge (ein Drehwinkel pro Richtung), die Zeiger-Matrix \(M \times N\) (jede Richtung bekommt alle \(N\) Zeiger), und zeiger.sum(axis=1) addiert pro Zeile die \(N\) Zeiger — übrig bleiben \(M\) komplexe Summen, eine pro Richtung.

Bevor wir damit schwenken, die Pflichtprüfung am Bekannten: Für \(N = 2\) und \(\Delta\Phi = 0\) muss exakt das Doppelspalt-\(\cos^2\) aus Kapitel 14 herauskommen, \(\cos^2(\pi d \sin\theta/\lambda)\) — zwei Zeiger der Länge 1 addieren sich je nach Zwischenwinkel:

# von oben: array_faktor(), k
theta = np.linspace(-np.pi / 2, np.pi / 2, 100001)

P_zwei = array_faktor(theta, [1, 1], d=0.5)
doppelspalt = np.cos(np.pi * 0.5 * np.sin(theta)) ** 2
print(f"N = 2 gegen das Kap.-14-cos²: max. Abweichung "
      f"{np.abs(P_zwei - doppelspalt).max():.1e}")
N = 2 gegen das Kap.-14-cos²: max. Abweichung 4.4e-16

Maschinengenauigkeit — der Array-Faktor ist die Doppelspaltformel, verallgemeinert auf \(N\) Quellen und einen Phasenknopf.

23.2.1 Mehr Elemente, schärfere Keule

Vorhersage-Punkt (PRIMM): Wir verdoppeln gleich die Elementzahl von 4 auf 8 auf 16 (Abstand fest \(d = \lambda/2\)). Was passiert mit der Breite der Hauptkeule — halbiert sie sich, viertelt sie sich, oder bleibt sie gleich? Denk an das Beugungsgitter aus Kapitel 14: Dort machten \(N\) Spalte die Maxima \(N\)-fach schmal.

Die Theorie gibt die Antwort exakt für die erste Nullstelle neben der Hauptkeule: Die Zeigersumme wird null, sobald die \(N\) Zeiger den Kreis einmal schließen (das Zeigerbild aus Kapitel 14), also bei \(N\,k d\sin\theta = 2\pi\) oder

\[\sin\theta_{\text{Null}} = \frac{\lambda}{N\,d}.\]

Die Keulenbreite fällt wie \(1/N\) — im Nenner steht \(N\,d\), die Gesamtlänge des Arrays in Wellenlängen. Es ist dieselbe Regel wie überall in diesem Buch, von der Spaltbreite (Kapitel 14) bis zum dicken Dipol (Kapitel 21): Große Strukturen bündeln scharf.

# von oben: array_faktor(), theta
fig, ax = plt.subplots(figsize=(7.0, 4.2))
for N, farbe in ((4, "tab:blue"), (8, "tab:green"), (16, "tab:red")):
    P = array_faktor(theta, np.ones(N), d=0.5)
    ax.plot(np.degrees(theta), 10 * np.log10(np.maximum(P, 1e-5)),
            color=farbe, lw=1.4, label=f"N = {N}")
    null = np.degrees(np.arcsin(1 / (N * 0.5)))
    ax.axvline(+null, color=farbe, ls="--", lw=0.9, alpha=0.7)
    ax.axvline(-null, color=farbe, ls="--", lw=0.9, alpha=0.7)
ax.set_xlim(-90, 90)
ax.set_ylim(-40, 2)
ax.set_xlabel("Winkel θ zur Breitseite (°)")
ax.set_ylabel("Array-Faktor (dB)")
ax.set_title("Verdopplung der Elemente halbiert die Keule "
             "(gestrichelt: Theorie-Nullstellen)")
ax.legend()
plt.show()

P_8 = array_faktor(theta, np.ones(8), d=0.5)
i_min = np.flatnonzero((P_8[1:-1] < P_8[:-2]) & (P_8[1:-1] < P_8[2:])) + 1
nullen = np.degrees(theta[i_min])
print(f"N = 8: erste Nullstelle gemessen {nullen[nullen > 0][0]:.2f}°, "
      f"Theorie {np.degrees(np.arcsin(1 / 4)):.2f}°")
Abbildung 23.1: Der Array-Faktor für 4, 8 und 16 Elemente (d = λ/2, ΔΦ = 0) in Dezibel. Die gestrichelten Senkrechten markieren die theoretische erste Nullstelle sin θ = λ/(N·d) — mit jeder Verdopplung der Elementzahl halbiert sich die Keulenbreite. Die Nebenkeulen bleiben dabei hartnäckig auf etwa −13 dB stehen; um sie kümmert sich der Taper-Abschnitt.
N = 8: erste Nullstelle gemessen 14.48°, Theorie 14.48°

Die Antwort auf den Vorhersage-Punkt: Sie halbiert sich — die Nullstelle rückt von \(\arcsin(1/2) = 30°\) (\(N = 4\)) auf \(14{,}48°\) (\(N = 8\)) auf \(7{,}18°\) (\(N = 16\)), und die Messung trifft die Theorie auf zwei Nachkommastellen.

23.2.2 Der Schwenk, vermessen

Jetzt drehen wir den Phasenknopf. Erfolgskriterium: Das Maximum des Array-Faktors (per np.argmax) liegt für jede Einstellung auf \(\arcsin(\Delta\Phi/(k d))\):

# von oben: array_faktor(), theta
print("ΔΦ      Keule (argmax)   Theorie arcsin(ΔΦ/kd)")
for dphi in (0, 45, 90, 135):
    P = array_faktor(theta, np.ones(8), d=0.5, dphi_grad=dphi)
    mess = np.degrees(theta[np.argmax(P)])
    theo = np.degrees(np.arcsin(np.deg2rad(dphi) / (k * 0.5)))
    print(f"{dphi:3d}°    {mess:+7.2f}°        {theo:+7.2f}°")
ΔΦ      Keule (argmax)   Theorie arcsin(ΔΦ/kd)
  0°      -0.00°          +0.00°
 45°     +14.48°         +14.48°
 90°     +30.00°         +30.00°
135°     +48.59°         +48.59°

Vier Einstellungen, vier Treffer auf zwei Nachkommastellen — die Handrechnung von oben (\(90° \to 30°\), \(135° \to 48{,}59°\)) steht in der Tabelle.

Hier ist die Spielwiese dazu. Sage vor jedem Lauf vorher, was passieren wird: Was macht DPHI_GRAD = 180? (Tipp: Für \(d = \lambda/2\) ist dann \(\sin\theta_0 = 1\) — die Keule legt sich flach auf die Arrayachse.) Was passiert bei D_LAMBDA = 1.5 — und woher kommen die zusätzlichen Keulen? (Der nächste Abschnitt klärt es.) Und wie ändert BELEGUNG = "dreieck" die Nebenkeulen?

23.3 Maxwell bestätigt: das Feldbild in Meep

Der Array-Faktor ist Fernfeld-Geometrie — Zeiger, Umwege, Phasen. Aber rechnet das volle Maxwell-System wirklich so? Wir prüfen es in der 2D-Welt aus Kapitel 9, mit Meep als Arbeitspferd (Kapitel 11).

Der Versuchsaufbau. Frage: Kippen die Wellenfronten einer phasengestaffelt gespeisten Quellreihe wirklich um \(\theta_0 = \arcsin(\Delta\Phi/(k d))\), und folgt die Strahlungskeule? Bühne: eine \(16\lambda \times 16\lambda\) große 2D-Zelle, ringsum 1 λ PML; acht \(E_z\)-Punktquellen im Abstand \(d = \lambda/2\) entlang \(y\), als Reihe bei \(x = -5\lambda\). Anfangs-/Randbedingungen: Feld startet bei null, die Quellen senden Dauerstrich (\(f = 1\)), der Lauf endet bei \(t = 40\) — die Zelle ist dann längst eingeschwungen (Laufzeit quer: 16 Zeiteinheiten). Messgrößen: das komplexe \(E_z\)-Feld der ganzen Zelle; daraus das Momentanbild \(\mathrm{Re}(E_z)\) und die zeitgemittelte Intensität \(|E_z|^2\). Erfolgskriterium: Front- und Keulenrichtung liegen auf der eingezeichneten Theorielinie, und als absoluter Gesundheitscheck (die Lektion aus Kapitel 21!) muss \(\max|E_z|\) in der Größenordnung 1 liegen — nicht \(10^{-19}\).

Zwei Meep-Bausteine sind neu. Erstens force_complex_fields=True: Meep rechnet normalerweise mit reellen Feldern; mit komplexen Feldern liefert ein eingeschwungener Dauerstrich-Lauf gleich beides — Momentanbild (Realteil) und Zeitmittel (\(|E_z|^2\)), ganz ohne Periodenmittelung wie in Kapitel 18. Zweitens bekommt mp.Source eine komplexe Amplitude: Der Faktor \(e^{\mathrm{i} n \Delta\Phi}\) ist mit Meeps \(e^{-\mathrm{i}\omega t}\)-Zeitkonvention genau die Zündverspätung um \(n\,\Delta\Phi\) — das Zeitsignal der Quelle \(n\) ist \(\cos(\omega t - n\Delta\Phi)\), unser Zündplan.

# von oben: k; ab hier Meep (Kap. 11): λ = 1, f = 1
import meep as mp
mp.verbosity(0)

N_EL, D_EL = 8, 0.5
SX = SY = 16
X_ARRAY = -5.0


def cw_feld(dphi_grad):
    """Eingeschwungener Dauerstrich-Lauf; liefert das komplexe Ez."""
    y0 = -(N_EL - 1) * D_EL / 2
    quellen = [
        mp.Source(mp.ContinuousSource(frequency=1.0), component=mp.Ez,
                  center=mp.Vector3(X_ARRAY, y0 + n * D_EL),
                  # komplexe Amplitude = Phasenknopf: Element n
                  # zündet um n·ΔΦ später (Meep-Konvention e^{−iωt})
                  amplitude=np.exp(1j * np.deg2rad(dphi_grad) * n))
        for n in range(N_EL)]
    sim = mp.Simulation(cell_size=mp.Vector3(SX, SY),
                        boundary_layers=[mp.PML(1.0)],
                        sources=quellen, resolution=16,
                        # komplexe Felder: Momentanbild UND |Ez|²
                        # aus einem einzigen Lauf
                        force_complex_fields=True)
    sim.run(until=40)
    return sim.get_array(center=mp.Vector3(), size=mp.Vector3(SX, SY),
                         component=mp.Ez)


PHASEN = list(range(0, 136, 15))          # 0°, 15°, …, 135°
felder = {p: cw_feld(p) for p in PHASEN}
# von oben: felder, PHASEN
print(f"Gesundheitscheck: max|Ez| (ΔΦ = 90°) = "
      f"{np.abs(felder[90]).max():.3f}")
Gesundheitscheck: max|Ez| (ΔΦ = 90°) = 2.128

Die Amplitude liegt bei 2 — die Quellen speisen wirklich ins Feld (nach dem Speisespalt-Debakel aus Kapitel 21 prüfen wir das grundsätzlich, bevor wir hübsche normierte Bilder ansehen).

# von oben: felder, N_EL, D_EL, SX, SY, X_ARRAY, k
ext = (-SX / 2, SX / 2, -SY / 2, SY / 2)
y_q = (np.arange(N_EL) - (N_EL - 1) / 2) * D_EL


def keulenlinie(ax, dphi_grad, farbe="k"):
    th0 = np.arcsin(np.deg2rad(dphi_grad) / (k * D_EL))
    ax.plot([X_ARRAY, X_ARRAY + 11.5 * np.cos(th0)],
            [0, 11.5 * np.sin(th0)], farbe + "--", lw=1.2, alpha=0.85)


fig, ax = plt.subplots(2, 3, figsize=(7.6, 5.3))
for j, p in enumerate((0, 90, 135)):
    ez = felder[p]
    re = np.real(ez).T                    # Meep-Array: (x, y) → Bild (y, x)
    inten = (np.abs(ez) ** 2).T
    th0 = np.degrees(np.arcsin(np.deg2rad(p) / (k * D_EL)))

    a = ax[0, j]
    a.imshow(re, origin="lower", extent=ext, cmap="RdBu_r",
             vmin=-np.abs(re).max() * 0.35, vmax=np.abs(re).max() * 0.35)
    a.plot([X_ARRAY] * N_EL, y_q, "k.", ms=3)
    keulenlinie(a, p)
    a.set_title(f"Re(Ez) — ΔΦ = {p}°", fontsize=9)
    a.set_xticks([]); a.set_yticks([])

    a = ax[1, j]
    a.imshow(inten / inten.max(), origin="lower", extent=ext,
             cmap="inferno", vmin=0, vmax=0.25)
    a.plot([X_ARRAY] * N_EL, y_q, "w.", ms=3)
    keulenlinie(a, p, "w")
    a.set_title(f"|Ez|² — Keule bei {th0:.0f}°", fontsize=9)
    a.set_xticks([]); a.set_yticks([])
fig.suptitle("Acht Quellen, drei Zündpläne: die Keule folgt dem "
             "Phasenknopf", fontsize=11)
fig.tight_layout()
plt.show()
Abbildung 23.2: Dieselbe Strahlerreihe, drei Zündpläne — oben das Momentanfeld Re(Ez) (rot/blau = Berg/Tal), unten die zeitgemittelte Intensität |Ez|² desselben Laufs. Bei ΔΦ = 0 laufen die Fronten parallel zur Reihe und die Keule zeigt zur Breitseite; bei 90° und 135° kippen die Fronten, und die Keule folgt exakt der gestrichelten Theorielinie arcsin(ΔΦ/kd) (30° bzw. 48,6°). Nichts an der Geometrie hat sich geändert — nur der Zündplan.

Oben sieht man den La-Ola-Mechanismus leibhaftig: Die Wellenberge liegen auf gekippten Linien, und je weiter der Phasenknopf gedreht wird, desto steiler die Front. Unten bestätigt die Intensität, dass die Energie der gekippten Front folgt — die Keule liegt auf der Theorielinie. Auffällig auch die Verbreiterung der Keule beim Schwenken (vergleiche 135° mit 0°): Das ist der Scan Loss, den wir weiter unten vermessen.

Im Film sieht man die Fronten laufen — und weil der Lauf mit komplexen Feldern gerechnet ist, kostet das keine neue Simulation: Eine Periode entsteht aus dem eingeschwungenen komplexen Feld als \(\mathrm{Re}(E_z\,e^{-\mathrm{i}\varphi})\) mit \(\varphi\) von 0 bis \(2\pi\) — der Zeiger-Trick aus der Moden-Animation von Kapitel 15.

Code der Animation (nur in der HTML-Fassung)
# von oben: felder, ext, y_q, N_EL, D_EL, X_ARRAY, k
from matplotlib import animation
from IPython.display import HTML

ez = felder[90]
vmax = np.abs(ez).max() * 0.35
fig, ax = plt.subplots(figsize=(5.8, 5.2))
im = ax.imshow(np.real(ez).T, origin="lower", extent=ext,
               cmap="RdBu_r", vmin=-vmax, vmax=vmax)
ax.plot([X_ARRAY] * N_EL, y_q, "k.", ms=4)
keulenlinie(ax, 90)
ax.set_xticks([]); ax.set_yticks([])
ax.set_title("ΔΦ = 90°: die gekippte Front läuft auf der 30°-Linie")

phis = np.linspace(0, 2 * np.pi, 16, endpoint=False)

def bild(i):
    im.set_data(np.real(ez * np.exp(-1j * phis[i])).T)
    return [im]

anim = animation.FuncAnimation(fig, bild, frames=len(phis),
                               interval=120)
plt.close(fig)
HTML(anim.to_jshtml(default_mode="loop"))

Und hier der Schwenk durch alle Zündpläne — die zehn schon gerechneten Läufe von \(\Delta\Phi = 0°\) bis \(135°\) als Intensitätsfilm, vor und zurück. Das ist die Bewegung, die beim Starlink-Terminal viele tausend Mal pro Sekunde passiert:

Code der Animation (nur in der HTML-Fassung)
# von oben: felder, PHASEN, ext, y_q, N_EL, D_EL, X_ARRAY, k
folge = PHASEN + PHASEN[-2::-1]            # hin und zurück
inten0 = np.abs(felder[0]) ** 2

fig, ax = plt.subplots(figsize=(5.8, 5.2))
im = ax.imshow(inten0.T / inten0.max(), origin="lower", extent=ext,
               cmap="inferno", vmin=0, vmax=0.25)
ax.plot([X_ARRAY] * N_EL, y_q, "w.", ms=4)
linie, = ax.plot([], [], "w--", lw=1.2, alpha=0.85)
ax.set_xticks([]); ax.set_yticks([])
titel = ax.set_title("")

def bild(i):
    p = folge[i]
    inten = np.abs(felder[p]) ** 2
    im.set_data(inten.T / inten.max())
    th0 = np.arcsin(np.deg2rad(p) / (k * D_EL))
    linie.set_data([X_ARRAY, X_ARRAY + 11.5 * np.cos(th0)],
                   [0, 11.5 * np.sin(th0)])
    titel.set_text(f"ΔΦ = {p:3d}°  →  Keule bei "
                   f"{np.degrees(th0):+.0f}°")
    return [im, linie, titel]

anim = animation.FuncAnimation(fig, bild, frames=len(folge),
                               interval=300)
plt.close(fig)
HTML(anim.to_jshtml(default_mode="loop"))

23.3.1 Die Nagelprobe: Fernfeld gegen Array-Faktor

Die Feldbilder überzeugen das Auge — jetzt die Zahlen. Der Versuchsaufbau: Frage: Ist das Maxwell-Richtdiagramm der Quellreihe wirklich der Array-Faktor, Punkt für Punkt? Bühne: eine kompakte \(4\lambda \times 8\lambda\)-Zelle (1 λ PML), dieselben acht Quellen mit \(\Delta\Phi = 90°\) — diesmal als Gauß-Puls um \(f = 1\), denn das near2far-Werkzeug aus Kapitel 14 sammelt sein Fernfeld am liebsten aus einem abklingenden Lauf. Randbedingungen: Die near2far-Box (vier Kanten, Außennormalen-Gewichte wie bei der 3D-Box in Kapitel 21) umschließt die Reihe vollständig und bleibt aus der PML. Messgrößen: \(|\vec S|(\theta)\) auf 179 Richtungen in 1000 λ Abstand, normiert. Erfolgskriterium: rms-Abweichung vom Array-Faktor unter 0,01 — und die Keule bei \(+30°\).

# von oben: N_EL, D_EL (8, 0.5), array_faktor(), k
y0 = -(N_EL - 1) * D_EL / 2
quellen_puls = [
    mp.Source(mp.GaussianSource(frequency=1.0, fwidth=0.3),
              component=mp.Ez, center=mp.Vector3(0, y0 + n * D_EL),
              amplitude=np.exp(1j * np.deg2rad(90) * n))
    for n in range(N_EL)]
sim_ff = mp.Simulation(cell_size=mp.Vector3(4, 8),
                       boundary_layers=[mp.PML(1.0)],
                       sources=quellen_puls, resolution=16,
                       force_complex_fields=True)
bx, by = 0.6, 2.4
n2f = sim_ff.add_near2far(
    1.0, 0, 1,
    mp.Near2FarRegion(mp.Vector3(+bx, 0), size=mp.Vector3(0, 2 * by)),
    mp.Near2FarRegion(mp.Vector3(-bx, 0), size=mp.Vector3(0, 2 * by),
                      weight=-1),
    mp.Near2FarRegion(mp.Vector3(0, +by), size=mp.Vector3(2 * bx, 0)),
    mp.Near2FarRegion(mp.Vector3(0, -by), size=mp.Vector3(2 * bx, 0),
                      weight=-1))
sim_ff.run(until_after_sources=mp.stop_when_fields_decayed(
    5, mp.Ez, mp.Vector3(0.5, 0), 1e-5))

th_ff = np.radians(np.linspace(-89, 89, 179))
S_ff = np.empty(len(th_ff))
for i, t in enumerate(th_ff):
    Ex, Ey, Ez, Hx, Hy, Hz = sim_ff.get_farfield(
        n2f, 1000.0 * mp.Vector3(np.cos(t), np.sin(t)))
    # zeitgemittelter Poynting-Vektor S = ½ Re(E × H*) (Kap. 18/21)
    Sx = 0.5 * np.real(Ey * np.conj(Hz) - Ez * np.conj(Hy))
    Sy = 0.5 * np.real(Ez * np.conj(Hx) - Ex * np.conj(Hz))
    S_ff[i] = np.hypot(Sx, Sy)
S_ff = S_ff / S_ff.max()
# von oben: th_ff, S_ff, array_faktor()
AF = array_faktor(th_ff, np.ones(N_EL), D_EL, dphi_grad=90)

fig, ax = plt.subplots(figsize=(7.0, 4.2))
ax.plot(np.degrees(th_ff), 10 * np.log10(np.maximum(AF, 1e-5)),
        "r-", lw=1.6, label="Array-Faktor (Zeigersumme)")
ax.plot(np.degrees(th_ff), 10 * np.log10(np.maximum(S_ff, 1e-5)),
        "bo", ms=3.5, label="Meep: near2far-Fernfeld")
ax.axvline(30, color="k", ls=":", lw=1)
ax.set_xlim(-90, 90)
ax.set_ylim(-40, 2)
ax.set_xlabel("Winkel θ zur Breitseite (°)")
ax.set_ylabel("Leistung, normiert (dB)")
ax.set_title("Maxwell gegen Zeigersumme: dasselbe Diagramm")
ax.legend(loc="lower left")
plt.show()

rms = np.sqrt(np.mean((S_ff - AF) ** 2))
print(f"rms-Abweichung Meep ↔ Array-Faktor: {rms:.4f}")
print(f"Meep-Keule bei {np.degrees(th_ff[np.argmax(S_ff)]):+.1f}°  "
      "(Theorie +30°)")
Abbildung 23.3: Das Maxwell-Fernfeld (blaue Punkte: Meep near2far, ein Pulslauf) gegen die Zeigersumme (rote Linie: Array-Faktor) für acht Quellen mit ΔΦ = 90°, in Dezibel. Beide Kurven liegen aufeinander — Hauptkeule bei +30°, alle Nebenkeulen an Ort und Pegel getroffen. Das volle Maxwell-System und die Fernfeld-Geometrie aus vier Zeilen NumPy sind dasselbe Rechenwerk.
rms-Abweichung Meep ↔ Array-Faktor: 0.0018
Meep-Keule bei +30.0°  (Theorie +30°)

Eine rms-Abweichung von 0,002 bei normierten Kurven: Das Maxwell-Fernfeld und die Zeigersumme sind deckungsgleich. Für Punktquellen (in der 2D-Ebene rundum gleich strahlend) ist der Array-Faktor also nicht Näherung, sondern das exakte Richtdiagramm — und wir dürfen die schnelle NumPy-Formel mit gutem Gewissen für alles Weitere benutzen.

23.4 Grating Lobes: die Geisterkeulen

Vorhersage-Punkt (PRIMM): Bisher stand der Elementabstand stur auf \(d = \lambda/2\). Was passiert, wenn wir die acht Quellen auseinanderziehen, auf \(d = 1{,}5\,\lambda\) — wird die Keule einfach schmaler (das Array ist ja jetzt dreimal so lang), oder geht etwas schief? Übung 14.4 hat die Antwort schon einmal von der anderen Seite gestreift.

Die Zeigersumme verrät es: Alle \(N\) Zeiger stehen nicht nur dann parallel, wenn der Drehwinkel pro Element null ist, sondern auch, wenn er ein ganzes Vielfaches von \(2\pi\) beträgt. Konstruktive Überlagerung also überall dort, wo

\[k d \sin\theta - \Delta\Phi = 2\pi m \quad\Longrightarrow\quad \sin\theta = \sin\theta_0 + m\,\frac{\lambda}{d}, \qquad m = 0, \pm 1, \pm 2, \dots\]

Das ist wortwörtlich die Gitterformel aus Kapitel 14 — dort wollten wir die hohen Ordnungen (sie sortierten die Farben), hier sind sie ein Ärgernis: Jede Lösung mit \(m \neq 0\) ist eine Grating Lobe („Gitterkeule”), eine Geisterkeule in voller Stärke, die dieselbe Leistung in eine ungewollte Richtung schickt. Ein Radar mit Grating Lobes sieht Geisterechos; ein Starlink-Panel würde fremde Satelliten stören. Für \(d \le \lambda/2\) (und Schwenk bis zum Horizont) hat \(\sin\theta = \sin\theta_0 \pm \lambda/d\) keine Lösung — deshalb unser braver Standardabstand.

# von oben: array_faktor(), theta
fig, ax = plt.subplots(figsize=(7.0, 4.2))
for d, farbe in ((0.5, "tab:blue"), (1.5, "tab:red")):
    P = array_faktor(theta, np.ones(8), d=d)
    ax.plot(np.degrees(theta), 10 * np.log10(np.maximum(P, 1e-5)),
            color=farbe, lw=1.4, label=f"d = {d} λ")
for w in (-41.8, 41.8):
    ax.axvline(w, color="tab:red", ls=":", lw=1.1)
ax.set_xlim(-90, 90)
ax.set_ylim(-40, 2)
ax.set_xlabel("Winkel θ zur Breitseite (°)")
ax.set_ylabel("Array-Faktor (dB)")
ax.set_title("d über λ: Geisterkeulen bei sin θ = ±λ/d")
ax.legend()
plt.show()
Abbildung 23.4: Acht Elemente ohne Schwenk (ΔΦ = 0), nur der Abstand wächst: Bei d = λ/2 (blau) gibt es eine einzige Hauptkeule; bei d = 1,5 λ (rot) tauchen Geisterkeulen in voller Höhe bei sin θ = ±λ/d, also ±41,8° auf (gepunktete Theorie-Senkrechten). Die Hauptkeule selbst ist schmaler geworden — das längere Array bündelt schärfer —, aber der Gewinn ist wertlos, wenn zwei Geister dieselbe Leistung woandershin schicken.

Die Antwort auf den Vorhersage-Punkt: beides. Die Hauptkeule wird schmaler — und bekommt zwei Geschwister in voller Höhe. Dass Maxwell das genauso sieht, zeigt das Feldbild — sechs weit auseinandergezogene Quellen, und die Zelle füllt sich mit drei Strahlbündeln:

# von oben: cw_feld() — Achtung, wir verstellen kurz N_EL und D_EL
N_EL, D_EL = 6, 1.5
ez_g = cw_feld(0)
print(f"Gesundheitscheck: max|Ez| = {np.abs(ez_g).max():.3f}")
y_q6 = (np.arange(N_EL) - (N_EL - 1) / 2) * D_EL

fig, ax = plt.subplots(figsize=(5.6, 5.2))
inten = (np.abs(ez_g) ** 2).T
ax.imshow(inten / inten.max(), origin="lower", extent=ext,
          cmap="inferno", vmin=0, vmax=0.25)
ax.plot([X_ARRAY] * N_EL, y_q6, "w.", ms=4)
for vorz in (-1, 0, +1):
    th_g = vorz * np.arcsin(1 / D_EL)
    ax.plot([X_ARRAY, X_ARRAY + 11.5 * np.cos(th_g)],
            [0, 11.5 * np.sin(th_g)], "w--", lw=1.1, alpha=0.85)
ax.set_xticks([]); ax.set_yticks([])
ax.set_title("d = 1,5 λ: ein Strahl bestellt, drei geliefert")
plt.show()
N_EL, D_EL = 8, 0.5
Gesundheitscheck: max|Ez| = 1.714
Abbildung 23.5: Das Meep-Feldbild zur Geisterkeulen-Formel: sechs Quellen im Abstand d = 1,5 λ, gleichphasig gespeist (ΔΦ = 0). Die zeitgemittelte Intensität zeigt drei Strahlbündel — die gewollte Hauptkeule zur Breitseite und die beiden Grating Lobes auf den gestrichelten ±41,8°-Theorielinien. Das Array verschenkt zwei Drittel seiner gebündelten Leistung an die Geister.

Beim Schwenken verschärft sich die Lage. Die Geister sitzen bei \(\sin\theta = \sin\theta_0 \pm \lambda/d\); schwenkt man die Hauptkeule auf \(\theta_0\), rückt der Geist auf der Gegenseite näher an den sichtbaren Bereich (\(|\sin\theta| \le 1\)) heran. Er bleibt genau dann draußen, wenn

\[\frac{d}{\lambda} \;<\; \frac{1}{1 + |\sin\theta_0|}.\]

Wer bis \(\pm 90°\) schwenken will, landet wieder bei \(d < \lambda/2\); wer nur \(\pm 30°\) braucht, darf sich \(d < 2\lambda/3\) leisten — mehr Abstand heißt weniger Elemente (und Elektronik!) für dieselbe Arraylänge, deshalb wird hier in der Praxis gefeilscht. Übung 23.2 lässt dich die Regel an einem konkreten Fall nachmessen.

23.5 Element mal Array: der Faltungssatz der Antennentechnik

Echte Arrays bestehen nicht aus isotropen Punktquellen, sondern aus Dipolen, Patches, Hörnern — Elementen mit eigenem Richtdiagramm. Wie kombiniert sich das Element-Diagramm mit dem Array-Faktor? Kapitel 14 hat die Antwort für den Doppelspalt vorgemacht: Dort war das Muster „Quellen-Streifen × Spaltbreiten-Hüllkurve”. Allgemein gilt die Produktregel

\[P_{\text{gesamt}}(\theta) = P_{\text{Element}}(\theta)\,\cdot\,|F_{\text{Array}}(\theta)|^2,\]

solange alle Elemente gleich sind und gleich ausgerichtet stehen. Warum, zeigt eine hübsche Umformung: Ein „Element mit Struktur” ist selbst eine kleine Quellgruppe — und die Belegung des Gesamtsystems entsteht, indem man die Element-Belegung an jede Array-Position stempelt. Dieses Stempeln ist mathematisch eine Faltung der beiden Belegungen, und gefaltete Belegungen haben multiplizierte Diagramme — der Faltungssatz, alter Bekannter aus der Signalverarbeitung.

Das lässt sich in drei Zeilen exakt nachprüfen. Als „Element” nehmen wir das kleinste interessante: zwei gegenphasige Quellen ([1, -1] — die Übung 14.3!), als Array acht uniforme Plätze:

# von oben: array_faktor(), theta
element = np.array([1.0, -1.0])      # gegenphasiges Quellenpaar
gruppe = np.ones(8)                  # 8 Array-Plätze, uniform
# np.convolve: diskrete Faltung — "stempelt" das erste Array an
# jede Position des zweiten und summiert Überlappendes
gesamt = np.convolve(element, gruppe)
print(f"Gesamtbelegung ({len(gesamt)} Elemente): {gesamt}")

links = array_faktor(theta, gesamt, d=0.5, normiert=False)
rechts = (array_faktor(theta, element, d=0.5, normiert=False)
          * array_faktor(theta, gruppe, d=0.5, normiert=False))
print(f"Faltungssatz | Produkt − Direktrechnung |: "
      f"{np.abs(links - rechts).max() / links.max():.1e}")
Gesamtbelegung (9 Elemente): [ 1.  0.  0.  0.  0.  0.  0.  0. -1.]
Faltungssatz | Produkt − Direktrechnung |: 3.7e-15

Maschinengenauigkeit — Diagramme multiplizieren sich, wenn Belegungen sich falten. Die Gesamtbelegung liest sich dabei fast von selbst: Das Quellenpaar an Platz 0 bis 7 gestempelt gibt [1, 0, 0, …, 0, -1] — innen löschen sich die Stempel aus, übrig bleiben die Ränder. Merke die Richtung der Regel: Ortsraum gefaltet = Winkelraum multipliziert.

Und damit fällt ein zweiter Groschen. Die Zeigersumme \(F = \sum_n a_n e^{\mathrm{i} n \psi}\) ist — Buchstabe für Buchstabe — die diskrete Fouriertransformation der Belegung \(a_n\), ausgewertet am Winkel \(\psi = k d\sin\theta - \Delta\Phi\). Das Richtdiagramm eines Arrays ist das Spektrum seiner Belegung:

# von oben: nichts Neues — np.fft.fft ist die komplexe Voll-DFT
# (Kap. 10 nutzte rfft für reelle Signale; fft nimmt auch komplexe
# Eingaben und liefert alle M Frequenzen). Die 8er-Belegung wird
# auf 4096 Punkte mit Nullen aufgefüllt ("zero-padding"): mehr
# Auswertepunkte derselben Summe, keine neue Information.
M = 4096
F_dft = np.fft.fft(np.ones(8), M)
psi_dft = 2 * np.pi * np.arange(M) / M
zeiger = np.sum(np.exp(-1j * np.arange(8)[None, :]
                       * psi_dft[:, None]), axis=1)
print(f"DFT gegen Zeigersumme: max. Abweichung "
      f"{np.abs(np.abs(F_dft)**2 - np.abs(zeiger)**2).max():.1e}")
DFT gegen Zeigersumme: max. Abweichung 1.3e-13

Wieder Maschinengenauigkeit. Diese Identität ist mehr als eine Kuriosität: Sie verdrahtet das Antennenkapitel mit Kapitel 10 — und sie macht die nächste Frage trivial.

23.6 Taper: die Nebenkeulen zähmen

In Abbildung 23.1 blieben die Nebenkeulen stur bei etwa \(-13\) dB stehen, egal wie viele Elemente wir spendierten. Für ein Radar sind \(-13\) dB viel: Ein starkes Echo in einer Nebenkeule ist von einem schwachen in der Hauptkeule nicht zu unterscheiden. Woher kommt die Zahl?

Aus Kapitel 10, jetzt rückwärts gelesen. Eine uniforme Belegung ist ein Rechteckfenster — und ihr Spektrum ist die \(\mathrm{sinc}\)-Funktion mit ihrer ersten Nebenkeule bei \(-13{,}3\) dB (für endliches \(N\) etwas darüber; gleich gemessen). In Kapitel 10 hieß das Problem „Leck-Effekt”, und die Lösung hieß Fensterfunktion: die Belegung zu den Rändern hin sanft ausblenden. In der Antennenwelt heißt dieselbe Lösung Amplituden-Taper — die äußeren Elemente schwächer speisen.

Der Faltungssatz liefert den schönsten Einstieg: Falte das Rechteck mit sich selbst, und du bekommst ein Dreieck (np.convolve(np.ones(8), np.ones(8)) ergibt 1, 2, …, 8, …, 2, 1). Gefaltete Belegung = multipliziertes Diagramm — das Dreieck-Diagramm ist das Quadrat des Rechteck-Diagramms, also muss jede Nebenkeule in Dezibel exakt doppelt so tief liegen. Und am Ende dieser Logik wartet die binomische Belegung (die Zeile aus dem Pascal-Dreieck): das Rechteck [1, 1] sieben Mal mit sich selbst gefaltet, das Diagramm also die siebte Potenz des Zweierzeigers — und der hat als einziger gar keine Nebenkeulen.

# von oben: array_faktor(), theta, P_8
import math


def nebenkeule_db(P):
    """Pegel der höchsten Nebenkeule (lokale Maxima unter 0,99)."""
    i = np.flatnonzero((P[1:-1] > P[:-2]) & (P[1:-1] > P[2:])) + 1
    pegel = [p for p in sorted(P[i], reverse=True) if 1e-9 < p < 0.99]
    return 10 * np.log10(pegel[0]) if pegel else -np.inf


dreieck = np.convolve(np.ones(8), np.ones(8))
# math.comb: Binomialkoeffizient "7 über n" — die Pascal-Zeile
binom = np.array([math.comb(7, n) for n in range(8)], float)

nk_uni = nebenkeule_db(P_8)
nk_dreieck = nebenkeule_db(array_faktor(theta, dreieck, d=0.5))
print(f"Nebenkeule uniform (N = 8):  {nk_uni:.2f} dB  "
      "(sinc-Grenzwert −13,26)")
print(f"Nebenkeule Dreieck (N = 15): {nk_dreieck:.2f} dB  "
      f"(2 × uniform = {2 * nk_uni:.2f})")
print(f"binomisch {binom}: Nebenkeulen unter −90 dB? "
      f"{nebenkeule_db(array_faktor(theta, binom, d=0.5)) < -90}")
Nebenkeule uniform (N = 8):  -12.80 dB  (sinc-Grenzwert −13,26)
Nebenkeule Dreieck (N = 15): -25.59 dB  (2 × uniform = -25.59)
binomisch [ 1.  7. 21. 35. 35. 21.  7.  1.]: Nebenkeulen unter −90 dB? True

Die Dreieck-Vorhersage trifft auf zwei Nachkommastellen: \(-25{,}59 = 2 \cdot (-12{,}80)\) dB. Kein Fit, keine Näherung — der Faltungssatz in Aktion. Aber Ruhe hat ihren Preis:

# von oben: array_faktor(), theta, dreieck, binom


def fwhm_grad(P):
    i = np.flatnonzero(P >= 0.5)
    return np.degrees(theta[i[-1]] - theta[i[0]])


fig, ax = plt.subplots(figsize=(7.0, 4.2))
for beleg, name, farbe in ((np.ones(8), "uniform", "tab:blue"),
                           (dreieck, "Dreieck", "tab:orange"),
                           (binom, "binomisch", "tab:green")):
    P = array_faktor(theta, beleg, d=0.5)
    ax.plot(np.degrees(theta), 10 * np.log10(np.maximum(P, 1e-7)),
            color=farbe, lw=1.4,
            label=f"{name} (FWHM {fwhm_grad(P):.1f}°)")
ax.axhline(-12.8, color="tab:blue", ls=":", lw=1)
ax.set_xlim(-90, 90)
ax.set_ylim(-65, 2)
ax.set_xlabel("Winkel θ zur Breitseite (°)")
ax.set_ylabel("Array-Faktor (dB)")
ax.set_title("Taper: Nebenkeulen gegen Keulenbreite tauschen")
ax.legend(fontsize=9)
plt.show()
Abbildung 23.6: Drei Belegungen, drei Diagramme (d = λ/2; uniform und binomisch mit N = 8, das gefaltete Dreieck hat N = 15 — sein Vorteil ist trotz der fast doppelten Arraylänge deutlich): Die uniforme Belegung (blau) hat die schmalste Hauptkeule, aber −12,8-dB-Nebenkeulen. Das Dreieck (orange) verdoppelt die Nebenkeulen-Tiefe in Dezibel exakt (−25,6 dB), die binomische Belegung (grün) hat gar keine mehr — bezahlt aber mit der breitesten Hauptkeule. Nebenkeulen lassen sich gegen Keulenbreite eintauschen, nie abschaffen.

Die uniforme Belegung bündelt am schärfsten (sie nutzt jede Quelle voll aus), die binomische ist vollkommen nebenkeulenfrei und fast doppelt so breit. Praktische Arrays (Radar: Taylor-Belegung; Spektralanalyse: Hann-Fenster) liegen dazwischen — es ist in beiden Welten derselbe Kompromiss, weil es dieselbe Mathematik ist. Wer in Kapitel 22 den Exponential-Taper gegen Reflexionen gesehen hat, erkennt auch ihn wieder: sanfte Übergänge statt harter Kanten, dort längs der Leitung, hier quer über die Apertur.

23.7 Scan Loss: der Preis des Schwenkens

Zurück zu den Feldbildern in Abbildung 23.2: Beim Schwenk auf \(48{,}6°\) war die Keule sichtbar breiter als bei der Breitseite. Der Grund ist Geometrie — von schräg gesehen erscheint das Array verkürzt. Wirksam ist nur die Projektion der Apertur auf die Blickrichtung, \(N d\cos\theta_0\), und mit der Apertur schrumpft die Bündelung:

\[D(\theta_0) \;\approx\; D(0)\,\cos\theta_0.\]

Das ist der Scan Loss. Wir messen ihn am Array-Faktor: Die Direktivität in 2D ist (ganz wie in Kapitel 21, nur mit Kreis- statt Kugelintegral) \(D = 2\pi P_{\max} / \oint P\, \mathrm{d}\varphi\) — das Integral läuft über den vollen Kreis, denn unsere Quellreihe strahlt nach vorn und hinten je eine Keule (das kürzt sich im Verhältnis \(D(\theta_0)/D(0)\) heraus):

# von oben: array_faktor(), k
theta_voll = np.linspace(-np.pi, np.pi, 400001)


def direktivitaet_2d(dphi_grad):
    P = array_faktor(theta_voll, np.ones(8), d=0.5,
                     dphi_grad=dphi_grad, normiert=False)
    return 2 * np.pi * P.max() / np.trapezoid(P, theta_voll)


D_breitseite = direktivitaet_2d(0)
schwenk = np.arange(0, 66, 5)
D_rel = np.array([direktivitaet_2d(np.degrees(k * 0.5 *
                                              np.sin(np.radians(t))))
                  for t in schwenk]) / D_breitseite

fig, ax = plt.subplots(figsize=(7.0, 4.0))
ax.plot(schwenk, np.cos(np.radians(schwenk)), "-", color="0.55",
        lw=1.6, label="cos θ₀ (projizierte Apertur)")
ax.plot(schwenk, D_rel, "bo", ms=5, label="Messung am Array-Faktor")
ax.set_xlabel("Schwenkwinkel θ₀ (°)")
ax.set_ylabel("D(θ₀) / D(0)")
ax.set_title(f"Scan Loss: D(0) = {D_breitseite:.1f}, und schräg "
             "wird das Array kleiner")
ax.legend()
plt.show()

print(f"θ₀ = 40°: D/D₀ = {D_rel[schwenk == 40][0]:.3f}   "
      f"(cos 40° = {np.cos(np.radians(40)):.3f})")
Abbildung 23.7: Scan Loss am Array-Faktor (N = 8, d = λ/2): die relative Direktivität D(θ₀)/D(0) für Schwenkwinkel bis 65° (blaue Punkte) gegen die cos-θ₀-Regel der projizierten Apertur (graue Linie). Bis etwa 45° folgt die Messung der Regel; darüber fällt sie schneller, weil die verbreiterte Keule zusätzlich am Rand des sichtbaren Bereichs „anstößt“. Schwenken kostet — ein Phased Array, das bis 60° schauen soll, muss diesen Verlust im Leistungsbudget einplanen.
θ₀ = 40°: D/D₀ = 0.723   (cos 40° = 0.766)

23.7.1 Echte Dipole: NEC rechnet die Verkopplung mit

Punktquellen kennen keine Nachbarn. Echte Dipole schon: Jeder steht im Nahfeld der anderen, ihre Ströme beeinflussen sich gegenseitig (Verkopplung), und beim Schwenken ändert sich diese Beeinflussung mit. Für Drahtantennen ist NEC das Werkzeug der Wahl (Kapitel 11; Drahtantennen kann Meep nicht, das war die Yagi-Lektion).

Der Versuchsaufbau. Frage: Folgt auch eine Reihe echter Dipole dem Zündplan, und was kostet der Schwenk wirklich? Bühne: sechs Halbwellendipole (\(0{,}47\,\lambda\), Drahtradius 1 mm) im Abstand \(d = \lambda/2\) bei 2450 MHz — die Maße der Yagi-Werkstatt aus Kapitel 11. Messgrößen: das Azimut-Gewinndiagramm in dBi für fünf Zündpläne; Keulenrichtung (Maximum im vorderen Halbraum) und Spitzengewinn. Erfolgskriterium: Keule auf \(\arcsin(\Delta\Phi/(k d))\) innerhalb der 1°-Rasterung; der Gewinnabfall sollte mindestens der cos-Regel folgen. Eine Konventionsfalle vorweg: NEC stammt aus der Elektrotechnik und rechnet mit \(e^{+\mathrm{j}\omega t}\) (Meep: \(e^{-\mathrm{i}\omega t}\)) — „später zünden” heißt dort negative Phase, unser Schwenk nach \(+\theta\) braucht also \(\Delta\Phi < 0\) auf der ex_card.

# von oben: k; NEC wie in Kap. 11 (necpp, SI-Einheiten!)
import necpp


def nec_array_diagramm(dphi_grad, n_dipole=6, d_lambda=0.5,
                       f_mhz=2450.0):
    """Azimut-Gewinn (dBi) einer phasengespeisten Dipolreihe."""
    lam = 299792458.0 / (f_mhz * 1e6)
    ctx = necpp.nec_create()
    for i in range(n_dipole):
        y = (i - (n_dipole - 1) / 2) * d_lambda * lam
        halb = 0.47 * lam / 2
        necpp.nec_wire(ctx, i + 1, 11, 0, y, -halb, 0, y, halb,
                       0.001, 1.0, 1.0)
    necpp.nec_geometry_complete(ctx, 0)
    necpp.nec_fr_card(ctx, 0, 1, f_mhz, 0)
    for i in range(n_dipole):
        ph = np.radians(dphi_grad * i)
        # Speisespannung mit Phase n·ΔΦ (Real-/Imaginärteil)
        necpp.nec_ex_card(ctx, 0, i + 1, 6, 0,
                          np.cos(ph), np.sin(ph), 0, 0, 0, 0)
    npts = 361
    necpp.nec_rp_card(ctx, 0, 1, npts, 0, 5, 0, 0, 90.0, 0.0, 0.0,
                      360.0 / (npts - 1), 0.0, 0.0)
    gewinn = np.array([necpp.nec_gain(ctx, 0, 0, j)
                       for j in range(npts)])
    necpp.nec_delete(ctx)
    return np.linspace(0, 360, npts), gewinn


print("ΔΦ        Keule    Theorie    Gewinn     Scan Loss   cos-Regel")
g_breitseite = None
for dphi in (0, -30, -60, -90, -120):
    phi_nec, g = nec_array_diagramm(dphi)
    vorn = (phi_nec <= 90) | (phi_nec >= 270)     # vorderer Halbraum
    i_peak = np.flatnonzero(vorn)[np.argmax(g[vorn])]
    keule = phi_nec[i_peak] if phi_nec[i_peak] <= 180 \
        else phi_nec[i_peak] - 360
    theo = np.degrees(np.arcsin(-np.radians(dphi) / (k * 0.5)))
    if g_breitseite is None:
        g_breitseite = g.max()
    cos_db = 10 * np.log10(np.cos(np.radians(theo)))
    print(f"{dphi:+5d}°   {keule:+6.1f}°  {theo:+7.2f}°   "
          f"{g.max():5.2f} dBi   {g.max() - g_breitseite:+5.2f} dB"
          f"   {cos_db:+5.2f} dB")
ΔΦ        Keule    Theorie    Gewinn     Scan Loss   cos-Regel
   +0°     +0.0°    -0.00°   11.05 dBi   +0.00 dB   +0.00 dB
  -30°     +9.0°    +9.59°   10.93 dBi   -0.12 dB   -0.06 dB
  -60°    +19.0°   +19.47°   10.67 dBi   -0.38 dB   -0.26 dB
  -90°    +29.0°   +30.00°   10.18 dBi   -0.87 dB   -0.62 dB
 -120°    +40.0°   +41.81°    9.62 dBi   -1.43 dB   -1.28 dB
# von oben: nec_array_diagramm(), k
fig = plt.figure(figsize=(6.2, 5.6))
ax = fig.add_subplot(projection="polar")
for dphi, farbe in ((0, "tab:blue"), (-60, "tab:green"),
                    (-120, "tab:red")):
    phi_nec, g = nec_array_diagramm(dphi)
    theo = np.degrees(np.arcsin(-np.radians(dphi) / (k * 0.5)))
    ax.plot(np.radians(phi_nec), np.clip(g - g.max(), -30, 0),
            color=farbe, lw=1.6,
            label=f"ΔΦ = {dphi}° → {theo:.0f}° ({g.max():.1f} dBi)")
ax.set_theta_zero_location("E")
ax.set_rticks([-30, -20, -10, -3, 0])
ax.set_rlabel_position(135)
ax.set_title("NEC: sechs echte Dipole folgen dem Zündplan", pad=18)
ax.legend(loc="lower left", bbox_to_anchor=(-0.14, -0.12), fontsize=8)
plt.show()
Abbildung 23.8: Drei Zündpläne der 6-Dipol-Reihe als NEC-Azimutdiagramm (Polarbild, Gewinn in dBi relativ zum jeweiligen Maximum, 30-dB-Dynamik; 0° = Breitseite). Die Keule wandert von 0° über 19° nach 42° — echte Drähte folgen demselben Umweg-gleich-Vorsprung-Gesetz wie die Punktquellen. Der Spitzengewinn fällt dabei von 11,05 auf 9,62 dBi; die Rückseite spiegelt das Bild, weil eine Dipolreihe ohne Reflektor nicht zwischen vorn und hinten unterscheiden kann.

Drei Befunde. Erstens: Die Keule sitzt für alle Zündpläne auf der Theorie (innerhalb der 1°-Rasterung — bei \(-120°\) liegt das Maximum bei \(40°\) statt \(41{,}8°\), weil die beim Schwenken breit gewordene Keule auf dem Gradraster abgetastet wird). Zweitens: Der Scan Loss ist real — \(-1{,}43\) dB bei \(41{,}8°\). Drittens, und das ist die ehrliche Pointe: Er ist größer als die cos-Regel (\(-1{,}28\) dB). Die Differenz ist die Verkopplung — die Dipole stehen einander im Nahfeld, und dieser Effekt steckt in keiner Zeigersumme. Genau dafür braucht man Werkzeuge wie NEC: Der Array-Faktor liefert das Design, der Feldlöser die Wahrheit (die Validierungs-Checkliste aus Kapitel 10 lässt grüßen).

23.8 Empfang ist Senden rückwärts

Bisher hat das Array gesendet. Die Reziprozität der Maxwell-Gleichungen (sie steckt letztlich in der Zeitumkehr-Symmetrie aus Kapitel 20) garantiert: Dasselbe Array mit demselben Zündplan empfängt auch am besten aus der Richtung \(\theta_0\) — man verzögert die \(N\) Empfangssignale elektronisch um genau die Zündplan-Zeiten und addiert sie. Signale aus \(\theta_0\) addieren sich dann phasenrichtig (\(N\)-fache Amplitude, \(N^2\)-fache Leistung), Signale aus anderen Richtungen verzettelt die Zeigersumme. Das Array „lauscht” in eine wählbare Richtung — und kann mehr: Mit \(N\) Phasenknöpfen lassen sich nicht nur Keulen formen, sondern auch gezielt Nullstellen auf Störer legen (Übung 23.4 baut so einen Störblocker). 5G-Masten, WLAN-Router mit mehreren Antennen und das Starlink-Terminal tun beides zugleich: senden und lauschen, jeweils mit eigenem Zündplan.

Zum Abschluss die zweite Spielwiese: dein eigenes Feldbild aus dem Zündplan (die La-Ola-Summe vom Kapitelanfang, jetzt mit allen Knöpfen). Vorhersage zuerst: Bei D_LAMBDA = 1.5 — wie viele Strahlen wirst du sehen, und unter welchen Winkeln? Und was passiert mit dem Bild, wenn du DPHI_GRAD auf 200 stellst, also \(\sin\theta_0 > 1\) verlangst? (Es gibt dann keine Richtung mehr, in der die Zeiger parallel stehen — das Array strahlt diffus in viele Richtungen. Solche „unsichtbaren” Zündpläne nutzt man tatsächlich, um nicht geortet zu werden.)

23.9 Das Kapitel-Programm

programme/kap23/kap23_arrays_beamforming.py bündelt alle Befunde eigenständig und mit assert-Schranken: die \(N = 2\)-Doppelspalt-Identität und die Schwenkreihe (auf \(0{,}05°\)), Faltungssatz und DFT-Identität (Maschinen- genauigkeit), die Taper-Kette uniform/Dreieck/binomisch (\(-12{,}80\) dB, exakt verdoppelt, nebenkeulenfrei), Grating Lobes bei \(d = 1{,}5\,\lambda\) samt Schwenk-Designregel, die Scan-Loss-Reihe, den Meep-Doppelcheck (Feldbild-Gesundheitswert und near2far gegen Array-Faktor, rms < 0,01), die NEC-Schwenkreihe (Keulen auf 2°) und den Null-Steuer-Trick (Störer < \(-60\) dB). Läuft in einer guten Minute.

TippMerkkasten
  • Umweg = Vorsprung: Die Keule einer Strahlerreihe zeigt dorthin, wo der Wegunterschied den Zündplan ausgleicht — \(\sin\theta_0 = \Delta\Phi/(k d)\). Richtung ist Phasensache, keine Geometriesache.
  • Der Array-Faktor ist die DFT der Belegung, ausgewertet bei \(\psi = k d \sin\theta - \Delta\Phi\); \(N = 2\) ist der Doppelspalt. Arraylänge \(N d\) setzt die Keulenbreite (\(\sin\theta_{\text{Null}} = \lambda/(N d)\)).
  • Grating Lobes: Geisterkeulen bei \(\sin\theta = \sin\theta_0 \pm m\lambda/d\); fernhalten mit \(d/\lambda < 1/(1 + |\sin\theta_0|)\) — voll schwenkbar heißt \(d \le \lambda/2\).
  • Faltungssatz: Belegungen falten sich, Diagramme multiplizieren sich (Element × Array-Faktor). Taper = Fensterfunktion: Nebenkeulen gegen Keulenbreite, nie umsonst (uniform \(-13\) dB, Dreieck doppelt so tief, binomisch keulenfrei).
  • Schwenken kostet (Scan Loss \(\approx \cos\theta_0\) aus der projizierten Apertur, real mehr durch Verkopplung), und Empfang = Senden rückwärts (Reziprozität): verzögern + addieren, Keulen und Nullen steuerbar.

Roter Faden

Dieses Kapitel ist die Ernte von Teil IV: Der Doppelspalt und das Zeigerbild aus Kapitel 14 wurden rückwärts gelesen (die Übungen 14.3/14.4 — Phasen-Hebel und \(d < \lambda\)-Regel — waren die Vorboten), die DFT aus Kapitel 10 entpuppte sich als Diagramm-Maschine samt Fenster-Kompromiss, das near2far-Fernfeld und die Direktivität aus Kapitel 21 lieferten das Messgeschirr, und die Reziprozität lehnt sich an die Zeitumkehr aus Kapitel 20. Auch Kapitel 22 wirkt mit: Jedes der \(N\) Elemente braucht seine eigene angepasste Speiseleitung — ein Phased Array ist immer auch ein Leitungs-Kunstwerk. Nach vorn: Kapitel 24 baut den Gegenentwurf — die Yagi erzeugt ihre Richtwirkung mit passiven Elementen, deren „Zündplan” die Verkopplung selbst übernimmt; Kapitel 26 lässt aus Quellreihen ganze Kristallgitter werden (Bragg statt Grating); und das inverse Design in Kapitel 31 wird Belegungen nicht mehr wählen, sondern optimieren lassen.

Übungen

Ü 23.1 (Verstehen). Ein 5G-Mast soll seine Keule um \(25°\) aus der Breitseite schwenken (Elementabstand \(d = \lambda/2\)). Berechne von Hand den nötigen Phasenversatz \(\Delta\Phi\) zwischen Nachbarelementen — und erkläre in zwei Sätzen, warum derselbe Trick mit einer einzigen Antenne prinzipiell nicht funktioniert.

\(\Delta\Phi = k d \sin\theta_0 = 2\pi \cdot \tfrac{1}{2} \cdot \sin 25° = \pi \cdot 0{,}4226 = 1{,}328\) rad \(= 76{,}1°\):

# von oben: k (= 2π, λ = 1)
dphi = np.degrees(k * 0.5 * np.sin(np.radians(25)))
print(f"ΔΦ = {dphi:.1f}° pro Element")
ΔΦ = 76.1° pro Element

Mit einer einzigen Antenne gibt es nichts, wogegen man eine Phase verstellen könnte: Richtwirkung entsteht aus Interferenz mehrerer Teilwellen mit definierten Phasenbeziehungen. Eine Einzelantenne kann ihr Diagramm nur über ihre Geometrie ändern — und die müsste man eben drehen.

Ü 23.2 (Verändern). Ein Entwickler wählt \(d = 0{,}7\, \lambda\) (spart Elemente) und will bis \(\theta_0 = 50°\) schwenken. Prüfe zuerst mit der Designregel \(d/\lambda < 1/(1 + |\sin\theta_0|)\), ob das gut geht, und sieh dir dann das Diagramm mit array_faktor an: Wo sitzt die Geisterkeule, und wie stark ist sie? Bis zu welchem Schwenkwinkel wäre \(d = 0{,}7\,\lambda\) in Ordnung?

Die Regel verlangt \(d/\lambda < 1/(1 + \sin 50°) = 0{,}566\) — mit \(0{,}7\) klar verletzt. Die Geisterkeule sitzt bei \(\sin\theta = \sin 50° - 1/0{,}7 = -0{,}663\), also \(-41{,}5°\), und zwar in voller Höhe (0 dB — sie ist keine Nebenkeule, sondern eine zweite Hauptkeule). Gut geht es, solange \(\sin\theta_0 < 1/0{,}7 - 1 = 0{,}4286\), also bis \(\theta_0 = 25{,}4°\):

# von oben: array_faktor(), theta, k
dphi_50 = np.degrees(k * 0.7 * np.sin(np.radians(50)))
P = array_faktor(theta, np.ones(8), d=0.7, dphi_grad=dphi_50)
i_max = np.flatnonzero((P[1:-1] > P[:-2]) & (P[1:-1] > P[2:])) + 1
gross = i_max[P[i_max] > 0.5]
print(f"Keulen über −3 dB bei {np.round(np.degrees(theta[gross]), 1)}°")
print(f"max. sauberer Schwenk: "
      f"{np.degrees(np.arcsin(1 / 0.7 - 1)):.1f}°")
Keulen über −3 dB bei [-41.5  50. ]°
max. sauberer Schwenk: 25.4°

Das gesparte Material bezahlt man also mit einem geschrumpften Schwenkbereich — genau dieser Handel wird bei jedem realen Array ausgefochten.

Ü 23.3 (Verändern). Die binomische Belegung hat gar keine Nebenkeulen — warum baut dann nicht jeder Radarbauer binomisch? Miss den Preis nach: Vergleiche für \(N = 8\), \(d = \lambda/2\) die Halbwertsbreite (FWHM) und die 2D-Direktivität der uniformen und der binomischen Belegung (nimm die direktivitaet_2d-Logik aus dem Scan-Loss-Abschnitt und gib ihr eine Belegung mit).

# von oben: array_faktor(), theta, theta_voll, binom, fwhm_grad()


def D_2d(belegung):
    P = array_faktor(theta_voll, belegung, d=0.5, normiert=False)
    return 2 * np.pi * P.max() / np.trapezoid(P, theta_voll)


for beleg, name in ((np.ones(8), "uniform"), (binom, "binomisch")):
    P = array_faktor(theta, beleg, d=0.5)
    print(f"{name:10s}: FWHM = {fwhm_grad(P):4.1f}°,   "
          f"D_2D = {D_2d(beleg):5.2f}")
uniform   : FWHM = 12.8°,   D_2D = 12.16
binomisch : FWHM = 22.9°,   D_2D =  7.39

Die binomische Keule ist fast doppelt so breit, und die Direktivität fällt von \(12{,}2\) auf \(7{,}4\) — fast die Hälfte der Bündelung ist weg. Nebenkeulenfreiheit ist der teuerste Punkt auf der Tauschkurve; reale Entwürfe (Taylor-, Tschebyscheff-Belegungen) kaufen „tief genug” statt „ganz weg”. Außerdem verlangt die binomische Belegung Speiseamplituden über einen Faktor 35 — auch das will erst einmal sauber gebaut sein (Kapitel 22 lässt grüßen).

Ü 23.4 (Übertragen). Ein GPS-Empfänger mit 8-Element-Array empfängt sein Nutzsignal aus \(0°\) — und einen Störsender aus \(40°\). Baue den Störblocker: Berechne zuerst, wie stark die uniforme Belegung (Keule auf \(0°\)) den Störer von sich aus dämpft. Lege dann eine Nullstelle auf \(40°\), indem du vom uniformen Gewichtsvektor seine Projektion auf den Störer-Steering-Vektor \(v_n = e^{\mathrm{i} n k d \sin 40°}\) abziehst — und miss, was das den Empfang aus \(0°\) kostet. (Empfangs-Beamforming: Die „Belegung” sind jetzt die Gewichte, mit denen die \(N\) Empfangssignale addiert werden — konjugiert, weil Empfangen der zeitumgekehrte Sendefall ist.)

# von oben: array_faktor(), theta, k
n8 = np.arange(8)
v_stoer = np.exp(1j * n8 * k * 0.5 * np.sin(np.radians(40)))
w_uni = np.ones(8, complex)
# np.vdot: Skalarprodukt, das das ERSTE Argument konjugiert —
# für komplexe Vektoren das richtige „wie viel a steckt in b"
# Projektion abziehen: w ⊥ v_stoer ⇒ Summe über den Störer = 0
w_null = w_uni - (np.vdot(v_stoer, w_uni)
                  / np.vdot(v_stoer, v_stoer)) * v_stoer

i_40 = np.argmin(np.abs(theta - np.radians(40)))
for w, name in ((w_uni, "uniform"), (w_null, "mit Null")):
    P = array_faktor(theta, np.conj(w), d=0.5, normiert=False)
    print(f"{name:9s}: Störer (40°) {10 * np.log10(P[i_40] / P.max()):8.1f} dB"
          f"   Maximum bei {np.degrees(theta[np.argmax(P)]):+5.1f}°"
          f"   Pegel dort {10 * np.log10(P.max() / 8**2):+6.2f} dB")
uniform  : Störer (40°)    -16.8 dB   Maximum bei  -0.0°   Pegel dort  +0.00 dB
mit Null : Störer (40°)   -110.4 dB   Maximum bei  -0.1°   Pegel dort  -0.18 dB

Die uniforme Belegung dämpft den Störer nur um knapp 17 dB (er sitzt zufällig nahe einer Nebenkeule). Die Projektion drückt ihn um über 100 dB — praktisch auf null —, während die Hauptkeule auf \(0°\) stehen bleibt und nur \(0{,}18\) dB verliert. Acht Phasenknöpfe, ein Skalarprodukt: mehr braucht ein adaptiver Störblocker im Kern nicht. Genau so (nur mit laufend nachgeführten Gewichten) schützen sich GPS-Empfänger in Flugzeugen gegen Jammer.

Das Kleingedruckte

Verkopplung und aktive Impedanz. Der NEC-Befund kratzte nur an der Oberfläche: Im dichten Array ändert sich die Eingangsimpedanz jedes Elements mit dem Schwenkwinkel („aktive Impedanz”), weil die Nachbarn mit wechselnder Phase in den Speisepunkt zurückstrahlen. Bei manchen Winkeln kann ein Array dadurch fast vollständig reflektieren („Scan Blindness”) — die Anpasskunst aus Kapitel 22 muss dann über den ganzen Schwenkbereich funktionieren.

Fläche statt Reihe. Reale Panels sind 2D-Gitter (das Starlink-Terminal: rund 1500 Patch-Elemente). Die gute Nachricht steckt im Faltungssatz: Eine Rechteck-Fläche ist die Faltung aus Zeile und Spalte, ihr Diagramm also das Produkt zweier Linien-Array-Faktoren — alles in diesem Kapitel gilt pro Achse weiter.

Phase ist nicht Laufzeit. Unser Phasenknopf stellt Phasen bei einer Frequenz. Breitbandige Signale brauchen echte Zeit-Verzögerungen, sonst schielt die Keule pro Frequenz woandershin („Beam Squint” — die Gitterformel hängt ja an \(\lambda\)). Große Radare mischen deshalb beides: grobe echte Verzögerung, feine Phase.

Digital wird es mächtig. Wer hinter jedem Element einen eigenen Empfänger samt Analog-Digital-Wandler hat, kann die Gewichte in Software setzen — und zwar viele Sätze gleichzeitig: mehrere Keulen, mehrere Nullen, pro Nutzer ein eigener Strahl (Massive MIMO im 5G-Netz). Aus demselben Datensatz lassen sich nachträglich beliebige Richtungen formen — Beamforming wird vom Hardware- zum Rechenproblem.

Und die Konvention. NEC (\(e^{+\mathrm{j}\omega t}\), Elektrotechnik) und Meep (\(e^{-\mathrm{i}\omega t}\), Physik) sind sich übers Vorzeichen der Phase uneins — dieselbe Zündverspätung heißt einmal \(-\Delta\Phi\), einmal \(+\Delta\Phi\). Das ist keine Physik, nur Buchhaltung; aber wer Phasen zwischen Werkzeugen überträgt, prüft das Vorzeichen am besten an einem Fall mit bekannter Antwort — so wie unsere 30°-Handrechnung.