From b703a8a25eab9215b84adf038e411fd9cfd91987 Mon Sep 17 00:00:00 2001 From: Rudolf Mahdal Date: Tue, 17 Mar 2026 10:14:51 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Prvn=C3=AD=20commit:=20P=C5=99id=C3=A1n?= =?UTF-8?q?=C3=AD=20k=C3=B3d=C5=AF=20pro=20generov=C3=A1n=C3=AD=20GRF=20a?= =?UTF-8?q?=20NUFFT,=20schuzky=201-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 1/1.py | 49 ++++ 1/1ext.py | 140 ++++++++++++ 2/__pycache__/fft1d.cpython-313.pyc | Bin 0 -> 5311 bytes 2/fft1d.py | 108 +++++++++ 2/main2.py | 35 +++ 3/3.2d.py | 216 ++++++++++++++++++ 3/3.py | 168 ++++++++++++++ 3/Figure_1.png | Bin 0 -> 64735 bytes 3/Untitled-sada2.py | 309 ++++++++++++++++++++++++++ 3/__pycache__/3.2d.cpython-313.pyc | Bin 0 -> 11890 bytes 3/gstest.py | 20 ++ 4/4.py | 140 ++++++++++++ 4/__pycache__/grf.cpython-313.pyc | Bin 0 -> 10904 bytes 4/grf.py | 165 ++++++++++++++ 4/main_fields.py | 153 +++++++++++++ 4/main_gstools.py | 134 +++++++++++ __pycache__/finufft.cpython-313.pyc | Bin 0 -> 1399 bytes __pycache__/functions.cpython-313.pyc | Bin 0 -> 607 bytes test/1drandom.py | 27 +++ test/2.1.py | 26 +++ test/2.py | 58 +++++ test/nfftrandom.py | 77 +++++++ 22 files changed, 1825 insertions(+) create mode 100644 1/1.py create mode 100644 1/1ext.py create mode 100644 2/__pycache__/fft1d.cpython-313.pyc create mode 100644 2/fft1d.py create mode 100644 2/main2.py create mode 100644 3/3.2d.py create mode 100644 3/3.py create mode 100644 3/Figure_1.png create mode 100644 3/Untitled-sada2.py create mode 100644 3/__pycache__/3.2d.cpython-313.pyc create mode 100644 3/gstest.py create mode 100644 4/4.py create mode 100644 4/__pycache__/grf.cpython-313.pyc create mode 100644 4/grf.py create mode 100644 4/main_fields.py create mode 100644 4/main_gstools.py create mode 100644 __pycache__/finufft.cpython-313.pyc create mode 100644 __pycache__/functions.cpython-313.pyc create mode 100644 test/1drandom.py create mode 100644 test/2.1.py create mode 100644 test/2.py create mode 100644 test/nfftrandom.py diff --git a/1/1.py b/1/1.py new file mode 100644 index 0000000..666f9f4 --- /dev/null +++ b/1/1.py @@ -0,0 +1,49 @@ +import numpy as np +import matplotlib.pyplot as plt +from scipy.fft import fft, ifft, fftfreq +p = True +N = 100 +x, dx = np.linspace(0, 2*np.pi, N, endpoint=False, retstep=True) +f_x = 1 + 2*np.sin(x) + 10*np.sin(5*x) + 3*np.cos(5*x) +F_k = fft(f_x) +if p: + print("dx:", dx) + print("x:", x) + print("f(x):", f_x) + print("F_k:", F_k) + +f_x_reconstructed = ifft(F_k) + +# --- VYKRESLENÍ VÝSLEDKŮ PRO KONTROLU --- +plt.figure(figsize=(12, 8)) + +# Graf 1: Původní a složená funkce (měly by se překrývat) +plt.subplot(2, 1, 1) +plt.plot(x, f_x, label="Původní f(x)", linewidth=3) +# ifft vrací komplexní čísla, pro graf vezmeme reálnou část (.real) +plt.plot(x, f_x_reconstructed.real, '--', label="Složená (iFFT)", color='red') +plt.title("Původní signál vs. Rekonstruovaný signál (Úspěšný test!)") +plt.legend() +plt.grid() + +# Graf 2: Zobrazení frekvenčního spektra (co vlastně FFT našla) +plt.subplot(2, 1, 2) +# Spočítáme frekvence pro osu X (zajímají nás jen kladné frekvence, proto úprava do N//2) +xf = fftfreq(N, dx)[:N//2] +print("xf:", xf) +# Amplituda (síla) signálu - musíme normalizovat dělením (2/N) +amplitudy = (2.0/N) * np.abs(F_k[:N//2]) +print("Amplitudy:", amplitudy) +# Ruční úprava nulté frekvence (DC složky - to je ta "1" na začátku rovnice) +amplitudy[0] = amplitudy[0] / 2 + +# Vykreslíme to jako sloupečky (stem plot) +plt.stem(xf, amplitudy) +plt.xlim(-0.1, 2) # Přiblížíme začátek grafu, aby byly vidět špičky +plt.title("Spektrum (FFT) - Všimni si špiček na frekvencích, které odpovídají rovnici") +plt.xlabel("Frekvence (Hz)") +plt.ylabel("Amplituda") +plt.grid() + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/1/1ext.py b/1/1ext.py new file mode 100644 index 0000000..fc32847 --- /dev/null +++ b/1/1ext.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +from scipy.fft import fft, ifft, fftfreq +p = False + +# ========================================== +# 1. Definice mřížky (Grid) a signálu +# ========================================== +N = 100 +x, dx = np.linspace(0, 2*np.pi, N, endpoint=False, retstep=True) + +# Rozložení signálu na jednotlivé komponenty (abychom viděli, co se z čeho skládá) +f1 = 1 * np.ones_like(x) # DC složka (konstanta 1) +f2 = 2 * np.sin(x) # Frekvence 1 +f3 = 10 * np.sin(5*x) # Frekvence 5 (sinusovka) +f4 = 3 * np.cos(5*x) # Frekvence 5 (kosinusovka) + +# Výsledná složená funkce ze zadání +f_x = f1 + f2 + f3 + f4 + +if p: + print("dx:", dx) + print("x:", x) + print("f(x):", f_x) + +# ========================================== +# 2. Výpočet FFT a zpětné transformace +# ========================================== +F_k = fft(f_x) +f_x_reconstructed = ifft(F_k) + +# Výpočet frekvencí pro osu X +xf_vsechny = fftfreq(N, dx) + +# Pro srozumitelné spektrum nás většinou zajímá jen kladná část spektra +xf_kladne = xf_vsechny[:N//2] +F_k_kladne = F_k[:N//2] + +# Amplitudy (síla signálu) - normalizace pomocí (2/N) +amplitudy = (2.0/N) * np.abs(F_k_kladne) +amplitudy[0] = amplitudy[0] / 2 # DC složka se nedělí dvěma, vracíme ji na správnou hodnotu + +# Fáze (posun signálu v radiánech) +faze = np.angle(F_k_kladne) +# Odfiltrování šumu z fáze: tam, kde je nulová nebo minimální amplituda, nemá smysl počítat fázi +faze[amplitudy < 0.1] = 0 + +# Výkonové spektrum (Power Spectrum - energie na dané frekvenci) +vykon = np.abs(F_k_kladne)**2 + + +# ========================================== +# 3. VYKRESLENÍ - OBRÁZEK 1: Časová oblast +# ========================================== +plt.figure(figsize=(12, 10)) +plt.subplot(3, 1, 1) +plt.plot(x, f_x, label="Výsledný signál f(x)", color='black', linewidth=2) +plt.title("1. Celkový složený signál f(x) v čase") +plt.ylabel("Amplituda") +plt.grid(True); plt.legend() + +plt.subplot(3, 1, 2) +plt.plot(x, f1, label="DC (1)", linestyle='--') +plt.plot(x, f2, label="2*sin(x)", linestyle='--') +plt.plot(x, f3, label="10*sin(5x)", linestyle='--') +plt.plot(x, f4, label="3*cos(5x)", linestyle='--') +plt.title("2. Jednotlivé " + "skryté" + " složky, ze kterých se signál skládá") +plt.ylabel("Amplituda") +plt.grid(True); plt.legend() + +plt.subplot(3, 1, 3) +plt.plot(x, f_x, label="Původní f(x)", linewidth=4, alpha=0.5) +plt.plot(x, f_x_reconstructed.real, '--', label="Složená (iFFT)", color='red') +plt.title("3. Původní signál vs. Rekonstruovaný signál (Úspěšný test!)") +plt.xlabel("x (Čas / Prostor)") +plt.ylabel("Amplituda") +plt.grid(True); plt.legend() +plt.tight_layout() + + +# ========================================== +# VYKRESLENÍ - OBRÁZEK 2: Surová komplexní čísla z FFT +# ========================================== +plt.figure(figsize=(12, 8)) +# Reálná část reprezentuje podíly KOSINUSOVÝCH funkcí +plt.subplot(2, 1, 1) +plt.stem(range(N), F_k.real, basefmt=" ") +plt.title("4. Surový výstup FFT: Reálná část (Odpovídá zastoupení KOSINUSŮ ve frekvencích)") +plt.ylabel("Reálná hodnota") +plt.grid(True) + +# Imaginární část reprezentuje podíly SINUSOVÝCH funkcí +plt.subplot(2, 1, 2) +plt.stem(range(N), F_k.imag, basefmt=" ") +plt.title("5. Surový výstup FFT: Imaginární část (Odpovídá zastoupení SINUSŮ ve frekvencích)") +plt.xlabel("Index pole (frekvenční koš)") +plt.ylabel("Imaginární hodnota") +plt.grid(True) +plt.tight_layout() + + +# ========================================== +# VYKRESLENÍ - OBRÁZEK 3: Polární forma (Amplituda a Fáze) +# ========================================== +plt.figure(figsize=(12, 8)) +plt.subplot(2, 1, 1) +plt.stem(xf_kladne, amplitudy, basefmt=" ") +plt.xlim(-0.1, 1.2) # Ořízneme graf jen na zajímavou část, ať vidíme špičky +plt.title("6. Amplitudové spektrum - To nejdůležitější (Síla jednotlivých frekvencí)") +plt.ylabel("Amplituda") +plt.grid(True) + +# Přidání textu přímo nad špičky v grafu pro přehlednost +for i, amp in enumerate(amplitudy): + if amp > 0.5: + plt.text(xf_kladne[i], amp + 0.5, f"{amp:.1f}", ha='center', fontweight='bold') + +plt.subplot(2, 1, 2) +plt.stem(xf_kladne, faze, basefmt=" ") +plt.xlim(-0.1, 1.2) +plt.title("7. Fázové spektrum - Fázový posun odhalených vln (v radiánech)") +plt.xlabel("Frekvence") +plt.ylabel("Fáze [rad]") +plt.grid(True) +plt.tight_layout() + + +# ========================================== +# VYKRESLENÍ - OBRÁZEK 4: Výkonové spektrum (Power Spectrum) +# ========================================== +plt.figure(figsize=(12, 4)) +plt.stem(xf_kladne, vykon, basefmt=" ", markerfmt='ro', linefmt='r-') +plt.xlim(-0.1, 1.2) +plt.title("8. Výkonové spektrum (Power Spectral Density) - Kde je ukryta většina energie signálu") +plt.xlabel("Frekvence") +plt.ylabel("Výkon (Absolutní hodnota na druhou)") +plt.grid(True) +plt.tight_layout() + +plt.show() \ No newline at end of file diff --git a/2/__pycache__/fft1d.cpython-313.pyc b/2/__pycache__/fft1d.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edae1cdb7c6a615f1674a937f9b692afff752fe9 GIT binary patch literal 5311 zcmbVQYit|G5x(P_$0PN!72EOSBu5r0S+d@?)z}VgONygdc1T~WRtktSd6a09chnx~ zm_~u}r&uGf z26!3=jW3vxiP8GZ$n2;5<{iXE5?P>SQCn8O!*5kvHowVl7`1d6{WkU8j^EWDli#lP zP-6xUojOYr1U0}5JQ6Q0<%FeMjIfj&7em6*$H8$XA;zF5A;!fZJf#F5O`@e-49H+G z&L_nXW2#~fgWSa^C`sin7~Vl2&cws!VX4Absm;#`yujPocSNk%wkQ&m zHaRAo5~4w#xw1}dRE#HL{AEo{ROG{vV1yTvci#t=9Uf*3FPBU5fiV;ggu_XV<$$lB z5jZA+xJ!``4{Kax;&wr z0{<9};fPA>p@0j+#3D(&rWE2R>Pu|LArGlofmayu6aUf-yLv2W)LLq7ff~O*oQLX7 zf(0YD*a%_--n8N~=&j&W~}5F)9O*l6eIi=$_ecoDp8v||+Zu07P6NGsdH z?}ACh#R4H-kRr)+U98!p3gcIG}5B5QMs62=DZl*c6Kdf=)u;2?QsfP`G|a?q3Ni%Ka2IR>;Va{U!(ELqGGbku2F|6=>Rwx2 z4e|V5;9(vChu4Zv#YO#1d)5=fU97nUUa%zHsntH9#xhPf5QKwZ2-f%v{Qbtqtq6}{ zdUYMJD}H0|FhOv&1Toy9>GQam`uNw-Xg6qNd7?%=>vZX9()3`8dU#B!f51|ym(xhR z0I;ipC&sQcBc%a#Q6v}zbSna5x|azD#<}E3OY{}y2on^Ai*BtW`U>Dv5L}8!eCS9& z7n67mEiq~*#Jh7`Xz5lj1~7&>c>?2SLUfML-~yZ|@~1W{24 zMv6Sop%k!G5fjUSFv8&}Mc%+Rl|Z7DygSE-N+b*&IM6|TN@MLTS+^>GM+ z_Yq((Li#mS8RC(Pu+x)$SE-D5#pb+5WvQ!l#`{le#fsAfkn-}RVWqNa+B`!|Qzk?o%HUhQ4kwjJ}Ev)^#dxwProQ=0^Bsht_P zJ}~Q@JNf$QN$=!^t9_aTiz-1w0DS88Qa}I~>@OgI-cOVZE$inae zj;Y7JZ1&R%IZsOoaS0SbDmUbA+YgyvF^d})86;i_efB*#+e^39xp6U+!Z{cdj>M9P zk25%raQoeA_Ta&pSxX7gDVa(z3BVSKIR&6bZ3?Pkp%-Va z_ciiOkA z%MOGHd$3X=C4r7Zp@1NwI0Slp8dNw^jF9purU=w%J7(-s$QKa?DTMtUVbems#q%Mh zW|Kl;K`~;1hR|h&yo_i}YxD=Aiuu)XoSN>$9p-R^kA)=6NQK5irD0^{#>5&O1j@tk zmzJRdl&mHk&TF2m=N`33ruNL6@>E0S#EPBDZp|FOZ=FT1(h% z2W#^c02QpaR!U&f@Fa>dFp2$YL)iqA^axIG)xwqnUfH>Se}RLt%$pXO5QGs0b=vFpDn%TeKg3EqTt6DrwC zw!()2WkXSeiaGTY=zc;_dE7^}e$$}dCNRl#BE@u2?$QucOhq9MtY@PpnIgW&SY$4U zp)}k^tbL<1;%Be%@Z}FT9E>EUaMmkCAog5hMp_b){Y)bAO6yuGx(^nA4&h|qQ=#HI zazh(=aMgq>70H`%3-6N#+}4y32?LrdR!-s&;?nTtuiXwPHuaLAJ=6iAKmvt$g$io7 z8|@v3G?=eKt-wjK3mEH0uxI)Djmm-T@*EEx_6f$DLkg1yxrP-7>; zA8s1lTQ3Jx>8J;~6nKM0!508NQw8ML8(IXYUKath$$dcXgTHhbk_9zWE)xSDSqQsB zh5w$Z#@Uyq_s?4v>T-^}Z9gQ`v~#NJ4c`nq%}#HJv0XB~2cm$hZoXlG&%3%Z1H~v% zn|HbwI&K^9wVjaLPJG&*ZyS`G2W98r@6aD!{mrY(=ZAlLMW(;GJ`5bp@9mVI?z|I{ z=@Xg02i6MN`qWDG)|uw%=C{#})Ld#d{O*ODe2!lj&R6$jtzXn^o$0;aJ3F4QXYI6C`h{i3?q$<%HQ0Da zguj+OWWUFuSObBO7z_jyvyh4>(pnN>mx7T*x)rAY7|78#aD{I<)cXL{+ti?puvHL7 z5S6iJJ=?QtR>dDhr(gho>r4Gmtr|&^{K{Y_O%H1cvgWVEuD=kjN0fnliF{-;kOv>R rYDwp657F3k+qB|2{>Zq6+_Kt19QwwmTb9qBTWLP|ALBN1k1FSX#6y+4 literal 0 HcmV?d00001 diff --git a/2/fft1d.py b/2/fft1d.py new file mode 100644 index 0000000..cf56f23 --- /dev/null +++ b/2/fft1d.py @@ -0,0 +1,108 @@ +""" +Generování náhodných polí pomocí spektrálních metod (FFT / NUFFT). +Úkoly 3.3: + 1) make_hermitian – reálná funkce z náhodných komplexních koeficientů + 2) generate_grf_fft – GRF na pravidelné mřížce (Gaussovská korelace) + 3) generate_grf_nufft – GRF na nepravidelné mřížce (finufft) + + literatura + +""" + +import numpy as np +from scipy.fft import fftfreq +import finufft + + +# SPEKTRÁLNÍ HUSTOTA + +def spectral_density_gaussian(omega, phi, sigma=1.0): + # C(r) = sigma^2 * exp(-r^2 / 2phi^2) => S(w) = sigma^2 * sqrt(2pi)*phi * exp(-w^2*phi^2/2) + return sigma**2 * np.sqrt(2 * np.pi) * phi * np.exp(-0.5 * (omega * phi)**2) + +def spectral_density_exponential(omega, phi, sigma=1.0): + # C(r) = sigma^2 * exp(-|r|/phi) => S(w) = sigma^2 * 2phi / (1 + (w*phi)^2) + return sigma**2 * 2 * phi / (1 + (omega * phi)**2) + + +# HERMITOVSKÁ SYMETRIE – zaručí reálný výstup IFFT + +def make_hermitian(f_hat, centered=False): + """ + Vyrobí hermitovsky symetrické koeficienty: f_hat[-k] = conj(f_hat[k]) + + centered=False – FFT pořadí: DC na indexu 0, záporné frekvence na konci + centered=True – centrované pořadí: DC uprostřed na indexu N//2 + """ + N = len(f_hat) + f = f_hat.copy() + + if centered: + zi = N // 2 # index DC (nulové frekvence) + f[zi] = f[zi].real + for i in range(1, N // 2): + f[zi - i] = np.conj(f[zi + i]) # záporné frekvence = sdružené kladných + else: + f[0] = f[0].real # DC + if N % 2 == 0: + f[N // 2] = f[N // 2].real # Nyquistova frekvence + for k in range(1, N // 2): + f[-k] = np.conj(f[k]) + + return f + + +# FFT – PRAVIDELNÁ MŘÍŽKA + +def generate_grf_fft(L, N, phi, sigma=1.0, seed=None, + spectral_density_fn=spectral_density_gaussian): + """ + Generuje 1D náhodné pole na pravidelné mřížce. + - náhodné koeficienty z N(0,1) komplexního rozdělení + - modulace filtrem sqrt(S(omega)) + - hermitovská symetrie -> reálný výstup přes IFFT + spectral_density_fn: spectral_density_gaussian | spectral_density_exponential + """ + rng = np.random.default_rng(seed) + dx = L / N + x = np.arange(N) * dx + + omega = 2 * np.pi * fftfreq(N, d=dx) # FFT pořadí + S_k = spectral_density_fn(omega, phi, sigma) + + white = (rng.standard_normal(N) + 1j * rng.standard_normal(N)) / np.sqrt(2) + f_hat = make_hermitian(white * np.sqrt(S_k), centered=False) + + field = np.fft.ifft(f_hat).real * N / np.sqrt(L) + return x, field + + +# NUFFT – NEPRAVIDELNÁ MŘÍŽKA + +def generate_grf_nufft(L, N_freq, M_points, phi, sigma=1.0, seed=None, x_points=None, + spectral_density_fn=spectral_density_gaussian): + """ + Generuje 1D náhodné pole na nepravidelné mřížce pomocí NUFFT typu 2 + (pravidelné frekvence -> nepravidelné prostorové body). + finufft očekává souřadnice v [-pi, pi]. + """ + rng = np.random.default_rng(seed) + + # centrované pořadí: k = -N/2, ..., -1, 0, +1, ..., N/2-1 + k = np.arange(-N_freq // 2, N_freq // 2) + omega = k * (2 * np.pi / L) + S_omega = spectral_density_fn(omega, phi, sigma) + + white = (rng.standard_normal(N_freq) + 1j * rng.standard_normal(N_freq)) / np.sqrt(2) + f_hat = make_hermitian(white * np.sqrt(S_omega), centered=True) + + if x_points is None: + x_nufft = rng.uniform(-np.pi, np.pi, M_points) + else: + x_nufft = (np.asarray(x_points) / L) * 2 * np.pi - np.pi # [0,L] -> [-pi,pi] + + field = finufft.nufft1d2(x_nufft, f_hat.astype(np.complex128)) + x_final = (x_nufft + np.pi) * (L / (2 * np.pi)) # [-pi,pi] -> [0,L] + return x_final, field.real + + diff --git a/2/main2.py b/2/main2.py new file mode 100644 index 0000000..5e04df5 --- /dev/null +++ b/2/main2.py @@ -0,0 +1,35 @@ +import matplotlib.pyplot as plt +import numpy as np +import fft1d as rf + +L, N, phi = 100.0, 1024, 3.0 + +fig, axes = plt.subplots(2, 2, figsize=(14, 8)) + +# --- Gaussovská --- +x, f = rf.generate_grf_fft(L, N, phi, seed=42) +axes[0][0].plot(x, f, lw=1) +axes[0][0].set_title(f"FFT Gaussovská (φ={phi})") + +x_nu, f_nu = rf.generate_grf_nufft(L, 256, 500, phi, seed=42) +idx = np.argsort(x_nu) +axes[0][1].plot(x_nu[idx], f_nu[idx], '-', alpha=0.4, color='gray', lw=0.8) +axes[0][1].scatter(x_nu, f_nu, s=12, c=f_nu, cmap='viridis', zorder=3) +axes[0][1].set_title(f"NUFFT Gaussovská (φ={phi})") + +# --- Exponenciální --- +x, f = rf.generate_grf_fft(L, N, phi, seed=42, spectral_density_fn=rf.spectral_density_exponential) +axes[1][0].plot(x, f, lw=1) +axes[1][0].set_title(f"FFT Exponenciální (φ={phi})") + +x_nu, f_nu = rf.generate_grf_nufft(L, 256, 500, phi, seed=42, spectral_density_fn=rf.spectral_density_exponential) +idx = np.argsort(x_nu) +axes[1][1].plot(x_nu[idx], f_nu[idx], '-', alpha=0.4, color='gray', lw=0.8) +axes[1][1].scatter(x_nu, f_nu, s=12, c=f_nu, cmap='viridis', zorder=3) +axes[1][1].set_title(f"NUFFT Exponenciální (φ={phi})") + +for ax in axes.flat: + ax.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/3/3.2d.py b/3/3.2d.py new file mode 100644 index 0000000..a7a38c3 --- /dev/null +++ b/3/3.2d.py @@ -0,0 +1,216 @@ +""" +Upravit zadání + +""" + +import random +import numpy as np +import finufft +# ============================================================================= +# CORRELATION TŘÍDY +# ============================================================================= + +class GaussianCorrelation: + """ + C(r) = sigma^2 * exp(-|r|^2 / 2*phi^2) + S_nD(omega) = sigma^2 * (sqrt(2pi)*phi)^dim * exp(-|omega|^2*phi^2/2) + """ + def __init__(self, L, N_freq, phi, sigma=1.0, dim=1): + self.phi = phi + self.sigma = sigma + self.dim = dim + self.N_freq = N_freq + self.L = L + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) # 1D osa frekvencí [rad/m] + + def spectral_density(self, omega_mag): + # omega_mag = |omega| (skalár nebo pole) + return (self.sigma**2 + * (np.sqrt(2 * np.pi) * self.phi)**self.dim + * np.exp(-0.5 * (omega_mag * self.phi)**2)) + + +class ExponentialCorrelation: + """ + C(r) = sigma^2 * exp(-|r|/phi) + 1D: S(w) = sigma^2 * 2*phi / (1 + (w*phi)^2) + 2D: S(w) = sigma^2 * 2*pi*phi^2 / (1 + (|w|*phi)^2)^(3/2) + """ + def __init__(self, L, N_freq, phi, sigma=1.0, dim=1): + self.phi = phi + self.sigma = sigma + self.dim = dim + self.N_freq = N_freq + self.L = L + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) + + def spectral_density(self, omega_mag): + if self.dim == 1: + return self.sigma**2 * 2 * self.phi / (1 + (omega_mag * self.phi)**2) + elif self.dim == 2: + return self.sigma**2 * 2 * np.pi * self.phi**2 / (1 + (omega_mag * self.phi)**2)**1.5 + else: + raise NotImplementedError("Exponential: dim > 2 není implementováno") + + +# ============================================================================= +# HERMITOVSKÁ SYMETRIE – zaručí reálný výstup IFFT +# centrované pořadí: DC uprostřed na indexu N//2 (v každé dimenzi) +# podmínka: f_hat[-k] = conj(f_hat[k]) pro všechny k +# ============================================================================= + +def make_hermitian_nd(f_hat): + """ + nD hermitovská symetrie v centrovaném pořadí. + f_hat shape: (N,) pro 1D, (N, N) pro 2D, (N, N, N) pro 3D. + """ + N = f_hat.shape[0] + zi = N // 2 # index DC v každé dimenzi + f = f_hat.copy() + + # DC (všechny indexy == zi) musí být reálná + dc_idx = tuple([zi] * f.ndim) + f[dc_idx] = f[dc_idx].real + + # pro každý multi-index k: f[-k] = conj(f[k]) + # iterujeme přes všechny indexy, záporné nastavíme jako sdružené kladných + for idx in np.ndindex(*f.shape): + # přeskočit DC a body s nulovým indexem (ty jsou sdružené sami sobě) + shifted = tuple(i - zi for i in idx) + if all(s <= 0 for s in shifted): + continue + neg_idx = tuple((zi - s) % N for s in shifted) + f[neg_idx] = np.conj(f[idx]) + + return f + + +def make_white_noise(N_freq, dim=1, seed=None): + """Komplexní bílý šum tvaru (N_freq,)*dim.""" + rng = np.random.default_rng(seed) + shape = (N_freq,) * dim + size = N_freq ** dim + flat = (rng.standard_normal(size) + 1j * rng.standard_normal(size)) / np.sqrt(2) + return flat.reshape(shape) + + +# ============================================================================= +# GENERATE GRF +# ============================================================================= + +def generate_grf_nufft(x_points, corr, weights=None, seed=None): + """ + Generuje náhodné pole v dim dimenzích pomocí NUFFT typu 2. + + x_points : (M,) pro 1D + (M, 2) pro 2D + (M, 3) pro 3D + corr : GaussianCorrelation | ExponentialCorrelation (s dim nastaveným) + weights : bílý šum tvaru (N_freq,)*dim; pokud None, vygeneruje se nový + """ + dim = corr.dim + N_freq = corr.N_freq + + if weights is None: + weights = make_white_noise(N_freq, dim=dim, seed=seed) + + # frekvenční mřížka v nD: |omega| pro každý bod + if dim == 1: + omega_mag = np.abs(corr.f_points) + else: + grids = np.meshgrid(*([corr.f_points] * dim), indexing='ij') + omega_mag = np.sqrt(sum(g**2 for g in grids)) # shape (N_freq,)*dim + + S = corr.spectral_density(omega_mag) + f_hat = make_hermitian_nd(weights * np.sqrt(S)) + + # souřadnice do [-pi, pi] + x_points = np.asarray(x_points) + L = corr.L + + if dim == 1: + x_nu = (x_points / L) * 2 * np.pi - np.pi + field = finufft.nufft1d2(x_nu, f_hat.astype(np.complex128)) + + elif dim == 2: + x_nu = (x_points[:, 0] / L) * 2 * np.pi - np.pi + y_nu = (x_points[:, 1] / L) * 2 * np.pi - np.pi + field = finufft.nufft2d2(x_nu, y_nu, f_hat.astype(np.complex128)) + + elif dim == 3: + x_nu = (x_points[:, 0] / L) * 2 * np.pi - np.pi + y_nu = (x_points[:, 1] / L) * 2 * np.pi - np.pi + z_nu = (x_points[:, 2] / L) * 2 * np.pi - np.pi + field = finufft.nufft3d2(x_nu, y_nu, z_nu, f_hat.astype(np.complex128)) + + else: + raise NotImplementedError("dim > 3 není podporováno finufft") + + return field.real + + +def generate_grf_fft(x_points, corr, weights=None, seed=None): + """Alias pro pravidelnou mřížku – volá generate_grf_nufft.""" + return generate_grf_nufft(x_points, corr, weights=weights, seed=seed) + + +# ============================================================================= +# demo +# ============================================================================= + +if __name__ == "__main__": + import matplotlib.pyplot as plt + + L, N, phi = 100.0, 256, 3.0 + seed = random.randint(3, 9) + # --- 1D --- + x = np.linspace(0, L, N) + x_irr = np.sort(np.random.uniform(0, L, 300)) + white1d = make_white_noise(N, dim=1, seed=seed) + + fig, axes = plt.subplots(2, 2, figsize=(14, 8)) + for row, (label, CorrClass) in enumerate([("Gaussovská", GaussianCorrelation), + ("Exponenciální", ExponentialCorrelation)]): + corr = CorrClass(L, N, phi, dim=1) + axes[row][0].plot(x, generate_grf_fft(x, corr, weights=white1d), lw=1) + axes[row][0].set_title(f"1D FFT {label} (φ={phi})") + f_nu = generate_grf_nufft(x_irr, corr, weights=white1d) + axes[row][1].plot(x_irr, f_nu, '-', alpha=0.4, color='gray', lw=0.8) + axes[row][1].scatter(x_irr, f_nu, s=12, c=f_nu, cmap='viridis', zorder=3) + axes[row][1].set_title(f"1D NUFFT {label} (φ={phi})") + for ax in axes.flat: ax.grid(True, alpha=0.3) + plt.tight_layout(); plt.show() + + # --- 2D --- + N2 = 64 # menší kvůli N^2 bodům + white2d = make_white_noise(N2, dim=2, seed=seed) + g = np.linspace(0, L, N2) + xx, yy = np.meshgrid(g, g) + pts2d = np.column_stack([xx.ravel(), yy.ravel()]) + + # nepravidelné body pro NUFFT + pts2d_irr = np.column_stack([np.random.uniform(0, L, N2**2), + np.random.uniform(0, L, N2**2)]) + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + for col, (label, CorrClass) in enumerate([("Gaussovská", GaussianCorrelation), + ("Exponenciální", ExponentialCorrelation)]): + corr2d = CorrClass(L, N2, phi, dim=2) + + # pravidelná mřížka (FFT) + field2d = generate_grf_nufft(pts2d, corr2d, weights=white2d).reshape(N2, N2) + im = axes[0][col].imshow(field2d, extent=[0, L, 0, L], origin='lower', cmap='viridis') + plt.colorbar(im, ax=axes[0][col]) + axes[0][col].set_title(f"2D FFT {label} (φ={phi})") + + # nepravidelná mřížka (NUFFT) – interpolováno na pravidelnou mřížku pro vizualizaci + f_irr = generate_grf_nufft(pts2d_irr, corr2d, weights=white2d) + from scipy.interpolate import griddata + field_interp = griddata(pts2d_irr, f_irr, (xx, yy), method='linear') + im2 = axes[1][col].imshow(field_interp, extent=[0, L, 0, L], origin='lower', cmap='viridis') + plt.colorbar(im2, ax=axes[1][col]) + axes[1][col].set_title(f"2D NUFFT {label} (φ={phi}, interpolováno)") + + plt.tight_layout(); plt.show() \ No newline at end of file diff --git a/3/3.py b/3/3.py new file mode 100644 index 0000000..7a4d587 --- /dev/null +++ b/3/3.py @@ -0,0 +1,168 @@ +""" +Generování náhodných polí pomocí spektrálních metod (FFT / NUFFT). +""" +import numpy as np +from scipy.fft import fftfreq +import finufft +# ============================================================================= +# CORRELATION TŘÍDY +# ============================================================================= +class GaussianCorrelation: + """ + C(r) = sigma^2 * exp(-r^2 / 2*phi^2) + S(w) = sigma^2 * sqrt(2pi) * phi * exp(-w^2*phi^2 / 2) + """ + def __init__(self, L, N_freq, phi, sigma=1.0): + self.phi = phi + self.sigma = sigma + # frekvenční body v centrovaném pořadí: k = -N/2, ..., N/2-1 + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) # úhlové frekvence [rad/m] + + def spectral_density(self, omega): + # S(w) = sigma^2 * sqrt(2pi) * phi * exp(-w^2*phi^2/2) + return self.sigma**2 * np.sqrt(2 * np.pi) * self.phi * np.exp(-0.5 * (omega * self.phi)**2) + +class ExponentialCorrelation: + """ + C(r) = sigma^2 * exp(-|r|/phi) + S(w) = sigma^2 * 2*phi / (1 + (w*phi)^2) + """ + def __init__(self, L, N_freq, phi, sigma=1.0): + self.phi = phi + self.sigma = sigma + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) + + def spectral_density(self, omega): + # S(w) = sigma^2 * 2*phi / (1 + (w*phi)^2) + return self.sigma**2 * 2 * self.phi / (1 + (omega * self.phi)**2) + +# ============================================================================= +# HERMITOVSKÁ SYMETRIE – zaručí reálný výstup IFFT +# ============================================================================= +def make_hermitian(f_hat, centered=False): + """ + Vyrobí hermitovsky symetrické koeficienty: f_hat[-k] = conj(f_hat[k]) + + centered=False – FFT pořadí: DC na indexu 0 + centered=True – centrované pořadí: DC uprostřed na indexu N//2 + """ + N = len(f_hat) + f = f_hat.copy() + if centered: + zi = N // 2 # index DC + f[zi] = f[zi].real + for i in range(1, N // 2): + f[zi - i] = np.conj(f[zi + i]) # f[-k] = conj(f[k]) + else: + f[0] = f[0].real # DC + if N % 2 == 0: + f[N // 2] = f[N // 2].real # Nyquistova frekvence + for k in range(1, N // 2): + f[-k] = np.conj(f[k]) + return f + + +def make_white_noise(N, seed=None): + """Komplexní bílý šum ze standardního normálního rozdělení.""" + rng = np.random.default_rng(seed) + return (rng.standard_normal(N) + 1j * rng.standard_normal(N)) / np.sqrt(2) + + +# ============================================================================= +# FFT – PRAVIDELNÁ MŘÍŽKA +# ============================================================================= + +def generate_grf_fft(x_points, corr, weights=None, seed=None): + """ + Generuje 1D náhodné pole na pravidelné mřížce pomocí FFT. + + x_points : ndarray (N,) – pravidelná prostorová mřížka + corr : GaussianCorrelation | ExponentialCorrelation + weights : komplexní koeficienty šumu (N_freq,); pokud None, vygenerují se nové + """ + N_freq = len(corr.f_points) + if weights is None: + weights = make_white_noise(N_freq, seed=seed) + + S = corr.spectral_density(corr.f_points) + f_hat = make_hermitian(weights * np.sqrt(S), centered=True) + + # stejná code path jako NUFFT -> identické výsledky na pravidelné mřížce + return generate_grf_nufft(x_points, corr, weights=weights) + + +# ============================================================================= +# NUFFT – NEPRAVIDELNÁ MŘÍŽKA +# ============================================================================= + +def generate_grf_nufft(x_points, corr, weights=None, seed=None): + """ + Generuje 1D náhodné pole na nepravidelné mřížce pomocí NUFFT typu 2. + + x_points : ndarray (M,) – libovolné prostorové souřadnice v [0, L] + corr : GaussianCorrelation | ExponentialCorrelation + weights : komplexní koeficienty šumu (N_freq,); pokud None, vygenerují se nové + -> stejné weights jako u FFT = srovnatelné realizace + """ + N_freq = len(corr.f_points) + if weights is None: + weights = make_white_noise(N_freq, seed=seed) + + S = corr.spectral_density(corr.f_points) + f_hat = make_hermitian(weights * np.sqrt(S), centered=True) + + # finufft očekává souřadnice v [-pi, pi] + L = x_points[-1] - x_points[0] + x_nufft = (x_points / L) * 2 * np.pi - np.pi # [0,L] -> [-pi,pi] + + field = finufft.nufft1d2(x_nufft, f_hat.astype(np.complex128)) + return field.real + + +# ============================================================================= +# demo.py +# ============================================================================= + +if __name__ == "__main__": + import matplotlib.pyplot as plt + + L, N, phi = 100.0, 1024, 3.0 + N_freq = N # stejný pro FFT i NUFFT -> srovnatelné realizace + + x_regular = np.linspace(0, L, N) + x_irregular = np.sort(np.random.uniform(0, L, 500)) + + corr_g = GaussianCorrelation(L, N_freq, phi) + corr_e = ExponentialCorrelation(L, N_freq, phi) + + white = make_white_noise(N_freq) # stejný šum pro obě metody + + fig, axes = plt.subplots(2, 2, figsize=(14, 8)) + + # Gaussovská + f_fft = generate_grf_fft(x_regular, corr_g, weights=white) + axes[0][0].plot(x_regular, f_fft, lw=1) + axes[0][0].set_title(f"FFT Gaussovská (φ={phi})") + + f_nu = generate_grf_nufft(x_irregular, corr_g, weights=white) + axes[0][1].plot(x_irregular, f_nu, "-", alpha=0.4, color="gray", lw=0.8) + axes[0][1].scatter(x_irregular, f_nu, s=12, c=f_nu, cmap="viridis", zorder=3) + axes[0][1].set_title(f"NUFFT Gaussovská (φ={phi})") + + # Exponenciální + f_fft = generate_grf_fft(x_regular, corr_e, weights=white) + axes[1][0].plot(x_regular, f_fft, lw=1) + axes[1][0].set_title(f"FFT Exponenciální (φ={phi})") + + f_nu = generate_grf_nufft(x_irregular, corr_e, weights=white) + axes[1][1].plot(x_irregular, f_nu, "-", alpha=0.4, color="gray", lw=0.8) + axes[1][1].scatter(x_irregular, f_nu, s=12, c=f_nu, cmap="viridis", zorder=3) + axes[1][1].set_title(f"NUFFT Exponenciální (φ={phi})") + + for ax in axes.flat: + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/3/Figure_1.png b/3/Figure_1.png new file mode 100644 index 0000000000000000000000000000000000000000..31851d1256942d5393ea2264655e828bed058bb4 GIT binary patch literal 64735 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

