codexpast-exam-of-the-chemistry-course-of-the-year-3-of-the-natural-sciences-course-of-the-university-of-cambridge2021paper-1-cluster-skeletons.py
past-exam-of-the-chemistry-course-of-the-year-3-of-the-natural-sciences-course-of-the-university-of-cambridge/2021/paper-1-cluster-skeletons.py
"""Original schematic cage geometries; Python 3.14, numpy, matplotlib.
Outputs the PNG basename to the current working directory.
"""
import itertools
import numpy as np
import matplotlib.pyplot as plt
PALETTE = ['#3274a1', '#e18b2b', '#3e9165', '#a55b9b', '#d05d58', '#7a7c4c']
def automorphisms(adj, colours):
"""Small coloured-graph backtracking, independent of point-group labels."""
n = len(adj)
candidates = [[j for j in range(n) if colours[j] == colours[i] and sum(adj[j]) == sum(adj[i])] for i in range(n)]
order = sorted(range(n), key=lambda i: (len(candidates[i]), -sum(adj[i])))
result = []
def visit(mapping, used):
if len(mapping) == n:
result.append([mapping[i] for i in range(n)])
return
i = order[len(mapping)]
for j in candidates[i]:
if j in used or any(adj[i][k] != adj[j][v] for k, v in mapping.items()):
continue
visit({**mapping, i: j}, used | {j})
visit({}, set())
return result
def orbits(adj, colours):
autos = automorphisms(adj, colours)
remaining = {i for i,c in enumerate(colours) if c == 'B'}
groups = []
while remaining:
i = min(remaining)
group = sorted({p[i] for p in autos})
groups.append(group)
remaining -= set(group)
return groups
def cages():
g = (1 + np.sqrt(5))/2
ico = np.array([(0,a,b*g) for a in [-1,1] for b in [-1,1]]
+ [(a,b*g,0) for a in [-1,1] for b in [-1,1]]
+ [(a*g,0,b) for a in [-1,1] for b in [-1,1]], float)
adj = np.isclose(np.linalg.norm(ico[:,None,:]-ico[None,:,:],axis=-1), 2)
p2 = np.where(adj[0])[0][0]
ec = ['P' if i in [0,p2] else 'B' for i in range(12)]
angles = np.arange(4)*np.pi/2
ten = np.vstack([[0,0,1.8], np.column_stack([np.cos(angles), np.sin(angles), np.full(4,.6)]),
np.column_stack([np.cos(angles+np.pi/4),np.sin(angles+np.pi/4), np.full(4,-.6)]), [0,0,-1.8]])
ta = np.zeros((10,10),bool)
edges = [(0,1+i) for i in range(4)] + [(9,5+i) for i in range(4)]
edges += [(1+i,1+(i+1)%4) for i in range(4)] + [(5+i,5+(i+1)%4) for i in range(4)]
edges += [(1+i,5+i) for i in range(4)] + [(1+i,5+(i-1)%4) for i in range(4)]
for i,j in edges:ta[i,j]=ta[j,i]=True
fc = ['P']+['BN']+['B']*8
return (ico,adj,ec), (ten,ta,fc)
def project(points):
theta = .39
p = points @ np.array([[np.cos(theta),-np.sin(theta),0],[np.sin(theta),np.cos(theta),0],[0,0,1]]).T
return np.column_stack([p[:,0]+.28*p[:,1],p[:,2]+.35*p[:,1]])
def cage(ax, points, adj, colours, title):
pts = project(points)
groups = orbits(adj, ['B' if c=='BN' else c for c in colours]) if 'BN' not in colours else orbits(adj,colours)
# BN is a distinct boron environment and is included explicitly below.
if 'BN' in colours:groups = [[colours.index('BN')]]+groups
labels = {i:chr(97+k) for k,g in enumerate(groups) for i in g}
for i,j in itertools.combinations(range(len(points)),2):
if adj[i,j]:ax.plot(*pts[[i,j]].T,color='#b9bdc2',lw=1.4,zorder=1)
for i,(x,y) in enumerate(pts):
label = 'P' if colours[i]=='P' else labels[i]
k=next((k for k,g in enumerate(groups) if i in g),None)
ax.scatter(x,y,s=310,color='#202933' if k is None else PALETTE[k],edgecolor='white',zorder=3)
ax.text(x,y,label,color='white',ha='center',va='center',weight='bold',fontsize=11,zorder=4)
if colours[i]=='BN':ax.annotate('B←NEt₃',(x,y),(x+.55,y+.6),fontsize=10,arrowprops={'arrowstyle':'-','color':'#444'})
sizes = [len(g) for g in groups]
ax.text(.5,-.03,'B-site classes: '+', '.join(f'{chr(97+i)} × {s}' for i,s in enumerate(sizes)),ha='center',va='top',transform=ax.transAxes,fontsize=9)
ax.set_title(title,fontsize=12)
ax.set_aspect('equal');ax.axis('off');ax.margins(.20)
def draw():
fig,axs=plt.subplots(2,2,figsize=(10.8,8.0),layout='constrained')
a=axs[0,0]
pts=np.array([[-1,-.5],[1,-.5],[0,1.1]])
for i,j in [(0,1),(1,2),(2,0)]:a.plot(*pts[[i,j]].T,color='#444',lw=1.5)
for x,y in pts:a.text(x,y,'BH₂',ha='center',va='center',bbox={'facecolor':'white','edgecolor':'none'},fontsize=12)
a.plot([-1,0,1],[-.5,-1.05,-.5],'--',color='#ad6027');a.text(0,-1.05,'H',ha='center',va='center',bbox={'facecolor':'white','edgecolor':'none'},fontsize=12)
a.plot([0,0],[1.1,1.85],color='#444');a.text(0,1.9,'L',ha='center',fontsize=12)
a.text(0,-1.65,'L = CO or NCO⁻ (N-bound)',ha='center',fontsize=11)
a.set(xlim=(-2,2),ylim=(-1.9,2.25),title='Triborane donor adduct: localized contact model');a.axis('off')
a=axs[0,1]
pts=np.array([[-1,-.6],[1,-.6],[.15,.25],[-.2,1.7]])
for i,j in itertools.combinations(range(4),2):a.plot(*pts[[i,j]].T,color='#777',lw=1.5)
for i,(x,y) in enumerate(pts):
a.text(x,y,'BH₂' if i<3 else 'RhH(PPh₃)₂',ha='center',va='center',fontsize=11,bbox={'facecolor':'white','edgecolor':'none'})
a.plot([-1,0,1],[-.6,-1.2,-.6],'--',color='#ad6027');a.text(0,-1.2,'H',ha='center',va='center',bbox={'facecolor':'white','edgecolor':'none'},fontsize=12)
a.text(0,-1.85,'RhB₃ skeleton; 6 skeletal electron pairs',ha='center',fontsize=10)
a.set(xlim=(-2,2),ylim=(-2.1,2.5),title='Rhodium as a skeletal vertex');a.axis('off')
e,f=cages()
cage(axs[1,0],*e,'E: adjacent P vertices on an icosahedron')
cage(axs[1,1],*f,'F: one six-signal positional model')
fig.savefig('paper-1-cluster-skeletons.png',dpi=155,facecolor='white')
plt.close(fig)
if __name__=='__main__':draw()