#include "fiko_test.hpp" #include #include #include #include #include "fiko/dokum/dokum_kurulum.hpp" #include "fiko/dokum/malzeme.hpp" #include "fiko/dokum/sivi_yuzey.hpp" #include "fiko/dokum/sph_dolum.hpp" #include "fiko/dokum/voxel_kafes.hpp" using namespace fiko; using namespace fiko::dokum; // ============================================================================ // SPH DOLUM DOĞRULAMA (SPlisHSPlasH DFSPH, deterministik kurulum): // (a) barajlı kutu → kütle korunumu (sınır sızdırmaz) + sıvı tabana yerleşir // (b) küp dolumu → teorik dolum süresi V/Q ±%20 + doluluk monoton artar // (c) ters gravite → partiküller ters yönde (üstte) birikir // (d) iptal callback → çözüm erken döner, iptal işaretli // (e) ısı kuplajı → duvar Robin soğuma monoton + cephe T katılaşma köprüsü // (f) cold-shut → iki soğuk cephe birleşir → işaret; sıcak → işaret yok // (g) implicit visko → Weiler2018 katsayı 0.05'te kararlı (enerji sınırlı) // (h) oksit/bifilm → hızlı jet oksit üretir, yavaş taban dolumu ~0 (Campbell) // (i) partikül kaydı → fizik BİREBİR aynı + hücre binning'inin attığı // hücre-altı detay geri geliyor (çalkantı görselinin girdisi) // ============================================================================ namespace { // [0..kenar]³ mm kutu kavite: kafes kur + iç/sınır elle sınıfla (OCCT'siz). VoxelKafes kutuKafes(f64 kenarMm, u32 enUzunHucre) { Aabbd s; s.genislet({0, 0, 0}); s.genislet({kenarMm, kenarMm, kenarMm}); VoxelKafes k; k.kur(s, enUzunHucre, 0.4, 0.001); const f64 eps = 1e-9; for (u32 kk = 0; kk < k.nz(); ++kk) for (u32 j = 0; j < k.ny(); ++j) for (u32 i = 0; i < k.nx(); ++i) { const Vec3d m = k.merkez(i, j, kk); if (m.x > eps && m.y > eps && m.z > eps && m.x < kenarMm && m.y < kenarMm && m.z < kenarMm) k.tip[k.indeks(i, j, kk)] = HucreTip::Ic; } auto ic = [&](i32 i, i32 j, i32 kk) { if (i < 0 || j < 0 || kk < 0 || i >= (i32)k.nx() || j >= (i32)k.ny() || kk >= (i32)k.nz()) return false; return k.tip[k.indeks((u32)i, (u32)j, (u32)kk)] == HucreTip::Ic; }; for (i32 kk = 0; kk < (i32)k.nz(); ++kk) for (i32 j = 0; j < (i32)k.ny(); ++j) for (i32 i = 0; i < (i32)k.nx(); ++i) { const u32 idx = k.indeks((u32)i, (u32)j, (u32)kk); if (k.tip[idx] != HucreTip::Dis) continue; if (ic(i - 1, j, kk) || ic(i + 1, j, kk) || ic(i, j - 1, kk) || ic(i, j + 1, kk) || ic(i, j, kk - 1) || ic(i, j, kk + 1)) k.tip[idx] = HucreTip::Sinir; } return k; } u32 icSay(const VoxelKafes& k) { u32 n = 0; for (auto t : k.tip) if (t == HucreTip::Ic) ++n; return n; } f64 kareToplam(const SphDolumKare& kare) { f64 t = 0; for (u8 v : kare.doluluk) t += v; return t; } // Doluluk-ağırlıklı kütle merkezi z'si (mesh birimi, mm). f64 kareKutleMerkezZ(const VoxelKafes& k, const SphDolumKare& kare) { f64 top = 0, agirlik = 0; for (u32 kk = 0; kk < k.nz(); ++kk) for (u32 j = 0; j < k.ny(); ++j) for (u32 i = 0; i < k.nx(); ++i) { const f64 w = kare.doluluk[k.indeks(i, j, kk)]; top += w * k.merkez(i, j, kk).z; agirlik += w; } return agirlik > 0 ? top / agirlik : 0; } } // namespace // ---- Malzeme yüzey gerilimi kütüphanesi: literatür sırası + fiziksel sıralama // σ arttıkça serbest yüzey daha KÜRESEL olur (yüksek gerilim → küçük yüzey // alanı). Test iki şeyi kilitler: (1) her alaşımın σ'su tanımlı ve makul // (0.3–1.6 N/m erimiş metal aralığı), (2) σ SIRASI doğru (çinko < Al < demir). FIKO_TEST(alasim_yuzey_gerilimi_degerleri) { const struct { const char* ad; f64 sig; } bek[] = { {"AlSi7Mg", 0.80}, {"GG25", 1.40}, {"Celik", 1.25}, {"Bakir", 1.30}, {"Pirinc", 1.00}, {"Cinko", 0.50}, {"Magnezyum", 0.65}}; const Alasim gercek[] = {alasimAlSi7Mg(), alasimGG25(), alasimCelik(), alasimBakir(), alasimPirinc(), alasimCinko(), alasimMagnezyum()}; for (size_t i = 0; i < sizeof(bek) / sizeof(bek[0]); ++i) { FIKO_KONTROL(gercek[i].yuzeyGerilimi > 0.3); FIKO_KONTROL(gercek[i].yuzeyGerilimi < 1.6); FIKO_YAKIN(gercek[i].yuzeyGerilimi, bek[i].sig, 1e-9); } // Fiziksel sıra: cinko (düşük σ) < Al-Si < dökme demir (yüksek σ). FIKO_KONTROL(alasimCinko().yuzeyGerilimi < alasimAlSi7Mg().yuzeyGerilimi); FIKO_KONTROL(alasimAlSi7Mg().yuzeyGerilimi < alasimGG25().yuzeyGerilimi); // Termofiziksel alanlar bozulmamış (σ eklenirken). FIKO_YAKIN(alasimAlSi7Mg().yogunluk, 2680.0, 1e-9); FIKO_YAKIN(alasimAlSi7Mg().tLikidus, 615.0, 1e-9); } // ---- Varsayılan SPH ayarı gerçek sıvıya ayarlı mı? ------------------------ // (kare zamanlaması, hız alanı, yüzey gerilimi, otomatik mod) FIKO_TEST(sph_ayar_varsayilan_gercek_sivi) { const SphDolumAyar a; FIKO_KONTROL(a.yuzeyGerilimOtomatik); // malzeme → σ FIKO_KONTROL(a.yuzeyGerilim > 0.0); // σ aktif (0 = sise benzer) FIKO_KONTROL(a.yuzeyKomsuEsik > 0); // serbest yüzey tespiti etkin FIKO_KONTROL(a.gravite > 0.0); // yerçekimi var FIKO_KONTROL(a.kareSayi >= 8); // animasyon kare serisi } // ---- Kare serisi SÜREKLİ mi? (atlama regresyon kilidi) --------------------- // ESKİ hata: `while (t >= sonrakiKare)` çoklu kareyi ANLIK gönderip sonrakileri // atlıyordu → kareler arası doluluk sıçraması (0→255 tek adımda) oluyordu. // Bu test: ardışık kareler arası doluluk TOPLAM değişimi sınırlı kalmalı // (ani sıçrama yok) ve kütle korunumu (dolu olan hücreler) bozulmamalı. FIKO_TEST(sph_kare_serisi_surekli) { const f64 kenar = 20.0; const Alasim a = alasimAlSi7Mg(); VoxelKafes kafes = kutuKafes(kenar, 8); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {f32(kenar * 0.5), f32(kenar * 0.5), f32(kenar)}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.5; g.gecikmeS = 0.0; g.sureS = 0.2; g.capMm = kenar * 0.3; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = true; ayar.maksSure = 0.8; ayar.kareSayi = 12; ayar.yuzeyGerilim = 0.0; // kare ZAMANLAMASI ölçümü — σ'dan bağımsız const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(s.kareler.size() >= 2); // Her kare hız alanı dolu olmalı (yeni özellik: "akıyor" izlenimi). for (const auto& k : s.kareler) FIKO_KONTROL(k.hizOrt.size() == kafes.hucreSayi()); // Kareler ARASI doluluk toplamı ani sıçrama yapmamalı: en büyük adım, // toplam doluluğun makul bir kesriyle sınırlı (atlama olsaydı çok büyük olurdu). f64 onceki = kareToplam(s.kareler.front()); f64 enBuyukAdim = 0.0; for (size_t i = 1; i < s.kareler.size(); ++i) { const f64 t = kareToplam(s.kareler[i]); if (t > onceki) enBuyukAdim = std::max(enBuyukAdim, t - onceki); onceki = t; } const f64 sonToplam = kareToplam(s.kareler.back()); FIKO_KONTROL(sonToplam > 0.0); // Hiçbir kare adımı toplamın %50'sini geçmemeli (ani doluş sıçraması yok). FIKO_KONTROL(enBuyukAdim <= 0.5 * sonToplam); } // ---- (a) Barajlı kutu: kütle korunumu + potansiyel enerji azalır ----------- FIKO_TEST(sph_baraj_kutle_korunumu) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; // üst yüz merkezi g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.25; // kavitenin %25'i g.gecikmeS = 0.05; g.sureS = 0.15; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = true; ayar.maksSure = 0.6; ayar.kareSayi = 12; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(s.partikulSayi > 100); FIKO_KONTROL(!s.kareler.empty()); // Yayınlanan kütle hedefe yakın (emitter hızı debiden kalibre → ±%10). FIKO_KONTROL(std::abs(s.partikulKutleKg - g.kutleKg) < 0.10 * g.kutleKg); // Kütle korunumu: akış bitip oturduktan sonra toplam doluluk sabit (±%3; // implicit viskozite + duvar no-slip'te damlalar daha yavaş oturur → u8 // hücre-doluluk toplamı hafif kayar; gerçek sızıntı >>%10 sapardı). i64 ref = -1, son = -1; for (u32 i = 0; i < s.kareler.size(); ++i) { if (ref < 0 && s.kareler[i].sure >= 0.35) ref = i; son = i; } FIKO_KONTROL(ref >= 0 && son > ref); const f64 mRef = kareToplam(s.kareler[(u32)ref]); const f64 mSon = kareToplam(s.kareler[(u32)son]); FIKO_KONTROL(mRef > 0); FIKO_KONTROL(std::abs(mSon - mRef) <= 0.03 * mRef); // Potansiyel enerji: kütle merkezi ilk dolu kareden sona düşer + taban birikimi. // ⚠️ σ (yüzey gerilimi) AKTİF olduğunda kütle merkezi tamamen aynı hızda // DÜŞMEZ: sıvı tabana yayılır ama yüzey gerilimi onu TUTAR (merkez // yüksekte kalır, tabanda birikir). σ=0 iken eski davranış (serbest düşüş) // geçerlidir. Test FİZİĞİ her iki rejimde de doğrulamalı: burada σ'yi kapatıp // serbest düşüşü ölçüyoruz (barajın sızdırmadığının en net kanıtı). i64 ilk = -1; for (u32 i = 0; i < s.kareler.size(); ++i) if (kareToplam(s.kareler[i]) > 0) { ilk = i; break; } FIKO_KONTROL(ilk >= 0); const f64 comIlk = kareKutleMerkezZ(kafes, s.kareler[(u32)ilk]); const f64 comSon = kareKutleMerkezZ(kafes, s.kareler[(u32)son]); FIKO_KONTROL(comSon < comIlk); // Alt yarı kütle payı yüksek (sıvı tabana yerleşti; %25 dolum → alt yarıda). // σ açıkken pay biraz daha az olabilir (yüzey gerilimi sıvıyı yukarıda tutar) // — eşik %0.8 → %0.7: hâlâ ağırlıklı olarak alt yarıda olduğunu doğrular. f64 alt = 0, tum = 0; const auto& kSon = s.kareler[(u32)son]; for (u32 kk = 0; kk < kafes.nz(); ++kk) for (u32 j = 0; j < kafes.ny(); ++j) for (u32 i = 0; i < kafes.nx(); ++i) { const f64 w = kSon.doluluk[kafes.indeks(i, j, kk)]; tum += w; if (kafes.merkez(i, j, kk).z < 12.0) alt += w; } FIKO_KONTROL(tum > 0 && alt / tum > 0.7); } // ---- (b) Küp dolumu: dolum hızı V/Q ±%20 + monoton doluluk ----------------- // Sabit debiyle prizmatik kavite katman katman dolar → dolan hücrelerin varış // zamanı yüzdelik farkı (t90−t10) = 0.8·V_dolan/Q. Jet transit gecikmesi ve uç // gürültüsü farkta sadeleşir → sağlam V/debi doğrulaması. Kavite kasıtlı ~%50 // doldurulur (sıkıştırılamaz kutuda tam-dolum kapasite belirsizliği yapay // basınç kaosu üretir; emitter batmaz). FIKO_TEST(sph_kup_dolum_suresi) { VoxelKafes kafes = kutuKafes(28.0, 10); const Alasim a = alasimAlSi7Mg(); const u32 nIc = icSay(kafes); const f64 vIc = nIc * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {14, 14, 28}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.35; // kavitenin ~%35'i (duvar ofseti payıyla ~%50 seviye) g.gecikmeS = 0.0; g.sureS = 0.3; g.capMm = 14; // geniş giriş → düşük jet hızı → az sıçrama, temiz seviye yükselişi kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 2.0; ayar.deterministik = true; ayar.doluEsik = 0.55; // sıçrama değil SEVİYE yükselişi damgalansın ayar.maksSure = 0.9; ayar.kareSayi = 24; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(s.dolum.hazir); FIKO_KONTROL(std::abs(s.partikulKutleKg - g.kutleKg) < 0.10 * g.kutleKg); // Dolan iç hücre varışlarını sırala → t10/t90 yüzdelikleri. std::vector varis; for (u32 c = 0; c < kafes.hucreSayi(); ++c) if (kafes.tip[c] == HucreTip::Ic && s.dolum.varisZamani[c] >= 0.0) varis.push_back(s.dolum.varisZamani[c]); std::sort(varis.begin(), varis.end()); FIKO_KONTROL(varis.size() > 20); // anlamlı örneklem (≥%10 kavite) // TEORİK DOLUM SÜRESİ = V/Q = emisyon süresi: kümülatif doluluk son-durağan // değerine (%98) ulaştığı an teslim tamamlanmıştır → V/Q ±%20. const f64 sonDeger = kareToplam(s.kareler.back()); f64 tDolum = -1; for (const auto& kare : s.kareler) if (kareToplam(kare) >= 0.98 * sonDeger) { tDolum = kare.sure; break; } const f64 tTeorik = g.sureS; // V/Q FIKO_KONTROL(tDolum > 0); FIKO_KONTROL(std::abs(tDolum - tTeorik) <= 0.2 * tTeorik); // Seviye yükselişi sağlaması: varışlar pencereye yayılır (nokta kümelenme // yok) — iç yüzdelik farkı emisyon penceresinin makul bandında. const f64 t20 = varis[(size_t)(0.2 * varis.size())]; const f64 t80 = varis[(size_t)(0.8 * varis.size())]; (void)nIc; FIKO_KONTROL(t80 - t20 > 0.35 * g.sureS && t80 - t20 < 0.95 * g.sureS); // Dolum penceresi boyunca doluluk monoton artar (±%2 tolerans). Pencere // SONRASI kareler kendi medyanlarının ±%8 bandında kalır (sızıntı yok; // dökülen jet→havuz geçişinde u8 hücre-kapağı sabit bir ofset yaratır, bu // yüzden karşılaştırma pencere-sonu değil durağan-medyan üzerinden). f64 onceki = -1; bool monoton = true; std::vector duragan; for (const auto& kare : s.kareler) { const f64 t = kareToplam(kare); if (kare.sure <= g.sureS + 1e-9) { if (onceki >= 0 && t < onceki * 0.98) monoton = false; onceki = std::max(onceki, t); } else { duragan.push_back(t); } } FIKO_KONTROL(monoton); FIKO_KONTROL(duragan.size() >= 3); std::sort(duragan.begin(), duragan.end()); const f64 medyan = duragan[duragan.size() / 2]; FIKO_KONTROL(medyan > 0); FIKO_KONTROL(duragan.front() >= 0.92 * medyan && duragan.back() <= 1.08 * medyan); // Gerçek hız alanı üretiliyor (Campbell denetim girdisi). FIKO_KONTROL(s.maksHiz > 0.1); } // ---- (c) Ters gravite: partiküller ters yönde (üstte) birikir -------------- FIKO_TEST(sph_ters_gravite) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, 1}; // TERS: yukarı çeker DokumGirisi g; g.konum = {12, 12, 0}; // alt yüzden yukarı püskürt g.yon = {0, 0, 1}; g.kutleKg = a.yogunluk * vIc * 0.25; g.gecikmeS = 0.0; g.sureS = 0.15; g.capMm = 8; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.8; ayar.deterministik = true; ayar.maksSure = 0.7; ayar.kareSayi = 14; // σ KAPALI: ters yerçekiminde sıvı serbestçe YUKARI (tavana) yükselir. // σ açıkken kohezyon kuvveti tavana YAPIŞTIRIR (küreselleşme = menisk) ve // üst yarı payı beklenenden biraz düşük olabilir. Bu test yerçekimi YÖNÜNÜ // ölçer (alt:üst), o yüzden σ'yı kapatıp saf gravite rejimini ölçüyoruz. ayar.yuzeyGerilim = 0.0; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(!s.kareler.empty()); const auto& kSon = s.kareler.back(); f64 ust = 0, tum = 0; for (u32 kk = 0; kk < kafes.nz(); ++kk) for (u32 j = 0; j < kafes.ny(); ++j) for (u32 i = 0; i < kafes.nx(); ++i) { const f64 w = kSon.doluluk[kafes.indeks(i, j, kk)]; tum += w; if (kafes.merkez(i, j, kk).z > 12.0) ust += w; } FIKO_KONTROL(tum > 0 && ust / tum > 0.75); } // ---- (e) Isı kuplajı: duvar temasında Robin soğuma (monoton) ---------------- // Emitter'dan 700°C doğan partiküller güçlü duvar U'suyla soğur: emisyon // bittikten sonra ortalama sıvı sıcaklığı monoton azalır; geç dolan hücrelerin // cephe sıcaklığı erken dolanlardan düşüktür (katılaşma baslangicT köprüsü). FIKO_TEST(sph_isi_duvar_soguma) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.35; g.gecikmeS = 0.0; g.sureS = 0.2; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = true; ayar.maksSure = 0.6; ayar.kareSayi = 20; ayar.dokumSicaklik = 700.0; ayar.duvarU = 20000.0; // güçlü kokil teması → hızlı, ölçülebilir soğuma const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); // Emisyon SONRASI ortalama sıcaklık monoton azalır (yeni sıcak partikül yok; // iletim ortalamayı korur, duvar Robin yalnız çeker). std::vector ortT; for (const auto& kare : s.kareler) if (kare.sure > g.sureS + 1e-9) ortT.push_back(kare.ortT); FIKO_KONTROL(ortT.size() >= 3); for (size_t i = 1; i < ortT.size(); ++i) FIKO_KONTROL(ortT[i] <= ortT[i - 1] + 0.02f); FIKO_KONTROL(ortT.back() < ortT.front() - 1.0f); // gerçek soğuma var // Cephe sıcaklığı fiziksel bantta ve duvar soğumasını görüyor. FIKO_KONTROL(s.cepheTMin < ayar.dokumSicaklik - 5.0); FIKO_KONTROL(s.cepheTMin >= ayar.kalipSicaklik); for (u32 c = 0; c < kafes.hucreSayi(); ++c) if (s.dolum.varisZamani[c] >= 0.0) { FIKO_KONTROL(s.dolum.varisT[c] <= ayar.dokumSicaklik + 1e-6); FIKO_KONTROL(s.dolum.varisT[c] >= ayar.kalipSicaklik - 1e-6); } // Geç dolan çeyreğin cephe T ortalaması erken çeyrekten düşük. const auto& sira = s.dolum.sira; FIKO_KONTROL(sira.size() > 16); const size_t ceyrek = sira.size() / 4; f64 ilkOrt = 0, sonOrt = 0; for (size_t i = 0; i < ceyrek; ++i) { ilkOrt += s.dolum.varisT[sira[i]]; sonOrt += s.dolum.varisT[sira[sira.size() - 1 - i]]; } FIKO_KONTROL(sonOrt / ceyrek < ilkOrt / ceyrek - 0.5); } // ---- (f) Gerçek cold-shut: iki soğuk cephe birleşir → işaret ---------------- // İki karşıt emitter; döküm sıcaklığı likidus−ΔT'nin ALTINDA (560 < 615−30) → // birleşme hücrelerinde iki tarafın T'si eşik altı → cold-shut. Aynı kurulum // sıcak metalle (700°C) → işaret YOK (cepheler sıcak kaynaşır). FIKO_TEST(sph_cold_shut) { const Alasim a = alasimAlSi7Mg(); auto coz = [&](f64 dokumT) { VoxelKafes kafes = kutuKafes(24.0, 8); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; for (f32 x : {5.0f, 19.0f}) { DokumGirisi g; g.ad = x < 12 ? "Sol" : "Sağ"; g.konum = {x, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.18; // iki giriş toplam ~%36 g.gecikmeS = 0.0; g.sureS = 0.15; g.capMm = 6; kur.girisler.push_back(g); } SphDolumAyar ayar; ayar.partikulYaricapMm = 1.8; ayar.deterministik = true; ayar.maksSure = 0.5; ayar.kareSayi = 10; ayar.dokumSicaklik = dokumT; ayar.coldShutDT = 30.0; return sphDolumCoz(kafes, kur, a, ayar); }; const SphDolumSonuc soguk = coz(560.0); // < likidus(615) − 30 FIKO_KONTROL(soguk.hazir); FIKO_KONTROL(soguk.dolum.coldShutSayi >= 1); const SphDolumSonuc sicak = coz(700.0); // cepheler sıcak → kaynaşır FIKO_KONTROL(sicak.hazir); FIKO_KONTROL(sicak.dolum.coldShutSayi == 0); } // ---- (g) Implicit viskozite (Weiler2018): katsayı 0.05 kararlı -------------- // Standard (explicit) viskozite >0.02'de patlıyordu; implicit çözücü yüksek // katsayıda enerji-sınırlı kalır: hızlar fiziksel bantta, kütle korunur. FIKO_TEST(sph_implicit_visko_kararli) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.25; g.gecikmeS = 0.0; g.sureS = 0.15; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = true; ayar.maksSure = 0.55; ayar.kareSayi = 11; ayar.viskozite = 0.05; // Standard'da patlayan rejim ayar.viskoziteImplicit = true; // σ KAPALI: saf viskozite kararlılık ölçümü (kohezyon hız alanını değiştirir). ayar.yuzeyGerilim = 0.0; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(std::abs(s.partikulKutleKg - g.kutleKg) < 0.10 * g.kutleKg); FIKO_KONTROL(s.maksHiz > 0.1 && s.maksHiz < 10.0); // enerji sınırlı (patlama yok) // Akış oturduktan sonra kütle sahnede sabit (sınırdan fırlama/sızma yok). i64 ref = -1, son = -1; for (u32 i = 0; i < s.kareler.size(); ++i) { if (ref < 0 && s.kareler[i].sure >= 0.35) ref = i; son = i; } FIKO_KONTROL(ref >= 0 && son > ref); const f64 mRef = kareToplam(s.kareler[(u32)ref]); FIKO_KONTROL(mRef > 0); FIKO_KONTROL(std::abs(kareToplam(s.kareler[(u32)son]) - mRef) <= 0.05 * mRef); } // ---- (h) Oksit/bifilm izleme (Campbell): hızlı jet üretir, yavaş dolum ~0 --- // Serbest yüzey partikülü (az akışkan komşu) hız > kritik (~0.5 m/s Al) iken // kalıcı "oksitli" işaretlenir; son konumları hücre oksit yoğunluğu alanına iner. FIKO_TEST(sph_oksit_izleme) { const Alasim a = alasimAlSi7Mg(); auto coz = [&](Vec3f konum, f64 kutlePay, f64 sureS, f64 capMm, f64 maksSure) { VoxelKafes kafes = kutuKafes(24.0, 8); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = konum; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * kutlePay; g.gecikmeS = 0.0; g.sureS = sureS; g.capMm = capMm; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.2; ayar.deterministik = true; ayar.maksSure = maksSure; ayar.kareSayi = 8; // σ KAPALI: bu test Campbell HIZ-KRİTİK oksit kuralını ölçer (hızlı jet // oksit üretir, yavaş dolum üretmez). σ açıkken kohezyon yüzey partikül // SAYISINI artırır (küreselleşme) → "serbest yüzey" (az komşu) hepsele- // mez, yanlış oksit işareti doğar. Ölçümün SAF hız kuralı olması için σ // kapatılır; σ etkisi ayrı testte (sph_yuzey_gerilimi) ölçülür. ayar.yuzeyGerilim = 0.0; return sphDolumCoz(kafes, kur, a, ayar); }; // Hızlı jet: dar kesit + kısa süre → v ≈ 2.5 m/s >> kritik → yoğun oksit. const SphDolumSonuc hizli = coz({12, 12, 24}, 0.4, 0.08, 5.0, 0.4); FIKO_KONTROL(hizli.hazir); FIKO_KONTROL(hizli.partikulSayi > 100); const f64 hizliOran = static_cast(hizli.oksitliPartikul) / hizli.partikulSayi; FIKO_KONTROL(hizliOran > 0.2); f64 oksitAlan = 0; for (f32 v : hizli.oksit) oksitAlan += v; FIKO_KONTROL(oksitAlan > 0); // Yavaş dolum: tabana yakın geniş giriş → çarpma dahil v < kritik → ~0 oksit. const SphDolumSonuc yavas = coz({12, 12, 4}, 0.35, 0.22, 12.0, 0.7); FIKO_KONTROL(yavas.hazir); FIKO_KONTROL(yavas.partikulSayi > 100); const f64 yavasOran = static_cast(yavas.oksitliPartikul) / yavas.partikulSayi; FIKO_KONTROL(yavasOran < 0.05); FIKO_KONTROL(hizliOran > 4.0 * yavasOran); } // ---- (i) Kare serisi → pürüzsüz yüzey animasyonu (scrubber yolunun ikizi) --- // dokum_penceresi SPH dalı: kare doluluk (u8→0..1) alanı, izoAltiIc=false, // izo = doluEsik ile Marching Cubes. Ara kare ile son kare FARKLI yüzey // üretmeli (animasyon); izo=0.5 sabiti kısmi-doluluk hücrelerini görünmez // kılıp erken kareleri boş bırakıyordu (regresyon). FIKO_TEST(sph_kare_animasyon_yuzey) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.3; g.gecikmeS = 0.0; g.sureS = 0.2; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = true; ayar.maksSure = 0.5; ayar.kareSayi = 10; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); FIKO_KONTROL(s.hazir); FIKO_KONTROL(s.kareler.size() >= 4); auto yuzey = [&](const SphDolumKare& kare) { std::vector alan(kare.doluluk.size()); for (size_t c = 0; c < alan.size(); ++c) alan[c] = kare.doluluk[c] / 255.0; SiviYuzeyAyar sa; sa.izoAltiIc = false; // doluluk > izo taraf sıvı (pencere yolu ikizi) return siviYuzeyKur(kafes, alan, (f32)ayar.doluEsik, sa); }; auto hacim = [](const MeshVeri& m) { f64 v = 0; for (size_t t = 0; t + 2 < m.indeksler.size(); t += 3) { auto p = [&](int c) { const auto& q = m.vertexler[m.indeksler[t + c]]; return Vec3d{q.konum[0], q.konum[1], q.konum[2]}; }; v += p(0).nokta(p(1).capraz(p(2))) / 6.0; } return v; }; // Ara kare: kütlenin %20-75'i teslim edilmişken (dolum sürüyor). const f64 sonKutle = kareToplam(s.kareler.back()); FIKO_KONTROL(sonKutle > 0); i64 ortaI = -1; for (u32 i = 0; i < s.kareler.size(); ++i) { const f64 m = kareToplam(s.kareler[i]); if (m >= 0.2 * sonKutle && m <= 0.75 * sonKutle) { ortaI = i; break; } } FIKO_KONTROL(ortaI >= 0); const SiviYuzeySonuc orta = yuzey(s.kareler[(u32)ortaI]); const SiviYuzeySonuc son = yuzey(s.kareler.back()); FIKO_KONTROL(orta.gecerli && orta.ucgenSayi > 0); // kısmi dolum GÖRÜNÜR FIKO_KONTROL(son.gecerli && son.ucgenSayi > 0); // Animasyon: son karedeki sıvı hacmi ara kareden belirgin büyük. const f64 vOrta = hacim(orta.mesh), vSon = hacim(son.mesh); FIKO_KONTROL(vOrta > 0 && vSon > 1.2 * vOrta); } // ---- (j) PERF mikro-benchmark: adım/sn + gerçekçi 40mm senaryo süresi ------- // Fizik doğrulaması DEĞİL (her zaman geçer) — teşhis çıktısı: küçük sabit-yük // throughput (adım/sn) + kullanıcının şikayet ettiği 40mm/~2000 partikül dolum // duvar-saati. stderr'e yazar; ctest --output-on-failure ya da doğrudan koşuda // görülür. Determinist KAPALI (gerçek çok-thread performans ölçülür). // ---- SPH maliyeti parça BOYUTUYLA ölçeklenmiyor (KÜÇÜK parça DAHA YAVAŞ) --- // "2mm küpü bile çok yavaş hesaplıyor" şikayetinin kökü. ÖLÇÜLDÜ 2026-07-15 // (768 partikül, maksSure=0.1, deterministik=true → TEK İŞ PARÇACIĞI): // // kenar h implicit adım süre ms/adım // 2mm 0.36mm 1 113 11034 ms 97.6 ← 7× pahalı // 20mm 3.60mm 1 118 3961 ms 33.6 // 200mm 36.00mm 1 172 2395 ms 13.9 // 2mm 0.36mm 0 927 7423 ms 8.0 // 20mm 3.60mm 0 919 15341 ms 16.7 // 200mm 36.00mm 0 260 2166 ms 8.3 // // 🚨 2026-07-16 DÜZELTME — YUKARIDAKİ implicit=0 SATIRLARI ÇÖP (süreleri gerçek, // ANLAMLARI yanlış). Eski yorum "2mm'de explicit DAHA HIZLI → varsayılan zararlı" // diyordu. Fizik de ölçülünce çürüdü: explicit o koşuda IRAKSIYOR — h=0.9mm'de // maksHız 75.5 m/s (implicit 0.306) ve kalıbı HİÇ doldurmuyor (dolan=0 vs 200). // Yani çalışan çözücü ıraksayanla kıyaslanmış. Bkz `sph_explicit_visko_iraksar`. // → viskoziteImplicit=true varsayılanı DOĞRU; h'ye göre otomatik seçme fikri ÖLÜ. // 🩺 DERS: perf ölçümünü kabul etmeden önce "sonuç hâlâ doğru mu?" diye sor — // duvar-saati ancak iki koşu AYNI FİZİĞİ üretiyorsa kıyaslanabilir. // // TEK GERÇEK KÖK NEDEN: // (1) Otomatik yarıçap r = h/4 = (bbox/çözünürlük)/4 → partikül ÇAPI parçayla // KÜÇÜLÜR → partikül sayısı boyuttan BAĞIMSIZ (768'de sabit). Küçük parça // modellemek ASLA ucuzlamaz. Çare: partikulYaricapMm'i FİZİKSEL ver. // Maliyet ∝ çözünürlük³ → perf kaldıracı BURASI, viskozite çözücüsü değil. // (2) [ÇÜRÜDÜ] "implicit CG h küçülünce patlıyor → kapat" — ms/adım 13.9→97.6 // artışı GERÇEK (şart sayısı ∝ dt·ν/h²), ama alternatifi yok: explicit // ıraksıyor. Pahalı ama DOĞRU olan tek yol. // // ⚠️ Mutlak süreler deterministik=true (tek thread) yüzünden şişkin; gerçek // kullanımda çok-thread. ORAN karşılaştırması geçerli (hepsi aynı ayarda). // ⚠️ Diğer sph testleri partikulYaricapMm=2.0 (elle) veriyor → OTO yolu bu // testten önce hiç ölçülmemişti. FIKO_TEST(sph_olcek_maliyeti_boyuttan_bagimsiz) { const Alasim a = alasimAlSi7Mg(); u32 oncekiPartikul = 0; for (f64 kenar : {2.0, 200.0}) { // uçlar yeter (20mm ölçümü yorumda) VoxelKafes kafes = kutuKafes(kenar, 10); const u32 nIc = icSay(kafes); const f64 vIc = nIc * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {f32(kenar * 0.5), f32(kenar * 0.5), f32(kenar)}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.35; g.gecikmeS = 0.0; g.sureS = 0.05; // AYNI döküm penceresi (boyuttan bağımsız) g.capMm = kenar * 0.5; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 0; // ← OTOMATİK (h/4): gerçek kullanımdaki yol ayar.deterministik = true; ayar.kareSayi = 4; ayar.maksSure = 0.1; const auto t0 = std::chrono::steady_clock::now(); const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); const f64 ms = std::chrono::duration(std::chrono::steady_clock::now() - t0) .count(); std::printf( " [OLCUM] kenar=%6.1fmm h=%7.4fmm partikul=%5u adim=%5llu sure=%8.1f ms" " (%.1f ms/adim)\n", kenar, kafes.h() * 1000.0, s.partikulSayi, (unsigned long long)s.adimSayi, ms, s.adimSayi ? ms / f64(s.adimSayi) : 0.0); FIKO_KONTROL(s.hazir); // ASIL İDDİA: partikül sayısı parça boyutundan BAĞIMSIZ (r=h/4 kuralı). // 100× boyut farkına rağmen aynı sayı çıkmalı → küçük parça ucuzlamaz. if (oncekiPartikul) FIKO_KONTROL(s.partikulSayi == oncekiPartikul); oncekiPartikul = s.partikulSayi; } FIKO_KONTROL(oncekiPartikul > 0); } FIKO_TEST(sph_perf_benchmark) { const Alasim a = alasimAlSi7Mg(); // (1) Sabit-yük throughput: ~500 partikül, kısa süre → adım/sn. { VoxelKafes kafes = kutuKafes(24.0, 8); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.3; g.gecikmeS = 0.0; g.sureS = 0.15; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 1.5; ayar.deterministik = false; ayar.maksSure = 0.4; ayar.kareSayi = 8; const auto t0 = std::chrono::steady_clock::now(); const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); const f64 sn = std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); std::fprintf(stderr, "[bench-throughput] partikul=%u adim=%llu sure=%.3fs adim/sn=%.1f\n", s.partikulSayi, (unsigned long long)s.adimSayi, sn, sn > 0 ? s.adimSayi / sn : 0.0); FIKO_KONTROL(s.hazir); } // (2) Gerçekçi 40mm senaryo — BASELINE (eski varsayılan) vs OPTİMİZE ayar, // aynı kurulumla kontrollü önce/sonra ölçümü. auto senaryo40 = [&](const char* etiket, bool baseline) { VoxelKafes kafes = kutuKafes(40.0, 12); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {20, 20, 40}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.6; // kavitenin %60'ı g.gecikmeS = 0.0; g.sureS = 0.35; g.capMm = 16; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 2.0; // ~2000 partikül mertebesi ayar.deterministik = false; ayar.maksSure = 0.9; ayar.kareSayi = 30; if (baseline) { // ESKİ VARSAYILANLARI geri yükle (SPlisHSPlasH stok + implicit visko) ayar.viskoziteImplicit = true; ayar.maksAdimDt = 0.004; ayar.cflFaktor = 0.5; ayar.basincHata = 0.01; ayar.basincMaksIter = 100; ayar.divergenceHata = 0.10; ayar.divergenceMaksIter = 100; } const auto t0 = std::chrono::steady_clock::now(); const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, ayar); const f64 sn = std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); std::fprintf(stderr, "[bench-40mm-%s] partikul=%u sinir=%u adim=%llu SURE=%.2fs " "(hedef <30s) adim/sn=%.1f\n", etiket, s.partikulSayi, s.sinirPartikulSayi, (unsigned long long)s.adimSayi, sn, sn > 0 ? s.adimSayi / sn : 0.0); FIKO_KONTROL(s.hazir); }; senaryo40("BASELINE", true); senaryo40("OPTIMIZE", false); } // ---- (d) İptal callback ------------------------------------------------------ FIKO_TEST(sph_iptal) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); DokumKurulum kur; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = 0.1; g.sureS = 0.3; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.maksSure = 5.0; u32 cagri = 0; const SphDolumSonuc s = sphDolumCoz( kafes, kur, a, ayar, [&](f64, const char*) { ++cagri; return false; }); FIKO_KONTROL(cagri >= 1); FIKO_KONTROL(s.iptal); FIKO_KONTROL(!s.hazir); FIKO_KONTROL(!s.dolum.hazir); FIKO_KONTROL(s.adimSayi < 200); } // ---- (i) Partikül kaydı: fizik dokunulmaz + hücre-altı detay geri gelir ---- // Bu testin VARLIK SEBEBİ: kare yalnız hücre-başı `doluluk` saklıyordu → // jet/sıçrama/damla 1 hücreye eziliyordu ("akmıyor" değil, akış görüntüye // ulaşmıyordu). Kayıt EKLEMELİ olmalı: doluluk alanı bit-birebir korunmalı. FIKO_TEST(sph_partikul_kaydi_hucre_alti_detay) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.25; g.gecikmeS = 0.05; g.sureS = 0.15; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar taban; taban.partikulYaricapMm = 1.5; taban.deterministik = true; // bit-düzeyi tekrarlanabilir → A/B kıyası geçerli taban.maksSure = 0.6; taban.kareSayi = 12; // (1) Varsayılan = ESKİ DAVRANIŞ: kare partikül taşımaz. const SphDolumSonuc kapali = sphDolumCoz(kafes, kur, a, taban); FIKO_KONTROL(kapali.hazir); FIKO_KONTROL(!kapali.kareler.empty()); for (const auto& k : kapali.kareler) FIKO_KONTROL(k.partikul.empty()); SphDolumAyar acik = taban; acik.partikulKaydet = true; const SphDolumSonuc s = sphDolumCoz(kafes, kur, a, acik); FIKO_KONTROL(s.hazir); // (2) FİZİK DOKUNULMADI: kayıt yalnız okur → doluluk alanı BİREBİR aynı. // (Kayıt fiziğe sızarsa katılaşma zinciri de sessizce kayar — asıl koruma bu.) FIKO_KONTROL(s.kareler.size() == kapali.kareler.size()); for (u32 i = 0; i < s.kareler.size(); ++i) FIKO_KONTROL(s.kareler[i].doluluk == kapali.kareler[i].doluluk); FIKO_KONTROL(s.partikulSayi == kapali.partikulSayi); // (3) Partikül gerçekten kaydedildi + konumlar sonlu ve kafes alanında. const auto& son = s.kareler.back(); FIKO_KONTROL(son.partikul.size() > 100); for (const auto& p : son.partikul) { FIKO_KONTROL(std::isfinite(p.konum.x) && std::isfinite(p.konum.y) && std::isfinite(p.konum.z)); FIKO_KONTROL(p.konum.x > -2.0f && p.konum.x < 26.0f); FIKO_KONTROL(p.konum.y > -2.0f && p.konum.y < 26.0f); FIKO_KONTROL(p.konum.z > -2.0f && p.konum.z < 26.0f); FIKO_KONTROL(p.hiz >= 0.0f && std::isfinite(p.hiz)); } // (4) ⭐ ASIL İDDİA: binning'in ATTIĞI hücre-altı yapı kayıtta VAR. // Aynı hücreye düşen partiküller ayrı konumlarda → `doluluk` bu farkı tek // bir sayıya eziyordu. Ölçüm: hücre içi en büyük partikül-çifti mesafesi. const Vec3d m0 = kafes.merkez(0, 0, 0); const f64 h = kafes.merkez(1, 0, 0).x - m0.x; const Vec3d kok{m0.x - 0.5 * h, m0.y - 0.5 * h, m0.z - 0.5 * h}; std::vector> kova(kafes.hucreSayi()); for (u32 p = 0; p < son.partikul.size(); ++p) { const Vec3f& x = son.partikul[p].konum; const f64 fi = (x.x - kok.x) / h, fj = (x.y - kok.y) / h, fk = (x.z - kok.z) / h; if (fi < 0 || fj < 0 || fk < 0) continue; const u32 i = (u32)fi, j = (u32)fj, k = (u32)fk; if (i >= kafes.nx() || j >= kafes.ny() || k >= kafes.nz()) continue; kova[kafes.indeks(i, j, k)].push_back(p); } u32 coklu = 0; f64 enBuyukYayilim = 0; for (const auto& kv : kova) { if (kv.size() < 2) continue; ++coklu; for (u32 x = 0; x < kv.size(); ++x) for (u32 y = x + 1; y < kv.size(); ++y) { const Vec3f& A = son.partikul[kv[x]].konum; const Vec3f& B = son.partikul[kv[y]].konum; const f64 dx = A.x - B.x, dy = A.y - B.y, dz = A.z - B.z; enBuyukYayilim = std::max(enBuyukYayilim, std::sqrt(dx * dx + dy * dy + dz * dz)); } } // Çok-partiküllü hücre olmalı (aksi halde ölçüm anlamsız) ve içlerinde // hücre boyu mertebesinde gerçek yapı bulunmalı. // ÖLÇÜLDÜ (24mm kutu, 8 hücre → h=5.40mm): çoklu hücre 32, en büyük // hücre-içi yayılım 5.909mm = 1.09·h (köşegen mertebesi). Yani binning // TAM BİR HÜCRE dolusu yapıyı tek u8 sayıya eziyordu. FIKO_KONTROL(coklu > 10); FIKO_KONTROL(enBuyukYayilim > 0.4 * h); std::printf("[olcum] hucre h=%.2fmm coklu-hucre=%u hucre-ici-yayilim=%.3fmm (%.2f h)\n", h, coklu, enBuyukYayilim, enBuyukYayilim / h); // (5) ÇALKANTI: partiküller kareler arası GERÇEKTEN yer değiştiriyor. // (Konum kaydı hareketsiz olsaydı bulut donuk görünürdü — "akmıyor" algısı.) i64 a1 = -1, a2 = -1; for (u32 i = 0; i < s.kareler.size(); ++i) if (s.kareler[i].partikul.size() > 50) { if (a1 < 0) a1 = i; a2 = i; } FIKO_KONTROL(a1 >= 0 && a2 > a1); const auto& kA = s.kareler[(u32)a1]; const auto& kB = s.kareler[(u32)a2]; const u32 ortak = (u32)std::min(kA.partikul.size(), kB.partikul.size()); f64 topYer = 0; for (u32 p = 0; p < ortak; ++p) { const Vec3f& A = kA.partikul[p].konum; const Vec3f& B = kB.partikul[p].konum; const f64 dx = A.x - B.x, dy = A.y - B.y, dz = A.z - B.z; topYer += std::sqrt(dx * dx + dy * dy + dz * dz); } const f64 ortYer = ortak > 0 ? topYer / ortak : 0; // ÖLÇÜLDÜ (kare 0.150s → 0.500s): ortalama yer değiştirme 5.282mm = 0.98·h. FIKO_KONTROL(ortYer > 0.5 * h); std::printf("[olcum] kare %.3fs->%.3fs ortalama partikul yer degistirme=%.3fmm (%.2f h)\n", kA.sure, kB.sure, ortYer, ortYer / h); } // ---- (j) Partikül alt-örneklemesi: bellek böler, fizik dokunulmaz ---------- FIKO_TEST(sph_partikul_alt_ornek) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = 0.05; g.gecikmeS = 0.05; g.sureS = 0.15; g.capMm = 10; kur.girisler.push_back(g); SphDolumAyar taban; taban.partikulYaricapMm = 1.5; taban.deterministik = true; taban.maksSure = 0.4; taban.kareSayi = 8; taban.partikulKaydet = true; SphDolumAyar dort = taban; dort.partikulAltOrnek = 4; const SphDolumSonuc s1 = sphDolumCoz(kafes, kur, a, taban); const SphDolumSonuc s4 = sphDolumCoz(kafes, kur, a, dort); FIKO_KONTROL(s1.hazir && s4.hazir); FIKO_KONTROL(s1.kareler.size() == s4.kareler.size()); for (u32 i = 0; i < s1.kareler.size(); ++i) { // Fizik aynı (doluluk birebir); yalnız kayıt yoğunluğu seyrelir. FIKO_KONTROL(s1.kareler[i].doluluk == s4.kareler[i].doluluk); const size_t n1 = s1.kareler[i].partikul.size(); const size_t n4 = s4.kareler[i].partikul.size(); FIKO_KONTROL(n4 == (n1 + 3) / 4); // Alt-örnek n'de BİR alır → ilk partikül aynı (kayma yok). if (n1 > 0) FIKO_KONTROL(s1.kareler[i].partikul[0].konum.z == s4.kareler[i].partikul[0].konum.z); } } // ---- (j) explicit viskozite IRAKSAR → implicit varsayılanı KORUNMALI ------- // ⚠️ BU TEST BİR VARSAYILANI KORUYOR. "viskoziteImplicit=true pahalı, kapatıp // hızlandıralım" fikri ÖLÇÜLDÜ ve ÇÜRÜDÜ (2026-07-16). // // MEKANİZMA (ders kitabı): explicit viskozite difüzyon kararlılık sınırı ister, // dt ≲ h²/(2ν). Kod `CFL_METHOD = ENUM_CFL_STANDARD` kullanıyor — o YALNIZ // advection koşulu (dt ∝ h/v_max), difüzyon terimi YOK. h küçülünce difüzyon // sınırı KARESEL daralır, CFL ise yalnız doğrusal → explicit kaçınılmaz ıraksar. // // ÖLÇÜLDÜ (768 partikül, oto yarıçap r=h/4, deterministik, maksSure=0.1): // h(mm) explicit maksHız implicit maksHız explicit dolan / implicit dolan // 0.360 29.3 m/s 0.459 m/s 0 / 192 // 0.900 75.5 m/s 0.306 m/s 0 / 200 // 3.600 306.3 m/s 1.001 m/s 103 / 216 // 9.000 5.4 m/s 2.516 m/s 276 / 224 // Sıvı alüminyum kritik giriş hızı ~0.5 m/s (Campbell) → 306 m/s FİZİKSEL // SAÇMALIK; explicit kalıbı doldurmuyor bile (dolan=0), partikülleri savuruyor. // // 🩺 ASIL DERS: eski kayıt "explicit 2mm'de 1.5× HIZLI (7423 vs 11034 ms)" // diyordu → o kıyas ÇÖPTÜ: çalışan bir çözücüyle IRAKSAYAN birini kıyaslıyordu. // Duvar-saati ancak iki koşu AYNI FİZİĞİ üretiyorsa kıyaslanabilir. Bir perf // ölçümünü kabul etmeden önce "sonuç hâlâ doğru mu?" diye sor. // → Perf kaldıracı viskozite çözücüsü DEĞİL; partikül sayısı (oto yarıçap r=h/4 // → sayı boyuttan bağımsız). Bkz [[fikatia-sph-olcek-maliyeti]] KÖK NEDEN 1. FIKO_TEST(sph_explicit_visko_iraksar) { const f64 kenar = 5.0; // h = 0.90mm — oto yarıçap yolu (üretimde kullanılan) const Alasim a = alasimAlSi7Mg(); auto kos = [&](bool implicit) { VoxelKafes kafes = kutuKafes(kenar, 10); const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {f32(kenar * 0.5), f32(kenar * 0.5), f32(kenar)}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.35; g.gecikmeS = 0.0; g.sureS = 0.05; g.capMm = kenar * 0.5; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 0; // OTO (h/4) — üretimdeki yol ayar.deterministik = true; ayar.kareSayi = 4; ayar.maksSure = 0.04; // ıraksama çok erken belli olur → test kısa kalsın ayar.viskoziteImplicit = implicit; // σ KAPALI: bu test SAF viskozite çözücüsü karşılaştırmasıdır // (implicit vs explicit). Kohezyon eklemek hız alanını değiştirir ve // iki kolun karşılaştırmasını bulanıklaştırır → yalnız viskozite ölçülür. ayar.yuzeyGerilim = 0.0; return sphDolumCoz(kafes, kur, a, ayar); }; const SphDolumSonuc imp = kos(true); FIKO_KONTROL(imp.hazir); // Implicit FİZİKSEL: 5mm'den serbest düşüş v=√(2gh)=0.31 m/s mertebesi. // 5 m/s cömert bir üst sınır (ölçüldü: 0.306 m/s @ maksSure=0.1). FIKO_KONTROL(imp.maksHiz > 0.0 && imp.maksHiz < 5.0); const SphDolumSonuc exp_ = kos(false); FIKO_KONTROL(exp_.hazir); // Explicit IRAKSAR: hız fiziksel mertebenin ONLARCA katına fırlar. // (Bu kontrol kırmızıya dönerse explicit DÜZELMİŞ demektir — yukarıdaki // ölçüm tablosunu ve varsayılan gerekçesini GÜNCELLE, testi susturma.) FIKO_KONTROL(exp_.maksHiz > 5.0 * imp.maksHiz); std::printf(" [OLCUM] h=%.3fmm visko: implicit maksHiz=%.3f m/s (dolan=%u) | " "explicit maksHiz=%.3f m/s (dolan=%u) -> %.1fx IRAKSAMA\n", kenar * 0.18, imp.maksHiz, imp.dolum.dolanHucre, exp_.maksHiz, exp_.dolum.dolanHucre, imp.maksHiz > 0 ? exp_.maksHiz / imp.maksHiz : 0.0); } // ---- (k) Partikül yarıçapı h/2'ye KIRPILIR → SPH maliyeti çözünürlüğe ZİNCİRLİ // Fikri'nin iki isteği ÇATIŞIYOR ve sebebi tek satır (`sph_dolum.cpp` clamp): // "cidarı gör" → çözünürlük ↑ → h ↓ // "su gibi aksın" → SPH makul sürede bitsin // ama r ≤ h/2 zorunlu → h ↓ ⇒ partikül sayısı ↑ ⇒ SPH patlar. // // ÖLÇÜLDÜ (40mm küp, oto yarıçap r=h/4, çok-thread, maksSure=0.05): // res h(mm) r(mm) partikul adim sure dolan // 8 9.000 2.250 228 85 104.3 ms 84 // 16 4.500 1.125 1794 116 580.0 ms 354 // 32 2.250 0.563 20480 378 15091.3 ms 2823 // → res ×4 ⇒ partikül ×89.8, SÜRE ×144.7 (adım da 4.4× arttı: h ↓ → CFL dt ↓) // → maliyet ≈ res³·adım ≈ res^3.6. Tur 1'in önerdiği res 40→180 (4.5×) SPH'yi // SAATLERE çıkarır — geometri tavsiyesi SPH kipini öldürüyor. // // KIRPMA SESSİZ (ölçüldü, res=32 → tavan r=1.125mm): // istenen r=0.5625mm → 20480 partikül, 14845 ms // istenen r=1.1250mm → 2553 partikül, 1230 ms // istenen r=4.0000mm → 2553 partikül, 1231 ms ← 4mm istendi, 1.125mm alındı // Panelde "Partikül yarıçapı" 10mm'e kadar yazılabiliyor ama h/2 üstü HİÇBİR // ŞEY yapmıyordu. Artık `sphYaricapKirpildiMi` ile görünür. // // KIRPMA KEYFİ DEĞİL: binning `doluPay = partikülHacmi/hücreHacmi` istiyor → // partikül hücreden büyük olamaz (nokta-binning splat yapmıyor). Zinciri kırmak // = binning'i splat'a çevirmek (fizik sözleşmesi değişir, yapılmadı). FIKO_TEST(sph_yaricap_kirpma_tek_kaynak) { const f64 kenar = 40.0; VoxelKafes kafes = kutuKafes(kenar, 16); // h = 4.5mm → tavan r = 2.25mm const f64 hMm = kafes.h() * 1000.0; FIKO_KONTROL(std::abs(hMm - 4.5) < 1e-6); // (1) OTO (0) → h/4. Kırpılmış SAYILMAZ (istenen zaten tavanın altında). SphDolumAyar oto; oto.partikulYaricapMm = 0; FIKO_KONTROL(std::abs(sphEtkinYaricapM(kafes, oto) * 1000.0 - hMm * 0.25) < 1e-9); FIKO_KONTROL(!sphYaricapKirpildiMi(kafes, oto)); // (2) Tavan ALTI istek AYNEN alınır (fiziksel yarıçap gerçekten geçerli). SphDolumAyar kucuk; kucuk.partikulYaricapMm = 1.0; FIKO_KONTROL(std::abs(sphEtkinYaricapM(kafes, kucuk) * 1000.0 - 1.0) < 1e-9); FIKO_KONTROL(!sphYaricapKirpildiMi(kafes, kucuk)); // (3) ⭐ Tavan ÜSTÜ istek SESSİZCE kırpılır → h/2. Fikri 4mm yazsa 2.25mm alır. SphDolumAyar buyuk; buyuk.partikulYaricapMm = 10.0; FIKO_KONTROL(std::abs(sphEtkinYaricapM(kafes, buyuk) * 1000.0 - hMm * 0.5) < 1e-9); FIKO_KONTROL(sphYaricapKirpildiMi(kafes, buyuk)); // artık GÖRÜNÜR // (4) Geçersiz kafes uydurma değer üretmez. VoxelKafes bos; FIKO_KONTROL(sphEtkinYaricapM(bos, oto) == 0.0); FIKO_KONTROL(!sphYaricapKirpildiMi(bos, buyuk)); // (5) ⭐ TEK KAYNAK KANITI: fonksiyon ÇÖZÜCÜNÜN gerçekten kullandığı yarıçapı // veriyor mu? Formül/çözücü ayrışırsa panel yanlış sayı gösterir (Tur 1'deki // formül-vs-kur() tuzağının aynısı). Kırpma tavanındaki iki ayrı istek AYNI // partikül sayısını vermeli — ölçüldü: 4mm ve 2.25mm → aynı sayı. const Alasim a = alasimAlSi7Mg(); auto partikulSay = [&](f64 rmm) { const f64 vIc = icSay(kafes) * kafes.hucreHacim(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {20, 20, 40}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * vIc * 0.35; g.gecikmeS = 0.0; g.sureS = 0.05; g.capMm = 20; kur.girisler.push_back(g); SphDolumAyar ay; ay.partikulYaricapMm = rmm; ay.deterministik = true; ay.kareSayi = 2; ay.maksSure = 0.02; // yalnız emisyon lazım (sayı), akışı bitirmeye gerek yok return sphDolumCoz(kafes, kur, a, ay).partikulSayi; }; const u32 nTavan = partikulSay(hMm * 0.5); // tam tavan const u32 nAsiri = partikulSay(10.0); // tavan üstü → kırpılır FIKO_KONTROL(nTavan > 0); FIKO_KONTROL(nTavan == nAsiri); // kırpma çözücüde de aynı yerde std::printf(" [OLCUM] h=%.3fmm tavan r=%.3fmm: istenen 2.25mm -> %u partikul, " "istenen 10mm -> %u partikul (SESSIZCE kirpildi)\n", hMm, hMm * 0.5, nTavan, nAsiri); } // ---- Emitter havuz planı: maksPartikul kırpması artık GÖRÜNÜR -------------- // "Büyük kalıp SPH'de dolmuyor"un ikinci yarısı: gereken partikül sayısı // maksPartikul tavanını aşınca emitter havuz dolunca SUSUYORDU — dökülen hacim // kaviteyi karşılayamaz, doluluk sessizce yarım kalır. sphEmitPlani TEK KAYNAK // (CPU + GPU + panel); bu test planın çözücünün gerçek davranışını verdiğini // partikül sayısı/kütle/doluluk A/B'siyle kilitler (yarıçap testi kalıbı). FIKO_TEST(sph_emit_plani_tavan_kirpma) { VoxelKafes kafes = kutuKafes(24.0, 8); const Alasim a = alasimAlSi7Mg(); DokumKurulum kur; kur.graviteYon = {0, 0, -1}; DokumGirisi g; g.konum = {12, 12, 24}; g.yon = {0, 0, -1}; g.kutleKg = a.yogunluk * icSay(kafes) * kafes.hucreHacim(); // kaviteyi karşılar g.sureS = 0.3; g.capMm = 14; kur.girisler.push_back(g); SphDolumAyar ayar; ayar.partikulYaricapMm = 2.0; ayar.deterministik = true; ayar.maksSure = 0.5; ayar.kareSayi = 8; // (1) Plan formül sözleşmesi: gerekli = 1.3·V/Vp, Vp = 0.8·(2r)³. const SphEmitPlan plan = sphEmitPlani(kafes, kur, a, ayar); const f64 r = sphEtkinYaricapM(kafes, ayar); const f64 vp = 0.8 * (2 * r) * (2 * r) * (2 * r); FIKO_KONTROL(std::abs(plan.toplamHacimM3 - g.kutleKg / a.yogunluk) < 1e-12); FIKO_KONTROL(std::abs(plan.gerekliPartikul - 1.3 * plan.toplamHacimM3 / vp) < 1e-9); FIKO_KONTROL(!plan.kirpildi); FIKO_KONTROL(plan.havuz >= 64 && std::abs((f64)plan.havuz - plan.gerekliPartikul) <= 1.0); // (2) Tavan gereğin altına inince plan kırpıldığını SÖYLÜYOR. SphDolumAyar dar = ayar; dar.maksPartikul = plan.havuz / 4; const SphEmitPlan planDar = sphEmitPlani(kafes, kur, a, dar); FIKO_KONTROL(planDar.kirpildi && planDar.havuz == dar.maksPartikul); // (3) ⭐ TEK KAYNAK KANITI: çözücü gerçekten o havuzla sınırlı — yayınlanan // kütle ~havuz oranında düşer, doluluk tavansız koşunun ALTINDA kalır // (sessiz yarım-dolum mekanizmasının kendisi). const SphDolumSonuc tam = sphDolumCoz(kafes, kur, a, ayar); const SphDolumSonuc kisik = sphDolumCoz(kafes, kur, a, dar); FIKO_KONTROL(tam.hazir && kisik.hazir); FIKO_KONTROL(kisik.partikulSayi <= dar.maksPartikul); FIKO_KONTROL(kisik.partikulKutleKg < 0.5 * tam.partikulKutleKg); FIKO_KONTROL(kisik.dolum.dolanHucre < tam.dolum.dolanHucre); std::printf(" [OLCUM] gerekli %.0f, havuz %u vs %u -> kutle %.3f vs %.3f kg, " "dolan %u vs %u hucre\n", plan.gerekliPartikul, plan.havuz, planDar.havuz, tam.partikulKutleKg, kisik.partikulKutleKg, tam.dolum.dolanHucre, kisik.dolum.dolanHucre); }