Mechanics of Defect Evolution Library
Loading...
Searching...
No Matches
create_periodicity_figure.py
Go to the documentation of this file.
1"""Translation lattice, affine field and periodically tiled polycrystal schematic."""
2from pathlib import Path
3import numpy as np
4import matplotlib
5matplotlib.use('Agg')
6import matplotlib.pyplot as plt
7from matplotlib.patches import Polygon, FancyArrowPatch
8OUT=Path(__file__).resolve().parents[1]/'figures'
9plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'pdf.fonttype':42})
10blue='#176b91'; orange='#d27a30'; dark='#203547'
11B=np.array([[1,.35],[0,1.]])
12
13def arrow(ax,a,b,color=blue):
14 ax.add_patch(FancyArrowPatch(a,b,arrowstyle='-|>',mutation_scale=12,color=color,lw=1.4,zorder=10))
15
16# Construct whole Voronoi grains for the central seed representatives.
17# Their union is a jagged fundamental unit; translated units tile the plane.
18# No grain is clipped at the lattice parallelogram.
19seeds=np.array([[-.32,-.3],[.2,-.32],[-.2,.17],[.31,.18],[.04,.42]])
20images=[(p+[i,j],k) for i in range(-1,2) for j in range(-1,2) for k,p in enumerate(seeds)]
21square=np.array([[-.5,-.5],[.5,-.5],[.5,.5],[-.5,.5]])
22def clip(poly,n,c):
23 result=[]
24 for a,b in zip(poly,np.roll(poly,-1,axis=0)):
25 da,db=a@n-c,b@n-c
26 if da<=1e-10:result.append(a)
27 if (da<0)!=(db<0):result.append(a+(b-a)*da/(da-db))
28 return np.array(result)
29grains=[]
30for k,p in enumerate(seeds):
31 poly=4*square.copy()
32 for q,_ in images:
33 if np.array_equal(p,q):continue
34 poly=clip(poly,q-p,(q@q-p@p)/2)
35 if len(poly)<3:break
36 if len(poly)>=3:grains.append((poly,k))
37# Shared grain edges are internal; retain only the unit's external perimeter.
38from collections import Counter
39edge_counts=Counter()
40edge_points={}
41for poly,_ in grains:
42 for a,b in zip(poly,np.roll(poly,-1,axis=0)):
43 key=tuple(sorted((tuple(np.round(a,9)),tuple(np.round(b,9)))))
44 edge_counts[key]+=1
45 edge_points[key]=np.array([a,b])
46perimeter=[edge_points[e] for e,n in edge_counts.items() if n==1]
47# Whole grains cover one fundamental-cell area without artificial cut faces.
48area=sum(abs(np.dot(p[:,0],np.roll(p[:,1],-1))-np.dot(p[:,1],np.roll(p[:,0],-1)))/2 for p,_ in grains)
49assert np.isclose(area,1), area
50assert all(n in (1,2) for n in edge_counts.values())
51colors=plt.get_cmap('Set2').colors
52
53def cell(ax,i,j,alpha=1):
54 for poly,k in grains:
55 ax.add_patch(Polygon((poly+[i,j])@B.T,fc=colors[k],ec='white',lw=.45,alpha=alpha,zorder=3))
56 for edge in perimeter:
57 ax.plot(*((edge+[i,j])@B.T).T,color=blue,lw=1.3 if i==j==0 else .5,alpha=alpha,zorder=6 if i==j==0 else 4)
58
59def lattice(ax,filled):
60 for i in range(-3,4):
61 for j in range(-3,4):
62 if filled:cell(ax,i,j,float(np.exp(-.28*(i*i+j*j))))
63 pt=B@np.array([i,j]);ax.plot(*pt,'.',color=blue,ms=2,alpha=.35,zorder=5)
64 for i in range(-3,4):
65 for ends in [np.array([[i,-3],[i,3]]),np.array([[-3,i],[3,i]])]:
66 ax.plot(*(ends@B.T).T,color=blue,lw=.5,alpha=.18,zorder=1)
67 if not filled:cell(ax,0,0)
68 ax.plot(0,0,'o',color=dark,ms=3,zorder=12)
69 ax.annotate('(0, 0)',(0,0),xytext=(5,-14),textcoords='offset points',fontsize=9,zorder=15,
70 bbox=dict(fc='white',ec='none',alpha=.85,pad=1))
71 for v,label,offset in [(B[:,0],r'$\mathbf{a}_1$',(0,.12)),(B[:,1],r'$\mathbf{a}_2$',(-.25,0))]:
72 arrow(ax,np.zeros(2),v);ax.text(*(v+offset),label,color=blue,zorder=15)
73 ax.set(xlim=(-2.65,2.65),ylim=(-2.1,2.1),aspect='equal');ax.axis('off')
74
75fig,axs=plt.subplots(1,3,figsize=(12,3.7))
76ax,cx,dx=axs.flat
77fieldfig,bx=plt.subplots(figsize=(6,3.5))
78for i in range(-1,2):
79 for j in range(-1,2):
80 q=(np.array([[0,0],[1,0],[1,1],[0,1]])+[i,j])@B.T
81 ax.add_patch(Polygon(q,fc='#edf4f8' if i==j==0 else 'white',ec=blue,alpha=1 if i==j==0 else .3))
82for v,t in [(B[:,0],r'$\mathbf{a}_1$'),(B[:,1],r'$\mathbf{a}_2$')]:
83 arrow(ax,np.zeros(2),v);ax.text(*(v*.6+[0,.1]),t,color=blue)
84p=np.array([.2,.55]);ax.scatter(*p,c=orange);ax.scatter(*(p+B[:,0]),c=orange);arrow(ax,p,p+B[:,0]);ax.text(.65,.78,'equivalent points',ha='center',fontsize=9)
85ax.set(xlim=(-.5,2),ylim=(-.4,1.5),aspect='equal');ax.axis('off')
86x=np.linspace(0,1,300);v=.12*np.sin(2*np.pi*x)
87bx.plot(x,v,label='periodic fluctuation',color=blue);bx.plot(x,.5*x+v,label='affine + periodic field',color=orange)
88bx.scatter([0,1],[0,.5],c=orange);bx.set(xlabel='x / L',ylabel='Displacement (illustrative units)');bx.legend(frameon=False,fontsize=9);bx.grid(alpha=.15)
89lattice(cx,False);lattice(dx,True)
90for a,t in zip(axs.flat,['(a) Translation lattice','(b) Polycrystal unit at the origin','(c) Space filled by translated units']):a.set_title(t,fontsize=11,pad=12)
91fig.tight_layout(h_pad=2.5,w_pad=2)
92for ext in ['pdf','png']:fig.savefig(OUT/f'periodicity.{ext}',bbox_inches='tight',dpi=180)
93
94bx.set_title('Affine jump + periodic fluctuation',fontsize=11)
95fieldfig.tight_layout()
96for ext in ['pdf','png']:fieldfig.savefig(OUT/f'affine_periodic_field.{ext}',bbox_inches='tight',dpi=180)