Statistiques
| Révision :

root / ase / constraints.py @ 20

Historique | Voir | Annoter | Télécharger (21,74 ko)

1
from math import sqrt
2

    
3
import numpy as np
4

    
5
__all__ = ['FixCartesian', 'FixBondLength', 'FixedMode', 'FixConstraintSingle',
6
           'FixAtoms', 'UnitCellFilter', 'FixScaled', 'StrainFilter',
7
           'FixedPlane', 'Filter', 'FixConstraint', 'FixedLine',
8
           'FixBondLengths']
9

    
10

    
11
def slice2enlist(s):
12
    """Convert a slice object into a list of (new, old) tuples."""
13
    if isinstance(s, (list, tuple)):
14
        return enumerate(s)
15
    if s.step == None:
16
        step = 1
17
    else:
18
        step = s.step
19
    if s.start == None:
20
        start = 0
21
    else:
22
        start = s.start
23
    return enumerate(range(start, s.stop, step))
24

    
25
class FixConstraint:
26
    """Base class for classes that fix one or more atoms in some way."""
27

    
28
    def index_shuffle(self, ind):
29
        """Change the indices.
30

31
        When the ordering of the atoms in the Atoms object changes,
32
        this method can be called to shuffle the indices of the
33
        constraints.
34

35
        ind -- List or tuple of indices.
36

37
        """
38
        raise NotImplementedError
39

    
40
    def repeat(self, m, n):
41
        """ basic method to multiply by m, needs to know the length
42
        of the underlying atoms object for the assignment of
43
        multiplied constraints to work.
44
        """
45
        raise NotImplementedError
46

    
47
class FixConstraintSingle(FixConstraint):
48
    "Base class for classes that fix a single atom."
49

    
50
    def index_shuffle(self, ind):
51
        "The atom index must be stored as self.a."
52
        newa = -1 # Signal error
53
        for new, old in slice2enlist(ind):
54
            if old == self.a:
55
                newa = new
56
                break
57
        if newa == -1:
58
            raise IndexError('Constraint not part of slice')
59
        self.a = newa
60

    
61
class FixAtoms(FixConstraint):
62
    """Constraint object for fixing some chosen atoms."""
63
    def __init__(self, indices=None, mask=None):
64
        """Constrain chosen atoms.
65

66
        Parameters
67
        ----------
68
        indices : list of int
69
           Indices for those atoms that should be constrained.
70
        mask : list of bool
71
           One boolean per atom indicating if the atom should be
72
           constrained or not.
73

74
        Examples
75
        --------
76
        Fix all Copper atoms:
77

78
        >>> c = FixAtoms(mask=[s == 'Cu' for s in atoms.get_chemical_symbols()])
79
        >>> atoms.set_constraint(c)
80

81
        Fix all atoms with z-coordinate less than 1.0 Angstrom:
82

83
        >>> c = FixAtoms(mask=atoms.positions[:, 2] < 1.0)
84
        >>> atoms.set_constraint(c)
85
        """
86

    
87
        if indices is None and mask is None:
88
            raise ValueError('Use "indices" or "mask".')
89
        if indices is not None and mask is not None:
90
            raise ValueError('Use only one of "indices" and "mask".')
91

    
92
        if mask is not None:
93
            self.index = np.asarray(mask, bool)
94
        else:
95
            # Check for duplicates
96
            srt = np.sort(indices)
97
            for i in range(len(indices)-1):
98
                if srt[i] == srt[i+1]:
99
                    raise ValueError("FixAtoms: The indices array contained duplicates.  Perhaps you wanted to specify a mask instead, but forgot the mask= keyword.")
100
            self.index = np.asarray(indices, int)
101

    
102
        if self.index.ndim != 1:
103
            raise ValueError('Wrong argument to FixAtoms class!')
104

    
105
    def adjust_positions(self, old, new):
106
        new[self.index] = old[self.index]
107

    
108
    def adjust_forces(self, positions, forces):
109
        forces[self.index] = 0.0
110

    
111
    def index_shuffle(self, ind):
112
        # See docstring of superclass
113
        if self.index.dtype == bool:
114
            self.index = self.index[ind]
115
        else:
116
            index = []
117
            for new, old in slice2enlist(ind):
118
                if old in self.index:
119
                    index.append(new)
