diff --git a/atomistics/calculators/ase.py b/atomistics/calculators/ase.py index 37485977..2b602b70 100644 --- a/atomistics/calculators/ase.py +++ b/atomistics/calculators/ase.py @@ -25,43 +25,54 @@ from atomistics.calculators.interface import TaskName -class ASEExecutor(object): +class ASEOutput(OutputStatic, OutputMolecularDynamics): def __init__(self, ase_structure, ase_calculator): self.structure = ase_structure self.structure.calc = ase_calculator + @property def forces(self): return self.structure.get_forces() + @property def energy(self): return self.structure.get_potential_energy() + @property def energy_pot(self): return self.structure.get_potential_energy() + @property def energy_tot(self): return ( self.structure.get_potential_energy() + self.structure.get_kinetic_energy() ) + @property def stress(self): return self.structure.get_stress(voigt=False) + @property def pressure(self): return self.structure.get_stress(voigt=False) + @property def cell(self): return self.structure.get_cell() + @property def positions(self): return self.structure.get_positions() + @property def velocities(self): return self.structure.get_velocities() + @property def temperature(self): return self.structure.get_temperature() + @property def volume(self): return self.structure.get_volume() @@ -107,11 +118,8 @@ def calc_static_with_ase( ase_calculator, output_keys=OutputStatic.keys(), ): - return OutputStatic( - **{k: getattr(ASEExecutor, k) for k in OutputStatic.keys()} - ).get( - ASEExecutor(ase_structure=structure, ase_calculator=ase_calculator), - *output_keys, + return ASEOutput(ase_structure=structure, ase_calculator=ase_calculator).get_output( + output_keys=output_keys ) @@ -120,16 +128,12 @@ def _calc_md_step_with_ase( ): structure.calc = ase_calculator MaxwellBoltzmannDistribution(atoms=structure, temperature_K=temperature) - ASEOutputMolecularDynamics = OutputMolecularDynamics( - **{k: getattr(ASEExecutor, k) for k in OutputMolecularDynamics.keys()} - ) cache = {q: [] for q in output_keys} for i in range(int(run / thermo)): dyn.run(thermo) - calc_dict = ASEOutputMolecularDynamics.get( - ASEExecutor(ase_structure=structure, ase_calculator=ase_calculator), - *output_keys, - ) + calc_dict = ASEOutput( + ase_structure=structure, ase_calculator=ase_calculator + ).get_output(output_keys=output_keys) for k, v in calc_dict.items(): cache[k].append(v) return {q: np.array(cache[q]) for q in output_keys} diff --git a/atomistics/calculators/lammps/calculator.py b/atomistics/calculators/lammps/calculator.py index 5ac82f0a..d48bf7e6 100644 --- a/atomistics/calculators/lammps/calculator.py +++ b/atomistics/calculators/lammps/calculator.py @@ -7,6 +7,7 @@ from pylammpsmpi import LammpsASELibrary from atomistics.calculators.interface import get_quantities_from_tasks +from atomistics.calculators.lammps.output import LammpsOutput from atomistics.calculators.lammps.helpers import ( lammps_calc_md, lammps_run, @@ -28,9 +29,11 @@ LAMMPS_MINIMIZE_VOLUME, ) from atomistics.calculators.wrapper import as_task_dict_evaluator -from atomistics.shared.thermal_expansion import OutputThermalExpansion -from atomistics.shared.output import OutputStatic, OutputMolecularDynamics - +from atomistics.shared.output import ( + OutputStatic, + OutputMolecularDynamics, + OutputThermalExpansion, +) if TYPE_CHECKING: from ase import Atoms @@ -132,12 +135,7 @@ def calc_static_with_lammps( lmp=lmp, **kwargs, ) - result_dict = OutputStatic( - forces=LammpsASELibrary.interactive_forces_getter, - energy=LammpsASELibrary.interactive_energy_pot_getter, - stress=LammpsASELibrary.interactive_pressures_getter, - volume=LammpsASELibrary.interactive_volume_getter, - ).get(lmp_instance, *output_keys) + result_dict = LammpsOutput(lmp=lmp_instance).get_output(output_keys=output_keys) lammps_shutdown(lmp_instance=lmp_instance, close_instance=lmp is None) return result_dict diff --git a/atomistics/calculators/lammps/helpers.py b/atomistics/calculators/lammps/helpers.py index 0dfe5f89..a1f32d78 100644 --- a/atomistics/calculators/lammps/helpers.py +++ b/atomistics/calculators/lammps/helpers.py @@ -5,9 +5,13 @@ from pylammpsmpi import LammpsASELibrary from atomistics.calculators.lammps.potential import validate_potential_dataframe +from atomistics.calculators.lammps.output import LammpsOutput +from atomistics.shared.output import ( + OutputMolecularDynamics, + OutputThermalExpansion, +) from atomistics.shared.thermal_expansion import get_thermal_expansion_output from atomistics.shared.tqdm_iterator import get_tqdm_iterator -from atomistics.shared.output import OutputMolecularDynamics, OutputThermalExpansion def lammps_run(structure, potential_dataframe, input_template=None, lmp=None, **kwargs): @@ -47,17 +51,7 @@ def lammps_calc_md_step( ): run_str_rendered = Template(run_str).render(run=run) lmp_instance.interactive_lib_command(run_str_rendered) - return OutputMolecularDynamics( - positions=LammpsASELibrary.interactive_positions_getter, - cell=LammpsASELibrary.interactive_cells_getter, - forces=LammpsASELibrary.interactive_forces_getter, - temperature=LammpsASELibrary.interactive_temperatures_getter, - energy_pot=LammpsASELibrary.interactive_energy_pot_getter, - energy_tot=LammpsASELibrary.interactive_energy_tot_getter, - pressure=LammpsASELibrary.interactive_pressures_getter, - velocities=LammpsASELibrary.interactive_velocities_getter, - volume=LammpsASELibrary.interactive_volume_getter, - ).get(lmp_instance, *output_keys) + return LammpsOutput(lmp=lmp_instance).get_output(output_keys=output_keys) def lammps_calc_md( diff --git a/atomistics/calculators/lammps/output.py b/atomistics/calculators/lammps/output.py new file mode 100644 index 00000000..13cf8776 --- /dev/null +++ b/atomistics/calculators/lammps/output.py @@ -0,0 +1,50 @@ +from atomistics.shared.output import OutputMolecularDynamics, OutputStatic + + +class LammpsOutput(OutputMolecularDynamics, OutputStatic): + def __init__(self, lmp): + self._lmp = lmp + + @property + def forces(self): + return self._lmp.interactive_forces_getter() + + @property + def energy(self): + return self._lmp.interactive_energy_pot_getter() + + @property + def stress(self): + return self._lmp.interactive_pressures_getter() + + @property + def volume(self): + return self._lmp.interactive_volume_getter() + + @property + def positions(self): + return self._lmp.interactive_positions_getter() + + @property + def cell(self): + return self._lmp.interactive_cells_getter() + + @property + def temperature(self): + return self._lmp.interactive_temperatures_getter() + + @property + def energy_pot(self): + return self._lmp.interactive_energy_pot_getter() + + @property + def energy_tot(self): + return self._lmp.interactive_energy_tot_getter() + + @property + def pressure(self): + return self._lmp.interactive_pressures_getter() + + @property + def velocities(self): + return self._lmp.interactive_velocities_getter() diff --git a/atomistics/calculators/qe.py b/atomistics/calculators/qe.py index 3048cf3d..b4228b9c 100644 --- a/atomistics/calculators/qe.py +++ b/atomistics/calculators/qe.py @@ -9,19 +9,23 @@ from atomistics.calculators.wrapper import as_task_dict_evaluator -class QEStaticParser(object): +class QEOutputStatic(OutputStatic): def __init__(self, filename): self.parser = io.read_pw_scf(filename=filename, use_alat=True) + @property def forces(self): return self.parser.forces + @property def energy(self): return self.parser.etot + @property def stress(self): return self.parser.stress + @property def volume(self): return self.parser.volume @@ -207,9 +211,7 @@ def calc_static_with_qe( call_qe_via_ase_command( calculation_name=calculation_name, working_directory=working_directory ) - return OutputStatic( - **{k: getattr(QEStaticParser, k) for k in OutputStatic.keys()} - ).get(QEStaticParser(filename=output_file_name), *output_keys) + return QEOutputStatic(filename=output_file_name).get_output(output_keys=output_keys) @as_task_dict_evaluator diff --git a/atomistics/shared/output.py b/atomistics/shared/output.py index f8a64fbb..ad1d4cfb 100644 --- a/atomistics/shared/output.py +++ b/atomistics/shared/output.py @@ -1,106 +1,327 @@ -import dataclasses +""" +The output module defines the abstract output classes for the different types of outputs defined by the atomistics +package. All output classes are abstract classes, which define the output as abstract properties and are derived from +the atomistics.shared.output.AbstractOutput class. +""" +from abc import ABC, abstractmethod + +import numpy as np + + +class AbstractOutput: + """ + Abstract Base class used for the implementation of the individual output classes. + """ + + def get_output(self, output_keys) -> dict: + """ + Evaluate multiple properties with a single function call by providing a list of output keys each referencing one + property as input and returning a dictionary with the property names as keys and the corresponding results as + values. + + Args: + output_keys (tuple): Tuple of output property names as strings to be evaluated + + Returns: + dict: dictionary with the property names as keys and the corresponding results as values. + """ + return {q: getattr(self, q) for q in output_keys} -@dataclasses.dataclass -class Output: @classmethod - def keys(cls): - return tuple(field.name for field in dataclasses.fields(cls)) - - def get(self, engine, *output: str) -> dict: - return {q: getattr(self, q)(engine) for q in output} - - -@dataclasses.dataclass -class OutputStatic(Output): - forces: callable - energy: callable - stress: callable - volume: callable - - -@dataclasses.dataclass -class OutputMolecularDynamics(Output): - positions: callable - cell: callable - forces: callable - temperature: callable - energy_pot: callable - energy_tot: callable - pressure: callable - velocities: callable - volume: callable - - -@dataclasses.dataclass -class OutputThermalExpansion(Output): - temperatures: callable - volumes: callable - - -@dataclasses.dataclass -class OutputThermodynamic(OutputThermalExpansion): - free_energy: callable - entropy: callable - heat_capacity: callable - - -@dataclasses.dataclass -class EquilibriumEnergy(Output): - energy_eq: callable - - -@dataclasses.dataclass -class EquilibriumVolume(Output): - volume_eq: callable - - -@dataclasses.dataclass -class EquilibriumBulkModul(Output): - bulkmodul_eq: callable - - -@dataclasses.dataclass -class EquilibriumBulkModulDerivative(Output): - b_prime_eq: callable - - -@dataclasses.dataclass -class OutputEnergyVolumeCurve( - EquilibriumEnergy, - EquilibriumVolume, - EquilibriumBulkModul, - EquilibriumBulkModulDerivative, -): - fit_dict: callable - energy: callable - volume: callable - - -@dataclasses.dataclass -class OutputElastic(Output): - elastic_matrix: callable - elastic_matrix_inverse: callable - bulkmodul_voigt: callable - bulkmodul_reuss: callable - bulkmodul_hill: callable - shearmodul_voigt: callable - shearmodul_reuss: callable - shearmodul_hill: callable - youngsmodul_voigt: callable - youngsmodul_reuss: callable - youngsmodul_hill: callable - poissonsratio_voigt: callable - poissonsratio_reuss: callable - poissonsratio_hill: callable - AVR: callable - elastic_matrix_eigval: callable - - -@dataclasses.dataclass -class OutputPhonons(Output): - mesh_dict: callable - band_structure_dict: callable - total_dos_dict: callable - dynamical_matrix: callable - force_constants: callable + def keys(cls) -> tuple: + """ + Return all public functions and properties defined in a given class. + + Returns: + tuple: Tuple of names all public functions and properties defined in the derived class as strings. + """ + return tuple( + [ + k + for k in cls.__dict__.keys() + if k[0] != "_" and k not in ["get_output", "keys"] + ] + ) + + +class OutputStatic(ABC, AbstractOutput): + """ + Output class for a static calculation of a supercell with n atoms. + """ + + @property + @abstractmethod + def forces(self) -> np.ndarray: # (n, 3) [eV / Ang^2] + pass + + @property + @abstractmethod + def energy(self) -> float: # [eV] + pass + + @property + @abstractmethod + def stress(self) -> np.ndarray: # (3, 3) [GPa] + pass + + @property + @abstractmethod + def volume(self) -> float: # [Ang^3] + pass + + +class OutputMolecularDynamics(ABC, AbstractOutput): + """ + Output class for a molecular dynamics calculation with t steps of a supercell with n atoms. + """ + + @property + @abstractmethod + def positions(self) -> np.ndarray: # (t, n, 3) [Ang] + pass + + @property + @abstractmethod + def cell(self) -> np.ndarray: # (t, 3, 3) [Ang] + pass + + @property + @abstractmethod + def forces(self) -> np.ndarray: # (n, 3) [eV / Ang^2] + pass + + @property + @abstractmethod + def temperature(self) -> np.ndarray: # (t) [K] + pass + + @property + @abstractmethod + def energy_pot(self) -> np.ndarray: # (t) [eV] + pass + + @property + @abstractmethod + def energy_tot(self) -> np.ndarray: # (t) [eV] + pass + + @property + @abstractmethod + def pressure(self) -> np.ndarray: # (t, 3, 3) [GPa] + pass + + @property + @abstractmethod + def velocities(self) -> np.ndarray: # (t, n, 3) [eV / Ang] + pass + + @property + @abstractmethod + def volume(self) -> np.ndarray: # (t) [Ang^3] + pass + + +class OutputThermalExpansion(ABC, AbstractOutput): + """ + Output class for a thermal expansion calculation iterating over T temperature steps. + """ + + @property + @abstractmethod + def temperatures(self) -> np.ndarray: # (T) [K] + pass + + @property + @abstractmethod + def volumes(self) -> np.ndarray: # (T) [Ang^3] + pass + + +class OutputThermodynamic(ABC, AbstractOutput): + """ + Output class for the calculation of the temperature dependence in T temperature steps of thermodynamic properties + """ + + @property + @abstractmethod + def temperatures(self) -> np.ndarray: # (T) [K] + pass + + @property + @abstractmethod + def volumes(self) -> np.ndarray: # (T) [Ang^3] + pass + + @property + @abstractmethod + def free_energy(self) -> np.ndarray: # (T) [eV] + pass + + @property + @abstractmethod + def entropy(self) -> np.ndarray: # (T) [eV] + pass + + @property + @abstractmethod + def heat_capacity(self) -> np.ndarray: # (T) [eV] + pass + + +class OutputEnergyVolumeCurve(ABC, AbstractOutput): + """ + Output class for the calculation on an energy volume curve calculation based on V strained cells. + """ + + @property + @abstractmethod + def energy_eq(self) -> float: # float [eV] + pass + + @property + @abstractmethod + def volume_eq(self) -> float: # float [Ang^3] + pass + + @property + @abstractmethod + def bulkmodul_eq(self) -> float: # float [GPa] + pass + + @property + @abstractmethod + def b_prime_eq(self) -> float: # float + pass + + @property + @abstractmethod + def fit_dict(self) -> dict: # dict + pass + + @property + @abstractmethod + def energy(self) -> np.ndarray: # (V) [eV] + pass + + @property + @abstractmethod + def volume(self) -> np.ndarray: # (V) [Ang^3] + pass + + +class OutputElastic(ABC, AbstractOutput): + """ + Output class for the calculation of elastic moduli from the elastic matrix of the elastic constants. + """ + + @property + @abstractmethod + def elastic_matrix(self) -> np.ndarray: # (6,6) [GPa] + pass + + @property + @abstractmethod + def elastic_matrix_inverse(self) -> np.ndarray: # (6,6) [GPa] + pass + + @property + @abstractmethod + def bulkmodul_voigt(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def bulkmodul_reuss(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def bulkmodul_hill(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def shearmodul_voigt(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def shearmodul_reuss(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def shearmodul_hill(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def youngsmodul_voigt(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def youngsmodul_reuss(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def youngsmodul_hill(self) -> float: # [GPa] + pass + + @property + @abstractmethod + def poissonsratio_voigt(self) -> float: + pass + + @property + @abstractmethod + def poissonsratio_reuss(self) -> float: + pass + + @property + @abstractmethod + def poissonsratio_hill(self) -> float: + pass + + @property + @abstractmethod + def AVR(self) -> float: + pass + + @property + @abstractmethod + def elastic_matrix_eigval(self) -> np.ndarray: # (6,6) [GPa] + pass + + +class OutputPhonons(ABC, AbstractOutput): + """ + Output class for the calculation of phonons using the finite displacement method + """ + + @property + @abstractmethod + def mesh_dict(self) -> dict: + pass + + @property + @abstractmethod + def band_structure_dict(self) -> dict: + pass + + @property + @abstractmethod + def total_dos_dict(self) -> dict: + pass + + @property + @abstractmethod + def dynamical_matrix(self) -> dict: + pass + + @property + @abstractmethod + def force_constants(self) -> dict: + pass diff --git a/atomistics/shared/thermal_expansion.py b/atomistics/shared/thermal_expansion.py index d8da1616..540d3000 100644 --- a/atomistics/shared/thermal_expansion.py +++ b/atomistics/shared/thermal_expansion.py @@ -1,27 +1,21 @@ from atomistics.shared.output import OutputThermalExpansion -class ThermalExpansionProperties: +class ThermalExpansionOutputWrapper(OutputThermalExpansion): def __init__(self, temperatures_lst, volumes_lst): self._temperatures_lst = temperatures_lst self._volumes_lst = volumes_lst + @property def volumes(self): return self._volumes_lst + @property def temperatures(self): return self._temperatures_lst def get_thermal_expansion_output(temperatures_lst, volumes_lst, output_keys): - return OutputThermalExpansion( - **{ - k: getattr(ThermalExpansionProperties, k) - for k in OutputThermalExpansion.keys() - } - ).get( - ThermalExpansionProperties( - temperatures_lst=temperatures_lst, volumes_lst=volumes_lst - ), - *output_keys, - ) + return ThermalExpansionOutputWrapper( + temperatures_lst=temperatures_lst, volumes_lst=volumes_lst + ).get_output(output_keys=output_keys) diff --git a/atomistics/workflows/elastic/elastic_moduli.py b/atomistics/workflows/elastic/elastic_moduli.py index 3fd07e6c..14eedb6b 100644 --- a/atomistics/workflows/elastic/elastic_moduli.py +++ b/atomistics/workflows/elastic/elastic_moduli.py @@ -1,7 +1,9 @@ -from functools import cache +from functools import cached_property import numpy as np +from atomistics.shared.output import OutputElastic + def get_bulkmodul_voigt(elastic_matrix): return ( @@ -121,96 +123,95 @@ def _hill_approximation(voigt, reuss): return 0.50 * (voigt + reuss) -class ElasticProperties: +class ElasticMatrixOutput(OutputElastic): def __init__(self, elastic_matrix): self._elastic_matrix = elastic_matrix + @property def elastic_matrix(self): return self._elastic_matrix - @cache + @cached_property def elastic_matrix_inverse(self): - return get_elastic_matrix_inverse(elastic_matrix=self.elastic_matrix()) + return get_elastic_matrix_inverse(elastic_matrix=self.elastic_matrix) - @cache + @cached_property def bulkmodul_voigt(self): - return get_bulkmodul_voigt(elastic_matrix=self.elastic_matrix()) + return get_bulkmodul_voigt(elastic_matrix=self.elastic_matrix) - @cache + @cached_property def shearmodul_voigt(self): - return get_shearmodul_voigt(elastic_matrix=self.elastic_matrix()) + return get_shearmodul_voigt(elastic_matrix=self.elastic_matrix) - @cache + @cached_property def bulkmodul_reuss(self): - return get_bulkmodul_reuss(elastic_matrix_inverse=self.elastic_matrix_inverse()) + return get_bulkmodul_reuss(elastic_matrix_inverse=self.elastic_matrix_inverse) - @cache + @cached_property def shearmodul_reuss(self): - return get_shearmodul_reuss( - elastic_matrix_inverse=self.elastic_matrix_inverse() - ) + return get_shearmodul_reuss(elastic_matrix_inverse=self.elastic_matrix_inverse) - @cache + @cached_property def bulkmodul_hill(self): return get_bulkmodul_hill( - bulkmodul_voigt=self.bulkmodul_voigt(), - bulkmodul_reuss=self.bulkmodul_reuss(), + bulkmodul_voigt=self.bulkmodul_voigt, + bulkmodul_reuss=self.bulkmodul_reuss, ) - @cache + @cached_property def shearmodul_hill(self): return get_shearmodul_hill( - shearmodul_voigt=self.shearmodul_voigt(), - shearmodul_reuss=self.shearmodul_reuss(), + shearmodul_voigt=self.shearmodul_voigt, + shearmodul_reuss=self.shearmodul_reuss, ) - @cache + @cached_property def youngsmodul_voigt(self): return get_youngsmodul_voigt( - bulkmodul_voigt=self.bulkmodul_voigt(), - shearmodul_voigt=self.shearmodul_voigt(), + bulkmodul_voigt=self.bulkmodul_voigt, + shearmodul_voigt=self.shearmodul_voigt, ) - @cache + @cached_property def poissonsratio_voigt(self): return get_poissonsratio_voigt( - bulkmodul_voigt=self.bulkmodul_voigt(), - shearmodul_voigt=self.shearmodul_voigt(), + bulkmodul_voigt=self.bulkmodul_voigt, + shearmodul_voigt=self.shearmodul_voigt, ) - @cache + @cached_property def youngsmodul_reuss(self): return get_youngsmodul_reuss( - bulkmodul_reuss=self.bulkmodul_reuss(), - shearmodul_reuss=self.shearmodul_reuss(), + bulkmodul_reuss=self.bulkmodul_reuss, + shearmodul_reuss=self.shearmodul_reuss, ) - @cache + @cached_property def poissonsratio_reuss(self): return get_poissonsratio_reuss( - bulkmodul_reuss=self.bulkmodul_reuss(), - shearmodul_reuss=self.shearmodul_reuss(), + bulkmodul_reuss=self.bulkmodul_reuss, + shearmodul_reuss=self.shearmodul_reuss, ) - @cache + @cached_property def youngsmodul_hill(self): return get_youngsmodul_hill( - bulkmodul_hill=self.bulkmodul_hill(), shearmodul_hill=self.shearmodul_hill() + bulkmodul_hill=self.bulkmodul_hill, shearmodul_hill=self.shearmodul_hill ) - @cache + @cached_property def poissonsratio_hill(self): return get_poissonsratio_hill( - bulkmodul_hill=self.bulkmodul_hill(), shearmodul_hill=self.shearmodul_hill() + bulkmodul_hill=self.bulkmodul_hill, shearmodul_hill=self.shearmodul_hill ) - @cache + @cached_property def AVR(self): return get_AVR( - shearmodul_voigt=self.shearmodul_voigt(), - shearmodul_reuss=self.shearmodul_reuss(), + shearmodul_voigt=self.shearmodul_voigt, + shearmodul_reuss=self.shearmodul_reuss, ) - @cache + @cached_property def elastic_matrix_eigval(self): - return get_elastic_matrix_eigval(elastic_matrix=self.elastic_matrix()) + return get_elastic_matrix_eigval(elastic_matrix=self.elastic_matrix) diff --git a/atomistics/workflows/elastic/workflow.py b/atomistics/workflows/elastic/workflow.py index 5fbbba01..6f408dd7 100644 --- a/atomistics/workflows/elastic/workflow.py +++ b/atomistics/workflows/elastic/workflow.py @@ -2,7 +2,7 @@ from atomistics.shared.output import OutputElastic from atomistics.workflows.interface import Workflow -from atomistics.workflows.elastic.elastic_moduli import ElasticProperties +from atomistics.workflows.elastic.elastic_moduli import ElasticMatrixOutput from atomistics.workflows.elastic.helper import ( generate_structures_helper, analyse_structures_helper, @@ -61,6 +61,6 @@ def analyse_structures(self, output_dict, output_keys=OutputElastic.keys()): self._data["strain_energy"] = strain_energy self._data["e0"] = ene0 self._data["A2"] = A2 - return OutputElastic( - **{k: getattr(ElasticProperties, k) for k in OutputElastic.keys()} - ).get(ElasticProperties(elastic_matrix=elastic_matrix), *output_keys) + return ElasticMatrixOutput(elastic_matrix=elastic_matrix).get_output( + output_keys=output_keys + ) diff --git a/atomistics/workflows/evcurve/debye.py b/atomistics/workflows/evcurve/debye.py index 50a79bdb..0e8bb522 100644 --- a/atomistics/workflows/evcurve/debye.py +++ b/atomistics/workflows/evcurve/debye.py @@ -7,7 +7,7 @@ from atomistics.workflows.evcurve.thermo import get_thermo_bulk_model -class DebyeThermalProperties(object): +class DebyeOutputThermodynamic(OutputThermodynamic): def __init__( self, fit_dict, @@ -31,15 +31,18 @@ def __init__( ) self._constant_volume = constant_volume + @property def free_energy(self): return ( self._pes.get_free_energy_p() - self._debye_model.interpolate(volumes=self._pes.get_minimum_energy_path()) ) / self._pes.num_atoms + @property def temperatures(self): return self._temperatures + @property def entropy(self): if not self._constant_volume: return ( @@ -54,6 +57,7 @@ def entropy(self): * self._pes.get_entropy_v() ) + @property def heat_capacity(self): if not self._constant_volume: heat_capacity = ( @@ -69,6 +73,7 @@ def heat_capacity(self): ) return np.array(heat_capacity.tolist() + [np.nan, np.nan]) + @property def volumes(self): if not self._constant_volume: return self._pes.get_minimum_energy_path() @@ -229,18 +234,13 @@ def get_thermal_properties( num_steps=50, output_keys=OutputThermodynamic.keys(), ): - return OutputThermodynamic( - **{k: getattr(DebyeThermalProperties, k) for k in OutputThermodynamic.keys()} - ).get( - DebyeThermalProperties( - fit_dict=fit_dict, - masses=masses, - t_min=t_min, - t_max=t_max, - t_step=t_step, - temperatures=temperatures, - constant_volume=constant_volume, - num_steps=num_steps, - ), - *output_keys, - ) + return DebyeOutputThermodynamic( + fit_dict=fit_dict, + masses=masses, + t_min=t_min, + t_max=t_max, + t_step=t_step, + temperatures=temperatures, + constant_volume=constant_volume, + num_steps=num_steps, + ).get_output(output_keys=output_keys) diff --git a/atomistics/workflows/evcurve/workflow.py b/atomistics/workflows/evcurve/workflow.py index d081868e..88540c35 100644 --- a/atomistics/workflows/evcurve/workflow.py +++ b/atomistics/workflows/evcurve/workflow.py @@ -2,13 +2,13 @@ from ase.atoms import Atoms from collections import OrderedDict -from atomistics.shared.output import OutputEnergyVolumeCurve -from atomistics.workflows.evcurve.fit import EnergyVolumeFit -from atomistics.workflows.interface import Workflow -from atomistics.workflows.evcurve.debye import ( - get_thermal_properties, +from atomistics.shared.output import ( + OutputEnergyVolumeCurve, OutputThermodynamic, ) +from atomistics.workflows.evcurve.fit import EnergyVolumeFit +from atomistics.workflows.interface import Workflow +from atomistics.workflows.evcurve.debye import get_thermal_properties def _strain_axes( @@ -97,28 +97,35 @@ def fit_ev_curve(volume_lst, energy_lst, fit_type, fit_order): ).fit_dict -class EnergyVolumeCurveProperties: +class EnergyVolumeCurveOutputWrapper(OutputEnergyVolumeCurve): def __init__(self, fit_module): self._fit_module = fit_module + @property def volume_eq(self): return self._fit_module.fit_dict["volume_eq"] + @property def energy_eq(self): return self._fit_module.fit_dict["energy_eq"] + @property def bulkmodul_eq(self): return self._fit_module.fit_dict["bulkmodul_eq"] + @property def b_prime_eq(self): return self._fit_module.fit_dict["b_prime_eq"] + @property def volume(self): return self._fit_module.fit_dict["volume"] + @property def energy(self): return self._fit_module.fit_dict["energy"] + @property def fit_dict(self): return { k: self._fit_module.fit_dict[k] @@ -175,24 +182,16 @@ def generate_structures(self): def analyse_structures( self, output_dict, output_keys=OutputEnergyVolumeCurve.keys() ): - self._fit_dict = OutputEnergyVolumeCurve( - **{ - k: getattr(EnergyVolumeCurveProperties, k) - for k in OutputEnergyVolumeCurve.keys() - } - ).get( - EnergyVolumeCurveProperties( - fit_module=fit_ev_curve_internal( - volume_lst=get_volume_lst(structure_dict=self._structure_dict), - energy_lst=get_energy_lst( - output_dict=output_dict, structure_dict=self._structure_dict - ), - fit_type=self.fit_type, - fit_order=self.fit_order, - ) - ), - *output_keys, - ) + self._fit_dict = EnergyVolumeCurveOutputWrapper( + fit_module=fit_ev_curve_internal( + volume_lst=get_volume_lst(structure_dict=self._structure_dict), + energy_lst=get_energy_lst( + output_dict=output_dict, structure_dict=self._structure_dict + ), + fit_type=self.fit_type, + fit_order=self.fit_order, + ) + ).get_output(output_keys=output_keys) return self.fit_dict def get_volume_lst(self): diff --git a/atomistics/workflows/phonons/workflow.py b/atomistics/workflows/phonons/workflow.py index dc47ebd5..802b5e99 100644 --- a/atomistics/workflows/phonons/workflow.py +++ b/atomistics/workflows/phonons/workflow.py @@ -6,7 +6,10 @@ from phonopy.file_IO import write_FORCE_CONSTANTS import structuretoolkit -from atomistics.shared.output import OutputThermodynamic, OutputPhonons +from atomistics.shared.output import ( + OutputThermodynamic, + OutputPhonons, +) from atomistics.workflows.interface import Workflow from atomistics.workflows.phonons.helper import ( get_supercell_matrix, @@ -18,7 +21,7 @@ from atomistics.workflows.phonons.units import VaspToTHz, kJ_mol_to_eV -class PhonopyProperties(object): +class PhonopyOutput(OutputPhonons): def __init__( self, phonopy_instance, @@ -73,6 +76,7 @@ def _calc_force_constants(self): ) self._force_constants = self._phonopy.force_constants + @property def mesh_dict(self): if self._force_constants is None: self._calc_force_constants() @@ -89,11 +93,13 @@ def mesh_dict(self): self._mesh_dict = self._phonopy.get_mesh_dict() return self._mesh_dict + @property def band_structure_dict(self): if self._band_structure_dict is None: self._calc_band_structure() return self._band_structure_dict + @property def total_dos_dict(self): if self._total_dos is None: self._phonopy.run_total_dos( @@ -106,34 +112,41 @@ def total_dos_dict(self): self._total_dos = self._phonopy.get_total_dos_dict() return self._total_dos + @property def dynamical_matrix(self): if self._band_structure_dict is None: self._calc_band_structure() return self._phonopy.dynamical_matrix.dynamical_matrix + @property def force_constants(self): if self._force_constants is None: self._calc_force_constants() return self._force_constants -class PhonopyThermalProperties(object): +class PhonopyOutputThermodynamic(OutputThermodynamic): def __init__(self, phonopy_instance): self._phonopy = phonopy_instance self._thermal_properties = phonopy_instance.get_thermal_properties_dict() + @property def free_energy(self): return self._thermal_properties["free_energy"] * kJ_mol_to_eV + @property def temperatures(self): return self._thermal_properties["temperatures"] + @property def entropy(self): return self._thermal_properties["entropy"] + @property def heat_capacity(self): return self._thermal_properties["heat_capacity"] + @property def volumes(self): return np.array( [self._phonopy.unitcell.get_volume()] @@ -233,28 +246,23 @@ def analyse_structures(self, output_dict, output_keys=OutputPhonons.keys()): output_dict = output_dict["forces"] forces_lst = [output_dict[k] for k in sorted(output_dict.keys())] self.phonopy.forces = forces_lst - self._phonopy_dict = OutputPhonons( - **{k: getattr(PhonopyProperties, k) for k in OutputPhonons.keys()} - ).get( - PhonopyProperties( - phonopy_instance=self.phonopy, - dos_mesh=self._dos_mesh, - shift=None, - is_time_reversal=True, - is_mesh_symmetry=True, - with_eigenvectors=False, - with_group_velocities=False, - is_gamma_center=False, - number_of_snapshots=self._number_of_snapshots, - sigma=None, - freq_min=None, - freq_max=None, - freq_pitch=None, - use_tetrahedron_method=True, - npoints=101, - ), - *output_keys, - ) + self._phonopy_dict = PhonopyOutput( + phonopy_instance=self.phonopy, + dos_mesh=self._dos_mesh, + shift=None, + is_time_reversal=True, + is_mesh_symmetry=True, + with_eigenvectors=False, + with_group_velocities=False, + is_gamma_center=False, + number_of_snapshots=self._number_of_snapshots, + sigma=None, + freq_min=None, + freq_max=None, + freq_pitch=None, + use_tetrahedron_method=True, + npoints=101, + ).get_output(output_keys=output_keys) return self._phonopy_dict def get_thermal_properties( @@ -293,12 +301,9 @@ def get_thermal_properties( band_indices=band_indices, is_projection=is_projection, ) - return OutputThermodynamic( - **{ - k: getattr(PhonopyThermalProperties, k) - for k in OutputThermodynamic.keys() - } - ).get(PhonopyThermalProperties(phonopy_instance=self.phonopy), *output_keys) + return PhonopyOutputThermodynamic(phonopy_instance=self.phonopy).get_output( + output_keys=output_keys + ) def get_dynamical_matrix(self, npoints=101): """ diff --git a/atomistics/workflows/quasiharmonic.py b/atomistics/workflows/quasiharmonic.py index 3cd6bc5b..061bd439 100644 --- a/atomistics/workflows/quasiharmonic.py +++ b/atomistics/workflows/quasiharmonic.py @@ -1,6 +1,6 @@ import numpy as np -from atomistics.shared.output import OutputThermodynamic, OutputPhonons +from atomistics.shared.output import OutputThermodynamic from atomistics.workflows.evcurve.workflow import ( EnergyVolumeCurveWorkflow, fit_ev_curve, @@ -112,21 +112,13 @@ def get_thermal_properties( not quantum_mechanical ): # heat capacity and entropy are not yet implemented for the classical approach. output_keys = ["free_energy", "temperatures", "volumes"] - return OutputThermodynamic( - **{ - k: getattr(QuasiHarmonicThermalProperties, k) - for k in OutputThermodynamic.keys() - } - ).get( - QuasiHarmonicThermalProperties( - temperatures=temperatures, - thermal_properties_dict=tp_collect_dict, - strain_lst=strain_lst, - volumes_lst=volume_lst, - volumes_selected_lst=vol_lst, - ), - *output_keys, - ) + return QuasiHarmonicOutputThermodynamic( + temperatures=temperatures, + thermal_properties_dict=tp_collect_dict, + strain_lst=strain_lst, + volumes_lst=volume_lst, + volumes_selected_lst=vol_lst, + ).get_output(output_keys=output_keys) def _get_thermal_properties_quantum_mechanical( @@ -232,7 +224,7 @@ def _get_thermal_properties_classical( return tp_collect_dict -class QuasiHarmonicThermalProperties(object): +class QuasiHarmonicOutputThermodynamic(OutputThermodynamic): def __init__( self, temperatures, @@ -263,18 +255,23 @@ def get_property(self, thermal_property): ] ) + @property def free_energy(self): return self.get_property(thermal_property="free_energy") + @property def temperatures(self): return self._temperatures + @property def entropy(self): return self.get_property(thermal_property="entropy") + @property def heat_capacity(self): return self.get_property(thermal_property="heat_capacity") + @property def volumes(self): return self._volumes_selected_lst diff --git a/tests/test_shared_output.py b/tests/test_shared_output.py new file mode 100644 index 00000000..cda97425 --- /dev/null +++ b/tests/test_shared_output.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from atomistics.shared.output import ( + OutputStatic, + OutputMolecularDynamics, + OutputThermalExpansion, + OutputThermodynamic, + OutputEnergyVolumeCurve, + OutputElastic, + OutputPhonons, +) + + +class TestSharedOutput(TestCase): + + def test_return_none(self): + for output_cls in [ + OutputStatic, + OutputMolecularDynamics, + OutputThermalExpansion, + OutputThermodynamic, + OutputEnergyVolumeCurve, + OutputElastic, + OutputPhonons, + ]: + output_cls.__abstractmethods__ = set() + + class Demo(output_cls): + def __init__(self): + super().__init__() + self._demo = None + + dm = Demo() + + for func in dir(dm): + if func[0] != "_" and func not in ['keys', 'get_output']: + self.assertIsNone(getattr(dm, func))