|
45 | 45 | GenericFileIOCalculator, |
46 | 46 | read_stdout |
47 | 47 | ) |
| 48 | +from ase.calculators.socketio import SocketIOCalculator |
48 | 49 | from ase.atoms import Atoms |
49 | 50 | from ase.dft.kpoints import BandPath |
50 | 51 | from ase.io import read |
@@ -135,6 +136,21 @@ def get_calculator_command(self, inputfile) -> List[str]: |
135 | 136 | # additional inputfile argument is not used. |
136 | 137 | return [] |
137 | 138 |
|
| 139 | + def socketio_argv_inet(self, port: Optional[int] = None) -> List[str]: |
| 140 | + port = 31415 if port is None else port |
| 141 | + return [ |
| 142 | + 'env', |
| 143 | + f'ABACUS_SOCKET_ADDRESS=localhost:{port}', |
| 144 | + *self._split_command, |
| 145 | + ] |
| 146 | + |
| 147 | + def socketio_argv_unix(self, socket: str) -> List[str]: |
| 148 | + return [ |
| 149 | + 'env', |
| 150 | + f'ABACUS_SOCKET_ADDRESS=/tmp/ipi_{socket}:UNIX', |
| 151 | + *self._split_command, |
| 152 | + ] |
| 153 | + |
138 | 154 | def version(self) -> str: |
139 | 155 | '''get the abacus version information''' |
140 | 156 | cmd_ = [*self._split_command, '--version'] |
@@ -443,6 +459,17 @@ def __init__(self, |
443 | 459 | directory=directory, |
444 | 460 | ) |
445 | 461 |
|
| 462 | + def write_input(self, atoms, properties=None, system_changes=None): |
| 463 | + if properties is None: |
| 464 | + properties = self.template.implemented_properties |
| 465 | + self.template.write_input( |
| 466 | + profile=self.profile, |
| 467 | + directory=Path(self.directory), |
| 468 | + atoms=atoms, |
| 469 | + parameters=self.parameters, |
| 470 | + properties=properties, |
| 471 | + ) |
| 472 | + |
446 | 473 | @classmethod |
447 | 474 | def restart(cls, profile=None, directory='.', **kwargs): |
448 | 475 | '''instantiate one ABACUS calculator from an existing job directory, |
@@ -557,11 +584,200 @@ def band_structure(self, efermi=None): |
557 | 584 | from ase.spectrum.band_structure import get_band_structure |
558 | 585 | return get_band_structure(calc=self, reference=efermi) |
559 | 586 |
|
| 587 | +class AbacusSocketIO(SocketIOCalculator): |
| 588 | + """ASE socket I/O calculator that launches ABACUS as an i-PI client. |
| 589 | +
|
| 590 | + A socket calculator owns one ABACUS process with one fixed INPUT/STRU |
| 591 | + setup. The i-PI protocol can update positions, but electronic-structure |
| 592 | + parameters such as k-points, spin, basis, pseudopotentials, and species |
| 593 | + require a new calculator instance. Energy-only ASE calls are accepted, but |
| 594 | + ABACUS still computes forces because i-PI GETFORCE returns energy, forces, |
| 595 | + and virial together. |
| 596 | + """ |
| 597 | + |
| 598 | + def __init__(self, |
| 599 | + profile=None, |
| 600 | + directory='.', |
| 601 | + port=None, |
| 602 | + unixsocket=None, |
| 603 | + timeout=None, |
| 604 | + log=None, |
| 605 | + **kwargs): |
| 606 | + inp = self._socket_inp(kwargs.pop('inp', {})) |
| 607 | + self.abacus = Abacus( |
| 608 | + profile=profile, |
| 609 | + directory=directory, |
| 610 | + inp=inp, |
| 611 | + **kwargs, |
| 612 | + ) |
| 613 | + self._reference_cell = None |
| 614 | + super().__init__( |
| 615 | + port=port, |
| 616 | + unixsocket=unixsocket, |
| 617 | + timeout=timeout, |
| 618 | + log=log, |
| 619 | + launch_client=self._launch_client, |
| 620 | + ) |
| 621 | + |
| 622 | + def calculate(self, atoms=None, properties=['energy'], system_changes=None): |
| 623 | + from ase.calculators.calculator import ( |
| 624 | + PropertyNotImplementedError, |
| 625 | + all_changes, |
| 626 | + ) |
| 627 | + from ase.stress import full_3x3_to_voigt_6_stress |
| 628 | + |
| 629 | + if system_changes is None: |
| 630 | + system_changes = all_changes |
| 631 | + if atoms is None: |
| 632 | + atoms = self.atoms |
| 633 | + if atoms is None: |
| 634 | + raise ValueError('AbacusSocketIO.calculate requires atoms') |
| 635 | + |
| 636 | + bad = [change for change in system_changes |
| 637 | + if change not in self.supported_changes] |
| 638 | + if self.atoms is not None and any(bad): |
| 639 | + raise PropertyNotImplementedError( |
| 640 | + 'Cannot change {} through IPI protocol. ' |
| 641 | + 'Please create new socket calculator.' |
| 642 | + .format(bad if len(bad) > 1 else bad[0])) |
| 643 | + |
| 644 | + self._check_fixed_cell(atoms) |
| 645 | + order = self._socket_sort_indices(atoms) |
| 646 | + socket_atoms = atoms[order] |
| 647 | + self.atoms = atoms.copy() |
| 648 | + |
| 649 | + if self.server is None: |
| 650 | + self.server = self.launch_server() |
| 651 | + proc = self.launch_client(socket_atoms, properties, |
| 652 | + port=self._port, |
| 653 | + unixsocket=self._unixsocket) |
| 654 | + self.server.proc = proc |
| 655 | + |
| 656 | + results = self.server.calculate(socket_atoms) |
| 657 | + results['free_energy'] = results['energy'] |
| 658 | + virial = results.pop('virial') |
| 659 | + if self.atoms.cell.rank == 3 and any(self.atoms.pbc): |
| 660 | + vol = atoms.get_volume() |
| 661 | + results['stress'] = -full_3x3_to_voigt_6_stress(virial) / vol |
| 662 | + if 'forces' in results: |
| 663 | + results['forces'] = self._forces_to_input_order( |
| 664 | + results['forces'], order) |
| 665 | + self.results.update(results) |
| 666 | + |
| 667 | + def _check_fixed_cell(self, atoms): |
| 668 | + from ase.calculators.calculator import PropertyNotImplementedError |
| 669 | + |
| 670 | + cell = atoms.cell.array.copy() |
| 671 | + if self._reference_cell is None: |
| 672 | + self._reference_cell = cell |
| 673 | + return |
| 674 | + max_delta = np.max(np.abs(cell - self._reference_cell)) |
| 675 | + if max_delta > 1.0e-10: |
| 676 | + raise PropertyNotImplementedError( |
| 677 | + 'AbacusSocketIO is fixed-cell only; create a new socket ' |
| 678 | + 'calculator for a changed cell, or use the normal Abacus ' |
| 679 | + 'FileIO calculator for variable-cell workflows.' |
| 680 | + ) |
| 681 | + |
| 682 | + def set(self, **kwargs): |
| 683 | + if kwargs: |
| 684 | + raise ValueError( |
| 685 | + 'AbacusSocketIO input parameters are fixed after construction; ' |
| 686 | + 'create a new AbacusSocketIO calculator to change k-points, ' |
| 687 | + 'spin, basis, pseudopotentials, species, or other INPUT/STRU ' |
| 688 | + 'settings.' |
| 689 | + ) |
| 690 | + return super().set(**kwargs) |
| 691 | + |
| 692 | + def _launch_client(self, atoms, properties=None, port=None, unixsocket=None): |
| 693 | + from subprocess import Popen |
| 694 | + |
| 695 | + if properties is None: |
| 696 | + properties = self.abacus.template.implemented_properties |
| 697 | + |
| 698 | + directory = Path(self.abacus.directory) |
| 699 | + directory.mkdir(exist_ok=True, parents=True) |
| 700 | + |
| 701 | + if hasattr(self.abacus, 'write_inputfiles'): |
| 702 | + self.abacus.write_inputfiles(atoms, properties) |
| 703 | + else: |
| 704 | + self.abacus.write_input(atoms, properties=properties) |
| 705 | + |
| 706 | + if unixsocket is not None: |
| 707 | + argv = self.abacus.profile.socketio_argv_unix(socket=unixsocket) |
| 708 | + else: |
| 709 | + argv = self.abacus.profile.socketio_argv_inet(port=port) |
| 710 | + |
| 711 | + stdout = open(directory / self.abacus.template.outputname, 'w') |
| 712 | + stderr = open(directory / self.abacus.template.errorname, 'w') |
| 713 | + try: |
| 714 | + return Popen(argv, cwd=directory, env=os.environ, |
| 715 | + stdout=stdout, stderr=stderr) |
| 716 | + finally: |
| 717 | + stdout.close() |
| 718 | + stderr.close() |
| 719 | + |
| 720 | + @staticmethod |
| 721 | + def _socket_inp(inp): |
| 722 | + inp = dict(inp) |
| 723 | + calculation = inp.get('calculation', 'scf') |
| 724 | + if calculation != 'scf': |
| 725 | + raise ValueError('ABACUS socket I/O requires calculation="scf"') |
| 726 | + inp.update({ |
| 727 | + 'calculation': 'scf', |
| 728 | + 'socket_driver': 1, |
| 729 | + 'cal_force': 1, |
| 730 | + }) |
| 731 | + return inp |
| 732 | + |
| 733 | + @staticmethod |
| 734 | + def _socket_sort_indices(atoms): |
| 735 | + return species_group_indices(atoms.get_chemical_symbols()) |
| 736 | + |
| 737 | + @staticmethod |
| 738 | + def _forces_to_input_order(forces, order): |
| 739 | + reordered = np.empty_like(forces) |
| 740 | + for sorted_index, original_index in enumerate(order): |
| 741 | + reordered[original_index] = forces[sorted_index] |
| 742 | + return reordered |
| 743 | + |
| 744 | + |
560 | 745 | class TestAbacusCalculator(unittest.TestCase): |
561 | 746 |
|
562 | 747 | here = Path(__file__).parent |
563 | 748 | pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' |
564 | 749 |
|
| 750 | + def test_socketio_species_order_mapping(self): |
| 751 | + atoms = Atoms(symbols=['Si', 'O', 'C', 'Si', 'O', 'C']) |
| 752 | + order = AbacusSocketIO._socket_sort_indices(atoms) |
| 753 | + self.assertEqual(order, [0, 3, 1, 4, 2, 5]) |
| 754 | + |
| 755 | + socket_forces = np.arange(18).reshape(6, 3) |
| 756 | + input_forces = AbacusSocketIO._forces_to_input_order( |
| 757 | + socket_forces, order) |
| 758 | + |
| 759 | + expected = np.empty_like(socket_forces) |
| 760 | + for sorted_index, original_index in enumerate(order): |
| 761 | + expected[original_index] = socket_forces[sorted_index] |
| 762 | + np.testing.assert_array_equal(input_forces, expected) |
| 763 | + |
| 764 | + def test_socketio_rejects_parameter_changes(self): |
| 765 | + calc = object.__new__(AbacusSocketIO) |
| 766 | + with self.assertRaisesRegex(ValueError, 'fixed after construction'): |
| 767 | + calc.set(kpts={'mode': 'mp-sampling', 'nk': [2, 2, 2]}) |
| 768 | + |
| 769 | + def test_socketio_rejects_cell_changes(self): |
| 770 | + from ase.calculators.calculator import PropertyNotImplementedError |
| 771 | + |
| 772 | + calc = object.__new__(AbacusSocketIO) |
| 773 | + calc.atoms = Atoms('Si', cell=[5.0, 5.0, 5.0], pbc=True) |
| 774 | + calc._reference_cell = calc.atoms.cell.array.copy() |
| 775 | + |
| 776 | + changed = calc.atoms.copy() |
| 777 | + changed.cell[0, 0] = 5.1 |
| 778 | + with self.assertRaisesRegex(PropertyNotImplementedError, 'fixed-cell'): |
| 779 | + calc._check_fixed_cell(changed) |
| 780 | + |
565 | 781 | def test_calculator_results(self): |
566 | 782 | from ase.build.bulk import bulk |
567 | 783 | silicon = bulk('Si', crystalstructure='diamond', a=5.43) |
|
0 commit comments