├── .gitignore ├── README.md ├── bubble.py ├── domain.py ├── flow_solver.py ├── fluid.py ├── input ├── single_bubble.txt └── two_bubbles.txt ├── io_manager.py ├── main.py └── parameter.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | output/ 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README # 2 | 3 | ### Project title ### 4 | 5 | * 2D gas-liquid multiphase flows using Front-Tracking type method 6 | 7 | * The code can be used to simulate bubble rising or droplet falling 8 | 9 | ### Example 10 | 11 | 12 | 13 | ### Contact Details ### 14 | 15 | * Haryo Mirsandi 16 | -------------------------------------------------------------------------------- /bubble.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bubble class 3 | Created on Sat Jun 20 19:06:17 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | import numpy as np 9 | import math 10 | 11 | class Bubble: 12 | total = 0 13 | 14 | def __init__(self, center_x, center_y, radius, point): 15 | """ 16 | Initialize the bubble. 17 | """ 18 | self.center_x = center_x 19 | self.center_y = center_y 20 | self.radius = radius 21 | self.point = point 22 | self.x = np.zeros(self.point*3) 23 | self.y = np.zeros(self.point*3) 24 | self.x_old = np.zeros(self.point*3) 25 | self.y_old = np.zeros(self.point*3) 26 | Bubble.total += 1 27 | 28 | def initialize_front(self): 29 | """ 30 | Determine the location of the initial spherical bubble. 31 | """ 32 | for i in range(self.point+2): 33 | self.x[i] = self.center_x-self.radius*math.sin(2.0*math.pi*i/self.point) 34 | self.y[i] = self.center_y+self.radius*math.cos(2.0*math.pi*i/self.point) 35 | 36 | def store_old_variables(self): 37 | """ 38 | Store old variables for second order scheme. 39 | """ 40 | self.x_old = self.x.copy() 41 | self.y_old = self.y.copy() 42 | 43 | def store_2nd_order_variables(self): 44 | """ 45 | Store second order variables. 46 | """ 47 | self.x = 0.5*(self.x_old+self.x) 48 | self.y = 0.5*(self.y_old+self.y) 49 | 50 | def calculate_surface_tension(self, domain, fluid_prop, face): 51 | """ 52 | Calculate the surface tension force on the lagrangian grid and 53 | distribute it to the surrounding eulerian grid cells. 54 | """ 55 | # initialize the variables to store the tangent vector 56 | tan_x = np.zeros(self.point+2) 57 | tan_y = np.zeros(self.point+2) 58 | # calculate the tangent vector 59 | for i in range(self.point+1): 60 | dist = math.sqrt((self.x[i+1]-self.x[i])**2+ 61 | (self.y[i+1]-self.y[i])**2) 62 | tan_x[i] = (self.x[i+1]-self.x[i])/dist 63 | tan_y[i] = (self.y[i+1]-self.y[i])/dist 64 | tan_x[self.point+1] = tan_x[1] 65 | tan_y[self.point+1] = tan_y[1] 66 | 67 | # distribute the surface tension force to the eulerian grid 68 | for i in range(1, self.point+1): 69 | # force in x-direction 70 | force_x = fluid_prop.sigma*(tan_x[i]-tan_x[i-1]) 71 | self.distribute_lagrangian_to_eulerian( 72 | domain, face.force_x, self.x[i], self.y[i], force_x, 1) 73 | # force in y-direction 74 | force_y = fluid_prop.sigma*(tan_y[i]-tan_y[i-1]); 75 | self.distribute_lagrangian_to_eulerian( 76 | domain, face.force_y, self.x[i], self.y[i], force_y, 2) 77 | 78 | @staticmethod 79 | def distribute_lagrangian_to_eulerian(domain, cell, x, y, value, axis): 80 | """ 81 | Distribute a value from a lagrangian point to neighboring eulerian cells. 82 | """ 83 | # assign the grid size 84 | if (axis == 1): # x-dir 85 | d1 = domain.dx; 86 | d2 = domain.dy; 87 | else: # y-dir 88 | d1 = domain.dy; 89 | d2 = domain.dx; 90 | 91 | # get the eulerian cell indices 92 | [index_x, index_y] = domain.get_cell_index(x, y, axis) 93 | # calculate the weighing coefficients 94 | [coeff_x, coeff_y] = domain.get_weight_coeff(x, y, index_x, index_y, axis) 95 | # distribute the force to the surrounding eulerian cells 96 | cell[index_x ,index_y ] = cell[index_x ,index_y ] + \ 97 | (1.0-coeff_x)*(1.0-coeff_y)*value/d1/d2; 98 | cell[index_x+1,index_y ] = cell[index_x+1,index_y ] + \ 99 | coeff_x*(1.0-coeff_y)*value/d1/d2; 100 | cell[index_x ,index_y+1] = cell[index_x ,index_y+1] + \ 101 | (1.0-coeff_x)*coeff_y*value/d1/d2; 102 | cell[index_x+1,index_y+1] = cell[index_x+1,index_y+1] + \ 103 | coeff_x*coeff_y*value/d1/d2 104 | 105 | def update_front_location(self, face, param, domain): 106 | """ 107 | Advect the location of marker points using the interpolated velocity field. 108 | """ 109 | # interpolate the velocity from the eulerian grid to the location of 110 | # marker point 111 | u_x = np.zeros(self.point+2) 112 | u_y = np.zeros(self.point+2) 113 | for i in range(1, self.point+1): 114 | # interpolate velocity in x-direction 115 | u_x[i] = self.interpolate_velocity(domain, face.u, self.x[i], 116 | self.y[i], 1) 117 | # interpolate velocity in y-direction 118 | u_y[i] = self.interpolate_velocity(domain, face.v, self.x[i], 119 | self.y[i], 2) 120 | 121 | # advect the marker point 122 | for i in range(1, self.point+1): 123 | self.x[i] = self.x[i]+param.dt*u_x[i] 124 | self.y[i] = self.y[i]+param.dt*u_y[i] 125 | self.x[0] = self.x[self.point]; 126 | self.y[0] = self.y[self.point]; 127 | self.x[self.point+1] = self.x[1]; 128 | self.y[self.point+1] = self.y[1]; 129 | 130 | @staticmethod 131 | def interpolate_velocity(domain, face_vel, x, y, axis): 132 | """ 133 | Interpolate velocities located on eulerian cells to a lagrangian marker 134 | point. 135 | """ 136 | # get the eulerian cell index 137 | [index_x, index_y] = domain.get_cell_index(x, y, axis) 138 | # calculate the weighing coefficient 139 | [coeff_x, coeff_y] = domain.get_weight_coeff(x, y, index_x, index_y, axis) 140 | # interpolate the surrounding velocities to the marker location 141 | vel = (1.0-coeff_x)*(1.0-coeff_y)*face_vel[index_x ,index_y ]+ \ 142 | coeff_x *(1.0-coeff_y)*face_vel[index_x+1,index_y ]+ \ 143 | (1.0-coeff_x)* coeff_y *face_vel[index_x ,index_y+1]+ \ 144 | coeff_x * coeff_y *face_vel[index_x+1,index_y+1] 145 | 146 | return vel 147 | 148 | def restructure_front(self, domain): 149 | """ 150 | Restructure the front to maintain the quality of the interface. 151 | """ 152 | self.x_old = self.x.copy() 153 | self.y_old = self.y.copy() 154 | j = 0 155 | for i in range(1, self.point+1): 156 | # check the distance 157 | dst = math.sqrt(((self.x_old[i]-self.x[j])/domain.dx)**2 + 158 | ((self.y_old[i]-self.y[j])/domain.dy)**2) 159 | if (dst > 0.5): # too big 160 | # add marker points 161 | j = j+1 162 | self.x[j] = 0.5*(self.x_old[i]+self.x[j-1]) 163 | self.y[j] = 0.5*(self.y_old[i]+self.y[j-1]) 164 | j = j+1 165 | self.x[j] = self.x_old[i] 166 | self.y[j] = self.y_old[i] 167 | elif (dst < 0.25): 168 | pass 169 | else: 170 | j = j+1 171 | self.x[j] = self.x_old[i] 172 | self.y[j] = self.y_old[i] 173 | self.point = j; 174 | self.x[0] = self.x[self.point]; 175 | self.y[0] = self.y[self.point]; 176 | self.x[self.point+1] = self.x[1]; 177 | self.y[self.point+1] = self.y[1]; -------------------------------------------------------------------------------- /domain.py: -------------------------------------------------------------------------------- 1 | """ 2 | Computational domain related classes 3 | Created on Sat Jun 20 19:45:03 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | import numpy as np 9 | import math 10 | 11 | class Domain: 12 | def __init__(self, lx, ly, nx, ny, gravx, gravy): 13 | """ 14 | Initialize the domain parameters. 15 | """ 16 | self.lx = lx 17 | self.ly = ly 18 | self.nx = nx 19 | self.ny = ny 20 | self.gravx = gravx 21 | self.gravy = gravy 22 | self.dx = self.lx/self.nx; 23 | self.dy = self.ly/self.ny; 24 | 25 | def get_cell_index(self, x, y, axis): 26 | """ 27 | Fetch the indices of the eulerian cell located on the left of a 28 | given point. 29 | """ 30 | if (axis == 1): # x-dir 31 | index_x = math.floor(x/self.dx); 32 | index_y = math.floor((y+0.5*self.dy)/self.dy) 33 | else: # y-dir 34 | index_x = math.floor((x+0.5*self.dx)/self.dx) 35 | index_y = math.floor(y/self.dy) 36 | return index_x, index_y 37 | 38 | def get_weight_coeff(self, x, y, index_x, index_y, axis): 39 | """ 40 | Calculate the weight coefficients of a point with respect to its 41 | location inside the eulerian cell. 42 | """ 43 | if (axis == 1): # x-dir 44 | coeff_x = x/self.dx-index_x 45 | coeff_y = (y+0.5*self.dy)/self.dy-index_y 46 | else: # y-dir 47 | coeff_x = (x+0.5*self.dx)/self.dx-index_x 48 | coeff_y = y/self.dy-index_y 49 | return coeff_x, coeff_y 50 | 51 | class Face: 52 | def __init__(self, domain): 53 | """ 54 | Initialize variables (liquid is at rest at the beginning). 55 | """ 56 | # velocity in x-direction 57 | self.u = np.zeros((domain.nx+1, domain.ny+2)) 58 | self.u_old = np.zeros((domain.nx+1, domain.ny+2)) 59 | self.u_temp = np.zeros((domain.nx+1, domain.ny+2)) 60 | # velocity in y-direction 61 | self.v = np.zeros((domain.nx+2, domain.ny+1)) 62 | self.v_old = np.zeros((domain.nx+2, domain.ny+1)) 63 | self.v_temp = np.zeros((domain.nx+2, domain.ny+1)) 64 | # forces 65 | self.force_x = np.zeros((domain.nx+2, domain.ny+2)) 66 | self.force_y = np.zeros((domain.nx+2, domain.ny+2)) 67 | 68 | def initialize_force(self, domain): 69 | """ 70 | Set the forces to zero. 71 | """ 72 | self.force_x = np.zeros((domain.nx+2, domain.ny+2)) 73 | self.force_y = np.zeros((domain.nx+2, domain.ny+2)) 74 | 75 | def store_old_variables(self): 76 | """ 77 | Store old variables for second order scheme. 78 | """ 79 | self.u_old = self.u.copy() 80 | self.v_old = self.v.copy() 81 | 82 | def store_2nd_order_variables(self): 83 | """ 84 | Store second order variables. 85 | """ 86 | self.u = 0.5*(self.u+self.u_old) 87 | self.v = 0.5*(self.v+self.v_old) 88 | 89 | class Center: 90 | def __init__(self, domain): 91 | """ 92 | Initialize variables stored at cell center. 93 | """ 94 | # set the grid 95 | self.x = np.linspace(-0.5, domain.nx+2-1.5, domain.nx+2)*domain.dx 96 | self.y = np.linspace(-0.5, domain.ny+2-1.5, domain.ny+2)*domain.dy; 97 | # pressure 98 | self.pres = np.zeros((domain.nx+2, domain.ny+2)) 99 | 100 | -------------------------------------------------------------------------------- /flow_solver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flow solver 3 | Created on Mon Jun 22 18:48:00 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | import numpy as np 9 | 10 | class FlowSolver: 11 | @staticmethod 12 | def update_wall_velocity(domain, face): 13 | """ 14 | Update the wall velocity (the domain is currently assumed as a box 15 | with no-slip boundary condition). 16 | """ 17 | u_south = 0; 18 | u_north = 0; 19 | v_west = 0; 20 | v_east = 0; 21 | face.u[:,0] = 2*u_south-face.u[:,1] 22 | face.v[0,:] = 2*v_west -face.v[1,:] 23 | face.u[:,domain.ny+1] = 2*u_north-face.u[:,domain.ny] 24 | face.v[domain.nx+1,:] = 2*v_east -face.v[domain.nx,:] 25 | 26 | @staticmethod 27 | def calculate_temporary_velocity(param, domain, fluid_prop, fluid, face): 28 | """ 29 | Calculate the temporary velocities without accounting for the pressure 30 | (first step of the second order projection method). 31 | """ 32 | # temporary u velocity (advection term) 33 | face.u_temp[1:domain.nx,1:domain.ny+1] = \ 34 | face.u[1:domain.nx ,1:domain.ny+1]+param.dt*(-0.25* 35 | (((face.u[2:domain.nx+1,1:domain.ny+1]+ 36 | face.u[1:domain.nx ,1:domain.ny+1])**2- 37 | (face.u[1:domain.nx ,1:domain.ny+1]+ 38 | face.u[0:domain.nx-1,1:domain.ny+1])**2)/domain.dx+ 39 | ((face.u[1:domain.nx ,2:domain.ny+2]+ 40 | face.u[1:domain.nx ,1:domain.ny+1])* 41 | (face.v[2:domain.nx+1,1:domain.ny+1]+ 42 | face.v[1:domain.nx ,1:domain.ny+1])- 43 | (face.u[1:domain.nx ,1:domain.ny+1]+ 44 | face.u[1:domain.nx ,0:domain.ny ])* 45 | (face.v[2:domain.nx+1,0:domain.ny ]+ 46 | face.v[1:domain.nx ,0:domain.ny ]))/domain.dy)+ 47 | face.force_x[1:domain.nx,1:domain.ny+1]/ 48 | (0.5*(fluid.rho[2:domain.nx+1,1:domain.ny+1]+ 49 | fluid.rho[1:domain.nx ,1:domain.ny+1]))- 50 | (1.0 -fluid_prop.cont_rho/ 51 | (0.5*(fluid.rho[2:domain.nx+1,1:domain.ny+1]+ 52 | fluid.rho[1:domain.nx ,1:domain.ny+1])))*domain.gravx) 53 | 54 | # temporary v velocity (advection term) 55 | face.v_temp[1:domain.nx+1,1:domain.ny] = \ 56 | face.v[1:domain.nx+1,1:domain.ny ]+param.dt*(-0.25* 57 | (((face.u[1:domain.nx+1,2:domain.ny+1]+ 58 | face.u[1:domain.nx+1,1:domain.ny ])* 59 | (face.v[2:domain.nx+2,1:domain.ny ]+ 60 | face.v[1:domain.nx+1,1:domain.ny ])- 61 | (face.u[0:domain.nx ,2:domain.ny+1]+ 62 | face.u[0:domain.nx ,1:domain.ny ])* 63 | (face.v[1:domain.nx+1,1:domain.ny ]+ 64 | face.v[0:domain.nx ,1:domain.ny ]))/domain.dx+ 65 | ((face.v[1:domain.nx+1,2:domain.ny+1]+ 66 | face.v[1:domain.nx+1,1:domain.ny ])**2- 67 | (face.v[1:domain.nx+1,1:domain.ny]+ 68 | face.v[1:domain.nx+1,0:domain.ny-1])**2)/domain.dy)+ 69 | face.force_y[1:domain.nx+1,1:domain.ny]/ 70 | (0.5*(fluid.rho[1:domain.nx+1,2:domain.ny+1]+ 71 | fluid.rho[1:domain.nx+1,1:domain.ny ]))- 72 | (1.0 -fluid_prop.cont_rho/ 73 | (0.5*(fluid.rho[1:domain.nx+1,2:domain.ny+1]+ 74 | fluid.rho[1:domain.nx+1,1:domain.ny ])))*domain.gravy) 75 | 76 | # temporary u velocity (diffusion term) 77 | face.u_temp[1:domain.nx ,1:domain.ny+1] = \ 78 | face.u_temp[1:domain.nx ,1:domain.ny+1]+param.dt*((1./domain.dx)*2.* 79 | (fluid.mu[2:domain.nx+1,1:domain.ny+1]*(1./domain.dx)* 80 | (face.u[2:domain.nx+1,1:domain.ny+1]- 81 | face.u[1:domain.nx ,1:domain.ny+1])- 82 | fluid.mu[1:domain.nx ,1:domain.ny+1]*(1./domain.dx)* 83 | (face.u[1:domain.nx ,1:domain.ny+1]- 84 | face.u[0:domain.nx-1,1:domain.ny+1]))+(1./domain.dy)*(0.25* 85 | (fluid.mu[1:domain.nx ,1:domain.ny+1]+ 86 | fluid.mu[2:domain.nx+1,1:domain.ny+1]+ 87 | fluid.mu[2:domain.nx+1,2:domain.ny+2]+ 88 | fluid.mu[1:domain.nx ,2:domain.ny+2])*((1./domain.dy)* 89 | (face.u[1:domain.nx ,2:domain.ny+2]- 90 | face.u[1:domain.nx ,1:domain.ny+1])+(1./domain.dx)* 91 | (face.v[2:domain.nx+1,1:domain.ny+1]- 92 | face.v[1:domain.nx ,1:domain.ny+1]))-0.25* 93 | (fluid.mu[1:domain.nx ,1:domain.ny+1]+ 94 | fluid.mu[2:domain.nx+1,1:domain.ny+1]+ 95 | fluid.mu[2:domain.nx+1,0:domain.ny ]+ 96 | fluid.mu[1:domain.nx ,0:domain.ny ])*((1./domain.dy)* 97 | (face.u[1:domain.nx ,1:domain.ny+1]- 98 | face.u[1:domain.nx ,0:domain.ny ])+(1./domain.dx)* 99 | (face.v[2:domain.nx+1,0:domain.ny ]- 100 | face.v[1:domain.nx ,0:domain.ny ]))))/(0.5* 101 | (fluid.rho[2:domain.nx+1,1:domain.ny+1]+ 102 | fluid.rho[1:domain.nx ,1:domain.ny+1])) 103 | 104 | # temporary v velocity (diffusion term) 105 | face.v_temp[1:domain.nx+1,1:domain.ny ] = \ 106 | face.v_temp[1:domain.nx+1,1:domain.ny ]+param.dt*((1./domain.dx)*(0.25* 107 | (fluid.mu[1:domain.nx+1,1:domain.ny ]+ 108 | fluid.mu[2:domain.nx+2,1:domain.ny ]+ 109 | fluid.mu[2:domain.nx+2,2:domain.ny+1]+ 110 | fluid.mu[1:domain.nx+1,2:domain.ny+1])*((1./domain.dy)* 111 | (face.u[1:domain.nx+1,2:domain.ny+1]- 112 | face.u[1:domain.nx+1,1:domain.ny ])+(1./domain.dx)* 113 | (face.v[2:domain.nx+2,1:domain.ny ]- 114 | face.v[1:domain.nx+1,1:domain.ny ]))-0.25* 115 | (fluid.mu[1:domain.nx+1,1:domain.ny ]+ 116 | fluid.mu[1:domain.nx+1,2:domain.ny+1]+ 117 | fluid.mu[0:domain.nx ,2:domain.ny+1]+ 118 | fluid.mu[0:domain.nx ,1:domain.ny ])*((1./domain.dy)* 119 | (face.u[0:domain.nx ,2:domain.ny+1]- 120 | face.u[0:domain.nx ,1:domain.ny ])+(1./domain.dx)* 121 | (face.v[1:domain.nx+1,1:domain.ny ]- 122 | face.v[0:domain.nx ,1:domain.ny ])))+(1./domain.dy)*2.* 123 | (fluid.mu[1:domain.nx+1,2:domain.ny+1]*(1./domain.dy)* 124 | (face.v[1:domain.nx+1,2:domain.ny+1]- 125 | face.v[1:domain.nx+1,1:domain.ny ])- 126 | fluid.mu[1:domain.nx+1,1:domain.ny ]*(1./domain.dy)* 127 | (face.v[1:domain.nx+1,1:domain.ny ]- 128 | face.v[1:domain.nx+1,0:domain.ny-1])))/(0.5* 129 | (fluid.rho[1:domain.nx+1,2:domain.ny+1]+ 130 | fluid.rho[1:domain.nx+1,1:domain.ny ])) 131 | 132 | @staticmethod 133 | def solve_pressure(param, domain, fluid, face, center): 134 | """ 135 | Calculate the pressure field. 136 | """ 137 | # initialize variables 138 | temp1 = np.zeros((domain.nx+2, domain.ny+2)) 139 | temp2 = np.zeros((domain.nx+2, domain.ny+2)) 140 | 141 | # calculate source term and the coefficient for pressure 142 | rho_temp = fluid.rho.copy() 143 | large_num = 1000; 144 | rho_temp[:,0] = large_num; 145 | rho_temp[:,domain.ny+1] = large_num; 146 | rho_temp[0,:] = large_num; 147 | rho_temp[domain.nx+1,:] = large_num; 148 | 149 | temp1[1:domain.nx+1,1:domain.ny+1] = (0.5/param.dt)* \ 150 | ((face.u_temp[1:domain.nx+1,1:domain.ny+1] 151 | -face.u_temp[0:domain.nx ,1:domain.ny+1])/domain.dx+ 152 | (face.v_temp[1:domain.nx+1,1:domain.ny+1] 153 | -face.v_temp[1:domain.nx+1,0:domain.ny ])/domain.dy) 154 | 155 | temp2[1:domain.nx+1,1:domain.ny+1] = 1.0/((1./domain.dx)* 156 | (1./(domain.dx* 157 | (rho_temp[2:domain.nx+2,1:domain.ny+1]+ 158 | rho_temp[1:domain.nx+1,1:domain.ny+1]))+ 159 | 1./(domain.dx* 160 | (rho_temp[0:domain.nx ,1:domain.ny+1]+ 161 | rho_temp[1:domain.nx+1,1:domain.ny+1])))+(1./domain.dy)* 162 | (1./(domain.dy* 163 | (rho_temp[1:domain.nx+1,2:domain.ny+2]+ 164 | rho_temp[1:domain.nx+1,1:domain.ny+1]))+ 165 | 1./(domain.dy* 166 | (rho_temp[1:domain.nx+1,0:domain.ny ]+ 167 | rho_temp[1:domain.nx+1,1:domain.ny+1])))) 168 | 169 | # construct the pressure field using SOR 170 | # TODO: create SOR function 171 | for it in range(param.max_iter): 172 | old_pres = center.pres.copy() 173 | for iskip in range(2): 174 | rb = iskip 175 | center.pres[1+rb:domain.nx+1:2,1:domain.ny+1:2] = \ 176 | ((1.0-param.beta)* 177 | center.pres[1+rb:domain.nx+1:2,1:domain.ny+1:2]+param.beta* 178 | temp2[1+rb:domain.nx+1:2,1:domain.ny+1:2]* 179 | ((1.0/domain.dx/domain.dx)* 180 | (center.pres[2+rb:domain.nx+2:2,1:domain.ny+1:2]/ 181 | (rho_temp[2+rb:domain.nx+2:2,1:domain.ny+1:2]+ 182 | rho_temp[1+rb:domain.nx+1:2,1:domain.ny+1:2])+ 183 | center.pres[ rb:domain.nx :2,1:domain.ny+1:2]/ 184 | (rho_temp[ rb:domain.nx :2,1:domain.ny+1:2]+ 185 | rho_temp[1+rb:domain.nx+1:2,1:domain.ny+1:2]))+ 186 | (1.0/domain.dy/domain.dy)* 187 | (center.pres[1+rb:domain.nx+1:2,2:domain.ny+2:2]/ 188 | (rho_temp[1+rb:domain.nx+1:2,2:domain.ny+2:2]+ 189 | rho_temp[1+rb:domain.nx+1:2,1:domain.ny+1:2])+ 190 | center.pres[1+rb:domain.nx+1:2,0:domain.ny :2]/ 191 | (rho_temp[1+rb:domain.nx+1:2,0:domain.ny :2]+ 192 | rho_temp[1+rb:domain.nx+1:2,1:domain.ny+1:2]))- 193 | temp1[1+rb:domain.nx+1:2,1:domain.ny+1:2])) 194 | 195 | rb=1-iskip 196 | center.pres[1+rb:domain.nx+1:2,2:domain.ny+1:2] = \ 197 | ((1.0-param.beta)* 198 | center.pres[1+rb:domain.nx+1:2,2:domain.ny+1:2]+param.beta* 199 | temp2[1+rb:domain.nx+1:2,2:domain.ny+1:2]* 200 | ((1.0/domain.dx/domain.dx)* 201 | (center.pres[2+rb:domain.nx+2:2,2:domain.ny+1:2]/ 202 | (rho_temp[2+rb:domain.nx+2:2,2:domain.ny+1:2]+ 203 | rho_temp[1+rb:domain.nx+1:2,2:domain.ny+1:2])+ 204 | center.pres[ rb:domain.nx:2 ,2:domain.ny+1:2]/ 205 | (rho_temp[ rb:domain.nx:2 ,2:domain.ny+1:2]+ 206 | rho_temp[1+rb:domain.nx+1:2,2:domain.ny+1:2]))+ 207 | (1.0/domain.dy/domain.dy)* 208 | (center.pres[1+rb:domain.nx+1:2,3:domain.ny+2:2]/ 209 | (rho_temp[1+rb:domain.nx+1:2,3:domain.ny+2:2]+ 210 | rho_temp[1+rb:domain.nx+1:2,2:domain.ny+1:2])+ 211 | center.pres[1+rb:domain.nx+1:2,1:domain.ny :2]/ 212 | (rho_temp[1+rb:domain.nx+1:2,1:domain.ny :2]+ 213 | rho_temp[1+rb:domain.nx+1:2,2:domain.ny+1:2]))- 214 | temp1[1+rb:domain.nx+1:2,2:domain.ny+1:2])) 215 | 216 | center.pres[0,:] = center.pres[1,:]; 217 | center.pres[domain.nx+1,:] = center.pres[domain.nx,:] 218 | center.pres[:,0] = center.pres[:,1]; 219 | center.pres[:,domain.ny+1] = center.pres[:,domain.ny] 220 | 221 | if (np.abs(old_pres-center.pres).max() < param.max_err): 222 | break 223 | 224 | @staticmethod 225 | def correct_velocity(param, domain, fluid, face, center): 226 | """ 227 | Correct the velocity by adding the pressure gradient. 228 | """ 229 | # correct velocity in x-direction 230 | face.u[1:domain.nx,1:domain.ny+1] = \ 231 | face.u_temp[1:domain.nx ,1:domain.ny+1]-param.dt*(2.0/domain.dx)* \ 232 | (center.pres[2:domain.nx+1,1:domain.ny+1]- 233 | center.pres[1:domain.nx ,1:domain.ny+1])/ \ 234 | (fluid.rho[2:domain.nx+1,1:domain.ny+1]+ 235 | fluid.rho[1:domain.nx ,1:domain.ny+1]) 236 | 237 | # correct velocity in y-direction 238 | face.v[1:domain.nx+1,1:domain.ny] = \ 239 | face.v_temp[1:domain.nx+1,1:domain.ny ]-param.dt*(2.0/domain.dy)* \ 240 | (center.pres[1:domain.nx+1,2:domain.ny+1]- 241 | center.pres[1:domain.nx+1,1:domain.ny ])/ \ 242 | (fluid.rho[1:domain.nx+1,2:domain.ny+1]+ 243 | fluid.rho[1:domain.nx+1,1:domain.ny ]) -------------------------------------------------------------------------------- /fluid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fluid related classes 3 | Created on Sat Jun 20 19:55:00 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | import numpy as np 9 | 10 | class FluidProp: 11 | def __init__(self, cont_rho, cont_mu, disp_rho, disp_mu, sigma): 12 | """ 13 | Initialize the fluid properties of the continuous and dispersed phases. 14 | """ 15 | # continuous phase 16 | self.cont_rho = cont_rho 17 | self.cont_mu = cont_mu 18 | # dispersed phase 19 | self.disp_rho = disp_rho 20 | self.disp_mu = disp_mu 21 | self.sigma = sigma 22 | 23 | class Fluid: 24 | def __init__(self, domain, fluid_prop): 25 | """ 26 | Initialize the density and viscosity fields using the properties from 27 | continuous phase. 28 | """ 29 | self.rho = np.zeros((domain.nx+2, domain.ny+2))+fluid_prop.cont_rho 30 | self.rho_old = np.zeros((domain.nx+2, domain.ny+2))+fluid_prop.cont_rho 31 | self.mu = np.zeros((domain.nx+2, domain.ny+2))+fluid_prop.cont_mu 32 | self.mu_old = np.zeros((domain.nx+2, domain.ny+2))+fluid_prop.cont_mu 33 | 34 | def initialize_domain(self, domain, center, bubble_list, fluid_prop): 35 | """ 36 | Set the fluid properties inside the discrete phase with an initial 37 | spherical shape. 38 | """ 39 | for i in range(1,domain.nx+1): 40 | for j in range(1,domain.ny+1): 41 | for bub in bubble_list: 42 | if ((center.x[i]-bub.center_x)**2+ 43 | (center.y[j]-bub.center_y)**2 < bub.radius**2): 44 | self.rho[i,j] = fluid_prop.disp_rho 45 | self.mu[i,j] = fluid_prop.disp_mu 46 | 47 | def store_old_variables(self): 48 | """ 49 | Store old variables for second order scheme. 50 | """ 51 | self.rho_old = self.rho.copy() 52 | self.mu_old = self.mu.copy() 53 | 54 | def store_2nd_order_variables(self): 55 | """ 56 | Store second order variables. 57 | """ 58 | self.rho = 0.5*(self.rho+self.rho_old) 59 | self.mu = 0.5*(self.mu+self.mu_old) 60 | 61 | def update_density(self, param, domain, bubble_list, fluid_prop): 62 | """ 63 | Update the density field using the density jump at the lagrangian 64 | interface. 65 | Linear averaging is used to get the value at each cell. 66 | """ 67 | # initialize the variables to store the density jump 68 | face_x = np.zeros((domain.nx+2, domain.ny+2)) 69 | face_y = np.zeros((domain.nx+2, domain.ny+2)) 70 | # distribute the density jump to the eulerian grid 71 | for bub in bubble_list: 72 | for i in range(1, bub.point+1): 73 | # density jump in x-direction 74 | force_x = -0.5*(bub.y[i+1]-bub.y[i-1])* \ 75 | (fluid_prop.disp_rho-fluid_prop.cont_rho) 76 | bub.distribute_lagrangian_to_eulerian(domain, face_x, bub.x[i], 77 | bub.y[i], force_x, 1) 78 | # density jump in y-direction 79 | force_y = 0.5*(bub.x[i+1]-bub.x[i-1])* \ 80 | (fluid_prop.disp_rho-fluid_prop.cont_rho); 81 | bub.distribute_lagrangian_to_eulerian(domain, face_y, bub.x[i], 82 | bub.y[i], force_y, 2) 83 | 84 | # construct the density field using SOR 85 | # TODO: create SOR function 86 | for it in range(param.max_iter): 87 | old_rho = self.rho.copy() 88 | for iskip in range(2): 89 | rb = iskip 90 | self.rho[1+rb:domain.nx+1:2,1:domain.ny+1:2] = (1.0-param.beta)* \ 91 | self.rho[1+rb:domain.nx+1:2,1:domain.ny+1:2]+param.beta*0.25* \ 92 | (self.rho[2+rb:domain.nx+2:2,1:domain.ny+1:2]+ 93 | self.rho[ rb:domain.nx :2,1:domain.ny+1:2]+ 94 | self.rho[1+rb:domain.nx+1:2,2:domain.ny+2:2]+ 95 | self.rho[1+rb:domain.nx+1:2,0:domain.ny :2]+ 96 | domain.dx*face_x[ rb:domain.nx :2,1:domain.ny+1:2]- 97 | domain.dx*face_x[1+rb:domain.nx+1:2,1:domain.ny+1:2]+ 98 | domain.dy*face_y[1+rb:domain.nx+1:2,0:domain.ny :2]- 99 | domain.dy*face_y[1+rb:domain.nx+1:2,1:domain.ny+1:2]) 100 | 101 | rb = 1-iskip 102 | self.rho[1+rb:domain.nx+1:2,2:domain.ny+1:2] = (1.0-param.beta)* \ 103 | self.rho[1+rb:domain.nx+1:2,2:domain.ny+1:2]+param.beta*0.25* \ 104 | (self.rho[2+rb:domain.nx+2:2,2:domain.ny+1:2]+ 105 | self.rho[ rb:domain.nx:2 ,2:domain.ny+1:2]+ 106 | self.rho[1+rb:domain.nx+1:2,3:domain.ny+2:2]+ 107 | self.rho[1+rb:domain.nx+1:2,1:domain.ny :2]+ 108 | domain.dx*face_x[ rb:domain.nx:2 ,2:domain.ny+1:2]- 109 | domain.dx*face_x[1+rb:domain.nx+1:2,2:domain.ny+1:2]+ 110 | domain.dy*face_y[1+rb:domain.nx+1:2,1:domain.ny :2]- 111 | domain.dy*face_y[1+rb:domain.nx+1:2,2:domain.ny+1:2]) 112 | 113 | self.rho[0,:] = self.rho[1,:]; 114 | self.rho[domain.nx+1,:] = self.rho[domain.nx,:] 115 | self.rho[:,0] = self.rho[:,1]; 116 | self.rho[:,domain.ny+1] = self.rho[:,domain.ny] 117 | 118 | if (np.abs(old_rho-self.rho).max() < param.max_err): 119 | break 120 | 121 | def update_viscosity(self, fluid_prop): 122 | """ 123 | Update the viscosity field using harmonic averaging. 124 | """ 125 | self.mu = self.rho-fluid_prop.cont_rho 126 | self.mu = self.mu*(fluid_prop.disp_mu -fluid_prop.cont_mu )/ \ 127 | (fluid_prop.disp_rho-fluid_prop.cont_rho) 128 | self.mu = self.mu+fluid_prop.cont_mu -------------------------------------------------------------------------------- /input/single_bubble.txt: -------------------------------------------------------------------------------- 1 | SOLVER PARAMETERS: 2 | number of step = 250 3 | time step = 5.0e-03 4 | maximum iteration = 200 5 | maximum error = 1.0e-03 6 | beta = 1.5 7 | output frequency = 4 8 | 9 | NUMERICAL PARAMETERS: 10 | domain length in x-dir = 2e+00 11 | domain length in y-dir = 4e+00 12 | total cells in x-dir = 40 13 | total cells in y-dir = 80 14 | gravity in x-dir = 0.0e+00 15 | gravity in y-dir = 1.0e+02 16 | 17 | PHYSICAL PROPERTIES: 18 | DISPERSED PHASE: 19 | density = 1.0e+00 20 | dynamic viscosity = 5.0e-02 21 | surface tension = 4.0e+00 22 | CONTINUOUS PHASE: 23 | density = 2.0e+00 24 | dynamic viscosity = 1.0e-02 25 | 26 | BUBBLE SIZE AND LOCATION: 27 | number of bubbles = 1 28 | radius = 0.3 29 | location x = 1.0 30 | location y = 0.4 31 | marker points = 100 -------------------------------------------------------------------------------- /input/two_bubbles.txt: -------------------------------------------------------------------------------- 1 | SOLVER PARAMETERS: 2 | number of step = 360 3 | time step = 6.0e-03 4 | maximum iteration = 200 5 | maximum error = 1.0e-03 6 | beta = 1.5 7 | output frequency = 4 8 | 9 | NUMERICAL PARAMETERS: 10 | domain length in x-dir = 4e+00 11 | domain length in y-dir = 8e+00 12 | total cells in x-dir = 60 13 | total cells in y-dir = 120 14 | gravity in x-dir = 0.0e+00 15 | gravity in y-dir = 1.0e+02 16 | 17 | PHYSICAL PROPERTIES: 18 | DISPERSED PHASE: 19 | density = 1.0e+00 20 | kinematic viscosity = 5.0e-02 21 | surface tension = 6.0e+00 22 | CONTINUOUS PHASE: 23 | density = 2.0e+00 24 | kinematic viscosity = 1.0e-02 25 | 26 | BUBBLE SIZE AND LOCATION: 27 | number of bubbles = 2 28 | radius = 0.2 29 | location x = 1.7 30 | location y = 0.25 31 | marker points = 100 32 | radius = 0.2 33 | location x = 2.3 34 | location y = 0.25 35 | marker points = 100 -------------------------------------------------------------------------------- /io_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Input output manager 3 | Created on Thu Jun 18 12:23:28 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | import glob, os 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | from parameter import Parameter 12 | from domain import Domain 13 | from fluid import FluidProp 14 | from bubble import Bubble 15 | 16 | class IOManager: 17 | @staticmethod 18 | def clean_dir(dir, pattern): 19 | """ 20 | Clean output directory. 21 | """ 22 | for file in glob.glob(dir+"/"+pattern): 23 | os.remove(file) 24 | 25 | @staticmethod 26 | def create_dir(dir): 27 | """ 28 | Create output directory. 29 | """ 30 | os.makedirs(dir, exist_ok=True) 31 | 32 | @staticmethod 33 | def read_input(filepath): 34 | """ 35 | Read simulation parameters from input file. 36 | """ 37 | with open(filepath) as file: 38 | # solver parameters 39 | file.readline() 40 | nstep = int(file.readline().split("=")[1]) 41 | dt = float(file.readline().split("=")[1]) 42 | max_iter = int(file.readline().split("=")[1]) 43 | max_err = float(file.readline().split("=")[1]) 44 | beta = float(file.readline().split("=")[1]) 45 | out_freq = int(file.readline().split("=")[1]) 46 | param = Parameter(nstep, dt, max_iter, max_err, beta, out_freq) 47 | file.readline() 48 | # numerical parameters 49 | file.readline() 50 | lx = float(file.readline().split("=")[1]) 51 | ly = float(file.readline().split("=")[1]) 52 | nx = int(file.readline().split("=")[1]) 53 | ny = int(file.readline().split("=")[1]) 54 | gravx = float(file.readline().split("=")[1]) 55 | gravy = float(file.readline().split("=")[1]) 56 | domain = Domain(lx, ly, nx, ny, gravx, gravy) 57 | file.readline() 58 | # physical properties 59 | # dispersed phase 60 | file.readline() 61 | file.readline() 62 | disp_rho = float(file.readline().split("=")[1]) 63 | disp_mu = float(file.readline().split("=")[1]) 64 | sigma = float(file.readline().split("=")[1]) 65 | # continuous phase 66 | file.readline() 67 | cont_rho = float(file.readline().split("=")[1]) 68 | cont_mu = float(file.readline().split("=")[1]) 69 | fluid_prop = FluidProp(cont_rho, cont_mu, disp_rho, disp_mu, sigma) 70 | file.readline() 71 | # bubble size and location 72 | bubble_list = [] 73 | file.readline() 74 | nbub = int(file.readline().split("=")[1]) 75 | for num in range(nbub): 76 | radius = float(file.readline().split("=")[1]) 77 | center_x = float(file.readline().split("=")[1]) 78 | center_y = float(file.readline().split("=")[1]) 79 | point = int(file.readline().split("=")[1]) 80 | bubble_list.append(Bubble(center_x, center_y, radius, point)) 81 | return param, domain, fluid_prop, bubble_list 82 | 83 | @staticmethod 84 | def visualize_results(face, domain, fluid, fluid_prop, bubble_list, 85 | time, nstep): 86 | """ 87 | Visualize the phase fraction field, velocity vector, and marker points. 88 | """ 89 | # calculate phase fraction 90 | alpha = fluid.rho - fluid_prop.cont_rho 91 | alpha = alpha * 1/(fluid_prop.disp_rho-fluid_prop.cont_rho) 92 | # create grid and calculate velocity at the cell center 93 | grid_x = np.linspace(0, domain.lx, domain.nx+1) 94 | grid_y = np.linspace(0, domain.ly, domain.ny+1) 95 | u_center = np.zeros((domain.nx+1, domain.ny+1)) 96 | v_center = np.zeros((domain.nx+1, domain.ny+1)) 97 | u_center[0:domain.nx+1,0:domain.ny+1]=0.5*( 98 | face.u[0:domain.nx+1,1:domain.ny+2]+ 99 | face.u[0:domain.nx+1,0:domain.ny+1]) 100 | v_center[0:domain.nx+1,0:domain.ny+1]=0.5*( 101 | face.v[1:domain.nx+2,0:domain.ny+1]+ 102 | face.v[0:domain.nx+1,0:domain.ny+1]) 103 | vel_mag = np.sqrt(np.square(u_center) + np.square(v_center)) 104 | # plot the phase fraction 105 | plt.clf() 106 | plt.imshow(np.rot90(alpha[1:domain.nx+1,1:domain.ny+1]), cmap='jet', 107 | extent=[0,domain.lx,0,domain.ly], aspect=1) 108 | plt.clim(0, 1) 109 | plt.xticks(fontsize=7) 110 | plt.yticks(fontsize=7) 111 | # set figure title 112 | caption = 'Time = %.3f s'% time 113 | plt.title(caption, fontsize=8) 114 | # set the colorbar 115 | cbar = plt.colorbar() 116 | cbar.ax.set_title('Phase fraction', rotation=0, size=8) 117 | cbar.ax.tick_params(labelsize=7) 118 | # plot the velocity vector 119 | plt.quiver(grid_x, grid_y, u_center.T, v_center.T, color='w') 120 | # plot contour of velocity magnitude 121 | plt.contour(grid_x, grid_y, vel_mag.T, colors='black', alpha=0.2) 122 | # plot the marker points 123 | for bub in bubble_list: 124 | plt.plot(bub.x[0:bub.point],bub.y[0:bub.point],'k',linewidth=2) 125 | # save the plot 126 | caption = 'output/bub_%03d.png' % nstep 127 | plt.savefig(caption,dpi=150) 128 | plt.pause(0.001) 129 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | A two-dimensional gas-liquid multiphase flows using a front-tracking type 3 | method. A set of Navier-Stokes equation is solved on a eulerian grid 4 | using a second order projection method. The fluid properties are advected 5 | by lagrangian marker points. The time marching is second order by using 6 | predictor-corrector method. The code can be used to simulate a bubble 7 | rising in a rectangular box. 8 | Created by: Haryo Mirsandi 9 | """ 10 | 11 | # import 12 | from io_manager import IOManager 13 | from domain import Face, Center 14 | from fluid import Fluid 15 | from flow_solver import FlowSolver 16 | 17 | # clean output folder 18 | io_man = IOManager() 19 | io_man.create_dir('output') 20 | io_man.clean_dir('output','bub*.png') 21 | 22 | # read input file 23 | filepath = 'input/two_bubbles.txt' 24 | [param, domain, fluid_prop, bubble_list] = io_man.read_input(filepath) 25 | 26 | # initialize variables (grid, velocity, pressure, and force) 27 | flow_solver = FlowSolver() 28 | face = Face(domain) 29 | center = Center(domain) 30 | 31 | # initialize the physical properties inside the domain 32 | fluid = Fluid(domain, fluid_prop) 33 | fluid.initialize_domain(domain, center, bubble_list, fluid_prop) 34 | 35 | # set the initial front (gas-liquid interface) 36 | for bub in bubble_list: 37 | bub.initialize_front() 38 | 39 | # visualize the initial condition 40 | io_man.visualize_results(face, domain, fluid, fluid_prop, bubble_list, 41 | param.time, 0) 42 | 43 | # start time-loop 44 | for nstep in range(1, param.nstep+1): 45 | # store old variables 46 | face.store_old_variables() 47 | fluid.store_old_variables() 48 | for bub in bubble_list: 49 | bub.store_old_variables() 50 | 51 | # second order loop 52 | for substep in range(2): 53 | # calculate the surface tension force at the front (lagrangian grid) 54 | # and distribute it to eulerian grid 55 | face.initialize_force(domain) 56 | for bub in bubble_list: 57 | bub.calculate_surface_tension(domain, fluid_prop, face) 58 | 59 | # update the tangential velocity at boundaries 60 | flow_solver.update_wall_velocity(domain, face) 61 | 62 | # calculate the (temporary) velocity 63 | flow_solver.calculate_temporary_velocity(param, domain, fluid_prop, 64 | fluid, face) 65 | 66 | # solve pressure 67 | flow_solver.solve_pressure(param, domain, fluid, face, center) 68 | 69 | # correct the velocity by adding the pressure gradient 70 | flow_solver.correct_velocity(param, domain, fluid, face, center) 71 | 72 | # update the front location 73 | for bub in bubble_list: 74 | bub.update_front_location(face, param, domain) 75 | 76 | # update physical properties 77 | fluid.update_density(param, domain, bubble_list, fluid_prop) 78 | fluid.update_viscosity(fluid_prop) 79 | 80 | # substep end 81 | # store second order variables 82 | face.store_2nd_order_variables() 83 | fluid.store_2nd_order_variables() 84 | for bub in bubble_list: 85 | bub.store_2nd_order_variables() 86 | 87 | # restructure the front 88 | for bub in bubble_list: 89 | bub.restructure_front(domain) 90 | 91 | # visualize the results 92 | param.time = param.time+param.dt 93 | if (nstep % param.out_freq == 0): 94 | io_man.visualize_results(face, domain, fluid, fluid_prop, bubble_list, 95 | param.time, nstep) 96 | # end time-loop 97 | print('program finished') 98 | -------------------------------------------------------------------------------- /parameter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parameter class 3 | Created on Sat Jun 20 19:48:20 2020 4 | 5 | @author: mirsandiharyo 6 | """ 7 | 8 | class Parameter: 9 | def __init__(self, nstep, dt, max_iter, max_err, beta, out_freq): 10 | """ 11 | Initialize simulation parameters. 12 | """ 13 | self.nstep = nstep 14 | self.dt = dt 15 | self.max_iter = max_iter 16 | self.max_err = max_err 17 | self.beta = beta 18 | self.out_freq = out_freq 19 | self.time = 0.0 --------------------------------------------------------------------------------