Exemplo n.º 1
0
class HasStorage(ABC):
    """
    A base class for objects that use HDF5 data serialization via the `DataContainer` class.
    """
    def __init__(self, *args, **kwargs):
        self._storage = DataContainer(table_name='storage')

    @property
    def storage(self):
        return self._storage

    def to_hdf(self, hdf, group_name=None):
        """
        Serialize everything in the `storage` field to HDF5.

        Args:
            hdf (ProjectHDFio): HDF5 group object.
            group_name (str, optional): HDF5 subgroup name.
        """
        if group_name is not None:
            hdf = hdf.create_group(group_name)
        self._storage.to_hdf(hdf=hdf)

    def from_hdf(self, hdf, group_name=None):
        """
        Restore the everything in the `storage` field from an HDF5 file.

        Args:
            hdf (ProjectHDFio): HDF5 group object.
            group_name (str, optional): HDF5 subgroup name.
        """
        if group_name is not None:
            hdf = hdf.create_group(group_name)
        self._storage.from_hdf(hdf=hdf)
Exemplo n.º 2
0
class ImageJob(GenericJob):
    """
    A job type for storing and processing image data.

    TODO: Consider allowing the `data` field of each image to be saved to hdf5...

    Attributes:
        images (DistributingList): A list of `Image` objects.
    """
    def __init__(self, project, job_name):
        super(ImageJob, self).__init__(project, job_name)
        self.__name__ = "ImageJob"
        self._images = DistributingList()
        self.input = DataContainer(table_name="input")
        self.output = DataContainer(table_name="output")

    @property
    def images(self):
        return self._images

    @images.setter
    def images(self, val):
        if isinstance(val, DistributingList):
            self._images = val
        elif isinstance(val, (tuple, list, np.ndarray)):
            if not all([isinstance(obj, Image) for obj in val]):
                raise ValueError(
                    "Only `Image`-type objects can be set to the `images` attribute."
                )
            self._images = DistributingList(val)
        else:
            raise ValueError(
                "Images was expecting a list-like object, but got {}".format(
                    type(val)))

    @staticmethod
    def _get_factors(n):
        i = int(n**0.5 + 0.5)
        while n % i != 0:
            i -= 1
        return i, int(n / i)

    def plot(self,
             mask=None,
             subplots_kwargs=None,
             imshow_kwargs=None,
             hide_axes=True):
        """
        Make a simple matplotlib `imshow` plot for each of the images on a grid.

        Args:
            mask (list/numpy.ndarray): An integer index mask for selecting a subset of the images to plot.
            subplots_kwargs (dict): Keyword arguments to pass to the figure generation. (Default is None.)
            imshow_kwargs (dict): Keyword arguments to pass to the `imshow` plotting command. (Default is None.)
            hide_axes (bool): Whether to hide axis ticks and labels. (Default is True.)

        Returns:
            (matplotlib.figure.Figure): The figure the plots are in.
            (list): The axes the plot is on.
        """

        if mask is not None:
            images = self.images[mask]
        else:
            images = self.images

        subplots_kwargs = subplots_kwargs or {}
        imshow_kwargs = imshow_kwargs or {}
        nrows, ncols = self._get_factors(len(images))
        fig, axes = plt.subplots(nrows=nrows, ncols=ncols, **subplots_kwargs)
        axes = np.atleast_2d(axes)
        for n, img in enumerate(images):
            i = int(np.floor(n / ncols))
            j = n % ncols
            ax = axes[i, j]
            img.plot(ax=ax, imshow_kwargs=imshow_kwargs, hide_axes=hide_axes)

        fig.tight_layout()
        return fig, axes

    def add_image(self,
                  source,
                  metadata=None,
                  as_gray=False,
                  relative_path=True):
        """
        Add an image to the job.

        Args:
            source (str/numpy.ndarray): The filepath to the data, or the raw array of data itself.
            metadata (Metadata): The metadata associated with the source. (Default is None.)
            as_gray (bool): Whether to interpret the new data as grayscale. (Default is False.)
            relative_path (bool): Whether the path provided is relative. (Default is True, automatically converts to an
                absolute path before setting the `source` value of the image.)
        """

        if not isfile(source) and not isinstance(source, np.ndarray):
            raise ValueError(
                "Could not find a file at {}, nor is source an array.".format(
                    source))
        if isinstance(source, str) and relative_path:
            source = abspath(source)
        self.images.append(
            Image(source=source, metadata=metadata, as_gray=as_gray))

    def add_images(self, sources, metadata=None, as_gray=False):
        """
        Add multiple images to the job.

        Args:
            sources (str/list/tuple/numpy.ndarray): When a string, uses the `glob` module to look for matching files.
                When list-like, iteratively uses each element as a new source.
            metadata (Metadata): The metadata associated with all these sources. (Default is None.)
            as_gray (bool): Whether to interpret all this data as grayscale. (Default is False.)
            relative_path (bool): Whether the path provided is relative. (Default is True, automatically converts to an
                absolute path before setting the `source` value of the image.)
        """

        if isinstance(sources, str):
            for match in iglob(sources):
                self.add_image(match, metadata=metadata, as_gray=as_gray)
        elif isinstance(sources, (list, tuple, np.ndarray)):
            for source in sources:
                self.add_image(source, metadata=metadata, as_gray=as_gray)

    def run(self, run_again=False, repair=False, debug=False, run_mode=None):
        super(ImageJob, self).run(run_again=run_again,
                                  repair=repair,
                                  debug=debug,
                                  run_mode=run_mode)

    def run_static(self):
        """This is just a toy example right now."""
        self.status.running = True
        if hasattr(self.input,
                   'filter') and self.input.filter == 'brightness_filter':
            fractions = []
            cutoffs = []
            masks = []
            for img in self.images:
                frac, cut, mask = brightness_filter(img)
                fractions.append(frac)
                cutoffs.append(cut)
                masks.append(mask)
            self.output.fractions = np.array(fractions)
            self.output.cutoffs = np.array(cutoffs)
            self.output.masks = np.array(masks)

        else:
            self.logger.warning("Didn't run anything. Check input.")
        self.status.collect = True
        self.run()

    def write_input(self):
        """Must define abstract method"""
        pass

    def collect_output(self):
        """Must define abstract method"""
        self.to_hdf()

    def to_hdf(self, hdf=None, group_name=None):
        """
        Store the job in an HDF5 file.

        Args:
            hdf (ProjectHDFio): HDF5 group object - optional
            group_name (str): HDF5 subgroup name - optional
        """
        super(ImageJob, self).to_hdf(hdf=hdf, group_name=group_name)
        self.input.to_hdf(hdf=self._hdf5, group_name=None)
        self.output.to_hdf(hdf=self._hdf5, group_name=None)
        with self._hdf5.open("images") as hdf5_server:
            for n, image in enumerate(self.images):
                image.to_hdf(hdf=hdf5_server, group_name="img{}".format(n))
        self._hdf5["n_images"] = n + 1

    def from_hdf(self, hdf=None, group_name=None):
        """
        Load the Protocol from an HDF5 file.

        Args:
            hdf (ProjectHDFio): HDF5 group object - optional
            group_name (str): HDF5 subgroup name - optional
        """
        super(ImageJob, self).from_hdf(hdf=hdf, group_name=group_name)
        self.input.from_hdf(hdf=self._hdf5, group_name=None)
        self.output.from_hdf(hdf=self._hdf5, group_name=None)
        with self._hdf5.open("images") as hdf5_server:
            for n in np.arange(self._hdf5["n_images"], dtype=int):
                img = Image(source=None)
                img.from_hdf(hdf=hdf5_server, group_name="img{}".format(n))
                self.images.append(img)