120
            if len(index) == 0:
121
                raise IndexError('All indices in FixAtoms not part of slice')
122
            self.index = np.asarray(index, int)
123

    
124
    def copy(self):
125
        if self.index.dtype == bool:
126
            return FixAtoms(mask=self.index.copy())
127
        else:
128
            return FixAtoms(indices=self.index.copy())
129

    
130
    def __repr__(self):
131
        if self.index.dtype == bool:
132
            return 'FixAtoms(mask=%s)' % ints2string(self.index.astype(int))
133
        return 'FixAtoms(indices=%s)' % ints2string(self.index)
134

    
135
    def repeat(self, m, n):
136
        i0 = 0
137
        l = len(self.index)
138
        natoms = 0
139
        if isinstance(m, int):
140
            m = (m, m, m)
141
        index_new = []
142
        for m2 in range(m[2]):
143
            for m1 in range(m[1]):
144
                for m0 in range(m[0]):
145
                    i1 = i0 + n
146
                    if self.index.dtype == bool:
147
                        index_new += self.index
148
                    else:
149
                        index_new += [i+natoms for i in self.index]
150
                    i0 = i1
151
                    natoms += n
152
        if self.index.dtype == bool:
153
            self.index = np.asarray(index_new, bool)
154
        else:
155
            self.index = np.asarray(index_new, int)
156
        return self
157

    
158
def ints2string(x, threshold=10):
159
    """Convert ndarray of ints to string."""
160
    if len(x) <= threshold:
161
        return str(x.tolist())
162
    return str(x[:threshold].tolist())[:-1] + ', ...]'
163

    
164
class FixBondLengths(FixConstraint):
165
    def __init__(self, pairs, iterations=10):
166
        self.constraints = [FixBondLength(a1, a2)
167
                            for a1, a2 in pairs]
168
        self.iterations = iterations
169

    
170
    def adjust_positions(self, old, new):
171
        for i in range(self.iterations):
172
            for constraint in self.constraints:
173
                constraint.adjust_positions(old, new)
174

    
175
    def adjust_forces(self, positions, forces):
176
        for i in range(self.iterations):
177
            for constraint in self.constraints:
178
                constraint.adjust_forces(positions, forces)
179

    
180
    def copy(self):
181
        return FixBondLengths([constraint.indices for constraint in self.constraints])
182

    
183
class FixBondLength(FixConstraint):
184
    """Constraint object for fixing a bond length."""
185
    def __init__(self, a1, a2):
186
        """Fix distance between atoms with indices a1 and a2."""
187
        self.indices = [a1, a2]
188

    
189
    def adjust_positions(self, old, new):
190
        p1, p2 = old[self.indices]
191
        d = p2 - p1
192
        p = sqrt(np.dot(d, d))
193
        q1, q2 = new[self.indices]
194
        d = q2 - q1
195
        q = sqrt(np.dot(d, d))
196
        d *= 0.5 * (p - q) / q
197
        new[self.indices] = (q1 - d, q2 + d)
198

    
199
    def adjust_forces(self, positions, forces):
200
        d = np.subtract.reduce(positions[self.indices])
201
        d2 = np.dot(d, d)
202
        d *= 0.5 * np.dot(np.subtract.reduce(forces[self.indices]), d) / d2
203
        forces[self.indices] += (-d, d)
204

    
205
    def index_shuffle(self, ind):
206
        'Shuffle the indices of the two atoms in this constraint'
207
        newa = [-1, -1] # Signal error
208
        for new, old in slice2enlist(ind):
209
            for i, a in enumerate(self.indices):
210
                if old == a:
211
                    newa[i] = new
212
        if newa[0] == -1 or newa[1] == -1:
213
            raise IndexError('Constraint not part of slice')
214
        self.indices = newa
215

    
216
    def copy(self):
217
        return FixBondLength(*self.indices)
218

    
219
    def __repr__(self):
220
        return 'FixBondLength(%d, %d)' % tuple(self.indices)
221

    
222
class FixedMode(FixConstraint):
223
    """Constrain atoms to move along directions orthogonal to
224
    a given mode only."""
225

    
226
    def __init__(self,indices,mode):
227
        if indices is None:
228
            raise ValueError('Use "indices".')
229
        if indices is not None:
230
            self.index = np.asarray(indices, int)
231
        self.mode = (np.asarray(mode) / np.sqrt((mode **2).sum())).reshape(-1)
232

    
233
    def adjust_positions(self, oldpositions, newpositions):
234
        newpositions = newpositions.ravel()
235
        oldpositions = oldpositions.ravel()
236
        step = newpositions - oldpositions
237
        newpositions -= self.mode * np.dot(step, self.mode)
238
        newpositions = newpositions.reshape(-1,3)
239
        oldpositions = oldpositions.reshape(-1,3)
240

    
241
    def adjust_forces(self, positions, forces):
242
        forces = forces.ravel()
243
        forces -= self.mode * np.dot(forces, self.mode)
244
        forces = forces.reshape(-1 ,3)
245

    
246
    def copy(self):
247
        return FixedMode(self.index.copy(), self.mode)
248

    
249
    def __repr__(self):
250
        return 'FixedMode(%d, %s)' % (ints2string(self.index), self.mode.tolist())
251

    
252
class FixedPlane(FixConstraintSingle):
253
    """Constrain an atom *a* to move in a given plane only.
254

255
    The plane is defined by its normal: *direction*."""
256

    
257
    def __init__(self, a, direction):
258
        self.a = a
259
        self.dir = np.asarray(direction) / sqrt(np.dot(direction, direction))
260

    
261
    def adjust_positions(self, oldpositions, newpositions):
262
        step = newpositions[self.a] - oldpositions[self.a]
263
        newpositions[self.a] -= self.dir * np.dot(step, self.dir)
264

    
265
    def adjust_forces(self, positions, forces):
266
        forces[self.a] -= self.dir * np.dot(forces[self.a], self.dir)
267

    
268
    def copy(self):
269
        return FixedPlane(self.a, self.dir)
270

    
271
    def __repr__(self):
272
        return 'FixedPlane(%d, %s)' % (self.a, self.dir.tolist())
273

    
274

    
275
class FixedLine(FixConstraintSingle):
276
    """Constrain an atom *a* to move on a given line only.
277

278
    The line is defined by its *direction*."""
279

    
280
    def __init__(self, a, direction):
281
        self.a = a
282
        self.dir = np.asarray(direction) / sqrt(np.dot(direction, direction))
283

    
284
    def adjust_positions(self, oldpositions, newpositions):
285
        step = newpositions[self.a] - oldpositions[self.a]
286
        x = np.dot(step, self.dir)
287
        newpositions[self.a] = oldpositions[self.a] + x * self.dir
288

    
289
    def adjust_forces(self, positions, forces):
290
        forces[self.a] = self.dir * np.dot(forces[self.a], self.dir)
291

    
292
    def copy(self):
293
        return FixedLine(self.a, self.dir)
294

    
295
    def __repr__(self):
296
        return 'FixedLine(%d, %s)' % (self.a, self.dir.tolist())
297

    
298
class FixCartesian(FixConstraintSingle):
299
    "Fix an atom in the directions of the cartesian coordinates."
300
    def __init__(self, a, mask=[1,1,1]):
301
        self.a=a
302
        self.mask = -(np.array(mask)-1)
303

    
304
    def adjust_positions(self, old, new):
305
        step = new - old
306
        step[self.a] *= self.mask
307
        new = old + step
308

    
309
    def adjust_forces(self, positions, forces):
310
        forces[self.a] *= self.mask
311

    
312
    def copy(self):
313
        return FixCartesian(self.a, self.mask)
314

    
315
    def __repr__(self):
316
        return 'FixCartesian(indice=%s mask=%s)' % (self.a, self.mask)
317

    
318
class fix_cartesian(FixCartesian):
319
    "Backwards compatibility for FixCartesian."
320
    def __init__(self, a, mask=[1,1,1]):
321
        import warnings
322
        super(fix_cartesian, self).__init__(a, mask)
323
        warnings.warn('fix_cartesian is deprecated. Please use FixCartesian' \
324
                      ' instead.', DeprecationWarning, stacklevel=2)
325

    
326
class FixScaled(FixConstraintSingle):
327
    "Fix an atom in the directions of the unit vectors."
328
    def __init__(self, cell, a, mask=[1,1,1]):
329
        self.cell = cell
330
        self.a = a
331
        self.mask = np.array(mask)
