Skip to content

Commit

Permalink
Overhaul of documentation/comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ml-evs committed Mar 9, 2018
1 parent 6a8a8a1 commit a4241c8
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 271 deletions.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# **ilustrado**
# `ilustrado`

ilustrado is a simple genetic algorithm created to run primarily on results from incomplete random structure searches.
## Summary

The main use case is for situations where the search space is prohibitively large to assure sufficient sampling. ilustrado makes extensive use of the [matador](https://bitbucket.org/me388/matador) API.
`ilustrado` is a Python package that implements a highly-customisable massively-parallel genetic algorithm for *ab initio* crystal structure prediction (CSP), with a focus on mapping out compositional phase diagrams. The aim of `ilustrado` was to provide a method of extending CSP results generated with random structure searching (AIRSS) or extrapolating from known chemically-relevant systems (species-swapping/prototyping).

Written by [Matthew Evans](http://www.tcm.phy.cam.ac.uk/~me388) [email protected] (2017).
The API is [fully-documented](http://www.tcm.phy.cam.ac.uk/~me388/ilustrado) and the source code can be found on [BitBucket](https://bitbucket.org/me388/ilustrado). `ilustrado` makes extensive use of the [matador](https://tcm.phy.cam.ac.uk/~me388/matador) API and interfaces with [CASTEP](http://www.castep.org/) for DFT-level relaxations. Written by [Matthew Evans](http://www.tcm.phy.cam.ac.uk/~me388) ([email protected]).

By default, fitnesses are evaluated as the distance from a binary or ternary convex hull that is passed as input. Duplicate structures are filtered out post-relaxation based on pair distribution function overlap. The standard mutation operators are implemented (cell and position noise, vacancy, atom permutation/transmutation) and additionally a Voronoi-based mutation, whereby one elemental sublattice is removed and reinstated (with a randomly modified number of lattice points) at the Voronoi points of the remaining overall crystal. Crossover is performed with the standard cut-and-splice method to ensure transferrability over many types of material systems. Several physical constraints (minimum atomic separations, densities, cell shapes) are applied to the trial structures before relaxation to improve efficiency. In order to maintain population diversity as the simulation progresses, the user can optionally disfavour frequently-visited regions of composition space. The entrypoint is a Python script that creates an `ArtificialSelector` object with the desired parameters (documented [here](http://www.tcm.phy.cam.ac.uk/~me388/ilustrado/ilustrado.html#ilustrado.ilustrado.ArtificialSelector)). Many examples can be found in `examples/`.

There are two `compute_mode`s in which to run `ilustrado` (examples of both can be found in `examples/`):
- `compute_mode='direct'` involves one `ilustrado` processes spawning `mpirun` calls either on local or remote partitions (i.e. either a node list is passed for running on a local cluster, or `ilustrado` itself is submitted as a HPC job). In this case, the user must manually restart the GA when the job finishes.
- `compute_mode='slurm'` performs the GA in interruptible steps; submitting `ilustrado` as a job will lead to the submission of many slurm array jobs for the relaxation, and a dependency job that re-runs the `ilustrado` process to check on the relaxations. In this case, the user only needs to submit one job.

## New in v0.3b:

- `sandbagging` of composition space
- `compute_mode='slurm'` that makes use of array jobs for "infinite" horizontal scalability
- improved documentation and examples

## API Docs
31 changes: 25 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
'm2r',
'sphinxcontrib.napoleon',
'sphinx.ext.mathjax']

# Add any paths that contain templates here, relative to this directory.
Expand All @@ -49,17 +51,17 @@

# General information about the project.
project = 'ilustrado'
copyright = '2017, Matthew Evans'
copyright = '2017-2018, Matthew Evans'
author = 'Matthew Evans'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.2b'
version = '0.3b'
# The full version, including alpha/beta/rc tags.
release = '0.2b'
release = '0.3b'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand All @@ -71,14 +73,32 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '*test*', 'setup.py']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'tests', '*tests*', 'setup.py']
exclude_path = ['ilustrado/tests']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False

# -- Napolean options -----------------------------------------------------

# Napoleon settings
#
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = False
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = False
napoleon_use_rtype = False
napoleon_use_keyword = False


# -- Options for HTML output ----------------------------------------------

Expand All @@ -87,13 +107,12 @@
#
html_theme = 'sphinx_rtd_theme'
html_them_path = [sphinx_rtd_theme.get_html_theme_path()]
html_them_path = [sphinx_rtd_theme.get_html_theme_path()]
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
Expand Down
10 changes: 1 addition & 9 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to ilustrado's documentation!
===================================

ilustrado is genetic algorithm code for crystal structure prediction.

Written by `Matthew Evans <www.tcm.phy.cam.ac.uk/~me388>`_, [email protected] (2017).

Indices and tables
==================
.. mdinclude:: ../README.md

* :ref:`genindex`
* :ref:`modindex`
Expand Down
89 changes: 45 additions & 44 deletions ilustrado/adapt.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# coding: utf-8
""" This file contains a wrapper for mutation and crossover. """
from .mutate import mutate
from .crossover import crossover
from .util import strip_useless
from matador.utils.cell_utils import cart2volume, frac2cart, cart2abc
from scipy.spatial.distance import cdist
from itertools import product
from traceback import print_exc
import numpy as np
import logging
import numpy as np
from scipy.spatial.distance import cdist
from matador.utils.cell_utils import cart2volume, frac2cart, cart2abc
from .mutate import mutate
from .crossover import crossover
from .util import strip_useless


def adapt(possible_parents, mutation_rate, crossover_rate,
Expand All @@ -18,24 +18,24 @@ def adapt(possible_parents, mutation_rate, crossover_rate,
""" Take a list of possible parents and randomly adapt
according to given mutation weightings.
Input:
Parameters:
| possible_parents : list(dict), list of all breeding stock,
| mutation_rate : float, rate of mutations relative to crossover,
| crossover_rate : float, see above.
possible_parents (list(dict)) : list of all breeding stock,
mutation_rate (float) : rate of mutations relative to crossover,
crossover_rate (float) : see above.
Args:
Keyword Arguments:
| mutations : list(str), list of desired mutations to choose from (as strings),
| max_num_mutations : int, rand(1, this) mutations will be performed,
| max_num_atoms : int, any structures with more than this many atoms will be filtered out.
| structure_filter : fn(doc), custom filter to pass to check_feasible.
| minsep_dict : dict, dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
mutations (list(str)) : list of desired mutations to choose from (as strings),
max_num_mutations (int) : rand(1, this) mutations will be performed,
max_num_atoms (int) : any structures with more than this many atoms will be filtered out.
structure_filter (fn(dict)) : custom filter to pass to check_feasible.
minsep_dict (dict) : dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
Returns:
| newborn : the mutated/newborn structure.
dict: the mutated/newborn structure.
"""
total_rate = mutation_rate + crossover_rate
Expand All @@ -53,17 +53,17 @@ def adapt(possible_parents, mutation_rate, crossover_rate,
from .mutate import nudge_positions, null_nudge_positions, permute_atoms
from .mutate import random_strain, vacancy, voronoi_shuffle
for mutation in mutations:
if mutation is 'nudge_positions':
if mutation == 'nudge_positions':
_mutations.append(nudge_positions)
elif mutation is 'null_nudge_positions':
elif mutation == 'null_nudge_positions':
_mutations.append(null_nudge_positions)
elif mutation is 'permute_atoms':
elif mutation == 'permute_atoms':
_mutations.append(permute_atoms)
elif mutation is 'random_strain':
elif mutation == 'random_strain':
_mutations.append(random_strain)
elif mutation is 'voronoi':
elif mutation == 'voronoi':
_mutations.append(voronoi_shuffle)
elif mutation is 'vacancy':
elif mutation == 'vacancy':
_mutations.append(vacancy)
else:
_mutations = None
Expand Down Expand Up @@ -135,22 +135,22 @@ def check_feasible(mutant, parents, max_num_atoms, structure_filter=None, minsep
* ensure number of atomic types is maintained,
* any custom filter is obeyed.
Input:
Parameters:
| mutant : dict, matador doc containing new structure.
| parents : list(dict), list of doc(s) containing parent structures.
| max_num_atoms : int, any structures with more than this many atoms will be filtered out.
mutant (dict) : matador doc containing new structure.
parents (list(dict)) : list of doc(s) containing parent structures.
max_num_atoms (int) : any structures with more than this many atoms will be filtered out.
Args:
Keyword Arguments:
| structure_filter : fn, any function that takes a matador document and returns True or False.
| minsep_dict : dict, dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
structure_filter (fn) : any function that takes a matador document and returns True or False.
minsep_dict (dict) : dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
Returns:
| feasibility : bool, determined by points above.
bool: True if structure is feasible, else False.
"""
# first check the structure filter
Expand All @@ -161,10 +161,11 @@ def check_feasible(mutant, parents, max_num_atoms, structure_filter=None, minsep
print(message)
return False
# check number of atoms
if 'num_atoms' not in mutant or 'num_atoms' != len(mutant['atom_types']):
if 'num_atoms' not in mutant or mutant['num_atoms'] != len(mutant['atom_types']):
mutant['num_atoms'] = len(mutant['atom_types'])
if mutant['num_atoms'] > max_num_atoms:
message = 'Mutant with {} contained too many atoms ({} vs {}).'.format(', '.join(mutant['mutations']), mutant['num_atoms'], max_num_atoms)
message = ('Mutant with {} contained too many atoms ({} vs {}).'
.format(', '.join(mutant['mutations']), mutant['num_atoms'], max_num_atoms))
logging.debug(message)
if debug:
print(message)
Expand Down Expand Up @@ -216,15 +217,15 @@ def check_feasible(mutant, parents, max_num_atoms, structure_filter=None, minsep
def minseps_feasible(mutant, minsep_dict=None, debug=False):
""" Check if minimum separations between species of atom are satisfied by mutant.
Input:
Parameters:
| mutant : dict, trial mutated structure
| minsep_dict : dict, dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
mutant (dict) : trial mutated structure
minsep_dict (dict) : dictionary containing element-specific minimum separations, e.g.
{('K', 'K'): 2.5, ('K', 'P'): 2.0}.
Returns:
| True if minseps are greater than desired value else False.
bool: True if minseps are greater than desired value else False.
"""
elems = set(mutant['atom_types'])
Expand All @@ -249,7 +250,7 @@ def minseps_feasible(mutant, minsep_dict=None, debug=False):
import periodictable
for elem_key in elem_pairs:
if elem_key not in minsep_dict:
minsep_dict[elem_key] = sum([periodictable.elements.symbol(elem).covalent_radius for elem in elem_key]) / 2.0
minsep_dict[elem_key] = sum([periodictable.elements.symbol(elem).covalent_radius for elem in elem_key]) / 2.

if 'positions_abs' not in mutant:
mutant['positions_abs'] = frac2cart(mutant['lattice_cart'], mutant['positions_frac'])
Expand All @@ -261,10 +262,10 @@ def minseps_feasible(mutant, minsep_dict=None, debug=False):
trans += np.asarray(mutant['lattice_cart'][ind]) * multi
distances = cdist(poscart+trans, poscart)
distances = np.ma.masked_where(distances < 1e-12, distances)
for i in range(len(distances)):
for j in range(len(distances[i])):
for i, dists in enumerate(distances):
for j, dist in enumerate(dists):
min_dist = minsep_dict[tuple(sorted([mutant['atom_types'][i], mutant['atom_types'][j]]))]
if distances[i][j] < min_dist:
if dist < min_dist:
message = 'Mutant with {} failed minsep check.'.format(', '.join(mutant['mutations']))
logging.debug(message)
return False
Expand Down
43 changes: 17 additions & 26 deletions ilustrado/crossover.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
# coding: utf-8
""" This file implements crossover functionality. """
from copy import deepcopy
import numpy as np
from matador.utils.chem_utils import get_stoich
from matador.utils.cell_utils import create_simple_supercell, standardize_doc_cell
from copy import deepcopy


def crossover(parents, method='random_slice', debug=False):
""" Attempt to create a child structure from two parents structures.
Input:
Parameters:
| parents: list(dict), list of two parent structures,
| method : str, currently only 'random_slice'.
parents (list(dict)) : list of two parent structures
method (str) : currently only 'random_slice'
Returns:
| newborn: dict, newborn structure from parents.
dict : newborn structure from parents.
"""

if method is 'random_slice':
if method == 'random_slice':
_crossover = random_slice
elif method is 'periodic_cut':
_crossover = periodic_cut

return _crossover(parents, debug=debug)

Expand All @@ -35,16 +33,16 @@ def random_slice(parent_seeds, standardize=True, supercell=True, shift=True, deb
parent structures. Both parent structures are cut and spliced along the
same crystallographic axis.
Input:
Parameters:
| parents : list(dict), parent structures to crossover,
| standardize : bool, use spglib to standardize parents pre-crossover,
| supercell : bool, make a random supercell to rescale parents,
| shift : bool, randomly shift atoms in parents to unbias.
parents (list(dict)) : parent structures to crossover,
standardize (bool) : use spglib to standardize parents pre-crossover,
supercell (bool) : make a random supercell to rescale parents,
shift (bool) : randomly shift atoms in parents to unbias.
Returns:
| child : dict, newborn structure from parents.
dict: newborn structure from parents.
"""
parents = deepcopy(parent_seeds)
Expand All @@ -61,8 +59,8 @@ def random_slice(parent_seeds, standardize=True, supercell=True, shift=True, deb

if supercell:
# check ratio of num atoms in parents and grow the smaller one
parent_extent_ratio = (parents[0]['cell_volume']
/ parents[1]['cell_volume'])
parent_extent_ratio = (parents[0]['cell_volume'] /
parents[1]['cell_volume'])
if debug:
print(parent_extent_ratio, parents[0]['cell_volume'],
'vs', parents[1]['cell_volume'])
Expand All @@ -76,7 +74,7 @@ def random_slice(parent_seeds, standardize=True, supercell=True, shift=True, deb
print(supercell_target, supercell_factor)
supercell_vector = [1, 1, 1]
if supercell_factor > 1:
for i in range(supercell_factor):
for ind in range(supercell_factor):
min_lat_vec_abs = 1e10
min_lat_vec_ind = -1
for i in range(3):
Expand All @@ -94,8 +92,8 @@ def random_slice(parent_seeds, standardize=True, supercell=True, shift=True, deb
standardize=False)
child['positions_frac'] = []
child['atom_types'] = []
child['lattice_cart'] = (cut_val * np.asarray(parents[0]['lattice_cart'])
+ (child_size-cut_val) * np.asarray(parents[1]['lattice_cart']))
child['lattice_cart'] = (cut_val * np.asarray(parents[0]['lattice_cart']) +
(child_size-cut_val) * np.asarray(parents[1]['lattice_cart']))
child['lattice_cart'] = child['lattice_cart'].tolist()

# choose slice axis
Expand All @@ -121,10 +119,3 @@ def random_slice(parent_seeds, standardize=True, supercell=True, shift=True, deb
child['stoichiometry'] = get_stoich(child['atom_types'])
child['num_atoms'] = len(child['atom_types'])
return child


def periodic_cut(parents):
""" Periodic cut a la CASTEP/Abraham & Probert. """
child = dict()
raise NotImplementedError
return child
Loading

0 comments on commit a4241c8

Please sign in to comment.