vfDe54}&<1b0t9l&8L|R5G-X+O$E!l(Zti z?B(}tKc4tHD!n#>>CSq=ALXnH4fcPT%>FBX6j(X_uar{B`|QwA#T63O{|oC8XaqpZ zuh-$tT3B5dfJO~ zq1E)%ltU-YMDe+&z&vIa@7iV`8k^Zv{kSsraAkFzROVvM+qMNp&iZMGTZoZkmNK0+ z-xvGG&PN5mMl|Xl7s}#j?PZ}u8;prXnI&2jTqDEymZVG0oN(+sZE-DA@q7?`+)$?b zmnz2N%=dLuvoUzfsVy#nHDKM+ zSnqGf?;Ryh{vFmo^xVF_EWNtI-#TojFIk+84=(G2N}MLC{mOf7!dlmbLI`O+;bq7j z0;J_V(>Gur0`IXaclAz{8B1ern-^m3>xTuf9|jGRVIPWZ5nkf*p?K@B9@EntcQA5c z_<-p2&ok0rJGz&~cYU8780+r3IM9t!Wb&#}ZBD|oKb;f)_U(gSK*Q?J+fst5!ksef zt{^S6uidi=B7XCP+)X<+9cp;q4yf(rYoBLP^-r7RS8|iVIhICXo8K9e)N;=pjEZcz z2&5E%-E>O}-YE4xTw9M}-mx=k+)3tp$%)p@Ic6{Qds6g=G>T$|e;=|v~lzVz~82^{;FMnQ-lu94J2;PA2hIrJsa1 ze5Qi8++-JBtGYTd*LWO(@HnoPS*(KCPr8%>JA>LfvmSg#2>#x%WOC(M`{@c(4yyak z2k{;<2_}f*MKBT#Pu5O;0Sp4f?xcGmn25QT`-^sK5rCKwB(W0Wc%P+gN{-1VX5c< zvJgJUUaKWV=3R5zY-Rg((roMx#`B}$9SOFLy$mZK6%(64GW zzZ0zh-`h-fYGOl(amC}|H*ah{?Kr-@ZLx;2mD9)Cp1H$qY{_QIL3fdsxLupN7}aFS zYS@S!yTKarMmMzZav^a%a`-TWmUGQ;v;#l-@NM&d!knRr6sX0-q&}#G}WB|2OP27+4%Ww$g>lxzD@Kz*Acq@SoyLw0o0pd&CQlNBb=^QlKwNNQT8auY4QR z(>M)_y*FtxGV;K#iAd2nC_?e~ub z9W>}jr1177w%w!`?mTrH3?&xcWa#Rj4=T^d8>GmbL7b$Xc%chh5y=Z&e(tp-%qvFL zv!tORp@A0ph@9EGH#ygN%rME?GUDtHc3RY&%WO1hfA@Xs8K!2@&Je;zhZteoLK5to zC|g~a0<2$~DY$6nl3_zX;%sX%8G891=K28XA;+7N6_IBaLbn5`sV{@yypMq^Y?F%U zFM=POOL{af_xN7HZ=c^u55&G)JMu*hR_;g58CBw@~R#+2P9_R6v;1uVF*K$FAb=Rp<7 z90w1DYb?qu`!aFOQrUyi(YgYWM2`a9wd9L|?hk88g$v!~y=CI7^v_QksVc<8;Ri;^ z;b2NES1N`j$M{^^;~k}_Q32&&Ps73|?KK)^X_LY@lSLw-#@Ob3aN+43(lfnN&np?3 zAH|xbl8-h{*Nao#-@jgIV?+3kdR9cfR(-^m1wcTpJncd)z!E;9Jzt;u5EFvwehfKF zs^&5iHI0l?kGevqC4RmG7E77+gp?G{qf0$#TPh`F=4ch@=hOlKLf-8Q@2yV%Q^VQw z`StybK0va_27DP$+}2}lgN%Zj%_pvtFC=mSl<-l@q@ycP|K!+gCP(yXs7l>|ykhgf z9r6e`Qwp_n5Ib{Oc~3Nf&Jz@q7EY1{?~#=Iw*-HWhKDftDER{fep>0tp@B0-d>)Xi zwICjD4biS{-wo0%fsU|IcDL_l$q|JXa?g8$cl^N}%0A&U1Dt3YT)P8peeShu9OC9Q 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 IZU6B90P)T=vj6}9 literal 0 HcmV?d00001 diff --git a/3/Untitled-sada2.py b/3/Untitled-sada2.py new file mode 100644 index 0000000..35c1b8b --- /dev/null +++ b/3/Untitled-sada2.py @@ -0,0 +1,309 @@ +""" +Živá vizualizace 2D GRF – spektrální metoda (FFT) +Každou sekundu nový white noise; přepínání Gaussovská ↔ Exponenciální. + +Postaveno na původním kódu (GaussianCorrelation, ExponentialCorrelation, +make_hermitian_nd, make_white_noise) — generování přes numpy.fft místo finufft, +výsledek je identický pro pravidelnou mřížku. +""" + +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.widgets import Button, Slider, RadioButtons +import matplotlib.gridspec as gridspec + +matplotlib.rcParams.update({ + "figure.facecolor": "#07080f", + "axes.facecolor": "#07080f", + "text.color": "#9ac8e8", + "axes.edgecolor": "#1a3a5a", + "axes.labelcolor": "#4a7a9a", + "xtick.color": "#2a5a7a", + "ytick.color": "#2a5a7a", + "font.family": "monospace", + "font.size": 9, +}) + + +# ============================================================================= +# CORRELATION TŘÍDY (beze změny logiky z původního souboru) +# ============================================================================= + +class GaussianCorrelation: + """C(r) = σ² exp(−|r|²/2φ²) → S_2D(ω) = σ²(√2π φ)² exp(−|ω|²φ²/2)""" + def __init__(self, L, N_freq, phi, sigma=1.0, dim=2): + self.phi, self.sigma, self.dim = phi, sigma, dim + self.N_freq, self.L = N_freq, L + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) + + def spectral_density(self, omega_mag): + return (self.sigma ** 2 + * (np.sqrt(2 * np.pi) * self.phi) ** self.dim + * np.exp(-0.5 * (omega_mag * self.phi) ** 2)) + + +class ExponentialCorrelation: + """C(r) = σ² exp(−|r|/φ) → S_2D(ω) = σ² 2π φ² / (1+|ω|²φ²)^(3/2)""" + def __init__(self, L, N_freq, phi, sigma=1.0, dim=2): + self.phi, self.sigma, self.dim = phi, sigma, dim + self.N_freq, self.L = N_freq, L + k = np.arange(-N_freq // 2, N_freq // 2) + self.f_points = k * (2 * np.pi / L) + + def spectral_density(self, omega_mag): + if self.dim == 2: + return (self.sigma ** 2 * 2 * np.pi * self.phi ** 2 + / (1 + (omega_mag * self.phi) ** 2) ** 1.5) + raise NotImplementedError("dim != 2") + + +# ============================================================================= +# HERMITOVSKÁ SYMETRIE (původní make_hermitian_nd – zrychlená pro 2D) +# ============================================================================= + +def make_hermitian_2d(f_hat): + """ + Hermitovská symetrie v centrovaném pořadí pro 2D pole. + Použito np.roll místo explicitní iterace → ~100× rychlejší. + """ + N = f_hat.shape[0] + f = f_hat.copy() + # f[-kx, -ky] = conj(f[kx, ky]) + # v centrovaném pořadí: index zi odpovídá k=0 + zi = N // 2 + # flip přes obě osy a otočit o jeden prvek, aby DC zůstalo na místě + conj_flip = np.conj(np.roll(np.roll(f[::-1, ::-1], 1, axis=0), 1, axis=1)) + # DC musí být reálný + mask = np.ones((N, N), dtype=bool) + mask[zi, zi] = False + # průměrujeme: f_herm = (f + conj_flip) / sqrt(2) zachovává rozptyl + f = (f + conj_flip) / np.sqrt(2) + f[zi, zi] = f[zi, zi].real + return f + + +def make_white_noise(N_freq, dim=2, seed=None): + """Komplexní bílý šum tvaru (N_freq,)*dim.""" + rng = np.random.default_rng(seed) + shape = (N_freq,) * dim + size = N_freq ** dim + flat = (rng.standard_normal(size) + 1j * rng.standard_normal(size)) / np.sqrt(2) + return flat.reshape(shape) + + +# ============================================================================= +# GENEROVÁNÍ GRF (pravidelná 2D mřížka – numpy.fft místo finufft) +# ============================================================================= + +def generate_grf_fft_2d(N, corr, weights=None, seed=None): + """ + Generuje 2D GRF na pravidelné mřížce N×N. + + Ekvivalentní generate_grf_nufft pro pts2d na pravidelné mřížce, + ale bez závislosti na finufft. + """ + if weights is None: + weights = make_white_noise(N, dim=2, seed=seed) + + # |ω| pro každý frekvenční bod + gx, gy = np.meshgrid(corr.f_points, corr.f_points, indexing='ij') + omega_mag = np.sqrt(gx ** 2 + gy ** 2) + + S = corr.spectral_density(omega_mag) # (N, N) + f_hat_centered = make_hermitian_2d(weights * np.sqrt(S)) + + # centrované → standardní FFT pořadí + f_hat = np.fft.ifftshift(f_hat_centered) + + # IFFT → reálná část + field = np.fft.ifft2(f_hat).real # (N, N) + return field + + +# ============================================================================= +# GUI +# ============================================================================= + +L = 100.0 +N = 128 +PHI = 8.0 + +CMAP = "turbo" + +CORR_CLASSES = { + "Gaussovská": GaussianCorrelation, + "Exponenciální": ExponentialCorrelation, +} + + +class LiveGRF: + def __init__(self): + self.corr_name = "Gaussovská" + self.phi = PHI + self.paused = False + self.frame = 0 + self._build_corr() + + # ── layout ────────────────────────────────────────────────────────── + self.fig = plt.figure(figsize=(9, 8), facecolor="#07080f") + self.fig.canvas.manager.set_window_title("2D GRF – živá vizualizace") + + gs = gridspec.GridSpec( + 4, 3, + figure=self.fig, + left=0.06, right=0.96, + top=0.93, bottom=0.04, + hspace=0.55, wspace=0.35, + height_ratios=[14, 1.2, 1.2, 1.2], + ) + + # hlavní obraz + self.ax_img = self.fig.add_subplot(gs[0, :]) + self.ax_img.set_aspect("equal") + self.ax_img.tick_params(labelsize=7) + self.ax_img.set_xlabel("x [m]", fontsize=8) + self.ax_img.set_ylabel("y [m]", fontsize=8) + + # slider φ + ax_sl = self.fig.add_subplot(gs[1, :]) + ax_sl.set_facecolor("#07080f") + self.slider = Slider( + ax_sl, "φ [korelační délka]", + valmin=1, valmax=30, valinit=self.phi, valstep=0.5, + color="#1a4a7a", track_color="#0d1a2a", + ) + self.slider.label.set_color("#4a9adf") + self.slider.valtext.set_color("#7ecfff") + self.slider.on_changed(self._on_phi) + + # radio tlačítka + ax_radio = self.fig.add_subplot(gs[2, 0]) + ax_radio.set_facecolor("#07080f") + self.radio = RadioButtons( + ax_radio, + labels=list(CORR_CLASSES.keys()), + active=0, + activecolor="#4a9adf", + ) + for lbl in self.radio.labels: + lbl.set_fontsize(9) + lbl.set_color("#9ac8e8") + self.radio.on_clicked(self._on_corr) + + # pause / step tlačítka + ax_btn_pause = self.fig.add_subplot(gs[2, 1]) + ax_btn_step = self.fig.add_subplot(gs[2, 2]) + for ax in (ax_btn_pause, ax_btn_step): + ax.set_facecolor("#07080f") + + self.btn_pause = Button(ax_btn_pause, "⏸ Pauza", + color="#0d1520", hovercolor="#162030") + self.btn_step = Button(ax_btn_step, "↻ Generovat", + color="#0d1520", hovercolor="#162030") + for btn in (self.btn_pause, self.btn_step): + btn.label.set_color("#7ecfff") + btn.label.set_fontsize(9) + self.btn_pause.on_clicked(self._on_pause) + self.btn_step.on_clicked(self._on_step) + + # info text ve spodku + self.ax_info = self.fig.add_subplot(gs[3, :]) + self.ax_info.axis("off") + self.info_txt = self.ax_info.text( + 0.5, 0.5, "", ha="center", va="center", + transform=self.ax_info.transAxes, + fontsize=8, color="#2a6a9a", + ) + + # colorbar + self.cbar = None + + # první snímek + field = self._gen() + self.im = self.ax_img.imshow( + field, extent=[0, L, 0, L], + origin="lower", cmap=CMAP, + interpolation="bilinear", + ) + self.cbar = self.fig.colorbar(self.im, ax=self.ax_img, + fraction=0.03, pad=0.02) + self.cbar.ax.tick_params(colors="#4a7a9a", labelsize=7) + self._set_title() + self._update_info(field) + + # ── helpers ───────────────────────────────────────────────────────────── + + def _build_corr(self): + cls = CORR_CLASSES[self.corr_name] + self.corr = cls(L, N, self.phi, dim=2) + + def _gen(self): + self.frame += 1 + return generate_grf_fft_2d(N, self.corr) + + def _set_title(self): + self.ax_img.set_title( + f"2D GRF · {self.corr_name} korelace · " + f"φ = {self.phi:.1f} m · frame #{self.frame:04d}", + fontsize=10, color="#7ecfff", pad=6, + ) + + def _update_info(self, field): + mn, mx, sd = field.min(), field.max(), field.std() + self.info_txt.set_text( + f"min = {mn:+.4f} max = {mx:+.4f} σ = {sd:.4f} " + f"N = {N}×{N} L = {L} m" + ) + + # ── callbacks ──────────────────────────────────────────────────────────── + + def _on_phi(self, val): + self.phi = val + self._build_corr() + self._refresh() + + def _on_corr(self, label): + self.corr_name = label + self._build_corr() + self._refresh() + + def _on_pause(self, _): + self.paused = not self.paused + self.btn_pause.label.set_text("▶ Spustit" if self.paused else "⏸ Pauza") + self.fig.canvas.draw_idle() + + def _on_step(self, _): + self._refresh() + + def _refresh(self): + field = self._gen() + self.im.set_data(field) + self.im.set_clim(field.min(), field.max()) + self._set_title() + self._update_info(field) + self.fig.canvas.draw_idle() + + # ── animace ────────────────────────────────────────────────────────────── + + def animate(self, _i): + if not self.paused: + self._refresh() + + def run(self): + self._anim = animation.FuncAnimation( + self.fig, + self.animate, + interval=1000, # každou sekundu + cache_frame_data=False, + ) + plt.show() + + +# ============================================================================= + +if __name__ == "__main__": + app = LiveGRF() + app.run() \ No newline at end of file diff --git a/3/__pycache__/3.2d.cpython-313.pyc b/3/__pycache__/3.2d.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ec10e902182679f07f807d1079759588f2929d1 GIT binary patch literal 11890 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 N!C9KpY?V#-{{vGHeVhOQ literal 0 HcmV?d00001 diff --git a/3/gstest.py b/3/gstest.py new file mode 100644 index 0000000..434603e --- /dev/null +++ b/3/gstest.py @@ -0,0 +1,20 @@ +import numpy as np +import matplotlib.pyplot as plt + +import gstools as gs +x = y = np.linspace(0, 100, 100) +model = gs.TPLStable( + dim=2, # spatial dimension + var=1, # variance (C is calculated internally, so variance is actually 1) + len_low=0, # lower truncation of the power law + len_scale=10, # length scale (a.k.a. range), len_up = len_low + len_scale + nugget=0.1, # nugget + anis=0.5, # anisotropy between main direction and transversal ones + angles=np.pi / 4, # rotation angles + alpha=1.5, # shape parameter from the stable model + hurst=0.7, # hurst coefficient from the power law +) +srf = gs.SRF(model, mean=1.0, seed=19970221) +srf.structured([x, y]) +srf.plot() +plt.show() # blocks until you close the window \ No newline at end of file diff --git a/4/4.py b/4/4.py new file mode 100644 index 0000000..52deecd --- /dev/null +++ b/4/4.py @@ -0,0 +1,140 @@ +""" +Porovnání našeho FFT/NUFFT přístupu s GSTools. + +GSTools používá RandMeth (Randomization Method): + u(x) = sqrt(sigma^2 / N) * sum_j [ Z1_j * cos() + Z2_j * sin() ] + kde k_j jsou náhodné vzorky ze spektrální hustoty S(omega) + a Z1, Z2 jsou nezávislé N(0,1) vzorky + +Náš přístup (FFT/NUFFT): + u(x) = IFFT[ white * sqrt(S(k)) ] + kde k jsou PRAVIDELNÉ frekvence a white je komplexní N(0,1) + +-> algoritmy jsou různé, srovnáváme proto statisticky přes variogram +""" + +import numpy as np +import matplotlib.pyplot as plt +import gstools as gs +import importlib.util +from pathlib import Path + +module_path = Path(__file__).resolve().parents[1] / "3" / "3.2d.py" +spec = importlib.util.spec_from_file_location("grf_2d_module", module_path) +grf_2d_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(grf_2d_module) + +GaussianCorrelation = grf_2d_module.GaussianCorrelation +make_white_noise = grf_2d_module.make_white_noise +generate_grf_nufft = grf_2d_module.generate_grf_nufft + + +L, N, phi, sigma = 100.0, 512, 5.0, 1.0 +seed = 42 + +# ============================================================================= +# 1. NAŠE METODA – 1D pole +# ============================================================================= +x = np.linspace(0, L, N) +corr = GaussianCorrelation(L, N, phi, sigma=sigma, dim=1) +white = make_white_noise(N, dim=1, seed=seed) +field_ours = generate_grf_nufft(x, corr, weights=white) + +# ============================================================================= +# 2. GSTOOLS – 1D pole +# ============================================================================= +model = gs.Gaussian(dim=1, var=sigma**2, len_scale=phi) +srf = gs.SRF(model, seed=seed) +field_gs = np.asarray(srf(x)).ravel() + +# --- Var 1: reimplementace RandMeth pomocí interních atributů GSTools --- +# po zavolání srf(x) jsou dostupné: +# srf.generator._z_1, _z_2 : N(0,1) vzorky (shape: mode_no,) +# srf.generator._cov_sample : náhodné frekvence k_j (shape: dim, mode_no) +z1 = srf.generator._z_1 # N(0,1) +z2 = srf.generator._z_2 # N(0,1) +k_j = srf.generator._cov_sample # shape (1, mode_no) pro 1D +mode_no = len(z1) + +# u(x) = sqrt(sigma^2 / N) * sum_j [ Z1_j*cos(k_j*x) + Z2_j*sin(k_j*x) ] +# k_j * x -> (mode_no, N) matice skalárních součinů +kx = k_j[0, :, np.newaxis] * x[np.newaxis, :] # (mode_no, N) +field_randmeth = np.sqrt(sigma**2 / mode_no) * np.sum( + z1[:, np.newaxis] * np.cos(kx) + z2[:, np.newaxis] * np.sin(kx), axis=0 +) + +# ============================================================================= +# 3. EMPIRICKÝ VARIOGRAM gamma(h) = 0.5 * E[(f(x) - f(x+h))^2] +# ============================================================================= + +def empirical_variogram(x, field, n_bins=30): + """Odhadne variogram z jedné realizace pomocí všech párů bodů.""" + h_vals, gamma_vals = [], [] + N = len(x) + # bereme podvzorek párů aby to nebylo O(N^2) příliš pomalé + rng = np.random.default_rng(0) + idx = rng.choice(N, size=min(N, 300), replace=False) + xi, fi = x[idx], field[idx] + + pairs_h, pairs_sq = [], [] + for i in range(len(xi)): + for j in range(i + 1, len(xi)): + pairs_h.append(abs(xi[i] - xi[j])) + pairs_sq.append((fi[i] - fi[j])**2) + + pairs_h = np.array(pairs_h) + pairs_sq = np.array(pairs_sq) + + bins = np.linspace(0, pairs_h.max(), n_bins + 1) + for k in range(n_bins): + mask = (pairs_h >= bins[k]) & (pairs_h < bins[k+1]) + if mask.sum() > 5: + h_vals.append(0.5 * (bins[k] + bins[k+1])) + gamma_vals.append(0.5 * pairs_sq[mask].mean()) + + return np.array(h_vals), np.array(gamma_vals) + +# teoretický variogram: gamma(h) = sigma^2 * (1 - C(h)/sigma^2) +# Gaussovský: C(h) = sigma^2 * exp(-h^2 / 2*phi^2) +h_theory = np.linspace(0, L/2, 200) +gamma_theory = sigma**2 * (1 - np.exp(-0.5 * (h_theory / phi)**2)) + +h_ours, g_ours = empirical_variogram(x, field_ours) +h_gs, g_gs = empirical_variogram(x, field_gs) +h_rm, g_rm = empirical_variogram(x, field_randmeth) + +# ============================================================================= +# GRAFY +# ============================================================================= + +fig, axes = plt.subplots(1, 3, figsize=(16, 4)) + +# pole: GSTools vs naše reimplementace RandMeth (měly by být identické) +axes[0].plot(x, field_gs, lw=1.5, label="GSTools") +axes[0].plot(x, field_randmeth, lw=1, label="naše RandMeth", linestyle='--', alpha=0.8) +axes[0].plot(x, field_ours, lw=1, label="naše NUFFT", alpha=0.6) +axes[0].set_title(f"1D realizace (φ={phi})") +axes[0].legend(fontsize=8); axes[0].grid(True, alpha=0.3) + +# variogram – naše vs GSTools +axes[1].plot(h_ours, g_ours, 'o-', ms=4, label="empirický – naše NUFFT") +axes[1].plot(h_gs, g_gs, 's-', ms=4, label="empirický – GSTools") +axes[1].plot(h_rm, g_rm, '^-', ms=4, label="empirický – naše RandMeth") +axes[1].plot(h_theory, gamma_theory, 'k--', lw=2, label="teoretický") +axes[1].set_title("Empirický variogram") +axes[1].set_xlabel("h [m]"); axes[1].set_ylabel("γ(h)") +axes[1].legend(); axes[1].grid(True, alpha=0.3) + +# variogram pouze GSTools (gs má vestavěný) +bin_edges = np.linspace(0, L/2, 31) +bin_centers, vario_gs = gs.vario_estimate([x], field_gs, bin_edges=bin_edges) +axes[2].scatter(bin_centers, vario_gs, + s=20, label="gs.vario_estimate") +axes[2].plot(h_theory, gamma_theory, 'k--', lw=2, label="teoretický") +axes[2].set_title("GSTools vario_estimate") +axes[2].set_xlabel("h [m]"); axes[2].set_ylabel("γ(h)") +axes[2].legend(); axes[2].grid(True, alpha=0.3) + +plt.suptitle(f"Srovnání naše NUFFT vs GSTools (Gaussovská korelace, φ={phi})") +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/4/__pycache__/grf.cpython-313.pyc b/4/__pycache__/grf.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9d6bc603b407a12fce63ecc10f37156fbaa90d7 GIT binary patch literal 10904 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 2 není implementováno") + + +# ============================================================================= +# HERMITOVSKÁ SYMETRIE f_hat[-k] = conj(f_hat[k]) → reálný IFFT výstup +# ============================================================================= + +def make_hermitian_nd(f_hat): + """nD hermitovská symetrie v centrovaném pořadí (DC uprostřed na indexu N//2).""" + N = f_hat.shape[0] + zi = N // 2 + f = f_hat.copy() + f[tuple([zi] * f.ndim)] = f[tuple([zi] * f.ndim)].real # DC reálná + for idx in np.ndindex(*f.shape): + shifted = tuple(i - zi for i in idx) + if all(s <= 0 for s in shifted): + continue + f[tuple((zi - s) % N for s in shifted)] = np.conj(f[idx]) + return f + + +def make_white_noise(N_freq, dim=1, seed=None, use_gstools_rng=False): + """ + Komplexní bílý šum tvaru (N_freq,)*dim ze standardního normálního rozdělení. + + use_gstools_rng=True – použije gstools.random.RNG (stejný generátor jako GSTools interně) + self._rng = RNG(seed) + z_1 = self._rng.random.normal(size=N) + z_2 = self._rng.random.normal(size=N) + use_gstools_rng=False – numpy default_rng (výchozí) + """ + size = N_freq ** dim + if use_gstools_rng: + from gstools.random import RNG + gs_rng = RNG(seed) + rs = gs_rng.random # numpy RandomState stream + flat = (rs.normal(size=size) + 1j * rs.normal(size=size)) / np.sqrt(2) + else: + rng = np.random.default_rng(seed) + flat = (rng.standard_normal(size) + 1j * rng.standard_normal(size)) / np.sqrt(2) + return flat.reshape((N_freq,) * dim) + + +# ============================================================================= +# GENEROVÁNÍ GRF +# ============================================================================= + +def generate_grf(x_points, corr, weights=None, seed=None): + """ + Generuje náhodné pole pomocí NUFFT typu 2. + + x_points : (M,) pro 1D, (M, 2) pro 2D + corr : GaussianCorrelation | ExponentialCorrelation + weights : bílý šum tvaru (N_freq,)*dim; None = vygeneruje se nový + + Vrací field.real tvaru (M,) + """ + dim = corr.dim + N_freq = corr.N_freq + L = corr.L + + if weights is None: + weights = make_white_noise(N_freq, dim=dim, seed=seed) + + # |omega| pro každý frekvenční bod + if dim == 1: + omega_mag = np.abs(corr.f_points) + else: + grids = np.meshgrid(*([corr.f_points] * dim), indexing='ij') + omega_mag = np.sqrt(sum(g**2 for g in grids)) + + S = corr.spectral_density(omega_mag) + f_hat = make_hermitian_nd(weights * np.sqrt(S)) + + x_points = np.asarray(x_points) + + if dim == 1: + x_nu = (x_points / L) * 2 * np.pi - np.pi # [0,L] -> [-pi,pi] + field = finufft.nufft1d2(x_nu, f_hat.astype(np.complex128)) + elif dim == 2: + x_nu = (x_points[:, 0] / L) * 2 * np.pi - np.pi + y_nu = (x_points[:, 1] / L) * 2 * np.pi - np.pi + field = finufft.nufft2d2(x_nu, y_nu, f_hat.astype(np.complex128)) + else: + raise NotImplementedError("dim > 2") + + # normalizace: C(0) = sigma² = (1/2π)*∫S(ω)dω ≈ (Δω/2π)*ΣS(ωk) = (1/L)*ΣS(ωk) + # -> faktor (1/L)^(dim/2) + norm = (1.0 / L) ** (dim / 2) + return (field * norm).real + +# ============================================================================= +# EMPIRICKÝ VARIOGRAM gamma(h) = 0.5 * E[(f(x)-f(x+h))^2] +# ============================================================================= + +def empirical_variogram(x, field, n_bins=30, n_sample=300): + """Odhadne variogram z realizace pole. Bere podvzorek n_sample bodů.""" + rng = np.random.default_rng(0) + idx = rng.choice(len(x), size=min(len(x), n_sample), replace=False) + xi, fi = x[idx], field[idx] + + pairs_h, pairs_sq = [], [] + for i in range(len(xi)): + for j in range(i + 1, len(xi)): + pairs_h.append(abs(xi[i] - xi[j])) + pairs_sq.append((fi[i] - fi[j])**2) + + pairs_h = np.array(pairs_h) + pairs_sq = np.array(pairs_sq) + bins = np.linspace(0, pairs_h.max(), n_bins + 1) + + h_vals, g_vals = [], [] + for k in range(n_bins): + mask = (pairs_h >= bins[k]) & (pairs_h < bins[k+1]) + if mask.sum() > 5: + h_vals.append(0.5 * (bins[k] + bins[k+1])) + g_vals.append(0.5 * pairs_sq[mask].mean()) + + return np.array(h_vals), np.array(g_vals) \ No newline at end of file diff --git a/4/main_fields.py b/4/main_fields.py new file mode 100644 index 0000000..1fc76d1 --- /dev/null +++ b/4/main_fields.py @@ -0,0 +1,153 @@ +""" +main_fields.py – Bod 3 + 4 + +Bod 3: GRF 1D – pravidelná a nepravidelná mřížka, body vyznačeny na ose +Bod 4: GRF 2D – pravidelná a nepravidelná mřížka +""" + +import numpy as np +import matplotlib.pyplot as plt +import finufft +from scipy.interpolate import griddata + +import grf + +L, N, phi, seed = 100.0, 512, 5.0, 42 +N_sparse = 50 # počet bodů řídké NUFFT mřížky +N_dense = 500 # počet bodů husté NUFFT mřížky +rng = np.random.default_rng(seed) + + +# ============================================================================= +# BOD 3: GRF 1D +# ============================================================================= + +x_reg = np.linspace(0, L, N) +x_irr = np.sort(rng.uniform(0, L, N // 2)) +white = grf.make_white_noise(N, dim=1, seed=seed) + +fig, axes = plt.subplots(2, 3, figsize=(18, 8)) +for row, (label, CorrClass) in enumerate([("Gaussovská", grf.GaussianCorrelation), + ("Exponenciální", grf.ExponentialCorrelation)]): + corr = CorrClass(L, N, phi, dim=1) + + f_r = grf.generate_grf(x_reg, corr, weights=white) + ax = axes[row][0] + ax.plot(x_reg, f_r, lw=1) + ax.plot(x_reg, np.full(N, f_r.min() - 0.3), + '|', color='steelblue', ms=6, label="body mřížky") + ax.set_title(f"FFT pravidelná – {label} φ={phi}") + ax.legend(fontsize=8); ax.grid(True, alpha=0.3) + + # reference: hustá pravidelná mřížka + x_ref = np.linspace(0, L, N) + f_ref = grf.generate_grf(x_ref, corr, weights=white) + + # řídká nepravidelná + x_sparse = np.sort(rng.uniform(0, L, N_sparse)) + f_sparse = grf.generate_grf(x_sparse, corr, weights=white) + # interpolace řídkých bodů na jemnou mřížku + from scipy.interpolate import interp1d + f_sparse_interp = interp1d(x_sparse, f_sparse, kind='cubic', + bounds_error=False, fill_value='extrapolate')(x_ref) + + ax = axes[row][1] + ax.plot(x_ref, f_sparse_interp, '-', color='darkorange', lw=1.5, alpha=0.4, + label=f"interp z {N_sparse} bodů") + ax.plot(x_ref, f_ref, 'k-', lw=1, label="reference") + ax.scatter(x_sparse, f_sparse, s=25, color='darkorange', zorder=3) + ax.plot(x_sparse, np.full(len(x_sparse), f_ref.min() - 0.3), + '|', color='darkorange', ms=8) + ax.set_title(f"NUFFT řídká ({N_sparse} bodů) – {label}") + ax.legend(fontsize=8); ax.grid(True, alpha=0.3) + + # hustá nepravidelná + x_dense = np.sort(rng.uniform(0, L, N_dense)) + f_dense = grf.generate_grf(x_dense, corr, weights=white) + f_dense_interp = interp1d(x_dense, f_dense, kind='cubic', + bounds_error=False, fill_value='extrapolate')(x_ref) + + ax = axes[row][2] + ax.plot(x_ref, f_dense_interp, '-', color='steelblue', lw=1.5, alpha=0.4, + label=f"interp z {N_dense} bodů") + ax.plot(x_ref, f_ref, 'k-', lw=1, label="reference") + ax.scatter(x_dense, f_dense, s=4, color='steelblue', zorder=3) + ax.plot(x_dense, np.full(len(x_dense), f_ref.min() - 0.3), + '|', color='steelblue', ms=4) + ax.set_title(f"NUFFT hustá ({N_dense} bodů) – {label}") + ax.legend(fontsize=8); ax.grid(True, alpha=0.3) + +plt.suptitle("Bod 3: GRF 1D") +plt.tight_layout(); plt.show() + + +# ============================================================================= +# BOD 4: GRF 2D +# ============================================================================= + +N2 = 64 +N2_sparse = 20 # řídká mřížka – počet bodů na osu (N2_sparse^2 celkem) +N2_dense = 64 # hustá mřížka – počet bodů na osu (N2_dense^2 celkem) + +white2d = grf.make_white_noise(N2, dim=2, seed=seed) +g = np.linspace(0, L, N2) +xx, yy = np.meshgrid(g, g) + +n_sparse = N2_sparse**2 +n_dense = N2_dense**2 +pts_sparse = np.column_stack([rng.uniform(0, L, n_sparse), rng.uniform(0, L, n_sparse)]) +pts_dense = np.column_stack([rng.uniform(0, L, n_dense), rng.uniform(0, L, n_dense)]) + +for label, CorrClass in [("Gaussovská", grf.GaussianCorrelation), + ("Exponenciální", grf.ExponentialCorrelation)]: + corr2d = CorrClass(L, N2, phi, dim=2) + + # 1. Pravidelná referenční mřížka (stejná) + pts_reg = np.column_stack([xx.ravel(), yy.ravel()]) + field_ref = grf.generate_grf(pts_reg, corr2d, weights=white2d).reshape(N2, N2) + + # 2. Generování z řídkých a hustých bodů (stejné) + f_sparse = grf.generate_grf(pts_sparse, corr2d, weights=white2d) + field_sparse_interp = griddata(pts_sparse, f_sparse, (xx, yy), method='linear') + + f_dense = grf.generate_grf(pts_dense, corr2d, weights=white2d) + field_dense_interp = griddata(pts_dense, f_dense, (xx, yy), method='linear') + + + # --- VIZUALIZACE (4 subploty, tečky všude, kam patří) --- + fig, axes = plt.subplots(1, 4, figsize=(22, 5)) # Širší plátno + vmin, vmax = field_ref.min(), field_ref.max() + + # Nastavení pro scatter-bar + sm = plt.cm.ScalarMappable(cmap='viridis', norm=plt.Normalize(vmin=vmin, vmax=vmax)) + + # 1. Referenční pole + im0 = axes[0].imshow(field_ref, extent=[0,L,0,L], origin='lower', cmap='viridis', vmin=vmin, vmax=vmax) + axes[0].set_title(f"Referenční pravidelná ({N2}×{N2})") + plt.colorbar(im0, ax=axes[0]) + + # 2. NOVÝ: Samotná nepravidelná mřížka (Barevné tečky) + sc = axes[1].scatter(pts_sparse[:, 0], pts_sparse[:, 1], c=f_sparse, cmap='viridis', s=18, vmin=vmin, vmax=vmax) + axes[1].set_title(f"Nepravidelná mřížka\n({n_sparse} vzorků)") + axes[1].set_xlim(0, L); axes[1].set_ylim(0, L) + axes[1].set_aspect('equal') + axes[1].grid(True, alpha=0.2) # Jemná mřížka pro orientaci + plt.colorbar(sc, ax=axes[1]) + + # 3. PŮVODNÍ (s tečkami): Interpolace z řídké mřížky + im2 = axes[2].imshow(field_sparse_interp, extent=[0,L,0,L], origin='lower', cmap='viridis', vmin=vmin, vmax=vmax) + # Tady jsou ty bílé tečky navrch + axes[2].plot(pts_sparse[:, 0], pts_sparse[:, 1], '.', color='white', ms=2.5, alpha=0.5, label='vzorky') + axes[2].set_title(f"NUFFT řídká ({n_sparse} bodů) + interp") + plt.colorbar(im2, ax=axes[2]) + + # 4. PŮVODNÍ (s tečkami): Interpolace z husté mřížky + im3 = axes[3].imshow(field_dense_interp, extent=[0,L,0,L], origin='lower', cmap='viridis', vmin=vmin, vmax=vmax) + # Tady jsou ty bílé tečky navrch (menší ms= a alpha=) + axes[3].plot(pts_dense[:, 0], pts_dense[:, 1], '.', color='white', ms=1.8, alpha=0.4) + axes[3].set_title(f"NUFFT hustá ({n_dense} bodů) + interp") + plt.colorbar(im3, ax=axes[3]) + + plt.suptitle(f"Bod 4: GRF 2D – {label} φ={phi}") + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/4/main_gstools.py b/4/main_gstools.py new file mode 100644 index 0000000..8e0a02d --- /dev/null +++ b/4/main_gstools.py @@ -0,0 +1,134 @@ +""" +main_gstools.py – Bod 5: Porovnání naší NUFFT metody s GSTools + +Srovnání přes empirický variogram – obě metody by měly konvergovat +k teoretické křivce gamma(h) = sigma^2 * (1 - exp(-h^2 / 2*phi^2)) +""" + +import importlib.util +from pathlib import Path +import numpy as np +import matplotlib.pyplot as plt +import gstools as gs +from scipy.interpolate import griddata + +spec = importlib.util.spec_from_file_location("grf", Path(__file__).parent / "grf.py") +grf = importlib.util.module_from_spec(spec) +spec.loader.exec_module(grf) + +L, N, phi, seed = 100.0, 1024, 5.0, 42 + +# ============================================================================= +# 1D – jedna realizace +# ============================================================================= + +x = np.linspace(0, L, N) +corr = grf.GaussianCorrelation(L, N, phi, sigma=1.0, dim=1) +model = gs.Gaussian(dim=1, var=1.0, len_scale=phi) + +white = grf.make_white_noise(N, dim=1, seed=seed) +field_nufft = grf.generate_grf(x, corr, weights=white) +field_gs = np.asarray(gs.SRF(model, seed=seed)(x)).ravel() + +# ============================================================================= +# VARIOGRAM – průměr přes N_real realizací +# ============================================================================= + +N_real = 100 +n_bins = 40 +bin_edges = np.linspace(0, L / 2, n_bins + 1) +bin_c = 0.5 * (bin_edges[:-1] + bin_edges[1:]) + +g_nufft = np.zeros(n_bins) +g_gs = np.zeros(n_bins) + +for i in range(N_real): + w = grf.make_white_noise(N, dim=1, seed=i) + f_n = grf.generate_grf(x, corr, weights=w) + _, g = grf.empirical_variogram(x, f_n, n_bins=n_bins) + if len(g) == n_bins: + g_nufft += g + + f_g = np.asarray(gs.SRF(model, seed=i)(x)).ravel() + _, g = grf.empirical_variogram(x, f_g, n_bins=n_bins) + if len(g) == n_bins: + g_gs += g + +g_nufft /= N_real +g_gs /= N_real + +h_th = np.linspace(0, L / 2, 300) +gamma_th = 1 - np.exp(-0.5 * (h_th / phi)**2) + +# ============================================================================= +# 2D +# ============================================================================= + +N2 = 128 +g2 = np.linspace(0, L, N2) +xx, yy = np.meshgrid(g2, g2) +rng_np = np.random.default_rng(seed) +pts_irr = np.column_stack([rng_np.uniform(0, L, N2**2), + rng_np.uniform(0, L, N2**2)]) + +corr2d = grf.GaussianCorrelation(L, N2, phi, sigma=1.0, dim=2) +white2d = grf.make_white_noise(N2, dim=2, seed=seed) +f_irr2d = grf.generate_grf(pts_irr, corr2d, weights=white2d) +field2d_nufft = griddata(pts_irr, f_irr2d, (xx, yy), method='linear') + +model2d = gs.Gaussian(dim=2, var=1.0, len_scale=phi) +field2d_gs = gs.SRF(model2d, seed=seed).structured([g2, g2]) + +# ============================================================================= +# GRAFY +# ============================================================================= + +fig = plt.figure(figsize=(16, 9)) +layout = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.3) + +# 1D realizace +ax0 = fig.add_subplot(layout[0, 0]) +ax0.plot(x, field_gs, lw=1.2, label="GSTools") +ax0.plot(x, field_nufft, lw=1, label="naše NUFFT", alpha=0.8) +ax0.set_title(f"1D realizace (φ={phi})") +ax0.set_xlabel("x [m]") +ax0.legend(fontsize=8); ax0.grid(True, alpha=0.3) + +# variogram průměr +ax1 = fig.add_subplot(layout[0, 1]) +ax1.plot(bin_c, g_nufft, 'o-', ms=3, label=f"naše NUFFT (avg {N_real})") +ax1.plot(bin_c, g_gs, 's-', ms=3, label=f"GSTools (avg {N_real})") +ax1.plot(h_th, gamma_th, 'k--', lw=2, label="teoretický") +ax1.set_title(f"Variogram – průměr přes {N_real} realizací") +ax1.set_xlabel("h [m]"); ax1.set_ylabel("γ(h)") +ax1.legend(fontsize=8); ax1.grid(True, alpha=0.3) + +# gs.vario_estimate +bin_c_gs, vario_gs_builtin = gs.vario_estimate([x], field_gs, bin_edges=bin_edges) +ax2 = fig.add_subplot(layout[0, 2]) +ax2.scatter(bin_c_gs, vario_gs_builtin, s=15, label="gs.vario_estimate") +ax2.plot(h_th, gamma_th, 'k--', lw=2, label="teoretický") +ax2.set_title("GSTools vario_estimate (1 realizace)") +ax2.set_xlabel("h [m]"); ax2.set_ylabel("γ(h)") +ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3) + +# 2D +ax3 = fig.add_subplot(layout[1, 0]) +im3 = ax3.imshow(field2d_nufft, extent=[0,L,0,L], origin='lower', cmap='viridis') +plt.colorbar(im3, ax=ax3) +ax3.set_title(f"2D naše NUFFT (φ={phi})") + +ax4 = fig.add_subplot(layout[1, 1]) +im4 = ax4.imshow(field2d_gs, extent=[0,L,0,L], origin='lower', cmap='viridis') +plt.colorbar(im4, ax=ax4) +ax4.set_title(f"2D GSTools (φ={phi})") + +ax5 = fig.add_subplot(layout[1, 2]) +diff = field2d_nufft - field2d_gs +im5 = ax5.imshow(diff, extent=[0,L,0,L], origin='lower', cmap='RdBu_r') +plt.colorbar(im5, ax=ax5) +ax5.set_title("Rozdíl (různé realizace)") + +plt.suptitle(f"Bod 5: Srovnání s GSTools – Gaussovská korelace φ={phi}", fontsize=13) +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/__pycache__/finufft.cpython-313.pyc b/__pycache__/finufft.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d6513ee046176502313a0c91e9931bfa1368689 GIT binary patch literal 1399 zcmbVL-D}%c6u*)!S(ZOihm|H>!D=X@3UgU9%#txWYDU_vTWGDNeGvpjx{lS!y6Tmb z#d$JeqwQ1oGRR}{FlIlDyk-w&1OEec9B{d1FxZpd%+fz#_bN6`aJGjX&^f<*&hLEa z=;}C=kpY##!=LOIF#!Hzhmj_z>h!!8h009F0wr4BOCi7js*p1JQj7XoE@dU zwdLGfJVG=xri(?|kBJulznKLAfC8<6lbV3#f2ndQYH~(V=F}NSbeIL_`*nFVDbDD8 z`O0|a4A3)rvYa1}(2B~qx5%CNigd3`a84J>*G}+hJp(jRODu8K%smY7+D06l^PAjKIQ$(Ei&*h}gaBiH-$(-SQa2}^PbVi-#|C_PSso6fGEcY8! z_t~oI^f@JiPwh9$_f z=LCdXCL%Um3)P6|B3v^ZB2-qZ291105}|8XsFU!XO;VeyHpXy0bPPK-JriE5q$H|ORTNV;M}$6DPmoDlj%q&o^RBnjU%f&gL?$B?xN?7)FU+>F*i#QM-W za2>i9iThRbZ3`UJEdf$AKh{@#h<&{rT891^GQ%2l17Fw7DngF_q5h?Q52Hu$n?N7F zIel9qeNsbK=)jwp-B$Kd{lBRi#5j(74JKcM>>nUM5P={yxIZQNdGcAZCrx#vsh!-; zt)0hR>FRE&Ik`XGn(nEK9d+^G>cRcrxvpC3Tq|{?QbQO>QSPhkh3ED&yO+&(viaS~ zpD*rR+!dSrzR(hykGt8qMv4+IT^* zC3^MVZ{Wvp@leQ33WeSZxp?%=HVa!H$-MXS-uv;-OPWq6CTLfGe@IUWxeUp|FrQl! zTF%ITJRv~FG>P&>C6rg%yLpSj8Tm~5%T;_9?B%T^{iqF=+!n?Iy0uQoCP|`if8sp@ zxnP{-MwL@>$`p4XPnk39NU#qAlys77c2qHbB7l#j9y96KSLr6`Ezap>gj?5@G42!;Y2X0OYi>EfSA5;9yBuU%%tqylClba!TWJ=pn4XY0GaR(Bh0A|DevA3G;-A5*x8N4@r1nZi%S8^G^&OALR> O%?hQoiDH`0d;bB!5v1|} literal 0 HcmV?d00001 diff --git a/test/1drandom.py b/test/1drandom.py new file mode 100644 index 0000000..e92b19b --- /dev/null +++ b/test/1drandom.py @@ -0,0 +1,27 @@ +import numpy as np +N = 1000 # Počet bodů mřížky +L = 100.0 # Fyzická délka (např. 100 metrů) +phi = 2.0 # Korelační délka (v tvém zadání) + +# Pravidelná mřížka v prostoru +x = np.linspace(0, L, N, endpoint=False) +dx = L / N + +# Úhlové frekvence omega +# fftfreq vrací frekvence v cyklech, proto musíme násobit 2*pi +freqs = np.fft.fftfreq(N, d=dx) * 2 * np.pi + +# S(omega) - Gaussovský recept +S_omega = np.sqrt(2 * np.pi) * phi * np.exp(-(freqs**2 * phi**2) / 2) +amplitude_filter = np.sqrt(S_omega * N / dx) + +# Náhodná komplexní čísla (Standardní Normální rozdělení) +# Generujeme real a imag část zvlášť +white_noise = (np.random.normal(0, 1, N) + 1j * np.random.normal(0, 1, N)) / np.sqrt(2) + +# Aplikujeme náš Gaussovský recept na náhodný šum +F_k = white_noise * amplitude_filter +# Tady použiješ tu svoji funkci, co zajistí zrcadlení (Hermitovskou symetrii) +F_k_final = fn.force_hermitian_symmetry(F_k) # To je ta naše funkce z minula +# Inverzní FFT nám vytvoří to náhodné pole v 1D +random_field = np.fft.ifft(F_k_final).real \ No newline at end of file diff --git a/test/2.1.py b/test/2.1.py new file mode 100644 index 0000000..f28b54a --- /dev/null +++ b/test/2.1.py @@ -0,0 +1,26 @@ +import numpy as np +import matplotlib.pyplot as plt +from scipy.fft import fft, ifft, fftfreq + +from functions import force_hermitian_symmetry +p = True +N = 100 +x, dx = np.linspace(0, 2*np.pi, N, endpoint=False, retstep=True) +f_x = 1 + 2*np.sin(x) + 10*np.sin(5*x) + 3*np.cos(5*x) +F_k = fft(f_x) +print("Před úpravou pro Hermitian symetrii:") +for i in range(len(F_k)): + if abs(F_k[i]) > 1e-10: # Tiskneme pouze nenulové koeficienty + print(f"F_k[{i}] = {F_k[i]}") + +# 1. Vygeneruješ si náhodnou první polovinu (indexy 1 až 49) + + +# 2. Tu druhou polovinu (indexy 51 až 99) vytvoříš jako zrcadlo +# np.flip pole otočí (aby 1 odpovídalo 99) +# np.conj otočí znaménka u 'j' +print("\nPo úpravě pro Hermitian symetrii:") +F_k = force_hermitian_symmetry(F_k) +for i in range(len(F_k)): + if abs(F_k[i]) > 1e-10: # Tiskneme pouze nenulové koeficienty + print(f"F_k[{i}] = {F_k[i]}") \ No newline at end of file diff --git a/test/2.py b/test/2.py new file mode 100644 index 0000000..9d9d00c --- /dev/null +++ b/test/2.py @@ -0,0 +1,58 @@ +import numpy as np +import matplotlib.pyplot as plt + +def generate_1d_grf(N, L, correlation_length): + # 1. Mřížka a frekvence + dx = L / N + x = np.linspace(0, L, N, endpoint=False) + freqs = np.fft.fftfreq(N, d=dx) * 2 * np.pi # Úhlová frekvence omega + + # 2. Spektrální hustota S(omega) pro Gaussovu korelaci + # S(w) = sqrt(2*pi)*l * exp(-w^2 * l^2 / 2) + l = correlation_length + psd = np.sqrt(2 * np.pi) * l * np.exp(-(freqs**2 * l**2) / 2) + + # 3. Generování náhodných komplexních koeficientů + # Standardní normální rozdělení (průměr 0, rozptyl 1) + white_noise = (np.random.normal(0, 1, N) + 1j * np.random.normal(0, 1, N)) / np.sqrt(2) + + # 4. Modulace šumu spektrální hustotou + # Odmocnina z PSD, protože amplituda = sqrt(výkonu) + coeffs_f = white_noise * np.sqrt(psd) + print("white_noise", white_noise) + print("x", x) + print("Frekvence", freqs) + print("PSD", psd) + print("Náhodné koeficienty před úpravou", coeffs_f) + + # 5. TECHNICKÁ ČÁST: Správné komplexní sdružení pro REÁLNÝ výstup + # Aby ifft vrátilo reálná čísla, musí platit f(k) = conj(f(-k)) + # Numpy to má uspořádané: [0, pos_freqs, nyquist, neg_freqs] + + coeffs_f[0] = coeffs_f[0].real * np.sqrt(2) # DC složka musí být reálná + half = N // 2 + for i in range(1, half): + coeffs_f[N - i] = np.conj(coeffs_f[i]) + + if N % 2 == 0: + coeffs_f[half] = coeffs_f[half].real * np.sqrt(2) # Nyquistova frekv. reálná + + # 6. Inverzní FFT + # Použijeme normalizaci 'ortho' nebo musíme násobit sqrt(N) podle definice + field = np.fft.ifft(coeffs_f * np.sqrt(N / dx)).real + + return x, field + +# Spuštění +N = 1000 +L = 100 +phi = 6.0 # korelační délka +x, y = generate_1d_grf(N, L, phi) + +plt.figure(figsize=(10, 4)) +plt.plot(x, y) +plt.title(f"Náhodné pole (Gaussovská korelace, $\ell={phi}$)") +plt.xlabel("x") +plt.ylabel("Hodnota") +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/test/nfftrandom.py b/test/nfftrandom.py new file mode 100644 index 0000000..e427884 --- /dev/null +++ b/test/nfftrandom.py @@ -0,0 +1,77 @@ +import numpy as np +import finufft +import matplotlib.pyplot as plt + +def generate_grf_nufft(N_freq, M_points, phi, L): + """ + N_freq: počet frekvenčních módů (pravidelná mřížka ve frekvenci) + M_points: počet nepravidelných bodů, které chceme vygenerovat + phi: korelační délka (vliv na hladkost) + L: celková délka domény + """ + + # --- 1. PŘÍPRAVA FREKVENCÍ --- + # Indexy frekvencí k (centrované kolem nuly pro NUFFT) + k = np.arange(-N_freq // 2, N_freq // 2) + # Převod indexů na úhlovou frekvenci omega + # (předpokládáme doménu 2*pi pro jednoduchost NUFFT) + omega = k * (2 * np.pi / L) + + # --- 2. SPEKTRÁLNÍ HUSTOTA S(omega) --- + # Gaussovská korelační funkce -> Gaussovské spektrum + S_omega = np.sqrt(2 * np.pi) * phi * np.exp(-(omega**2 * phi**2) / 2) + + # --- 3. GENEROVÁNÍ NÁHODNÝCH KOEFICIENTŮ --- + # Standardní normální rozdělení pro Re a Im část + white_noise = (np.random.normal(0, 1, N_freq) + 1j * np.random.normal(0, 1, N_freq)) / np.sqrt(2) + + # Modulace šumu filtrem (odmocnina ze spektrální hustoty) + f_hat = white_noise * np.sqrt(S_omega) + + # --- 4. TECHNICKÉ ZAJIŠTĚNÍ REÁLNOSTI (Bod 1) --- + # Pro NUFFT s frekvencemi centrovanými kolem 0: + # f_hat(-k) musí být conj(f_hat(k)) + # Najdeme index nuly + zero_idx = N_freq // 2 + f_hat[zero_idx] = f_hat[zero_idx].real # DC složka reálná + + for i in range(1, N_freq // 2): + f_hat[zero_idx - i] = np.conj(f_hat[zero_idx + i]) + + # --- 5. NEPRAVIDELNÁ MŘÍŽKA (Bod 3) --- + # Vygenerujeme náhodné pozice v rozsahu [0, L] + # finufft nufft1d2 standardně očekává souřadnice v [-pi, pi] + x_irregular = np.random.uniform(-np.pi, np.pi, M_points) + + # Výpočet NUFFT typu 2 (z pravidelných frekvencí do nepravidelných bodů) + # nufft1d2(souřadnice, koeficienty) + field = finufft.nufft1d2(x_irregular, f_hat.astype(np.complex128)) + + # Vrátíme reálnou část (imaginární je díky symetrii prakticky nulová) + # Musíme přeškálovat x zpět na naši délku L, pokud chceme + x_final = (x_irregular + np.pi) * (L / (2 * np.pi)) + + return x_final, field.real + +# --- TESTOVACÍ SPUŠTĚNÍ --- +N = 256 # Počet frekvencí (přesnost spektra) +M = 500 # Počet bodů v prostoru (nepravidelných) +L = 100.0 # Délka území +phi = 3 # Zkus měnit (0.5 pro zubaté, 5.0 pro hladké) + +x_coords, y_values = generate_grf_nufft(N, M, phi, L) + +# Seřazení pro hezký graf (protože body jsou náhodně rozházené) +sort_idx = np.argsort(x_coords) +x_plot = x_coords[sort_idx] +y_plot = y_values[sort_idx] + +plt.figure(figsize=(12, 5)) +plt.plot(x_plot, y_plot, '-', alpha=0.5, color='gray') +plt.scatter(x_coords, y_values, s=10, c=y_values, cmap='viridis') +plt.title(f"1D Náhodné pole (Gaussovská korelace $\\phi={phi}$)\nNepravidelná mřížka (NUFFT)") +plt.xlabel("Pozice [m]") +plt.ylabel("Hodnota (např. výška)") +plt.colorbar(label="Amplituda") +plt.grid(True) +plt.show() \ No newline at end of file From b633a07886fdcbb4fbcf176faff2812c124f3202 Mon Sep 17 00:00:00 2001 From: Rudolf Mahdal Date: Tue, 24 Mar 2026 09:32:08 +0100 Subject: [PATCH 2/4] Reorganized the repository, removed unnecessary files --- 2/__pycache__/fft1d.cpython-313.pyc | Bin 5311 -> 0 bytes 3/Untitled-sada2.py | 309 -------------------------- 3/__pycache__/3.2d.cpython-313.pyc | Bin 11890 -> 0 bytes 4/4.py | 140 ------------ 4/__pycache__/grf.cpython-313.pyc | Bin 10904 -> 0 bytes {1 => Old/1_fft_basics}/1.py | 0 {1 => Old/1_fft_basics}/1ext.py | 0 {2 => Old/2_grf_1d}/fft1d.py | 0 {2 => Old/2_grf_1d}/main2.py | 0 {3 => Old/3_grf_2d}/3.2d.py | 0 {3 => Old/3_grf_2d}/3.py | 0 {3 => Old/3_grf_2d}/Figure_1.png | Bin {3 => Old/3_grf_2d}/gstest.py | 0 __pycache__/finufft.cpython-313.pyc | Bin 1399 -> 0 bytes __pycache__/functions.cpython-313.pyc | Bin 607 -> 0 bytes 4/grf.py => grf.py | 0 gstoolsnufft.py | 71 ++++++ 4/main_fields.py => main_fields.py | 0 4/main_gstools.py => main_gstools.py | 44 ++-- test/1drandom.py | 27 --- test/2.1.py | 26 --- test/2.py | 58 ----- test/nfftrandom.py | 77 ------- 23 files changed, 101 insertions(+), 651 deletions(-) delete mode 100644 2/__pycache__/fft1d.cpython-313.pyc delete mode 100644 3/Untitled-sada2.py delete mode 100644 3/__pycache__/3.2d.cpython-313.pyc delete mode 100644 4/4.py delete mode 100644 4/__pycache__/grf.cpython-313.pyc rename {1 => Old/1_fft_basics}/1.py (100%) rename {1 => Old/1_fft_basics}/1ext.py (100%) rename {2 => Old/2_grf_1d}/fft1d.py (100%) rename {2 => Old/2_grf_1d}/main2.py (100%) rename {3 => Old/3_grf_2d}/3.2d.py (100%) rename {3 => Old/3_grf_2d}/3.py (100%) rename {3 => Old/3_grf_2d}/Figure_1.png (100%) rename {3 => Old/3_grf_2d}/gstest.py (100%) delete mode 100644 __pycache__/finufft.cpython-313.pyc delete mode 100644 __pycache__/functions.cpython-313.pyc rename 4/grf.py => grf.py (100%) create mode 100644 gstoolsnufft.py rename 4/main_fields.py => main_fields.py (100%) rename 4/main_gstools.py => main_gstools.py (74%) delete mode 100644 test/1drandom.py delete mode 100644 test/2.1.py delete mode 100644 test/2.py delete mode 100644 test/nfftrandom.py diff --git a/2/__pycache__/fft1d.cpython-313.pyc b/2/__pycache__/fft1d.cpython-313.pyc deleted file mode 100644 index edae1cdb7c6a615f1674a937f9b692afff752fe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5311 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 diff --git a/3/Untitled-sada2.py b/3/Untitled-sada2.py deleted file mode 100644 index 35c1b8b..0000000 --- a/3/Untitled-sada2.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -Živá vizualizace 2D GRF – spektrální metoda (FFT) -Každou sekundu nový white noise; přepínání Gaussovská ↔ Exponenciální. - -Postaveno na původním kódu (GaussianCorrelation, ExponentialCorrelation, -make_hermitian_nd, make_white_noise) — generování přes numpy.fft místo finufft, -výsledek je identický pro pravidelnou mřížku. -""" - -import numpy as np -import matplotlib -import matplotlib.pyplot as plt -import matplotlib.animation as animation -from matplotlib.widgets import Button, Slider, RadioButtons -import matplotlib.gridspec as gridspec - -matplotlib.rcParams.update({ - "figure.facecolor": "#07080f", - "axes.facecolor": "#07080f", - "text.color": "#9ac8e8", - "axes.edgecolor": "#1a3a5a", - "axes.labelcolor": "#4a7a9a", - "xtick.color": "#2a5a7a", - "ytick.color": "#2a5a7a", - "font.family": "monospace", - "font.size": 9, -}) - - -# ============================================================================= -# CORRELATION TŘÍDY (beze změny logiky z původního souboru) -# ============================================================================= - -class GaussianCorrelation: - """C(r) = σ² exp(−|r|²/2φ²) → S_2D(ω) = σ²(√2π φ)² exp(−|ω|²φ²/2)""" - def __init__(self, L, N_freq, phi, sigma=1.0, dim=2): - self.phi, self.sigma, self.dim = phi, sigma, dim - self.N_freq, self.L = N_freq, L - k = np.arange(-N_freq // 2, N_freq // 2) - self.f_points = k * (2 * np.pi / L) - - def spectral_density(self, omega_mag): - return (self.sigma ** 2 - * (np.sqrt(2 * np.pi) * self.phi) ** self.dim - * np.exp(-0.5 * (omega_mag * self.phi) ** 2)) - - -class ExponentialCorrelation: - """C(r) = σ² exp(−|r|/φ) → S_2D(ω) = σ² 2π φ² / (1+|ω|²φ²)^(3/2)""" - def __init__(self, L, N_freq, phi, sigma=1.0, dim=2): - self.phi, self.sigma, self.dim = phi, sigma, dim - self.N_freq, self.L = N_freq, L - k = np.arange(-N_freq // 2, N_freq // 2) - self.f_points = k * (2 * np.pi / L) - - def spectral_density(self, omega_mag): - if self.dim == 2: - return (self.sigma ** 2 * 2 * np.pi * self.phi ** 2 - / (1 + (omega_mag * self.phi) ** 2) ** 1.5) - raise NotImplementedError("dim != 2") - - -# ============================================================================= -# HERMITOVSKÁ SYMETRIE (původní make_hermitian_nd – zrychlená pro 2D) -# ============================================================================= - -def make_hermitian_2d(f_hat): - """ - Hermitovská symetrie v centrovaném pořadí pro 2D pole. - Použito np.roll místo explicitní iterace → ~100× rychlejší. - """ - N = f_hat.shape[0] - f = f_hat.copy() - # f[-kx, -ky] = conj(f[kx, ky]) - # v centrovaném pořadí: index zi odpovídá k=0 - zi = N // 2 - # flip přes obě osy a otočit o jeden prvek, aby DC zůstalo na místě - conj_flip = np.conj(np.roll(np.roll(f[::-1, ::-1], 1, axis=0), 1, axis=1)) - # DC musí být reálný - mask = np.ones((N, N), dtype=bool) - mask[zi, zi] = False - # průměrujeme: f_herm = (f + conj_flip) / sqrt(2) zachovává rozptyl - f = (f + conj_flip) / np.sqrt(2) - f[zi, zi] = f[zi, zi].real - return f - - -def make_white_noise(N_freq, dim=2, seed=None): - """Komplexní bílý šum tvaru (N_freq,)*dim.""" - rng = np.random.default_rng(seed) - shape = (N_freq,) * dim - size = N_freq ** dim - flat = (rng.standard_normal(size) + 1j * rng.standard_normal(size)) / np.sqrt(2) - return flat.reshape(shape) - - -# ============================================================================= -# GENEROVÁNÍ GRF (pravidelná 2D mřížka – numpy.fft místo finufft) -# ============================================================================= - -def generate_grf_fft_2d(N, corr, weights=None, seed=None): - """ - Generuje 2D GRF na pravidelné mřížce N×N. - - Ekvivalentní generate_grf_nufft pro pts2d na pravidelné mřížce, - ale bez závislosti na finufft. - """ - if weights is None: - weights = make_white_noise(N, dim=2, seed=seed) - - # |ω| pro každý frekvenční bod - gx, gy = np.meshgrid(corr.f_points, corr.f_points, indexing='ij') - omega_mag = np.sqrt(gx ** 2 + gy ** 2) - - S = corr.spectral_density(omega_mag) # (N, N) - f_hat_centered = make_hermitian_2d(weights * np.sqrt(S)) - - # centrované → standardní FFT pořadí - f_hat = np.fft.ifftshift(f_hat_centered) - - # IFFT → reálná část - field = np.fft.ifft2(f_hat).real # (N, N) - return field - - -# ============================================================================= -# GUI -# ============================================================================= - -L = 100.0 -N = 128 -PHI = 8.0 - -CMAP = "turbo" - -CORR_CLASSES = { - "Gaussovská": GaussianCorrelation, - "Exponenciální": ExponentialCorrelation, -} - - -class LiveGRF: - def __init__(self): - self.corr_name = "Gaussovská" - self.phi = PHI - self.paused = False - self.frame = 0 - self._build_corr() - - # ── layout ────────────────────────────────────────────────────────── - self.fig = plt.figure(figsize=(9, 8), facecolor="#07080f") - self.fig.canvas.manager.set_window_title("2D GRF – živá vizualizace") - - gs = gridspec.GridSpec( - 4, 3, - figure=self.fig, - left=0.06, right=0.96, - top=0.93, bottom=0.04, - hspace=0.55, wspace=0.35, - height_ratios=[14, 1.2, 1.2, 1.2], - ) - - # hlavní obraz - self.ax_img = self.fig.add_subplot(gs[0, :]) - self.ax_img.set_aspect("equal") - self.ax_img.tick_params(labelsize=7) - self.ax_img.set_xlabel("x [m]", fontsize=8) - self.ax_img.set_ylabel("y [m]", fontsize=8) - - # slider φ - ax_sl = self.fig.add_subplot(gs[1, :]) - ax_sl.set_facecolor("#07080f") - self.slider = Slider( - ax_sl, "φ [korelační délka]", - valmin=1, valmax=30, valinit=self.phi, valstep=0.5, - color="#1a4a7a", track_color="#0d1a2a", - ) - self.slider.label.set_color("#4a9adf") - self.slider.valtext.set_color("#7ecfff") - self.slider.on_changed(self._on_phi) - - # radio tlačítka - ax_radio = self.fig.add_subplot(gs[2, 0]) - ax_radio.set_facecolor("#07080f") - self.radio = RadioButtons( - ax_radio, - labels=list(CORR_CLASSES.keys()), - active=0, - activecolor="#4a9adf", - ) - for lbl in self.radio.labels: - lbl.set_fontsize(9) - lbl.set_color("#9ac8e8") - self.radio.on_clicked(self._on_corr) - - # pause / step tlačítka - ax_btn_pause = self.fig.add_subplot(gs[2, 1]) - ax_btn_step = self.fig.add_subplot(gs[2, 2]) - for ax in (ax_btn_pause, ax_btn_step): - ax.set_facecolor("#07080f") - - self.btn_pause = Button(ax_btn_pause, "⏸ Pauza", - color="#0d1520", hovercolor="#162030") - self.btn_step = Button(ax_btn_step, "↻ Generovat", - color="#0d1520", hovercolor="#162030") - for btn in (self.btn_pause, self.btn_step): - btn.label.set_color("#7ecfff") - btn.label.set_fontsize(9) - self.btn_pause.on_clicked(self._on_pause) - self.btn_step.on_clicked(self._on_step) - - # info text ve spodku - self.ax_info = self.fig.add_subplot(gs[3, :]) - self.ax_info.axis("off") - self.info_txt = self.ax_info.text( - 0.5, 0.5, "", ha="center", va="center", - transform=self.ax_info.transAxes, - fontsize=8, color="#2a6a9a", - ) - - # colorbar - self.cbar = None - - # první snímek - field = self._gen() - self.im = self.ax_img.imshow( - field, extent=[0, L, 0, L], - origin="lower", cmap=CMAP, - interpolation="bilinear", - ) - self.cbar = self.fig.colorbar(self.im, ax=self.ax_img, - fraction=0.03, pad=0.02) - self.cbar.ax.tick_params(colors="#4a7a9a", labelsize=7) - self._set_title() - self._update_info(field) - - # ── helpers ───────────────────────────────────────────────────────────── - - def _build_corr(self): - cls = CORR_CLASSES[self.corr_name] - self.corr = cls(L, N, self.phi, dim=2) - - def _gen(self): - self.frame += 1 - return generate_grf_fft_2d(N, self.corr) - - def _set_title(self): - self.ax_img.set_title( - f"2D GRF · {self.corr_name} korelace · " - f"φ = {self.phi:.1f} m · frame #{self.frame:04d}", - fontsize=10, color="#7ecfff", pad=6, - ) - - def _update_info(self, field): - mn, mx, sd = field.min(), field.max(), field.std() - self.info_txt.set_text( - f"min = {mn:+.4f} max = {mx:+.4f} σ = {sd:.4f} " - f"N = {N}×{N} L = {L} m" - ) - - # ── callbacks ──────────────────────────────────────────────────────────── - - def _on_phi(self, val): - self.phi = val - self._build_corr() - self._refresh() - - def _on_corr(self, label): - self.corr_name = label - self._build_corr() - self._refresh() - - def _on_pause(self, _): - self.paused = not self.paused - self.btn_pause.label.set_text("▶ Spustit" if self.paused else "⏸ Pauza") - self.fig.canvas.draw_idle() - - def _on_step(self, _): - self._refresh() - - def _refresh(self): - field = self._gen() - self.im.set_data(field) - self.im.set_clim(field.min(), field.max()) - self._set_title() - self._update_info(field) - self.fig.canvas.draw_idle() - - # ── animace ────────────────────────────────────────────────────────────── - - def animate(self, _i): - if not self.paused: - self._refresh() - - def run(self): - self._anim = animation.FuncAnimation( - self.fig, - self.animate, - interval=1000, # každou sekundu - cache_frame_data=False, - ) - plt.show() - - -# ============================================================================= - -if __name__ == "__main__": - app = LiveGRF() - app.run() \ No newline at end of file diff --git a/3/__pycache__/3.2d.cpython-313.pyc b/3/__pycache__/3.2d.cpython-313.pyc deleted file mode 100644 index 8ec10e902182679f07f807d1079759588f2929d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11890 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 N!C9KpY?V#-{{vGHeVhOQ diff --git a/4/4.py b/4/4.py deleted file mode 100644 index 52deecd..0000000 --- a/4/4.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Porovnání našeho FFT/NUFFT přístupu s GSTools. - -GSTools používá RandMeth (Randomization Method): - u(x) = sqrt(sigma^2 / N) * sum_j [ Z1_j * cos() + Z2_j * sin() ] - kde k_j jsou náhodné vzorky ze spektrální hustoty S(omega) - a Z1, Z2 jsou nezávislé N(0,1) vzorky - -Náš přístup (FFT/NUFFT): - u(x) = IFFT[ white * sqrt(S(k)) ] - kde k jsou PRAVIDELNÉ frekvence a white je komplexní N(0,1) - --> algoritmy jsou různé, srovnáváme proto statisticky přes variogram -""" - -import numpy as np -import matplotlib.pyplot as plt -import gstools as gs -import importlib.util -from pathlib import Path - -module_path = Path(__file__).resolve().parents[1] / "3" / "3.2d.py" -spec = importlib.util.spec_from_file_location("grf_2d_module", module_path) -grf_2d_module = importlib.util.module_from_spec(spec) -spec.loader.exec_module(grf_2d_module) - -GaussianCorrelation = grf_2d_module.GaussianCorrelation -make_white_noise = grf_2d_module.make_white_noise -generate_grf_nufft = grf_2d_module.generate_grf_nufft - - -L, N, phi, sigma = 100.0, 512, 5.0, 1.0 -seed = 42 - -# ============================================================================= -# 1. NAŠE METODA – 1D pole -# ============================================================================= -x = np.linspace(0, L, N) -corr = GaussianCorrelation(L, N, phi, sigma=sigma, dim=1) -white = make_white_noise(N, dim=1, seed=seed) -field_ours = generate_grf_nufft(x, corr, weights=white) - -# ============================================================================= -# 2. GSTOOLS – 1D pole -# ============================================================================= -model = gs.Gaussian(dim=1, var=sigma**2, len_scale=phi) -srf = gs.SRF(model, seed=seed) -field_gs = np.asarray(srf(x)).ravel() - -# --- Var 1: reimplementace RandMeth pomocí interních atributů GSTools --- -# po zavolání srf(x) jsou dostupné: -# srf.generator._z_1, _z_2 : N(0,1) vzorky (shape: mode_no,) -# srf.generator._cov_sample : náhodné frekvence k_j (shape: dim, mode_no) -z1 = srf.generator._z_1 # N(0,1) -z2 = srf.generator._z_2 # N(0,1) -k_j = srf.generator._cov_sample # shape (1, mode_no) pro 1D -mode_no = len(z1) - -# u(x) = sqrt(sigma^2 / N) * sum_j [ Z1_j*cos(k_j*x) + Z2_j*sin(k_j*x) ] -# k_j * x -> (mode_no, N) matice skalárních součinů -kx = k_j[0, :, np.newaxis] * x[np.newaxis, :] # (mode_no, N) -field_randmeth = np.sqrt(sigma**2 / mode_no) * np.sum( - z1[:, np.newaxis] * np.cos(kx) + z2[:, np.newaxis] * np.sin(kx), axis=0 -) - -# ============================================================================= -# 3. EMPIRICKÝ VARIOGRAM gamma(h) = 0.5 * E[(f(x) - f(x+h))^2] -# ============================================================================= - -def empirical_variogram(x, field, n_bins=30): - """Odhadne variogram z jedné realizace pomocí všech párů bodů.""" - h_vals, gamma_vals = [], [] - N = len(x) - # bereme podvzorek párů aby to nebylo O(N^2) příliš pomalé - rng = np.random.default_rng(0) - idx = rng.choice(N, size=min(N, 300), replace=False) - xi, fi = x[idx], field[idx] - - pairs_h, pairs_sq = [], [] - for i in range(len(xi)): - for j in range(i + 1, len(xi)): - pairs_h.append(abs(xi[i] - xi[j])) - pairs_sq.append((fi[i] - fi[j])**2) - - pairs_h = np.array(pairs_h) - pairs_sq = np.array(pairs_sq) - - bins = np.linspace(0, pairs_h.max(), n_bins + 1) - for k in range(n_bins): - mask = (pairs_h >= bins[k]) & (pairs_h < bins[k+1]) - if mask.sum() > 5: - h_vals.append(0.5 * (bins[k] + bins[k+1])) - gamma_vals.append(0.5 * pairs_sq[mask].mean()) - - return np.array(h_vals), np.array(gamma_vals) - -# teoretický variogram: gamma(h) = sigma^2 * (1 - C(h)/sigma^2) -# Gaussovský: C(h) = sigma^2 * exp(-h^2 / 2*phi^2) -h_theory = np.linspace(0, L/2, 200) -gamma_theory = sigma**2 * (1 - np.exp(-0.5 * (h_theory / phi)**2)) - -h_ours, g_ours = empirical_variogram(x, field_ours) -h_gs, g_gs = empirical_variogram(x, field_gs) -h_rm, g_rm = empirical_variogram(x, field_randmeth) - -# ============================================================================= -# GRAFY -# ============================================================================= - -fig, axes = plt.subplots(1, 3, figsize=(16, 4)) - -# pole: GSTools vs naše reimplementace RandMeth (měly by být identické) -axes[0].plot(x, field_gs, lw=1.5, label="GSTools") -axes[0].plot(x, field_randmeth, lw=1, label="naše RandMeth", linestyle='--', alpha=0.8) -axes[0].plot(x, field_ours, lw=1, label="naše NUFFT", alpha=0.6) -axes[0].set_title(f"1D realizace (φ={phi})") -axes[0].legend(fontsize=8); axes[0].grid(True, alpha=0.3) - -# variogram – naše vs GSTools -axes[1].plot(h_ours, g_ours, 'o-', ms=4, label="empirický – naše NUFFT") -axes[1].plot(h_gs, g_gs, 's-', ms=4, label="empirický – GSTools") -axes[1].plot(h_rm, g_rm, '^-', ms=4, label="empirický – naše RandMeth") -axes[1].plot(h_theory, gamma_theory, 'k--', lw=2, label="teoretický") -axes[1].set_title("Empirický variogram") -axes[1].set_xlabel("h [m]"); axes[1].set_ylabel("γ(h)") -axes[1].legend(); axes[1].grid(True, alpha=0.3) - -# variogram pouze GSTools (gs má vestavěný) -bin_edges = np.linspace(0, L/2, 31) -bin_centers, vario_gs = gs.vario_estimate([x], field_gs, bin_edges=bin_edges) -axes[2].scatter(bin_centers, vario_gs, - s=20, label="gs.vario_estimate") -axes[2].plot(h_theory, gamma_theory, 'k--', lw=2, label="teoretický") -axes[2].set_title("GSTools vario_estimate") -axes[2].set_xlabel("h [m]"); axes[2].set_ylabel("γ(h)") -axes[2].legend(); axes[2].grid(True, alpha=0.3) - -plt.suptitle(f"Srovnání naše NUFFT vs GSTools (Gaussovská korelace, φ={phi})") -plt.tight_layout() -plt.show() \ No newline at end of file diff --git a/4/__pycache__/grf.cpython-313.pyc b/4/__pycache__/grf.cpython-313.pyc deleted file mode 100644 index f9d6bc603b407a12fce63ecc10f37156fbaa90d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10904 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 O%?hQoiDH`0d;bB!5v1|} diff --git a/4/grf.py b/grf.py similarity index 100% rename from 4/grf.py rename to grf.py diff --git a/gstoolsnufft.py b/gstoolsnufft.py new file mode 100644 index 0000000..d9ebf8d --- /dev/null +++ b/gstoolsnufft.py @@ -0,0 +1,71 @@ +""" +gstools_nufft3.py – Rekonstrukce GSTools pole pomocí NUFFT typ 3 + +GSTools (RandMeth) počítá: + f(x) = sqrt(1/N) * sum_k [ z1_k*cos(w_k*x) + z2_k*sin(w_k*x) ] + = Re[ sum_k c_k * exp(i*w_k*x) ] kde c_k = z1_k - i*z2_k + +finufft.nufft1d3: neuniformní frekvence s_k -> hodnoty na libovolných bodech x_j +""" + +import numpy as np +import matplotlib.pyplot as plt +import gstools as gs +import finufft + +L, phi, seed = 100.0, 5.0, 42 +N = 512 +x = np.linspace(0, L, N) + +# --- 1. Vyhodnotíme GSTools normálně --- +model = gs.Gaussian(dim=1, var=1.0, len_scale=phi) +srf = gs.SRF(model, seed=seed) +field_gs = np.asarray(srf(x)).ravel() + +# --- 2. Vytáhneme interní stav generátoru --- +gen = srf.generator +print("Atributy generátoru:", [a for a in dir(gen) if not a.startswith('__')]) + +z1 = gen._z_1 # shape (N_modes,) +z2 = gen._z_2 # shape (N_modes,) +modes = gen._cov_sample # shape (1, N_modes) pro 1D + +omega = modes[0] # frekvence w_k (1D) + +print(f"Počet módů: {len(omega)}") +print(f"Rozsah frekvencí: [{omega.min():.4f}, {omega.max():.4f}]") + +# --- 3. Rekonstrukce přes NUFFT typ 3 --- +# f(x_j) = Re[ sum_k c_k * exp(i * w_k * x_j) ] +# c_k = (z1_k - i*z2_k) / sqrt(N_modes) +N_modes = len(omega) +c = (z1 - 1j * z2) / np.sqrt(N_modes) + +# nufft1d3: vstupy jsou neuniformní frekvence a neuniformní body +# pozor: finufft očekává x v libovolných reálných hodnotách (ne nutně [-pi,pi]) +field_nufft3 = finufft.nufft1d3( + omega.astype(np.float64), # neuniformní frekvence s_k + c.astype(np.complex128), # komplexní váhy c_k + x.astype(np.float64), # body kde chceme hodnoty + eps=1e-9 +).real + +# --- 4. Porovnání --- +max_diff = np.max(np.abs(field_gs - field_nufft3)) +print(f"\nMax |GSTools - NUFFT3|: {max_diff:.2e}") + +fig, axes = plt.subplots(2, 1, figsize=(12, 7)) + +axes[0].plot(x, field_gs, lw=1.5, label="GSTools přímý výpočet") +axes[0].plot(x, field_nufft3, lw=1, label="NUFFT typ 3 rekonstrukce", alpha=0.8, ls='--') +axes[0].set_title(f"1D pole – GSTools vs NUFFT3 rekonstrukce (φ={phi})") +axes[0].set_xlabel("x [m]") +axes[0].legend(); axes[0].grid(True, alpha=0.3) + +axes[1].plot(x, field_gs - field_nufft3, lw=1, color='red') +axes[1].set_title(f"Rozdíl: max = {max_diff:.2e}") +axes[1].set_xlabel("x [m]"); axes[1].set_ylabel("GSTools − NUFFT3") +axes[1].grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/4/main_fields.py b/main_fields.py similarity index 100% rename from 4/main_fields.py rename to main_fields.py diff --git a/4/main_gstools.py b/main_gstools.py similarity index 74% rename from 4/main_gstools.py rename to main_gstools.py index 8e0a02d..dea1671 100644 --- a/4/main_gstools.py +++ b/main_gstools.py @@ -39,23 +39,28 @@ bin_edges = np.linspace(0, L / 2, n_bins + 1) bin_c = 0.5 * (bin_edges[:-1] + bin_edges[1:]) -g_nufft = np.zeros(n_bins) -g_gs = np.zeros(n_bins) +g_nufft = np.zeros(n_bins) +g_gs = np.zeros(n_bins) +cnt_nufft = np.zeros(n_bins, dtype=int) # počet realizací které přispěly +cnt_gs = np.zeros(n_bins, dtype=int) for i in range(N_real): w = grf.make_white_noise(N, dim=1, seed=i) f_n = grf.generate_grf(x, corr, weights=w) _, g = grf.empirical_variogram(x, f_n, n_bins=n_bins) if len(g) == n_bins: - g_nufft += g + g_nufft += g + cnt_nufft += 1 f_g = np.asarray(gs.SRF(model, seed=i)(x)).ravel() _, g = grf.empirical_variogram(x, f_g, n_bins=n_bins) if len(g) == n_bins: - g_gs += g + g_gs += g + cnt_gs += 1 -g_nufft /= N_real -g_gs /= N_real +# dělíme skutečným počtem přispívajících realizací (ne vždy N_real) +g_nufft = np.divide(g_nufft, cnt_nufft, where=cnt_nufft > 0) +g_gs = np.divide(g_gs, cnt_gs, where=cnt_gs > 0) h_th = np.linspace(0, L / 2, 300) gamma_th = 1 - np.exp(-0.5 * (h_th / phi)**2) @@ -71,9 +76,9 @@ pts_irr = np.column_stack([rng_np.uniform(0, L, N2**2), rng_np.uniform(0, L, N2**2)]) -corr2d = grf.GaussianCorrelation(L, N2, phi, sigma=1.0, dim=2) -white2d = grf.make_white_noise(N2, dim=2, seed=seed) -f_irr2d = grf.generate_grf(pts_irr, corr2d, weights=white2d) +corr2d = grf.GaussianCorrelation(L, N2, phi, sigma=1.0, dim=2) +white2d = grf.make_white_noise(N2, dim=2, seed=seed) +f_irr2d = grf.generate_grf(pts_irr, corr2d, weights=white2d) field2d_nufft = griddata(pts_irr, f_irr2d, (xx, yy), method='linear') model2d = gs.Gaussian(dim=2, var=1.0, len_scale=phi) @@ -112,22 +117,33 @@ ax2.set_xlabel("h [m]"); ax2.set_ylabel("γ(h)") ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3) -# 2D +# 2D NUFFT ax3 = fig.add_subplot(layout[1, 0]) im3 = ax3.imshow(field2d_nufft, extent=[0,L,0,L], origin='lower', cmap='viridis') plt.colorbar(im3, ax=ax3) ax3.set_title(f"2D naše NUFFT (φ={phi})") +# 2D GSTools ax4 = fig.add_subplot(layout[1, 1]) im4 = ax4.imshow(field2d_gs, extent=[0,L,0,L], origin='lower', cmap='viridis') plt.colorbar(im4, ax=ax4) ax4.set_title(f"2D GSTools (φ={phi})") +# Histogram hodnot obou 2D polí – ukazuje stejné statistické vlastnosti ax5 = fig.add_subplot(layout[1, 2]) -diff = field2d_nufft - field2d_gs -im5 = ax5.imshow(diff, extent=[0,L,0,L], origin='lower', cmap='RdBu_r') -plt.colorbar(im5, ax=ax5) -ax5.set_title("Rozdíl (různé realizace)") +vals_nufft = field2d_nufft[~np.isnan(field2d_nufft)].ravel() +vals_gs = field2d_gs.ravel() +bins_hist = np.linspace(min(vals_nufft.min(), vals_gs.min()), + max(vals_nufft.max(), vals_gs.max()), 40) +ax5.hist(vals_nufft, bins=bins_hist, alpha=0.5, density=True, label="naše NUFFT") +ax5.hist(vals_gs, bins=bins_hist, alpha=0.5, density=True, label="GSTools") +# teoretické N(0,1) +from scipy.stats import norm as sp_norm +xn = np.linspace(bins_hist[0], bins_hist[-1], 200) +ax5.plot(xn, sp_norm.pdf(xn, 0, 1), 'k--', lw=2, label="N(0,1)") +ax5.set_title("Histogram hodnot 2D polí") +ax5.set_xlabel("hodnota pole"); ax5.set_ylabel("hustota") +ax5.legend(fontsize=8); ax5.grid(True, alpha=0.3) plt.suptitle(f"Bod 5: Srovnání s GSTools – Gaussovská korelace φ={phi}", fontsize=13) plt.tight_layout() diff --git a/test/1drandom.py b/test/1drandom.py deleted file mode 100644 index e92b19b..0000000 --- a/test/1drandom.py +++ /dev/null @@ -1,27 +0,0 @@ -import numpy as np -N = 1000 # Počet bodů mřížky -L = 100.0 # Fyzická délka (např. 100 metrů) -phi = 2.0 # Korelační délka (v tvém zadání) - -# Pravidelná mřížka v prostoru -x = np.linspace(0, L, N, endpoint=False) -dx = L / N - -# Úhlové frekvence omega -# fftfreq vrací frekvence v cyklech, proto musíme násobit 2*pi -freqs = np.fft.fftfreq(N, d=dx) * 2 * np.pi - -# S(omega) - Gaussovský recept -S_omega = np.sqrt(2 * np.pi) * phi * np.exp(-(freqs**2 * phi**2) / 2) -amplitude_filter = np.sqrt(S_omega * N / dx) - -# Náhodná komplexní čísla (Standardní Normální rozdělení) -# Generujeme real a imag část zvlášť -white_noise = (np.random.normal(0, 1, N) + 1j * np.random.normal(0, 1, N)) / np.sqrt(2) - -# Aplikujeme náš Gaussovský recept na náhodný šum -F_k = white_noise * amplitude_filter -# Tady použiješ tu svoji funkci, co zajistí zrcadlení (Hermitovskou symetrii) -F_k_final = fn.force_hermitian_symmetry(F_k) # To je ta naše funkce z minula -# Inverzní FFT nám vytvoří to náhodné pole v 1D -random_field = np.fft.ifft(F_k_final).real \ No newline at end of file diff --git a/test/2.1.py b/test/2.1.py deleted file mode 100644 index f28b54a..0000000 --- a/test/2.1.py +++ /dev/null @@ -1,26 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy.fft import fft, ifft, fftfreq - -from functions import force_hermitian_symmetry -p = True -N = 100 -x, dx = np.linspace(0, 2*np.pi, N, endpoint=False, retstep=True) -f_x = 1 + 2*np.sin(x) + 10*np.sin(5*x) + 3*np.cos(5*x) -F_k = fft(f_x) -print("Před úpravou pro Hermitian symetrii:") -for i in range(len(F_k)): - if abs(F_k[i]) > 1e-10: # Tiskneme pouze nenulové koeficienty - print(f"F_k[{i}] = {F_k[i]}") - -# 1. Vygeneruješ si náhodnou první polovinu (indexy 1 až 49) - - -# 2. Tu druhou polovinu (indexy 51 až 99) vytvoříš jako zrcadlo -# np.flip pole otočí (aby 1 odpovídalo 99) -# np.conj otočí znaménka u 'j' -print("\nPo úpravě pro Hermitian symetrii:") -F_k = force_hermitian_symmetry(F_k) -for i in range(len(F_k)): - if abs(F_k[i]) > 1e-10: # Tiskneme pouze nenulové koeficienty - print(f"F_k[{i}] = {F_k[i]}") \ No newline at end of file diff --git a/test/2.py b/test/2.py deleted file mode 100644 index 9d9d00c..0000000 --- a/test/2.py +++ /dev/null @@ -1,58 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -def generate_1d_grf(N, L, correlation_length): - # 1. Mřížka a frekvence - dx = L / N - x = np.linspace(0, L, N, endpoint=False) - freqs = np.fft.fftfreq(N, d=dx) * 2 * np.pi # Úhlová frekvence omega - - # 2. Spektrální hustota S(omega) pro Gaussovu korelaci - # S(w) = sqrt(2*pi)*l * exp(-w^2 * l^2 / 2) - l = correlation_length - psd = np.sqrt(2 * np.pi) * l * np.exp(-(freqs**2 * l**2) / 2) - - # 3. Generování náhodných komplexních koeficientů - # Standardní normální rozdělení (průměr 0, rozptyl 1) - white_noise = (np.random.normal(0, 1, N) + 1j * np.random.normal(0, 1, N)) / np.sqrt(2) - - # 4. Modulace šumu spektrální hustotou - # Odmocnina z PSD, protože amplituda = sqrt(výkonu) - coeffs_f = white_noise * np.sqrt(psd) - print("white_noise", white_noise) - print("x", x) - print("Frekvence", freqs) - print("PSD", psd) - print("Náhodné koeficienty před úpravou", coeffs_f) - - # 5. TECHNICKÁ ČÁST: Správné komplexní sdružení pro REÁLNÝ výstup - # Aby ifft vrátilo reálná čísla, musí platit f(k) = conj(f(-k)) - # Numpy to má uspořádané: [0, pos_freqs, nyquist, neg_freqs] - - coeffs_f[0] = coeffs_f[0].real * np.sqrt(2) # DC složka musí být reálná - half = N // 2 - for i in range(1, half): - coeffs_f[N - i] = np.conj(coeffs_f[i]) - - if N % 2 == 0: - coeffs_f[half] = coeffs_f[half].real * np.sqrt(2) # Nyquistova frekv. reálná - - # 6. Inverzní FFT - # Použijeme normalizaci 'ortho' nebo musíme násobit sqrt(N) podle definice - field = np.fft.ifft(coeffs_f * np.sqrt(N / dx)).real - - return x, field - -# Spuštění -N = 1000 -L = 100 -phi = 6.0 # korelační délka -x, y = generate_1d_grf(N, L, phi) - -plt.figure(figsize=(10, 4)) -plt.plot(x, y) -plt.title(f"Náhodné pole (Gaussovská korelace, $\ell={phi}$)") -plt.xlabel("x") -plt.ylabel("Hodnota") -plt.grid(True) -plt.show() \ No newline at end of file diff --git a/test/nfftrandom.py b/test/nfftrandom.py deleted file mode 100644 index e427884..0000000 --- a/test/nfftrandom.py +++ /dev/null @@ -1,77 +0,0 @@ -import numpy as np -import finufft -import matplotlib.pyplot as plt - -def generate_grf_nufft(N_freq, M_points, phi, L): - """ - N_freq: počet frekvenčních módů (pravidelná mřížka ve frekvenci) - M_points: počet nepravidelných bodů, které chceme vygenerovat - phi: korelační délka (vliv na hladkost) - L: celková délka domény - """ - - # --- 1. PŘÍPRAVA FREKVENCÍ --- - # Indexy frekvencí k (centrované kolem nuly pro NUFFT) - k = np.arange(-N_freq // 2, N_freq // 2) - # Převod indexů na úhlovou frekvenci omega - # (předpokládáme doménu 2*pi pro jednoduchost NUFFT) - omega = k * (2 * np.pi / L) - - # --- 2. SPEKTRÁLNÍ HUSTOTA S(omega) --- - # Gaussovská korelační funkce -> Gaussovské spektrum - S_omega = np.sqrt(2 * np.pi) * phi * np.exp(-(omega**2 * phi**2) / 2) - - # --- 3. GENEROVÁNÍ NÁHODNÝCH KOEFICIENTŮ --- - # Standardní normální rozdělení pro Re a Im část - white_noise = (np.random.normal(0, 1, N_freq) + 1j * np.random.normal(0, 1, N_freq)) / np.sqrt(2) - - # Modulace šumu filtrem (odmocnina ze spektrální hustoty) - f_hat = white_noise * np.sqrt(S_omega) - - # --- 4. TECHNICKÉ ZAJIŠTĚNÍ REÁLNOSTI (Bod 1) --- - # Pro NUFFT s frekvencemi centrovanými kolem 0: - # f_hat(-k) musí být conj(f_hat(k)) - # Najdeme index nuly - zero_idx = N_freq // 2 - f_hat[zero_idx] = f_hat[zero_idx].real # DC složka reálná - - for i in range(1, N_freq // 2): - f_hat[zero_idx - i] = np.conj(f_hat[zero_idx + i]) - - # --- 5. NEPRAVIDELNÁ MŘÍŽKA (Bod 3) --- - # Vygenerujeme náhodné pozice v rozsahu [0, L] - # finufft nufft1d2 standardně očekává souřadnice v [-pi, pi] - x_irregular = np.random.uniform(-np.pi, np.pi, M_points) - - # Výpočet NUFFT typu 2 (z pravidelných frekvencí do nepravidelných bodů) - # nufft1d2(souřadnice, koeficienty) - field = finufft.nufft1d2(x_irregular, f_hat.astype(np.complex128)) - - # Vrátíme reálnou část (imaginární je díky symetrii prakticky nulová) - # Musíme přeškálovat x zpět na naši délku L, pokud chceme - x_final = (x_irregular + np.pi) * (L / (2 * np.pi)) - - return x_final, field.real - -# --- TESTOVACÍ SPUŠTĚNÍ --- -N = 256 # Počet frekvencí (přesnost spektra) -M = 500 # Počet bodů v prostoru (nepravidelných) -L = 100.0 # Délka území -phi = 3 # Zkus měnit (0.5 pro zubaté, 5.0 pro hladké) - -x_coords, y_values = generate_grf_nufft(N, M, phi, L) - -# Seřazení pro hezký graf (protože body jsou náhodně rozházené) -sort_idx = np.argsort(x_coords) -x_plot = x_coords[sort_idx] -y_plot = y_values[sort_idx] - -plt.figure(figsize=(12, 5)) -plt.plot(x_plot, y_plot, '-', alpha=0.5, color='gray') -plt.scatter(x_coords, y_values, s=10, c=y_values, cmap='viridis') -plt.title(f"1D Náhodné pole (Gaussovská korelace $\\phi={phi}$)\nNepravidelná mřížka (NUFFT)") -plt.xlabel("Pozice [m]") -plt.ylabel("Hodnota (např. výška)") -plt.colorbar(label="Amplituda") -plt.grid(True) -plt.show() \ No newline at end of file From ac854f2ec0c57d2cd6fa8530998cd1d2989d9764 Mon Sep 17 00:00:00 2001 From: Rudolf Mahdal Date: Tue, 24 Mar 2026 14:15:10 +0100 Subject: [PATCH 3/4] fixed phi, variograms --- grf.py | 34 ++++++---- main_gstools.py | 170 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 138 insertions(+), 66 deletions(-) diff --git a/grf.py b/grf.py index 4724045..2e7ba4d 100644 --- a/grf.py +++ b/grf.py @@ -140,26 +140,36 @@ def generate_grf(x_points, corr, weights=None, seed=None): # ============================================================================= def empirical_variogram(x, field, n_bins=30, n_sample=300): - """Odhadne variogram z realizace pole. Bere podvzorek n_sample bodů.""" + """ + Odhadne variogram z realizace pole. + Funguje pro 1D i 2D: + x: (M,) pro 1D + x: (M, 2) pro 2D + Bere podvzorek n_sample bodů kvůli rychlosti. + """ rng = np.random.default_rng(0) - idx = rng.choice(len(x), size=min(len(x), n_sample), replace=False) + x = np.asarray(x) + idx = rng.choice(len(field), size=min(len(field), n_sample), replace=False) xi, fi = x[idx], field[idx] - pairs_h, pairs_sq = [], [] - for i in range(len(xi)): - for j in range(i + 1, len(xi)): - pairs_h.append(abs(xi[i] - xi[j])) - pairs_sq.append((fi[i] - fi[j])**2) + # vzdálenost – funguje pro 1D i 2D + if xi.ndim == 1: + dists = np.abs(xi[:, None] - xi[None, :]) # (n, n) + else: + diff = xi[:, None, :] - xi[None, :, :] # (n, n, 2) + dists = np.sqrt((diff ** 2).sum(axis=-1)) # (n, n) - pairs_h = np.array(pairs_h) - pairs_sq = np.array(pairs_sq) - bins = np.linspace(0, pairs_h.max(), n_bins + 1) + # jen horní trojúhelník (každý pár jednou) + i_upper, j_upper = np.triu_indices(len(xi), k=1) + pairs_h = dists[i_upper, j_upper] + pairs_sq = (fi[i_upper] - fi[j_upper]) ** 2 + bins = np.linspace(0, pairs_h.max(), n_bins + 1) h_vals, g_vals = [], [] for k in range(n_bins): - mask = (pairs_h >= bins[k]) & (pairs_h < bins[k+1]) + mask = (pairs_h >= bins[k]) & (pairs_h < bins[k + 1]) if mask.sum() > 5: - h_vals.append(0.5 * (bins[k] + bins[k+1])) + h_vals.append(0.5 * (bins[k] + bins[k + 1])) g_vals.append(0.5 * pairs_sq[mask].mean()) return np.array(h_vals), np.array(g_vals) \ No newline at end of file diff --git a/main_gstools.py b/main_gstools.py index dea1671..f4c5bff 100644 --- a/main_gstools.py +++ b/main_gstools.py @@ -3,70 +3,102 @@ Srovnání přes empirický variogram – obě metody by měly konvergovat k teoretické křivce gamma(h) = sigma^2 * (1 - exp(-h^2 / 2*phi^2)) + +POZOR na parametrizaci GSTools Gaussian: + GSTools: C(r) = exp(-pi * r^2 / (4 * len_scale^2)) + Naše: C(r) = exp(-r^2 / (2 * phi^2)) + Převod: len_scale_gs = phi * sqrt(pi/2) ≈ phi * 1.2533 + +POZOR na spektrální rozlišení: + Frekvenční krok Δω = 2π/L – při velkém phi (phi > L/8) spadne + do spektrální hustoty jen pár frekvenčních bodů a naše NUFFT + metoda nebude správně odhadovat rozptyl. Řešení: zvýšit L. """ -import importlib.util -from pathlib import Path import numpy as np import matplotlib.pyplot as plt import gstools as gs from scipy.interpolate import griddata +from scipy.stats import norm as sp_norm +import grf + -spec = importlib.util.spec_from_file_location("grf", Path(__file__).parent / "grf.py") -grf = importlib.util.module_from_spec(spec) -spec.loader.exec_module(grf) +L, N, phi, seed = 100.0, 1024, 11.0, 42 -L, N, phi, seed = 100.0, 1024, 5.0, 42 +# Převod phi -> len_scale pro GSTools Gaussian +# GSTools: C(r) = exp(-pi*r^2 / (4*ls^2)), naše: C(r) = exp(-r^2 / (2*phi^2)) +# => ls = phi * sqrt(pi/2) +len_scale_gs = phi * np.sqrt(np.pi / 2) + +# Varování při špatném poměru phi/L +delta_omega = 2 * np.pi / L +spectral_width = 1.0 / phi +n_spectral_pts = spectral_width / delta_omega +if n_spectral_pts < 3: + print(f"VAROVÁNÍ: phi={phi}, L={L} -> do spektra spadnou jen " + f"~{n_spectral_pts:.1f} frekvenční body. " + f"Zvyš L (doporučeno L > {int(8*phi)+1}) nebo sniž phi.") # ============================================================================= # 1D – jedna realizace # ============================================================================= -x = np.linspace(0, L, N) +x = np.linspace(0, L, N) corr = grf.GaussianCorrelation(L, N, phi, sigma=1.0, dim=1) -model = gs.Gaussian(dim=1, var=1.0, len_scale=phi) +model = gs.Gaussian(dim=1, var=1.0, len_scale=len_scale_gs) -white = grf.make_white_noise(N, dim=1, seed=seed) -field_nufft = grf.generate_grf(x, corr, weights=white) -field_gs = np.asarray(gs.SRF(model, seed=seed)(x)).ravel() +white = grf.make_white_noise(N, dim=1, seed=seed) +field_nufft = grf.generate_grf(x, corr, weights=white) +field_gs = np.asarray(gs.SRF(model, seed=seed)(x)).ravel() # ============================================================================= -# VARIOGRAM – průměr přes N_real realizací +# VARIOGRAM 1D – průměr přes N_real realizací # ============================================================================= N_real = 100 n_bins = 40 -bin_edges = np.linspace(0, L / 2, n_bins + 1) +# Oříznutí na L/3 – při větších lagy je málo párů a odhad je nespolehlivý +h_max = L / 3 +bin_edges = np.linspace(0, h_max, n_bins + 1) bin_c = 0.5 * (bin_edges[:-1] + bin_edges[1:]) -g_nufft = np.zeros(n_bins) -g_gs = np.zeros(n_bins) -cnt_nufft = np.zeros(n_bins, dtype=int) # počet realizací které přispěly +g_nufft = np.zeros(n_bins) +g_gs = np.zeros(n_bins) +cnt_nufft = np.zeros(n_bins, dtype=int) cnt_gs = np.zeros(n_bins, dtype=int) for i in range(N_real): w = grf.make_white_noise(N, dim=1, seed=i) f_n = grf.generate_grf(x, corr, weights=w) - _, g = grf.empirical_variogram(x, f_n, n_bins=n_bins) - if len(g) == n_bins: - g_nufft += g - cnt_nufft += 1 + h_v, g = grf.empirical_variogram(x, f_n, n_bins=n_bins) + # empirical_variogram vrací variabilní délku – mapujeme do fixních binů + for k, hk in enumerate(h_v): + bi = np.searchsorted(bin_edges[1:], hk) + if 0 <= bi < n_bins: + g_nufft[bi] += g[k] + cnt_nufft[bi] += 1 f_g = np.asarray(gs.SRF(model, seed=i)(x)).ravel() - _, g = grf.empirical_variogram(x, f_g, n_bins=n_bins) - if len(g) == n_bins: - g_gs += g - cnt_gs += 1 + h_v, g = grf.empirical_variogram(x, f_g, n_bins=n_bins) + for k, hk in enumerate(h_v): + bi = np.searchsorted(bin_edges[1:], hk) + if 0 <= bi < n_bins: + g_gs[bi] += g[k] + cnt_gs[bi] += 1 -# dělíme skutečným počtem přispívajících realizací (ne vždy N_real) g_nufft = np.divide(g_nufft, cnt_nufft, where=cnt_nufft > 0) g_gs = np.divide(g_gs, cnt_gs, where=cnt_gs > 0) -h_th = np.linspace(0, L / 2, 300) +# Maska – zobraz jen biny kde máme data +mask_n = cnt_nufft > 0 +mask_g = cnt_gs > 0 + +# Teoretický variogram: C(r) = exp(-r^2 / 2*phi^2) => gamma(h) = 1 - C(h) +h_th = np.linspace(0, h_max, 300) gamma_th = 1 - np.exp(-0.5 * (h_th / phi)**2) # ============================================================================= -# 2D +# 2D – generování a variogram přímo z nepravidelných bodů # ============================================================================= N2 = 128 @@ -78,20 +110,45 @@ corr2d = grf.GaussianCorrelation(L, N2, phi, sigma=1.0, dim=2) white2d = grf.make_white_noise(N2, dim=2, seed=seed) -f_irr2d = grf.generate_grf(pts_irr, corr2d, weights=white2d) + +# Generujeme pole v nepravidelných bodech +f_irr2d = grf.generate_grf(pts_irr, corr2d, weights=white2d) + +# ✅ Interpolace jen pro vizualizaci – variogram počítáme z původních bodů field2d_nufft = griddata(pts_irr, f_irr2d, (xx, yy), method='linear') -model2d = gs.Gaussian(dim=2, var=1.0, len_scale=phi) +model2d = gs.Gaussian(dim=2, var=1.0, len_scale=len_scale_gs) field2d_gs = gs.SRF(model2d, seed=seed).structured([g2, g2]) +# ✅ Variogram 2D – počítáme z nepravidelných bodů, ne z interpolované mřížky +h_max_2d = L / 3 +n_bins_2d = 30 + +# NUFFT: přímo z pts_irr a f_irr2d +h_2d_nufft, g_2d_nufft = grf.empirical_variogram(pts_irr, f_irr2d, + n_bins=n_bins_2d) + +# GSTools: z pravidelné mřížky (jako (M,2) pole bodů) +pts_reg = np.column_stack([xx.ravel(), yy.ravel()]) +f_gs_reg = field2d_gs.ravel() +h_2d_gs, g_2d_gs = grf.empirical_variogram(pts_reg, f_gs_reg, + n_bins=n_bins_2d) + +# Oříznutí na L/3 +mask_2d_n = h_2d_nufft <= h_max_2d +mask_2d_g = h_2d_gs <= h_max_2d + +h_th_2d = np.linspace(0, h_max_2d, 300) +gamma_th_2d = 1 - np.exp(-0.5 * (h_th_2d / phi)**2) + # ============================================================================= # GRAFY # ============================================================================= fig = plt.figure(figsize=(16, 9)) -layout = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.3) +layout = fig.add_gridspec(2, 3, hspace=0.45, wspace=0.32) -# 1D realizace +# --- 1D realizace --- ax0 = fig.add_subplot(layout[0, 0]) ax0.plot(x, field_gs, lw=1.2, label="GSTools") ax0.plot(x, field_nufft, lw=1, label="naše NUFFT", alpha=0.8) @@ -99,50 +156,55 @@ ax0.set_xlabel("x [m]") ax0.legend(fontsize=8); ax0.grid(True, alpha=0.3) -# variogram průměr +# --- Variogram 1D průměr --- ax1 = fig.add_subplot(layout[0, 1]) -ax1.plot(bin_c, g_nufft, 'o-', ms=3, label=f"naše NUFFT (avg {N_real})") -ax1.plot(bin_c, g_gs, 's-', ms=3, label=f"GSTools (avg {N_real})") -ax1.plot(h_th, gamma_th, 'k--', lw=2, label="teoretický") -ax1.set_title(f"Variogram – průměr přes {N_real} realizací") +ax1.plot(bin_c[mask_n], g_nufft[mask_n], 'o-', ms=3, + label=f"naše NUFFT (avg {N_real})") +ax1.plot(bin_c[mask_g], g_gs[mask_g], 's-', ms=3, + label=f"GSTools (avg {N_real})") +ax1.plot(h_th, gamma_th, 'k--', lw=2, label="teoretický") +ax1.set_title(f"Variogram 1D – průměr přes {N_real} realizací\n" + f"(zobrazeno do h = L/3 = {h_max:.0f} m)") ax1.set_xlabel("h [m]"); ax1.set_ylabel("γ(h)") +ax1.set_xlim(0, h_max) ax1.legend(fontsize=8); ax1.grid(True, alpha=0.3) -# gs.vario_estimate -bin_c_gs, vario_gs_builtin = gs.vario_estimate([x], field_gs, bin_edges=bin_edges) +# --- GSTools vario_estimate (1 realizace) --- +bin_edges_gs = np.linspace(0, h_max, n_bins + 1) +bin_c_gs, vario_gs_builtin = gs.vario_estimate([x], field_gs, + bin_edges=bin_edges_gs) ax2 = fig.add_subplot(layout[0, 2]) ax2.scatter(bin_c_gs, vario_gs_builtin, s=15, label="gs.vario_estimate") ax2.plot(h_th, gamma_th, 'k--', lw=2, label="teoretický") -ax2.set_title("GSTools vario_estimate (1 realizace)") +ax2.set_title(f"GSTools vario_estimate (1 realizace)\n" + f"(zobrazeno do h = L/3 = {h_max:.0f} m)") ax2.set_xlabel("h [m]"); ax2.set_ylabel("γ(h)") +ax2.set_xlim(0, h_max) ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3) -# 2D NUFFT +# --- 2D NUFFT (interpolovaná vizualizace) --- ax3 = fig.add_subplot(layout[1, 0]) im3 = ax3.imshow(field2d_nufft, extent=[0,L,0,L], origin='lower', cmap='viridis') plt.colorbar(im3, ax=ax3) ax3.set_title(f"2D naše NUFFT (φ={phi})") -# 2D GSTools +# --- 2D GSTools --- ax4 = fig.add_subplot(layout[1, 1]) im4 = ax4.imshow(field2d_gs, extent=[0,L,0,L], origin='lower', cmap='viridis') plt.colorbar(im4, ax=ax4) ax4.set_title(f"2D GSTools (φ={phi})") -# Histogram hodnot obou 2D polí – ukazuje stejné statistické vlastnosti +# --- Variogram 2D – z nepravidelných bodů --- ax5 = fig.add_subplot(layout[1, 2]) -vals_nufft = field2d_nufft[~np.isnan(field2d_nufft)].ravel() -vals_gs = field2d_gs.ravel() -bins_hist = np.linspace(min(vals_nufft.min(), vals_gs.min()), - max(vals_nufft.max(), vals_gs.max()), 40) -ax5.hist(vals_nufft, bins=bins_hist, alpha=0.5, density=True, label="naše NUFFT") -ax5.hist(vals_gs, bins=bins_hist, alpha=0.5, density=True, label="GSTools") -# teoretické N(0,1) -from scipy.stats import norm as sp_norm -xn = np.linspace(bins_hist[0], bins_hist[-1], 200) -ax5.plot(xn, sp_norm.pdf(xn, 0, 1), 'k--', lw=2, label="N(0,1)") -ax5.set_title("Histogram hodnot 2D polí") -ax5.set_xlabel("hodnota pole"); ax5.set_ylabel("hustota") +ax5.plot(h_2d_nufft[mask_2d_n], g_2d_nufft[mask_2d_n], 'o-', ms=3, + label="naše NUFFT (z irr. bodů)") +ax5.plot(h_2d_gs[mask_2d_g], g_2d_gs[mask_2d_g], 's-', ms=3, + label="GSTools (z reg. mřížky)") +ax5.plot(h_th_2d, gamma_th_2d, 'k--', lw=2, label="teoretický") +ax5.set_title(f"Variogram 2D (do L/3 = {h_max_2d:.0f} m)\n" + f"✅ počítáno z původních bodů, ne z interpolace") +ax5.set_xlabel("h [m]"); ax5.set_ylabel("γ(h)") +ax5.set_xlim(0, h_max_2d) ax5.legend(fontsize=8); ax5.grid(True, alpha=0.3) plt.suptitle(f"Bod 5: Srovnání s GSTools – Gaussovská korelace φ={phi}", fontsize=13) From 4766aae21eb0e2401750ff9562699407d7963258 Mon Sep 17 00:00:00 2001 From: Rudolf Mahdal Date: Sun, 12 Apr 2026 22:54:23 +0200 Subject: [PATCH 4/4] refactor(grf): dataclasses, type annotations, English docstrings - @dataclass on both correlation classes, f_points via field(init=False) - type annotations on all functions (np.ndarray, int | None, bool) - NumPy-style docstrings with Parameters / Returns / Raises - empirical_variogram: vectorized triu_indices, works for 1D and 2D - removed unused scipy.fft imports --- grf.py | 221 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 170 insertions(+), 51 deletions(-) diff --git a/grf.py b/grf.py index 2e7ba4d..915ac9b 100644 --- a/grf.py +++ b/grf.py @@ -3,6 +3,7 @@ """ import numpy as np +from dataclasses import dataclass, field import finufft from scipy.fft import fft, ifft, fftfreq, fftshift @@ -10,41 +11,138 @@ # ============================================================================= # KORELAČNÍ TŘÍDY # ============================================================================= - +@dataclass class GaussianCorrelation: + """Gaussian correlation model. + + Correlation function: + C(r) = sigma^2 * exp(-|r|^2 / (2 * phi^2)) + + Spectral density: + S(omega) = sigma^2 * (sqrt(2*pi) * phi)^dim * exp(-|omega|^2 * phi^2 / 2) + + Parameters + ---------- + L : float + Domain size [m]. Assumes a cubic domain [0, L]^dim. + N_freq : int + Number of frequency points per axis. Total N_freq^dim nodes in Fourier space. + phi : float + Correlation length [m]. Controls the spatial range of dependence. + sigma : float + Standard deviation of the field (square root of variance). Default 1.0. + dim : int + Spatial dimension (1 or 2). Default 1. + + Attributes + ---------- + f_points : np.ndarray, shape (N_freq,) + Centered angular frequencies [rad/m]: + omega_k = k * (2*pi / L) for k = -N_freq//2, ..., N_freq//2 - 1 """ - C(r) = sigma^2 * exp(-|r|^2 / 2*phi^2) - S(w) = sigma^2 * (sqrt(2pi)*phi)^dim * exp(-|w|^2*phi^2/2) - """ - def __init__(self, L, N_freq, phi, sigma=1.0, dim=1): - self.L, self.N_freq, self.phi, self.sigma, self.dim = L, N_freq, phi, sigma, dim - k = np.arange(-N_freq // 2, N_freq // 2) - self.f_points = k * (2 * np.pi / L) # centrované úhlové frekvence - - def spectral_density(self, omega_mag): - return (self.sigma**2 - * (np.sqrt(2 * np.pi) * self.phi) ** self.dim - * np.exp(-0.5 * (omega_mag * self.phi)**2)) - - + L: float + N_freq: int + phi: float + sigma: float = 1.0 + dim: int = 1 + f_points: np.ndarray = field(init=False, repr=False) + + def __post_init__(self) -> None: + k = np.arange(-self.N_freq // 2, self.N_freq // 2) + self.f_points = k * (2 * np.pi / self.L) + + def spectral_density(self, omega_mag: np.ndarray) -> np.ndarray: + """Evaluate spectral density S(|omega|). + + Parameters + ---------- + omega_mag : np.ndarray, arbitrary shape + Magnitudes of angular frequencies [rad/m]. + + Returns + ------- + S : np.ndarray, same shape as omega_mag + """ + return ( + self.sigma ** 2 + * (np.sqrt(2 * np.pi) * self.phi) ** self.dim + * np.exp(-0.5 * (omega_mag * self.phi) ** 2) + ) + + +@dataclass class ExponentialCorrelation: + """Exponential (Matérn-1/2) correlation model. + + Correlation function: + C(r) = sigma^2 * exp(-|r| / phi) + + Spectral density: + 1D: S(omega) = sigma^2 * 2*phi / (1 + (omega*phi)^2) + 2D: S(omega) = sigma^2 * 2*pi*phi^2 / (1 + (|omega|*phi)^2)^(3/2) + + Parameters + ---------- + L : float + Domain size [m]. Assumes a cubic domain [0, L]^dim. + N_freq : int + Number of frequency points per axis. Total N_freq^dim nodes in Fourier space. + phi : float + Correlation length [m]. Controls the spatial range of dependence. + sigma : float + Standard deviation of the field (square root of variance). Default 1.0. + dim : int + Spatial dimension (1 or 2). Default 1. + + Attributes + ---------- + f_points : np.ndarray, shape (N_freq,) + Centered angular frequencies [rad/m]: + omega_k = k * (2*pi / L) for k = -N_freq//2, ..., N_freq//2 - 1 """ - C(r) = sigma^2 * exp(-|r|/phi) - 1D: S(w) = sigma^2 * 2*phi / (1 + (w*phi)^2) - 2D: S(w) = sigma^2 * 2*pi*phi^2 / (1 + (|w|*phi)^2)^(3/2) - """ - def __init__(self, L, N_freq, phi, sigma=1.0, dim=1): - self.L, self.N_freq, self.phi, self.sigma, self.dim = L, N_freq, phi, sigma, dim - k = np.arange(-N_freq // 2, N_freq // 2) - self.f_points = k * (2 * np.pi / L) - - def spectral_density(self, omega_mag): + + L: float + N_freq: int + phi: float + sigma: float = 1.0 + dim: int = 1 + f_points: np.ndarray = field(init=False, repr=False) + + def __post_init__(self) -> None: + k = np.arange(-self.N_freq // 2, self.N_freq // 2) + self.f_points = k * (2 * np.pi / self.L) + + def spectral_density(self, omega_mag: np.ndarray) -> np.ndarray: + """Evaluate spectral density S(|omega|). + + Parameters + ---------- + omega_mag : np.ndarray, arbitrary shape + Magnitudes of angular frequencies [rad/m]. + + Returns + ------- + S : np.ndarray, same shape as omega_mag + + Raises + ------ + NotImplementedError + For dim > 2. + """ if self.dim == 1: - return self.sigma**2 * 2 * self.phi / (1 + (omega_mag * self.phi)**2) + return self.sigma ** 2 * 2 * self.phi / (1 + (omega_mag * self.phi) ** 2) elif self.dim == 2: - return self.sigma**2 * 2 * np.pi * self.phi**2 / (1 + (omega_mag * self.phi)**2)**1.5 + return ( + self.sigma ** 2 + * 2 * np.pi * self.phi ** 2 + / (1 + (omega_mag * self.phi) ** 2) ** 1.5 + ) else: - raise NotImplementedError("Exponential: dim > 2 není implementováno") + raise NotImplementedError( + f"ExponentialCorrelation: dim={self.dim} not implemented (max dim=2)" + ) + + # ============================================================================= @@ -65,21 +163,38 @@ def make_hermitian_nd(f_hat): return f -def make_white_noise(N_freq, dim=1, seed=None, use_gstools_rng=False): - """ - Komplexní bílý šum tvaru (N_freq,)*dim ze standardního normálního rozdělení. - - use_gstools_rng=True – použije gstools.random.RNG (stejný generátor jako GSTools interně) - self._rng = RNG(seed) - z_1 = self._rng.random.normal(size=N) - z_2 = self._rng.random.normal(size=N) - use_gstools_rng=False – numpy default_rng (výchozí) +def make_white_noise( + N_freq: int, + dim: int = 1, + seed: int | None = None, + use_gstools_rng: bool = False, +) -> np.ndarray: + """Generate complex white noise in Fourier space. + + Each component is an independent complex Gaussian variable with zero mean + and unit variance: z ~ CN(0, 1). + + Parameters + ---------- + N_freq : int + Number of frequency points per axis. + dim : int + Spatial dimension. Default 1. + seed : int or None + Random seed for reproducibility. None = random seed. + use_gstools_rng : bool + True - use gstools.random.RNG for direct comparison with GSTools. + False - use numpy.random.default_rng (default, recommended). + + Returns + ------- + noise : np.ndarray, shape (N_freq,) * dim, dtype complex128 """ size = N_freq ** dim if use_gstools_rng: from gstools.random import RNG gs_rng = RNG(seed) - rs = gs_rng.random # numpy RandomState stream + rs = gs_rng.random flat = (rs.normal(size=size) + 1j * rs.normal(size=size)) / np.sqrt(2) else: rng = np.random.default_rng(seed) @@ -88,18 +203,22 @@ def make_white_noise(N_freq, dim=1, seed=None, use_gstools_rng=False): # ============================================================================= -# GENEROVÁNÍ GRF +# GRF GENERATION # ============================================================================= -def generate_grf(x_points, corr, weights=None, seed=None): - """ - Generuje náhodné pole pomocí NUFFT typu 2. +def generate_grf( + x_points: np.ndarray, + corr: GaussianCorrelation | ExponentialCorrelation, + weights: np.ndarray | None = None, + seed: int | None = None, +) -> np.ndarray: + """Generate a GRF realization at arbitrary points via NUFFT type 2. - x_points : (M,) pro 1D, (M, 2) pro 2D + x_points : np.ndarray, shape (M,) for 1D or (M, 2) for 2D corr : GaussianCorrelation | ExponentialCorrelation - weights : bílý šum tvaru (N_freq,)*dim; None = vygeneruje se nový + weights : white noise, shape (N_freq,)^dim; None = generate from seed - Vrací field.real tvaru (M,) + Returns field.real, shape (M,) """ dim = corr.dim N_freq = corr.N_freq @@ -136,16 +255,16 @@ def generate_grf(x_points, corr, weights=None, seed=None): return (field * norm).real # ============================================================================= -# EMPIRICKÝ VARIOGRAM gamma(h) = 0.5 * E[(f(x)-f(x+h))^2] +# EMPIRICKÝ V`ARIOGRAM gamma(h) = 0.5 * E[(f(x)-f(x+h))^2] # ============================================================================= def empirical_variogram(x, field, n_bins=30, n_sample=300): """ - Odhadne variogram z realizace pole. - Funguje pro 1D i 2D: - x: (M,) pro 1D - x: (M, 2) pro 2D - Bere podvzorek n_sample bodů kvůli rychlosti. + Estimate the empirical variogram from a single field realization. + Works for both 1D and 2D. Subsamples n_sample points to reduce O(M^2) cost. + + x : shape (M,) for 1D, (M, 2) for 2D + field : shape (M,) """ rng = np.random.default_rng(0) x = np.asarray(x)