Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright 2011-2018 Kwant authors. 

2# 

3# This file is part of Kwant. It is subject to the license terms in the file 

4# LICENSE.rst found in the top-level directory of this distribution and at 

5# https://kwant-project.org/license. A list of Kwant authors can be found in 

6# the file AUTHORS.rst at the top-level directory of this distribution and at 

7# https://kwant-project.org/authors. 

8 

9import collections 

10import inspect 

11import cmath 

12import warnings 

13 

14import tinyarray as ta 

15import numpy as np 

16import scipy.linalg 

17import scipy.spatial 

18 

19from . import builder, system, plotter 

20from .linalg import lll 

21from .builder import herm_conj, HermConjOfFunc 

22from .lattice import TranslationalSymmetry 

23from ._common import get_parameters, memoize 

24 

25 

26__all__ = ['wraparound', 'plot_2d_bands'] 

27 

28 

29def _set_signature(func, params): 

30 """Set the signature of 'func'. 

31 

32 Parameters 

33 ---------- 

34 func : callable 

35 params: sequence of str 

36 Parameter names to put in the signature. These will be added as 

37 'POSITIONAL_ONLY' type parameters. 

38 """ 

39 params = [inspect.Parameter(name, inspect.Parameter.POSITIONAL_ONLY) 

40 for name in params] 

41 func.__signature__ = inspect.Signature(params) 

42 

43@memoize 

44def _callable_herm_conj(val): 

45 """Keep the same id for every 'val'.""" 

46 return HermConjOfFunc(val) 

47 

48## This wrapper is needed so that finalized systems that 

49## have been wrapped can be queried for their symmetry, which 

50## is needed for Brillouin zone calculations (plotting). 

51 

52class WrappedBuilder(builder.Builder): 

53 

54 def finalized(self): 

55 ret = super().finalized() 

56 ret._momentum_names = self._momentum_names 

57 ret._wrapped_symmetry = self._wrapped_symmetry 

58 return ret 

59 

60 

61def wraparound(builder, keep=None, *, coordinate_names='xyz'): 

62 """Replace translational symmetries by momentum parameters. 

63 

64 A new Builder instance is returned. By default, each symmetry is replaced 

65 by one scalar momentum parameter that is appended to the already existing 

66 arguments of the system. Optionally, one symmetry may be kept by using the 

67 `keep` argument. The momentum parameters will have names like 'k_n' where 

68 the 'n' are specified by 'coordinate_names'. 

69 

70 Parameters 

71 ---------- 

72 builder : `~kwant.builder.Builder` 

73 keep : int, optional 

74 Which (if any) translational symmetry to keep. 

75 coordinate_names : sequence of strings, default: 'xyz' 

76 The names of the coordinates along the symmetry 

77 directions of 'builder'. 

78 

79 Notes 

80 ----- 

81 Wraparound is stop-gap functionality until Kwant 2.x which will include 

82 support for higher-dimension translational symmetry in the low-level system 

83 format. It will be deprecated in the 2.0 release of Kwant. 

84 """ 

85 

86 @memoize 

87 def bind_site(val): 

88 def f(*args): 

89 a, *args = args 

90 return val(a, *args[:mnp]) 

91 

92 assert callable(val) 

93 _set_signature(f, get_parameters(val) + momenta) 

94 return f 

95 

96 @memoize 

97 def bind_hopping_as_site(elem, val): 

98 def f(*args): 

99 a, *args = args 

100 phase = cmath.exp(1j * ta.dot(elem, args[mnp:])) 

101 v = val(a, sym.act(elem, a), *args[:mnp]) if callable(val) else val 

102 pv = phase * v 

103 return pv + herm_conj(pv) 

104 

105 params = ('_site0',) 

106 if callable(val): 

107 params += get_parameters(val)[2:] # cut off both site parameters 

108 _set_signature(f, params + momenta) 

109 return f 

110 

111 @memoize 

112 def bind_hopping(elem, val): 

