Coverage for ase / cli / build.py: 58.82%

102 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1# fmt: off 

2 

3# Note: 

4# Try to avoid module level import statements here to reduce 

5# import time during CLI execution 

6import sys 

7 

8import numpy as np 

9 

10 

11class CLICommand: 

12 """Build an atom, molecule or bulk structure. 

13 

14 Atom: 

15 

16 ase build <chemical symbol> ... 

17 

18 Molecule: 

19 

20 ase build <formula> ... 

21 

22 where <formula> must be one of the formulas known to ASE 

23 (see here: https://ase-lib.org/ase/build/build.html#molecules). 

24 

25 Bulk: 

26 

27 ase build -x <crystal structure> <formula> ... 

28 

29 Examples 

30 -------- 

31 

32 ase build Li # lithium atom 

33 ase build Li -M 1 # ... with a magnetic moment of 1 

34 ase build Li -M 1 -V 3.5 # ... in a 7x7x7 Ang cell 

35 ase build H2O # water molecule 

36 ase build -x fcc Cu -a 3.6 # FCC copper 

37 """ 

38 

39 @staticmethod 

40 def add_arguments(parser): 

41 add = parser.add_argument 

42 add('name', metavar='formula/input-file', 

43 help='Chemical formula or input filename.') 

44 add('output', nargs='?', help='Output file.') 

45 add('-M', '--magnetic-moment', 

46 metavar='M1,M2,...', 

47 help='Magnetic moments. ' 

48 'Use "-M 1" or "-M 2.3,-2.3"') 

49 add('--modify', metavar='...', 

50 help='Modify atoms with Python statement. ' 

51 'Example: --modify="atoms.positions[-1,2]+=0.1"') 

52 add('-V', '--vacuum', type=float, 

53 help='Amount of vacuum to add around isolated atoms ' 

54 '(in Angstrom)') 

55 add('-v', '--vacuum0', type=float, 

56 help='Deprecated. Use -V or --vacuum instead') 

57 add('--unit-cell', metavar='CELL', 

58 help='Unit cell in Angstrom. Examples: "10.0" or "9,10,11"') 

59 add('--bond-length', type=float, metavar='LENGTH', 

60 help='Bond length of dimer in Angstrom') 

61 add('-x', '--crystal-structure', 

62 help='Crystal structure', 

63 choices=['sc', 'fcc', 'bcc', 'hcp', 'diamond', 

64 'zincblende', 'rocksalt', 'cesiumchloride', 

65 'fluorite', 'wurtzite']) 

66 add('-a', '--lattice-constant', default='', metavar='LENGTH', 

67 help='Lattice constant or comma-separated lattice constantes in ' 

68 'Angstrom') 

69 add('--orthorhombic', action='store_true', 

70 help='Use orthorhombic unit cell') 

71 add('--cubic', action='store_true', 

72 help='Use cubic unit cell') 

73 add('-r', '--repeat', 

74 help='Repeat unit cell. Use "-r 2" or "-r 2,3,1"') 

75 add('-g', '--gui', action='store_true', 

76 help='open ase gui') 

77 add('--periodic', action='store_true', 

78 help='make structure fully periodic') 

79 

80 @staticmethod 

81 def run(args, parser): 

82 from ase.db import connect 

83 from ase.io import read, write 

84 from ase.visualize import view 

85 

86 if args.vacuum0: 

87 parser.error('Please use -V or --vacuum instead!') 

88 

89 if '.' in args.name: 

90 # Read from file: 

91 atoms = read(args.name) 

92 elif args.crystal_structure: 

93 atoms = build_bulk(args) 

94 else: 

95 atoms = build_molecule(args) 

96 

97 if args.magnetic_moment: 

98 magmoms = np.array( 

99 [float(m) for m in args.magnetic_moment.split(',')]) 