Exemplo n.º 3
0
class SQSJob(AtomisticGenericJob):
    """
    Produces special quasirandom structures designed to duplicate truly random chemical distributions as well as
    possible while using finite periodic cells.

    A pyiron wrapper for the [SQS code of Dominik Gehringer](https://github.com/dgehringer/sqsgenerator).

    'Structural models used in calculations of properties of substitutionally random $A_{1-x}B_x$ alloys are usually
    constructed by randomly occupying each of the N sites of a periodic cell by A or B. We show that it is possible to
    design ‘‘special quasirandom structures’’ (SQS’s) that mimic for small N (even N=8) the first few, physically most
    relevant radial correlation functions of a perfectly random structure far better than the standard technique does.
    We demonstrate the usefulness of these SQS’s by calculating optical and thermodynamic properties of a number of
    semiconductor alloys in the local-density formalism.'
    From the abstract of Zunger, Wei, Ferreira, and Bernard, Phys. Rev. Lett. 65 (1990) 353,
    DOI:https://doi.org/10.1103/PhysRevLett.65.353

    Input:
        - mole_fractions (dict): Approximate chemical composition for the output structure(s), using chemical symbols
            as the keys and floats as the values. Values should sum to 1, but within reason will be automatically
            adjusted to accommodate the number of atoms in the provided structure (adjustments printed to standard out).
            Vacancies can also be included using the key '0'.
        - weights (list/numpy.ndarray): Specifies the weights of the individual shells. (Default is None, which uses the
            inverse of the shell number for the weight, i.e.
            [1, 0.5, 0.3333333333, 0.25, 0.2, 0.166666667, 0.1428571429].)
        - objective (float): Specifies the value the objective functions. The program tries to reach the specified
            objective function. (Default is 0.)
        - iterations (int): How many iterations to make searching for the most special quasirandom structure using a
            random shuffling procedure. (Default is 1e6)
        - n_output_structures (int): How many different SQS structures to return (in decreasing special
            quasirandomness). (Default is 1.)

    Example:
        Case I: Get SQS for a given mole fraction:

        >>> job = SQSJob(job_name='sqs')
        >>> job.structure = structure
        >>> job.input.mole_fractions = {'Al': 0.8, 'Ni':0.2}
        >>> job.run()
        
        Case II: Get SQS for a given structure already containing at least 2 elements:

        >>> job = SQSJob(job_name='sqs')
        >>> job.structure = structure
        >>> job.run()

        In Case II, if the mole fractions will be overwritten if you specify the values (like in Case I)
    """

    publication = {
        "sqs": {
            "method": {
                "title":
                "Special quasirandom structures",
                "author": [
                    "Zunger, A.", "Wei, S.-H.", "Ferreira, L.G.",
                    "Bernard, J.E."
                ],
                "journal":
                "Phys. Rev. Lett.",
                "volume":
                "65",
                "issue":
                "3",
                "pages":
                "353",
                "numpages":
                "0",
                "month":
                "July",
                "publisher":
                "American Physical Society",
                "doi":
                "10.1103/PhysRevLett.65.353",
                "url":
                "https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.65.353",
            }
        }
    }

    @import_alarm
    def __init__(self, project, job_name):
        super(SQSJob, self).__init__(project, job_name)
        # self.input = InputList(table_name='input')
        self.input = DataContainer(table_name='custom_dict')
        self.input.mole_fractions = dict()
        self.input.weights = None
        self.input.objective = 0.0
        self.input.iterations = 1e6
        self.input.n_output_structures = 1
        self._python_only_job = True
        self._lst_of_struct = []
        self.__hdf_version__ = "0.2.0"
        self.__name__ = "SQSJob"
        s.publication_add(self.publication)

    @property
    def list_of_structures(self):
        return self._lst_of_struct

    def validate_ready_to_run(self):
        super(SQSJob, self).validate_ready_to_run()
        if len(self.input.mole_fractions) == 0:
            chem = np.unique(self.structure.get_chemical_symbols(),
                             return_counts=True)
            self.input.mole_fractions = dict(
                zip(chem[0], chem[1] / np.sum(chem[1])))
        if len(self.input.mole_fractions) == 1:
            raise ValueError('There must be at least two chemical elements')

    def db_entry(self):
        """
        Generate the initial database entry

        Returns:
            (dict): db_dict
        """
        db_dict = super(SQSJob, self).db_entry()
        if self.structure:
            struct_len = len(self.structure)
            mole_fractions = {
                k: v
                for k, v in self.input.mole_fractions.items()
            }
            el_lst = sorted(mole_fractions.keys())
            atom_number_lst = [
                int(np.round(mole_fractions[el] * struct_len)) for el in el_lst
            ]
            db_dict["ChemicalFormula"] = "".join(
                [e + str(n) for e, n in zip(el_lst, atom_number_lst)])
        return db_dict

    def list_structures(self):
        if self.status.finished:
            return self._lst_of_struct
        else:
            return []

    def get_structure(self, iteration_step=-1, wrap_atoms=True):
        """
        Gets the structure from a given iteration step of the simulation (MD/ionic relaxation). For static calculations
        there is only one ionic iteration step
        Args:
            iteration_step (int): Step for which the structure is requested
            wrap_atoms (bool): True if the atoms are to be wrapped back into the unit cell
        Returns:
            pyiron.atomistics.structure.atoms.Atoms: The required structure
        """
        return self.list_structures()[iteration_step]

    # This function is executed
    def run_static(self):
        self._lst_of_struct, decmp, iterations, cycle_time = get_sqs_structures(
            structure=self.structure,
            mole_fractions={
                k: v
                for k, v in self.input.mole_fractions.items()
            },
            weights=self.input.weights,
            objective=self.input.objective,
            iterations=self.input.iterations,
            output_structures=self.input.n_output_structures,
            num_threads=self.server.cores)
        for i, structure in enumerate(self._lst_of_struct):
            with self.project_hdf5.open("output/structures/structure_" +
                                        str(i)) as h5:
                structure.to_hdf(h5)
        with self.project_hdf5.open("output") as h5:
            h5["decmp"] = decmp
            h5["cycle_time"] = cycle_time
            h5["iterations"] = iterations
        self.status.finished = True
        self.project.db.item_update(self._runtime(), self.job_id)

    def to_hdf(self, hdf=None, group_name=None):
        super().to_hdf(hdf=hdf, group_name=group_name)
        self._structure_to_hdf()
        with self.project_hdf5.open("input") as h5in:
            self.input.to_hdf(h5in)

    def from_hdf(self, hdf=None, group_name=None):
        super().from_hdf(hdf=hdf, group_name=group_name)
        self._structure_from_hdf()
        self._backwards_compatible_input_from_hdf()
        with self.project_hdf5.open("output/structures") as hdf5_output:
            structure_names = hdf5_output.list_groups()
        for group in structure_names:
            with self.project_hdf5.open("output/structures/" +
                                        group) as hdf5_output:
                self._lst_of_struct.append(Atoms().from_hdf(hdf5_output))

    def _backwards_compatible_input_from_hdf(self):
        if "HDF_VERSION" in self.project_hdf5.list_nodes():
            version = self.project_hdf5["HDF_VERSION"]
        else:
            version = "0.1.0"

        if version == "0.1.0":
            with self.project_hdf5.open("input") as hdf5_input:
                gp = GenericParameters(table_name="custom_dict")
                gp.from_hdf(hdf5_input)
                for k in gp.keys():
                    self.input[k] = gp[k]
        elif version == "0.2.0":
            with self.project_hdf5.open("input") as hdf5_input:
                self.input.from_hdf(hdf5_input)
        else:
            raise ValueError("Cannot handle hdf version: {}".format(version))