113 def f(*args): 

114 a, b, *args = args 

115 phase = cmath.exp(1j * ta.dot(elem, args[mnp:])) 

116 v = val(a, sym.act(elem, b), *args[:mnp]) if callable(val) else val 

117 return phase * v 

118 

119 params = ('_site0', '_site1') 

120 if callable(val): 

121 params += get_parameters(val)[2:] # cut off site parameters 

122 _set_signature(f, params + momenta) 

123 return f 

124 

125 @memoize 

126 def bind_sum(num_sites, *vals): 

127 """Construct joint signature for all 'vals'.""" 

128 

129 def f(*in_args): 

130 acc = 0 

131 for val, selection in val_selection_pairs: 

132 if selection: # Otherwise: reuse previous out_args. 

133 out_args = tuple(in_args[i] for i in selection) 

134 if callable(val): 

135 acc = acc + val(*out_args) 

136 else: 

137 acc = acc + val 

138 return acc 

139 

140 params = collections.OrderedDict() 

141 

142 # Add the leading one or two 'site' parameters. 

143 site_params = ['_site{}'.format(i) for i in range(num_sites)] 

144 for name in site_params: 

145 params[name] = inspect.Parameter( 

146 name, inspect.Parameter.POSITIONAL_ONLY) 

147 

148 # Add all the other parameters (except for the momenta). Setup the 

149 # 'selections'. 

150 selections = [] 

151 for val in vals: 

152 if not callable(val): 

153 selections.append(()) 

154 continue 

155 val_params = get_parameters(val)[num_sites:] 

156 assert val_params[mnp:] == momenta 

157 val_params = val_params[:mnp] 

158 selections.append((*site_params, *val_params)) 

159 for p in val_params: 

160 # Skip parameters that exist in previously added functions. 

161 if p in params: 

162 continue 

163 params[p] = inspect.Parameter( 

164 p, inspect.Parameter.POSITIONAL_ONLY) 

165 

166 # Sort values such that ones with the same arguments are bunched. 

167 # Prepare 'val_selection_pairs' that is used in the function 'f' above. 

168 params_keys = list(params.keys()) 

169 val_selection_pairs = [] 

170 prev_selection = None 

171 argsort = sorted(range(len(selections)), key=selections.__getitem__) 

172 momenta_sel = tuple(range(mnp, 0, 1)) 

173 for i in argsort: 

174 selection = selections[i] 

175 if selection and selection != prev_selection: 

176 prev_selection = selection = tuple( 

177 params_keys.index(s) for s in selection) + momenta_sel 

178 else: 

179 selection = () 

180 val_selection_pairs.append((vals[i], selection)) 

181 

182 # Finally, add the momenta. 

183 for k in momenta: 

184 params[k] = inspect.Parameter( 

185 k, inspect.Parameter.POSITIONAL_ONLY) 

186 

187 f.__signature__ = inspect.Signature(params.values()) 

188 return f 

189 

190 try: 

191 momenta = ['k_{}'.format(coordinate_names[i]) 

192 for i in range(len(builder.symmetry.periods))] 

193 except IndexError: 

194 raise ValueError("All symmetry directions must have a name specified " 

195 "in coordinate_names") 

196 

197 if keep is None: 

198 ret = WrappedBuilder() 

199 sym = builder.symmetry 

200 else: 

201 periods = list(builder.symmetry.periods) 

202 ret = WrappedBuilder(TranslationalSymmetry(periods.pop(keep))) 

203 sym = TranslationalSymmetry(*periods) 

204 momenta.pop(keep) 

205 momenta = tuple(momenta) 

206 mnp = -len(momenta) # Used by the bound functions above. 

207 

208 # Store the names of the momentum parameters and the symmetry of the 

209 # old Builder (this will be needed for band structure plotting) 

210 ret._momentum_names = momenta 

211 ret._wrapped_symmetry = builder.symmetry 

212 

213 # Wrapped around system retains conservation law and chiral symmetry. 

