diff --git a/phono3py/api_phono3py.py b/phono3py/api_phono3py.py index 7138b90a..4dc2bfbc 100644 --- a/phono3py/api_phono3py.py +++ b/phono3py/api_phono3py.py @@ -1225,11 +1225,11 @@ def run_phonon_solver(self, grid_points=None): def generate_displacements( self, - distance: float = 0.03, + distance: Optional[float] = None, cutoff_pair_distance: Optional[float] = None, is_plusminus: Union[bool, str] = "auto", is_diagonal: bool = True, - number_of_snapshots: Optional[int] = None, + number_of_snapshots: Optional[Union[int, Literal["auto"]]] = None, random_seed: Optional[int] = None, max_distance: Optional[float] = None, ): @@ -1269,10 +1269,11 @@ def generate_displacements( Parameters ---------- distance : float, optional - Constant displacement Euclidean distance. Default is 0.03. For - random direction and random distance displacements generation, this - value is also used as `min_distance`, is used to replace generated - random distances smaller than this value by this value. + Constant displacement Euclidean distance. Default is None, which + gives 0.03. For random direction and random distance displacements + generation, this value is also used as `min_distance`, is used to + replace generated random distances smaller than this value by this + value. cutoff_pair_distance : float, optional This is used as a cutoff Euclidean distance to determine if each pair of displacements is considered to calculate fc3 or not. Default @@ -1288,13 +1289,14 @@ def generate_displacements( vectors of the supercell. With True, direction not along the basis vectors can be chosen when the number of the displacements may be reduced. - number_of_snapshots : int or None, optional + number_of_snapshots : int, "auto", or None, optional Number of snapshots of supercells with random displacements. Random - displacements are generated displacing all atoms in random - directions with a fixed displacement distance specified by - 'distance' parameter, i.e., all atoms in supercell are displaced - with the same displacement distance in direct space. Default is - None. + displacements are generated by shifting all atoms in random + directions by a fixed distance specified by the `distance` + parameter. In other words, all atoms in the supercell are displaced + by the same distance in direct space. When “auto”, the minimum + required number of snapshots is estimated using symfc and then + doubled. The default is None. random_seed : int or None, optional Random seed for random displacements generation. Default is None. max_distance : float or None, optional @@ -1303,11 +1305,29 @@ def generate_displacements( displacements generation, this value is used as `max_distance`. """ - if number_of_snapshots is not None and number_of_snapshots > 0: + if distance is None: + _distance = 0.03 + else: + _distance = distance + + if number_of_snapshots is not None and ( + number_of_snapshots == "auto" or number_of_snapshots > 0 + ): + if number_of_snapshots == "auto": + from phonopy.interface.symfc import SymfcFCSolver + + _number_of_snapshots = ( + SymfcFCSolver( + self._supercell, self._symmetry + ).estimate_numbers_of_supercells(orders=[3])[3] + * 2 + ) + else: + _number_of_snapshots = number_of_snapshots self._dataset = self._generate_random_displacements( - number_of_snapshots, + _number_of_snapshots, len(self._supercell), - distance=distance, + distance=_distance, is_plusminus=is_plusminus is True, random_seed=random_seed, max_distance=max_distance, @@ -1321,7 +1341,7 @@ def generate_displacements( ) self._dataset = direction_to_displacement( direction_dataset, - distance, + _distance, self._supercell, cutoff_distance=cutoff_pair_distance, ) @@ -1329,15 +1349,15 @@ def generate_displacements( if self._phonon_supercell_matrix is not None and self._phonon_dataset is None: self.generate_fc2_displacements( - distance=distance, is_plusminus=is_plusminus, is_diagonal=False + distance=_distance, is_plusminus=is_plusminus, is_diagonal=False ) def generate_fc2_displacements( self, - distance: float = 0.03, + distance: Optional[float] = None, is_plusminus: str = "auto", is_diagonal: bool = False, - number_of_snapshots: Optional[int] = None, + number_of_snapshots: Optional[Union[int, Literal["auto"]]] = None, random_seed: Optional[int] = None, max_distance: Optional[float] = None, ): @@ -1357,10 +1377,11 @@ def generate_fc2_displacements( Parameters ---------- distance : float, optional - Constant displacement Euclidean distance. Default is 0.03. For - random direction and random distance displacements generation, this - value is also used as `min_distance`, is used to replace generated - random distances smaller than this value by this value. + Constant displacement Euclidean distance. Default is None, which + gives 0.03. For random direction and random distance displacements + generation, this value is also used as `min_distance`, is used to + replace generated random distances smaller than this value by this + value. is_plusminus : True, False, or 'auto', optional With True, atomis are displaced in both positive and negative directions. With False, only one direction. With 'auto', mostly @@ -1372,13 +1393,14 @@ def generate_fc2_displacements( the supercell. With True, direction not along the basis vectors can be chosen when the number of the displacements may be reduced. Default is False. - number_of_snapshots : int or None, optional + number_of_snapshots : int, "auto", or None, optional Number of snapshots of supercells with random displacements. Random - displacements are generated displacing all atoms in random - directions with a fixed displacement distance specified by - 'distance' parameter, i.e., all atoms in supercell are displaced - with the same displacement distance in direct space. Default is - None. + displacements are generated by shifting all atoms in random + directions by a fixed distance specified by the `distance` + parameter. In other words, all atoms in the supercell are displaced + by the same distance in direct space. When “auto”, the minimum + required number of snapshots is estimated using symfc and then + doubled. The default is None. random_seed : int or None, optional Random seed for random displacements generation. Default is None. max_distance : float or None, optional @@ -1387,11 +1409,30 @@ def generate_fc2_displacements( displacements generation, this value is used as `max_distance`. """ - if number_of_snapshots is not None and number_of_snapshots > 0: + if distance is None: + _distance = 0.03 + else: + _distance = distance + + if number_of_snapshots is not None and ( + number_of_snapshots == "auto" or number_of_snapshots > 0 + ): + if number_of_snapshots == "auto": + from phonopy.interface.symfc import SymfcFCSolver + + _number_of_snapshots = ( + SymfcFCSolver( + self._supercell, self._symmetry + ).estimate_numbers_of_supercells(orders=[2])[2] + * 2 + ) + else: + _number_of_snapshots = number_of_snapshots + self._phonon_dataset = self._generate_random_displacements( - number_of_snapshots, + _number_of_snapshots, len(self._phonon_supercell), - distance=distance, + distance=_distance, is_plusminus=is_plusminus is True, random_seed=random_seed, max_distance=max_distance, @@ -1403,7 +1444,7 @@ def generate_fc2_displacements( is_diagonal=is_diagonal, ) self._phonon_dataset = directions_to_displacement_dataset( - phonon_displacement_directions, distance, self._phonon_supercell + phonon_displacement_directions, _distance, self._phonon_supercell ) self._phonon_supercells_with_displacements = None diff --git a/phono3py/cui/create_supercells.py b/phono3py/cui/create_supercells.py index bb0b03b8..3513caac 100644 --- a/phono3py/cui/create_supercells.py +++ b/phono3py/cui/create_supercells.py @@ -34,6 +34,7 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import numpy as np from phonopy.interface.calculator import write_supercells_with_displacements from phonopy.structure.cells import print_cell @@ -83,10 +84,13 @@ def create_phono3py_supercells( random_seed=settings.random_seed, ) - if settings.random_displacements_fc2: + if ( + settings.random_displacements_fc2 + or settings.phonon_supercell_matrix is not None + ): phono3py.generate_fc2_displacements( distance=distance, - is_plusminus=settings.is_plusminus_displacement, + is_plusminus=settings.is_plusminus_displacement_fc2, number_of_snapshots=settings.random_displacements_fc2, random_seed=settings.random_seed, ) @@ -156,4 +160,21 @@ def create_phono3py_supercells( if log_level: print("Number of displacements for special fc2: %d" % num_disps) + if log_level: + identity = np.eye(3, dtype=int) + n_pure_trans = sum( + [ + (r == identity).all() + for r in phono3py.symmetry.symmetry_operations["rotations"] + ] + ) + + if len(phono3py.supercell) // len(phono3py.primitive) != n_pure_trans: + print("*" * 72) + print( + "Note: " + 'A better primitive cell can be chosen by using "--pa auto" option.' + ) + print("*" * 72) + return phono3py diff --git a/phono3py/cui/phono3py_argparse.py b/phono3py/cui/phono3py_argparse.py index 8ac6400f..086bb51d 100644 --- a/phono3py/cui/phono3py_argparse.py +++ b/phono3py/cui/phono3py_argparse.py @@ -629,6 +629,13 @@ def get_parser(fc_symmetry=False, is_nac=False, load_phono3py_yaml=False): default=False, help="Set plus minus displacements", ) + parser.add_argument( + "--pm-fc2", + dest="is_plusminus_displacements_fc2", + action="store_true", + default=False, + help="Set plus minus displacements for extra fc2", + ) parser.add_argument( "--pp-unit-conversion", dest="pp_unit_conversion", @@ -676,17 +683,15 @@ def get_parser(fc_symmetry=False, is_nac=False, load_phono3py_yaml=False): "--rd", "--random-displacements", dest="random_displacements", - type=int, default=None, - help="Number of supercells with random displacements", + help='Number of supercells with random displacements or "auto"', ) parser.add_argument( "--rd-fc2", "--random-displacements-fc2", dest="random_displacements_fc2", - type=int, default=None, - help="Number of phonon supercells with random displacements", + help='Number of phonon supercells with random displacements or "auto"', ) parser.add_argument( "--read-collision", diff --git a/phono3py/cui/settings.py b/phono3py/cui/settings.py index 096ee8d2..ffb29124 100644 --- a/phono3py/cui/settings.py +++ b/phono3py/cui/settings.py @@ -66,6 +66,7 @@ class Phono3pySettings(Settings): "is_kappa_star": True, "is_lbte": False, "is_N_U": False, + "is_plusminus_displacement_fc2": "auto", "is_real_self_energy": False, "is_reducible_collision_matrix": False, "is_spectral_function": False, @@ -214,6 +215,10 @@ def set_is_N_U(self, val): """Set is_N_U.""" self._v["is_N_U"] = val + def set_is_plusminus_displacement_fc2(self, val): + """Set is_plusminus_displacement_fc2.""" + self._v["is_plusminus_displacement_fc2"] = val + def set_is_real_self_energy(self, val): """Set is_real_self_energy.""" self._v["is_real_self_energy"] = val @@ -488,6 +493,10 @@ def _read_options(self): if self._args.is_N_U: self._confs["N_U"] = ".true." + if "is_plusminus_displacements_fc2" in self._args: + if self._args.is_plusminus_displacements_fc2: + self._confs["pm_fc2"] = ".true." + if "is_real_self_energy" in self._args: if self._args.is_real_self_energy: self._confs["real_self_energy"] = ".true." @@ -714,7 +723,6 @@ def _parse_conf(self): "pinv_method", "pinv_solver", "num_points_in_batch", - "random_displacements_fc2", "scattering_event_class", ): self.set_parameter(conf_key, int(confs[conf_key])) @@ -803,6 +811,22 @@ def _parse_conf(self): else: self.set_parameter("temperatures", vals) + if conf_key == "random_displacements_fc2": + rd = confs["random_displacements_fc2"] + if rd.lower() == "auto": + self.set_parameter("random_displacements_fc2", "auto") + else: + try: + self.set_parameter("random_displacements_fc2", int(rd)) + except ValueError: + self.setting_error(f"{conf_key.upper()} is incorrectly set.") + + if conf_key == "pm_fc2": + if confs["pm_fc2"].lower() == ".false.": + self.set_parameter("pm_fc2", False) + elif confs["pm_fc2"].lower() == ".true.": + self.set_parameter("pm_fc2", True) + def _set_settings(self): self.set_settings() params = self._parameters @@ -919,6 +943,10 @@ def _set_settings(self): if "N_U" in params: self._settings.set_is_N_U(params["N_U"]) + # Plus minus displacement for fc2 + if "pm_fc2" in params: + self._settings.set_is_plusminus_displacement_fc2(params["pm_fc2"]) + # Solve reducible collision matrix but not reduced matrix if "reducible_collision_matrix" in params: self._settings.set_is_reducible_collision_matrix( diff --git a/test/phonon3/test_displacements.py b/test/phonon3/test_displacements.py index 91e2e1e8..b4ed0093 100644 --- a/test/phonon3/test_displacements.py +++ b/test/phonon3/test_displacements.py @@ -1,6 +1,11 @@ """Tests of displacements.py.""" +import itertools +from typing import Literal, Optional, Union + import numpy as np +import pytest +from phonopy.structure.atoms import PhonopyAtoms import phono3py from phono3py import Phono3py @@ -74,7 +79,49 @@ ] -def test_duplicates_agno2(agno2_cell): +@pytest.mark.parametrize( + "is_plusminus,distance,number_of_snapshots", + itertools.product([False, True], [None, 0.06], [8, "auto"]), +) +def test_random_disps_agno2( + agno2_cell: PhonopyAtoms, + is_plusminus: bool, + distance: Optional[float], + number_of_snapshots: Union[int, Literal["auto"]], +): + """Test duplicated pairs of displacements.""" + ph3 = phono3py.load(unitcell=agno2_cell, supercell_matrix=[2, 1, 2]) + ph3.generate_displacements( + number_of_snapshots=number_of_snapshots, + distance=distance, + is_plusminus=is_plusminus, + ) + + ph3.generate_fc2_displacements( + number_of_snapshots=number_of_snapshots, + distance=distance, + is_plusminus=is_plusminus, + ) + + for d, n_d in zip((ph3.displacements, ph3.phonon_displacements), (92, 4)): + if number_of_snapshots == "auto": + assert len(d) == n_d * (is_plusminus + 1) + else: + assert len(d) == 8 * (is_plusminus + 1) + + if distance is None: + np.testing.assert_allclose( + np.linalg.norm(d, axis=2).ravel(), 0.03, atol=1e-8 + ) + else: + np.testing.assert_allclose( + np.linalg.norm(d, axis=2).ravel(), 0.06, atol=1e-8 + ) + if is_plusminus: + np.testing.assert_allclose(d[: len(d) // 2], -d[len(d) // 2 :], atol=1e-8) + + +def test_duplicates_agno2(agno2_cell: PhonopyAtoms): """Test duplicated pairs of displacements.""" ph3 = phono3py.load(unitcell=agno2_cell, supercell_matrix=[1, 1, 1]) ph3.generate_displacements()