Моделирование жидкости методом SPH

Материал из Department of Theoretical and Applied Mechanics
Перейти к: навигация, поиск
Курсовые работы по ВМДС: 2016-2017 > Моделирование жидкости методом SPH

Курсовой проект по Механике дискретных сред

Исполнитель: Шварёв Николай

Группа: 09 (43604/1)

Семестр: осень 2016

Описание метода[править]

SPH (Smoothed Particle Hydrodynamics, Гидродинамика сглаженных частиц) - вычислительный лагранжев метод для симуляции жидкостей и газа, который используется в биологии, астрофизике, баллистике, вулканографии, океанографии.

Сутью метода SPH является разбиение жидкости на дискретные элементы-частицы. Любая физическая величина любой частицы может быть получена путём суммирования соответствующих величин всех частиц которые находятся в пределах двух сглаженных с помощью функции ядра длин.

Значение любой величины A на любом расстоянии r задается формулой:

[math]A(\pmb{r})= \sum_{j}m_{j}\frac{A_{j}}{ρ_{j}}W(|\pmb r -\pmb r_{j}|,h) \qquad \qquad \qquad \qquad \qquad j = 1...N,[/math]

где [math]m_{j}[/math] - масса частицы j, [math]A_{j}[/math] - значение величины A для частицы j, [math]ρ_{j}[/math] - плотность частицы j, W - функция ядра, h - радиус обрезания.

Таким образом, плотность любой частицы высчитывается по формуле:

[math]ρ(\pmb{r})= \sum_{j}m_{j}W(|\pmb r -\pmb r_{j}|,h) \qquad \qquad \qquad \qquad \qquad i,j = 1...N,[/math]

В нашем случае роль функции ядра исполняет функция Люси:

[math] W(r \lt h) = \frac{5}{π{b}^{2}}(1+3\frac{r}{h}){(1-\frac{r}{h})}^{3}, [/math]

где [math]r = |\pmb r - \pmb r_{j}|[/math]

Используя уравнение Эйлера, уравнения изменения плотности и положения

[math] \begin{cases} \frac{d\pmb v}{dt} = -\frac{1}{ρ}∇P + \pmb g \\ \frac{dρ}{dt} = -ρ∇\cdot\pmb v \\ \frac{d\pmb r}{dt} = \pmb v \\ \end{cases} [/math]

получаем уравнение движения каждой частицы:

[math]\frac{d\pmb v_{a}}{dt} = - \sum_{b}m_{b}(\frac{P_{b}}{{ρ_{b}}^{2}} + \frac{P_{a}}{{ρ_{a}}^{2}} + П_{ab})∇_{a}W_{ab} \qquad \qquad \qquad \qquad \qquad a,b = 1...N,[/math]

где давление P в нашей задаче вычисляется по формуле:

[math]P = B (({\frac{ρ}{ρ_{0}}})^{\gamma} -1)[/math],

B - модуль упругости, [math]ρ_{0}[/math] - равновесная плотность.

А [math]П_{ab}[/math] - член, отвечающий за вязкость, который в нашем случае рассчитывается так:

[math]П_{ab} = -\frac{\alpha h c}{ρ_{a b}}(\frac{\pmb v_{ab} \cdot \pmb r_{ab}}{{r_{ab}}^{2}+0.01{h}^{2}}) [/math]

Поставленная задача[править]

Рассмотреть двумерную задачу о разрушении плотины: как поведет себя столб жидкости при разрушении (исчезновении) одной из стенок, удерживающей этот столб.

Реализация[править]

Текст программы на языке JavaScript:

Файл "4.js"

  1 // m: Вязкоупругий шар
  2 // Используется исходник Воробьёва Сергея от исходиника программы Цветкова Balls Версия 6.3 от 05.05.2014
  3 
  4 
  5 function MainBalls(canvas, slider_01, text_01, slider_02, text_02) {
  6 
  7     canvas.onselectstart = function () {return false;};     // запрет выделения canvas
  8 
  9     // Предварительные установки
 10 
 11     var context = canvas.getContext("2d");                  // на context происходит рисование
 12     canvas.oncontextmenu = function (e) {return false;};    // блокировка контекстного меню
 13 
 14     var Pi = 3.1415926;                 // число "пи"
 15 	var count = 0;						// счетчик в первом присваивании ro0
 16 	
 17 	var γ = 7;
 18 	var ro0 = 1;
 19 	var alpha = 1;
 20 	var speedOfLight = 300000000;
 21 	//var Bb = ro0 * speedOfLight* speedOfLight / 7;                          // модуль упругости по определению, но частицы разлетаются
 22 	var Bb = 10;											//был 1, потом 5
 23 	var Cab = 0.5;										//более красивое при ~2, но изначально было 5
 24 	
 25 	
 26     var m0 = 1;                         // масштаб массы
 27     var t0 = 1;                         // масштаб времени (период колебаний исходной системы)
 28     var a0 = 1;                         // масштаб расстояния (диаметр шара)
 29 
 30     var g0 = a0 / t0 / t0;              // масштаб ускорения (ускорение, при котором за t0 будет пройдено расстояние a0)
 31     var k0 = 2 * Pi / t0;               // масштаб частоты
 32     var C0 = m0 * k0 * k0;              // масштаб жесткости
 33     var B0 = 2 * m0 * k0;               // масштаб вязкости
 34 
 35     // *** Задание физических параметров ***
 36 
 37     var Ny = 50	;   //увеличить до 50-100, было 16, потом 22                     // число шаров, помещающихся по вертикали в окно (задает размер шара относительно размера окна)
 38     var m = 1* m0;                     // масса
 39     var CWall = 10 * C0;                // жесткость стен
 40     var CBall = 0.1 * CWall;            // жесткость между частицами
 41     var BVisc = 0.008 * B0;             // вязкость среды        было 0.008
 42     var BWall = 0.03 * B0;              // вязкость на стенках
 43     
 44     var r = 0.5 * a0;                   // радиус частицы в расчетных координатах
 45     var K = 0.7; //   ???                       // все силы, зависящие от радиуса, ограничиваются значением, реализующимся при r/a = K
 46     var a = 2 * r;                      // равновесное расстояние между частицами
 47     var aCut = 2.5 * r;                   // радиус обрезания
 48 	var alfa = 2;                     // коэффициент для хрупкого вз. Лен-Дж
 49 
 50     // *** Задание вычислительных параметров ***
 51 
 52     var fps = 20;                       // frames per second - число кадров в секунду (качечтво отображения)
 53     var spf = 100;                      // steps per frame   - число шагов интегрирования между кадрами (скорость расчета)
 54     var dt  = 0.04 * t0 / fps;          // шаг интегрирования (качество расчета)    //было 0.04*to/fps
 55 	var mg = 1 * m * g0;             // сила тяжести в начальный момент времени
 56 
 57     // Выполнение программы
 58 	var sqrt3 = Math.sqrt(3);
 59     var r2 = r * r;                     // ___в целях оптимизации___
 60     var a2 = a * a;                     // ___в целях оптимизации___
 61     var D = a2 * CBall / 72;            // энергия связи между частицами
 62     var LJCoeff = 12 * D / a2;          // коэффициент для расчета потенциала Л-Дж
 63     var bet = Math.pow(13 / 7, 1/6) * a;    // коэффициент для SLJ потенциала
 64     var bet2 = bet * bet;                     // ___в целях оптимизации___
 65     var SLJDenominator = 1 / (aCut * aCut - bet2);    // знаменатель для расчета SLJ потенциала
 66 	var sqrtkoef = Math.sqrt(alfa/(1+alfa));  //___в целях оптимизации___
 67 
 68     var Ka = K * a;                     // ___в целях оптимизации___
 69     var K2a2 = K * K * a2;              // ___в целях оптимизации___
 70 
 71     var dNd = null;                     // ссылка на захваченный курсором шар (drag & drop)
 72 	
 73 	var numberOfBorder = 0;
 74 
 75     this.setSlider_01 = function(c) {mg = c * m * g0;}; // функция для слайдера гравитации; 
 76 	this.setSlider_02 = function(c) {10;}; // функция для слайдера количества шаров по Х; 
 77 	
 78     // Настройка интерфейса
 79 
 80     slider_01.min = 0;               slider_01.max = 5;
 81     slider_01.step = 0.05;
 82     slider_01.value = mg / m / g0;          // начальное значение ползунка должно задаваться после min и max
 83     text_01.value = mg / m / g0;
 84 
 85 	
 86 //	slider_02.min = 1;
 87 //	slider_02.step = 1;
 88 //	slider_02.max = w-3;
 89 	
 90 
 91 	
 92     // Запуск новой системы
 93 
 94     // следующие переменные должны пересчитываться каждый раз, когда мы изменяем значение Ny
 95     var scale, w, h;
 96     var rScale13, rScaleShift;
 97     this.newSystem = function() {
 98         scale = canvas.height / Ny / a0;    // масштабный коэффициент для перехода от расчетных к экранным координатам
 99         w = canvas.width / scale;           // ширина окна в расчетных координатах
100         h = canvas.height / scale;          // высота окна в расчетных координатах
101 
102         rScale13 = r * scale * 1.3;         // ___в целях оптимизации___
103         rScaleShift = r * scale / 5;        // ___в целях оптимизации___
104 		
105 		this.setBorder();
106         this.setQuad(10,10);   //размером 10х10                 // сразу создаем конфигурацию
107 		this.setFence();
108     };
109 
110     // Работа с мышью
111 
112     var mx_, my_;                               // буфер позиции мыши (для расчета скорости при отпускании шара)
113 
114     canvas.onmousedown = function(e) {          // функция при нажатии клавиши мыши
115         var m = mouseCoords(e);                 // получаем расчетные координаты курсора мыши
116         // цикл в обратную сторону, чтобы захватывать шар, нарисованный "сверху"
117         // (т.к. цикл рисования идет в обычном порядке)
118         for (var i = balls.length - 1; i >= 0; i--) {
119             var b = balls[i];
120             var rx = b.x - m.x;
121             var ry = b.y - m.y;
122             var rLen2 = rx * rx + ry * ry;              // квадрат расстояния между курсором и центром шара
123             if (rLen2 <= r2) {                          // курсор нажал на шар
124                 if (e.which == 1) {                     // нажата левая клавиша мыши
125                     dNd = b;
126                     dNd.xPlus = dNd.x - m.x;            // сдвиг курсора относительно центра шара по x
127                     dNd.yPlus = dNd.y - m.y;            // сдвиг курсора относительно центра шара по y
128                     mx_ = m.x;    my_ = m.y;
129                     canvas.onmousemove = mouseMove;     // пока клавиша нажата - работает функция перемещения
130                 } else if (e.which == 3)                // нажата правая клавиша мыши
131                     balls.splice(i, 1);                 // удалить шар
132                 return;
133             }
134         }
135 
136         // если не вышли по return из цикла - нажатие было вне шара, добавляем новый
137         if (e.which == 1) {
138             dNd = addNewBall(m.x, m.y, true);   // добавляем шар и сразу захватываем его курсором
139             if (dNd == null) return;            // если шар не добавился (из за стен или других шаров) - возвращаемся
140             dNd.xPlus = 0;  dNd.yPlus = 0;      // держим шар по центру
141             mx_ = m.x;    my_ = m.y;
142             canvas.onmousemove = mouseMove;     // пока клавиша нажата - работает функция перемещения
143         }
144     };
145 
146     document.onmouseup = function(e) {          // функция при отпускании клавиши мыши
147         canvas.onmousemove = null;              // когда клавиша отпущена - функции перемещения нету
148         dNd = null;                             // когда клавиша отпущена - захваченного курсором шара нету
149     };
150 
151     function mouseMove(e) {                     // функция при перемещении мыши, работает только с зажатой ЛКМ
152         var m = mouseCoords(e);                 // получаем расчетные координаты курсора мыши
153         dNd.x = m.x + dNd.xPlus;
154         dNd.y = m.y + dNd.yPlus;
155         dNd.vx = 0.6 * (m.x - mx_) / dt / fps;   dNd.vy = 0.6 * (m.y - my_) / dt / fps;
156         mx_ = m.x;    my_ = m.y;
157     }
158 
159     function mouseCoords(e) {                   // функция возвращает расчетные координаты курсора мыши
160         var m = [];
161         var rect = canvas.getBoundingClientRect();
162         m.x = (e.clientX - rect.left) / scale;
163         m.y = (e.clientY - rect.top) / scale;
164         return m;
165     }
166 
167     // Работа с массивом
168 
169     var balls = [];                             // массив шаров
170     var addNewBall =  function(x, y, check) {
171         // проверка - не пересекается ли новый шар со стенами или уже существующими шарами
172         if (check) {
173             if (x - r < 0 || x + r > w || y - r < 0 || y + r > h) return null;
174             for (var i = 0; i < balls.length; i++) {
175                 var rx = balls[i].x - x;
176                 var ry = balls[i].y - y;
177                 var rLen2 = rx * rx + ry * ry;
178                 if (rLen2 < 4 * r2) return null;
179             }
180         }
181 
182         var b = [];
183 
184         b.x = x;                b.y = y;        // расчетные координаты шара
185         b.fx = 0;               b.fy = mg;      // сила, действующая на шар                  								    , вернул знак
186         b.vx = 0;               b.vy = 0;       // скорость 
187 		b.ro = 0;                         
188 		b.ro0 = ro0;
189 		b.ax = 0;
190 		b.ay = 0;
191 		b.m = m;
192 		
193 		b.latX = Math.floor(b.x / aCut);
194 		b.latY = Math.floor(b.y / aCut);
195 		
196 		balls[balls.length] = b;                // добавить элемент в конец массива
197         return b;
198     };
199   
200     this.setTriangularLattice = function(latX,latY) {            // задать на поле треугольную решетку (Серегин код, апгрейднутый мной)
201 //		countFence = 0;
202 		
203 	//        balls = [];
204        	for (var j = Math.floor(latY); j >= 0 ; j--)    
205           for (var i = 1; i < Math.floor(latX / r)-1  ; i++)
206               if ((i + j) % 2 == 0) addNewBall(4 * r + i*r, h - 4*r - r*sqrt3*j - r, false);
207 		  
208 	};
209     
210     this.setQuad = function(latX,latY) {               // квадратная конфигурация (мой код)  размера latX x latY
211 	    for (var j = Math.floor(latY); j > 0; j--)
212 		   for (var i = 0; i < Math.floor(latX); i++)
213 				addNewBall(r*2*(i+2) + r, h - 2*r*(j+1) -r ,false);
214 	
215     };
216 	
217 	this.setBorder = function(){                                                 //установка границ на пределе прорисовки
218 		balls = [];
219 		numberOfBorder = 0;
220 		
221 		this.deleteFence();
222 		for (var i = 0; i < 2; i++)
223 		{
224 			for (var j = 0; j < w ; j++){
225 				addNewBall(j + r, i*2*r + r,false);
226 				numberOfBorder++;			
227 			}
228 			for (var j = 0; j < w + 1; j++){
229 				addNewBall(j - r,h - 2 * r * i - r,false);
230 				numberOfBorder++;
231 			}
232 			for (var j = 2; j < h - 2; j++){
233 				addNewBall(2 * r * i + r, j + r, false);
234 				numberOfBorder++;
235 			}
236 			for (var j = 2; j < h - 2; j++){
237 				addNewBall(w - 2 * r * i - r / 2, j + r, false);
238 				numberOfBorder++;
239 			}
240 		}
241 	}	
242  	var countFence = 0;
243 	var countVarFence = true;
244 	this.setFence = function(){
245 		if (countVarFence){
246 			countFence = 0;
247 			for(var i = 0; i < 2; i++)
248 				for(var j = Math.round(3*h/4); j > 0 ; j--){
249 					addNewBall(26 * r + 2 * r * i , h - 3 * r - 2*r*j,false);
250 					countFence++;
251 			}
252 			countVarFence = false;
253 		}
254 
255 	}
256 	
257 	this.deleteFence = function(){
258 		b1 = balls;
259 		balls = [];
260 		for(var i = 0; i < b1.length - countFence; i++)
261 			balls[i] = b1[i];
262 		
263 		countFence = 0;
264 		countVarFence = true;
265 
266 	}
267 
268 
269   
270  // this.setEmpty = function() {balls = [];};                    //за ненадобностью, заменен на задание границ
271 
272     // Основной цикл программы
273 
274     function control() {
275         physics();
276         draw();
277     }
278 
279     // Расчетная часть программы
280 
281     function physics() {                            // то, что происходит каждый шаг времени
282         for (var s = 1; s <= spf; s++) {
283 
284 			for (var i = 0; i < balls.length; i++) {					//обнуление 
285                 balls[i].fy = 0;
286 				balls[i].fx = 0;
287 				balls[i].ro = 0;                         
288 				balls[i].ax = 0;
289 				balls[i].ay = 0;
290             }
291 
292 			impAverageX = 0;
293 			
294 
295 			for (var i = 0; i < balls.length; i++) {						//расчет плотностей
296 				var b1 = balls[i];
297                 for (var j = 0; j < balls.length; j++) {  
298 					var b2 = balls[j];	
299 					if((b2.latX == (b1.latX - 1)) | (b2.latX == (b1.latX)) | (b2.latX == (b1.latX + 1)) | (b2.latY == (b1.latY - 1)) | (b2.latY == (b1.latY)) | (b2.latY == (b1.latY + 1))){
300 	
301 						var rx = b1.x - b2.x;   
302 						var ry = b1.y - b2.y;         					// вектор смотрит на первый шар (на b1 из b2)
303 						var r2 = rx * rx + ry * ry;                         // квадрат расстояния между шарами
304 						var rLen = Math.sqrt(r2);                           //расстояние между шарами
305 						if (rLen <= aCut) {
306 		
307 							b1.ro += b2.m * (5 / (Pi * aCut * aCut)) * (1 + 3 * rLen / aCut) * (1 - rLen / aCut)* (1 - rLen / aCut)* (1 - rLen / aCut);  // ядро Люси
308 							//console.log(b1.ro);
309 						}
310 					}
311 				}
312 				balls[i] = b1;
313 
314 			}
315 			
316 /*			if (count != balls.length){														//изменение плотности при появлении нового шарика
317 
318 				var ro0 = balls[0].ro
319 				for(var i = 1; i < balls.length; i++){
320 					if (balls[i].ro < ro0){
321 						ro0 = balls[i].ro;
322 					}
323 							
324 				}
325 
326 				for (var i = 0; i < balls.length; i++){
327 					balls[i].ro0 = ro0;
328 				}
329 				count = balls.length;
330 
331 			}
332 */		
333 	
334 			for (var i = 0; i < balls.length; i++) {									    //расчет давлений для каждой частицы
335 				balls[i].p = Bb * (Math.pow(balls[i].ro/balls[i].ro0,γ) - 1); 
336 	//			balls[i].p = Bb*(balls[i].ro-ro0);                                      //с сайта кафедры
337  			}
338 
339 			
340 			for (var i = numberOfBorder; i < balls.length - countFence; i++) {									//расчет ускорений
341 				var b1 = balls[i];
342                 for (var j = 0; j < balls.length; j++) {                   
343                     var b2 = balls[j];	
344 					if((b2.latX == (b1.latX - 1)) | (b2.latX == (b1.latX)) | (b2.latX == (b1.latX + 1)) | (b2.latY == (b1.latY - 1)) | (b2.latY == (b1.latY)) | (b2.latY == (b1.latY + 1))){
345 
346 						var rx = b1.x - b2.x;   
347 						var ry = b1.y - b2.y;         					// вектор смотрит на первый шар (b1 из "нуля")
348 						var r2 = rx * rx + ry * ry;                         // квадрат расстояния между шарами
349 						var rLen = Math.sqrt(r2);                           //расстояние между шарами
350 						
351 						var vx = b1.vx - b2.vx;
352 						var vy = b1.vy - b2.vy;
353 
354 						if (rLen <= aCut) {
355 							var PabX = -alpha * aCut * Cab /((b1.ro + b2.ro)/2) * vx * rx/(rLen * rLen + 0.01 * aCut * aCut);
356 							var PabY = -alpha * aCut * Cab /((b1.ro + b2.ro)/2) * vy * ry/(rLen * rLen + 0.01 * aCut * aCut);
357 							
358 							b1.ax += b2.m * 60 / (Math.pow(aCut,6) * Pi) * (aCut - rLen) * (aCut - rLen) * (b2.p * rx / (b2.ro * b2.ro) + b1.p * rx / (b1.ro * b1.ro) + PabX * rx);
359 							b1.ay += b2.m * 60 / (Math.pow(aCut,6) * Pi) * (aCut - rLen) * (aCut - rLen) * (b2.p * ry / (b2.ro * b2.ro) + b1.p * ry / (b1.ro * b1.ro) + PabY * ry);
360 						}
361 					}
362 				}
363 			
364                 if (b1 == dNd) continue;  // если шар схвачен курсором - его вз. со стенами и перемещение не считаем
365 
366 //                if (b1.y + r > h) { b1.fy += -CWall * (b1.y + r - h) - BWall * b1.vy; }
367 //                if (b1.y - r < 0) { b1.fy += -CWall * (b1.y - r) - BWall * b1.vy;}
368 //                if (b1.x + r > w) { b1.fx += -CWall * (b1.x + r - w) - BWall * b1.vx; }
369 //                if (b1.x - r < 0) { b1.fx += -CWall * (b1.x - r) - BWall * b1.vx; }
370 				balls[i] = b1;
371 			}
372 		    for (var i = numberOfBorder; i < balls.length - countFence; i++){
373 				
374 				b1 = balls[i];
375 				b1.vx += (b1.fx + b1.ax) * dt;        
376 				b1.vy += (b1.fy + b1.ay + mg / m) * dt;
377 				b1.x += b1.vx * dt;             
378 				b1.y += b1.vy * dt;
379 				b1.latX = Math.floor(b1.x / aCut);
380 				b1.latY = Math.floor(b1.y / aCut);
381 				balls[i] = b1;
382 //				impAverageX += m * b1.vx;
383 				
384 			}
385 			//console.log(impAverageX/balls.length)
386 			//console.log(numberOfBorder);
387 				
388     }
389 }
390 
391     // Рисование
392   context.fillStyle = "#d3692e";
393     function draw() {
394         context.clearRect(0, 0, w * scale, h * scale);      // очистить экран
395         for (var i = 0; i < balls.length; i++){
396             var xS = balls[i].x * scale;           var yS = balls[i].y * scale;
397             context.beginPath();
398             context.arc(xS, yS, r * scale, 0, 2 * Math.PI, false);
399             context.closePath();
400             context.fill();
401         }
402     }
403 
404     // Запуск системы
405     this.newSystem();
406     setInterval(control, 1000 / fps);
407     // след. функция обновляет информацию о количестве частиц на поле
408     setInterval(function(){document.getElementById('ballsNum').innerHTML = balls.length;}, 1000 / 20);
409 }

Возможности программы[править]

В текущей версии программы можно рассматривать разрушение столба жидкости в изначально разных конфигурациях:

- квадратная решетка размером 10x38;

- треугольная решетка размером 11x38;

- пустое поле, в котором можно создать собственную конфигурацию (ЛКМ - создание частицы).

В любой момент можно поставить или убрать забор. Также есть возможность изменения гравитации от 0g до 5g.

Результаты[править]

Рисунок 1. Начальная конфигурация
Рисунок 2. Успокоившийся столб жидкости
Рисунок 3. Отскок от границы
Рисунок 4. Осевшее на дно












































Ссылки по теме[править]

  • J.J.Monaghan "Smoothed particle hydrodynamics", 1992;
  • W.G.Hoover, C.G.Hoover "SPAM-Based Recipes for Continuum Simulation".