214 # We use 'bind_site' to add the momenta arguments if required. 

215 cons = builder.conservation_law 

216 ret.conservation_law = bind_site(cons) if callable(cons) else cons 

217 chiral = builder.chiral 

218 ret.chiral = bind_site(chiral) if callable(chiral) else chiral 

219 

220 if builder.particle_hole is not None or builder.time_reversal is not None: 

221 warnings.warn('`particle_hole` and `time_reversal` symmetries are set ' 

222 'on the input builder. However they are ignored for the ' 

223 'wrapped system, since Kwant lacks a way to express the ' 

224 'existence (or not) of a symmetry at k != 0.', 

225 RuntimeWarning, stacklevel=2) 

226 ret.particle_hole = None 

227 ret.time_reversal = None 

228 

229 ret.vectorize = builder.vectorize 

230 

231 sites = {} 

232 hops = collections.defaultdict(list) 

233 

234 # Store lists of values, so that multiple values can be assigned to the 

235 # same site or hopping. 

236 for site, val in builder.site_value_pairs(): 

237 # Every 'site' is in the FD of the original symmetry. 

238 # Move the sites to the FD of the remaining symmetry, this guarantees that 

239 # every site in the new system is an image of an original FD site translated 

240 # purely by the remaining symmetry. 

241 sites[ret.symmetry.to_fd(site)] = [val] # a list to append wrapped hoppings 

242 

243 for hop, val in builder.hopping_value_pairs(): 

244 a, b = hop 

245 # 'a' is in the FD of original symmetry. 

246 # Get translation from FD of original symmetry to 'b', 

247 # this is different from 'b_dom = sym.which(b)'. 

248 b_dom = builder.symmetry.which(b) 

249 # Throw away part that is in the remaining translation direction, so we get 

250 # an element of 'sym' which is being wrapped 

251 b_dom = ta.array([t for i, t in enumerate(b_dom) if i != keep]) 

252 # Pull back using the remainder, which is purely in the wrapped directions. 

253 # This guarantees that 'b_wa' is an image of an original FD site translated 

254 # purely by the remaining symmetry. 

255 b_wa = sym.act(-b_dom, b) 

256 # Move the hopping to the FD of the remaining symmetry 

257 a, b_wa = ret.symmetry.to_fd(a, b_wa) 

258 

259 if a == b_wa: 

260 # The hopping gets wrapped-around into an onsite Hamiltonian. 

261 # Since site `a` already exists in the system, we can simply append. 

262 sites[a].append(bind_hopping_as_site(b_dom, val)) 

263 else: 

264 # The hopping remains a hopping. 

265 if any(b_dom): 

266 # The hopping got wrapped-around. 

267 val = bind_hopping(b_dom, val) 

268 

269 # Make sure that there is only one entry for each hopping 

270 # pointing in one direction, modulo the remaining translations. 

271 b_wa_r, a_r = ret.symmetry.to_fd(b_wa, a) 

272 if (b_wa_r, a_r) in hops: 

273 assert (a, b_wa) not in hops 

274 if callable(val): 

275 val = _callable_herm_conj(val) 

276 else: 

277 val = herm_conj(val) 

278 

279 hops[b_wa_r, a_r].append((val, b_dom)) 

280 else: 

281 hops[a, b_wa].append((val, b_dom)) 

282 

283 # Copy stuff into result builder, converting lists of more than one element 

284 # into summing functions. 

285 for site, vals in sites.items(): 

286 if len(vals) == 1: 

287 # no need to bind onsites without extra wrapped hoppings 

288 ret[site] = vals[0] 

289 else: 

290 val = vals[0] 

291 vals[0] = bind_site(val) if callable(val) else val 

292 ret[site] = bind_sum(1, *vals) 

293 

294 for hop, vals_doms in hops.items(): 

295 if len(vals_doms) == 1: 

296 # no need to bind hoppings that are not already bound 

