सीयूडीए पर नवियर-स्टोक्स समीकरण और द्रव सिमुलेशन

हाय, हैब्र। इस लेख में, हम एक असंगत तरल पदार्थ के लिए नवियर-स्टोक्स समीकरण से निपटेंगे, संख्यात्मक रूप से इसे हल करेंगे, और एक सुंदर सिमुलेशन बनायेंगे जो CUDA पर समानांतर कंप्यूटिंग द्वारा काम करता है। मुख्य लक्ष्य यह दिखाना है कि आप मॉडलिंग तरल पदार्थ और गैसों की समस्या को हल करते समय गणित को समीकरण में अंतर्निहित कैसे लागू कर सकते हैं।



चेतावनी
लेख में बहुत सारा गणित है, इसलिए जो लोग इस मुद्दे के तकनीकी पक्ष में रुचि रखते हैं वे सीधे एल्गोरिथम के कार्यान्वयन अनुभाग पर जा सकते हैं। हालाँकि, मैं फिर भी यह सलाह दूंगा कि आप पूरा लेख पढ़ें और समाधान के सिद्धांत को समझने की कोशिश करें। यदि आपके पास पढ़ने के अंत तक कोई प्रश्न है, तो मुझे पोस्ट पर टिप्पणियों में उनका जवाब देने में खुशी होगी।

नोट: यदि आप मोबाइल डिवाइस से Habr पढ़ रहे हैं, और आप सूत्र नहीं देखते हैं, तो साइट के पूर्ण संस्करण का उपयोग करें

एक असंगत द्रव के लिए नवियर-स्टोक्स समीकरण


