Finite Element Method, FEM, MES
|
|
|
|
Istnieje wiele wariantów i odmian FEM. Dzięki proponowanemu podejściu łatwiej się ''połapać''. Unikamy zamieszania wielością podejść do tematu, koncepcji i technicznych/implementacyjnych szczegółów.
Jak znaleźć wektor pewnej przestrzeni, który aproksymuje wektor przestrzeni o większym wymiarze?
Ogólna idea poszukiwania elementu (wektora/funkcji) u(x) pewnej przestrzeni przybliżającego zadany element f(x):
u(x)=N∑i=0cIψi(x)
gdzie
Sposób opisu i notacja zaproponowane w materiałach pozwalają na (nieco) łatwiejsze rozpoczęcie pracy i zrozumienie zasady działania wybrane w sposób ułatwiający zrozumienie open-source'owego pakietu FEniCS – biblioteki do obliczeń metodą elementów skończonych.
Zadanie:
Znajdź przybliżenie wektora f=(3,5) wzdłuż zadanego kierunku.
analogie: element - punkt - wektor - funkcja
|
|
V=span{ψ0}
Niech
(a,b) – punkt/wektor przestrzeni (dwuwymiarowej)
(u,v) – iloczyn skalarny dwóch wektorów przestrzeni (dowolnie-wymiarowej)
∂E∂c0=0
E(c0)=(e,e)=(f−u,f−u)=(f−c0ψ0,f−c0ψ0)=(f,f)−2c0(f,ψ0)+c20(ψ0,ψ0)
∂E∂c0=−2(f,ψ0)+2c0(ψ0,ψ0)=0
c0=(f,ψ0)(ψ0,ψ0)=3a+5ba2+b2
Spostrzeżenie (na później): warunek znikania pochodnej (1) można równoważnie zapisać jako:
(e,ψ0)=0
(e,ψ0)=(f−u,ψ0)=(f,ψ0)−(u,ψ0)....(∗−2...)
[f−u]⋅[ψ0]=[f]⋅[ψ0]−[u]⋅[ψ0]
|
|
Dla danego wektora f, znajdź przybliżenie u∈V:
V=span{ψ0,…,ψN}
(span czyt. przestrzeń rozpięta na wektorach...)
Mając dany zbiór wektorów niezależnych liniowo ψ0,…,ψN, dowolny wektor u∈V można zapisać jako:
u=N∑j=0cjψj
Wykazać, że przy pomocy kombinacji liniowej pewnych 3 wektorów przestrzeni 3D można skonstruować dowolny wektor tej przestrzeni.
Idea: znaleźć takie c0,…,cN, aby E=||e||2 był minimalizowany, e=f−u.
E(c0,…,cN)=(e,e)=(f−∑jcjψj,f−∑jcjψj)=(f,f)−2N∑j=0cj(f,ψj)+N∑p=0N∑q=0cpcq(ψp,ψq)
∂E∂ci=0,i=0,…,N
Po odrobinie obliczeń otrzymuje się układ równań liniowych:
N∑j=0Ai,jcj=bi,i=0,…,NAi,j=(ψi,ψj)bi=(ψi,f)
Można pokazać, że poszukiwanie minimalnego ||e|| jest równoważne poszukiwaniu e ortogonalnego do wszystkich v∈V:
(e,v)=0,∀v∈V
co jest równoważne temu aby e był ortogonalny do każdego wektora bazowego:
(e,ψi)=0,i=0,…,N
Warunek ortogonalności – podstawa metody Galerkina. Generuje ten sam układ równań liniowych co MNK.
Niech V będzie przestrzenią funkcyjną rozpiętą na zbiorze funkcji bazowych ψ0,…,ψN,
V=span{ψ0,…,ψN}
Dowolną funkcję tej przestrzeni u∈V można przedstawić jako kombinację liniową funkcji bazowych:
u=∑j∈Iscjψj,Is={0,1,…,N}
Tak jak dla przestrzeni wektorowych, minimalizujemy normę błędu E, ze względu na współczynniki cj, j∈Is:
E=(e,e)=(f−u,f−u)=(f(x)−∑j∈Iscjψj(x),f(x)−∑j∈Iscjψj(x))
∂E∂ci=0,i=∈Is
Czym jest iloczyn skalarny jeśli ψi jest funkcją?
(f,g)=∫Ωf(x)g(x)dx
(iloczyn skalarny funkcji cg jako uogólnienie il. skalarnego funkcji dyskretnych (u,v)=∑jujvj)
E(c0,…,cN)=(e,e)=(f−u,f−u)=(f,f)−2∑j∈Iscj(f,ψi)+∑p∈Is∑q∈Iscpcq(ψp,ψq)
∂E∂ci=0,i=∈Is
Obliczenia identyczne jak dla przypadku wektorowego -> w rezultacie otrzymujemy układ równań liniowych
N∑j∈IsAi,jcj=bi, i∈Is,Ai,j=(ψi,ψj), bi=(f,ψi)
Jak poprzednio minimalizacja (e,e) jest równoważna
(e,ψi)=0,i∈Is
co z kolei jest równoważne
(e,v)=0,∀v∈V
Równoważność wzorów jak dla przestrzeni wektorowych.
Równoważność wzorów jak dla wyprowadzenia przy pomocy MNK.
Dla zadanej funkcji f(x)=10(x−1)2−1 znaleźć jej przybliżenie funkcją liniową.
V=span{1,x}
czyli ψ0(x)=1, ψ1(x)=x oraz N=1. Szukane
u=c0ψ0(x)+c1ψ1(x)=c0+c1x
A0,0=(ψ0,ψ0)=∫211⋅1dx=1A0,1=(ψ0,ψ1)=∫211⋅xdx=3/2A1,0=A0,1=3/2A1,1=(ψ1,ψ1)=∫21x⋅xdx=7/3b1=(f,ψ0)=∫21(10(x−1)2−1)⋅1dx=7/3b2=(f,ψ1)=∫21(10(x−1)2−1)⋅xdx=13/3
Rozwiązanie układu równań 2x2:
c0=−38/3,c1=10,u(x)=10x−383
[1323273][c0c1]=[73133]→[c0c1]=[−38/310]
u(x)=10x−1223
Problem: napisać program/funkcję, który przeprowadzi obliczenia (obliczenie całek i rozwiązanie układu równań liniowych) i zwróci rozwiązanie postaci n u(x)=∑jcjψj(x).
Niech
sympy
oznaczoną symbolem f
(funkcję zmiennej (symbolu) x
)psi
będzie listą funkcji {ψi}i∈Is,Omega
będzie dwuelementową krotką/listą zawierającą początek i koniec przedziału Ωimport sympy as sym
def least_squares(f, psi, Omega):
N = len(psi) - 1
A = sym.zeros((N+1, N+1))
b = sym.zeros((N+1, 1))
x = sym.Symbol('x')
for i in range(N+1):
for j in range(i, N+1):
A[i,j] = sym.integrate(psi[i]*psi[j],
(x, Omega[0], Omega[1]))
A[j,i] = A[i,j]
b[i,0] = sym.integrate(psi[i]*f, (x, Omega[0], Omega[1]))
c = A.LUsolve(b)
u = 0
for i in range(len(psi)):
u += c[i,0]*psi[i]
return u, c
Spostrzeżenie: macierz układu jest symetryczna, dzięki czemu można zoptymalizować proces wyznaczania jej elementów
f
) , sym.integrate
zwróci wtedy obiekt typu sym.Integral
. Ulepszenie kodu: sprawdzenie czy takie zdarzenie wystąpiło i ew. obliczenia numeryczne.def least_squares(f, psi, Omega, symbolic=True):
...
for i in range(N+1):
for j in range(i, N+1):
integrand = psi[i]*psi[j]
if symbolic:
I = sym.integrate(integrand, (x, Omega[0], Omega[1]))
if not symbolic or isinstance(I, sym.Integral):
# Could not integrate symbolically,
# fall back on numerical integration
integrand = sym.lambdify([x], integrand)
I = sym.mpmath.quad(integrand, [Omega[0], Omega[1]])
A[i,j] = A[j,i] = I
integrand = psi[i]*f
if symbolic:
I = sym.integrate(integrand, (x, Omega[0], Omega[1]))
if not symbolic or isinstance(I, sym.Integral):
integrand = sym.lambdify([x], integrand)
I = sym.mpmath.quad(integrand, [Omega[0], Omega[1]])
b[i,0] = I
...
Graficzne porównanie f i u:
def comparison_plot(f, u, Omega, filename='tmp.pdf'):
x = sym.Symbol('x')
# Turn f and u to ordinary Python functions
f = sym.lambdify([x], f, modules="numpy")
u = sym.lambdify([x], u, modules="numpy")
resolution = 401 # no of points in plot
xcoor = linspace(Omega[0], Omega[1], resolution)
exact = f(xcoor)
approx = u(xcoor)
plot(xcoor, approx)
hold('on')
plot(xcoor, exact)
legend(['approximation', 'exact'])
savefig(filename)
>>> from approx1D import *
>>> x = sym.Symbol('x')
>>> f = 10*(x-1)**2-1
>>> u, c = least_squares(f=f, psi=[1, x], Omega=[1, 2])
>>> comparison_plot(f, u, Omega=[1, 2])
>>> from approx1D import *
>>> x = sym.Symbol('x')
>>> f = 10*(x-1)**2-1
>>> u, c = least_squares(f=f, psi=[1, x, x**2], Omega=[1, 2])
>>> print u
10*x**2 - 20*x + 9
>>> print sym.expand(f)
10*x**2 - 20*x + 9
Rozwiązanie przybliżone ≡ rozwiązanie dokładne, jeśli f∈V!
least_squares
: dla i>2, ci=0
Jeśli f∈V, MNK oraz metoda Galerkina zwrócą u=f.
Jeśli f∈V, wtedy f=∑j∈Isdjψj, dla pewnego {di}i∈Is. Wtedy
bi=(f,ψi)=∑j∈Isdj(ψj,ψi)=∑j∈IsdjAi,j
a URL ∑jAi,jcj=bi, i∈Is, przedstawia sie:
∑j∈IscjAi,j=∑j∈IsdjAi,j,i∈Is
co oznacza, że ci=di dla i∈Is, czyli u jest tożsame z f.
Poprzednie wnioski -> teoria i obliczenia symboliczne \ldots
Co w przypadku obliczeń numerycznych? -> (rozwiązanie URL macierzami liczb zmiennoprzecinkowych)
f to wciąż funkcja kwadratowa przybliżana przez
u(x)=c0+c1x+c2x2+c3x3+⋯+cNxN
Oczekiwane: c2=c3=⋯=cN=0, skoro f∈V oznacza u=f.
A naprawdę?
teoria | sympy | numpy32 | numpy64 |
---|---|---|---|
9 | 9.62 | 5.57 | 8.98 |
-20 | -23.39 | -7.65 | -19.93 |
10 | 17.74 | -4.50 | 9.96 |
0 | -9.19 | 4.13 | -0.26 |
0 | 5.25 | 2.99 | 0.72 |
0 | 0.18 | -1.21 | -0.93 |
0 | -2.48 | -0.41 | 0.73 |
0 | 1.81 | -0.013 | -0.36 |
0 | -0.66 | 0.08 | 0.11 |
0 | 0.12 | 0.04 | -0.02 |
0 | -0.001 | -0.02 | 0.002 |
matrix
oraz lu_solve
z biblioteki sympy.mpmath.fp
numpy
4B liczby zmiennoprzecinkowenumpy
8B liczby zmiennoprzecinkowe
Źródło kłopotów: funkcje xi dla bardzo dużych i stają się praktycznie liniowo zależne
4 rozwiązania zadania przybliżenia paraboli |
Wykresy funkcji xi dla i=0…14 |
Aproksymacja funkcji f szeregiem Fouriera
u(x)=∑iaisiniπx=N∑j=0cjsin((j+1)πx)
to tylko ''zmiana bazy'':
V=span{sinπx,sin2πx,…,sin(N+1)πx}
Obliczenia z wykorzystaniem funkcji least_squares
:
N = 3
from sympy import sin, pi
psi = [sin(pi*(i+1)*x) for i in range(N+1)]
f = 10*(x-1)**2 - 1
Omega = [0, 1]
u, c = least_squares(f, psi, Omega)
comparison_plot(f, u, Omega)
L: N=3, P: N=11:
Dla każdej f.bazowej jest ψi(0)=0 przez co u(0)=0≠f(0)=9. Podobna sytuacja dla x=1. Wartości u na brzegach będą zawsze niepoprawne!
u(x)=f(0)(1−x)+xf(1)+∑j∈Iscjψj(x)
Dodatkowy wyraz nie tylko zapewnia u(0)=f(0) oraz u(1)=f(1), ale także zaskakująco dobrze poprawia jakość aproksymacji!
N=3 vs N=11:
Zalety wyboru funkcji sinus jako funkcji bazowych:
W ogólnym przypadku, dla baz ortogonalnych, Ai,j jest macierzą diagonalną, a nieznane współczynniki ci można łatwo obliczyć:
ci=biAi,i=(f,ψi)(ψi,ψi)
def least_squares_orth(f, psi, Omega):
N = len(psi) - 1
A = [0]*(N+1)
b = [0]*(N+1)
x = sym.Symbol('x')
for i in range(N+1):
A[i] = sym.integrate(psi[i]**2, (x, Omega[0], Omega[1]))
b[i] = sym.integrate(psi[i]*f, (x, Omega[0], Omega[1]))
c = [b[i]/A[i] for i in range(len(b))]
u = 0
for i in range(len(psi)):
u += c[i]*psi[i]
return u, c
symbolic
).sym.integrate
zwraca sym.Integral
), obliczenia wykonywane numerycznie (w przypadku funkcji sinus nie powinno być problemów z symbolicznym obliczeniem ∫Ωφ2idx)
def least_squares_orth(f, psi, Omega, symbolic=True):
...
for i in range(N+1):
# Diagonal matrix term
A[i] = sym.integrate(psi[i]**2, (x, Omega[0], Omega[1]))
# Right-hand side term
integrand = psi[i]*f
if symbolic:
I = sym.integrate(integrand, (x, Omega[0], Omega[1]))
if not symbolic or isinstance(I, sym.Integral):
print 'numerical integration of', integrand
integrand = sym.lambdify([x], integrand)
I = sym.mpmath.quad(integrand, [Omega[0], Omega[1]])
b[i] = I
...
Inny sposób znalezienia przybliżenia f(x) przez u(x)=∑jcjψj:
u(xi)=∑j∈Iscjψj(xi)=f(xi)i∈Is,N
Współczynniki wygenerowanego układu równań to po prostu wartości funkcji, nie ma potrzeby całkowania!
∑j∈IsAi,jcj=bi,i∈IsAi,j=ψj(xi)bi=f(xi)
W ogólnym przypadku macierz wynikowa niesymetryczna: ψj(xi)≠ψi(xj)
Zmienna points
przechowuje punkty kolokacji
def interpolation(f, psi, points):
N = len(psi) - 1
A = sym.zeros((N+1, N+1))
b = sym.zeros((N+1, 1))
x = sym.Symbol('x')
# Turn psi and f into Python functions
psi = [sym.lambdify([x], psi[i]) for i in range(N+1)]
f = sym.lambdify([x], f)
for i in range(N+1):
for j in range(N+1):
A[i,j] = psi[j](points[i])
b[i,0] = f(points[i])
c = A.LUsolve(b)
u = 0
for i in range(len(psi)):
u += c[i,0]*psi[i](x)
return u
(4/3,5/3) vs (1,2):
u(xi)=∑j∈Iscjψj(xi)=f(xi),i=0,1,…,m
∑j∈IsAi,jcj=bi,i=0,1,…,m
Ai,j=ψj(xi),bi=f(xi)
Bi,j=∑kATi,kAk,j=∑kAk,iAk,j=m∑k=0ψi(xk)ψj(xk)di=∑kATi,kbk=∑kAk,ibk=m∑k=0ψi(xk)f(xk)
def regression(f, psi, points):
N = len(psi) - 1
m = len(points)
# Use numpy arrays and numerical computing
B = np.zeros((N+1, N+1))
d = np.zeros(N+1)
# Wrap psi and f in Python functions rather than expressions
# so that we can evaluate psi at points[i]
x = sym.Symbol('x')
psi_sym = psi # save symbolic expression
psi = [sym.lambdify([x], psi[i]) for i in range(N+1)]
f = sym.lambdify([x], f)
for i in range(N+1):
for j in range(N+1):
B[i,j] = 0
for k in range(m+1):
B[i,j] += psi[i](points[k])*psi[j](points[k])
d[i] = 0
for k in range(m+1):
d[i] += psi[i](points[k])*f(points[k])
c = np.linalg.solve(B, d)
u = sum(c[i]*psi_sym[i] for i in range(N+1))
return u, c
import sympy as sym
x = sym.Symbol('x')
f = 10*(x-1)**2 - 1
psi = [1, x]
Omega = [1, 2]
m_values = [2-1, 8-1, 64-1]
# Create m+3 points and use the inner m+1 points
for m in m_values:
points = np.linspace(Omega[0], Omega[1], m+3)[1:-1]
u, c = regression(f, psi, points)
comparison_plot(f, u, Omega, points=points,
points_legend='%d interpolation points' % (m+1))
u(x)=10x−13.2,2 punktyu(x)=10x−12.7,8 punktówu(x)=10x−12.7,64 punkty
Motywacja::
Własność wielomianów Lagrange'a ψj:
ψi(xj)=δij,δij={1,i=j0,i≠j
Zatem, ci=f(xi) and
u(x)=∑j∈Isf(xi)ψi(x)
ψi(x)=N∏j=0,j≠ix−xjxi−xj=x−x0xi−x0⋯x−xi−1xi−xi−1x−xi+1xi−xi+1⋯x−xNxi−xN
def Lagrange_polynomial(x, i, points):
p = 1
for k in range(len(points)):
if k != i:
p *= (x - points[k])/(points[i] - points[k])
return p
12 punktów, dwa wielomiany stopnia 11 (Uwaga!: ψ2(x2)≠0 i ψ7(x7)≠0
Problem: oscylacje w okolicach krańców przedziałów dla większych N.
Odpowiedni dobór węzłów interpolacji – węzły Czebyszewa:
xi=12(a+b)+12(b−a)cos(2i+12(N+1)π),i=0…,N
na przedziale [a,b].
12 punktów, dwa wielomiany stopnia 11.
Uwaga!: Tym razem węzły są inaczej rozmieszczone!
Mniej oscylacyjny charakter wielomianów w porównaniu do węzłów równomiernie rozmieszczonych.
Nośnik funkcji: domknięcie zbioru argumentów funkcji, dla których ma ona wartość różną od zera (takie iksy dla których f(x)≠0)
Podzielmy Ω na Ne rozdzielnych podobszarów podobszarów – elementów:
Ω=Ω(0)∪⋯∪Ω(Ne)
Na każdym elemencie wprowadzamy Nn wezłów (punktów): x0,…,xNn−1
Struktura nodes
– współrzędne węzłów.
Struktura elements
– numery (globalne) węzłów tworzących
odpowiedni element.
nodes = [0, 1.2, 2.4, 3.6, 4.8, 5]
elements = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]
nodes = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
elements = [[0, 1, 2], [2, 3, 4], [4, 5, 6], [6, 7, 8]]
d = 3 # d+1 nodes per element
num_elements = 4
num_nodes = num_elements*d + 1
nodes = [i*0.5 for i in range(num_nodes)]
elements = [[i*d+j for j in range(d+1)] for i in range(num_elements)]
nodes = [1.5, 5.5, 4.2, 0.3, 2.2, 3.1]
elements = [[2, 1], [4, 5], [0, 4], [3, 0], [5, 2]]
Ważna własność: ci to wartość funkcji u w węźle i, xi:
u(xi)=∑j∈Iscjφj(xi)=ciφi(xi)=ci
Powód: φj(xi)=0 jeśli i≠j i φi(xi)=1
Ponieważ Ai,j=∫φiφjdx, większość współczynników macierzy będzie równa zero -> macierze rzadkie
φi(x)={0,x<xi−1(x−xi−1)/hxi−1≤x<xi1−(x−xi)/h,xi≤x<xi+10,x≥xi+1
Uproszczenie: elementy jednakowej długości.
A2,3=∫Ωφ2φ3dx: φ2φ3≠0 jedynie na elemencie 2. Dla tego elementu:
φ3(x)=(x−x2)/h,φ2(x)=1−(x−x2)/h
A2,3=∫Ωφ2φ3dx=∫x3x2(1−x−x2h)x−x2hdx=h6
A2,2=∫x2x1(x−x1h)2dx+∫x3x2(1−x−x2h)2dx=2h3
Ai,i−1=∫Ωφiφi−1dx=?
Ai,i−1=∫Ωφiφi−1dx=∫xi−1xi−2φiφi−1dx⏟φi=0+∫xixi−1φiφi−1dx+∫xi+1xiφiφi−1dx⏟φi−1=0=∫xixi−1(x−xih)⏟φi(x)(1−x−xi−1h)⏟φi−1(x)dx=h6
bi=∫Ωφi(x)f(x)dx=∫xixi−1x−xi−1hf(x)dx+∫xi+1xi(1−x−xih)f(x)dx
Do dalszych obliczeń potrzebna konkretna postać f(x) ...
A=h6(210141012),b=h212(2−3h12−14h10−17h)
c0=h26,c1=h−56h2,c2=2h−236h2
u(x)=c0φ0(x)+c1φ1(x)+c2φ2(x)
Przypomnienie: jeśli f∈V, u odtworzy rozwiązanie bezbłędnie. Jeśli f to parabola, dowolna siatka elementów typu P2 (1 lub wiele elementów) sprawi wygeneruje u=f. To samo dotyczyć będzie elementów typu P3, P4, itd., ponieważ one wszystkie potrafią odtworzyć wielomian 2. stopnia bezbłędnie.
(ang. assemble - gromadzić, składać, zbierać)
assembling, assemblacja?
Ai,j=∫Ωφiφjdx=∑e∫Ω(e)φiφjdx,A(e)i,j=∫Ω(e)φiφjdx
Ważne spostrzeżenia:
˜A(e)={˜A(e)r,s},˜A(e)r,s=∫Ω(e)φq(e,r)φq(e,s)dx,r,s∈Id={0,…,d}
|
|
TODO
TODO
TODO
bi=∫Ωf(x)φi(x)dx=∑e∫Ω(e)f(x)φi(x)dx,b(e)i=∫Ω(e)f(x)φi(x)dx
Ważne spostrzeżenia:
Assembling:
bq(e,r):=bq(e,r)+˜b(e)r,r∈Id
Normalizacja współrzędnych położenia:
Zamiast całkować w granicach [xL,xR]
˜A(e)r,s=∫Ω(e)φq(e,r)(x)φq(e,s)(x)dx=∫xRxLφq(e,r)(x)φq(e,s)(x)dx
można transformować przedział [xL,xR] na przedział unormowany [−1,1] o współrzędnej lokalnej X
(Transformacja afiniczna)
x=12(xL+xR)+12(xR−xL)X
inaczej
x=xm+12hX,xm=(xL+xR)/2,h=xR−xL
Transformacja odwrotna:
X=2x+(xL+xR)(xR−xL)
Zmiana granic całkowania -> całkowanie na przedziale unormowanym: podstawienie x(X) w miejsce x.
Lokalne funkcje bazowe we współrzędnych unormowanych:
˜φr(X)=φq(e,r)(x(X))
x=12(xL+xR)+12(xR−xL)X↓dx=12(xR−xL)dX
˜A(e)r,s=∫Ω(e)φq(e,r)(x)φq(e,s)(x)dx=1∫−1˜φr(X)˜φs(X)dxdX⏟detJ=h/2dX
˜A(e)r,s=1∫−1˜φr(X)˜φs(X)detJdX
˜b(e)r=∫Ω(e)f(x)φq(e,r)(x)dx=1∫−1f(x(X))˜φr(X)detJdX
˜φ0(X)=12(1−X)˜φ1(X)=12(1+X)
(proste funkcje wielomianowe zamiast definicji funkcji na podprzedziałach
˜φ0(X)=12(X−1)X˜φ1(X)=1−X2˜φ2(X)=12(X+1)X
Łatwość wygenerowania elementów dowolnego rzędu... Jak?
Założenie: elementy typu P1, oraz funkcja f(x)=x(1−x).
˜A(e)0,0=∫1−1˜φ0(X)˜φ0(X)h2dX=∫1−112(1−X)12(1−X)h2dX=h8∫1−1(1−X)2dX=h3˜A(e)1,0=∫1−1˜φ1(X)˜φ0(X)h2dX=∫1−112(1+X)12(1−X)h2dX=h8∫1−1(1−X2)dX=h6˜A(e)0,1=˜A(e)1,0˜A(e)1,1=∫1−1˜φ1(X)˜φ1(X)h2dX=∫1−112(1+X)12(1+X)h2dX=h8∫1−1(1+X)2dX=h3
˜b(e)0=∫1−1f(x(X))˜φ0(X)h2dX=∫1−1(xm+12hX)(1−(xm+12hX))12(1−X)h2dX=−124h3+16h2xm−112h2−12hx2m+12hxm˜b(e)1=∫1−1f(x(X))˜φ1(X)h2dX=∫1−1(xm+12hX)(1−(xm+12hX))12(1+X)h2dX=−124h3−16h2xm+112h2−12hx2m+12hxm
xm: środek elementu
>>> import sympy as sym
>>> x, x_m, h, X = sym.symbols('x x_m h X')
>>> sym.integrate(h/8*(1-X)**2, (X, -1, 1))
h/3
>>> sym.integrate(h/8*(1+X)*(1-X), (X, -1, 1))
h/6
>>> x = x_m + h/2*X
>>> b_0 = sym.integrate(h/4*x*(1-x)*(1-X), (X, -1, 1))
>>> print b_0
-h**3/24 + h**2*x_m/6 - h**2/12 - h*x_m**2/2 + h*x_m/2
fe_approx1D.py
Niech ˜φr(X) będzie wielomianem Lagrange'a stopnia d
:
import sympy as sym
import numpy as np
def phi_r(r, X, d):
if isinstance(X, sym.Symbol):
h = sym.Rational(1, d) # node spacing
nodes = [2*i*h - 1 for i in range(d+1)]
else:
# assume X is numeric: use floats for nodes
nodes = np.linspace(-1, 1, d+1)
return Lagrange_polynomial(X, r, nodes)
def Lagrange_polynomial(x, i, points):
p = 1
for k in range(len(points)):
if k != i:
p *= (x - points[k])/(points[i] - points[k])
return p
def basis(d=1):
"""Return the complete basis."""
X = sym.Symbol('X')
phi = [phi_r(r, X, d) for r in range(d+1)]
return phi
def element_matrix(phi, Omega_e, symbolic=True):
n = len(phi)
A_e = sym.zeros((n, n))
X = sym.Symbol('X')
if symbolic:
h = sym.Symbol('h')
else:
h = Omega_e[1] - Omega_e[0]
detJ = h/2 # dx/dX
for r in range(n):
for s in range(r, n):
A_e[r,s] = sym.integrate(phi[r]*phi[s]*detJ, (X, -1, 1))
A_e[s,r] = A_e[r,s]
return A_e
>>> from fe_approx1D import *
>>> phi = basis(d=1)
>>> phi
[1/2 - X/2, 1/2 + X/2]
>>> element_matrix(phi, Omega_e=[0.1, 0.2], symbolic=True)
[h/3, h/6]
[h/6, h/3]
>>> element_matrix(phi, Omega_e=[0.1, 0.2], symbolic=False)
[0.0333333333333333, 0.0166666666666667]
[0.0166666666666667, 0.0333333333333333]
def element_vector(f, phi, Omega_e, symbolic=True):
n = len(phi)
b_e = sym.zeros((n, 1))
# Make f a function of X
X = sym.Symbol('X')
if symbolic:
h = sym.Symbol('h')
else:
h = Omega_e[1] - Omega_e[0]
x = (Omega_e[0] + Omega_e[1])/2 + h/2*X # mapping
f = f.subs('x', x) # substitute mapping formula for x
detJ = h/2 # dx/dX
for r in range(n):
b_e[r] = sym.integrate(f*phi[r]*detJ, (X, -1, 1))
return b_e
Zwróć uwagę na f.subs('x', x)
-> podstawienie x(X) za x
w formule na f
(od teraz f
jest funkcją f(X))
sympy
zawsze da radęsympy
zwróci obiekt typu Integral
zamiast liczby)
def element_vector(f, phi, Omega_e, symbolic=True):
...
I = sym.integrate(f*phi[r]*detJ, (X, -1, 1)) # try...
if isinstance(I, sym.Integral):
h = Omega_e[1] - Omega_e[0] # Ensure h is numerical
detJ = h/2
integrand = sym.lambdify([X], f*phi[r]*detJ)
I = sym.mpmath.quad(integrand, [-1, 1])
b_e[r] = I
...
def assemble(nodes, elements, phi, f, symbolic=True):
N_n, N_e = len(nodes), len(elements)
zeros = sym.zeros if symbolic else np.zeros
A = zeros((N_n, N_n))
b = zeros((N_n, 1))
for e in range(N_e):
Omega_e = [nodes[elements[e][0]], nodes[elements[e][-1]]]
A_e = element_matrix(phi, Omega_e, symbolic)
b_e = element_vector(f, phi, Omega_e, symbolic)
for r in range(len(elements[e])):
for s in range(len(elements[e])):
A[elements[e][r],elements[e][s]] += A_e[r,s]
b[elements[e][r]] += b_e[r]
return A, b
if symbolic:
c = A.LUsolve(b) # sympy arrays, symbolic Gaussian elim.
else:
c = np.linalg.solve(A, b) # numpy arrays, numerical solve
Uwaga: obliczanie współczynników macierzy A
, b
oraz rozwiązanie URL
A.LUsolve(b)
może być baaardzo czasochłonne$\ldots$
>>> h, x = sym.symbols('h x')
>>> nodes = [0, h, 2*h]
>>> elements = [[0, 1], [1, 2]]
>>> phi = basis(d=1)
>>> f = x*(1-x)
>>> A, b = assemble(nodes, elements, phi, f, symbolic=True)
>>> A
[h/3, h/6, 0]
[h/6, 2*h/3, h/6]
[ 0, h/6, h/3]
>>> b
[ h**2/6 - h**3/12]
[ h**2 - 7*h**3/6]
[5*h**2/6 - 17*h**3/12]
>>> c = A.LUsolve(b)
>>> c
[ h**2/6]
[12*(7*h**2/12 - 35*h**3/72)/(7*h)]
[ 7*(4*h**2/7 - 23*h**3/21)/(2*h)]
>>> nodes = [0, 0.5, 1]
>>> elements = [[0, 1], [1, 2]]
>>> phi = basis(d=1)
>>> x = sym.Symbol('x')
>>> f = x*(1-x)
>>> A, b = assemble(nodes, elements, phi, f, symbolic=False)
>>> A
[ 0.166666666666667, 0.0833333333333333, 0]
[0.0833333333333333, 0.333333333333333, 0.0833333333333333]
[ 0, 0.0833333333333333, 0.166666666666667]
>>> b
[ 0.03125]
[0.104166666666667]
[ 0.03125]
>>> c = A.LUsolve(b)
>>> c
[0.0416666666666666]
[ 0.291666666666667]
[0.0416666666666666]
>>> d=1; N_e=8; Omega=[0,1] # 8 linear elements on [0,1]
>>> phi = basis(d)
>>> f = x*(1-x)
>>> nodes, elements = mesh_symbolic(N_e, d, Omega)
>>> A, b = assemble(nodes, elements, phi, f, symbolic=True)
>>> A
[h/3, h/6, 0, 0, 0, 0, 0, 0, 0]
[h/6, 2*h/3, h/6, 0, 0, 0, 0, 0, 0]
[ 0, h/6, 2*h/3, h/6, 0, 0, 0, 0, 0]
[ 0, 0, h/6, 2*h/3, h/6, 0, 0, 0, 0]
[ 0, 0, 0, h/6, 2*h/3, h/6, 0, 0, 0]
[ 0, 0, 0, 0, h/6, 2*h/3, h/6, 0, 0]
[ 0, 0, 0, 0, 0, h/6, 2*h/3, h/6, 0]
[ 0, 0, 0, 0, 0, 0, h/6, 2*h/3, h/6]
[ 0, 0, 0, 0, 0, 0, 0, h/6, h/3]
Uwaga (zadanie domowe): Wykonaj obliczenia na kartce papieru w celu potwierdzenia wartości poszczególnych elementów powyższej macierzy (pomocne w zrozumieniu materiału).
A=h6(210⋯⋯⋯⋯⋯0141⋱⋮0141⋱⋮⋮⋱⋱⋱0⋮⋮⋱⋱⋱⋱⋱⋮⋮0141⋱⋮⋮⋱⋱⋱⋱0⋮⋱1410⋯⋯⋯⋯⋯012)
A=h30(42−10000002162000000−1282−10000002162000000−1282−10000002162000000−1282−10000002162000000−124)
Postać specyficznych macierzy Ai,j:
Wskazówki:
scipy.sparse
Zadanie: Porównać rozwiązanie zadania przybliżenia funkcji f(x) przy pomocy siatki Ne elementów skończonych o funkcjach bazowych rzędu d.
import sympy as sym
from fe_approx1D import approximate
x = sym.Symbol('x')
approximate(f=x*(1-x)**8, symbolic=False, d=1, N_e=4)
approximate(f=x*(1-x)**8, symbolic=False, d=2, N_e=2)
approximate(f=x*(1-x)**8, symbolic=False, d=1, N_e=8)
approximate(f=x*(1-x)**8, symbolic=False, d=2, N_e=4)
Najczęstsza interpretacja:
wartości u (wymagane do geometrii i aproksymacji funkcji)
Problem:
– wielkości reprezentowane przez cj (niewiadome w URL) -> najczęściej: wartości funkcji w węźle ∑j∈Iscjφj(xi)=ci
wierzchołki -> komórki -> interpolacja geometrii
węzły, stopnie swobody -> interpolacja funkcji
vertices
, cells
, dof_map
vertices
(równoważne strukturze nodes
dla elementów P1)cells[e][r]
numer globalny dla wierchołka r
elementu e
(równoważne strukturze elements
dla elementów typu P1)dof_map[e,r]
odwzorowanie lokalnego indeksu stopnia swobody r
elementu e
na number globalny (równoważne strukturze elements
dla elementów typu Pd)
W trakcie assemblingu należy skorzystać ze struktury dof_map
:
A[dof_map[e][r], dof_map[e][s]] += A_e[r,s]
b[dof_map[e][r]] += b_e[r]
|
vertices = [0, 0.4, 1]
cells = [[0, 1], [1, 2]]
dof_map = [[0, 1, 2], [2, 3, 4]]
|
Przykład: Ta sama siatka, ale u to funkcja stała na każdej komórce (przedziałami stała) -> elementy typu P0.
Te same struktury vertices
i cells
, ale dodatkowo
dof_map = [[0], [1]]
Można traktować te elementy jak elementy z interpolacją opartą na węźle znajdującym się pośrodku elementu.
Od tej pory będziemy wykorzystywać struktury cells
, vertices
, i dof_map
.
# Use modified fe_approx1D module
from fe_approx1D_numint import *
x = sym.Symbol('x')
f = x*(1 - x)
N_e = 10
# Create mesh with P3 (cubic) elements
vertices, cells, dof_map = mesh_uniform(N_e, d=3, Omega=[0,1])
# Create basis functions on the mesh
phi = [basis(len(dof_map[e])-1) for e in range(N_e)]
# Create linear system and solve it
A, b = assemble(vertices, cells, dof_map, phi, f)
c = np.linalg.solve(A, b)
# Make very fine mesh and sample u(x) on this mesh for plotting
x_u, u = u_glob(c, vertices, cells, dof_map,
resolution_per_element=51)
plot(x_u, u)
Funkcja approximate
''opakowuje'' polecenia z poprzedniego slajdu:
from fe_approx1D_numint import *
x=sym.Symbol("x")
for N_e in 4, 8:
approximate(x*(1-x), d=0, N_e=N_e, Omega=[0,1])
Błąd jako funkcja:
e(x)=f(x)−u(x)
Błąd – dyskretna wartość -> normy:
L2 error: ||e||L2=(∫Ωe2dx)1/2
Szacowanie całki:
u_glob
, które zwróci x
i u
), a następnie
Ponieważ elementy mogą być różnych rozmiarów (długości) siatka dyskretna może być niejednorodna, (ponadto powtórzone punkty na granicach elementów, widziane z perspektywy dwóch sąsiadujących elementów)
->
potrzebna prymitywna implementacja wzoru trapezów:
∫Ωg(x)dx≈n−1∑j=012(g(xj)+g(xj+1))(xj+1−xj)
# Given c, compute x and u values on a very fine mesh
x, u = u_glob(c, vertices, cells, dof_map,
resolution_per_element=101)
# Compute the error on the very fine mesh
e = f(x) - u
e2 = e**2
# Vectorized Trapezoidal rule
E = np.sqrt(0.5*np.sum((e2[:-1] + e2[1:])*(x[1:] - x[:-1]))
Teoria i eksperymenty pokazują, że aplikacja MNK czy metody Galerkina dla elementów skończonych typu Pd o tej samej długości h daje błąd:
||e||L2=C|fd+1|hd+1
gdzie C zależy od d i Ω=[0,L] ale nie zależy od h, oraz
|fd+1|2=∫L0(dd+1fdxd+1)2dx
Niech dana będzie unormowana komórka [−1,1] z dwoma węzłami X=−1 i X=1. Stopnie swobody:
Uwzględnienie wartości pochodnych zadanej funkcji w węzłach jako stopni swobody zapewnia kontrolę ciągłości pochodnej.
4 ogranicznia na ˜φr (1 dla stopnia swobody r, 0 dla pozostałych):
Cztery układy równań liniowych z 4 niewiadomymi - współczynnikami wielomianów 3 stopnia.
˜φ0(X)=1−34(X+1)2+14(X+1)3˜φ1(X)=−(X+1)(1−12(X+1))2˜φ2(X)=34(X+1)2−12(X+1)3˜φ3(X)=−12(X+1)(12(X+1)2−(X+1))
# definition of the interval ends
x = np.array([-1, 1])
C = [] # list of polynomials stored as coefficients
B = [] # list of basis functions
dB = [] # list of the derivatives of basis functions
for k in np.arange(0,4):
A = np.array( [[ x[0]**3, x[0]**2, x[0], 1 ],
[ 3*x[0]**2, 2*x[0], 1, 0 ],
[ x[1]**3, x[1]**2, x[1], 1 ],
[ 3*x[1]**2, 2*x[1], 1, 0 ]])
b = np.zeros( (4,1) ); b[k] = 1
c = np.linalg.solve(A, b); C.append( c )
B.append( lambda x: C[k][0,0] * x**3 + C[k][1,0] * x**2 + C[k][2,0] * x + C[k][3,0] )
dB.append( lambda x: 3* C[k][0,0] * x**2 + 2*C[k][1,0] * x + C[k][2,0] )
# Check numerically that resulting cubic polynomial
# fulfills imposed requirements
A = [1, 1, 2, 1] # basis function coefficients
# U(x[0]) dU(x[0]) U(x[1]) dU(x[1])
xx = np.arange(-1,1, 0.001)
U = np.zeros(xx.shape)
for k in np.arange(0,4):
U = U + A[k] * B[k](xx)
# numerical approximation of the derivatives at the ends of the interval
dl = (U[1]-U[0])/(xx[1]-xx[0])
dr = (U[-1]-U[-2])/(xx[-1]-xx[-2])
numericalApproximationOfA = [ U[0], dl, U[-1], dr]
print(numericalApproximationOfA)
Wynik działania skryptu:
[1.0, 0.9992502500000269, 1.999000749750002, 0.9977517500000526]
|
|
∫1−1g(X)dX≈M∑j=0wjg(ˉXj),
gdzie
Różne metody -> różny wybór węzłów i wag
(ang. midpoint rule) – metoda punktu środkowego
Najprostsza metoda
∫1−1g(X)dX≈2g(0),ˉX0=0, w0=2,
Dokładna dla funkcji podcałkowych będących wielomianami 1. stopnia
Wzór trapezów:
∫1−1g(X)dX≈g(−1)+g(1),ˉX0=−1, ˉX1=1, w0=w1=1,
Wzór Simpsona (parabol):
∫1−1g(X)dX≈13(g(−1)+4g(0)+g(1)),
gdzie
ˉX0=−1, ˉX1=0, ˉX2=1, w0=w2=13, w1=43
M=1:ˉX0=−1√3, ˉX1=1√3, w0=w1=1M=2:ˉX0=−√35, ˉX0=0, ˉX2=√35, w0=w2=59, w1=89
Plik `numint.py zawiera zbiór węzłów i wag dla metody Gaussa-Legendre'a.
Rozwiązania i algorytmy przedstawione dla aproksymacji funkcji f(x) w 1D da się rozwinąć i ''przenieść'' na przypadki funkcji f(x,y) w 2D i f(x,y,z) w 3D. Ogólne wzory pozostają takie same.
Iloczyn skalarny w 2D:
(f,g)=∫Ωf(x,y)g(x,y)dxdy
Zastosowanie MNK lub metody Galerkina da URL:
∑j∈IsAi,jcj=bi,i∈IsAi,j=(ψi,ψj)bi=(f,ψi)
Problem: Jak skonstruować dwuwymiarowe funkcje bazowe ψi(x,y)?
Korzystając z funkcji bazowych 1D zmiennej x oraz funkcji bazowych 1D zmiennej y:
Vx=span{ˆψ0(x),…,ˆψNx(x)}Vy=span{ˆψ0(y),…,ˆψNy(y)}
Przestrzeń wektorowa 2D może być zdefinowana jako iloczyn tensorowy V=Vx⊗Vy z funkcjami bazowymi:
ψp,q(x,y)=ˆψp(x)ˆψq(y)p∈Ix,q∈Iy.
Niech dane będą dwa wektory a=(a0,…,aM) i b=(b0,…,bN). Ich zewnętrznym iloczynem tensorowym ( iloczynem diadycznym jeśli N=M) jest p=a⊗b zdefiniowane jako:
pi,j=aibj,i=0,…,M, j=0,…,N.
Uwaga: p to macierz/tablica dwuwymiarowa
Przykład: baza 2D jako iloczyn tensorowy przestrzeni 1D:
ψp,q(x,y)=ˆψp(x)ˆψq(y),p∈Ix,q∈Iy
(dowolnych wymiarów) ->
iloczyn tensorowy wektorów(dowolnych wymiarów) ->
iloczyn diadyczny(tego samego wymiaru)
tensorBaza przestrzeni 2D wymaga dwóch indeksów (i podwójnego sumowania) :
u=∑p∈Ix∑q∈Iycp,qψp,q(x,y)
Lub tylko jednego indeksu
u=∑j∈Iscjψj(x,y)
jeśli posiadamy odwzorowanie (p,q)→i:
ψi(x,y)=ˆψp(x)ˆψq(y),i=p(Ny+1)+q or i=q(Nx+1)+p
Dla dwuelementowej bazy 1D
{1,x}
iloczyn tensorowy (wszystkie kombinacje) generuje bazę przestrzeni 2D:
ψ0,0=1,ψ1,0=x,ψ0,1=y,ψ1,1=xy
W notacji jednoindeksowej:
ψ0=1,ψ1=x,ψ2=y,ψ3=xy
ψ0=1,ψ1=x,ψ2=y,ψ3=xy
∫Ly0∫Lx0{[1⋅11⋅x1⋅y1⋅xyx⋅1x⋅xx⋅yx⋅xyy⋅1y⋅xy⋅yy⋅xyxy⋅1xy⋅xxy⋅yxy⋅xy][c0c1c2c3]=[1⋅f(x,y)x⋅f(x,y)y⋅f(x,y)xy⋅f(x,y)]}dxdy
Funkcja aproksymowana (kwadratowa) f(x,y)=(1+x2)(1+2y2) (po lewej), funkcja aproksymująca (biliniowa) u (po prawej) (x2y2 vs xy):
Zmiany w kodzie w stosunku do wersji 1D (approx1D.py
):
Omega = [[0, L_x], [0, L_y]]
import sympy as sym
integrand = psi[i]*psi[j]
I = sym.integrate(integrand,
(x, Omega[0][0], Omega[0][1]),
(y, Omega[1][0], Omega[1][1]))
# Fall back on numerical integration if symbolic integration
# was unsuccessful
if isinstance(I, sym.Integral):
integrand = sym.lambdify([x,y], integrand)
I = sym.mpmath.quad(integrand,
[Omega[0][0], Omega[0][1]],
[Omega[1][0], Omega[1][1]])
Iloczyn tensorowy bazy potęgowej xi (bazy Taylora):
def taylor(x, y, Nx, Ny):
return [x**i*y**j for i in range(Nx+1) for j in range(Ny+1)]
Iloczyn tensorowy bazy sinusoidalnej sin((i+1)πx):
def sines(x, y, Nx, Ny):
return [sym.sin(sym.pi*(i+1)*x)*sym.sin(sym.pi*(j+1)*y)
for i in range(Nx+1) for j in range(Ny+1)]
Cały kod w approx2D.py
.
>>> from approx2D import *
>>> f = (1+x**2)*(1+2*y**2)
>>> psi = taylor(x, y, 1, 1)
>>> Omega = [[0, 2], [0, 2]]
>>> u, c = least_squares(f, psi, Omega)
>>> print u
8*x*y - 2*x/3 + 4*y/3 - 1/9
>>> print sym.expand(f)
2*x**2*y**2 + x**2 + 2*y**2 + 1
Dodajemy funkcje bazowe o wyższych potęgach tak, aby f∈V. Spodziewany wynik: u=f
>>> psi = taylor(x, y, 2, 2)
>>> u, c = least_squares(f, psi, Omega)
>>> print u
2*x**2*y**2 + x**2 + 2*y**2 + 1
>>> print u-f
0
Kluczowa idea:
V=Vx⊗Vy⊗Vz
a(q)=(a(q)0,…,a(q)Nq),q=0,…,mp=a(0)⊗⋯⊗a(m)pi0,i1,…,im=a(0)i1a(1)i1⋯a(m)im
W szczególności dla 3D:
ψp,q,r(x,y,z)=ˆψp(x)ˆψq(y)ˆψr(z)u(x,y,z)=∑p∈Ix∑q∈Iy∑r∈Izcp,q,rψp,q,r(x,y,z)
Zalety FEM w zastosowaniach 2D i 3D:
FEM w 1D: głównie dla celów dydaktycznych, debugowania
2D:
3D:
Element trójkątny typu P1: aproksymacja u na każdym elemencie (komórce) funkcją liniową ax+by+c
˜φ0(X,Y)=1−X−Y˜φ1(X,Y)=X˜φ2(X,Y)=Y
Funkcje bazowe ˜φr wyższych stopni opierają się na większej liczbie węzłów (stopni swobody)
Transformacja (Odwzorowanie) komórki we współrzędnych unormowanych
X=(X,Y)do komórki we współrzędnych globalnych:
x=(x,y):
x=∑r˜φ(1)r(X)xq(e,r)
gdzie
Odwzorowanie zachowuje liniowość ścian i krawędzi.
Idea: Wykorzystanie funkcji bazowych elementu (nie tylko funkcji typu P1 ale i wyższych rzędów) do odwzorowania geometrii:
x=∑r˜φr(X)xq(e,r)
Zaleta: pozwala generować elementy o geomtrii nielinowej
Wymagana transformacja całek z Ω(e) (komórka we współrządnych globalnych) w ˜Ωr (komórka unormowana/odniesienia):
∫Ω(e)φi(x)φj(x)dx=∫˜Ωr˜φi(X)˜φj(X)detJdX∫Ω(e)φi(x)f(x)dx=∫˜Ωr˜φi(X)f(x(X))detJdX
gdzie dx=dxdy lub dx=dxdydz oraz detJ to wyznacznik jakobianu odwzorowania x(X).
J=[∂x∂X∂x∂Y∂y∂X∂y∂Y],detJ=∂x∂X∂y∂Y−∂x∂Y∂y∂X
Odwzorowanie afiniczne (33): detJ=2Δ, Δ=powierzchnia komórki/elementu
Ogólna idea FEM oraz kroki algorytmu – takie same niezależnie od wymiarowości geometrii.
Im wyższy wymiar przestrzeni, tym większy nakład obliczeniowy. ze względu na komplikację wzorów.
Obliczenia ręczne - nużące, podatne na popełnienie pomyłki.
Automatyzacja i algorytmizacja problemu pożądana.