297 val, b_dom = vals_doms[0] 

298 ret[hop] = val 

299 else: 

300 new_vals = [bind_hopping(b_dom, val) if callable(val) 

301 and not any(b_dom) # skip hoppings already bound 

302 else val for val, b_dom in vals_doms] 

303 ret[hop] = bind_sum(2, *new_vals) 

304 return ret 

305 

306 

307def plot_2d_bands(syst, k_x=31, k_y=31, params=None, 

308 mask_brillouin_zone=False, extend_bbox=0, file=None, 

309 show=True, dpi=None, fig_size=None, ax=None): 

310 """Plot 2D band structure of a wrapped around system. 

311 

312 This function is primarily useful for systems that have translational 

313 symmetry vectors that are non-orthogonal (e.g. graphene). This function 

314 will properly plot the band structure in an orthonormal basis in k-space, 

315 as opposed to in the basis of reciprocal lattice vectors (which would 

316 produce a "skewed" Brillouin zone). 

317 

318 If your system has orthogonal lattice vectors, you are probably better 

319 off using `kwant.plotter.spectrum`. 

320 

321 Parameters 

322 ---------- 

323 syst : `kwant.system.FiniteSystem` 

324 A 2D system that was finalized from a Builder produced by 

325 `kwant.wraparound.wraparound`. Note that this *must* be a finite 

326 system; so `kwant.wraparound.wraparound` should have been called with 

327 ``keep=None``. 

328 k_x, k_y : int or sequence of float, default: 31 

329 Either a number of sampling points, or a sequence of points at which 

330 the band structure is to be evaluated, in units of inverse length. 

331 params : dict, optional 

332 Dictionary of parameter names and their values, not including the 

333 momentum parameters. 

334 mask_brillouin_zone : bool, default: False 

335 If True, then the band structure will only be plotted over the first 

336 Brillouin zone. By default the band structure is plotted over a 

337 rectangular bounding box that contains the Brillouin zone. 

338 extend_bbox : float, default: 0 

339 Amount by which to extend the region over which the band structure is 

340 plotted, expressed as a proportion of the Brillouin zone bounding box 

341 length. i.e. ``extend_bbox=0.1`` will extend the region by 10% (in all 

342 directions). 

343 file : string or file object, optional 

344 The output file. If None, output will be shown instead. 

345 show : bool, default: False 

346 Whether ``matplotlib.pyplot.show()`` is to be called, and the output is 

347 to be shown immediately. Defaults to `True`. 

348 dpi : float, optional 

349 Number of pixels per inch. If not set the ``matplotlib`` default is 

350 used. 

351 fig_size : tuple, optional 

352 Figure size `(width, height)` in inches. If not set, the default 

353 ``matplotlib`` value is used. 

354 ax : ``matplotlib.axes.Axes`` instance, optional 

355 If `ax` is not `None`, no new figure is created, but the plot is done 

356 within the existing Axes `ax`. in this case, `file`, `show`, `dpi` 

357 and `fig_size` are ignored. 

358 

359 Returns 

360 ------- 

361 fig : matplotlib figure 

362 A figure with the output if `ax` is not set, else None. 

363 

364 Notes 

365 ----- 

366 This function produces plots where the units of momentum are inverse 

367 length. This is contrary to `kwant.plotter.bands`, where the units 

368 of momentum are inverse lattice constant. 

369 

370 If the lattice vectors for the symmetry of ``syst`` are not orthogonal, 

371 then part of the plotted band structure will be outside the first Brillouin 

372 zone (inside the bounding box of the brillouin zone). Setting 

373 ``mask_brillouin_zone=True`` will cause the plot to be truncated outside of 

374 the first Brillouin zone. 

375 

376 See Also 

377 -------- 

378 kwant.plotter.spectrum 

379 """ 

380 if not hasattr(syst, '_wrapped_symmetry'): 

381 raise TypeError("Expecting a system that was produced by " 

382 "'kwant.wraparound.wraparound'.") 