332

    
333
    def adjust_positions(self, old, new):
334
        scaled_old = np.linalg.solve(self.cell.T, old.T).T
335
        scaled_new = np.linalg.solve(self.cell.T, new.T).T
336
        for n in range(3):
337
            if self.mask[n]:
338
                scaled_new[self.a, n] = scaled_old[self.a, n]
339
        new[self.a] = np.dot(scaled_new, self.cell)[self.a]
340

    
341
    def adjust_forces(self, positions, forces):
342
        scaled_forces = np.linalg.solve(self.cell.T, forces.T).T
343
        scaled_forces[self.a] *= -(self.mask-1)
344
        forces[self.a] = np.dot(scaled_forces, self.cell)[self.a]
345

    
346
    def copy(self):
347
        return FixScaled(self.cell ,self.a, self.mask)
348

    
349
    def __repr__(self):
350
        return 'FixScaled(indice=%s mask=%s)' % (self.a, self.mask)
351

    
352
class fix_scaled(FixScaled):
353
    "Backwards compatibility for FixScaled."
354
    def __init__(self, cell, a, mask=[1,1,1]):
355
        import warnings
356
        super(fix_scaled, self).__init__(cell, a, mask)
357
        warnings.warn('fix_scaled is deprecated. Please use FixScaled ' \
358
                      'instead.', DeprecationWarning, stacklevel=2)
359

    
360
class Filter:
361
    """Subset filter class."""
362
    def __init__(self, atoms, indices=None, mask=None):
363
        """Filter atoms.
364

365
        This filter can be used to hide degrees of freedom in an Atoms
366
        object.
367

368
        Parameters
369
        ----------
370
        indices : list of int
371
           Indices for those atoms that should remain visible.
372
        mask : list of bool
373
           One boolean per atom indicating if the atom should remain
374
           visible or not.
375
        """
376

    
377
        self.atoms = atoms
378
        self.constraints = []
379

    
380
        if indices is None and mask is None:
381
            raise ValuError('Use "indices" or "mask".')
382
        if indices is not None and mask is not None:
383
            raise ValuError('Use only one of "indices" and "mask".')
384

    
385
        if mask is not None:
386
            self.index = np.asarray(mask, bool)
387
            self.n = self.index.sum()
388
        else:
389
            self.index = np.asarray(indices, int)
390
            self.n = len(self.index)
391

    
392
    def get_cell(self):
393
        """Returns the computational cell.
394

395
        The computational cell is the same as for the original system.
396
        """
397
        return self.atoms.get_cell()
398

    
399
    def get_pbc(self):
400
        """Returns the periodic boundary conditions.
401

402
        The boundary conditions are the same as for the original system.
403
        """
404
        return self.atoms.get_pbc()
405

    
406
    def get_positions(self):
407
        "Return the positions of the visible atoms."
408
        return self.atoms.get_positions()[self.index]
409

    
410
    def set_positions(self, positions):
411
        "Set the positions of the visible atoms."
412
        pos = self.atoms.get_positions()
413
        pos[self.index] = positions
414
        self.atoms.set_positions(pos)
415

    
416
    def get_momenta(self):
417
        "Return the momenta of the visible atoms."
418
        return self.atoms.get_momenta()[self.index]
419

    
420
    def set_momenta(self, momenta):
421
        "Set the momenta of the visible atoms."
422
        mom = self.atoms.get_momenta()
423
        mom[self.index] = momenta
424
        self.atoms.set_momenta(mom)
425

    
426
    def get_atomic_numbers(self):
427
        "Return the atomic numbers of the visible atoms."
428
        return self.atoms.get_atomic_numbers()[self.index]
429

    
430
    def set_atomic_numbers(self, atomic_numbers):
431
        "Set the atomic numbers of the visible atoms."
432
        z = self.atoms.get_atomic_numbers()
433
        z[self.index] = atomic_numbers
434
        self.atoms.set_atomic_numbers(z)
435

    
436
    def get_tags(self):
437
        "Return the tags of the visible atoms."
438
        return self.atoms.get_tags()[self.index]
439

    
440
    def set_tags(self, tags):
441
        "Set the tags of the visible atoms."
442
        tg = self.atoms.get_tags()
443
        tg[self.index] = tags
