Skip to content

Commit 5711ab3

Browse files
author
jianrui geng
committed
Address socket i-PI review feedback
1 parent 28ea99f commit 5711ab3

17 files changed

Lines changed: 531 additions & 37 deletions

File tree

docs/advanced/input_files/input-main.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [suffix](#suffix)
1010
- [ntype](#ntype)
1111
- [calculation](#calculation)
12+
- [socket\_driver](#socket_driver)
1213
- [esolver\_type](#esolver_type)
1314
- [symmetry](#symmetry)
1415
- [symmetry\_prec](#symmetry_prec)
@@ -583,7 +584,6 @@
583584
- relax: perform structure relaxation calculations, the relax_nmax parameter depicts the maximal number of ionic iterations
584585
- cell-relax: perform cell relaxation calculations
585586
- md: perform molecular dynamics simulations
586-
- socket: run as a socket client for external drivers using the i-PI protocol
587587
- get_pchg: obtain partial (band-decomposed) charge densities (for LCAO basis only). See out_pchg for more information
588588
- get_wf: obtain real space wave functions (for LCAO basis only). See out_wfc_norm and out_wfc_re_im for more information
589589
- get_s: obtain the overlap matrix formed by localized orbitals (for LCAO basis with multiple k points). the file name is SR.csr with file format being the same as that generated by out_mat_hs2
@@ -593,6 +593,15 @@
593593
- test_neighbour: obtain information of neighboring atoms (for LCAO basis only), please specify a positive search_radius manually
594594
- **Default**: scf
595595

596+
### socket_driver
597+
598+
- **Type**: Boolean
599+
- **Availability**: *calculation==scf*
600+
- **Description**: If set to True, ABACUS keeps the calculation type as scf and receives atomic positions from an external driver through the i-PI socket protocol.
601+
602+
> Note: Use calculation = scf with socket_driver = True. Set ABACUS_SOCKET_ADDRESS to host:port or path:UNIX to choose the socket endpoint; if unset, localhost:31415 is used.
603+
- **Default**: False
604+
596605
### esolver_type
597606

598607
- **Type**: String
@@ -844,7 +853,7 @@
844853
- **Description**: Charge extrapolation method for MD, relaxation, and socket-driven calculations.
845854

846855
When set to default, ABACUS chooses second-order for md, first-order for
847-
relax/cell-relax/socket, and atomic for other calculations. Socket-driven
856+
relax/cell-relax and socket_driver calculations, and atomic for other calculations. Socket-driven
848857
molecular dynamics can explicitly set second-order if the external driver
849858
updates structures smoothly enough for second-order extrapolation.
850859
- **Default**: default

docs/advanced/interface/ase.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,64 @@ In the new implementation, we limit the range of functionalties supported to mai
103103

104104
Please read the examples in `interfaces/ASE_interface/examples/` for more details.
105105

106+
### Socket I/O with ASE
107+
108+
For socket-driven ASE workflows, use the `AbacusSocketIO` calculator. ASE runs the i-PI socket server, while ABACUS keeps `calculation=scf` and is launched with `socket_driver=1` as the client. The protocol is simple: ASE sends atomic positions and cell data to ABACUS; ABACUS evaluates one SCF step for that structure and returns energy, forces, and virial. See the [ASE socket I/O documentation](https://ase-lib.org/ase/calculators/socketio/socketio.html) and the i-PI reference paper, [Ceriotti et al., Comput. Phys. Commun. 185, 1019-1026 (2014)](https://doi.org/10.1016/j.cpc.2013.10.027), for the protocol background.
109+
110+
Build ABACUS as usual before using this interface. PW-only builds work with `basis_type=pw`; LCAO socket calculations require an LCAO-enabled executable. No extra socket library is required.
111+
112+
With CMake, choose the executable according to the basis:
113+
114+
```bash
115+
cmake -S . -B build-pw -DENABLE_MPI=ON -DENABLE_LCAO=OFF
116+
cmake --build build-pw --target abacus_pw_para -j
117+
118+
cmake -S . -B build-lcao -DENABLE_MPI=ON -DENABLE_LCAO=ON
119+
cmake --build build-lcao --target abacus_basic_para -j
120+
```
121+
122+
With the ABACUS toolchain workflow, build the normal ABACUS executable with LCAO support when `basis_type=lcao` is needed, then pass that executable to `AbacusProfile(command=...)`. The ASE interface can be installed from this repository with:
123+
124+
```bash
125+
cd interfaces/ASE_interface
126+
pip install .
127+
```
128+
129+
A minimal socket calculator setup is:
130+
131+
```python
132+
from ase.optimize import BFGS
133+
from abacuslite import AbacusProfile, AbacusSocketIO
134+
135+
aprof = AbacusProfile(
136+
command="mpirun -np 4 /path/to/abacus",
137+
pseudo_dir="/path/to/pseudopotentials",
138+
orbital_dir="/path/to/orbitals",
139+
omp_num_threads=1,
140+
)
141+
142+
abacus = AbacusSocketIO(
143+
profile=aprof,
144+
directory="socketio",
145+
unixsocket="abacus_si",
146+
pseudopotentials={"Si": "Si_ONCV_PBE-1.0.upf"},
147+
basissets={"Si": "Si_gga_8au_100Ry_2s2p1d.orb"},
148+
inp={"calculation": "scf", "basis_type": "lcao", "kspacing": 0.1},
149+
)
150+
151+
with abacus as calc:
152+
atoms.calc = calc
153+
BFGS(atoms).run(fmax=0.05)
154+
```
155+
156+
`AbacusSocketIO` sets `socket_driver=1` and `cal_force=1` automatically. The socket endpoint is selected by the calculator arguments and passed to ABACUS through `ABACUS_SOCKET_ADDRESS`, for example `localhost:31415` or `/tmp/ipi_abacus_si:UNIX`. Calling `atoms.get_potential_energy()` is supported, but the ABACUS client still computes forces because the i-PI `GETFORCE` exchange returns energy, forces, and virial as one response.
157+
158+
A socket calculator owns one ABACUS process initialized from one fixed `INPUT`/`STRU` setup. Reuse the same `AbacusSocketIO` instance only for position updates under the same electronic-structure settings and the same cell. Do not change `kpts`, `kspacing`, `nspin`, `basis_type`, `basissets`, pseudopotentials, species, atom count, cell, or other core `INPUT`/`STRU` parameters through an existing socket calculator; create a new `AbacusSocketIO` instance and a new ABACUS client process for those changes. `AbacusSocketIO` rejects cell changes before sending them to ABACUS, and the ABACUS socket driver also checks incoming POSDATA cells against the initial `STRU` cell and exits if they differ.
159+
160+
The i-PI protocol does not transmit element symbols. `AbacusSocketIO` therefore sorts the internal socket atoms with the same first-occurrence species grouping used when writing `STRU`, and maps returned forces back to the original ASE `Atoms` order. This avoids silent force/atom mismatches when structures are read from CIF, extxyz, POSCAR, or other formats whose atom order is not already grouped for ABACUS. Users should not manually reorder atoms for socket I/O; pass the physical ASE `Atoms` object directly to the calculator.
161+
162+
A complete fixed-cell validation and benchmark example is available in `interfaces/ASE_interface/examples/socketio.py`.
163+
106164
## SPAP Analysis
107165

108166
[SPAP](https://github.com/chuanxun/StructurePrototypeAnalysisPackage) (Structure Prototype Analysis Package) is written by Dr. Chuanxun Su to analyze symmetry and compare similarity of large amount of atomic structures. The coordination characterization function (CCF) is used to

docs/parameters.yaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ parameters:
2828
* relax: perform structure relaxation calculations, the relax_nmax parameter depicts the maximal number of ionic iterations
2929
* cell-relax: perform cell relaxation calculations
3030
* md: perform molecular dynamics simulations
31-
* socket: run as a socket client for external drivers using the i-PI protocol
3231
* get_pchg: obtain partial (band-decomposed) charge densities (for LCAO basis only). See out_pchg for more information
3332
* get_wf: obtain real space wave functions (for LCAO basis only). See out_wfc_norm and out_wfc_re_im for more information
3433
* get_s: obtain the overlap matrix formed by localized orbitals (for LCAO basis with multiple k points). the file name is SR.csr with file format being the same as that generated by out_mat_hs2
@@ -39,6 +38,16 @@ parameters:
3938
default_value: scf
4039
unit: ""
4140
availability: ""
41+
- name: socket_driver
42+
category: System variables
43+
type: Boolean
44+
description: |
45+
If set to True, ABACUS keeps the calculation type as scf and receives atomic positions from an external driver through the i-PI socket protocol.
46+
47+
[NOTE] Use calculation = scf with socket_driver = True. Set ABACUS_SOCKET_ADDRESS to host:port or path:UNIX to choose the socket endpoint; if unset, localhost:31415 is used.
48+
default_value: "False"
49+
unit: ""
50+
availability: calculation==scf
4251
- name: esolver_type
4352
category: System variables
4453
type: String
@@ -333,7 +342,7 @@ parameters:
333342
Charge extrapolation method for MD, relaxation, and socket-driven calculations.
334343
335344
When set to default, ABACUS chooses second-order for md, first-order for
336-
relax/cell-relax/socket, and atomic for other calculations. Socket-driven
345+
relax/cell-relax and socket_driver calculations, and atomic for other calculations. Socket-driven
337346
molecular dynamics can explicitly set second-order if the external driver
338347
updates structures smoothly enough for second-order extrapolation.
339348
default_value: default

interfaces/ASE_interface/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Please refer to the example scripts in the `examples` folder. Recommended learni
3232
7. **constraintmd.py** - Constrained molecular dynamics simulation
3333
8. **metadynamics.py** - Metadynamics simulation
3434
9. **neb.py** - Nudged Elastic Band (NEB) calculation
35+
10. **socketio.py** - ASE optimization with the AbacusSocketIO calculator running ABACUS as an i-PI socket client
3536

3637
More usage examples will be provided in future versions.
3738

interfaces/ASE_interface/abacuslite/core.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
GenericFileIOCalculator,
4646
read_stdout
4747
)
48+
from ase.calculators.socketio import SocketIOCalculator
4849
from ase.atoms import Atoms
4950
from ase.dft.kpoints import BandPath
5051
from ase.io import read
@@ -135,6 +136,21 @@ def get_calculator_command(self, inputfile) -> List[str]:
135136
# additional inputfile argument is not used.
136137
return []
137138

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+
138154
def version(self) -> str:
139155
'''get the abacus version information'''
140156
cmd_ = [*self._split_command, '--version']
@@ -443,6 +459,17 @@ def __init__(self,
443459
directory=directory,
444460
)
445461

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+
446473
@classmethod
447474
def restart(cls, profile=None, directory='.', **kwargs):
448475
'''instantiate one ABACUS calculator from an existing job directory,
@@ -557,11 +584,200 @@ def band_structure(self, efermi=None):
557584
from ase.spectrum.band_structure import get_band_structure
558585
return get_band_structure(calc=self, reference=efermi)
559586

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+
560745
class TestAbacusCalculator(unittest.TestCase):
561746

562747
here = Path(__file__).parent
563748
pporb = here.parent.parent.parent / 'tests' / 'PP_ORB'
564749

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+
565781
def test_calculator_results(self):
566782
from ase.build.bulk import bulk
567783
silicon = bulk('Si', crystalstructure='diamond', a=5.43)

0 commit comments

Comments
 (0)