383 if isinstance(syst, system.InfiniteSystem): 

384 msg = ("All symmetry directions must be wrapped around: specify " 

385 "'keep=None' when calling 'kwant.wraparound.wraparound'.") 

386 raise TypeError(msg) 

387 if isinstance(syst, builder.Builder): 387 ↛ 388line 387 didn't jump to line 388, because the condition on line 387 was never true

388 msg = ("Expecting a finalized system: remember to finalize your " 

389 "system with 'syst.finalized()'.") 

390 raise TypeError(msg) 

391 

392 params = params or {} 

393 lat_ndim, space_ndim = syst._wrapped_symmetry.periods.shape 

394 

395 if lat_ndim != 2: 

396 raise ValueError("Expected a system with a 2D translational symmetry.") 

397 if space_ndim != lat_ndim: 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true

398 raise ValueError("Lattice dimension must equal realspace dimension.") 

399 

400 # columns of B are lattice vectors 

401 B = np.array(syst._wrapped_symmetry.periods).T 

402 # columns of A are reciprocal lattice vectors 

403 A = np.linalg.pinv(B).T 

404 

405 ## calculate the bounding box for the 1st Brillouin zone 

406 

407 # Get lattice points that neighbor the origin, in basis of lattice vectors 

408 reduced_vecs, transf = lll.lll(A.T) 

409 neighbors = ta.dot(lll.voronoi(reduced_vecs), transf) 

410 # Add the origin to these points. 

411 klat_points = np.concatenate(([[0] * lat_ndim], neighbors)) 

412 # Transform to cartesian coordinates and rescale. 

413 # Will be used in 'outside_bz' function, later on. 

414 klat_points = 2 * np.pi * np.dot(klat_points, A.T) 

415 # Calculate the Voronoi cell vertices 

416 vor = scipy.spatial.Voronoi(klat_points) 

417 around_origin = vor.point_region[0] 

418 bz_vertices = vor.vertices[vor.regions[around_origin]] 

419 # extract bounding box 

420 k_max = np.max(np.abs(bz_vertices), axis=0) 

421 

422 ## build grid along each axis, if needed 

423 ks = [] 

424 for k, km in zip((k_x, k_y), k_max): 

425 k = np.array(k) 

426 if not k.shape: 

427 if extend_bbox: 

428 km += km * extend_bbox 

429 k = np.linspace(-km, km, k) 

430 ks.append(k) 

431 

432 # TODO: It is very inefficient to call 'momentum_to_lattice' once for 

433 # each point (for trivial Hamiltonians 60% of the time is spent 

434 # doing this). We should instead transform the whole grid in one call. 

435 

436 def momentum_to_lattice(k): 

437 k, residuals = scipy.linalg.lstsq(A, k)[:2] 

438 if np.any(abs(residuals) > 1e-7): 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true

439 raise RuntimeError("Requested momentum doesn't correspond" 

440 " to any lattice momentum.") 

441 return k 

442 

443 def ham(k_x, k_y=None, **params): 

444 # transform into the basis of reciprocal lattice vectors 

445 k = momentum_to_lattice([k_x] if k_y is None else [k_x, k_y]) 

446 p = dict(zip(syst._momentum_names, k), **params) 

447 return syst.hamiltonian_submatrix(params=p, sparse=False) 

448 

449 def outside_bz(k_x, k_y, **_): 

450 dm = scipy.spatial.distance_matrix(klat_points, [[k_x, k_y]]) 

451 return np.argmin(dm) != 0 # is origin no closest 'klat_point' to 'k'? 

452 

453 fig = plotter.spectrum(ham, 

454 x=('k_x', ks[0]), 

455 y=('k_y', ks[1]) if lat_ndim == 2 else None, 

456 params=params, 

457 mask=(outside_bz if mask_brillouin_zone else None), 

458 file=file, show=show, dpi=dpi, 

459 fig_size=fig_size, ax=ax) 

460 return fig