444
        self.atoms.set_tags(tg)
445

    
446
    def get_forces(self, *args, **kwargs):
447
        return self.atoms.get_forces(*args, **kwargs)[self.index]
448

    
449
    def get_stress(self):
450
        return self.atoms.get_stress()
451

    
452
    def get_stresses(self):
453
        return self.atoms.get_stresses()[self.index]
454

    
455
    def get_masses(self):
456
        return self.atoms.get_masses()[self.index]
457

    
458
    def get_potential_energy(self):
459
        """Calculate potential energy.
460

461
        Returns the potential energy of the full system.
462
        """
463
        return self.atoms.get_potential_energy()
464

    
465
    def get_calculator(self):
466
        """Returns the calculator.
467

468
        WARNING: The calculator is unaware of this filter, and sees a
469
        different number of atoms.
470
        """
471
        return self.atoms.get_calculator()
472

    
473
    def has(self, name):
474
        """Check for existance of array."""
475
        return self.atoms.has(name)
476

    
477
    def __len__(self):
478
        "Return the number of movable atoms."
479
        return self.n
480

    
481
    def __getitem__(self, i):
482
        "Return an atom."
483
        return self.atoms[self.index[i]]
484

    
485

    
486
class StrainFilter:
487
    """Modify the supercell while keeping the scaled positions fixed.
488

489
    Presents the strain of the supercell as the generalized positions,
490
    and the global stress tensor (times the volume) as the generalized
491
    force.
492

493
    This filter can be used to relax the unit cell until the stress is
494
    zero.  If MDMin is used for this, the timestep (dt) to be used
495
    depends on the system size. 0.01/x where x is a typical dimension
496
    seems like a good choice.
497

498
    The stress and strain are presented as 6-vectors, the order of the
499
    components follow the standard engingeering practice: xx, yy, zz,
500
    yz, xz, xy.
501

502
    """
503
    def __init__(self, atoms, mask=None):
504
        """Create a filter applying a homogeneous strain to a list of atoms.
505

506
        The first argument, atoms, is the atoms object.
507

508
        The optional second argument, mask, is a list of six booleans,
509
        indicating which of the six independent components of the
510
        strain that are allowed to become non-zero.  It defaults to
511
        [1,1,1,1,1,1].
512

513
        """
514

    
515
        self.atoms = atoms
516
        self.strain = np.zeros(6)
517

    
518
        if mask is None:
519
            self.mask = np.ones(6)
520
        else:
521
            self.mask = np.array(mask)
522

    
523
        self.origcell = atoms.get_cell()
524

    
525
    def get_positions(self):
526
        return self.strain.reshape((2, 3))
527

    
528
    def set_positions(self, new):
529
        new = new.ravel() * self.mask
530
        eps = np.array([[1.0 + new[0], 0.5 * new[5], 0.5 * new[4]],
531
                        [0.5 * new[5], 1.0 + new[1], 0.5 * new[3]],
532
                        [0.5 * new[4], 0.5 * new[3], 1.0 + new[2]]])
533

    
534
        self.atoms.set_cell(np.dot(self.origcell, eps), scale_atoms=True)
535
        self.strain[:] = new
536

    
537
    def get_forces(self):
538
        stress = self.atoms.get_stress()
539
        return -self.atoms.get_volume() * (stress * self.mask).reshape((2, 3))
540

    
541
    def get_potential_energy(self):
542
        return self.atoms.get_potential_energy()
543

    
544
    def has(self, x):
545
        return self.atoms.has(x)
546

    
547
    def __len__(self):
548
        return 2
549

    
550
class UnitCellFilter:
551
    """Modify the supercell and the atom positions. """
552
    def __init__(self, atoms, mask=None):