Exemplo n.º 4
0
class Calphy(GenericJob):
    """
    Class to set up and run calphy jobs for calculation of free energies using LAMMPS.

    An input structure (:attr:`structure`) and interatomic potential (:attr:`potential`) are necessary input options. The additional input options such as the temperature and pressure are specified in the :meth:`.calc_free_energy` method. Depending on the input parameters, a corresponding calculation mode is selected. Further input options can be accessed through :attr:`input.md` and :attr:`input.tolerance`.

    An example which calculates the free energy of Cu using an interatomic potential:

    ```python
    job.structure = pr.create.structure.ase.bulk('Cu', cubic=True).repeat(5)
    job.potential = "2001--Mishin-Y--Cu-1--LAMMPS--ipr1"
    job.calc_free_energy(temperature=1100, pressure=0, reference_phase="solid")
    job.run()
    ```

    In order to calculate the free energy of the liquid phase, the `reference_phase` should be set to `liquid`.

    The different modes can be selected as follows:

    For free energy at a given temperature and pressure:

    ```python
    job.calc_free_energy(temperature=1100, pressure=0, reference_phase="solid")
    ```

    Alternatively, :func:`calc_mode_fe` can be used.

    To obtain the free energy between a given temperature range (temperature scaling):

    ```python
    job.calc_free_energy(temperature=[1100, 1400], pressure=0, reference_phase="solid")
    ```

    Alternatively, :func:`calc_mode_ts` can be used.

    For free energy between a given pressure range (pressure scaling)

    ```python
    job.calc_free_energy(temperature=1000, pressure=[0, 100000], reference_phase="solid")
    ```

    Alternatively, :func:`calc_mode_pscale` can be used.

    To obtain the free energy difference between two interatomic potentials (alchemy/upsampling)

    ```python
    job.potential = ["2001--Mishin-Y--Cu-1--LAMMPS--ipr1", "1986--Foiles-S-M--Cu--LAMMPS--ipr1"]
    job.calc_free_energy(temperature=1100, pressure=0, reference_phase="solid")
    job.run()
    ```

    Alternatively, :func:`calc_mode_alchemy` can be used.

    The way `pressure` is specified determines how the barostat affects the system. For isotropic pressure control:

    ```python
    job.calc_free_energy(temperature=[1100, 1400], pressure=0, reference_phase="solid")
    ```

    For anisotropic pressure control:

    ```python
    job.calc_free_energy(temperature=[1100, 1400], pressure=[0, 0, 0], reference_phase="solid")
    ```

    To constrain the lattice:

    ```python
    job.calc_free_energy(temperature=[1100, 1400], pressure=None, reference_phase="solid")
    ```

    In addition the boolean option :attr:`input.npt` can be used to determine the MD ensemble. If True, temperature integration and alchemy/upsampling are carried out in the NPT ensemble. If False, the NVT ensemble is employed.

    After the calculation is over, the various output options can be accessed through `job.output`.
    """
    def __init__(self, project, job_name):
        super().__init__(project, job_name)
        self.input = DataContainer(inputdict, table_name="inputdata")
        self._potential_initial = None
        self._potential_final = None
        self.input.potential_initial_name = None
        self.input.potential_final_name = None
        self.input.structure = None
        self.output = DataContainer(table_name="output")
        self._data = None
        self.input._pot_dict_initial = None
        self.input._pot_dict_final = None

    def set_potentials(self, potential_filenames: Union[list, str]):
        """
        Set the interatomic potential from a given name

        Args:
            potential_filenames (list, str): list of filenames

        Returns:
            None
        """
        if not isinstance(potential_filenames, list):
            potential_filenames = [potential_filenames]
        if len(potential_filenames) > 0:
            if isinstance(potential_filenames[0], pd.DataFrame):
                potential = potential_filenames[0]
                self.input._pot_dict_initial = potential  # .to_dict()
            else:
                potential = LammpsPotentialFile().find_by_name(
                    potential_filenames[0])
                self.input.potential_initial_name = potential_filenames[0]
            self._potential_initial = LammpsPotential()
            self._potential_initial.df = potential

        if len(potential_filenames) > 1:
            if isinstance(potential_filenames[1], pd.DataFrame):
                potential = potential_filenames[1]
                self.input._pot_dict_final = potential  # .to_dict()
            else:
                potential = LammpsPotentialFile().find_by_name(
                    potential_filenames[1])
                self.input.potential_final_name = potential_filenames[1]
            self._potential_final = LammpsPotential()
            self._potential_final.df = potential
        if len(potential_filenames) > 2:
            raise ValueError("Maximum two potentials can be provided")

    def get_potentials(self) -> List[str]:
        """
        Return the interatomic potentials

        Args:
            None

        Returns:
            list of str: list of interatomic potentials
        """
        if self._potential_final is None:
            return [self._potential_initial.df]
        else:
            return [self._potential_initial.df, self._potential_final.df]

    def copy_pot_files(self):
        """
        Copy potential files to the working directory

        Args:
            None

        Returns:
            None
        """
        if self._potential_initial is not None:
            self._potential_initial.copy_pot_files(self.working_directory)
        if self._potential_final is not None:
            self._potential_final.copy_pot_files(self.working_directory)

    def _prepare_pair_styles(self) -> Tuple[List, List]:
        """
        Prepare pair style and pair coeff

        Args:
            None

        Returns:
            list: pair style and pair coeff
        """
        pair_style = []
        pair_coeff = []
        if self._potential_initial is not None:
            pair_style.append(self._potential_initial.df["Config"].to_list()[0]
                              [0].strip().split()[1:][0])
            pair_coeff.append(
                " ".join(self._potential_initial.df["Config"].to_list()[0]
                         [1].strip().split()[1:]))
        if self._potential_final is not None:
            pair_style.append(self._potential_final.df["Config"].to_list()[0]
                              [0].strip().split()[1:][0])
            pair_coeff.append(
                " ".join(self._potential_final.df["Config"].to_list()[0]
                         [1].strip().split()[1:]))
        return pair_style, pair_coeff

    def _potential_from_hdf(self):
        """
        Recreate the potential from filename stored in hdf5

        Args:
            None

        Returns:
            None
        """
        filenames = []
        if self.input.potential_initial_name is not None:
            filenames.append(self.input.potential_initial_name)
        elif self.input._pot_dict_initial is not None:
            filenames.append(pd.DataFrame(data=self.input._pot_dict_initial))
        if self.input.potential_final_name is not None:
            filenames.append(self.input.potential_final_name)
        elif self.input._pot_dict_final is not None:
            filenames.append(pd.DataFrame(data=self.input._pot_dict_final))

        self.set_potentials(filenames)

    @property
    def potential(self):
        potentials = self.get_potentials()
        if len(potentials) == 1:
            return potentials[0]
        return potentials

    @potential.setter
    def potential(self, potential_filenames):
        self.set_potentials(potential_filenames)

    @property
    def structure(self):
        return self.input.structure

    @structure.setter
    def structure(self, val):
        self.input.structure = val

    def view_potentials(self) -> List:
        """
        View a list of available interatomic potentials

        Args:
            None

        Returns:
            list: list of available potentials
        """
        if not self.structure:
            raise ValueError("please assign a structure first")
        else:
            list_of_elements = set(self.structure.get_chemical_symbols())
        list_of_potentials = LammpsPotentialFile().find(list_of_elements)
        if list_of_potentials is not None:
            return list_of_potentials
        else:
            raise TypeError(
                "No potentials found for this kind of structure: ",
                str(list_of_elements),
            )

    def structure_to_lammps(self):
        """
        Convert structure to LAMMPS structure

        Args:
            None

        Returns:
            list: pair style and pair coeff
        """
        prism = UnfoldingPrism(self.input.structure.cell)
        lammps_structure = self.input.structure.copy()
        lammps_structure.set_cell(prism.A)
        lammps_structure.positions = np.matmul(self.input.structure.positions,
                                               prism.R)
        return lammps_structure

    def write_structure(self, structure, file_name: str,
                        working_directory: str):
        """
        Write structure to file

        Args:
            structure: input structure
            file_name (str): output file name
            working_directory (str): output working directory

        Returns:
            None
        """
        lmp_structure = LammpsStructure()
        lmp_structure.potential = self._potential_initial
        lmp_structure.el_eam_lst = set(structure.get_chemical_symbols())
        lmp_structure.structure = self.structure_to_lammps()
        lmp_structure.write_file(file_name=file_name, cwd=working_directory)

    def determine_mode(self):
        """
        Determine the calculation mode

        Args:
            None

        Returns:
            None
        """
        if len(self.get_potentials()) == 2:
            self.input.mode = "alchemy"
            self.input.reference_phase = "alchemy"
        elif isinstance(self.input.pressure, list):
            if len(self.input.pressure) == 2:
                self.input.mode = "pscale"
        elif isinstance(self.input.temperature, list):
            if len(self.input.temperature) == 2:
                self.input.mode = "ts"
        else:
            self.input.mode = "fe"
        # if mode was not set, raise Error
        if self.input.mode is None:
            raise RuntimeError("Could not determine the mode")

    def write_input(self):
        """
        Write input for calphy calculation

        Args:
            None

        Returns:
            None
        """
        # now prepare the calculation
        calc = Calculation()
        for key in inputdict.keys():
            if key not in ["md", "tolerance"]:
                setattr(calc, key, self.input[key])
        for key in inputdict["md"].keys():
            setattr(calc.md, key, self.input["md"][key])
        for key in inputdict["tolerance"].keys():
            setattr(calc.tolerance, key, self.input["tolerance"][key])

        file_name = "conf.data"
        self.write_structure(self.structure, file_name, self.working_directory)
        calc.lattice = os.path.join(self.working_directory, "conf.data")

        self.copy_pot_files()
        pair_style, pair_coeff = self._prepare_pair_styles()
        calc._fix_potential_path = False
        calc.pair_style = pair_style
        calc.pair_coeff = pair_coeff

        calc.element = list(np.unique(self.structure.get_chemical_symbols()))
        calc.mass = list(np.unique(self.structure.get_masses()))

        calc.queue.cores = self.server.cores
        self.calc = calc

    def calc_mode_fe(
        self,
        temperature: float = None,
        pressure: Union[list, float, None] = None,
        reference_phase: str = None,
        n_equilibration_steps: int = 15000,
        n_switching_steps: int = 25000,
        n_print_steps: int = 0,
        n_iterations: int = 1,
    ):
        """
        Calculate free energy at given conditions

        Args:
            None

        Returns:
            None
        """
        if temperature is None:
            raise ValueError("provide a temperature")
        self.input.temperature = temperature
        self.input.pressure = pressure
        self.input.reference_phase = reference_phase
        self.input.n_equilibration_steps = n_equilibration_steps
        self.input.n_switching_steps = n_switching_steps
        self.input.n_print_steps = n_print_steps
        self.input.n_iterations = n_iterations
        self.input.mode = "fe"

    def calc_mode_ts(
        self,
        temperature: float = None,
        pressure: Union[list, float, None] = None,
        reference_phase: str = None,
        n_equilibration_steps: int = 15000,
        n_switching_steps: int = 25000,
        n_print_steps: int = 0,
        n_iterations: int = 1,
    ):
        """
        Calculate free energy between given temperatures

        Args:
            None

        Returns:
            None
        """
        if temperature is None:
            raise ValueError("provide a temperature")
        self.input.temperature = temperature
        self.input.pressure = pressure
        self.input.reference_phase = reference_phase
        self.input.n_equilibration_steps = n_equilibration_steps
        self.input.n_switching_steps = n_switching_steps
        self.input.n_print_steps = n_print_steps
        self.input.n_iterations = n_iterations
        self.input.mode = "ts"

    def calc_mode_alchemy(
        self,
        temperature: float = None,
        pressure: Union[list, float, None] = None,
        reference_phase: str = None,
        n_equilibration_steps: int = 15000,
        n_switching_steps: int = 25000,
        n_print_steps: int = 0,
        n_iterations: int = 1,
    ):
        """
        Perform upsampling/alchemy between two interatomic potentials

        Args:
            None

        Returns:
            None
        """
        if temperature is None:
            raise ValueError("provide a temperature")
        self.input.temperature = temperature
        self.input.pressure = pressure
        self.input.reference_phase = reference_phase
        self.input.n_equilibration_steps = n_equilibration_steps
        self.input.n_switching_steps = n_switching_steps
        self.input.n_print_steps = n_print_steps
        self.input.n_iterations = n_iterations
        self.input.mode = "alchemy"

    def calc_mode_pscale(
        self,
        temperature: float = None,
        pressure: Union[list, float, None] = None,
        reference_phase: str = None,
        n_equilibration_steps: int = 15000,
        n_switching_steps: int = 25000,
        n_print_steps: int = 0,
        n_iterations: int = 1,
    ):
        """
        Calculate free energy between two given pressures

        Args:
            None

        Returns:
            None
        """
        if temperature is None:
            raise ValueError("provide a temperature")
        self.input.temperature = temperature
        self.input.pressure = pressure
        self.input.reference_phase = reference_phase
        self.input.n_equilibration_steps = n_equilibration_steps
        self.input.n_switching_steps = n_switching_steps
        self.input.n_print_steps = n_print_steps
        self.input.n_iterations = n_iterations
        self.input.mode = "pscale"

    def calc_free_energy(
        self,
        temperature: float = None,
        pressure: Union[list, float, None] = None,
        reference_phase: str = None,
        n_equilibration_steps: int = 15000,
        n_switching_steps: int = 25000,
        n_print_steps: int = 0,
        n_iterations: int = 1,
    ):
        """
        Calculate free energy at given conditions

        Args:
            None

        Returns:
            None
        """
        if temperature is None:
            raise ValueError("provide a temperature")
        self.input.temperature = temperature
        self.input.pressure = pressure
        self.input.reference_phase = reference_phase
        self.input.n_equilibration_steps = n_equilibration_steps
        self.input.n_switching_steps = n_switching_steps
        self.input.n_print_steps = n_print_steps
        self.input.n_iterations = n_iterations
        self.determine_mode()

    def run_static(self):
        self.status.running = True
        if self.input.reference_phase == "alchemy":
            job = Alchemy(calculation=self.calc,
                          simfolder=self.working_directory)
        elif self.input.reference_phase == "solid":
            job = Solid(calculation=self.calc,
                        simfolder=self.working_directory)
        elif self.input.reference_phase == "liquid":
            job = Liquid(calculation=self.calc,
                         simfolder=self.working_directory)
        else:
            raise ValueError("Unknown reference state")

        if self.input.mode == "alchemy":
            routine_alchemy(job)
        elif self.input.mode == "fe":
            routine_fe(job)
        elif self.input.mode == "ts":
            routine_ts(job)
        elif self.input.mode == "pscale":
            routine_pscale(job)
        else:
            raise ValueError("Unknown mode")
        self._data = job.report
        # save conc for later use
        self.input.concentration = job.concentration
        del self._data["input"]
        self.status.collect = True
        self.run()

    def collect_general_output(self):
        """
        Collect the output from calphy

        Args:
            None

        Returns:
            None
        """
        if self._data is not None:
            if "spring_constant" in self._data["average"].keys():
                self.output.spring_constant = self._data["average"][
                    "spring_constant"]
            self.output.energy_free = self._data["results"]["free_energy"]
            self.output.energy_free_error = self._data["results"]["error"]
            self.output.energy_free_reference = self._data["results"][
                "reference_system"]
            self.output.energy_work = self._data["results"]["work"]
            self.output.temperature = self.input.temperature
            f_ediff, b_ediff, flambda, blambda = self.collect_ediff()
            self.output.forward_energy_diff = list(f_ediff)
            self.output.backward_energy_diff = list(b_ediff)
            self.output.forward_lambda = list(flambda)
            self.output.backward_lambda = list(blambda)
            if self.input.mode == "ts":
                datfile = os.path.join(self.working_directory,
                                       "temperature_sweep.dat")
                t, fe, ferr = np.loadtxt(datfile,
                                         unpack=True,
                                         usecols=(0, 1, 2))
                self.output.energy_free = np.array(fe)
                self.output.energy_free_error = np.array(ferr)
                self.output.temperature = np.array(t)

            elif self.input.mode == "pscale":
                datfile = os.path.join(self.working_directory,
                                       "pressure_sweep.dat")
                p, fe, ferr = np.loadtxt(datfile,
                                         unpack=True,
                                         usecols=(0, 1, 2))
                self.output.energy_free = np.array(fe)
                self.output.energy_free_error = np.array(ferr)
                self.output.pressure = np.array(p)

    def collect_ediff(self):
        """
        Calculate the energy difference between reference system and system of interest

        Args:
            None

        Returns:
            None
        """
        f_ediff = []
        b_ediff = []

        for i in range(1, self.input.n_iterations + 1):
            fwdfilename = os.path.join(self.working_directory,
                                       "forward_%d.dat" % i)
            bkdfilename = os.path.join(self.working_directory,
                                       "backward_%d.dat" % i)
            nelements = self.calc.n_elements

            if self.input.reference_phase == "solid":
                fdui = np.loadtxt(fwdfilename,
                                  unpack=True,
                                  comments="#",
                                  usecols=(0, ))
                bdui = np.loadtxt(bkdfilename,
                                  unpack=True,
                                  comments="#",
                                  usecols=(0, ))

                fdur = np.zeros(len(fdui))
                bdur = np.zeros(len(bdui))

                for i in range(nelements):
                    fdur += self.input.concentration[i] * np.loadtxt(
                        fwdfilename,
                        unpack=True,
                        comments="#",
                        usecols=(i + 1, ))
                    bdur += self.input.concentration[i] * np.loadtxt(
                        bkdfilename,
                        unpack=True,
                        comments="#",
                        usecols=(i + 1, ))

                flambda = np.loadtxt(fwdfilename,
                                     unpack=True,
                                     comments="#",
                                     usecols=(nelements + 1, ))
                blambda = np.loadtxt(bkdfilename,
                                     unpack=True,
                                     comments="#",
                                     usecols=(nelements + 1, ))
            else:
                fdui, fdur, flambda = np.loadtxt(fwdfilename,
                                                 unpack=True,
                                                 comments="#",
                                                 usecols=(0, 1, 2))
                bdui, bdur, blambda = np.loadtxt(bkdfilename,
                                                 unpack=True,
                                                 comments="#",
                                                 usecols=(0, 1, 2))

            f_ediff.append(fdui - fdur)
            b_ediff.append(bdui - bdur)

        return f_ediff, b_ediff, flambda, blambda

    def collect_output(self):
        self.collect_general_output()
        self.to_hdf()

    def to_hdf(self, hdf=None, group_name=None):
        super().to_hdf(hdf=hdf, group_name=group_name)
        self.input.to_hdf(self.project_hdf5)
        self.output.to_hdf(self.project_hdf5)

    def from_hdf(self, hdf=None, group_name=None):
        super().from_hdf(hdf=hdf, group_name=group_name)
        self.input.from_hdf(self.project_hdf5)
        self.output.from_hdf(self.project_hdf5)
        self._potential_from_hdf()