100 atoms.set_initial_magnetic_moments( 

101 np.tile(magmoms, len(atoms) // len(magmoms))) 

102 

103 if args.modify: 

104 exec(args.modify, {'atoms': atoms}) 

105 

106 if args.repeat is not None: 

107 r = args.repeat.split(',') 

108 if len(r) == 1: 

109 r = 3 * r 

110 atoms = atoms.repeat([int(c) for c in r]) 

111 

112 if args.gui: 

113 view(atoms) 

114 

115 if args.output: 

116 write(args.output, atoms) 

117 elif sys.stdout.isatty(): 

118 write(args.name + '.json', atoms) 

119 else: 

120 con = connect(sys.stdout, type='json') 

121 con.write(atoms, name=args.name) 

122 

123 

124def build_molecule(args): 

125 from ase.atoms import Atoms 

126 from ase.build import molecule 

127 from ase.data import ( 

128 atomic_numbers, 

129 covalent_radii, 

130 ground_state_magnetic_moments, 

131 ) 

132 from ase.symbols import string2symbols 

133 

134 try: 

135 # Known molecule or atom? 

136 atoms = molecule(args.name) 

137 except (NotImplementedError, KeyError): 

138 symbols = string2symbols(args.name) 

139 if len(symbols) == 1: 

140 Z = atomic_numbers[symbols[0]] 

141 magmom = ground_state_magnetic_moments[Z] 

142 atoms = Atoms(args.name, magmoms=[magmom]) 

143 elif len(symbols) == 2: 

144 # Dimer 

145 if args.bond_length is None: 

146 b = (covalent_radii[atomic_numbers[symbols[0]]] + 

147 covalent_radii[atomic_numbers[symbols[1]]]) 

148 else: 

149 b = args.bond_length 

150 atoms = Atoms(args.name, positions=[(0, 0, 0), 

151 (b, 0, 0)]) 

152 else: 

153 raise ValueError('Unknown molecule: ' + args.name) 

154 else: 

155 if len(atoms) == 2 and args.bond_length is not None: 

156 atoms.set_distance(0, 1, args.bond_length) 

157 

158 if args.unit_cell is None: 

159 if args.vacuum: 

160 atoms.center(vacuum=args.vacuum) 

161 else: 

162 atoms.center(about=[0, 0, 0]) 

163 else: 

164 a = [float(x) for x in args.unit_cell.split(',')] 

165 if len(a) == 1: 

166 cell = [a[0], a[0], a[0]] 

167 elif len(a) == 3: 

168 cell = a 

169 else: 

170 a, b, c, alpha, beta, gamma = a 

171 degree = np.pi / 180.0 

172 cosa = np.cos(alpha * degree) 

173 cosb = np.cos(beta * degree) 

174 sinb = np.sin(beta * degree) 

175 cosg = np.cos(gamma * degree) 

176 sing = np.sin(gamma * degree) 

177 cell = [[a, 0, 0], 

178 [b * cosg, b * sing, 0], 

179 [c * cosb, c * (cosa - cosb * cosg) / sing, 

180 c * np.sqrt( 

181 sinb**2 - ((cosa - cosb * cosg) / sing)**2)]] 

182 atoms.cell = cell 

183 atoms.center() 

184 

185 atoms.pbc = args.periodic 

186 

187 return atoms 

188 

189 

190def build_bulk(args): 

191 from ase.build import bulk 

192 

193 L = args.lattice_constant.replace(',', ' ').split() 

194 d = {key: float(x) for key, x in zip('ac', L)} 

195 atoms = bulk(args.name, crystalstructure=args.crystal_structure, 

196 a=d.get('a'), c=d.get('c'), 

197 orthorhombic=args.orthorhombic, cubic=args.cubic) 

198 

199 M, X = {'Fe': (2.3, 'bcc'), 

200 'Co': (1.2, 'hcp'), 

201 'Ni': (0.6, 'fcc')}.get(args.name, (None, None)) 

202 if M is not None and args.crystal_structure == X: 

203 atoms.set_initial_magnetic_moments([M] * len(atoms)) 

204 

205 return atoms