def euphonic_calculate_modes(filename: str, cutoff: float = 20., gamma: bool = True, acoustic_sum_rule: Optional[str] = 'reciprocal'): """ Read force constants file with Euphonic and sample frequencies/modes :param filename: Input data :param cutoff: Sampling density of Brillouin-zone. Specified as real-space length cutoff in Angstrom. :param gamma: Shift sampling grid to include the Gamma-point. :param acoustic_sum_rule: Apply acoustic sum rule correction to force constants: options are 'realspace' and 'reciprocal', specifying different implementations of the correction. If None, no correction is applied. This option is referred to as "asr" in the Euphonic python API and command-line tools. :returns: euphonic.QpointPhononModes """ from math import ceil from euphonic.cli.utils import force_constants_from_file from euphonic.util import mp_grid fc = force_constants_from_file(filename) recip_lattice_lengths = np.linalg.norm( fc.crystal.reciprocal_cell().to('1/angstrom').magnitude, axis=1) mp_sampling = [ceil(x) for x in (cutoff * recip_lattice_lengths / (2 * np.pi))] qpts = mp_grid(mp_sampling) if gamma: mp_sampling = np.array(mp_sampling, dtype=int) # Shift directions with even number of samples by half the grid spacing offsets = ((mp_sampling + 1) % 2) * (0.5 / mp_sampling) qpts += offsets logger.notice('Calculating phonon modes on {} grid'.format( 'x'.join(map(str, mp_sampling)))) modes = fc.calculate_qpoint_phonon_modes(qpts, asr=acoustic_sum_rule) return modes
def main(params: List[str] = None): parser = get_parser() args = get_args(parser, params) data = load_data_from_file(args.filename) mode_widths = None if isinstance(data, euphonic.ForceConstants): recip_length_unit = euphonic.ureg(f'1 / {args.length_unit}') grid_spec = _grid_spec_from_args(data.crystal, grid=args.grid, grid_spacing=(args.grid_spacing * recip_length_unit)) print("Force Constants data was loaded. Calculating phonon modes " "on {} q-point grid...".format(' x '.join( [str(x) for x in grid_spec]))) if args.adaptive: if args.shape != 'gauss': raise ValueError('Currently only Gaussian shape is supported ' 'with adaptive broadening') cmkwargs = _calc_modes_kwargs(args) cmkwargs['return_mode_widths'] = True modes, mode_widths = data.calculate_qpoint_frequencies( mp_grid(grid_spec), **cmkwargs) if args.energy_broadening: mode_widths *= args.energy_broadening else: modes = data.calculate_qpoint_frequencies( mp_grid(grid_spec), **_calc_modes_kwargs(args)) elif isinstance(data, euphonic.QpointPhononModes): print("Phonon band data was loaded.") modes = data modes.frequencies_unit = args.energy_unit ebins, energy_unit = _get_energy_bins_and_units(args.energy_unit, modes, args.ebins, emin=args.e_min, emax=args.e_max) dos = modes.calculate_dos(ebins, mode_widths=mode_widths) if args.energy_broadening and not args.adaptive: dos = dos.broaden(args.energy_broadening * energy_unit, shape=args.shape) if args.x_label is None: x_label = f"Energy / {dos.x_data.units:~P}" else: x_label = args.x_label if args.y_label is None: y_label = "" else: y_label = args.y_label fig = plot_1d(dos, title=args.title, x_label=x_label, y_label=y_label, y_min=0, lw=1.0) matplotlib_save_or_show(save_filename=args.save_to)
class TestForceConstantsCalculateQPointPhononModes: def get_lzo_fc(): return ForceConstants.from_castep( get_castep_path('LZO', 'La2Zr2O7.castep_bin')) lzo_params = [ (get_lzo_fc(), 'LZO', [get_test_qpts(), {}], 'LZO_no_asr_qpoint_phonon_modes.json'), (get_lzo_fc(), 'LZO', [get_test_qpts(), { 'asr': 'realspace' }], 'LZO_realspace_qpoint_phonon_modes.json'), (get_lzo_fc(), 'LZO', [get_test_qpts(), { 'asr': 'reciprocal' }], 'LZO_reciprocal_qpoint_phonon_modes.json') ] def get_si2_fc(): return ForceConstants.from_castep( get_castep_path('Si2-sc-skew', 'Si2-sc-skew.castep_bin')) si2_params = [ (get_si2_fc(), 'Si2-sc-skew', [get_test_qpts(), {}], 'Si2-sc-skew_no_asr_qpoint_phonon_modes.json'), (get_si2_fc(), 'Si2-sc-skew', [get_test_qpts(), { 'asr': 'realspace' }], 'Si2-sc-skew_realspace_qpoint_phonon_modes.json'), (get_si2_fc(), 'Si2-sc-skew', [get_test_qpts(), { 'asr': 'reciprocal' }], 'Si2-sc-skew_reciprocal_qpoint_phonon_modes.json') ] def get_quartz_fc(): return ForceConstants.from_castep( get_castep_path('quartz', 'quartz.castep_bin')) quartz_params = [ (get_quartz_fc(), 'quartz', [get_test_qpts(), { 'asr': 'reciprocal', 'splitting': False }], 'quartz_reciprocal_qpoint_phonon_modes.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts(), { 'asr': 'reciprocal', 'splitting': False, 'eta_scale': 0.75 } ], 'quartz_reciprocal_qpoint_phonon_modes.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts('split'), { 'asr': 'reciprocal', 'splitting': True, 'insert_gamma': False } ], 'quartz_split_reciprocal_qpoint_phonon_modes.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts('split_insert_gamma'), { 'asr': 'reciprocal', 'splitting': True, 'insert_gamma': True } ], 'quartz_split_reciprocal_qpoint_phonon_modes.json') ] nacl_params = [ (ForceConstants.from_phonopy(path=get_phonopy_path('NaCl', ''), summary_name='phonopy_nacl.yaml'), 'NaCl', [get_test_qpts(), { 'asr': 'reciprocal' }], 'NaCl_reciprocal_qpoint_phonon_modes.json') ] cahgo2_params = [ (ForceConstants.from_phonopy(path=get_phonopy_path('CaHgO2', ''), summary_name='mp-7041-20180417.yaml'), 'CaHgO2', [get_test_qpts(), { 'asr': 'reciprocal' }], 'CaHgO2_reciprocal_qpoint_phonon_modes.json') ] @pytest.mark.parametrize( 'fc, material, all_args, expected_qpoint_phonon_modes_file', lzo_params + quartz_params + nacl_params + si2_params + cahgo2_params) @pytest.mark.parametrize('reduce_qpts, n_threads', [(False, 0), (True, 0), (True, 1), (True, 2)]) def test_calculate_qpoint_phonon_modes(self, fc, material, all_args, expected_qpoint_phonon_modes_file, reduce_qpts, n_threads): func_kwargs = all_args[1] func_kwargs['reduce_qpts'] = reduce_qpts if n_threads == 0: func_kwargs['use_c'] = False else: func_kwargs['use_c'] = True func_kwargs['n_threads'] = n_threads qpoint_phonon_modes = fc.calculate_qpoint_phonon_modes( all_args[0], **func_kwargs) expected_qpoint_phonon_modes = ExpectedQpointPhononModes( os.path.join(get_qpt_ph_modes_dir(material), expected_qpoint_phonon_modes_file)) # Only give gamma-acoustic modes special treatment if the acoustic # sum rule has been applied if not 'asr' in func_kwargs.keys(): gamma_atol = None else: gamma_atol = 0.5 check_qpt_ph_modes(qpoint_phonon_modes, expected_qpoint_phonon_modes, frequencies_atol=1e-4, frequencies_rtol=2e-5, acoustic_gamma_atol=gamma_atol) @pytest.mark.parametrize( ('fc, material, all_args, expected_qpoint_phonon_modes_file, ' 'expected_modw_file'), [(get_quartz_fc(), 'quartz', [ mp_grid([5, 5, 4]), { 'asr': 'reciprocal', 'return_mode_widths': True } ], 'quartz_554_full_qpoint_phonon_modes.json', 'quartz_554_full_mode_widths.json'), (get_lzo_fc(), 'LZO', [ mp_grid([2, 2, 2]), { 'asr': 'reciprocal', 'return_mode_widths': True } ], 'lzo_222_full_qpoint_phonon_modes.json', 'lzo_222_full_mode_widths.json')]) @pytest.mark.parametrize('n_threads', [0, 2]) def test_calculate_qpoint_phonon_modes_with_mode_widths( self, fc, material, all_args, expected_qpoint_phonon_modes_file, expected_modw_file, n_threads): func_kwargs = all_args[1] if n_threads == 0: func_kwargs['use_c'] = False else: func_kwargs['use_c'] = True func_kwargs['n_threads'] = n_threads qpoint_phonon_modes, modw = fc.calculate_qpoint_phonon_modes( all_args[0], **func_kwargs) with open(os.path.join(get_fc_dir(), expected_modw_file), 'r') as fp: modw_dict = json.load(fp) expected_modw = modw_dict['mode_widths'] * ureg( modw_dict['mode_widths_unit']) expected_qpoint_phonon_modes = ExpectedQpointPhononModes( os.path.join(get_qpt_ph_modes_dir(material), expected_qpoint_phonon_modes_file)) # Only give gamma-acoustic modes special treatment if the acoustic # sum rule has been applied if not 'asr' in func_kwargs.keys(): gamma_atol = None else: gamma_atol = 0.5 check_qpt_ph_modes(qpoint_phonon_modes, expected_qpoint_phonon_modes, frequencies_atol=1e-4, frequencies_rtol=2e-5, acoustic_gamma_atol=gamma_atol) assert modw.units == expected_modw.units npt.assert_allclose(modw.magnitude, expected_modw.magnitude, atol=2e-4, rtol=5e-5) # ForceConstants stores some values (supercell image list, vectors # for the Ewald sum) so check repeated calculations give the same # result def test_repeated_calculate_qpoint_phonon_modes_doesnt_change_result(self): fc = get_fc('quartz') qpt_ph_modes1 = fc.calculate_qpoint_phonon_modes(get_test_qpts(), asr='realspace') qpt_ph_modes2 = fc.calculate_qpoint_phonon_modes(get_test_qpts(), asr='realspace') check_qpt_ph_modes(qpt_ph_modes1, qpt_ph_modes2) @pytest.mark.parametrize( 'fc, material, qpt, kwargs, expected_qpt_ph_modes_file', [(get_fc('quartz'), 'quartz', np.array([[1., 1., 1.]]), { 'splitting': True }, 'quartz_single_qpoint_phonon_modes.json')]) def test_calculate_qpoint_phonon_modes_single_qpt( self, fc, material, qpt, kwargs, expected_qpt_ph_modes_file): qpoint_phonon_modes = fc.calculate_qpoint_phonon_modes(qpt, **kwargs) expected_qpoint_phonon_modes = ExpectedQpointPhononModes( os.path.join(get_qpt_ph_modes_dir(material), expected_qpt_ph_modes_file)) check_qpt_ph_modes(qpoint_phonon_modes, expected_qpoint_phonon_modes, frequencies_atol=1e-4, frequencies_rtol=2e-5) weights = np.array([0.1, 0.05, 0.05, 0.2, 0.2, 0.15, 0.15, 0.2, 0.1]) weights_output_split_gamma = np.array([ 0.1, 0.05, 0.025, 0.025, 0.2, 0.1, 0.1, 0.075, 0.075, 0.075, 0.075, 0.2, 0.1 ]) @pytest.mark.parametrize( 'fc, qpts, weights, expected_weights, kwargs', [(get_fc('quartz'), get_test_qpts(), weights, weights, {}), (get_fc('quartz'), get_test_qpts('split_insert_gamma'), weights, weights_output_split_gamma, { 'insert_gamma': True })]) def test_calculate_qpoint_phonon_modes_with_weights_sets_weights( self, fc, qpts, weights, expected_weights, kwargs): qpt_ph_modes_weighted = fc.calculate_qpoint_phonon_modes( qpts, weights=weights, **kwargs) npt.assert_allclose(qpt_ph_modes_weighted.weights, expected_weights) @pytest.mark.parametrize( 'fc, qpts, weights, expected_weights, kwargs', [(get_fc('quartz'), get_test_qpts(), weights, weights, {}), (get_fc('quartz'), get_test_qpts('split_insert_gamma'), weights, weights_output_split_gamma, { 'insert_gamma': True })]) def test_calculate_qpoint_phonon_modes_with_weights_doesnt_change_result( self, fc, qpts, weights, expected_weights, kwargs): qpt_ph_modes_weighted = fc.calculate_qpoint_phonon_modes( qpts, weights=weights, **kwargs) qpt_ph_modes_unweighted = fc.calculate_qpoint_phonon_modes( qpts, **kwargs) qpt_ph_modes_unweighted.weights = expected_weights check_qpt_ph_modes(qpt_ph_modes_weighted, qpt_ph_modes_unweighted) @pytest.mark.parametrize('asr', ['realspace', 'reciprocal']) def test_calc_qpt_ph_mds_asr_with_nonsense_fc_raises_warning(self, asr): fc = ForceConstants.from_json_file( os.path.join(get_fc_dir(), 'quartz_random_force_constants.json')) with pytest.warns(UserWarning): fc.calculate_qpoint_phonon_modes(get_test_qpts(), asr=asr)
def sample_sphere_structure_factor( fc: ForceConstants, mod_q: Quantity, dw: DebyeWaller = None, dw_spacing: Quantity = 0.025 * ureg('1/angstrom'), temperature: Optional[Quantity] = 273. * ureg['K'], sampling: str = 'golden', npts: int = 1000, jitter: bool = False, energy_bins: Quantity = None, scattering_lengths: Union[dict, str] = 'Sears1992', **calc_modes_args ) -> Spectrum1D: """Sample structure factor, averaging over a sphere of constant |q| (Specifically, this is the one-phonon inelastic-scattering structure factor as implemented in QpointPhononModes.calculate_structure_factor().) Parameters ---------- fc Force constant data for system mod_q scalar radius of sphere from which vector q samples are taken dw Debye-Waller exponent used for evaluation of scattering function. If not provided, this is generated automatically over Monkhorst-Pack q-point mesh determined by ``dw_spacing``. dw_spacing Maximum distance between q-points in automatic q-point mesh (if used) for Debye-Waller calculation. temperature Temperature for Debye-Waller calculation. If both temperature and dw are set to None, Debye-Waller factor will be omitted. sampling Sphere-sampling scheme. (Case-insensitive) options are: - 'golden': Fibonnaci-like sampling that steps regularly along one spherical coordinate while making irrational steps in the other - 'sphere-projected-grid': Regular 2-D square mesh projected onto sphere. npts will be distributed as evenly as possible (i.e. using twice as many 'longitude' as 'lattitude' lines), rounding up if necessary. - 'spherical-polar-grid': Mesh over regular subdivisions in spherical polar coordinates. npts will be rounded up as necessary in the same scheme as for sphere-projected-grid. 'Latitude' lines are evenly-spaced in z - 'spherical-polar-improved': npts distributed as regularly as possible using spherical polar coordinates: 'latitude' lines are evenly-spaced in z and points are distributed among these rings to obtain most even spacing possible. - 'random-sphere': Points are distributed randomly in unit square and projected onto sphere. npts Number of samples. Note that some sampling methods have constraints on valid values and will round up as appropriate. jitter For non-random sampling schemes, apply an additional random displacement to each point. energy_bins Preferred energy bin edges. If not provided, will setup 1000 bins (1001 bin edges) from 0 to 1.05 * [max energy] scattering_lengths Dict of neutron scattering lengths labelled by element. If a string is provided, this selects coherent scattering lengths from reference data by setting the 'label' argument of the euphonic.util.get_reference_data() function. **calc_modes_args other keyword arguments (e.g. 'use_c') will be passed to ForceConstants.calculate_qpoint_phonon_modes() Returns ------- Spectrum1D """ if isinstance(scattering_lengths, str): scattering_lengths = get_reference_data( physical_property='coherent_scattering_length', collection=scattering_lengths) # type: dict if temperature is not None: if (dw is None): dw_qpts = mp_grid(fc.crystal.get_mp_grid_spec(dw_spacing)) dw_phonons = fc.calculate_qpoint_phonon_modes(dw_qpts, **calc_modes_args) dw = dw_phonons.calculate_debye_waller(temperature ) # type: DebyeWaller else: if not np.isclose(dw.temperature.to('K').magnitude, temperature.to('K').magnitude): raise ValueError('Temperature argument is not consistent with ' 'temperature stored in DebyeWaller object.') qpts_cart = _get_qpts_sphere(npts, sampling=sampling, jitter=jitter ) * mod_q qpts_frac = _qpts_cart_to_frac(qpts_cart, fc.crystal) phonons = fc.calculate_qpoint_phonon_modes(qpts_frac, **calc_modes_args ) # type: QpointPhononModes if energy_bins is None: energy_bins = _get_default_bins(phonons) s = phonons.calculate_structure_factor( scattering_lengths=scattering_lengths, dw=dw) return s.calculate_1d_average(energy_bins)
class TestForceConstantsCalculateQPointFrequencies: def get_lzo_fc(): return ForceConstants.from_castep( get_castep_path('LZO', 'La2Zr2O7.castep_bin')) lzo_params = [ (get_lzo_fc(), 'LZO', [get_test_qpts(), {}], 'LZO_no_asr_qpoint_frequencies.json'), (get_lzo_fc(), 'LZO', [get_test_qpts(), { 'asr': 'realspace' }], 'LZO_realspace_qpoint_frequencies.json') ] def get_quartz_fc(): return ForceConstants.from_castep( get_castep_path('quartz', 'quartz.castep_bin')) quartz_params = [ (get_quartz_fc(), 'quartz', [get_test_qpts(), { 'asr': 'reciprocal', 'splitting': False }], 'quartz_reciprocal_qpoint_frequencies.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts(), { 'asr': 'reciprocal', 'splitting': False, 'eta_scale': 0.75 } ], 'quartz_reciprocal_qpoint_frequencies.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts('split'), { 'asr': 'reciprocal', 'splitting': True, 'insert_gamma': False } ], 'quartz_split_reciprocal_qpoint_frequencies.json'), (get_quartz_fc(), 'quartz', [ get_test_qpts('split_insert_gamma'), { 'asr': 'reciprocal', 'splitting': True, 'insert_gamma': True } ], 'quartz_split_reciprocal_qpoint_frequencies.json') ] @pytest.mark.parametrize( 'fc, material, all_args, expected_qpoint_frequencies_file', lzo_params + quartz_params) @pytest.mark.parametrize('reduce_qpts, n_threads', [(False, 0), (True, 0), (True, 1), (True, 2)]) def test_calculate_qpoint_frequencies(self, fc, material, all_args, expected_qpoint_frequencies_file, reduce_qpts, n_threads): func_kwargs = all_args[1] func_kwargs['reduce_qpts'] = reduce_qpts if n_threads == 0: func_kwargs['use_c'] = False else: func_kwargs['use_c'] = True func_kwargs['n_threads'] = n_threads qpt_freqs = fc.calculate_qpoint_frequencies(all_args[0], **func_kwargs) expected_qpt_freqs = get_expected_qpt_freqs( material, expected_qpoint_frequencies_file) # Only give gamma-acoustic modes special treatment if the acoustic # sum rule has been applied if not 'asr' in func_kwargs.keys(): gamma_atol = None else: gamma_atol = 0.5 check_qpt_freqs(qpt_freqs, expected_qpt_freqs, frequencies_atol=1e-4, frequencies_rtol=2e-5, acoustic_gamma_atol=gamma_atol) @pytest.mark.parametrize( ('fc, material, all_args, expected_qpoint_frequencies_file, ' 'expected_modw_file'), [(get_quartz_fc(), 'quartz', [ mp_grid([5, 5, 4]), { 'asr': 'reciprocal', 'return_mode_widths': True } ], 'quartz_554_full_qpoint_frequencies.json', 'quartz_554_full_mode_widths.json'), (get_lzo_fc(), 'LZO', [ mp_grid([2, 2, 2]), { 'asr': 'reciprocal', 'return_mode_widths': True } ], 'lzo_222_full_qpoint_frequencies.json', 'lzo_222_full_mode_widths.json')]) @pytest.mark.parametrize('n_threads', [0, 2]) def test_calculate_qpoint_frequencies_with_mode_widths( self, fc, material, all_args, expected_qpoint_frequencies_file, expected_modw_file, n_threads): func_kwargs = all_args[1] if n_threads == 0: func_kwargs['use_c'] = False else: func_kwargs['use_c'] = True func_kwargs['n_threads'] = n_threads qpt_freqs, modw = fc.calculate_qpoint_frequencies( all_args[0], **func_kwargs) with open(os.path.join(get_fc_dir(), expected_modw_file), 'r') as fp: modw_dict = json.load(fp) expected_modw = modw_dict['mode_widths'] * ureg( modw_dict['mode_widths_unit']) expected_qpt_freqs = get_expected_qpt_freqs( material, expected_qpoint_frequencies_file) # Only give gamma-acoustic modes special treatment if the acoustic # sum rule has been applied if not 'asr' in func_kwargs.keys(): gamma_atol = None else: gamma_atol = 0.5 check_qpt_freqs(qpt_freqs, expected_qpt_freqs, frequencies_atol=1e-4, frequencies_rtol=2e-5, acoustic_gamma_atol=gamma_atol) assert modw.units == expected_modw.units npt.assert_allclose(modw.magnitude, expected_modw.magnitude, atol=2e-4, rtol=5e-5) weights = np.array([0.1, 0.05, 0.05, 0.2, 0.2, 0.15, 0.15, 0.2, 0.1]) weights_output_split_gamma = np.array([ 0.1, 0.05, 0.025, 0.025, 0.2, 0.1, 0.1, 0.075, 0.075, 0.075, 0.075, 0.2, 0.1 ]) @pytest.mark.parametrize( 'fc, qpts, weights, expected_weights, kwargs', [(get_fc('quartz'), get_test_qpts(), weights, weights, {}), (get_fc('quartz'), get_test_qpts('split_insert_gamma'), weights, weights_output_split_gamma, { 'insert_gamma': True })]) def test_calculate_qpoint_frequencies_with_weights_sets_weights( self, fc, qpts, weights, expected_weights, kwargs): qpt_freqs_weighted = fc.calculate_qpoint_frequencies(qpts, weights=weights, **kwargs) npt.assert_allclose(qpt_freqs_weighted.weights, expected_weights)
def test_444_grid(self): qpts = mp_grid([4,4,4]) expected_qpts = np.loadtxt(os.path.join('data','qgrid_444.txt')) npt.assert_equal(qpts, expected_qpts)