553
        """Create a filter that returns the atomic forces and unit
554
        cell stresses together, so they can simultaneously be
555
        minimized.
556

557
        The first argument, atoms, is the atoms object.
558

559
        The optional second argument, mask, is a list of booleans,
560
        indicating which of the six independent
561
        components of the strain are relaxed.
562
        1, True = relax to zero
563
        0, False = fixed, ignore this component
564

565
        use atom Constraints, e.g. FixAtoms, to control relaxation of
566
        the atoms.
567

568
        #this should be equivalent to the StrainFilter
569
        >>> atoms = Atoms(...)
570
        >>> atoms.set_constraint(FixAtoms(mask=[True for atom in atoms]))
571
        >>> ucf = UCFilter(atoms)
572

573
        You should not attach this UCFilter object to a
574
        trajectory. Instead, create a trajectory for the atoms, and
575
        attach it to an optimizer like this:
576

577
        >>> atoms = Atoms(...)
578
        >>> ucf = UCFilter(atoms)
579
        >>> qn = QuasiNewton(ucf)
580
        >>> traj = PickleTrajectory('TiO2.traj','w',atoms)
581
        >>> qn.attach(traj)
582
        >>> qn.run(fmax=0.05)
583

584
        Helpful conversion table
585
        ========================
586
        0.05 eV/A^3   = 8 GPA
587
        0.003 eV/A^3  = 0.48 GPa
588
        0.0006 eV/A^3 = 0.096 GPa
589
        0.0003 eV/A^3 = 0.048 GPa
590
        0.0001 eV/A^3 = 0.02 GPa
591
        """
592

    
593
        self.atoms = atoms
594
        self.strain = np.zeros(6)
595

    
596
        if mask is None:
597
            self.mask = np.ones(6)
598
        else:
599
            self.mask = np.array(mask)
600

    
601
        self.origcell = atoms.get_cell()
602

    
603
    def get_positions(self):
604
        '''
605
        this returns an array with shape (natoms+2,3).
606

607
        the first natoms rows are the positions of the atoms, the last
608
        two rows are the strains associated with the unit cell
609
        '''
610

    
611
        atom_positions = self.atoms.get_positions()
612
        strains = self.strain.reshape((2, 3))
613

    
614
        natoms = len(self.atoms)
615
        all_pos = np.zeros((natoms+2,3),np.float)
616
        all_pos[0:natoms,:] = atom_positions
617
        all_pos[natoms:,:] = strains
618

    
619
        return all_pos
620

    
621
    def set_positions(self, new):
622
        '''
623
        new is an array with shape (natoms+2,3).
624

625
        the first natoms rows are the positions of the atoms, the last
626
        two rows are the strains used to change the cell shape.
627

628
        The atom positions are set first, then the unit cell is
629
        changed keeping the atoms in their scaled positions.
630
        '''
631

    
632
        natoms = len(self.atoms)
633

    
634
        atom_positions = new[0:natoms,:]
635
        self.atoms.set_positions(atom_positions)
636

    
637
        new = new[natoms:,:] #this is only the strains
638
        new = new.ravel() * self.mask
639
        eps = np.array([[1.0 + new[0], 0.5 * new[5], 0.5 * new[4]],
640
                        [0.5 * new[5], 1.0 + new[1], 0.5 * new[3]],
641
                        [0.5 * new[4], 0.5 * new[3], 1.0 + new[2]]])
642

    
643
        self.atoms.set_cell(np.dot(self.origcell, eps), scale_atoms=True)
644
        self.strain[:] = new
645

    
646
    def get_forces(self,apply_constraint=False):
647
        '''
648
        returns an array with shape (natoms+2,3) of the atomic forces
649
        and unit cell stresses.
650

651
        the first natoms rows are the forces on the atoms, the last
652
        two rows are the stresses on the unit cell, which have been
653
        reshaped to look like "atomic forces". i.e.,
654

655
        f[-2] = -vol*[sxx,syy,szz]*mask[0:3]
656
        f[-1] = -vol*[syz, sxz, sxy]*mask[3:]
657

658
        apply_constraint is an argument expected by ase
659
        '''
660

    
661
        stress = self.atoms.get_stress()
662
        atom_forces = self.atoms.get_forces()
663

    
664
        natoms = len(self.atoms)
665
        all_forces = np.zeros((natoms+2,3),np.float)
666
        all_forces[0:natoms,:] = atom_forces
667

    
668
        vol = self.atoms.get_volume()
669
        stress_forces = -vol * (stress * self.mask).reshape((2, 3))
670
        all_forces[natoms:,:] = stress_forces
671
        return all_forces
672

    
673
    def get_potential_energy(self):
674
        return self.atoms.get_potential_energy()
675

    
676
    def has(self, x):
677
        return self.atoms.has(x)
678

    
679
    def __len__(self):
680
        return (2 + len(self.atoms))