{{आंशिक {\ bf \ vec {u}} \ ओवर {\ आंशिक t}} = - - - ({\ bf \ vec {u}} \ cdot \ nabla) {\ bf \ vec {u}} {{1 \ over \ rho} \ nabla {\ bf p} + \ nu \ nabla ^ 2 {\ bf \ vec {u}} + {\ bf \ vec {F}}

{\ आंशिक {\ bf \ vec {u}} \ over {\ आंशिक t}} = - - ({\ bf \ vec {u}} \ cdot \ nabla) {\ bf \ vec {u}} {{1 \ over \ rho} \ nabla {\ bf p} + \ nu \ nabla ^ 2 {\ bf \ vec {u}} + {\ bf \ vec {F}}


मुझे लगता है कि हर कोई कम से कम एक बार इस समीकरण के बारे में सुनता है, कुछ, शायद विश्लेषणात्मक रूप से इसके विशेष मामलों को भी हल करते हैं, लेकिन सामान्य शब्दों में यह समस्या अब तक अनसुलझी है। बेशक, हम इस लेख में सहस्राब्दी समस्या को हल करने का लक्ष्य निर्धारित नहीं करते हैं, हालांकि हम अभी भी इसके लिए पुनरावृत्ति विधि को लागू कर सकते हैं। लेकिन शुरुआत के लिए, आइए इस सूत्र में संकेतन देखें।

परंपरागत रूप से, नवियर-स्टोक्स समीकरण को पांच भागों में विभाजित किया जा सकता है:

  •  ि वी सी यूवी आर ि टी     - एक बिंदु पर द्रव वेग के परिवर्तन की दर को दर्शाता है (हम अपने सिमुलेशन में प्रत्येक कण के लिए इस पर विचार करेंगे)।
  • - ({\ bf \ vec {u}} \ cdot \ nabla) {\ bf \ vec {}} - अंतरिक्ष में द्रव की गति।
  • - 1 वी आर आर एच    एन बी एल बी एफ पी   क्या कण पर दबाव डाला गया है (यहां)  आर एच - द्रव घनत्व गुणांक)।
  •  nu nabla2 bf vecu - माध्यम की चिपचिपाहट (यह जितना बड़ा होता है, उतना ही मजबूत तरल अपने हिस्से पर लागू होने वाले बल का प्रतिरोध करता है),  n - चिपचिपापन गुणांक)।
  •  bf vecF - बाहरी बल जो हम द्रव पर लागू करते हैं (हमारे मामले में, बल बहुत विशिष्ट भूमिका निभाएगा - यह उपयोगकर्ता द्वारा किए गए कार्यों को प्रतिबिंबित करेगा।

इसके अलावा, चूंकि हम एक असंगत और सजातीय द्रव के मामले पर विचार करेंगे, हमारे पास एक और समीकरण है:  nabla cdot bf vecu=0 । पर्यावरण में ऊर्जा निरंतर है, कहीं नहीं जाती है, कहीं से नहीं आती है।

उन सभी पाठकों को वंचित करना गलत होगा जो वेक्टर विश्लेषण से परिचित नहीं हैं, इसलिए, एक ही समय में और संक्षेप में उन सभी ऑपरेटरों के माध्यम से जाते हैं जो समीकरण में मौजूद हैं (हालांकि, मैं दृढ़ता से यह याद रखने की सलाह देता हूं कि व्युत्पन्न, अंतर और वेक्टर क्या हैं, क्योंकि वे सभी को रेखांकित करते हैं। नीचे क्या चर्चा की जाएगी)।

हम नबला ऑपरेटर के साथ शुरू करते हैं, जो इस तरह के एक वेक्टर है (हमारे मामले में, यह दो-घटक होगा, क्योंकि हम तरल पदार्थ को दो-आयामी स्थान में मॉडल करेंगे):

 n= pmatrix ि  िx, ि  िy pmatrix


नबला ऑपरेटर एक वेक्टर अंतर ऑपरेटर है और इसे स्केलर फ़ंक्शन और वेक्टर वेक्टर दोनों पर लागू किया जा सकता है। स्केलर के मामले में, हमें फ़ंक्शन (इसके आंशिक डेरिवेटिव के वेक्टर) का ग्रेडिएंट मिलता है, और वेक्टर के मामले में, कुल्हाड़ियों के साथ आंशिक डेरिवेटिव का योग। इस ऑपरेटर की मुख्य विशेषता यह है कि इसके माध्यम से आप वेक्टर विश्लेषण के मुख्य संचालन - ग्रेड ( ग्रेडिएंट ), div ( विचलन ), रोट ( रोटर ) को व्यक्त कर सकते हैं  n2 ( लाप्लास ऑपरेटर )। यह तुरंत ध्यान देने योग्य है कि अभिव्यक्ति ( bf vecu cdot nabla) bf vec$ नहीं करने के लिए नहीं ( nabla cdot bf vecu) bf vec$ - नबला संचालक के पास कम्यूटिटी नहीं है।

जैसा कि हम बाद में देखेंगे, इन अभिव्यक्तियों को एक असतत स्थान पर ले जाते समय विशेष रूप से सरल किया जाता है, जिसमें हम सभी गणनाओं को अंजाम देंगे, इसलिए यदि आप इस सब के साथ क्या करें, इस बारे में बहुत स्पष्ट नहीं हैं, तो चिंतित न हों। कार्य को कई भागों में विभाजित करने के बाद, हम क्रमिक रूप से उनमें से प्रत्येक को हल करेंगे और यह सब हमारे पर्यावरण के लिए कई कार्यों के अनुक्रमिक अनुप्रयोग के रूप में प्रस्तुत करेंगे।

नवियर-स्टोक्स समीकरण का संख्यात्मक समाधान


कार्यक्रम में हमारे तरल पदार्थ का प्रतिनिधित्व करने के लिए, हमें समय पर मनमाने ढंग से प्रत्येक द्रव कण की स्थिति का गणितीय प्रतिनिधित्व प्राप्त करने की आवश्यकता है। इसके लिए सबसे सुविधाजनक तरीका एक समन्वय विमान के रूप में अपने राज्य को संग्रहीत करने वाले कणों का एक वेक्टर क्षेत्र बनाना है:

छवि

हमारे दो-आयामी सरणी के प्रत्येक सेल में, हम एक समय में कण वेग को संग्रहीत करेंगे t: bf vecu=u( bf vecx,t), bf vecx= startpmatrixx,y endpmrrix , और कणों के बीच की दूरी को निरूपित किया जाता है  x और  y क्रमशः। कोड में, यह हमारे लिए प्रत्येक समीकरण के गति मान को बदलने के लिए पर्याप्त होगा, कई समीकरणों के एक सेट को हल करता है।

अब हम ग्रेडिएंट, डाइवरेज और लाप्लास ऑपरेटर को हमारे समन्वय ग्रिड के रूप में लेते हैं। i,j - सरणी में सूचकांक, \ bf \ vec {u} _ {(x)}, \ vec {u} _ {(y)) - वेक्टर से संबंधित घटकों को लेते हुए):
ऑपरेटरपरिभाषाअसतत एनालॉग
स्नातक nabla bfp= startpmatrix ि bfp over िx, ि bfp over िy pmatrixpi+1,jpi1,j over2 x,pi,j+1pi,j1 over2 deltay
div\ nabla \ cdot \ bf \ vec {u} = {{\ _ u u \ _ \ _ आंशिक x} + {\ n आंशिक u \ over \ आंशिक y}}  vecu(x)i+1,j vecu(x)i1,j over2 x+ vecu(y)i,j+1 vecu(y)i,j1 over2 deltay
 bf Delta bf nabla2p= ि2p over िx2+ ि2p over िy2pi+1,j2pi,j+pi1,j over((x)2+pi,j+12pi,j+pi,j1 over( deltay)2
सड़ांध bf nabla  vecu=ि vecu over िy ि vecu over िx vecu(y)i,j+1 vecu(y)i,j1 over2 deltay vecu(x)i+1,j vecu(x)i1,j over2 deltax

यदि हम ऐसा मानते हैं तो हम वेक्टर ऑपरेटरों के असतत फार्मूले को और सरल बना सकते हैं  x= y= । यह धारणा एल्गोरिदम की सटीकता को बहुत प्रभावित नहीं करेगी, हालांकि, यह प्रति पुनरावृत्ति संचालन की संख्या को कम करती है, और आम तौर पर आंखों के लिए अभिव्यक्ति को अधिक सुखद बनाती है।

कण आंदोलन


ये कथन तभी काम करते हैं, जब हम वर्तमान में विचार के तहत निकटतम कणों को पा सकते हैं। उन्हें खोजने के साथ जुड़े सभी संभावित लागतों को कम करने के लिए, हम उनके आंदोलन को ट्रैक नहीं करेंगे, लेकिन जहां कण पुनरावृत्ति की शुरुआत से आगे आने वाले समय में आंदोलन के प्रक्षेपवक्र का अनुमान लगाते हैं (दूसरे शब्दों में, वेग वेक्टर समय को बदलने से घटाते हैं) वर्तमान स्थिति)। सरणी के प्रत्येक तत्व के लिए इस तकनीक का उपयोग करते हुए, हम यह सुनिश्चित करेंगे कि किसी भी कण में "पड़ोसी" होगा:

छवि

लगा रहे हैं q - एक सरणी तत्व जो कण की स्थिति को संग्रहीत करता है, हम समय के साथ इसकी स्थिति की गणना के लिए निम्न सूत्र प्राप्त करते हैं   (हम मानते हैं कि त्वरण और दबाव के रूप में सभी आवश्यक मापदंडों की गणना पहले ही की जा चुकी है):

q( vec bfx,t+ deltat)=q( bf vecx bf vecu "=,t)


हम तुरंत ध्यान दें कि पर्याप्त रूप से छोटे के लिए   और हम सेल की सीमा से बाहर कभी नहीं जा सकते हैं, इसलिए उपयोगकर्ता को कणों को देने के लिए सही गति चुनना बहुत महत्वपूर्ण है।

सेल की सीमा से टकराने या गैर-पूर्णांक निर्देशांक के मामले में सटीकता की हानि से बचने के लिए, हम चार निकटतम कणों के राज्यों के बिलिनियर प्रक्षेप को अंजाम देंगे और इसे बिंदु पर सही मान के रूप में लेंगे। सिद्धांत रूप में, ऐसी विधि व्यावहारिक रूप से अनुकरण की सटीकता को कम नहीं करेगी, और साथ ही इसे लागू करना काफी सरल है, इसलिए हम इसका उपयोग करेंगे।

चिपचिपापन


प्रत्येक तरल में एक निश्चित चिपचिपाहट होती है, उसके भागों पर बाहरी बलों के प्रभाव को रोकने की क्षमता (शहद और पानी एक अच्छा उदाहरण होगा, कुछ मामलों में उनकी चिपचिपाहट गुणांक परिमाण के एक क्रम से भिन्न होती है)। चिपचिपापन सीधे तरल द्वारा अधिग्रहित त्वरण को प्रभावित करता है, और नीचे सूत्र द्वारा व्यक्त किया जा सकता है, अगर संक्षिप्तता के लिए हम थोड़ी देर के लिए अन्य शर्तों को छोड़ देते हैं:

 ि vec bfu over िt= nu nabla2 bf vecu

। इस स्थिति में, गति के लिए पुनरावृत्ति समीकरण निम्न रूप लेता है:

u( bf vecx,t+ deltat)=u( bf vecx,t)+ nu deltat nabla2 bfvecu


हम इस समानता को थोड़ा बदल देंगे, इसे रूप में लाएंगे  bfA vecx= vecb (रैखिक समीकरणों की एक प्रणाली का मानक रूप):

({\ bf I} - \ nu \ delta t \ nabla ^ 2) u ({\ bf \ vec {x}}, t + delta t) = {u ({\ bf \ vec [x}}}, t )}


जहाँ  bfI पहचान मैट्रिक्स है। समीकरणों की कई समान प्रणालियों को हल करने के लिए हमें बाद में जैकोबी पद्धति को लागू करने के लिए ऐसे परिवर्तनों की आवश्यकता है। हम बाद में भी इस पर चर्चा करेंगे।

बाहरी ताकतें


एल्गोरिथ्म का सबसे सरल चरण है, माध्यम से बाहरी बलों का अनुप्रयोग। उपयोगकर्ता के लिए, यह माउस या इसके आंदोलन के साथ स्क्रीन पर क्लिक के रूप में परिलक्षित होगा। बाहरी बल को निम्नलिखित सूत्र द्वारा वर्णित किया जा सकता है, जिसे हम मैट्रिक्स के प्रत्येक तत्व के लिए लागू करते हैं (  vec bfG - गति वेक्टर xp,yp - माउस स्थिति x,y - वर्तमान सेल के निर्देशांक, - कार्रवाई की त्रिज्या, स्केलिंग पैरामीटर):

 vec bfF= vec bfG deltat bfexp left((xxp)2+(yyp)2rr n)


एक आवेग वेक्टर को आसानी से माउस की पिछली स्थिति और वर्तमान एक (यदि वहाँ एक था) के बीच अंतर के रूप में गणना की जा सकती है, और यहां आप अभी भी रचनात्मक हो सकते हैं। यह एल्गोरिथ्म के इस हिस्से में है कि हम रंगों को एक तरल, इसकी रोशनी आदि के साथ जोड़ सकते हैं। बाहरी बलों में गुरुत्वाकर्षण और तापमान भी शामिल हो सकते हैं, और हालांकि इस तरह के मापदंडों को लागू करना मुश्किल नहीं है, हम इस लेख में उन पर विचार नहीं करेंगे।

दबाव


नवियर-स्टोक्स समीकरण में दबाव वह बल है जो कणों को किसी भी बाहरी बल को लागू करने के बाद उन्हें उपलब्ध सभी जगह को भरने से रोकता है। तुरंत, इसकी गणना बहुत कठिन है, लेकिन हेल्महोल्त्ज़ अपघटन प्रमेय को लागू करके हमारी समस्या को बहुत सरल किया जा सकता है

हम कहते हैं  bf vecW विस्थापन, बाहरी बलों और चिपचिपाहट की गणना के बाद प्राप्त वेक्टर क्षेत्र। इसमें नॉनज़रो डाइवर्जेंस होगा, जो तरल की अपूर्णता की स्थिति का विरोध करता है (  nabla cdot bf vecu=0 ), और इसे ठीक करने के लिए, दबाव की गणना करना आवश्यक है। हेल्महोल्त्ज़ अपघटन प्रमेय के अनुसार,  bf vecW दो क्षेत्रों के योग के रूप में प्रतिनिधित्व किया जा सकता है:

 bf vecW= vecu+ nablap


जहाँ  bf - यह वेक्टर क्षेत्र है जिसे हम शून्य विचलन के साथ देख रहे हैं। इस लेख में इस समानता का कोई प्रमाण नहीं दिया जाएगा, लेकिन अंत में आप एक विस्तृत विवरण के साथ एक लिंक पा सकते हैं। हम स्केलर प्रेशर फ़ील्ड की गणना के लिए निम्न सूत्र प्राप्त करने के लिए अभिव्यक्ति के दोनों किनारों पर नाबला ऑपरेटर को लागू कर सकते हैं:

 nabla cdot bf vecW= nabla cdot( vecu+ nablap)= nabla cdot vecu+ nnabla2p= nabla2p


ऊपर लिखा अभिव्यक्ति दबाव के लिए पॉइसन समीकरण है। हम इसे पूर्वोक्त जैकोबी विधि द्वारा भी हल कर सकते हैं, और इस तरह नवियर-स्टोक्स समीकरण में अंतिम अज्ञात चर खोज सकते हैं। सिद्धांत रूप में, रैखिक समीकरणों की प्रणालियों को विभिन्न और परिष्कृत तरीकों से हल किया जा सकता है, लेकिन हम अभी भी उनमें से सबसे सरल पर ध्यान केंद्रित करेंगे, ताकि इस लेख को और अधिक बोझ न डालें।

सीमा और प्रारंभिक शर्तें


परिमित डोमेन पर बनाए गए किसी भी अंतर समीकरण को सही ढंग से निर्दिष्ट प्रारंभिक या सीमा शर्तों की आवश्यकता होती है, अन्यथा हमें शारीरिक रूप से गलत परिणाम प्राप्त होने की बहुत संभावना है। समन्वय ग्रिड के किनारों के पास तरल पदार्थ के व्यवहार को नियंत्रित करने के लिए सीमा की स्थिति स्थापित की जाती है, और प्रारंभिक शर्तें उन मापदंडों को निर्दिष्ट करती हैं जो कार्यक्रम शुरू होने के समय कणों के पास होती हैं।

प्रारंभिक शर्तें बहुत सरल होंगी - शुरू में द्रव स्थिर है (कण वेग शून्य है), और दबाव भी शून्य है। दिए गए सूत्रों द्वारा गति और दबाव के लिए सीमा की स्थिति निर्धारित की जाएगी:

 bf vecu0,j+ bf vecu1,j over2 deltay=0, bf vecui,0+ bf vecui,1 over2 x=0

{\ bf p_ {0, j} - \ bf p_ {1, j} \ over \ delta x} = 0, {\ bf p_ {i, 0} - \ bf p_ {i, 1} \ _ \ _ डेल्टा y} = 0


इस प्रकार, किनारों पर कणों का वेग किनारों पर वेग के विपरीत होगा (जिससे वे किनारे से पीछे हट जाएंगे), और दबाव सीमा के बगल में मूल्य के बराबर है। इन ऑपरेशनों को सरणी के सभी बाउंडिंग तत्वों पर लागू किया जाना चाहिए (उदाहरण के लिए, एक ग्रिड आकार है N M , तो हम आकृति में नीले रंग में चिह्नित कोशिकाओं के लिए एल्गोरिदम लागू करते हैं):

छवि

डाई


अब हमारे पास क्या है, आप पहले से ही बहुत सारी दिलचस्प चीजें ले सकते हैं। उदाहरण के लिए, एक तरल में डाई के प्रसार का एहसास करने के लिए। ऐसा करने के लिए, हमें बस एक और स्केलर फ़ील्ड बनाए रखने की आवश्यकता है, जो सिमुलेशन के प्रत्येक बिंदु पर पेंट की मात्रा के लिए जिम्मेदार होगा। डाई को अद्यतन करने का सूत्र गति के समान है, और इसे इस प्रकार व्यक्त किया जाता है:

 िd over िt=( vec bfu cdot nabla)d+ gamma nabla2d+S


सूत्र में डाई के साथ क्षेत्र को फिर से भरने के लिए जिम्मेदार (संभवतः उपयोगकर्ता क्लिक करता है) के आधार पर, सीधे बिंदु पर डाई की मात्रा है, और - प्रसार गुणांक। इसे हल करना मुश्किल नहीं है, क्योंकि सूत्रों की व्युत्पत्ति पर सभी बुनियादी काम पहले से ही किए गए हैं, और यह कुछ प्रतिस्थापन बनाने के लिए पर्याप्त है। आरजीबी प्रारूप में पेंट को रंग के रूप में कोड में लागू किया जा सकता है, और इस मामले में कई वास्तविक मूल्यों के साथ संचालन के लिए कार्य कम हो जाता है।

vorticity


Vorticity समीकरण नवियर-स्टोक्स समीकरण का प्रत्यक्ष हिस्सा नहीं है, लेकिन यह एक तरल में डाई की गति के प्रशंसनीय सिमुलेशन के लिए एक महत्वपूर्ण पैरामीटर है। इस तथ्य के कारण कि हम एक असतत क्षेत्र पर एक एल्गोरिथ्म का उत्पादन कर रहे हैं, साथ ही साथ फ़्लोटिंग-पॉइंट मानों की सटीकता में नुकसान के कारण, यह प्रभाव खो गया है, और इसलिए हमें अंतरिक्ष में प्रत्येक बिंदु पर अतिरिक्त बल लागू करके इसे पुनर्स्थापित करने की आवश्यकता है। इस बल के वेक्टर के रूप में नामित किया गया है  bf vecT और निम्न सूत्रों द्वारा निर्धारित किया जाता है:

 bf omega= nabla  vecu

 vec eta= nabla| omega|

 bf vec psi= vec eta over| vec eta|

 bf vecT= epsilon( vec psi  omega) x


  रोटर को वेग सदिश पर लागू करने का परिणाम है (इसकी परिभाषा लेख की शुरुआत में दी गई है),  vec eta - पूर्ण मानों के अदिश क्षेत्र का ढाल   vec psi एक सामान्यीकृत वेक्टर का प्रतिनिधित्व करता है  vec eta , और  epsilon एक स्थिर है जो नियंत्रित करता है कि हमारे द्रव में कितने बड़े भंवर होंगे।

रेखीय समीकरणों के सिस्टम को हल करने के लिए जैकोबी विधि


नवियर-स्टोक्स समीकरणों का विश्लेषण करने में, हम समीकरणों की दो प्रणालियों के साथ आए - एक चिपचिपाहट के लिए और दूसरा दबाव के लिए। वे एक पुनरावृत्त एल्गोरिथ्म द्वारा हल किए जा सकते हैं, जिसे निम्नलिखित पुनरावृत्त सूत्र द्वारा वर्णित किया जा सकता है:

x(k+1)i,j=x(k)i1,j+x(k)i+1,j+x(k)i,j1+x(k)i,j+1+ Alphabi,j over beta


हमारे लिए x - सरणी तत्व एक अदिश या वेक्टर क्षेत्र का प्रतिनिधित्व करते हैं। k - पुनरावृत्ति संख्या, हम इसे कम करने और उत्पादकता बढ़ाने के लिए गणना या इसके विपरीत की सटीकता बढ़ाने के लिए इसे समायोजित कर सकते हैं।

हमारे द्वारा प्रतिस्थापित चिपचिपाहट की गणना करने के लिए: x=b= bf vecu =1 over nu deltat =4+  , यहाँ पैरामीटर है   - भार का योग। इस प्रकार, हमें एक क्षेत्र के मूल्यों को स्वतंत्र रूप से पढ़ने और उन्हें दूसरे में लिखने के लिए कम से कम दो वेक्टर वेग क्षेत्रों को संग्रहीत करने की आवश्यकता है। औसतन, जैकोबी विधि द्वारा वेग क्षेत्र की गणना के लिए, 20-50 पुनरावृत्तियों को अंजाम देना आवश्यक है, जो कि सीपीयू पर गणना करने पर काफी होता है।

दबाव समीकरण के लिए, हम निम्नलिखित प्रतिस्थापन करते हैं: x=pb= nabla bf cdot vecW =1 =4 । नतीजतन, हमें मूल्य मिलता है pi,j deltat बिंदु पर। लेकिन चूंकि इसका उपयोग केवल वेग क्षेत्र से घटाए गए ढाल की गणना करने के लिए किया जाता है, अतिरिक्त परिवर्तनों को छोड़ा जा सकता है। दबाव क्षेत्र के लिए, 40-80 पुनरावृत्तियों को करना सबसे अच्छा है, क्योंकि छोटी संख्या के साथ विसंगति ध्यान देने योग्य हो जाती है।

एल्गोरिदम कार्यान्वयन


हम C ++ में एल्गोरिथ्म को लागू करेंगे, हमें कुडा टूलकिट की भी आवश्यकता है (आप इसे एनवीडिया वेबसाइट पर स्थापित करने के लिए कैसे पढ़ सकते हैं), साथ ही साथ एसएफएमएल भी । हमें एल्गोरिथ्म को समानांतर करने के लिए CUDA की आवश्यकता है, और SFML का उपयोग केवल एक विंडो बनाने और स्क्रीन पर एक चित्र प्रदर्शित करने के लिए किया जाएगा (सिद्धांत रूप में, यह ओपनजीएल में लिखा जा सकता है, लेकिन प्रदर्शन का अंतर महत्वहीन होगा, लेकिन कोड 200 अन्य लाइनों से बढ़ जाएगा)।

कोडा टूलकिट


सबसे पहले, हम कार्यों को समानांतर बनाने के लिए कोडा टूलकिट का उपयोग करने के बारे में थोड़ी बात करेंगे। एनवीडिया द्वारा एक अधिक विस्तृत मार्गदर्शिका प्रदान की जाती है, इसलिए यहां हम खुद को केवल सबसे आवश्यक तक ही सीमित रखते हैं। यह भी माना जाता है कि आप संकलक को स्थापित करने में सक्षम थे, और आप त्रुटियों के बिना एक परीक्षण परियोजना बनाने में सक्षम थे।

एक फ़ंक्शन बनाने के लिए जो जीपीयू पर चलता है, आपको पहले यह घोषित करने की आवश्यकता है कि हम कितने कर्नेल का उपयोग करना चाहते हैं, और कर्नेल के कितने ब्लॉक हमें आवंटित करने की आवश्यकता है। इसके लिए, कोडा टूलकिट हमें एक विशेष संरचना प्रदान करता है - dim3 , इसके सभी x, y, z मानों को डिफ़ॉल्ट रूप से सेट करके 1. फ़ंक्शन को कॉल करते समय इसे एक तर्क के रूप में निर्दिष्ट करके, हम आवंटित कर्नेल की संख्या को नियंत्रित कर सकते हैं। चूंकि हम द्वि-आयामी सरणी के साथ काम कर रहे हैं, इसलिए हमें कंस्ट्रक्टर में केवल दो फ़ील्ड सेट करने की आवश्यकता है: x और y :

dim3 threadsPerBlock(x_threads, y_threads); dim3 numBlocks(size_x / x_threads, y_size / y_threads); 

जहाँ size_x और size_y सरणी के आकार को संसाधित किया जा रहा है। हस्ताक्षर और फ़ंक्शन कॉल निम्नानुसार हैं (ट्रिपल कोण ब्रैकेट्स को कुडा संकलक द्वारा संसाधित किया जाता है):

 void __global__ deviceFunction(); // declare deviceFunction<<<numBlocks, threadsPerBlock>>>(); // call from host 

फ़ंक्शन में ही, आप ब्लॉक सूत्र के माध्यम से दो आयामी सरणी के सूचकांक को पुनर्स्थापित कर सकते हैं और इस ब्लॉक में कर्नेल नंबर निम्न सूत्र का उपयोग कर सकते हैं:

 int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; 

यह ध्यान दिया जाना चाहिए कि वीडियो कार्ड पर निष्पादित फ़ंक्शन को __global__ टैग के साथ चिह्नित किया जाना चाहिए, और शून्य भी लौटाया जाना चाहिए, इसलिए अक्सर गणना परिणामों को तर्क के रूप में लिखा जाता है और वीडियो कार्ड की स्मृति में पूर्व-आवंटित किया जाता है।

CudaMalloc और CudaFree वीडियो कार्ड पर मेमोरी को मुक्त करने और आवंटित करने के लिए जिम्मेदार हैं। हम पॉइंटर्स पर मेमोरी क्षेत्र में काम कर सकते हैं जो वे वापस लौटते हैं, लेकिन हम मुख्य कोड से डेटा तक नहीं पहुंच सकते हैं। गणना परिणामों को वापस करने का सबसे आसान तरीका मानक मेम्ची के समान, कोडमैकेसी का उपयोग करना है , लेकिन वीडियो कार्ड से मुख्य मेमोरी और इसके विपरीत डेटा की प्रतिलिपि बनाने में सक्षम है।

SFML और विंडो रेंडर


इस सभी ज्ञान के साथ, हम आखिरकार सीधे कोड लिखने के लिए आगे बढ़ सकते हैं। शुरू करने के लिए, चलो main.cpp फ़ाइल बनाएँ और वहाँ प्रस्तुत विंडो के लिए सभी सहायक कोड रखें:

main.cpp
 #include <SFML/Graphics.hpp> #include <chrono> #include <cstdlib> #include <cmath> //SFML REQUIRED TO LAUNCH THIS CODE #define SCALE 2 #define WINDOW_WIDTH 1280 #define WINDOW_HEIGHT 720 #define FIELD_WIDTH WINDOW_WIDTH / SCALE #define FIELD_HEIGHT WINDOW_HEIGHT / SCALE static struct Config { float velocityDiffusion; float pressure; float vorticity; float colorDiffusion; float densityDiffusion; float forceScale; float bloomIntense; int radius; bool bloomEnabled; } config; void setConfig(float vDiffusion = 0.8f, float pressure = 1.5f, float vorticity = 50.0f, float cDiffuion = 0.8f, float dDiffuion = 1.2f, float force = 1000.0f, float bloomIntense = 25000.0f, int radius = 100, bool bloom = true); void computeField(uint8_t* result, float dt, int x1pos = -1, int y1pos = -1, int x2pos = -1, int y2pos = -1, bool isPressed = false); void cudaInit(size_t xSize, size_t ySize); void cudaExit(); int main() { cudaInit(FIELD_WIDTH, FIELD_HEIGHT); srand(time(NULL)); sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), ""); auto start = std::chrono::system_clock::now(); auto end = std::chrono::system_clock::now(); sf::Texture texture; sf::Sprite sprite; std::vector<sf::Uint8> pixelBuffer(FIELD_WIDTH * FIELD_HEIGHT * 4); texture.create(FIELD_WIDTH, FIELD_HEIGHT); sf::Vector2i mpos1 = { -1, -1 }, mpos2 = { -1, -1 }; bool isPressed = false; bool isPaused = false; while (window.isOpen()) { end = std::chrono::system_clock::now(); std::chrono::duration<float> diff = end - start; window.setTitle("Fluid simulator " + std::to_string(int(1.0f / diff.count())) + " fps"); start = end; window.clear(sf::Color::White); sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); if (event.type == sf::Event::MouseButtonPressed) { if (event.mouseButton.button == sf::Mouse::Button::Left) { mpos1 = { event.mouseButton.x, event.mouseButton.y }; mpos1 /= SCALE; isPressed = true; } else { isPaused = !isPaused; } } if (event.type == sf::Event::MouseButtonReleased) { isPressed = false; } if (event.type == sf::Event::MouseMoved) { std::swap(mpos1, mpos2); mpos2 = { event.mouseMove.x, event.mouseMove.y }; mpos2 /= SCALE; } } float dt = 0.02f; if (!isPaused) computeField(pixelBuffer.data(), dt, mpos1.x, mpos1.y, mpos2.x, mpos2.y, isPressed); texture.update(pixelBuffer.data()); sprite.setTexture(texture); sprite.setScale({ SCALE, SCALE }); window.draw(sprite); window.display(); } cudaExit(); return 0; } 


मुख्य समारोह की शुरुआत में लाइन

 std::vector<sf::Uint8> pixelBuffer(FIELD_WIDTH * FIELD_HEIGHT * 4); 

एक निरंतर लंबाई के साथ एक आयामी सरणी के रूप में एक RGBA छवि बनाता है। हम इसे अन्य मापदंडों (माउस पोजीशन, फ्रेम के बीच का अंतर) के साथ कंप्यूटफिल्ड फंक्शन में पास करेंगे। उत्तरार्द्ध, साथ ही कई अन्य कार्यों को कर्नेल.यू में घोषित किया गया है और GPU पर निष्पादित कोड को कॉल करें। आप एसएफएमएल वेबसाइट पर किसी भी फ़ंक्शन पर प्रलेखन पा सकते हैं, फ़ाइल कोड में सुपर-दिलचस्प कुछ भी नहीं हो रहा है, इसलिए हम लंबे समय तक वहां नहीं रुकते हैं।

GPU कम्प्यूटिंग


Gpu के तहत कोड लिखना शुरू करने के लिए, पहले एक कर्नेल। Cu फ़ाइल बनाएं और उसमें कई सहायक कक्षाएं परिभाषित करें: Color3f, Vec2, config, SystemConfig :

kernel.cu (डेटा संरचनाएं)
 struct Vec2 { float x = 0.0, y = 0.0; __device__ Vec2 operator-(Vec2 other) { Vec2 res; res.x = this->x - other.x; res.y = this->y - other.y; return res; } __device__ Vec2 operator+(Vec2 other) { Vec2 res; res.x = this->x + other.x; res.y = this->y + other.y; return res; } __device__ Vec2 operator*(float d) { Vec2 res; res.x = this->x * d; res.y = this->y * d; return res; } }; struct Color3f { float R = 0.0f; float G = 0.0f; float B = 0.0f; __host__ __device__ Color3f operator+ (Color3f other) { Color3f res; res.R = this->R + other.R; res.G = this->G + other.G; res.B = this->B + other.B; return res; } __host__ __device__ Color3f operator* (float d) { Color3f res; res.R = this->R * d; res.G = this->G * d; res.B = this->B * d; return res; } }; struct Particle { Vec2 u; // velocity Color3f color; }; static struct Config { float velocityDiffusion; float pressure; float vorticity; float colorDiffusion; float densityDiffusion; float forceScale; float bloomIntense; int radius; bool bloomEnabled; } config; static struct SystemConfig { int velocityIterations = 20; int pressureIterations = 40; int xThreads = 64; int yThreads = 1; } sConfig; void setConfig( float vDiffusion = 0.8f, float pressure = 1.5f, float vorticity = 50.0f, float cDiffuion = 0.8f, float dDiffuion = 1.2f, float force = 5000.0f, float bloomIntense = 25000.0f, int radius = 500, bool bloom = true ) { config.velocityDiffusion = vDiffusion; config.pressure = pressure; config.vorticity = vorticity; config.colorDiffusion = cDiffuion; config.densityDiffusion = dDiffuion; config.forceScale = force; config.bloomIntense = bloomIntense; config.radius = radius; config.bloomEnabled = bloom; } static const int colorArraySize = 7; Color3f colorArray[colorArraySize]; static Particle* newField; static Particle* oldField; static uint8_t* colorField; static size_t xSize, ySize; static float* pressureOld; static float* pressureNew; static float* vorticityField; static Color3f currentColor; static float elapsedTime = 0.0f; static float timeSincePress = 0.0f; static float bloomIntense; int lastXpos = -1, lastYpos = -1; 

विधि नाम के सामने __host__ विशेषता का अर्थ है कि कोड को CPU पर निष्पादित किया जा सकता है, __device__ , इसके विपरीत, GPU के तहत कोड एकत्र करने के लिए कंपाइलर को बाध्य करता है। कोड दो-घटक वैक्टर, रंग के साथ काम करने के लिए आदिम घोषित करता है, मापदंडों के साथ कॉन्फ़िगर करता है जिसे रनटाइम में बदला जा सकता है, साथ ही कई स्थिर बिंदुओं को सरणियों के लिए, जिसे हम गणना के लिए बफ़र्स के रूप में उपयोग करेंगे।

cudaInit और cudaExit को भी बहुत मामूली रूप से परिभाषित किया गया है:

kernel.cu (init)
 void cudaInit(size_t x, size_t y) { setConfig(); colorArray[0] = { 1.0f, 0.0f, 0.0f }; colorArray[1] = { 0.0f, 1.0f, 0.0f }; colorArray[2] = { 1.0f, 0.0f, 1.0f }; colorArray[3] = { 1.0f, 1.0f, 0.0f }; colorArray[4] = { 0.0f, 1.0f, 1.0f }; colorArray[5] = { 1.0f, 0.0f, 1.0f }; colorArray[6] = { 1.0f, 0.5f, 0.3f }; int idx = rand() % colorArraySize; currentColor = colorArray[idx]; xSize = x, ySize = y; cudaSetDevice(0); cudaMalloc(&colorField, xSize * ySize * 4 * sizeof(uint8_t)); cudaMalloc(&oldField, xSize * ySize * sizeof(Particle)); cudaMalloc(&newField, xSize * ySize * sizeof(Particle)); cudaMalloc(&pressureOld, xSize * ySize * sizeof(float)); cudaMalloc(&pressureNew, xSize * ySize * sizeof(float)); cudaMalloc(&vorticityField, xSize * ySize * sizeof(float)); } void cudaExit() { cudaFree(colorField); cudaFree(oldField); cudaFree(newField); cudaFree(pressureOld); cudaFree(pressureNew); cudaFree(vorticityField); } 

आरंभीकरण समारोह में, हम दो-आयामी सरणियों के लिए मेमोरी आवंटित करते हैं, उन रंगों की एक सरणी निर्दिष्ट करते हैं जिनका उपयोग हम तरल को चित्रित करने के लिए करेंगे, और साथ ही डिफ़ॉल्ट मानों को कॉन्फ़िगरेशन में सेट करेंगे। CudaExit में, हम सिर्फ सभी बफ़र्स को मुक्त करते हैं। विरोधाभासी के रूप में यह लगता है, दो आयामी सरणियों के भंडारण के लिए, यह एक आयामी सरणियों का उपयोग करने के लिए सबसे अधिक फायदेमंद है, जिस तक पहुंच निम्नलिखित अभिव्यक्ति के साथ किया जाएगा:

 array[y * size_x + x]; // equals to array[y][x] 

हम कण विस्थापन समारोह के साथ प्रत्यक्ष एल्गोरिदम का कार्यान्वयन शुरू करते हैं। फ़ील्ड ओल्डफ़िल्ड और न्यूफ़िल्ड (वह फ़ील्ड जहाँ डेटा आता है और जहाँ उन्हें लिखा जाता है), सरणी का आकार, साथ ही समय डेल्टा और घनत्व गुणांक (तरल में डाई के विघटन में तेजी लाने और मध्यम को विशेष रूप से संवेदनशील नहीं बनाने के लिए संवेदनशील बनाने के लिए स्थानांतरित किया जाता है) उपयोगकर्ता कार्रवाई)। मध्यवर्ती मूल्यों की गणना के माध्यम से बिलिनियर प्रक्षेप समारोह को शास्त्रीय तरीके से कार्यान्वित किया जाता है :

kernel.cu (विज्ञापन)
 // interpolates quantity of grid cells __device__ Particle interpolate(Vec2 v, Particle* field, size_t xSize, size_t ySize) { float x1 = (int)vx; float y1 = (int)vy; float x2 = (int)vx + 1; float y2 = (int)vy + 1; Particle q1, q2, q3, q4; #define CLAMP(val, minv, maxv) min(maxv, max(minv, val)) #define SET(Q, x, y) Q = field[int(CLAMP(y, 0.0f, ySize - 1.0f)) * xSize + int(CLAMP(x, 0.0f, xSize - 1.0f))] SET(q1, x1, y1); SET(q2, x1, y2); SET(q3, x2, y1); SET(q4, x2, y2); #undef SET #undef CLAMP float t1 = (x2 - vx) / (x2 - x1); float t2 = (vx - x1) / (x2 - x1); Vec2 f1 = q1.u * t1 + q3.u * t2; Vec2 f2 = q2.u * t1 + q4.u * t2; Color3f C1 = q2.color * t1 + q4.color * t2; Color3f C2 = q2.color * t1 + q4.color * t2; float t3 = (y2 - vy) / (y2 - y1); float t4 = (vy - y1) / (y2 - y1); Particle res; res.u = f1 * t3 + f2 * t4; res.color = C1 * t3 + C2 * t4; return res; } // adds quantity to particles using bilinear interpolation __global__ void advect(Particle* newField, Particle* oldField, size_t xSize, size_t ySize, float dDiffusion, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; float decay = 1.0f / (1.0f + dDiffusion * dt); Vec2 pos = { x * 1.0f, y * 1.0f }; Particle& Pold = oldField[y * xSize + x]; // find new particle tracing where it came from Particle p = interpolate(pos - Pold.u * dt, oldField, xSize, ySize); pu = pu * decay; p.color = p.color * decay; newField[y * xSize + x] = p; } 

यह चिपचिपाहट प्रसार समारोह को कई भागों में विभाजित करने का निर्णय लिया गया था: कम्प्यूट्यूटिफ्यूजन को मुख्य कोड से कहा जाता है, जो फैलाना और गणना को पूर्व निर्धारित समय की गणना करता है , और फिर सरणी को स्वैप करता है जहां से हम डेटा लेते हैं और जहां हम इसे लिखते हैं। समानांतर डेटा प्रोसेसिंग को लागू करने का यह सबसे आसान तरीका है, लेकिन हम दोगुनी मेमोरी खर्च कर रहे हैं।

दोनों कार्यों में जैकोबी पद्धति की विविधताएं हैं। JacobiColor और jacobiVelocity का शरीर तुरंत जाँच करता है कि वर्तमान तत्व सीमा पर नहीं हैं - इस मामले में, हमें उन्हें सीमा और प्रारंभिक स्थितियों के खंड में वर्णित सूत्रों के अनुसार सेट करना होगा।

kernel.cu (फैलाना)
 // performs iteration of jacobi method on color grid field __device__ Color3f jacobiColor(Particle* colorField, size_t xSize, size_t ySize, Vec2 pos, Color3f B, float alpha, float beta) { Color3f xU, xD, xL, xR, res; int x = pos.x; int y = pos.y; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = colorField[int(y) * xSize + int(x)] SET(xU, x, y - 1).color; SET(xD, x, y + 1).color; SET(xL, x - 1, y).color; SET(xR, x + 1, y).color; #undef SET res = (xU + xD + xL + xR + B * alpha) * (1.0f / beta); return res; } // calculates color field diffusion __global__ void computeColor(Particle* newField, Particle* oldField, size_t xSize, size_t ySize, float cDiffusion, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; Vec2 pos = { x * 1.0f, y * 1.0f }; Color3f c = oldField[y * xSize + x].color; float alpha = cDiffusion * cDiffusion / dt; float beta = 4.0f + alpha; // perfom one iteration of jacobi method (diffuse method should be called 20-50 times per cell) newField[y * xSize + x].color = jacobiColor(oldField, xSize, ySize, pos, c, alpha, beta); } // performs iteration of jacobi method on velocity grid field __device__ Vec2 jacobiVelocity(Particle* field, size_t xSize, size_t ySize, Vec2 v, Vec2 B, float alpha, float beta) { Vec2 vU = B * -1.0f, vD = B * -1.0f, vR = B * -1.0f, vL = B * -1.0f; #define SET(U, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) U = field[int(y) * xSize + int(x)].u SET(vU, vx, vy - 1); SET(vD, vx, vy + 1); SET(vL, vx - 1, vy); SET(vR, vx + 1, vy); #undef SET v = (vU + vD + vL + vR + B * alpha) * (1.0f / beta); return v; } // calculates nonzero divergency velocity field u __global__ void diffuse(Particle* newField, Particle* oldField, size_t xSize, size_t ySize, float vDiffusion, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; Vec2 pos = { x * 1.0f, y * 1.0f }; Vec2 u = oldField[y * xSize + x].u; // perfoms one iteration of jacobi method (diffuse method should be called 20-50 times per cell) float alpha = vDiffusion * vDiffusion / dt; float beta = 4.0f + alpha; newField[y * xSize + x].u = jacobiVelocity(oldField, xSize, ySize, pos, u, alpha, beta); } // performs several iterations over velocity and color fields void computeDiffusion(dim3 numBlocks, dim3 threadsPerBlock, float dt) { // diffuse velocity and color for (int i = 0; i < sConfig.velocityIterations; i++) { diffuse<<<numBlocks, threadsPerBlock>>>(newField, oldField, xSize, ySize, config.velocityDiffusion, dt); computeColor<<<numBlocks, threadsPerBlock>>>(newField, oldField, xSize, ySize, config.colorDiffusion, dt); std::swap(newField, oldField); } } 

बाहरी बल के उपयोग को एक ही फ़ंक्शन के माध्यम से लागू किया जाता है - applyForce , जो एक तर्क के रूप में माउस की स्थिति, डाई के रंग, साथ ही साथ कार्रवाई की त्रिज्या लेता है। इसकी मदद से, हम कणों को गति दे सकते हैं, साथ ही उन्हें पेंट भी कर सकते हैं। भ्राता प्रतिपादक आपको क्षेत्र को बहुत तेज नहीं बनाने की अनुमति देता है, और साथ ही निर्दिष्ट त्रिज्या में काफी स्पष्ट है।

kernel.cu (बल)
 // applies force and add color dye to the particle field __global__ void applyForce(Particle* field, size_t xSize, size_t ySize, Color3f color, Vec2 F, Vec2 pos, int r, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; float e = expf((-(powf(x - pos.x, 2) + powf(y - pos.y, 2))) / r); Vec2 uF = F * dt * e; Particle& p = field[y * xSize + x]; pu = pu + uF; color = color * e + p.color; p.color.R = min(1.0f, color.R); p.color.G = min(1.0f, color.G); p.color.B = min(1.0f, color.B); } 

Vorticity की गणना पहले से ही एक अधिक जटिल प्रक्रिया है, इसलिए हम इसे computeVorticity और applyVorticity में लागू करते हैं, हम यह भी ध्यान देते हैं कि उनके लिए दो ऐसे वेक्टर ऑपरेटरों को curl (रोटर) और absGradient (निरपेक्ष क्षेत्र मूल्यों का ढाल) के रूप में परिभाषित करना आवश्यक है। अतिरिक्त भंवर प्रभावों को निर्दिष्ट करने के लिए, हम गुणा करते हैं ढाल वेक्टर के घटक पर 1 , और फिर लंबाई द्वारा विभाजित करके इसे सामान्य करें (यह जांचने के लिए भूलकर कि वेक्टर नॉनजरो है):

कर्नेल। क्यू (भेद्यता)
 // computes curl of velocity field __device__ float curl(Particle* field, size_t xSize, size_t ySize, int x, int y) { Vec2 C = field[int(y) * xSize + int(x)].u; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = field[int(y) * xSize + int(x)] float x1 = -Cx, x2 = -Cx, y1 = -Cy, y2 = -Cy; SET(x1, x + 1, y).ux; SET(x2, x - 1, y).ux; SET(y1, x, y + 1).uy; SET(y2, x, y - 1).uy; #undef SET float res = ((y1 - y2) - (x1 - x2)) * 0.5f; return res; } // computes absolute value gradient of vorticity field __device__ Vec2 absGradient(float* field, size_t xSize, size_t ySize, int x, int y) { float C = field[int(y) * xSize + int(x)]; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = field[int(y) * xSize + int(x)] float x1 = C, x2 = C, y1 = C, y2 = C; SET(x1, x + 1, y); SET(x2, x - 1, y); SET(y1, x, y + 1); SET(y2, x, y - 1); #undef SET Vec2 res = { (abs(x1) - abs(x2)) * 0.5f, (abs(y1) - abs(y2)) * 0.5f }; return res; } // computes vorticity field which should be passed to applyVorticity function __global__ void computeVorticity(float* vField, Particle* field, size_t xSize, size_t ySize) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; vField[y * xSize + x] = curl(field, xSize, ySize, x, y); } // applies vorticity to velocity field __global__ void applyVorticity(Particle* newField, Particle* oldField, float* vField, size_t xSize, size_t ySize, float vorticity, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; Particle& pOld = oldField[y * xSize + x]; Particle& pNew = newField[y * xSize + x]; Vec2 v = absGradient(vField, xSize, ySize, x, y); vy *= -1.0f; float length = sqrtf(vx * vx + vy * vy) + 1e-5f; Vec2 vNorm = v * (1.0f / length); Vec2 vF = vNorm * vField[y * xSize + x] * vorticity; pNew = pOld; pNew.u = pNew.u + vF * dt; } 

एल्गोरिथ्म में अगला चरण स्केलर दबाव क्षेत्र की गणना और वेग क्षेत्र पर इसके प्रक्षेपण होगा। ऐसा करने के लिए, हमें 4 कार्यों को लागू करने की आवश्यकता है: डायवर्जेंसी , जो गति विचलन, जैकोबायप्योर पर विचार करेगी , जो दबाव के लिए जैकोबी विधि को लागू करती है, और कम्प्यूट्रेटइम्प्लिमेंट के साथ कंप्यूटसेप्टर , इटैलिक फील्ड गणना करते हुए:

कर्नेल। क्यू (दबाव)
 // performs iteration of jacobi method on pressure grid field __device__ float jacobiPressure(float* pressureField, size_t xSize, size_t ySize, int x, int y, float B, float alpha, float beta) { float C = pressureField[int(y) * xSize + int(x)]; float xU = C, xD = C, xL = C, xR = C; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = pressureField[int(y) * xSize + int(x)] SET(xU, x, y - 1); SET(xD, x, y + 1); SET(xL, x - 1, y); SET(xR, x + 1, y); #undef SET float pressure = (xU + xD + xL + xR + alpha * B) * (1.0f / beta); return pressure; } // computes divergency of velocity field __device__ float divergency(Particle* field, size_t xSize, size_t ySize, int x, int y) { Particle& C = field[int(y) * xSize + int(x)]; float x1 = -1 * Cux, x2 = -1 * Cux, y1 = -1 * Cuy, y2 = -1 * Cuy; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = field[int(y) * xSize + int(x)] SET(x1, x + 1, y).ux; SET(x2, x - 1, y).ux; SET(y1, x, y + 1).uy; SET(y2, x, y - 1).uy; #undef SET return (x1 - x2 + y1 - y2) * 0.5f; } // performs iteration of jacobi method on pressure field __global__ void computePressureImpl(Particle* field, size_t xSize, size_t ySize, float* pNew, float* pOld, float pressure, float dt) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; float div = divergency(field, xSize, ySize, x, y); float alpha = -1.0f * pressure * pressure; float beta = 4.0; pNew[y * xSize + x] = jacobiPressure(pOld, xSize, ySize, x, y, div, alpha, beta); } // performs several iterations over pressure field void computePressure(dim3 numBlocks, dim3 threadsPerBlock, float dt) { for (int i = 0; i < sConfig.pressureIterations; i++) { computePressureImpl<<<numBlocks, threadsPerBlock>>>(oldField, xSize, ySize, pressureNew, pressureOld, config.pressure, dt); std::swap(pressureOld, pressureNew); } } 

प्रक्षेपण दो छोटे कार्यों में फिट बैठता है - परियोजना और दबाव के लिए इसे ढाल कहा जाता है। इसे हमारे सिमुलेशन एल्गोरिदम का अंतिम चरण कहा जा सकता है:

kernel.cu (परियोजना)
 // computes gradient of pressure field __device__ Vec2 gradient(float* field, size_t xSize, size_t ySize, int x, int y) { float C = field[y * xSize + x]; #define SET(P, x, y) if (x < xSize && x >= 0 && y < ySize && y >= 0) P = field[int(y) * xSize + int(x)] float x1 = C, x2 = C, y1 = C, y2 = C; SET(x1, x + 1, y); SET(x2, x - 1, y); SET(y1, x, y + 1); SET(y2, x, y - 1); #undef SET Vec2 res = { (x1 - x2) * 0.5f, (y1 - y2) * 0.5f }; return res; } // projects pressure field on velocity field __global__ void project(Particle* newField, size_t xSize, size_t ySize, float* pField) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; Vec2& u = newField[y * xSize + x].u; u = u - gradient(pField, xSize, ySize, x, y); } 

प्रक्षेपण के बाद, हम सुरक्षित रूप से छवि को बफर और विभिन्न पोस्ट-इफेक्ट्स तक पहुंचाने के लिए आगे बढ़ सकते हैं। पेंट फ़ंक्शन कण फ़ील्ड से RGBA सरणी में रंगों की प्रतिलिपि बनाता है। ApplyBloom फ़ंक्शन भी लागू किया गया था, जो तरल को हाइलाइट करता है जब कर्सर को उस पर रखा जाता है और माउस बटन दबाया जाता है। अनुभव से, यह तकनीक उपयोगकर्ता की आंखों के लिए तस्वीर को अधिक सुखद और दिलचस्प बनाती है, लेकिन यह बिल्कुल भी आवश्यक नहीं है।

पोस्ट-प्रोसेसिंग में, आप उन स्थानों को भी हाइलाइट कर सकते हैं जहां द्रव की गति सबसे अधिक होती है, गति वेक्टर के आधार पर रंग बदलते हैं, विभिन्न प्रभाव जोड़ते हैं, आदि, लेकिन हमारे मामले में हम खुद को एक प्रकार की न्यूनतम तक सीमित कर लेंगे, क्योंकि इसके साथ भी छवियां बहुत आकर्षक हैं (विशेष रूप से गतिशीलता में) :

कर्नेल। क्यू (पेंट)
 // adds flashlight effect near the mouse position __global__ void applyBloom(uint8_t* colorField, size_t xSize, size_t ySize, int xpos, int ypos, float bloomIntense) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; int pos = 4 * (y * xSize + x); float e = expf(-(powf(x - xpos, 2) + powf(y - ypos, 2)) * (1.0f / (bloomIntense + 1e-5f))); uint8_t R = colorField[pos + 0]; uint8_t G = colorField[pos + 1]; uint8_t B = colorField[pos + 2]; uint8_t maxval = max(R, max(G, B)); colorField[pos + 0] = min(255.0f, R + maxval * e); colorField[pos + 1] = min(255.0f, G + maxval * e); colorField[pos + 2] = min(255.0f, B + maxval * e); } // fills output image with corresponding color __global__ void paint(uint8_t* colorField, Particle* field, size_t xSize, size_t ySize) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; float R = field[y * xSize + x].color.R; float G = field[y * xSize + x].color.G; float B = field[y * xSize + x].color.B; colorField[4 * (y * xSize + x) + 0] = min(255.0f, 255.0f * R); colorField[4 * (y * xSize + x) + 1] = min(255.0f, 255.0f * G); colorField[4 * (y * xSize + x) + 2] = min(255.0f, 255.0f * B); colorField[4 * (y * xSize + x) + 3] = 255; } 

और अंत में, हमारे पास अभी भी एक मुख्य कार्य है जिसे हम main.cpp - computeField से कहते हैं। यह एल्गोरिथम के सभी टुकड़ों को एक साथ जोड़ता है, वीडियो कार्ड पर कोड को कॉल करता है, और जीपीयू से सीपीयू तक के डेटा को भी कॉपी करता है। इसमें गति वेक्टर की गणना और डाई रंग की पसंद भी शामिल है, जिसे हम लागू करते हैं :

kernel.cu (मुख्य कार्य)
 // main function, calls vorticity -> diffusion -> force -> pressure -> project -> advect -> paint -> bloom void computeField(uint8_t* result, float dt, int x1pos, int y1pos, int x2pos, int y2pos, bool isPressed) { dim3 threadsPerBlock(sConfig.xThreads, sConfig.yThreads); dim3 numBlocks(xSize / threadsPerBlock.x, ySize / threadsPerBlock.y); // curls and vortisity computeVorticity<<<numBlocks, threadsPerBlock>>>(vorticityField, oldField, xSize, ySize); applyVorticity<<<numBlocks, threadsPerBlock>>>(newField, oldField, vorticityField, xSize, ySize, config.vorticity, dt); std::swap(oldField, newField); // diffuse velocity and color computeDiffusion(numBlocks, threadsPerBlock, dt); // apply force if (isPressed) { timeSincePress = 0.0f; elapsedTime += dt; // apply gradient to color int roundT = int(elapsedTime) % colorArraySize; int ceilT = int((elapsedTime) + 1) % colorArraySize; float w = elapsedTime - int(elapsedTime); currentColor = colorArray[roundT] * (1 - w) + colorArray[ceilT] * w; Vec2 F; float scale = config.forceScale; Fx = (x2pos - x1pos) * scale; Fy = (y2pos - y1pos) * scale; Vec2 pos = { x2pos * 1.0f, y2pos * 1.0f }; applyForce<<<numBlocks, threadsPerBlock>>>(oldField, xSize, ySize, currentColor, F, pos, config.radius, dt); } else { timeSincePress += dt; } // compute pressure computePressure(numBlocks, threadsPerBlock, dt); // project project<<<numBlocks, threadsPerBlock>>>(oldField, xSize, ySize, pressureOld); cudaMemset(pressureOld, 0, xSize * ySize * sizeof(float)); // advect advect<<<numBlocks, threadsPerBlock>>>(newField, oldField, xSize, ySize, config.densityDiffusion, dt); std::swap(newField, oldField); // paint image paint<<<numBlocks, threadsPerBlock>>>(colorField, oldField, xSize, ySize); // apply bloom in mouse pos if (config.bloomEnabled && timeSincePress < 5.0f) { applyBloom<<<numBlocks, threadsPerBlock>>>(colorField, xSize, ySize, x2pos, y2pos, config.bloomIntense / timeSincePress); } // copy image to cpu size_t size = xSize * ySize * 4 * sizeof(uint8_t); cudaMemcpy(result, colorField, size, cudaMemcpyDeviceToHost); cudaError_t error = cudaGetLastError(); if (error != cudaSuccess) { std::cout << cudaGetErrorName(error) << std::endl; } } 

निष्कर्ष


इस लेख में, हमने नवियर-स्टोक्स समीकरण को हल करने के लिए एक संख्यात्मक एल्गोरिथ्म का विश्लेषण किया और एक अपूर्ण तरल पदार्थ के लिए एक छोटा सिमुलेशन प्रोग्राम लिखा। शायद हम सभी पेचीदगियों को नहीं समझते थे, लेकिन मैं आशा करता हूं कि सामग्री आपके लिए दिलचस्प और उपयोगी हो, और कम से कम द्रव मॉडलिंग के क्षेत्र में एक अच्छे परिचय के रूप में सेवा की।

इस लेख के लेखक के रूप में, मैं ईमानदारी से किसी भी टिप्पणी और परिवर्धन की सराहना करूंगा, और इस पोस्ट के तहत आपके सभी सवालों के जवाब देने की कोशिश करूंगा।

अतिरिक्त सामग्री


आप इस लेख में मेरे जीथूब भंडार में सभी स्रोत कोड पा सकते हैं। सुधार के किसी भी सुझाव का स्वागत है।

मूल सामग्री जो इस लेख के लिए आधार के रूप में कार्य करती है, आप एनवीडिया की आधिकारिक वेबसाइट पर पढ़ सकते हैं। यह शेडर्स की भाषा में एल्गोरिथ्म के कुछ हिस्सों के कार्यान्वयन के उदाहरण भी प्रस्तुत करता है:
developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch38.html

हेल्महोल्ट्ज़ अपघटन प्रमेय का प्रमाण और द्रव यांत्रिकी पर भारी मात्रा में अतिरिक्त सामग्री इस पुस्तक में पाई जा सकती है (अंग्रेजी, खंड 1.2 देखें):
चोरिन, एजे और जेई मार्सडेन। 1993. द्रव यांत्रिकी का गणितीय परिचय। तीसरा संस्करण। स्प्रिंगर।

एक अंग्रेजी बोलने वाले YouTube का चैनल, गणित से संबंधित गुणवत्ता की सामग्री बनाना, और विशेष रूप से (अंग्रेजी) में अंतर समीकरणों को हल करना। मदद करने के लिए बहुत वर्णनात्मक वीडियो आप गणित और भौतिकी में बहुत सी बातें का सार समझ में:
3Blue1Brown - यूट्यूब
अंतर समीकरण (3Blue1Brown)

इसके अलावा धन्यवाद WhiteBlackGoose सहायता के लिए इस लेख के लिए सामग्री तैयार करने में।


और अंत में, एक छोटा सा बोनस - कार्यक्रम में लिया गया कुछ सुंदर स्क्रीनशॉट:


डायरेक्ट स्ट्रीम (डिफ़ॉल्ट सेटिंग्स)


व्हर्लपूल (एप्लायर्स में बड़ा त्रिज्या)


वेव (उच्च vorticity + प्रसार)

इसके अलावा, लोकप्रिय मांग से, मैंने सिमुलेशन के साथ एक वीडियो जोड़ा:

Source: https://habr.com/ru/post/hi470742/


All Articles