-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path1D_diffusion.py
More file actions
363 lines (308 loc) · 17.7 KB
/
1D_diffusion.py
File metadata and controls
363 lines (308 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""
Interactive 1D Diffusion Animation
Author: Marcelo Falcão de Oliveira
Affiliation: University of São Paulo (USP)
São Carlos School of Engineering (EESC)
Materials Engineering Department (SMM)
Contact: marcelo.falcao@usp.br
Description:
It ilustrates the solution of the transient diffusion equation in 1D
(2nd Fick's law), for example, trhough a wall or plate. The user can interact
with the animation plot by setting vertex positions, selecting boundary
conditions (Dirichlet, Neumann dC/dx=0 or periodic), setting initial
profiles and controlling de diffusion velocity.
License:
MIT License (https://opensource.org/licenses/MIT)
Purpose:
Educational tool for demonstrating 1D transient diffusion.
Packages needed:
matplotlib, numpy
Usage:
$ python 1D_diffusion.py
- Instructions are provided in the first plot
Date: January, 2024
Version: 2.0
Note:
Many improvement attempts were performed with ChatGPT-3.5 without success.
So this script remains "human like".
"""
howtouse = '''
INSTRUCTIONS
1. **Set Concentration:**
- Click and drag points to adjust concentration values (y).
2. **Toggle Points:**
- Press 't' to hide/show points for dragging.
3. **Change Boundary Conditions:**
- Press 'b' to cycle among Dirichlet, Neumann (dC/dx=0),
or periodic.
4. **Toggle Diffusion On/Off:**
- Press 'd' to start/stop diffusion run.
(Figure interactivity holds with running diffusion.)
5. **Timestep Size:**
- Press '+' to double the timestep.
- Press '-' to reduce the timestep by half.
6. **Reset Initial Profile:**
- Press 'r' to cycle among resetting initial profiles.
7. **Toggle Help Message:**
- Press 'h' to hide or show this help message.
'''
import numpy as np # numpy is needed for matrix functions, it makes the code fast
from matplotlib.backend_bases import MouseButton # needed for mouse interactivity with matplotlib objects
from matplotlib.path import Path # needed to build paths that can be decorated
from matplotlib.patches import PathPatch # needed to build patches, i.e., filling areas
import matplotlib.pyplot as plt # pyplot is used for general plotting
import matplotlib.animation as animation # matplotlib animation functions
def pattern(_id):
"""
Here we define some resetting profiles for the initial concentration curve
"""
# pattern 0
# full of zeros
pat = np.zeros(n)
# pattern 1
# half zeros half ones, we concatenate an array of ones and another of zeros
if _id == 1:
pat = np.concatenate((np.full(int(n/2-1),1),[0.5],np.full(int(n/2+1),0)))
# pattern 2
# alternating 1/3 of zeros or ones, we concatenate 3 arrays
if _id == 2:
b0 = np.full(int(n/3),0)
pat = np.concatenate((b0,[0.5],np.full(int(n/3),1),[0.5],b0))
# pattern 3
# alternating 1/4 of zeros or ones, we concatenate 6 arrays
if _id == 3:
b0 = np.full(int(n/8),0)
b1 = np.full(int(n/4),1)
pat = np.concatenate((b0,b1,b0,[0],b0,b1,b0))
# pattern 4
# full of ones
if _id == 4:
pat = np.full(n,1)
return pat
class PathInteractor:
"""
Main core of the script for plot interactivity and animation.
It is a very extensive modification of the original class
PathInteractor code from matplotlib examples found in
https://matplotlib.org/stable/gallery/event_handling/path_editor.html#path-editor
"""
showverts = True # variable to control whether vertices (points) are shown or not
epsilon = 13 # max pixel distance to count as a vertex hit
_ind = None # variable for identification of the active vertex for user's editing
bc = 'Dirichlet' # initial boundary condition
timestepfactor = 1 # initial timestep multiplier
helpshow = True # variable to control toggle of help message
pattern_id = 4 # last pattern id for curve resetting
def __init__(self, figure, pathpatch, mesh):
"""
This is the initialization function of the class
"""
self.canvas = figure.canvas # pass figure canvas to an internal class variable
self.ax = pathpatch.axes # pass patchpatch axes to an internal class variable
self.pathpatch = pathpatch # pass the pathpatch to an internal class variable
self.pathpatch.set_animated(True) # makes the pathpatch animated
self.ax2 = mesh # pass the pcolormesh to an internal class variable
self.vertices = self.pathpatch.get_path().vertices # internal variable for the path vertices
self.vertices = self.vertices[1:-1] # trim out the path borders (they are used only for the patch)
shape = colormesh.get_coordinates().shape # get colormesh grid matrix shape
self.mat = mesh.get_array().reshape(shape[2],shape[1]) # reshape the colormesh array values to an internal class variable
x, y = zip(*self.vertices) # gets x and y coordinates of the pathpatch and arrange them in vectors
# we build a line (not visible) with markers on the path for user's editing, it is also animated
self.line, = self.ax.plot(
x, y, marker='s', markersize='7', markerfacecolor='r', linewidth=0, animated=True)
# plot title with initial bondary condition
self.ax.set_title('1D Diffusion Animation - Boundary Condition: Dirichlet')
# we add the help message into the plot
self.helpbox = self.ax.text(-0.9,0.1, howtouse, color='blue', fontsize=10.5, wrap=True)
self.canvas.mpl_connect('draw_event', self.on_draw) # detects drawing event and executes on_draw function
self.canvas.mpl_connect('button_press_event', self.on_button_press) # detects mouse button press and executes on_button_press function
self.canvas.mpl_connect('key_press_event', self.on_key_press) # detects keybord press and executes on_key_press fucntion
self.canvas.mpl_connect('button_release_event', self.on_button_release) # detects mouse button release and executes on_button_release function
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) # detects mouse movement and executes on_mouse_move function
self.keepdiffusing = False # variable to toggle on/off the animation (diffusion)
# we define the animation, infinite loop (frames=None) and 10 ms bettween frames
self.anim = animation.FuncAnimation(figure, self.animate, frames=None, interval=10, cache_frame_data=False)
plt.show() # now we can show our plot
def animate(self, frame):
"""
Animates our plot, perform a diffusion step and refresh the canvas
"""
# perform a diffusion step only if diffusion is on
if self.keepdiffusing:
self.vertices = self.timestep(self.vertices) # apply the diffusion step
self.canvas.blit(self.ax.bbox) # refresh canvas with fast rendering over the background
def get_ind_under_point(self, event):
"""
Return the index of the point closest to the event position or *None*
if no point is within ``self.epsilon`` to the event position.
"""
xy = self.vertices # get vertices
xyt = self.pathpatch.get_transform().transform(xy) # get vertices coordinates in the screen
xt, yt = xyt[:, 0], xyt[:, 1] # split x and y coordinates in two vectors
d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) # calculates the distance between the click event and the vertices
ind = d.argmin() # get the id of the vertex with the minor distance
return ind if d[ind] < self.epsilon else None # if the distance is shorter than epsilon returns the vertex' id
def on_draw(self, event):
"""
This function is activated everytime a drawing event occurs; blit, for example.
"""
self.line.set_data(zip(*self.vertices)) # update marker's (vertices) positions
self.ax.draw_artist(self.pathpatch) # draw the path and the patch
self.ax.draw_artist(self.line) # draw the interactive markers (line)
self.mat[:,:] = self.vertices[:,1] # updates the matrix of the colormesh
self.ax2.set_array(self.mat)
def on_button_press(self, event):
"""
Function to handle mouse button press
"""
if (event.inaxes is None # it works only if the mouse is inside axes or left mouse button
or event.button != MouseButton.LEFT # and vertices are visible
or not self.showverts):
return
self._ind = self.get_ind_under_point(event) # gets the vertex identification if close enough the mouse click
def on_button_release(self, event):
"""
Function to handle mouse button release
"""
if (event.button != MouseButton.LEFT # it works only if left mouse button
or not self.showverts): # and vertices are visible
return
self._ind = None # since the mouse was released no vertex is active
def on_key_press(self, event):
"""
Function to handle keyboard pressing
"""
# check if the mouse is inside the plot or do nothing
if not event.inaxes:
return
# show or hide vertices as well as toggle path user's interaction
if event.key == 't':
self.showverts = not self.showverts # flip showverts condition
self.line.set_visible(self.showverts) # set markers (line) visibility
# if vertices are hidden none is avaliable for editing
if not self.showverts:
self._ind = None
# cycle among boundary conditions
if event.key == 'b':
if self.bc == 'Dirichlet': # from Dirichlet to Neumann
self.bc = 'Neumann'
elif self.bc == 'Neumann': # from Neumann to periodic
self.bc = 'Periodic'
else:
self.bc = 'Dirichlet' # back to Dirichlet
# apply the periodic condition if set
if self.bc == 'Periodic':
self.vertices[n-1][1] = self.vertices[0][1]
# change the boundary conditon in the title
self.ax.set_title('1D Diffusion Animation - Boundary Condition: ' + self.bc)
# toggle diffusion on/off
if event.key == 'd':
self.keepdiffusing = not self.keepdiffusing # flip keepdiffusing condition
# cycle the resetting path curve according to the patterns
if event.key == 'r':
if self.pattern_id == 0:
self.pattern_id = 1
elif self.pattern_id == 1:
self.pattern_id = 2
elif self.pattern_id == 2:
self.pattern_id = 3
elif self.pattern_id == 3:
self.pattern_id = 4
elif self.pattern_id == 4:
self.pattern_id = 0
self.vertices[:,1] = pattern(self.pattern_id) # pass the concentration pattern to the path
if not self.showverts: # make the markers (line) visible
self.showverts = True
self.line.set_visible(True)
self.timestepfactor = 1 # set the timestep multiplier back to 1
if event.key == '+': # double the timestep multiplier
self.timestepfactor *= 2
if event.key == '-': # half the timestep multiplier
self.timestepfactor *= 0.5
# toggle help visibility
if event.key == 'h':
self.helpshow = not self.helpshow # flip helpshow condition
self.helpbox.set_visible(self.helpshow) # apply the condition
def on_mouse_move(self, event):
"""
Function to handle mouse movements.
"""
if (self._ind is None # it works only if the mouse is inside axes or left mouse button
or event.inaxes is None # and vertices are visible
or event.button != MouseButton.LEFT
or not self.showverts):
return
y = event.ydata # allow moving only along y between 0 and 1
if event.ydata > 1: y = 1
if event.ydata < 0: y = 0
self.vertices[self._ind][1] = y # move only the active vertex
if self.bc == 'Periodic' and self._ind == 0: # if bc is periodic the extremes are the same
self.vertices[n-1][1] = self.vertices[0][1]
if self.bc == 'Periodic' and self._ind == (n-1): # if bc is periodic the extremes are the same
self.vertices[0][1] = self.vertices[n-1][1]
def timestep(self, m):
"""
This function applies the Crank-Nicolson scheme for solving the 2nd Fick's law equation.
Notice how we can avoid nested for loops by using numpy matrix functions.
To understand the matricial solution refer to the lecture documentation.
"""
m0 = m[:,1].copy() # make a copy (column) of the initial vector of values
dt = 1/1000*self.timestepfactor # time step size which is also the lambda of the C-N scheme
Lidiag = np.full(len(m), 1+dt) # main diagonal of lambda matrix for implicit terms
Lioffdiag = np.full(len(m)-1, -dt/2) # off diagonals of lambda matrix for implicit terms
Li = np.diag(Lioffdiag,-1)+np.diag(Lidiag,0)+np.diag(Lioffdiag,1) # lambda tridiagonal matrix for implicit terms
Lediag = np.full(len(m), 1-dt) # main diagonal of lambda matrix for explicit terms
Leoffdiag = np.full(len(m)-1, dt/2) # off diagonals of lambda matrix for explicit terms
Le = np.diag(Leoffdiag,-1)+np.diag(Lediag,0)+np.diag(Leoffdiag,1) # lambda tridiagonal matrix for explicit terms
# Apply the boundary condition
if self.bc == 'Dirichlet':
Li[0,0] = Li[len(m)-1,len(m)-1] = 1 # main diagonal extremes
Le[0,0] = Le[len(m)-1,len(m)-1] = 1
Li[0,1] = Li[len(m)-1,len(m)-2] = 0 # off diagonal extremes
Le[0,1] = Le[len(m)-1,len(m)-2] = 0
if self.bc == 'Neumann':
Li[0,1] = Li[len(m)-1,len(m)-2] = -dt # off diagonal extremes
Le[0,1] = Le[len(m)-1,len(m)-2] = dt
if self.bc == 'Periodic':
Li[0,len(m)-2,] = Li[len(m)-1,1] = -dt/2 # off anti-diagonal extremes
Le[0,len(m)-2,] = Le[len(m)-1,1] = dt/2
m[:,1] = np.matmul(np.linalg.inv(Li),np.matmul(Le,m0)) # solves the matricial equation: m = Li^(-1).Le.m0
return m # returns the calculated matrix
# Execute the main code only if this script is run directly, not when imported as a module
if __name__ == "__main__":
n = 41 # number of main vertices in the path
fig, (ax1, ax2) = plt.subplots(2, sharex=True, height_ratios=[9, 1]) # we define 2 subplots with different heights
fig.set_figwidth(6) # figure width
fig.set_figheight(6) # figure height
fig.subplots_adjust(hspace=0.05) # vertical space between subplots
# define the main vertices in the path between -1 and 1, with zero concentration in all points
verts = np.array([np.linspace(-1,1,n),np.zeros(n)]).flatten(order='F').reshape(n,2)
# we add 2 fixed vertices at the path extremities to build a decorative patch from the horizintal axis
fullpath = np.concatenate((np.array([[-1,0]]),verts,np.array([[1,0]])),axis=0)
# we define a path
path = Path(fullpath)
# now we define a patch with the path
patch = PathPatch(
path, facecolor='lightgreen', edgecolor='blue', linewidth=2)
# the patch is added to the upper subplot
ax1.add_patch(patch)
# we set the x axis limits
ax1.set_xlim(-1, 1)
# we set the y axis limits
ax1.set_ylim(0, 1.01)
# we set the x axis title at the bottom subplot
ax2.set_xlabel('Relative position')
# turn off the y labels in the bottom subplot
ax2.yaxis.set_tick_params(labelleft=False)
# turn off the y ticks in the bottom subplot
ax2.set_yticks([])
# we set the y axis title
ax1.set_ylabel('Concentration')
# we define the X, Y matrices for meshgrid: n x 2
X, Y = np.meshgrid(np.linspace(-1,1,n), np.array([0,1]))
# we define the initial values of the mesh points
mat = (X+Y)*0
# we add the colormesh to the bottom subplot
colormesh = ax2.pcolormesh(X, Y, mat, cmap='Purples', vmin=0, vmax=1, shading='gouraud')
# we call the class for path interaction and animation, passing figure, patch and colormesh
interactor = PathInteractor(fig, patch, colormesh)