Hasta ahora hemos dividido un intervalor \(\mathbb R\) mediante una partición.
Si vamos a trabajar con un problema un un sub-dominio de \(\mathbb R^d\) el primer paso es partirlo en regiones pequeñas, debemos extender esta idea a “mallas”.
Una malla es una división de nuestro dominios en polígonos convexos.
Dado un dominio en dimensión \(d \ge 2\) construir una descomposición es complicado. Hay diferentes software que resuelven este problema
Supuesto que ya tenemos una malla, la pregunta es ¿cómo escribo mis ecuaciones sobre la malla?
Una malla admisible de un conjunto \(\Omega\) es un par \((\mathcal T, \mathcal E)\) donde \(\mathcal T\) es es una familia de polígonos conexos \(K\) (llamados volúmenes de control), \(\mathcal E\) una familia de subconjuntos \(\sigma\) de \(\overline \Omega\) contenidos en hiperplanos de \(\mathbb R^d\) (llamados aristas o lados) tales que:
Son disjuntos: \(K , L \in \mathcal T\) distintos, entonces \(K \cap L = \emptyset\).
Para cada \(K\) existe \(\mathcal E_K \subset \mathcal E\) con \(\partial K = \cup_{\sigma \in \mathcal E_K} \overline \sigma\).
La unión de sus cierres genera \(\mathcal T\): \(\bigcup_{K \in \mathcal T} \overline K = \overline \Omega\)
Entre cualesquiera \(K, L \in \mathcal L\) distintos la “interfaz” \(\overline K \cap \overline L\) tiene medida \(\mathcal H^{d-1}\) igual 0 o es \(\overline \sigma\) con \(\sigma \in \mathcal E\). Llamamos a \(\sigma = K|L\).
Llamamos \(\mathcal N(K)\) el conjunto de \(L\) tales que \(\mathcal H^{d-1} (K|L) \ne 0\).
El flujo sigue siendo \[
\bm \flux = -\nabla \rho = -\left( \frac{\partial \rho}{\partial x} , \frac{\partial \rho}{\partial y} \right)
\]
Como en la malla cuadrada las normales son \((1,0)\) y \((0,1)\) (salvo el signo), es fácil hacer los cálculos. Hacemos las diferencias centradas \[
\frac{\diff}{\diff t} \rho_{ij} (t) = - \frac{2 \rho_{ij} (t) - \rho_{i+1,j}(t) - \rho_{i-1,j}(t) }{(\Delta x)^2} - \frac{2 \rho_{ij} (t) - \rho_{i,j+1}(t) - \rho_{i,j-1}(t) }{(\Delta y)^2}
\]
El método de Euler explícito nos lleva a una condición \[
2\frac{\Delta t}{(\Delta x)^2} + 2\frac{\Delta t}{(\Delta y)^2} \le 1.
\] Dado que ahora trabajamos con matrices \(N \times N\) es computacionalmente muy costosa.
En dimensión 1, era fácil despejar \(A \bm \rho^{n+1} = \bm \rho^n\).
Pero ahora \(\rho^n\) es una matriz, y ya no es tan fácil hacer esta escritura.
Producto de Kronecker
La manera natural de pensar en \(\rho_{ij}\) es como matriz.
Sin embargo, las aplicaciones sobre matrices son tensores de orden 3. Son difíciles de manejar.
Para evitar esto, hacemos un re-shape de la matriz \(N \times M\) a vector \(NM\)\[
\begin{aligned}
\mathrm{vec} : \mathcal M_{N,M} &\longrightarrow \mathbb R^{NM} \\
(\rho_{ij}) &\longmapsto \Big( (\rho_{11}, \rho_{12}, \cdots, \rho_{1M}), (\rho_{21}, \rho_{22}, \cdots, \rho_{2M}), \cdots , (\rho_{N1}, \rho_{N2}, \cdots, \rho_{NM}) \Big)^t
\end{aligned}
\]
Podemos pensar en esto es tomar ver esta operación como el “desmontaje” de un producto tensorial. Sean \(u_i\) la base canónica de \(\mathbb R^N\) y \(v_j\) la de \(\mathbb R^M\). Asociando el producto tensorial \(u_i \times v_j\) con la matriz que tiene sólo un 1 y en la entrada (\(i,j\)), podemos escribir \[
\rho = \sum_{i,j} \rho_{ij} u_i \otimes v_j.
\]
Dado que tenemos un operador (“diferencial”) operando sólo en \(u_i\) querríamos ver cómo es la matriz que corresponde a aplicarlos los dos. Querremos hacer lo mismo operando sólo en \(v_j\). Dados operadores \(A\) y \(B\), se define el operador \(A \otimes B\) de la siguiente forma \[
(A \otimes B) \rho = \sum_{i,j} \rho_{ij} (A u_i) \otimes (B v_j).
\] Si queremos expresar esto como una matriz \(C \in M_{NM, NM}\), la escribimos por bloques \[
C = \begin{pmatrix}
C_{11} & \cdots & C_{1N} \\
\vdots & \ddots & \vdots \\
C_{N1} & \cdots & C_{NN}
\end{pmatrix}, \qquad C_{ij } \in \mathcal M_{M,M}
\] Por un lado \[
(A\otimes B) (u_k \otimes v_\ell) = \left( \sum_i a_{ik} u_i \right) \otimes \left( \sum_j b_{j\ell} v_j \right) = \sum_{i,j} a_{ik}b_{j \ell} (u_i \otimes v_j) .
\] Comprobamos la entrada \(K = M(k-1) + \ell\)-ésima fila \[
(C \mathrm{vec} (u_k \otimes v_\ell) )_K = ( C e_{M(k-1) + \ell} )_K = C_{K , M(k-1) + \ell } = (C_{ik})_{j \ell},
\] de modo que \[
C_{ik} = a_{ik} B.
\] A la operación \(\otimes : \mathcal M_{N} \times \mathcal M_M \to \mathcal M_{NM}\)\[
A \otimes B = \begin{pmatrix}
a_{11}B & \cdots & a_{1N} B \\
\vdots & \ddots & \vdots \\
a_{N1} B & \cdots & a_{NN} B
\end{pmatrix}
\] se la denomina producto de Kronecker de las matrices.
Observación. Ocurre que si \(X\) es una matriz entonces
\[
(A \otimes B) \mathrm{vec} (X) = \mathrm{vec}( B^t X A )
\]
Programando \(\mathrm{vec}\) y \(\mathrm{vec}^{-1}\)
functionmat2vec(A) (N,M) =size(A) u =zeros(N*M,1)for i in1:N, j in1:M u[M*(i-1)+j] = A[i,j]endreturn uendfunctionvec2mat(u,N) M =div(length(u),N) A =zeros(N,M)for i in1:N, j in1:M A[i,j] = u[M*(i-1) + j] endreturn Aend;
Ecuación de transporte. Euler implícito.
Construímos la matriz \(A\) tal que \[A \mathrm{vec}(\rho^{n+1}) = \mathrm{vec}(\rho^n)\] mediante el producto de Kronecker.
Como es muy grande (\(N^2 \times N^2 = N^4\)) pero está llena de 0s, la almacenamos como matriz sparse
N=32; C =0.5;D =zeros(N,N)for i=1:N D[i,i] =1endfor i=1:N-1 D[i+1,i] =-1endD[1,N] =-1usingLinearAlgebra, Kronecker, SparseArraysId =Matrix(1.0I, N, N)A =sparse( Matrix(1.0I, N*N, N*N) + C * ( a1 * D ⊗ Id + a2 * Id ⊗ D ) )
Cuando se trabaja en dimensión espacial \(d>1\), especialmente con métodos implícitos, la complejidad computacional aumenta. Tomando \(N\) puntos en cada dimensión, el tamaño de \(\bm \rho\) es \(N^d\). Si se trabaja con un método implícito, las matrices que aparecen son de tamaño \(N^d\times N^d\) (generalmente sparse).
Una forma de lidiar con este problema es utilizar dimensional splitting.
Idea
Empecemos por la idea del splitting. Por establecer un marco de trabajo, consideremos el problema \[
\frac{\partial u}{\partial t} = Au + Bu, \qquad u(0) = u_0,
\] donde \(A,B\) son “operadores” lineales o no-lineales en \(u\).
Una idea que surge es resolver iterativamente problemas de la forma \[
\begin{aligned}
\frac{\partial v}{\partial t}&= Av, \qquad v(0) = v_0,\\
\frac{\partial w}{\partial t}& = Bw, \qquad w(0) = w_0,
\end{aligned}
\]
Para ser precisos queremos resolver \[
\begin{aligned}
\frac{\partial v}{\partial t}&= Av, \text{ para } t \in [0,1/n] \qquad v(0) = u_0,\\
\frac{\partial w}{\partial t}& = Bw, \text{ para } t \in [0,1/n]\qquad w(0) = v(1/n),\\
\\
\frac{\partial v}{\partial t}&= Av, \text{ para } t \in [1/n,2/n] \qquad v(1/n) = w(1/n),\\
\frac{\partial w}{\partial t}& = Bw, \text{ para } t \in [1/n,2/n] \qquad w(1/n) = v(2/n),
\\
\vdots
\end{aligned}
\]
Con la notación de semigrupos, denotemos \[
u(t) = S(t)u_0, \qquad v(t) = S_A (t) v_0, \qquad \text{y} w(t) = S_B (t) w_0
\] Definamos \[
u_k (t) = S_B (\tfrac t k ) \circ S_A (\tfrac t k ) \circ \overset k \cdots \circ S_B (\tfrac t k ) \circ S_A (\tfrac t k ).
\] En ocasiones se cumple que \[
u_k (t) \to u(t) , \qquad \text{ as } k \to \infty.
\]
Esta idea se puede extender a muchos operadores. La formulación más elegante del resultado es \[\begin{equation}
u(t) = \lim_{k\to \infty} \Big[ S_B (\tfrac t k ) S_A (\tfrac t k ) \Big]^k u_0
\end{equation}\] A esta fórmula se la conoce como fórmula de Trotter-Kato.
La intuición viene del caso lineal, en el que \[
e^{(A+B)t} = \lim_k \left( e^{B \tfrac t k } e^{A\tfrac t k } \right) ^k
\] Esta fórmula es exacta para, por ejemplo, matrices.
Dimensional splitting
Si queremos resolver un problema de forma \[
\frac{\partial \rho}{\partial t} = - \frac{\partial }{\partial x} f(\rho) - \frac{\partial }{\partial y} g(\rho)
\]
Podemos tomar \[
A u = - \frac{\partial}{\partial x} f(u) , \qquad B u = - \frac{\partial }{\partial y} g(u).
\]
Para calcular \(g_{ij}^{n+1}\) utilizamos \(\rho^*\) o \(\rho^{n+1}\), pero no \(\rho^n\).
Si el método es implícito, en casa paso tenemos que resolver:
\(M\) sistemas de \(N\) ecuaciones para calcular \(\bm \rho^*\)
\(N\) sistemas de \(M\) ecuaciones para calcular \(\bm \rho^{n+1}\)
Si \(N = M\) en el método de Newton hay \(2N\) matrices \(N \times N\). Pero no una matriz \((N^2) \times (N^2)\).
Observación. Strang demostró que la convergencia de \[
u(t) = \lim_{k\to \infty} \Big[ S_A (\tfrac t {2k} ) S_B (\tfrac t k ) S_A (\tfrac t {2k} ) \Big]^k u_0
\] es mejor que la de la fórmula original de Trotter-Kato.
Ecuación de transporte con condiciones periódicas
Construímos las matrices en ambas direcciones
T =3.0; N=32; (x,Δx,ρ) =create_mesh(N); C =0.5; Δt = C*ΔxD =zeros(N,N)for i=1:N D[i,i] =1endfor i=1:N-1 D[i+1,i] =-1endD[1,N] =-1Id =Matrix(1.0I, N, N)A1 =sparse( Id + C * a1 * D )A2 =sparse( Id + C * a2 * D )@giffor n=1:ceil(T/Δt)global ρmyplot(x,ρ, (n-1)*Δt, "Transporte. Euler implícito. Dimensional splitting")# Difusión en x con y fijafor j in1:N ρ[:,j] = A1 \ ρ[:,j] end# Difusión en y con x fijafor i in1:N ρ[i,:] = A2 \ ρ[i,:] endend
Ecuación de calor con condiciones periódicas
T =3e-2; N=32; (x,Δx,ρ) =create_mesh(N); Δt =1e-3; C = Δt / (Δx)^2A =sparse(SymTridiagonal((1+2*C)*ones(N),-C*ones(N-1)))A[1,N] = A[N,1] =-C;anim =@animatefor n=1:ceil(T/Δt) global ρmyplot(x,ρ,(n-1)*Δt,"Calor. Euler implícito. Dimensional splitting")# Difusión en x con y fijafor j in1:N ρ[:,j] = A \ ρ[:,j] end# Difusión en y con x fijafor i in1:N ρ[i,:] = A \ ρ[i,:] endendgif(anim,fps=3) # Reduce la velocidad del gif
Mallas generales
Habitualmente el dominio no es un cuadrado y, en general, no es deseable usar mallas cuadradas.
Sin embargo, en el caso de mallas generales más difícil hacer una teoría uniforme.
También se pueden añadir otras condiciones: Dirichlet, Robin, …
Programando la ecuación de transporte
El paquete Triangulate.jl
Podríamos trabajar con cualquier tipo de mallas. Pero vamos a trabajar con mallas triangulares (para mantenerlas en Elementos Finitos).
Para la construcción de la malla vamos a utilizar el paquete Triangulate.jl. Es una interfaz del algoritmo Triangle de Jonathan Richard Shewchuk.
Podremos una condición de masa sobre los diferentes triángulos, para que la malla sea pequeña.
usingTriangulate, Printf, Plotsfunctioncreate_mesh() triin=Triangulate.TriangulateIO() triin.pointlist=Matrix{Cdouble}([-1.0-1.0 ; 1.00.0 ; 1.01.0 ; 0.60.6; 0.01.0]') triin.segmentlist=Matrix{Cint}([12 ; 23 ; 34 ; 45 ; 51 ]') triin.segmentmarkerlist=Vector{Int32}([1, 2, 3, 4, 5]) maxarea=1e-2 area=@sprintf("%.15f",maxarea) (triout,vorout) =triangulate("pa$(area)DQ", triin)return trioutend;mesh =create_mesh();## Structure of the output# mesh.pointlist[i,j] i-th coordinate of the j-th point# mesh.trianglelist[i,j] = k means that mesh.pointlist[k] is the i-th vertex of the j-th triangle# segmentlist means that the segment from segmentlist[1,j] to segmentlist[2,j] is on the boundary
centroids =zeros(2,numberoftriangles(mesh))for j in1:numberoftriangles(mesh) centroids[:,j] =sum( [ mesh.pointlist[:, k] for k in mesh.trianglelist[:, j] ] )/3end
functionplot_triangle!(j) x = mesh.pointlist[1,1:end] y = mesh.pointlist[2,1:end] plt =plot!( [ x[mesh.trianglelist[1:3,j]]; x[mesh.trianglelist[1,j]]], [ y[mesh.trianglelist[1:3,j]]; y[mesh.trianglelist[1,j]]], color=:blue, label="", legend =:outertopright, )end# Plot all trianglesplot()for L in1:numberoftriangles(mesh)plot_triangle!(L)scatter!([centroids[1,L]],[centroids[2,L]],label="",color=:green)endplot!()
Área
Como solo vamos a trabajar con un malla triangular, podemos calcular las áreas con la fórmula de Heron
functionarea_of_triangle(vertices::Vector{Vector{Float64}}) # Formula de Heron para el area del triangulo de vértices vertices[1], vertices[2] y vertices[3] l1 =sqrt( (vertices[1][1]-vertices[2][1])^2+ (vertices[1][2] - vertices[2][2])^2 ) l2 =sqrt( (vertices[2][1]-vertices[3][1])^2+ (vertices[2][2] - vertices[3][2])^2 ) l3 =sqrt( (vertices[3][1]-vertices[1][1])^2+ (vertices[3][2] - vertices[1][2])^2 ) s = ( l1 + l2 + l3 ) /2returnsqrt( s * (s-l1) * (s-l2) * (s-l3) )endareas =ones(numberoftriangles(mesh))for k =1:numberoftriangles(mesh) index_of_vertices_of_T = mesh.trianglelist[:,k] vertices = [ mesh.pointlist[:,index_of_vertices_of_T[i]] for i in1:3 ] areas[k] =area_of_triangle(vertices)end
Para mallas generales se puede utilizar que \[
m(K) = \int_K 1 \, dx = \frac 1 d \int_K (\mathrm{div}(x)) dx = \frac 1 d \int_{\partial K} x \cdot n_K \approx \frac 1 d \sum_{\sigma \in \mathcal E_K} m(\sigma) x_{\sigma} \cdot n_{K,\sigma}
\]
donde \(x_\sigma \in \sigma\) y usar que, en dimensión 2, \(m(\sigma)\) es sólo la longitud de un segmento.
Vecinos
Vamos a almacenar las aristas
struct Edge index_of_start_point::Int # Index in mesh.pointlist index_of_end_point::Int # Index in mesh.pointlist mid_point::Vector triangles::Tuple{Int,Int} # Indexes in mesh.trianglelist to the sides unit_normal_exterior:: Vector{Real} # Unit vector exterior to triangles[1] length::Realend
functionunit_normal_vector(K::Int,edge::Edge)::Vectorif edge.triangles[1] == K unit_normal = edge.unit_normal_exteriorelseif edge.triangles[2] == K unit_normal =- edge.unit_normal_exteriorelseerror("$edge is not an edge of $K")endend;
Extraemos información adicional de la malla
Calculamos los vecinos y las aristas interiores, es decir, los triángulos con \(\sigma = K|L \in \mathcal E\)
# Store the interior edgesinterior_edges =Vector{Edge}()# Given a triangle, which triangle share which edges # neighbours[K] is a vector, whose elements are tuples (L,index_of_K|L)neighbours = [Vector{Tuple{Int64,Int64}}() for _ in1:numberoftriangles(mesh) ];
Ahora tenemos que rellenar estos vectores con la información de la malla
Code
for K in1:numberoftriangles(mesh)for L in K+1:numberoftriangles(mesh)ifsum([ point in mesh.trianglelist[:,L] for point in mesh.trianglelist[:,K] ]) ==2 indexes_of_points_in_edge =intersect( mesh.trianglelist[:,K] , mesh.trianglelist[:,L] ) index_third_point_in_K =setdiff(mesh.trianglelist[:,K],indexes_of_points_in_edge)[1] index_third_point_in_L =setdiff(mesh.trianglelist[:,L],indexes_of_points_in_edge)[1]# Recover the points P = mesh.pointlist[:,indexes_of_points_in_edge[1]] Q = mesh.pointlist[:,indexes_of_points_in_edge[2]] R = mesh.pointlist[:,index_third_point_in_K] S = mesh.pointlist[:,index_third_point_in_L] edge_tangent = P - Q edge_length =sqrt( sum(edge_tangent.^2 )) normal = [edge_tangent[2],-edge_tangent[1]] / edge_lengthifsum( (S-R).* normal ) <=0 normal .*=-1.0end edge =Edge(indexes_of_points_in_edge[1], indexes_of_points_in_edge[2], (P+Q)/2, (K,L), normal, edge_length )append!(interior_edges, [edge]) current_edge =length(interior_edges)append!(neighbours[K],[(L,current_edge)])append!(neighbours[L],[(K,current_edge)])endendend
Calculamos el flujo de \(K\) a \(L\)
functionvelocity_times_exterior_normal_on_edge(K::Int,edge::Edge) sum( velocity( edge.mid_point ) .*unit_normal_vector(K,edge) ) end
velocity_times_exterior_normal_on_edge (generic function with 1 method)
functionup_wind_flux(K,L,edge,ρ) v =velocity_times_exterior_normal_on_edge(K,edge)return ρ[K] *max(v,0) + ρ[L] *min(v,0)end
up_wind_flux (generic function with 1 method)
Avanzamos en tiempo y representamos la superficie con PyPlot (GR) no representa bien
Code
usingPyPlot # GR does not plot this wellpyplot(fmt=:png)
Inicializamos en \(\rho_0 = 1\) y dejamos correr el tiempo
velocity(x) =-xΔt =1e-2T =1ρ =ones(numberoftriangles(mesh))δ =zeros(numberoftriangles(mesh))anim =@animatefor t in0:Δt:Tglobal ρfor K in1:numberoftriangles(mesh) δ[K] =1/areas[K]*sum([ interior_edges[index_edge].length*up_wind_flux(K,L,interior_edges[index_edge],ρ) for (L,index_edge) in neighbours[K] ]) end ρ -= Δt * δ surface(centroids[1,:],centroids[2,:],ρ,title="t=$t",colorbar =false, zlims = (0,10))end every 1; fps=5
gif(anim)
Bibliografía
References
Eymard, R., Gallouët, T. & Herbin, R. (1997) Finite volume methods. In: Handbook of numerical analysis.