diff --git a/README.md b/README.md
index 9aabd4aa..c8969443 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,35 @@
-# generative-reactor-designs
-Repository to hold the framework and code to produce generative reactor designs.
+# ROLLO (Reactor evOLutionary aLgorithm Optimizer)
+
+ROLLO is a Python package that applies evolutionary algorithm techniques to optimize nuclear reactor design.
+
+Documentation on the usage of ROLLO is hosted at: ## INSERT WEBSITE ##
+
+## Installation
+`python -m pip install rollo`
+
+## Running ROLLO
+`python -m rollo -i -c `
+
+Command line flags:
+| Flag | Description | Mandatory ? |
+| ----------- | ----------- | ----------- |
+| -i | name of input file | Yes |
+| -c| name of checkpoint file | No |
+
+The checkpoint file holds the results from the ROLLO simulation and also acts
+as a restart file. Thus, if a ROLLO simulation ends prematurely, the checkpoint
+file can be used to restart the code from the most recent population and
+continue the simulation.
+
+## To build documentation
+`cd docs`
+
+`make html`
+
+## To upload to PyPI
+`python3 -m build`
+
+`python3 -m twine upload dist/*`
+
+## License
+ROLLO is distributed under the 3-Clause BSD License.
diff --git a/examples/fhr-slab/README.md b/examples/fhr-slab/README.md
new file mode 100644
index 00000000..da144f4e
--- /dev/null
+++ b/examples/fhr-slab/README.md
@@ -0,0 +1,8 @@
+**ROLLO input file and supporting OpenMC templating scripts for maximizing keff
+in a FHR fuel slab by varying the slab's TRISO fuel particle distribution.**
+
+`rollo_input.json`: ROLLO input file
+
+`openmc_input.py`: OpenMC templating script
+
+`constants.py`: Supporting functions for `openmcinp.py`
diff --git a/examples/fhr-slab/constants.py b/examples/fhr-slab/constants.py
new file mode 100644
index 00000000..76aba0e0
--- /dev/null
+++ b/examples/fhr-slab/constants.py
@@ -0,0 +1,123 @@
+import openmc
+from numpy import pi
+
+# Constants
+T_r1 = 2135e-5
+T_r2 = 3135e-5
+T_r3 = 3485e-5
+T_r4 = 3835e-5
+T_r5 = 4235e-5
+T_pitch = 0.09266
+
+uoc_9 = openmc.Material()
+uoc_9.set_density("g/cc", 11)
+uoc_9.add_nuclide("U235", 2.27325e-3)
+uoc_9.add_nuclide("U238", 2.269476e-2)
+uoc_9.add_nuclide("O16", 3.561871e-2)
+uoc_9.add_nuclide("C0", 9.79714e-3)
+uoc_9.temperature = 1110
+uoc_9.volume = 4 / 3 * pi * (T_r1 ** 3) * 101 * 210 * 4 * 36
+
+por_c = openmc.Material()
+por_c.set_density("g/cc", 1)
+por_c.add_nuclide("C0", 5.013980e-2)
+por_c.temperature = 948
+
+si_c = openmc.Material()
+si_c.set_density("g/cc", 3.2)
+si_c.add_nuclide("Si28", 4.431240e-2)
+si_c.add_nuclide("Si29", 2.25887e-3)
+si_c.add_nuclide("Si30", 1.48990e-3)
+si_c.add_nuclide("C0", 4.806117e-2)
+si_c.temperature = 948
+
+graphite = openmc.Material()
+graphite.set_density("g/cc", 1.8)
+graphite.add_nuclide("C0", 9.025164e-2)
+graphite.temperature = 948
+
+triso_4_layers = openmc.Material()
+triso_4_layers.add_nuclide("C0", 0.06851594519357823)
+triso_4_layers.add_nuclide("Si28", 0.009418744960032735)
+triso_4_layers.add_nuclide("Si29", 0.00048013017638108395)
+triso_4_layers.add_nuclide("Si30", 0.0003166830980933728)
+triso_4_layers.set_density("sum")
+triso_4_layers.temperature = 948
+
+lm_graphite = openmc.Material()
+lm_graphite.set_density("g/cc", 1.8)
+lm_graphite.add_nuclide("C0", 9.025164e-2)
+lm_graphite.temperature = 948
+
+flibe = openmc.Material()
+flibe.set_density("g/cc", 1.95)
+flibe.add_nuclide("Li6", 1.383014e-6)
+flibe.add_nuclide("Li7", 2.37132e-2)
+flibe.add_nuclide("Be9", 1.18573e-2)
+flibe.add_nuclide("F19", 4.74291e-2)
+flibe.temperature = 948
+
+mats = openmc.Materials(
+ (uoc_9, por_c, si_c, graphite, lm_graphite, flibe, triso_4_layers)
+)
+
+# 4 layer triso
+two_spheres = [openmc.Sphere(r=r) for r in [T_r1, T_r5]]
+two_triso_cells = [
+ openmc.Cell(fill=uoc_9, region=-two_spheres[0]),
+ openmc.Cell(fill=triso_4_layers, region=+two_spheres[0] & -two_spheres[1]),
+ openmc.Cell(fill=lm_graphite, region=+two_spheres[1]),
+]
+two_triso_univ = openmc.Universe(cells=two_triso_cells)
+
+
+def create_prism(left, right, left_refl, right_refl):
+ if left_refl:
+ xplane_left = +openmc.XPlane(x0=left, boundary_type="reflective")
+ else:
+ xplane_left = +openmc.XPlane(x0=left)
+ if right_refl:
+ xplane_right = -openmc.XPlane(x0=right, boundary_type="reflective")
+ else:
+ xplane_right = -openmc.XPlane(x0=right)
+ prism = (
+ xplane_left &
+ xplane_right &
+ +openmc.YPlane(y0=0.35) &
+ -openmc.YPlane(y0=0.35 + 2.55) &
+ +openmc.ZPlane(z0=0, boundary_type="reflective") &
+ -openmc.ZPlane(z0=T_pitch * 20, boundary_type="reflective")
+ )
+ return prism
+
+
+def create_prism_vertical(bot, top):
+ yplane_bot = +openmc.YPlane(y0=bot)
+ yplane_top = -openmc.YPlane(y0=top)
+ prism = (
+ +openmc.XPlane(x0=2) &
+ -openmc.XPlane(x0=2 + 23.1) &
+ yplane_bot &
+ yplane_top &
+ +openmc.ZPlane(z0=0, boundary_type="reflective") &
+ -openmc.ZPlane(z0=T_pitch * 20, boundary_type="reflective")
+ )
+ return prism
+
+
+def create_lattice(region, pf):
+ try:
+ centers_1 = openmc.model.pack_spheres(radius=T_r5, region=region, pf=pf)
+ trisos_1 = [openmc.model.TRISO(T_r5, two_triso_univ, c) for c in centers_1]
+ prism = openmc.Cell(region=region)
+ lower_left_1, upper_right_1 = prism.region.bounding_box
+ shape = tuple(((upper_right_1 - lower_left_1) / 0.4).astype(int))
+ pitch_1 = (upper_right_1 - lower_left_1) / shape
+ lattice_1 = openmc.model.create_triso_lattice(
+ trisos_1, lower_left_1, pitch_1, shape, lm_graphite
+ )
+ prism.fill = lattice_1
+ except:
+ prism = openmc.Cell(region=region)
+ prism.fill = lm_graphite
+ return prism
diff --git a/examples/fhr-slab/openmc_input.py b/examples/fhr-slab/openmc_input.py
new file mode 100644
index 00000000..91149af9
--- /dev/null
+++ b/examples/fhr-slab/openmc_input.py
@@ -0,0 +1,133 @@
+import openmc
+import numpy as np
+from numpy import sin, cos, tan, pi
+import sys
+
+sys.path.insert(1, "../")
+from constants import *
+
+# Templating
+total_pf = 0.0979
+sine_a = {{sine_a}}
+sine_b = {{sine_b}}
+sine_c = {{sine_c}}
+vol_total = 23.1 * 2.55 * T_pitch * 20
+vol_slice = 2.31 * 2.55 * T_pitch * 20
+
+x_left = +openmc.XPlane(x0=0, boundary_type="periodic")
+x_right = -openmc.XPlane(x0=27.1, boundary_type="periodic")
+y_top = -openmc.YPlane(y0=3.25, boundary_type="periodic")
+y_bot = +openmc.YPlane(y0=0, boundary_type="periodic")
+y_top.periodic_surface = y_bot
+x_left.periodic_surface = x_right
+z_top = -openmc.ZPlane(z0=T_pitch * 20, boundary_type="reflective")
+z_bot = +openmc.ZPlane(z0=0, boundary_type="reflective")
+bounds = openmc.Cell(fill=flibe)
+bounds.region = x_left & x_right & y_top & y_bot & z_top & z_bot
+
+plank_x_left = +openmc.XPlane(x0=2)
+plank_x_right = -openmc.XPlane(x0=2 + 23.1)
+plank_y_top = -openmc.YPlane(y0=0.35 + 2.55)
+plank_y_bot = +openmc.YPlane(y0=0.35)
+plank_region = plank_x_left & plank_x_right & plank_y_top & plank_y_bot & z_top & z_bot
+bounds.region &= ~plank_region
+
+graphite1_x_right = -openmc.XPlane(x0=2)
+graphite1 = openmc.Cell(fill=graphite)
+graphite1.region = x_left & graphite1_x_right & y_top & y_bot & z_top & z_bot
+bounds.region &= ~graphite1.region
+
+graphite2_x_left = +openmc.XPlane(x0=25.1)
+graphite2 = openmc.Cell(fill=graphite)
+graphite2.region = graphite2_x_left & x_right & y_top & y_bot & z_top & z_bot
+bounds.region &= ~graphite2.region
+
+boundaries = np.arange(2, 27.1, 2.31)
+prism_1 = create_prism(boundaries[0], boundaries[1], False, False)
+prism_2 = create_prism(boundaries[1], boundaries[2], False, False)
+prism_3 = create_prism(boundaries[2], boundaries[3], False, False)
+prism_4 = create_prism(boundaries[3], boundaries[4], False, False)
+prism_5 = create_prism(boundaries[4], boundaries[5], False, False)
+prism_6 = create_prism(boundaries[5], boundaries[6], False, False)
+prism_7 = create_prism(boundaries[6], boundaries[7], False, False)
+prism_8 = create_prism(boundaries[7], boundaries[8], False, False)
+prism_9 = create_prism(boundaries[8], boundaries[9], False, False)
+prism_10 = create_prism(boundaries[9], boundaries[10], False, False)
+
+# triso PF distribution
+vol_triso = 4 / 3 * pi * T_r5 ** 3
+no_trisos = total_pf * vol_total / vol_triso
+midpoints = []
+for x in range(len(boundaries) - 1):
+ midpoints.append((boundaries[x] + boundaries[x + 1]) / 2)
+midpoints = np.array(midpoints)
+sine_val = sine_a * sin(sine_b * midpoints + sine_c) + 2
+sine_val = np.where(sine_val < 0, 0, sine_val)
+triso_z = sine_val / sum(sine_val) * no_trisos
+pf_z = triso_z * vol_triso / vol_slice
+
+prism_cell_1 = create_lattice(prism_1, pf_z[0])
+prism_cell_2 = create_lattice(prism_2, pf_z[1])
+prism_cell_3 = create_lattice(prism_3, pf_z[2])
+prism_cell_4 = create_lattice(prism_4, pf_z[3])
+prism_cell_5 = create_lattice(prism_5, pf_z[4])
+prism_cell_6 = create_lattice(prism_6, pf_z[5])
+prism_cell_7 = create_lattice(prism_7, pf_z[6])
+prism_cell_8 = create_lattice(prism_8, pf_z[7])
+prism_cell_9 = create_lattice(prism_9, pf_z[8])
+prism_cell_10 = create_lattice(prism_10, pf_z[9])
+
+univ = openmc.Universe(
+ cells=[
+ bounds,
+ graphite1,
+ graphite2,
+ prism_cell_1,
+ prism_cell_2,
+ prism_cell_3,
+ prism_cell_4,
+ prism_cell_5,
+ prism_cell_6,
+ prism_cell_7,
+ prism_cell_8,
+ prism_cell_9,
+ prism_cell_10,
+ ]
+)
+geom = openmc.Geometry(univ)
+
+# settings
+point = openmc.stats.Point((13.5, 1.7, T_pitch * 9.5))
+src = openmc.Source(space=point)
+settings = openmc.Settings()
+settings.source = src
+settings.batches = 10
+settings.inactive = 2
+settings.particles = 100
+settings.temperature = {"multipole": True, "method": "interpolation"}
+
+plot = openmc.Plot()
+plot.basis = "xy"
+plot.origin = (13.5, 1.7, T_pitch * 9.5)
+plot.width = (30, 4)
+plot.pixels = (1000, 200)
+colors = {
+ uoc_9: "yellow",
+ por_c: "black",
+ si_c: "orange",
+ graphite: "grey",
+ flibe: "blue",
+ lm_graphite: "red",
+}
+plot.color_by = "material"
+plot.colors = colors
+plots = openmc.Plots()
+plots.append(plot)
+
+# export
+mats.export_to_xml()
+geom.export_to_xml()
+settings.export_to_xml()
+plots.export_to_xml()
+openmc.run()
+# openmc.run(openmc_exec="openmc-ccm-nompi",threads=16)
diff --git a/examples/fhr-slab/rollo_input.json b/examples/fhr-slab/rollo_input.json
new file mode 100644
index 00000000..076ff176
--- /dev/null
+++ b/examples/fhr-slab/rollo_input.json
@@ -0,0 +1,32 @@
+{
+ "control_variables": {
+ "sine_a": {"min": 0.0, "max": 2.0},
+ "sine_b": {"min": 0.0, "max": 1.57},
+ "sine_c": {"min": 0.0, "max": 6.28}
+ },
+ "evaluators": {
+ "openmc": {
+ "input_script": "openmcinp.py",
+ "inputs": ["sine_a", "sine_b", "sine_c"],
+ "outputs": ["keff"],
+ "keep_files": false
+ }
+ },
+ "constraints": {"keff": {"operator": [">="], "constrained_val": [1.0]}},
+ "algorithm": {
+ "parallel": "multiprocessing",
+ "objective": ["max"],
+ "optimized_variable": ["keff"],
+ "pop_size": 4,
+ "generations": 10,
+ "mutation_probability": 0.2374127402121101,
+ "mating_probability": 0.4699131568275016,
+ "selection_operator": {"operator": "selTournament", "inds": 1, "tournsize": 5},
+ "mutation_operator": {
+ "operator": "mutPolynomialBounded",
+ "eta": 0.2374127402121101,
+ "indpb": 0.2374127402121101
+ },
+ "mating_operator": {"operator": "cxBlend", "alpha": 0.4699131568275016}
+ }
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..b5a3c468
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = [
+ "setuptools>=42",
+ "wheel"
+]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/docs/lit-review/2020-generative-reactor-design-lit-review.bib b/reports/lit-review/2020-generative-reactor-design-lit-review.bib
similarity index 100%
rename from docs/lit-review/2020-generative-reactor-design-lit-review.bib
rename to reports/lit-review/2020-generative-reactor-design-lit-review.bib
diff --git a/docs/lit-review/Makefile b/reports/lit-review/Makefile
similarity index 97%
rename from docs/lit-review/Makefile
rename to reports/lit-review/Makefile
index 545e7278..3f3a0432 100644
--- a/docs/lit-review/Makefile
+++ b/reports/lit-review/Makefile
@@ -24,7 +24,7 @@ epub:
ebook-convert $(manuscript).html $(manuscript).epub
clean:
- rm -f *.bcf *.run.xml *.fdb_latexmk *.fls *.dvi *.toc *.aux *.gz *.out *.log *.bbl *.blg *.log *.spl *~ *.spl *.zip *.acn *.glo *.ist *.epub
+ rm -f *.bcf *.run.xml *.fdb_latexmk *.fls *.dvi *.toc *.aux *.gz *.out *.log *.bbl *.blg *.log *.spl *~ *.spl *.zip *.acn *.glo *.ist *.epub, *.glsdefs
realclean: clean
rm -rf $(manuscript).dvi
diff --git a/docs/lit-review/figures/.DS_Store b/reports/lit-review/figures/.DS_Store
similarity index 100%
rename from docs/lit-review/figures/.DS_Store
rename to reports/lit-review/figures/.DS_Store
diff --git a/docs/lit-review/figures/cell.png b/reports/lit-review/figures/cell.png
similarity index 100%
rename from docs/lit-review/figures/cell.png
rename to reports/lit-review/figures/cell.png
diff --git a/docs/lit-review/lit-review.tex b/reports/lit-review/lit-review.tex
similarity index 100%
rename from docs/lit-review/lit-review.tex
rename to reports/lit-review/lit-review.tex
diff --git a/rollo/__init__.py b/rollo/__init__.py
new file mode 100644
index 00000000..280c35a1
--- /dev/null
+++ b/rollo/__init__.py
@@ -0,0 +1,12 @@
+from rollo.algorithm import *
+from rollo.backend import *
+from rollo.constraints import *
+from rollo.evaluation import *
+from rollo.executor import *
+from rollo.input_validation import *
+from rollo.moltres_evaluation import *
+from rollo.openmc_evaluation import *
+from rollo.special_variables import *
+from rollo.toolbox_generator import *
+
+__version__ = "0.1.1"
diff --git a/rollo/__main__.py b/rollo/__main__.py
new file mode 100644
index 00000000..921f698e
--- /dev/null
+++ b/rollo/__main__.py
@@ -0,0 +1,29 @@
+from rollo import executor
+import sys
+import getopt
+
+
+def main():
+ argv = sys.argv[1:]
+ msg = "python rollo -i -c "
+ try:
+ opts, args = getopt.getopt(argv, "i:c:")
+ opts_dict = {}
+ for opt, arg in opts:
+ opts_dict[opt] = arg
+ if "-i" in opts_dict:
+ if "-c" in opts_dict:
+ new_run = executor.Executor(
+ input_file=opts_dict["-i"], checkpoint_file=opts_dict["-c"]
+ )
+ else:
+ new_run = executor.Executor(input_file=opts_dict["-i"])
+ new_run.execute()
+ if len(opts) == 0:
+ raise Exception("To run rollo: " + msg)
+ except getopt.GetoptError:
+ raise Exception("To run rollo: " + msg)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rollo/algorithm.py b/rollo/algorithm.py
new file mode 100644
index 00000000..611d1967
--- /dev/null
+++ b/rollo/algorithm.py
@@ -0,0 +1,322 @@
+from .backend import BackEnd
+import random
+import warnings
+import sys
+
+try:
+ from mpi4py import MPI
+ import dill
+
+ MPI.pickle.__init__(dill.dumps, dill.loads)
+ from mpi4py.futures import MPICommExecutor
+except:
+ warnings.warn(
+ "Failed to import mpi4py. (Only necessary for parallel method: mpi_evals). \
+ Please ignore this warning if you are using other parallel methods such \
+ as multiprocessing and none."
+ )
+
+
+class Algorithm(object):
+ """The Algorithm class contains methods to initialize and execute the genetic
+ algorithm. It executes a general genetic algorithm framework that uses the
+ hyperparameters defined in the deap_toolbox, applies constraints defined
+ in the constraints_obj, evaluates fitness values using the evaluation
+ function produced by Evaluation contained in the deap_toolbox, and saves
+ all the results with BackEnd.
+
+ Parameters
+ ----------
+ deap_toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+ constraint_obj : rollo.constraints.Constraints
+ Holds information about constraints for the problem and functions to
+ apply the constraints
+ checkpoint_file : str
+ Name of checkpoint file
+ deap_creator : deap.creator object
+ DEAP meta-factory allowing to create classes that will fulfill the
+ needs of the evolutionary algorithms
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+
+ Attributes
+ ----------
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+ constraint_obj : rollo.constraints.Constraints
+ Holds information about constraints for the problem and functions to
+ apply the constraints
+ cp_file : str
+ Name of checkpoint file
+ backend : rollo.backend.Backend
+ Contains and manipulates the output backend
+ parallel_method : str
+ parallelization method (none, multiprocessing, mpi_evals)
+
+ """
+
+ def __init__(
+ self,
+ deap_toolbox,
+ constraint_obj,
+ checkpoint_file,
+ deap_creator,
+ control_dict,
+ output_dict,
+ input_dict,
+ start_time,
+ parallel_method,
+ ):
+ self.toolbox = deap_toolbox
+ self.constraint_obj = constraint_obj
+ self.cp_file = checkpoint_file
+ self.backend = BackEnd(
+ checkpoint_file,
+ deap_creator,
+ control_dict,
+ output_dict,
+ input_dict,
+ start_time,
+ )
+ self.parallel_method = parallel_method
+
+ def generate(self):
+ """Executes the genetic algorithm and outputs the summarized results
+ into an output file
+
+ Returns
+ -------
+ list
+ list of deap.creator.Ind for final generation
+
+ """
+
+ pop = self.toolbox.population(n=self.toolbox.pop_size)
+ if self.parallel_method == "multiprocessing":
+ try:
+ import multiprocessing_on_dill as multiprocessing
+
+ pool = multiprocessing.Pool()
+ self.toolbox.register("map", pool.map)
+ except:
+ warnings.warn(
+ "multiprocessing_on_dill failed to import, rollo will run serially."
+ )
+ pass
+ elif self.parallel_method == "mpi_evals":
+ try:
+ if MPI.COMM_WORLD.rank > 0:
+ while MPI.COMM_WORLD.bcast(None):
+ with MPICommExecutor(MPI.COMM_WORLD, root=0) as executor:
+ pass
+ sys.exit(0)
+ except:
+ warnings.warn("MPI Failed.")
+ pass
+ if self.cp_file:
+ self.backend.initialize_checkpoint_backend()
+ pop = self.backend.results["population"]
+ random.setstate(self.backend.results["rndstate"])
+ else:
+ self.backend.initialize_new_backend()
+ pop = self.initialize_pop(pop)
+ self.cp_file = "checkpoint.pkl"
+ print(self.backend.results["logbook"])
+ for gen in range(self.backend.results["start_gen"] + 1, self.toolbox.ngen):
+ pop = self.apply_algorithm_ngen(pop, gen)
+ print(self.backend.results["logbook"])
+ print("rollo Simulation Completed!")
+ if self.parallel_method == "mpi_evals":
+ try:
+ MPI.COMM_WORLD.bcast(False)
+ except:
+ pass
+ return pop
+
+ def initialize_pop(self, pop):
+ """Initialize population for genetic algorithm
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for previous generation
+
+ Returns
+ -------
+ list
+ list of deap.creator.Ind with fitnesses evaluated
+
+ """
+
+ print("Entering generation 0...")
+ for i, ind in enumerate(pop):
+ ind.gen = 0
+ ind.num = i
+ # evaluate fitness values of initial pop
+ invalids = [ind for ind in pop if not ind.fitness.valid]
+ copy_invalids = [self.toolbox.clone(ind) for ind in invalids]
+ if self.parallel_method == "mpi_evals":
+ try:
+ MPI.COMM_WORLD.bcast(True)
+ with MPICommExecutor(MPI.COMM_WORLD, root=0) as executor:
+ fitnesses = executor.map(self.toolbox.evaluate, list(pop))
+ except:
+ warnings.warn("MPI Failed, rollo will run serially.")
+ fitnesses = self.toolbox.map(self.toolbox.evaluate, pop)
+ else:
+ fitnesses = self.toolbox.map(self.toolbox.evaluate, pop)
+ # assign fitness values to individuals
+ for ind, fitness in zip(pop, fitnesses):
+ fitness_vals = []
+ for i in range(self.toolbox.objs):
+ fitness_vals.append(fitness[i])
+ ind.fitness.values = tuple(fitness_vals)
+ ind.output = fitness
+ pop = self.constraint_obj.apply_constraints(pop)
+ self.backend.update_backend(pop, 0, copy_invalids, random.getstate())
+ return pop
+
+ def apply_algorithm_ngen(self, pop, gen):
+ """Apply genetic algorithm to a population
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for previous generation
+ gen: int
+ generation number
+
+ Returns
+ -------
+ list
+ list of deap.creator.Ind for new generation
+
+ """
+ print("Entering generation " + str(gen) + "...")
+ pop = self.apply_selection_operator(pop)
+ pop = self.apply_mating_operator(pop)
+ pop = self.apply_mutation_operator(pop)
+ # define pop's gen, ind num
+ for i, ind in enumerate(pop):
+ ind.gen = gen
+ ind.num = i
+ # evaluate fitness of newly created pop for inds with invalid fitness
+ invalids = [ind for ind in pop if not ind.fitness.valid]
+ copy_invalids = [self.toolbox.clone(ind) for ind in invalids]
+ if self.parallel_method == "mpi_evals":
+ try:
+ MPI.COMM_WORLD.bcast(True)
+ with MPICommExecutor(MPI.COMM_WORLD, root=0) as executor:
+ fitnesses = executor.map(self.toolbox.evaluate, list(invalids))
+ except:
+ warnings.warn("MPI Failed, rollo will run serially.")
+ fitnesses = self.toolbox.map(self.toolbox.evaluate, list(invalids))
+ else:
+ fitnesses = self.toolbox.map(self.toolbox.evaluate, list(invalids))
+ # assign fitness values to individuals
+ for ind, fitness in zip(invalids, fitnesses):
+ fitness_vals = []
+ for i in range(self.toolbox.objs):
+ fitness_vals.append(fitness[i])
+ ind.fitness.values = tuple(fitness_vals)
+ ind.output = fitness
+ pop = self.constraint_obj.apply_constraints(pop)
+ self.backend.update_backend(pop, gen, copy_invalids, random.getstate())
+ return pop
+
+ def apply_selection_operator(self, pop):
+ """Applies selection operator to population
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for that generation
+
+ Returns
+ -------
+ list
+ new list of deap.creator.Ind after selection operator application
+
+ """
+
+ pre_pop = self.toolbox.select(pop)
+ select_pop = [self.toolbox.clone(ind) for ind in pre_pop]
+ # extend pop length to pop_size
+ while len(select_pop) != self.toolbox.pop_size:
+ select_pop.append(self.toolbox.clone(random.choice(pre_pop)))
+ return select_pop
+
+ def apply_mating_operator(self, pop):
+ """Applies mating operator to population
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for that generation
+
+ Returns
+ -------
+ list
+ new list of deap.creator.Ind after mating operator application
+
+ """
+
+ final_pop = []
+ for child1, child2 in zip(pop[::2], pop[1::2]):
+ new_child1 = self.toolbox.clone(child1)
+ new_child2 = self.toolbox.clone(child2)
+ if random.random() < self.toolbox.cxpb:
+ outside_bounds = True
+ while outside_bounds:
+ self.toolbox.mate(new_child1, new_child2)
+ del new_child1.fitness.values, new_child2.fitness.values
+ outside_bounds = False
+ for i, val in enumerate(new_child1):
+ if val < self.toolbox.min_list[i]:
+ outside_bounds = True
+ if val > self.toolbox.max_list[i]:
+ outside_bounds = True
+ for i, val in enumerate(new_child2):
+ if val < self.toolbox.min_list[i]:
+ outside_bounds = True
+ if val > self.toolbox.max_list[i]:
+ outside_bounds = True
+ final_pop.append(new_child1)
+ final_pop.append(new_child2)
+ return final_pop
+
+ def apply_mutation_operator(self, pop):
+ """Applies mutation operator to population
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for that generation
+
+ Returns
+ -------
+ list
+ new list of deap.creator.Ind after mutation operator application
+
+ """
+
+ final_pop = []
+ for mutant in pop:
+ new_mutant = self.toolbox.clone(mutant)
+ if random.random() < self.toolbox.mutpb:
+ outside_bounds = True
+ while outside_bounds:
+ self.toolbox.mutate(new_mutant)
+ del new_mutant.fitness.values
+ outside_bounds = False
+ for i, val in enumerate(new_mutant):
+ if val < self.toolbox.min_list[i]:
+ outside_bounds = True
+ if val > self.toolbox.max_list[i]:
+ outside_bounds = True
+ final_pop.append(new_mutant)
+ return final_pop
diff --git a/rollo/backend.py b/rollo/backend.py
new file mode 100644
index 00000000..f8d1ddc1
--- /dev/null
+++ b/rollo/backend.py
@@ -0,0 +1,210 @@
+from deap import tools
+import pickle
+import numpy
+import time
+
+
+class BackEnd(object):
+ """The BackEnd class contains methods to save genetic algorithm population
+ results into a pickled checkpoint file and to restart a partially completed
+ genetic algorithm from the checkpoint file.
+
+ Parameters
+ ----------
+ checkpoint_file : str
+ Name of checkpoint file
+ deap_creator : deap.creator object
+ DEAP meta-factory allowing to create classes that will fulfill the
+ needs of the evolutionary algorithms
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_file : str
+ input file contents
+ start_time : float
+ time the simulation began
+
+ Attributes
+ ----------
+ results : dict
+ contains results from simulation
+ checkpoint_file : str
+ Name of checkpoint file
+ creator : deap.creator object
+ DEAP meta-factory allowing to create classes that will fulfill the
+ needs of the evolutionary algorithms
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_file : str
+ input file contents
+ start_time : float
+ time the simulation began
+
+ """
+
+ def __init__(
+ self,
+ checkpoint_file,
+ deap_creator,
+ control_dict,
+ output_dict,
+ input_file,
+ start_time,
+ ):
+ self.results = {}
+ self.checkpoint_file = checkpoint_file
+ self.creator = deap_creator
+ self.control_dict = control_dict
+ self.output_dict = output_dict
+ self.input_file = input_file
+ self.start_time = start_time
+ self.initialize_stats()
+
+ def initialize_new_backend(self):
+ """Initializes brand new backend object"""
+
+ self.results["input_file"] = self.input_file
+ self.results["start_gen"] = 0
+ self.results["halloffame"] = tools.HallOfFame(maxsize=1)
+ self.results["logbook"] = tools.Logbook()
+ self.results["logbook"].header = "time", "gen", "evals", "oup", "ind"
+ self.results["logbook"].chapters["ind"].header = "avg", "min", "max"
+ self.results["logbook"].chapters["oup"].header = "avg", "std", "min", "max"
+ self.results["all"] = {}
+ self.results["all"]["ind_naming"] = self.ind_naming()
+ self.results["all"]["oup_naming"] = self.output_naming()
+ self.results["all"]["populations"] = []
+ self.results["all"]["outputs"] = []
+ self.checkpoint_file = "checkpoint.pkl"
+ return
+
+ def ind_naming(self):
+ """Returns a dict with control variable name as key and their ordered
+ position in Ind as value
+
+ Returns
+ -------
+ dict
+ control variable name as key and ordered position in Ind as value
+
+ """
+
+ names = []
+ for ind in self.control_dict:
+ if self.control_dict[ind][1] > 1:
+ for i in range(self.control_dict[ind][1]):
+ names.append(ind + "_" + str(i))
+ else:
+ names.append(ind)
+ names_dict = {}
+ for i, n in enumerate(names):
+ names_dict[n] = i
+ return names_dict
+
+ def output_naming(self):
+ """Returns a dict with output parameter name as key and their ordered
+ position as value
+
+ Returns
+ -------
+ dict
+ output parameter name as key and ordered position as value
+
+ """
+ oup_dict = {}
+ for i, oup in enumerate(self.output_dict):
+ oup_dict[oup] = i
+ return oup_dict
+
+ def initialize_checkpoint_backend(self):
+ """Initialize backend when checkpoint is used"""
+
+ creator = self.creator
+ with open(self.checkpoint_file, "rb") as cp_file:
+ cp = pickle.load(cp_file)
+ self.results["population"] = cp["population"]
+ self.results["start_gen"] = cp["generation"]
+ self.results["halloffame"] = cp["halloffame"]
+ self.results["logbook"] = cp["logbook"]
+ self.results["rndstate"] = cp["rndstate"]
+ self.results["all"] = cp["all"]
+ return
+
+ def initialize_stats(self):
+ """Initialize DEAP statistics"""
+ stats_ind = tools.Statistics(key=lambda ind: ind)
+ stats_ind.register("avg", numpy.mean, axis=0)
+ stats_ind.register("std", numpy.std, axis=0)
+ stats_ind.register("min", numpy.min, axis=0)
+ stats_ind.register("max", numpy.max, axis=0)
+ stats_oup = tools.Statistics(key=lambda ind: ind.output)
+ stats_oup.register("avg", numpy.mean, axis=0)
+ stats_oup.register("std", numpy.std, axis=0)
+ stats_oup.register("min", numpy.min, axis=0)
+ stats_oup.register("max", numpy.max, axis=0)
+ self.mstats = tools.MultiStatistics(ind=stats_ind, oup=stats_oup)
+ return
+
+ def update_backend(self, pop, gen, invalid_ind, rndstate):
+ """Updates backend. Called after every generation
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for that generation
+ gen : int
+ generation number
+ invalid_ind : list
+ list of deap.creator.Ind whose fitnesses had to be evaluated
+ rndstate : tuple
+ current state of the random number generator
+
+ """
+
+ self.results["halloffame"].update(pop)
+ record = self.mstats.compile(pop)
+ self.results["logbook"].record(
+ time=time.time() - self.start_time,
+ gen=gen,
+ evals=len(invalid_ind),
+ **record
+ )
+ self.results["all"]["populations"].append(pop)
+ pop_oup = []
+ for ind in pop:
+ pop_oup.append(ind.output)
+ self.results["all"]["outputs"].append(pop_oup)
+ evaluator_files = {}
+ try:
+ for solver in self.input_file["evaluators"]:
+ with open(
+ self.input_file["evaluators"][solver]["input_script"], "r"
+ ) as file:
+ evaluator_files[solver + "_input"] = file.read()
+ try:
+ with open(
+ self.input_file["evaluators"][solver]["output_script"], "r"
+ ) as file:
+ evaluator_files[solver + "_output"] = file.read()
+ except:
+ pass
+ except:
+ pass
+ cp = dict(
+ input_file=self.input_file,
+ evaluator_files=evaluator_files,
+ population=pop,
+ generation=gen,
+ halloffame=self.results["halloffame"],
+ logbook=self.results["logbook"],
+ rndstate=rndstate,
+ all=self.results["all"],
+ )
+ with open(self.checkpoint_file, "wb") as cp_file:
+ pickle.dump(cp, cp_file)
+ return
diff --git a/rollo/constraints.py b/rollo/constraints.py
new file mode 100644
index 00000000..28263fdf
--- /dev/null
+++ b/rollo/constraints.py
@@ -0,0 +1,131 @@
+import operator
+import warnings
+import random
+
+
+class Constraints(object):
+ """The Constraints class contains methods to initialize constraints defined
+ in the input file and applies the constraints by removing individuals that
+ do not meet the constraint.
+
+ Parameters
+ ----------
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_constraints : dict
+ constraints sub-dictionary from input file
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+
+ Attributes
+ ----------
+ constraints : list
+ list of constraints information
+ numbered_oup_dict : dict
+ output parameter name as key and ordered position as value
+ ops: dict
+ dict of accepted operators
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+
+ """
+
+ def __init__(self, output_dict, input_constraints, toolbox):
+ self.constraints = self.constraints_list(input_constraints)
+ self.numbered_oup_dict = self.output_dict_numbered(output_dict)
+ self.ops = {
+ "<": operator.lt,
+ "<=": operator.le,
+ "==": operator.eq,
+ "!=": operator.ne,
+ ">=": operator.ge,
+ ">": operator.gt,
+ }
+ self.toolbox = toolbox
+
+ def output_dict_numbered(self, output_dict):
+ """Returns dictionary of output variables and their corresponding index
+
+ Parameters
+ ----------
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+
+ Returns
+ -------
+ dict
+ output parameter name as key and ordered position as value
+
+ """
+
+ numbered_oup_dict = {}
+ for i, key in enumerate(output_dict):
+ numbered_oup_dict[key] = i
+ return numbered_oup_dict
+
+ def constraints_list(self, input_constraints):
+ """Returns list of constraints information
+
+ Parameters
+ ----------
+ input_constraints : dict
+ constraints sub-dictionary from input file
+
+ Returns
+ -------
+ constraints_list : list
+ list of constraints information
+
+ """
+
+ constraints_list = []
+ for c in input_constraints:
+ for i in range(len(input_constraints[c]["operator"])):
+ constraints_list.append(
+ [
+ c,
+ {
+ "op": input_constraints[c]["operator"][i],
+ "val": input_constraints[c]["constrained_val"][i],
+ },
+ ]
+ )
+ return constraints_list
+
+ def apply_constraints(self, pop):
+ """Removes individuals in population that fail to meet constraints
+
+ Parameters
+ ----------
+ pop : list
+ list of deap.creator.Ind for that generation
+
+ Returns
+ -------
+ list
+ list of deap.creator.Ind for that generation with individuals that
+ fail to meet constraints removed
+
+ """
+
+ new_pop = []
+ for ind in pop:
+ not_constrained = True
+ for i, c in enumerate(self.constraints):
+ index = self.numbered_oup_dict[c[0]]
+ if self.ops[c[1]["op"]](ind.output[index], c[1]["val"]):
+ pass
+ else:
+ not_constrained = False
+ if not_constrained:
+ new_pop.append(ind)
+ final_pop = [self.toolbox.clone(ind) for ind in new_pop]
+ while len(final_pop) < len(pop):
+ final_pop.append(self.toolbox.clone(random.choice(new_pop)))
+ warnings.warn(
+ str(len(pop) - len(new_pop)) +
+ " out of " +
+ str(len(pop)) +
+ " inds were constrained"
+ )
+ return final_pop
diff --git a/rollo/evaluation.py b/rollo/evaluation.py
new file mode 100644
index 00000000..ff3b9368
--- /dev/null
+++ b/rollo/evaluation.py
@@ -0,0 +1,301 @@
+import os
+import subprocess
+import ast
+import shutil
+import time
+from jinja2 import nativetypes
+from rollo.openmc_evaluation import OpenMCEvaluation
+from rollo.moltres_evaluation import MoltresEvaluation
+
+
+class Evaluation:
+ """Holds functions that generate and execute the evaluation solver's scripts.
+
+ DEAP's (evolutionary algorithm package) fitness evaluator requires an
+ evaluation function to evaluate each individual's fitness values. The
+ Evaluation class contains a method that creates an evaluation function that
+ runs the nuclear software and returns the required fitness values, defined
+ in the input file.
+
+ Attributes
+ ----------
+ supported_solvers : list of str
+ list of supported evaluation software
+ input_scripts : dict
+ key is evaluation software name, value is that evaluation software's
+ template input script name
+ output_scripts : dict
+ key is evaluation software name, value is that evaluation software's
+ template output script name
+ eval_dict : dict
+ key is evaluation software name, value is a class containing the
+ functions to evaluate its output files
+
+ """
+
+ def __init__(self):
+ self.supported_solvers = ["openmc", "moltres"]
+ self.input_scripts = {}
+ self.output_scripts = {}
+ # Developers can add new solvers to self.eval_dict below
+ self.eval_dict = {"openmc": OpenMCEvaluation(), "moltres": MoltresEvaluation()}
+
+ def add_evaluator(self, solver_name, input_script, output_script):
+ """Adds information about an evaluator to the Evaluation class object
+ for later use in eval_fn_generator.
+
+ Parameters
+ ----------
+ solver_name : str
+ name of solver
+ input_script : str
+ input script name
+ output_script : str
+ optional output script name
+ """
+
+ self.input_scripts[solver_name] = input_script
+ try:
+ self.output_scripts[solver_name] = output_script
+ except:
+ pass
+ return
+
+ def eval_fn_generator(self, control_dict, output_dict, input_evaluators):
+ """Returns a function that accepts a DEAP individual and returns a
+ tuple of output values listed in outputs
+
+ Parameters
+ ----------
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_evaluators : dict
+ evaluators sub-dictionary from input file
+
+ Returns
+ -------
+ eval_function : function
+ function that runs the evaluation software and returns output values
+ output by the software
+
+ """
+
+ def eval_function(ind):
+ """Accepts a DEAP individual and returns a tuple of output values
+ listed in outputs
+
+ Parameters
+ ----------
+ ind : deap.creator.Ind
+ Created in `rollo.toolbox_generator.ToolboxGenerator`. It is
+ a list with special attributes.
+
+ Returns
+ -------
+ tuple
+ output values from evaluators ordered by output_dict
+
+ """
+
+ self.rank_time = time.time()
+ control_vars = self.name_ind(ind, control_dict, input_evaluators)
+ output_vals = [None] * len(output_dict)
+
+ for solver in input_evaluators:
+ # path name for solver's run
+ path = solver + "_" + str(ind.gen) + "_" + str(ind.num)
+ # render jinja-ed input script
+ rendered_script = self.render_jinja_template_python(
+ script=self.input_scripts[solver],
+ control_vars_solver=control_vars[solver],
+ )
+ # enter directory for this particular solver's run
+ os.mkdir(path)
+ os.chdir(path)
+ # run solver's function where run is executed
+ exec("self." + solver + "_run(rendered_script)")
+ # go back to normal directory with all files
+ os.chdir("../")
+ # get output values
+ output_vals = self.get_output_vals(
+ output_vals, solver, output_dict, control_vars, path
+ )
+ if input_evaluators[solver]["keep_files"] == False:
+ shutil.rmtree(path)
+ return tuple(output_vals)
+
+ return eval_function
+
+ def get_output_vals(self, output_vals, solver, output_dict, control_vars, path):
+ """Returns a populated list with output values for each solver
+
+ Parameters
+ ----------
+ output_vals : list
+ empty list of the correct size
+ solver : str
+ name of solver
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ control_vars : dict
+ maps the control_dict's variable names to values from ind list to
+ order the output_vals correctly
+ path : str
+ path name
+
+ Returns
+ -------
+ output_vals : list
+ output values requested by rollo input file in correct order
+
+ """
+
+ if self.output_scripts[solver]:
+ # copy rendered output script into a new file in the particular solver's run
+ shutil.copyfile(
+ self.output_scripts[solver], path + "/" + solver + "_output.py"
+ )
+ # enter directory for this particular solver's run
+ os.chdir(path)
+ # run the output script
+ oup_bytes = self.system_call("python " + solver + "_output.py")
+ # return the output script's printed dictionary into a variable
+ oup_script_results = ast.literal_eval(oup_bytes.decode("utf-8"))
+ # go back to normal directory with all files
+ os.chdir("../")
+ for i, var in enumerate(output_dict):
+ if output_dict[var] == solver:
+ # if variable is a control variable
+ if var in control_vars[solver]:
+ output_vals[i] = control_vars[solver][var]
+ # if variable's analysis script is pre-defined
+ elif var in self.eval_dict[solver].pre_defined_outputs:
+ os.chdir(path)
+ method = getattr(self.eval_dict[solver], "evaluate_" + var)
+ output_vals[i] = method()
+ os.chdir("../")
+ # if variable's defined in output script
+ else:
+ output_vals[i] = oup_script_results[var]
+ return output_vals
+
+ def system_call(self, command):
+ """Runs evaluation software output file
+
+ Parameters
+ ----------
+ command : str
+ command to run in the command line
+
+ Returns
+ -------
+ str
+ printed output from running evaluation software output file
+
+ """
+
+ p = subprocess.Popen([command], stdout=subprocess.PIPE, shell=True)
+ return p.stdout.read()
+
+ def name_ind(self, ind, control_dict, input_evaluators):
+ """Returns a dictionary that maps the control_dict's variable names to
+ values from ind list
+
+ Parameters
+ ----------
+ ind : deap.creator.Ind
+ Created in `rollo.toolbox_generator.ToolboxGenerator`. It is
+ a list with special attributes.
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ input_evaluators : dict
+ evaluators sub-dictionary from input file
+
+ Returns
+ -------
+ control_vars : dict
+ maps the control_dict's variable names to values from ind list to
+ order the output_vals correctly
+
+ """
+
+ control_vars = {}
+ for solver in input_evaluators:
+ control_vars[solver] = {}
+ for i, var in enumerate(control_dict):
+ if control_dict[var][1] == 1:
+ ind_vars = ind[i]
+ else:
+ ind_vars = []
+ for j in range(control_dict[var][1]):
+ ind_vars.append(ind[i + j])
+ control_vars[control_dict[var][0]][var] = ind_vars
+ return control_vars
+
+ def render_jinja_template_python(self, script, control_vars_solver):
+ """Renders a jinja2 templated python file and returns a templated python
+ script. This will be used by solver's with a python interface such as
+ OpenMC.
+
+ Parameters
+ ----------
+ script : str
+ name of evaluator template script
+ control_vars_solver : str
+ name of evaluation solver software
+
+ Returns
+ -------
+ rendered_template : str
+ rendered evaluator template script
+
+ """
+
+ env = nativetypes.NativeEnvironment()
+ with open(script) as s:
+ imported_script = s.read()
+ template = nativetypes.NativeTemplate(imported_script)
+ render_str = "template.render("
+ for inp in control_vars_solver:
+ render_str += "**{'" + inp + "':" + str(control_vars_solver[inp]) + "},"
+ render_str += ")"
+ rendered_template = eval(render_str)
+ return rendered_template
+
+ def render_jinja_template(self):
+ """Renders a jinja2 templated input file. This will be used by solver's
+ with a text based interface such as Moltres
+
+ ### TO BE POPULATED
+ """
+
+ return
+
+ def openmc_run(self, rendered_openmc_script):
+ """Runs the rendered openmc script
+
+ Parameters
+ ----------
+ rendered_openmc_script : str
+ rendered OpenMC evaluator template script
+
+ """
+
+ f = open("openmc_input.py", "w+")
+ f.write(rendered_openmc_script)
+ f.close()
+ with open("output.txt", "wb") as output:
+ subprocess.call(["python", "-u", "./openmc_input.py"], stdout=output)
+ return
+
+ def moltres_run(self, rendered_moltres_script):
+ """Runs the rendered moltres input file
+
+ ### TO BE POPULATED
+ """
+
+ return
diff --git a/rollo/executor.py b/rollo/executor.py
new file mode 100644
index 00000000..8b461208
--- /dev/null
+++ b/rollo/executor.py
@@ -0,0 +1,256 @@
+import rollo
+from rollo.input_validation import InputValidation
+from rollo.special_variables import SpecialVariables
+from rollo.algorithm import Algorithm
+from rollo.constraints import Constraints
+from rollo.toolbox_generator import ToolboxGenerator
+import json
+import time
+from collections import OrderedDict
+
+
+class Executor(object):
+ """Executes rollo simulation from start to finish.
+
+ Instances of this class can be used to perform a rollo run.
+
+ The Executor class drives the ROLLO code execution with the following
+ steps in the execute method:
+ 1) User input file validation with InputValidation
+ 2) Evaluation function generation with Evaluation class
+ 3) DEAP toolbox initialization with ToolboxGenerator class
+ 4) Constraint initialization with Constraints class
+ 5) Genetic algorithm execution with Algorithm class
+
+ Parameters
+ ----------
+ input_file : str
+ Name of input file
+ checkpoint_file : str, optional
+ Name of checkpoint file
+
+ Attributes
+ ----------
+ input_file : str
+ Name of input file
+ checkpoint_file : str
+ Name of checkpoint file
+
+ """
+
+ def __init__(self, input_file, checkpoint_file=None):
+ self.input_file = input_file
+ self.checkpoint_file = checkpoint_file
+
+ def execute(self):
+ """Executes rollo simulation to generate reactor designs. \n
+ 1) Read and validate input file
+ 2) Initialize evaluator
+ 3) Initialize DEAP toolbox
+ 4) Initialize constraints
+ 5) Run genetic algorithm
+
+ """
+ t0 = time.time()
+ input_dict = self.read_input_file()
+ iv = InputValidation(input_dict)
+ iv.validate()
+ complete_input_dict = iv.add_all_defaults(input_dict)
+ # organize control variables and output dict
+ control_dict, output_dict = self.organize_input_output(complete_input_dict)
+ # generate evaluator function
+ evaluator_fn = self.load_evaluator(
+ control_dict, output_dict, complete_input_dict
+ )
+ # DEAP toolbox set up
+ toolbox, creator = self.load_toolbox(
+ evaluator_fn,
+ complete_input_dict["algorithm"],
+ complete_input_dict["control_variables"],
+ control_dict,
+ )
+ # load constraints if they exist
+ constraints = self.load_constraints(
+ output_dict, complete_input_dict["constraints"], toolbox
+ )
+ alg = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=constraints,
+ checkpoint_file=self.checkpoint_file,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict=complete_input_dict,
+ start_time=t0,
+ parallel_method=complete_input_dict["algorithm"]["parallel"],
+ )
+ alg.generate()
+ t1 = time.time()
+ print("Total time in simulation " + str(t1 - t0) + " seconds")
+ return
+
+ def read_input_file(self):
+ """Reads a json input file and returns a dictionary
+
+ Returns
+ -------
+ data : dict
+ json input file converted into a dict
+
+ """
+
+ with open(self.input_file) as json_file:
+ data = json.load(json_file)
+ return data
+
+ def organize_input_output(self, input_dict):
+ """Labels the control variables and output variables with numbers
+ to keep consistency between evaluation, constraints, and algorithm
+ classes
+
+ Parameters
+ ----------
+ input_dict : dict
+ input file dict
+
+ Returns
+ -------
+ control_vars : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and count of variables as each value
+ output_vars : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+
+ """
+
+ input_ctrl_vars = input_dict["control_variables"]
+ input_evaluators = input_dict["evaluators"]
+ input_algorithm = input_dict["algorithm"]
+
+ # define control variables dict
+ control_vars = OrderedDict()
+ sv = SpecialVariables()
+ special_control_vars = sv.special_variables
+ for solver in input_evaluators:
+ for var in input_evaluators[solver]["inputs"]:
+ if var in special_control_vars:
+ method = getattr(sv, var + "_num")
+ num = method(input_ctrl_vars[var])
+ control_vars[var] = [solver, num]
+ else:
+ control_vars[var] = [solver, 1]
+
+ # define output variables dict
+ output_vars = OrderedDict()
+ optimized_variable = input_algorithm["optimized_variable"]
+ # find optimized variable
+ var_to_solver = {}
+ for solver in input_evaluators:
+ for var in input_evaluators[solver]["outputs"]:
+ var_to_solver[var] = solver
+ for opt_var in optimized_variable:
+ output_vars[opt_var] = var_to_solver[opt_var]
+ # put in the rest of the output variables
+ for solver in input_evaluators:
+ for var in input_evaluators[solver]["outputs"]:
+ if var not in optimized_variable:
+ output_vars[var] = solver
+
+ return control_vars, output_vars
+
+ def load_evaluator(self, control_dict, output_dict, input_dict):
+ """Creates an Evaluation function object
+
+ Parameters
+ ----------
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_dict : dict
+ input file dict with default values filled
+
+ Returns
+ -------
+ evaluator_fn : function
+ function that runs the evaluation software and returns output values
+ output by the software
+
+ """
+ input_evaluators = input_dict["evaluators"]
+ evaluator = rollo.Evaluation()
+ for solver in input_evaluators:
+ solver_dict = input_evaluators[solver]
+ try:
+ output_script = solver_dict["output_script"]
+ except:
+ output_script = None
+ evaluator.add_evaluator(
+ solver_name=solver,
+ input_script=solver_dict["input_script"],
+ output_script=output_script,
+ )
+ evaluator_fn = evaluator.eval_fn_generator(
+ control_dict, output_dict, input_dict["evaluators"]
+ )
+ return evaluator_fn
+
+ def load_toolbox(
+ self, evaluator_fn, input_algorithm, input_ctrl_vars, control_dict
+ ):
+ """Creates a DEAP toolbox object based on user-defined
+ parameters.
+
+ Parameters
+ ----------
+ evaluator_fn : function
+ function that runs the evaluation software and returns output values
+ output by the software
+ input_algorithm : dict
+ algorithm sub-dictionary from input file
+ input_ctrl_vars : dict
+ control variables sub-dictionary from input file
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+
+ Returns
+ -------
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+ creator : deap.creator object
+ DEAP meta-factory allowing to create classes that will fulfill the
+ needs of the evolutionary algorithms
+
+ """
+ toolbox_generator = ToolboxGenerator()
+ toolbox, creator = toolbox_generator.setup(
+ evaluator_fn, input_algorithm, input_ctrl_vars, control_dict
+ )
+ return toolbox, creator
+
+ def load_constraints(self, output_dict, input_constraints, toolbox):
+ """Creates a Constraints object loaded with user-defined
+ constraints information.
+
+ Parameters
+ ----------
+ output_dict : OrderedDict
+ Ordered dict of output variables as keys and solvers as values
+ input_constraints : dict
+ constraints sub-dictionary from input file
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with genetic algorithm parameters for this
+ creator
+
+ Returns
+ -------
+ constraints_obj : rollo.Constraints object
+ Constraints object loaded with constraint information from the
+ input file
+
+ """
+
+ constraint_obj = Constraints(output_dict, input_constraints, toolbox)
+ return constraint_obj
diff --git a/rollo/input_validation.py b/rollo/input_validation.py
new file mode 100644
index 00000000..5cfd1fc8
--- /dev/null
+++ b/rollo/input_validation.py
@@ -0,0 +1,565 @@
+from jsonschema import validate
+from rollo.special_variables import SpecialVariables
+
+
+class InputValidation:
+ """The InputValidation class contains methods to read and validate the JSON
+ ROLLO input file to ensure the user defined all key parameters. If the user
+ did not, ROLLO raises an exception to tell the user which parameters are
+ missing.
+
+ Attributes
+ ----------
+ input : dict
+ rollo json input file as a dict
+
+ """
+
+ def __init__(self, input_dict):
+ self.input = input_dict
+
+ def add_all_defaults(self, input_dict):
+ """ Goes through the entire input_dict and adds default inputs if they
+ are missing from the input_dict
+
+ Parameters
+ ----------
+ input_dict: dict
+ input file dict
+
+ Returns
+ -------
+ reloaded_input_dict: dict
+ input file dict with additional missing default inputs
+
+ """
+ input_evaluators = {}
+ for solver in input_dict["evaluators"]:
+ input_evaluators[solver] = input_dict["evaluators"][solver]
+ input_evaluators[solver] = self.default_check(
+ input_evaluators[solver], "keep_files", True
+ )
+ input_algorithm = input_dict["algorithm"]
+ input_algorithm = self.default_check(input_algorithm, "objective", "min")
+ input_algorithm = self.default_check(input_algorithm, "pop_size", 100)
+ input_algorithm = self.default_check(input_algorithm, "generations", 10)
+ input_algorithm = self.default_check(
+ input_algorithm, "selection_operator", {"operator": "selBest", "inds": 1}
+ )
+ input_algorithm = self.default_check(
+ input_algorithm,
+ "mutation_operator",
+ {"operator": "mutGaussian", "indpb": 0.5, "mu": 0.5, "sigma": 0.5},
+ )
+ input_algorithm = self.default_check(
+ input_algorithm, "mating_operator", {"operator": "cxOnePoint"}
+ )
+ reloaded_input_dict = input_dict.copy()
+ reloaded_input_dict["evaluators"] = input_evaluators
+ reloaded_input_dict["algorithm"] = input_algorithm
+ return reloaded_input_dict
+
+ def default_check(self, input_dict, variable, default_val):
+ """Checks if a single variable is missing from a dict, and adds a
+ default value if it is
+
+ Parameters
+ ----------
+ input_dict: dict
+ input file dict
+ variable: str
+ variable name
+ default_val: any type accepted
+ default input for that variable (can be str, float, dict, etc.)
+
+ Returns
+ -------
+ input_dict: dict
+ input file dict with additional missing default input defined by
+ parameters of this function
+
+ """
+ try:
+ a = input_dict[variable]
+ except KeyError:
+ input_dict[variable] = default_val
+ return input_dict
+
+ def validate(self):
+ """Validates the input dictionary and throws errors if the input file
+ does not meet rollo input file rules.
+
+ """
+
+ # validate top layer of JSON input
+ schema_top_layer = {
+ "type": "object",
+ "properties": {
+ "control_variables": {"type": "object"},
+ "evaluators": {"type": "object"},
+ "constraints": {"type": "object"},
+ "algorithm": {"type": "object"},
+ },
+ }
+ validate(instance=self.input, schema=schema_top_layer)
+ self.validate_correct_keys(
+ self.input,
+ ["control_variables", "evaluators", "algorithm"],
+ ["constraints"],
+ "top level",
+ )
+
+ # validate control variables
+ try:
+ input_ctrl_vars = self.input["control_variables"]
+ except KeyError:
+ print(
+ " At least 1 control variable must \
+ be defined."
+ )
+ else:
+ self.validate_ctrl_vars(input_ctrl_vars)
+
+ # validate evaluators
+ try:
+ input_evaluators = self.input["evaluators"]
+ except KeyError:
+ print(
+ " At least 1 evaluator must be \
+ defined."
+ )
+ else:
+ self.validate_evaluators(input_evaluators)
+
+ # validate constraints
+ try:
+ input_constraints = self.input["constraints"]
+ except KeyError:
+ pass
+ else:
+ self.validate_constraints(input_constraints, input_evaluators)
+
+ # validate algorithm
+ try:
+ input_algorithm = self.input["algorithm"]
+ except KeyError:
+ print(" The algorithm must be defined.")
+ else:
+ self.validate_algorithm(input_algorithm, input_evaluators)
+ return
+
+ def validate_algorithm(self, input_algorithm, input_evaluators):
+ """Validates the "algorithm" segment of the JSON input file"""
+
+ # schema validation
+ schema_algorithm = {
+ "type": "object",
+ "properties": {
+ "parallel": {"type": "string"},
+ "objective": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "optimized_variable": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "pop_size": {"type": "number"},
+ "generations": {"type": "number"},
+ "mutation_probability": {"type": "number"},
+ "mating_probability": {"type": "number"},
+ "selection_operator": {"type": "object"},
+ "mutation_operator": {"type": "object"},
+ "mating_operator": {"type": "object"},
+ },
+ }
+ validate(instance=input_algorithm, schema=schema_algorithm)
+ # key validation
+ self.validate_correct_keys(
+ input_algorithm,
+ ["optimized_variable"],
+ [
+ "parallel",
+ "objective",
+ "pop_size",
+ "generations",
+ "mutation_probability",
+ "mating_probability",
+ "selection_operator",
+ "mutation_operator",
+ "mating_operator",
+ ],
+ "algorithm",
+ )
+ # validation for objective and optimized variable
+ self.validate_in_list(
+ input_algorithm["parallel"],
+ ["none", "multiprocessing", "mpi_evals"],
+ "parallel",
+ )
+ for obj in input_algorithm["objective"]:
+ self.validate_in_list(obj, ["min", "max"], "objective")
+ output_list = []
+ for solver in input_evaluators:
+ output_list += input_evaluators[solver]["outputs"]
+ for opt_var in input_algorithm["optimized_variable"]:
+ self.validate_in_list(
+ opt_var,
+ output_list,
+ "optimized_variable",
+ )
+
+ # validation for operator sections
+ self.validate_algorithm_operators("selection", input_algorithm)
+ self.validate_algorithm_operators("mutation", input_algorithm)
+ self.validate_algorithm_operators("mating", input_algorithm)
+
+ # k value cannot be larger than pop size
+ if input_algorithm["selection_operator"]["operator"] == "selTournament":
+ if (
+ input_algorithm["selection_operator"]["inds"] >
+ input_algorithm["pop_size"]
+ ):
+ raise Exception("Population size must be larger than inds.")
+ return
+
+ def validate_algorithm_operators(self, operator_type, input_algorithm):
+ """Validates the genetic algorithm operators
+
+ Parameters
+ ----------
+ operator_type : str
+ types are selection, mutation, and mating
+ input_algorithm : dict
+ algorithm sub-dictionary from input file
+
+ """
+
+ deap_operators = {
+ "selection": {
+ "selTournament": ["inds", "tournsize"],
+ "selNSGA2": ["inds"],
+ "selBest": ["inds"],
+ },
+ "mutation": {
+ "mutPolynomialBounded": ["eta", "indpb"],
+ },
+ "mating": {"cxOnePoint": [], "cxUniform": ["indpb"], "cxBlend": ["alpha"]},
+ }
+
+ try:
+ op = input_algorithm[operator_type + "_operator"]
+ except KeyError:
+ pass
+ else:
+ # first check for operator
+ try:
+ op_op = op["operator"]
+ except KeyError:
+ print(
+ " You must define an operator for the " +
+ operator_type +
+ "_operator"
+ )
+ raise
+ else:
+ self.validate_in_list(
+ op_op,
+ list(deap_operators[operator_type].keys()),
+ operator_type + "_operator's operator",
+ )
+ schema_op = {"type": "object", "properties": {}}
+ schema_op["operator"] = {"type": "string"}
+ for var in deap_operators[operator_type][op_op]:
+ schema_op["properties"][var] = {"type": "number"}
+ validate(
+ instance=input_algorithm[operator_type + "_operator"],
+ schema=schema_op,
+ )
+ self.validate_correct_keys(
+ input_algorithm[operator_type + "_operator"],
+ deap_operators[operator_type][op_op] + ["operator"],
+ [],
+ operator_type + " operator: " + op_op,
+ )
+ return
+
+ def validate_constraints(self, input_constraints, input_evaluators):
+ """Validates the constraints segment of the JSON input file
+
+ Parameters
+ ----------
+ input_constraints : dict
+ constraints sub-dictionary from input file
+ input_evaluators : dict
+ evaluators sub-dictionary from input file
+
+ """
+
+ # check if constraints are in evaluators outputs
+ allowed_constraints = []
+ for evaluator in input_evaluators:
+ allowed_constraints += input_evaluators[evaluator]["outputs"]
+ for constraint in input_constraints:
+ self.validate_in_list(constraint, allowed_constraints, "Constraints")
+ # schema validation
+ schema_constraints = {"type": "object", "properties": {}}
+ for constraint in input_constraints:
+ schema_constraints["properties"][constraint] = {
+ "type": "object",
+ "properties": {
+ "operator": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "constrained_val": {
+ "type": "array",
+ "items": {"type": "number"},
+ },
+ },
+ }
+ validate(instance=input_constraints, schema=schema_constraints)
+ # key validation
+ for constraint in input_constraints:
+ self.validate_correct_keys(
+ input_constraints[constraint],
+ ["operator", "constrained_val"],
+ [],
+ "constraint: " + constraint,
+ )
+ for op in input_constraints[constraint]["operator"]:
+ self.validate_in_list(
+ op,
+ [">", ">=", "=", "<", "<="],
+ constraint + "'s operator variable",
+ )
+ return
+
+ def validate_ctrl_vars(self, input_ctrl_vars):
+ """Validates the control variables segment of the JSON input file
+
+ Parameters
+ ----------
+ input_ctrl_vars : dict
+ control variables sub-dictionary from input file
+
+ """
+ # special control variables with a non-conforming input style defined in
+ # input*** (add file name that has this)
+ # add to this list if a developer adds a special control variable
+ sv = SpecialVariables()
+ special_ctrl_vars = sv.special_variables
+
+ # validate regular control variables
+ # schema validation
+ schema_ctrl_vars = {"type": "object", "properties": {}}
+ variables = []
+ for var in input_ctrl_vars:
+ if var not in special_ctrl_vars:
+ schema_ctrl_vars["properties"][var] = {
+ "type": "object",
+ "properties": {
+ "max": {"type": "number"},
+ "min": {"type": "number"},
+ },
+ }
+ variables.append(var)
+ validate(instance=input_ctrl_vars, schema=schema_ctrl_vars)
+ # key validation
+ for var in variables:
+ self.validate_correct_keys(
+ input_ctrl_vars[var], ["min", "max"], [], "control variable: " + var
+ )
+
+ # validate special control variables
+ # add validation here if developer adds new special input variable
+ # polynomial
+ try:
+ input_ctrl_vars_poly = input_ctrl_vars["polynomial_triso"]
+ except KeyError:
+ pass
+ else:
+ # schema validation
+ schema_ctrl_vars_poly = {
+ "type": "object",
+ "properties": {
+ "order": {"type": "number"},
+ "min": {"type": "number"},
+ "max": {"type": "number"},
+ "radius": {"type": "number"},
+ "volume": {"type": "number"},
+ "slices": {"type": "number"},
+ "height": {"type": "number"},
+ },
+ }
+ validate(instance=input_ctrl_vars_poly, schema=schema_ctrl_vars_poly)
+ # key validation
+ self.validate_correct_keys(
+ input_ctrl_vars_poly,
+ ["order", "min", "max", "radius", "volume", "slices", "height"],
+ [],
+ "control variable: polynomial_triso",
+ )
+ return
+
+ def validate_evaluators(self, input_evaluators):
+ """Validates the evaluators segment of the JSON input file
+
+ Parameters
+ ----------
+ input_evaluators : dict
+ evaluators sub-dictionary from input file
+
+ """
+ # evaluators available
+ # add to this list if a developer adds a new evaluator
+ available_evaluators = ["openmc", "moltres"]
+ # add to this dict if a developers adds a new predefined output
+ # for an evaluator
+ pre_defined_outputs = {"openmc": ["keff"]}
+
+ # validate evaluators
+ self.validate_correct_keys(
+ input_evaluators, [], available_evaluators, "evaluators"
+ )
+ schema_evaluators = {"type": "object", "properties": {}}
+
+ # validate each evaluator
+ for evaluator in input_evaluators:
+ schema_evaluators["properties"][evaluator] = {
+ "type": "object",
+ "properties": {
+ "input_script": {"type": "string"},
+ "inputs": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "outputs": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "output_script": {"type": "string"},
+ "keep_files": {"type": "boolean"},
+ },
+ }
+ validate(instance=input_evaluators, schema=schema_evaluators)
+ for evaluator in input_evaluators:
+ self.validate_correct_keys(
+ input_evaluators[evaluator],
+ ["input_script", "inputs", "outputs"],
+ ["output_script", "keep_files"],
+ "evaluator: " + evaluator,
+ )
+ # check if outputs are in predefined outputs or inputs, and if not
+ # output_script must be defined
+ in_list, which_strings = self.validate_if_in_list(
+ input_evaluators[evaluator]["outputs"],
+ pre_defined_outputs[evaluator] + input_evaluators[evaluator]["inputs"],
+ )
+ if not in_list:
+ try:
+ a = input_evaluators[evaluator]["output_script"]
+ except KeyError:
+ print(
+ " You must define an output_script for evaluator: " +
+ evaluator +
+ " since the outputs: " +
+ str(which_strings) +
+ " are not inputs or pre-defined outputs."
+ )
+ raise
+ return
+
+ def validate_if_in_list(self, input_strings, accepted_strings):
+ """Checks if strings are in a defined list of strings and returns a
+ boolean
+
+ Parameters
+ ----------
+ input_strings : list of str
+ list of variable names to check
+ accepted_strings : list of str
+ list of variable names to check against
+
+ Returns
+ -------
+ in_list : bool
+ boolean indicating if all input_strings are in accepted_strings
+ which_strings : list
+ list of variables from input_strings that are not in accepted_strings
+
+ """
+ in_list = True
+ which_strings = []
+ for string in input_strings:
+ if string not in accepted_strings:
+ in_list = False
+ which_strings.append(string)
+ return in_list, which_strings
+
+ def validate_in_list(self, variable, accepted_variables, name):
+ """Checks if a variable is in a list of accepted variables
+
+ Parameters
+ ----------
+ variable : str
+ name of variable to check
+ accepted_variables : list of str
+ name of variables to check against
+ name : str
+ parameter name
+
+ """
+ assert variable in accepted_variables, (
+ " variable: " +
+ name +
+ ", only accepts: " +
+ str(accepted_variables) +
+ " not variable: " +
+ variable
+ )
+ return
+
+ def validate_correct_keys(
+ self, dict_to_validate, key_names, optional_key_names, variable_type
+ ):
+ """Runs a try except routine for to check if all key names are in the
+ dict_to_validate and ensure no unwanted keys are defined
+
+ Parameters
+ ----------
+ dict_to_validate : dict
+ dict to validate
+ key_names : list of str
+ names of required keys
+ optional_key_names : list of str
+ names of optional keys
+ variable_type : str
+ parameter name
+
+ """
+ try:
+ combined_key_names = key_names + optional_key_names
+ for key in key_names:
+ a = dict_to_validate[key]
+ for key in dict_to_validate:
+ assert key in combined_key_names, (
+ " Only " +
+ str(combined_key_names) +
+ " are accepted for " +
+ variable_type +
+ ", not variable: " +
+ key
+ )
+ except KeyError:
+ print(
+ " " +
+ str(key_names) +
+ " variables must be defined for " +
+ variable_type
+ )
+ raise
+ except AssertionError as error:
+ print(error)
+ raise
+ return
diff --git a/rollo/moltres_evaluation.py b/rollo/moltres_evaluation.py
new file mode 100644
index 00000000..28d342c6
--- /dev/null
+++ b/rollo/moltres_evaluation.py
@@ -0,0 +1,14 @@
+class MoltresEvaluation:
+ """The MoltresEvaluation class contains ROLLO built-in methods for evaluating
+ Moltres output files. Developers can update thisfile with methods to
+ evaluate frequently used Moltres outputs.
+
+ Attributes
+ ----------
+ pre_defined_outputs : list
+ list of variables names with evaluation functions in this class
+
+ """
+
+ def __init__(self):
+ self.pre_defined_outputs = []
diff --git a/rollo/openmc_evaluation.py b/rollo/openmc_evaluation.py
new file mode 100644
index 00000000..ab1902fa
--- /dev/null
+++ b/rollo/openmc_evaluation.py
@@ -0,0 +1,36 @@
+import glob
+import openmc
+
+
+class OpenMCEvaluation:
+ """The OpenMCEvaluation class contains ROLLO built-in methods for evaluating
+ OpenMC output files. Developers can update this file with methods to
+ evaluate frequently used OpenMC outputs.
+ - To add new openmc output variable evaluation functions, create a function
+ called "evaluate_###". ### names the openmc output variable. Also, add ###
+ name as a str into the pre_defined_output list.
+
+ Attributes
+ ----------
+ pre_defined_outputs : list
+ list of variables names with evaluation functions in this class
+
+ """
+
+ def __init__(self):
+ self.pre_defined_outputs = ["keff"]
+
+ def evaluate_keff(self):
+ """This function analyzes the openmc output file and returns keff value
+
+ Returns
+ -------
+ float
+ keff value
+
+ """
+ for file in glob.glob("statepoint*"):
+ h5file = file
+ sp = openmc.StatePoint(h5file, autolink=False)
+ keff = sp.k_combined.nominal_value
+ return keff
diff --git a/rollo/special_variables.py b/rollo/special_variables.py
new file mode 100644
index 00000000..e2aa738a
--- /dev/null
+++ b/rollo/special_variables.py
@@ -0,0 +1,67 @@
+import random
+import numpy as np
+
+
+class SpecialVariables:
+ """A class to hold special developer-defined variables
+
+ Attributes
+ ----------
+ special_variables : list
+ names of special variables
+
+ """
+
+ def __init__(self):
+ # developer's should add to this list when defining a new special
+ # variable
+ self.special_variables = ["polynomial_triso"]
+ return
+
+ def polynomial_triso_num(self, poly_dict):
+ """Returns number of variables
+
+ Returns
+ -------
+ int
+ number of control variables in polynomial_triso special variable
+ """
+
+ return poly_dict["order"] + 1
+
+ def polynomial_triso_values(self, poly_dict, var_dict):
+ """Returns a list of polynomial coefficients
+
+ Parameters
+ ----------
+ poly_dict : dict
+ polynomial_triso sub-dictionary from input file
+ var_dict : dict
+ control variable sub-dictionary from input file
+
+ Returns
+ -------
+ list
+ list of polynomial coefficients
+
+ """
+
+ total_pf = var_dict["packing_fraction"]
+ dz = poly_dict["slices"]
+ vol_total = poly_dict["volume"]
+ dz_vals = np.linspace(0, poly_dict["height"], dz)
+ vol_triso = 4 / 3 * np.pi * poly_dict["radius"] ** 3
+ no_trisos = total_pf * vol_total / vol_triso
+ poly_val_under_zero = [1.0] * dz
+ pf_z_over_max = [1.0] * dz
+ while (len(poly_val_under_zero) > 0) or (len(pf_z_over_max) > 0):
+ poly = []
+ for i in range(poly_dict["order"] + 1):
+ poly.append(random.uniform(poly_dict["min"], poly_dict["max"]))
+ poly_val = np.array([0.0] * dz)
+ for i in range(poly_dict["order"] + 1):
+ poly_val += poly[i] * dz_vals ** (poly_dict["order"] - i)
+ pf_z = poly_val / sum(poly_val) * no_trisos * vol_triso / vol_total
+ pf_z_over_max = [i for i in pf_z if i > 0.25]
+ poly_val_under_zero = [i for i in poly_val if i < 0]
+ return poly
diff --git a/rollo/toolbox_generator.py b/rollo/toolbox_generator.py
new file mode 100644
index 00000000..c5c10a24
--- /dev/null
+++ b/rollo/toolbox_generator.py
@@ -0,0 +1,261 @@
+from deap import base, creator, tools
+from rollo.special_variables import SpecialVariables
+import random
+
+
+class ToolboxGenerator(object):
+ """The ToolboxGenerator class initializes DEAP's toolbox and creator
+ modules with genetic algorithm hyperparameters defined in the input file."""
+
+ def setup(self, evaluator_fn, input_algorithm, input_ctrl_vars, control_dict):
+ """sets up DEAP toolbox with user-defined inputs
+
+ Parameters
+ ----------
+ evaluator_fn : function
+ function that runs the evaluation software and returns output values
+ input_algorithm : dict
+ algorithm sub-dictionary from input file
+ input_ctrl_vars : dict
+ control variables sub-dictionary from input file
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+
+ Returns
+ -------
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+ creator : deap.creator object
+ DEAP meta-factory allowing to create classes that will fulfill the
+ needs of the evolutionary algorithms
+
+ """
+
+ weight_list = []
+ for obj in input_algorithm["objective"]:
+ if obj == "min":
+ weight_list.append(-1.0)
+ elif obj == "max":
+ weight_list.append(+1.0)
+ creator.create("obj", base.Fitness, weights=tuple(weight_list))
+ creator.create("Ind", list, fitness=creator.obj)
+ toolbox = base.Toolbox()
+ # register control variables + individual
+ sv = SpecialVariables()
+ special_control_vars = sv.special_variables
+ for var in input_ctrl_vars:
+ if var not in special_control_vars:
+ var_dict = input_ctrl_vars[var]
+ toolbox.register(var, random.uniform, var_dict["min"], var_dict["max"])
+ toolbox.register(
+ "individual", self.individual_values, input_ctrl_vars, control_dict, toolbox
+ )
+ toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+ toolbox.register("evaluate", evaluator_fn)
+ min_list, max_list = self.min_max_list(control_dict, input_ctrl_vars)
+ toolbox.min_list, toolbox.max_list = min_list, max_list
+ toolbox = self.add_toolbox_operators(
+ toolbox,
+ selection_dict=input_algorithm["selection_operator"],
+ mutation_dict=input_algorithm["mutation_operator"],
+ mating_dict=input_algorithm["mating_operator"],
+ min_list=min_list,
+ max_list=max_list,
+ )
+ toolbox.pop_size = input_algorithm["pop_size"]
+ toolbox.ngen = input_algorithm["generations"]
+ toolbox.mutpb = input_algorithm["mutation_probability"]
+ toolbox.cxpb = input_algorithm["mating_probability"]
+ toolbox.objs = len(input_algorithm["objective"])
+ return toolbox, creator
+
+ def individual_values(self, input_ctrl_vars, control_dict, toolbox):
+ """Returns an individual with ordered control variable values
+
+ Parameters
+ ----------
+ input_ctrl_vars : dict
+ control variables sub-dictionary from input file
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ toolbox : deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+
+ Returns
+ -------
+ deap.creator.Ind
+ Created in `rollo.toolbox_generator.ToolboxGenerator`. It is
+ a list with special attributes.
+
+ """
+
+ var_dict = {}
+ input_vals = []
+ sv = SpecialVariables()
+ special_control_vars = sv.special_variables
+ for var in control_dict:
+ if var in special_control_vars:
+ # this func must return a list
+ method = getattr(sv, var + "_values")
+ result = method(input_ctrl_vars[var], var_dict)
+ input_vals += result
+ var_dict[var] = result
+ else:
+ result = getattr(toolbox, var)()
+ input_vals += [result]
+ var_dict[var] = result
+ return creator.Ind(input_vals)
+
+ def min_max_list(self, control_dict, input_ctrl_vars):
+ """Returns an ordered list of min values and max values for the
+ individual
+
+ Parameters
+ ----------
+ control_dict : OrderedDict
+ Ordered dict of control variables as keys and a list of their
+ solver and number of variables as each value
+ input_ctrl_vars : dict
+ control variables sub-dictionary from input file
+
+ Returns
+ -------
+ min_list : list
+ ordered list of minimum values for individual variables
+ max_list : list
+ ordered list of maximum values for individual variables
+
+ """
+
+ min_list = []
+ max_list = []
+ for var in control_dict:
+ for i in range(control_dict[var][1]):
+ min_list.append(input_ctrl_vars[var]["min"])
+ max_list.append(input_ctrl_vars[var]["max"])
+ return min_list, max_list
+
+ def add_toolbox_operators(
+ self, toolbox, selection_dict, mutation_dict, mating_dict, min_list, max_list
+ ):
+ """Adds selection, mutation, and mating operators to the deap toolbox
+
+ Parameters
+ ----------
+ toolbox : deap.base.Toolbox object
+ initialized DEAP toolbox
+ selection_dict : dict
+ selection_operator sub-sub-directory from input file
+ mutation_dict : dict
+ mutation_operator sub-sub-directory from input file
+ mating_dict : dict
+ mating_operator sub-sub-directory from input file
+ min_list : list
+ ordered list of minimum values for individual variables
+ max_list : list
+ ordered list of maximum values for individual variables
+
+ Returns
+ -------
+ deap.base.Toolbox object
+ DEAP toolbox populated with user-defined genetic algorithm parameters
+
+ """
+
+ toolbox = self.add_selection_operators(toolbox, selection_dict)
+ toolbox = self.add_mutation_operators(
+ toolbox, mutation_dict, min_list, max_list
+ )
+ toolbox = self.add_mating_operators(toolbox, mating_dict)
+ return toolbox
+
+ def add_selection_operators(self, toolbox, selection_dict):
+ """Adds selection operator to the deap toolbox
+
+ Parameters
+ ----------
+ toolbox : deap.base.Toolbox object
+ initialized DEAP toolbox
+ selection_dict : dict
+ selection_operator sub-sub-directory from input file
+
+ Returns
+ -------
+ deap.base.Toolbox object
+ DEAP toolbox populated with user-defined selection operator genetic
+ algorithm parameters
+
+ """
+
+ operator = selection_dict["operator"]
+ if operator == "selTournament":
+ toolbox.register(
+ "select",
+ tools.selTournament,
+ k=selection_dict["inds"],
+ tournsize=selection_dict["tournsize"],
+ )
+ elif operator == "selNSGA2":
+ toolbox.register("select", tools.selNSGA2, k=selection_dict["inds"])
+ elif operator == "selBest":
+ toolbox.register("select", tools.selBest, k=selection_dict["inds"])
+ return toolbox
+
+ def add_mutation_operators(self, toolbox, mutation_dict, min_list, max_list):
+ """Adds mutation operator to the deap toolbox
+
+ Parameters
+ ----------
+ toolbox : deap.base.Toolbox object
+ initialized DEAP toolbox
+ mutation_dict : dict
+ mutation_operator sub-sub-directory from input file
+
+ Returns
+ -------
+ deap.base.Toolbox object
+ DEAP toolbox populated with user-defined mutation operator genetic
+ algorithm parameters
+
+ """
+
+ operator = mutation_dict["operator"]
+ if operator == "mutPolynomialBounded":
+ toolbox.register(
+ "mutate",
+ tools.mutPolynomialBounded,
+ eta=mutation_dict["eta"],
+ indpb=mutation_dict["indpb"],
+ low=min_list,
+ up=max_list,
+ )
+ return toolbox
+
+ def add_mating_operators(self, toolbox, mating_dict):
+ """Adds mating operator to the deap toolbox
+
+ Parameters
+ ----------
+ toolbox : deap.base.Toolbox object
+ initialized DEAP toolbox
+ mating_dict : dict
+ mating_operator sub-sub-directory from input file
+
+ Returns
+ -------
+ deap.base.Toolbox object
+ DEAP toolbox populated with user-defined mating operator genetic
+ algorithm parameters
+
+ """
+
+ operator = mating_dict["operator"]
+ if operator == "cxOnePoint":
+ toolbox.register("mate", tools.cxOnePoint)
+ elif operator == "cxUniform":
+ toolbox.register("mate", tools.cxUniform, indpb=mating_dict["indpb"])
+ elif operator == "cxBlend":
+ toolbox.register("mate", tools.cxBlend, alpha=mating_dict["alpha"])
+ return toolbox
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..59dad872
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+import setuptools
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+setuptools.setup(
+ name="rollo",
+ version="0.1.1",
+ author="Gwendolyn J.Y. Chee",
+ author_email="gwendolynchee95@gmail.com",
+ description="Reactor Evolutionary Algorithm Optimizer",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/arfc/rollo",
+ packages=setuptools.find_packages(),
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ ],
+ python_requires=">=3.6",
+ install_requires=[
+ "deap",
+ "numpy",
+ "openmc",
+ "jsonschema",
+ "jinja2",
+ ],
+ entry_points={"console_scripts": ["arfc=rollo.__main__:main"]},
+)
diff --git a/tests/input_test_files/generate_backend_pickle.py b/tests/input_test_files/generate_backend_pickle.py
new file mode 100644
index 00000000..84110e76
--- /dev/null
+++ b/tests/input_test_files/generate_backend_pickle.py
@@ -0,0 +1,79 @@
+""" This file creates "test_checkpoint.pkl" that is used in test_backend.py
+for testing purposes
+"""
+
+import pickle
+import random
+from deap import base, creator, tools, algorithms
+import numpy as np
+
+creator.create("obj", base.Fitness, weights=(1.0,))
+creator.create("Ind", list, fitness=creator.obj)
+toolbox = base.Toolbox()
+toolbox.register("pf", random.uniform, 0, 1)
+toolbox.register("poly", random.uniform, 1, 2)
+toolbox.pop_size = 10
+toolbox.ngen = 10
+
+
+def ind_vals():
+ pf = toolbox.pf()
+ poly = toolbox.poly()
+ return creator.Ind([pf, poly, pf + poly])
+
+
+toolbox.register("individual", ind_vals)
+toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+
+
+def evaluator_fn(ind):
+ return tuple([ind[0] + ind[1], 5])
+
+
+toolbox.register("evaluate", evaluator_fn)
+
+
+pop = toolbox.population(n=toolbox.pop_size)
+fitnesses = toolbox.map(toolbox.evaluate, pop)
+for ind, fitness in zip(pop, fitnesses):
+ ind.fitness.values = (fitness[0],)
+ ind.output = fitness
+invalids = [ind for ind in pop if not ind.fitness.valid]
+hof = tools.HallOfFame(maxsize=1)
+hof.update(pop)
+logbook = tools.Logbook()
+stats = tools.Statistics(lambda ind: ind.fitness.values)
+stats.register("avg", np.mean)
+stats.register("max", np.max)
+record = stats.compile(pop)
+gen = 0
+logbook.record(gen=gen, evals=len(invalids), **record)
+all = {}
+all["ind_naming"] = {
+ "packing_fraction": 0,
+ "polynomial_triso_0": 1,
+ "polynomial_triso_1": 2,
+ "polynomial_triso_2": 3,
+ "polynomial_triso_3": 4,
+}
+all["oup_naming"] = {
+ "packing_fraction": 0,
+ "keff": 1,
+ "num_batches": 2,
+ "max_temp": 3,
+}
+all["populations"] = [pop]
+for ind in pop:
+ oup = ind.output
+all["outputs"] = [oup]
+cp = dict(
+ population=pop,
+ generation=gen,
+ halloffame=hof,
+ logbook=logbook,
+ rndstate=random.getstate(),
+ all=all,
+)
+checkpoint_file = "test_checkpoint.pkl"
+with open(checkpoint_file, "wb") as cp_file:
+ pickle.dump(cp, cp_file)
diff --git a/tests/input_test_files/input_test_eval_fn_generator_openmc_output.py b/tests/input_test_files/input_test_eval_fn_generator_openmc_output.py
new file mode 100644
index 00000000..282c2c5a
--- /dev/null
+++ b/tests/input_test_files/input_test_eval_fn_generator_openmc_output.py
@@ -0,0 +1,5 @@
+import openmc
+
+sp = openmc.StatePoint("statepoint.10.h5")
+num_batches = sp.n_batches
+print({"num_batches": num_batches})
diff --git a/tests/input_test_files/input_test_eval_fn_generator_openmc_template.py b/tests/input_test_files/input_test_eval_fn_generator_openmc_template.py
new file mode 100644
index 00000000..bd9c2e7d
--- /dev/null
+++ b/tests/input_test_files/input_test_eval_fn_generator_openmc_template.py
@@ -0,0 +1,155 @@
+import openmc
+import numpy as np
+from numpy import sin, cos, tan, pi
+
+# Templating
+total_pf = {{packing_fraction}}
+poly_coeff = {{polynomial_triso}}
+
+# Constants
+T_r1 = 2135e-5
+T_r2 = 3135e-5
+T_r3 = 3485e-5
+T_r4 = 3835e-5
+T_r5 = 4235e-5
+
+uoc_9 = openmc.Material()
+uoc_9.set_density("g/cc", 11)
+uoc_9.add_nuclide("U235", 2.27325e-3)
+uoc_9.add_nuclide("U238", 2.269476e-2)
+uoc_9.add_nuclide("O16", 3.561871e-2)
+uoc_9.add_nuclide("C0", 9.79714e-3)
+uoc_9.temperature = 1110
+uoc_9.volume = 4 / 3 * pi * (T_r1 ** 3) * 101 * 210 * 4 * 36
+
+por_c = openmc.Material()
+por_c.set_density("g/cc", 1)
+por_c.add_nuclide("C0", 5.013980e-2)
+por_c.temperature = 948
+
+si_c = openmc.Material()
+si_c.set_density("g/cc", 3.2)
+si_c.add_nuclide("Si28", 4.431240e-2)
+si_c.add_nuclide("Si29", 2.25887e-3)
+si_c.add_nuclide("Si30", 1.48990e-3)
+si_c.add_nuclide("C0", 4.806117e-2)
+si_c.temperature = 948
+
+graphite = openmc.Material()
+graphite.set_density("g/cc", 1.8)
+graphite.add_nuclide("C0", 9.025164e-2)
+graphite.temperature = 948
+
+lm_graphite = openmc.Material()
+lm_graphite.set_density("g/cc", 1.8)
+lm_graphite.add_nuclide("C0", 9.025164e-2)
+lm_graphite.temperature = 948
+
+flibe = openmc.Material()
+flibe.set_density("g/cc", 1.95)
+flibe.add_nuclide("Li6", 1.383014e-6)
+flibe.add_nuclide("Li7", 2.37132e-2)
+flibe.add_nuclide("Be9", 1.18573e-2)
+flibe.add_nuclide("F19", 4.74291e-2)
+flibe.temperature = 948
+
+mats = openmc.Materials((uoc_9, por_c, si_c, graphite, lm_graphite, flibe))
+
+spheres = [openmc.Sphere(r=r) for r in [T_r1, T_r2, T_r3, T_r4, T_r5]]
+triso_cells = [
+ openmc.Cell(fill=uoc_9, region=-spheres[0]),
+ openmc.Cell(fill=por_c, region=+spheres[0] & -spheres[1]),
+ openmc.Cell(fill=graphite, region=+spheres[1] & -spheres[2]),
+ openmc.Cell(fill=si_c, region=+spheres[2] & -spheres[3]),
+ openmc.Cell(fill=graphite, region=+spheres[3] & -spheres[4]),
+]
+triso_univ = openmc.Universe(cells=triso_cells)
+
+mats.export_to_xml()
+
+# shape of reactor
+outer = openmc.Cell(fill=graphite)
+z_height = 25
+outer.region = (
+ +openmc.XPlane(x0=0, boundary_type="reflective") &
+ -openmc.XPlane(x0=0.4, boundary_type="reflective") &
+ +openmc.YPlane(y0=0, boundary_type="reflective") &
+ -openmc.YPlane(y0=0.4, boundary_type="reflective") &
+ +openmc.ZPlane(z0=0, boundary_type="reflective") &
+ -openmc.ZPlane(z0=0.2 + z_height, boundary_type="reflective")
+)
+
+# triso PF distribution
+total_core_vol = 1
+vol_triso = 4 / 3 * pi * T_r5 ** 3
+total_trisos = round(total_pf * total_core_vol / vol_triso)
+dz = 10
+z_vals = np.arange(1, dz + 1)
+z = (
+ poly_coeff[0] * z_vals ** 3 +
+ poly_coeff[1] * z_vals ** 2 +
+ poly_coeff[2] * z_vals +
+ poly_coeff[3]
+)
+z_trisos = z / (sum(z)) * total_trisos
+pf_z = z_trisos * vol_triso / (total_core_vol / dz)
+z_thick = z_height / dz
+
+# core
+all_prism_univ = openmc.Universe()
+small_prism = (
+ +openmc.XPlane(x0=0.1) &
+ -openmc.XPlane(x0=0.3) &
+ +openmc.YPlane(y0=0.1) &
+ -openmc.YPlane(y0=0.3) &
+ +openmc.ZPlane(z0=0.1) &
+ -openmc.ZPlane(z0=0.1 + z_thick)
+)
+all_prism_regions = small_prism
+for i in range(dz):
+ prism_region = small_prism
+ # prism_cell = openmc.Cell(fill=lm_graphite,)
+ # print('PACKFRAC',pf_z[i])
+ try:
+ centers = openmc.model.pack_spheres(
+ radius=T_r5, region=prism_region, pf=pf_z[i]
+ )
+ except ZeroDivisionError:
+ centers = []
+ # print('here')
+ trisos = [openmc.model.TRISO(T_r5, triso_univ, c) for c in centers]
+ # print(pf_z[i],len(trisos)*4/3*pi*T_r5**3/(z_thick*0.2*0.2))
+ prism_cell = openmc.Cell(region=prism_region)
+ lower_left, upper_right = prism_cell.region.bounding_box
+ shape = (1, 1, 1)
+ pitch = (upper_right - lower_left) / shape
+ lattice = openmc.model.create_triso_lattice(
+ trisos, lower_left, pitch, shape, lm_graphite
+ )
+ prism_cell.fill = lattice
+ prism_univ = openmc.Universe(cells=(prism_cell,))
+ z_trans = i * z_thick
+ prism_region_new = prism_region.translate((0, 0, z_trans))
+ prism_cell_new = openmc.Cell(fill=prism_univ, region=prism_region_new)
+ prism_cell_new.translation = (0, 0, z_trans)
+ all_prism_univ.add_cell(prism_cell_new)
+ all_prism_regions |= prism_region_new
+prism_areas = openmc.Cell(fill=all_prism_univ, region=all_prism_regions)
+outer.region &= ~all_prism_regions
+# geometry
+univ = openmc.Universe(cells=[outer, prism_areas])
+geom = openmc.Geometry(univ)
+geom.export_to_xml()
+
+# settings
+point = openmc.stats.Point((0.2, 0.2, 12.5))
+src = openmc.Source(space=point)
+settings = openmc.Settings()
+settings.source = src
+settings.batches = 10
+settings.inactive = 2
+settings.particles = 10
+settings.temperature = {"multipole": True, "method": "interpolation"}
+settings.export_to_xml()
+
+openmc.run()
diff --git a/tests/input_test_files/input_test_evaluation_get_output_vals.py b/tests/input_test_files/input_test_evaluation_get_output_vals.py
new file mode 100644
index 00000000..e701b741
--- /dev/null
+++ b/tests/input_test_files/input_test_evaluation_get_output_vals.py
@@ -0,0 +1 @@
+print({"random": 3})
diff --git a/tests/input_test_files/input_test_evaluation_get_output_vals_moltres.py b/tests/input_test_files/input_test_evaluation_get_output_vals_moltres.py
new file mode 100644
index 00000000..cda05f56
--- /dev/null
+++ b/tests/input_test_files/input_test_evaluation_get_output_vals_moltres.py
@@ -0,0 +1 @@
+print({"max_temp": 1000})
diff --git a/tests/input_test_files/input_test_render_jinja_template_python.py b/tests/input_test_files/input_test_render_jinja_template_python.py
new file mode 100644
index 00000000..f104b473
--- /dev/null
+++ b/tests/input_test_files/input_test_render_jinja_template_python.py
@@ -0,0 +1,2 @@
+total_pf = {{packing_fraction}}
+poly_coeff = {{polynomial_triso}}
diff --git a/tests/input_test_files/test_evaluation/statepoint_input_openmc_evaluation.20.h5 b/tests/input_test_files/test_evaluation/statepoint_input_openmc_evaluation.20.h5
new file mode 100644
index 00000000..6eb3fe4e
Binary files /dev/null and b/tests/input_test_files/test_evaluation/statepoint_input_openmc_evaluation.20.h5 differ
diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py
new file mode 100644
index 00000000..c2dbc09f
--- /dev/null
+++ b/tests/test_algorithm.py
@@ -0,0 +1,225 @@
+from rollo.algorithm import Algorithm
+from rollo.constraints import Constraints
+from deap import base, creator, tools
+import random
+import os
+from collections import OrderedDict
+
+creator.create(
+ "obj",
+ base.Fitness,
+ weights=(
+ 1.0,
+ -1.0,
+ ),
+)
+creator.create("Ind", list, fitness=creator.obj)
+toolbox = base.Toolbox()
+toolbox.register("pf", random.uniform, 0, 1)
+toolbox.register("poly", random.uniform, 1, 2)
+toolbox.pop_size = 10
+toolbox.ngen = 10
+toolbox.min_list = [0.0, 1.0, 1.0]
+toolbox.max_list = [1.0, 2.0, 3.0]
+
+
+def ind_vals():
+ pf = toolbox.pf()
+ poly = toolbox.poly()
+ return creator.Ind([pf, poly, pf + poly])
+
+
+toolbox.register("individual", ind_vals)
+toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+k = 5
+toolbox.register("select", tools.selBest, k=k)
+toolbox.register("mate", tools.cxUniform, indpb=1.0)
+toolbox.register(
+ "mutate",
+ tools.mutPolynomialBounded,
+ eta=0.5,
+ indpb=1.0,
+ low=[0.0, 1.0, 1.0],
+ up=[1.0, 2.0, 3.0],
+)
+toolbox.cxpb = 1.0
+toolbox.mutpb = 1.0
+toolbox.objs = 2
+
+control_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+)
+output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+)
+
+
+def evaluator_fn(ind):
+ return tuple([ind[0] + ind[1], 5])
+
+
+toolbox.register("evaluate", evaluator_fn)
+test_constraints = Constraints(
+ output_dict=OrderedDict({"total": "openmc", "random": "openmc"}),
+ input_constraints={
+ "total": {"operator": [">", "<"], "constrained_val": [1.5, 2.5]}
+ },
+ toolbox=toolbox,
+)
+
+
+def test_generate():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ final_pop = a.generate()
+ assert len(final_pop) == toolbox.pop_size
+ for ind in final_pop:
+ for i, val in enumerate(ind):
+ assert val > toolbox.min_list[i]
+ assert val < toolbox.max_list[i]
+ assert ind.fitness.values[0] > 1.5
+ assert ind.fitness.values[0] < 2.5
+ assert len(final_pop) == toolbox.pop_size
+ assert len(a.backend.results["logbook"]) == toolbox.pop_size
+ os.remove("checkpoint.pkl")
+
+
+def test_initialize_pop():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ a.backend.initialize_new_backend()
+ pop = toolbox.population(n=5)
+ new_pop = a.initialize_pop(pop)
+ assert len(new_pop) == len(pop)
+ for i, ind in enumerate(new_pop):
+ assert ind.fitness.values[0] < 3
+ assert ind.fitness.values[0] > 1
+ assert ind.output[1] == 5
+ assert type(ind) is creator.Ind
+ assert ind[0] < 1
+ assert ind[0] > 0
+ assert ind[1] > 1
+ assert ind[1] < 2
+ assert ind.gen == 0
+ assert ind.fitness.values[0] > 1.5
+ assert ind.fitness.values[0] < 2.5
+ os.remove("checkpoint.pkl")
+
+
+def test_apply_algorithm_ngen():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ pop = toolbox.population(n=10)
+ a.backend.initialize_new_backend()
+ new_pop = [toolbox.clone(ind) for ind in pop]
+ new_pop = a.initialize_pop(new_pop)
+ new_pop = a.apply_algorithm_ngen(new_pop, 0)
+
+ for ind in new_pop:
+ for i, val in enumerate(ind):
+ assert val > toolbox.min_list[i]
+ assert val < toolbox.max_list[i]
+ assert ind.fitness.values[0] > 1.5
+ assert ind.fitness.values[0] < 2.5
+ assert len(new_pop) == toolbox.pop_size
+ assert new_pop != pop
+ os.remove("checkpoint.pkl")
+
+
+def test_apply_selection_operator():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ a.backend.initialize_new_backend()
+ pop = toolbox.population(n=toolbox.pop_size)
+ pop = a.initialize_pop(pop)
+ cloned_pop = [toolbox.clone(ind) for ind in pop]
+ selected_pop = a.apply_selection_operator(cloned_pop)
+ expected_inds = [toolbox.clone(ind) for ind in pop]
+ expected_inds.sort(key=lambda x: x[2])
+ expected_inds = expected_inds[k:]
+ for s in selected_pop:
+ assert s in expected_inds
+ os.remove("checkpoint.pkl")
+
+
+def test_apply_mating_operator():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ pop = toolbox.population(n=toolbox.pop_size)
+ mated_pop = [toolbox.clone(ind) for ind in pop]
+ mated_pop = a.apply_mating_operator(mated_pop)
+ for i in range(len(pop)):
+ if i % 2 == 0:
+ assert pop[i] == mated_pop[i + 1]
+
+
+def test_apply_mutation_operator():
+ a = Algorithm(
+ deap_toolbox=toolbox,
+ constraint_obj=test_constraints,
+ checkpoint_file=None,
+ deap_creator=creator,
+ control_dict=control_dict,
+ output_dict=output_dict,
+ input_dict={},
+ start_time=0,
+ parallel_method="none",
+ )
+ pop = toolbox.population(n=toolbox.pop_size)
+ mutated_pop = [toolbox.clone(ind) for ind in pop]
+ mutated_pop = a.apply_mutation_operator(mutated_pop)
+ for j, mutant in enumerate(mutated_pop):
+ for i, val in enumerate(mutant):
+ assert val > toolbox.min_list[i]
+ assert val < toolbox.max_list[i]
+ assert mutant != pop[j]
diff --git a/tests/test_backend.py b/tests/test_backend.py
new file mode 100644
index 00000000..4193bbdc
--- /dev/null
+++ b/tests/test_backend.py
@@ -0,0 +1,160 @@
+from rollo.backend import BackEnd
+from deap import base, creator, tools
+import os
+import random
+from collections import OrderedDict
+
+creator.create(
+ "obj",
+ base.Fitness,
+ weights=(
+ 1.0,
+ -1.0,
+ ),
+)
+creator.create("Ind", list, fitness=creator.obj)
+toolbox = base.Toolbox()
+toolbox.register("pf", random.uniform, 0, 1)
+toolbox.register("poly", random.uniform, 1, 2)
+toolbox.pop_size = 10
+toolbox.ngen = 10
+
+
+def ind_vals():
+ pf = toolbox.pf()
+ poly = toolbox.poly()
+ return creator.Ind([pf, poly, pf + poly])
+
+
+toolbox.register("individual", ind_vals)
+toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+
+
+def evaluator_fn(ind):
+ return tuple([ind[0] + ind[1], 5])
+
+
+toolbox.register("evaluate", evaluator_fn)
+control_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+)
+output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+)
+
+input_file = {} # placeholder
+
+
+def test_initialize_new_backend():
+ b = BackEnd(
+ "square_checkpoint.pkl", creator, control_dict, output_dict, input_file, 0
+ )
+ b.initialize_new_backend()
+ assert b.results["start_gen"] == 0
+ assert type(b.results["halloffame"]) == tools.HallOfFame
+ assert type(b.results["logbook"]) == tools.Logbook
+ assert type(b.results["all"]) == dict
+
+
+def test_ind_naming():
+ b = BackEnd(
+ "square_checkpoint.pkl", creator, control_dict, output_dict, input_file, 0
+ )
+ ind_dict = b.ind_naming()
+ expected_ind_dict = {
+ "packing_fraction": 0,
+ "polynomial_triso_0": 1,
+ "polynomial_triso_1": 2,
+ "polynomial_triso_2": 3,
+ "polynomial_triso_3": 4,
+ }
+ assert ind_dict == expected_ind_dict
+
+
+def test_output_naming():
+ b = BackEnd(
+ "square_checkpoint.pkl", creator, control_dict, output_dict, input_file, 0
+ )
+ oup_dict = b.output_naming()
+ expected_oup_dict = {
+ "packing_fraction": 0,
+ "keff": 1,
+ "num_batches": 2,
+ "max_temp": 3,
+ }
+ assert oup_dict == expected_oup_dict
+
+
+def test_initialize_checkpoint_backend():
+ os.chdir("./input_test_files")
+ os.system("python generate_backend_pickle.py")
+ os.chdir("../")
+ b = BackEnd(
+ "input_test_files/test_checkpoint.pkl",
+ creator,
+ control_dict,
+ output_dict,
+ input_file,
+ 0,
+ )
+ b.initialize_checkpoint_backend()
+ pop = b.results["population"]
+ assert len(pop) == 10
+ for ind in pop:
+ assert ind[0] > 0
+ assert ind[0] < 1
+ assert ind[1] > 1
+ assert ind[1] < 2
+ assert ind.fitness.values[0] > 1
+ assert ind.fitness.values[0] < 3
+ assert b.results["start_gen"] == 0
+ assert b.results["halloffame"].items[0] == max(pop, key=lambda x: x[2])
+ assert type(b.results["logbook"]) == tools.Logbook
+ assert len(b.results["logbook"]) == 1
+ os.remove("./input_test_files/test_checkpoint.pkl")
+
+
+def test_update_backend():
+ os.chdir("./input_test_files")
+ os.system("python generate_backend_pickle.py")
+ os.chdir("../")
+ b = BackEnd(
+ "input_test_files/test_checkpoint.pkl",
+ creator,
+ control_dict,
+ output_dict,
+ input_file,
+ 0,
+ )
+ b.initialize_checkpoint_backend()
+ new_pop = toolbox.population(n=toolbox.pop_size)
+ gen = 1
+ fitnesses = toolbox.map(toolbox.evaluate, new_pop)
+ for ind, fitness in zip(new_pop, fitnesses):
+ ind.fitness.values = (
+ fitness[0],
+ fitness[1],
+ )
+ ind.output = fitness
+ invalids = [ind for ind in new_pop if not ind.fitness.valid]
+ rndstate = random.getstate()
+ b.update_backend(new_pop, gen, invalids, rndstate)
+ pop = b.results["population"]
+ assert b.results["halloffame"].items[0] == max(pop + new_pop, key=lambda x: x[2])
+ assert len(b.results["logbook"]) == 2
+ bb = BackEnd(
+ "input_test_files/test_checkpoint.pkl",
+ creator,
+ control_dict,
+ output_dict,
+ input_file,
+ 0,
+ )
+ bb.initialize_checkpoint_backend()
+ assert len(bb.results["logbook"]) == 2
+ os.remove("./input_test_files/test_checkpoint.pkl")
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
new file mode 100644
index 00000000..7db7b28a
--- /dev/null
+++ b/tests/test_constraints.py
@@ -0,0 +1,71 @@
+import pytest
+from rollo.constraints import Constraints
+from deap import base, creator
+from collections import OrderedDict
+
+test_output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+)
+
+test_input_constraints = {
+ "keff": {"operator": [">=", "<="], "constrained_val": [1, 1.2]},
+ "max_temp": {"operator": ["<"], "constrained_val": [500]},
+}
+toolbox = base.Toolbox()
+
+
+def test_output_dict_numbered():
+ c = Constraints(test_output_dict, test_input_constraints, toolbox)
+ numbered_oup_dict = c.output_dict_numbered(test_output_dict)
+ expected_num_oup_dict = {
+ "packing_fraction": 0,
+ "keff": 1,
+ "num_batches": 2,
+ "max_temp": 3,
+ }
+ assert numbered_oup_dict == expected_num_oup_dict
+
+
+def test_constraints_list():
+ c = Constraints(test_output_dict, test_input_constraints, toolbox)
+ constraints_list = c.constraints_list(test_input_constraints)
+ expected_constraints_list = [
+ ["keff", {"op": ">=", "val": 1}],
+ ["keff", {"op": "<=", "val": 1.2}],
+ ["max_temp", {"op": "<", "val": 500}],
+ ]
+ assert constraints_list == expected_constraints_list
+
+
+def test_apply_constraints():
+ creator.create(
+ "obj",
+ base.Fitness,
+ weights=(
+ -1.0,
+ 1.0,
+ ),
+ )
+ creator.create("Ind", list, fitness=creator.obj)
+ ind1 = creator.Ind([1])
+ ind2 = creator.Ind([2])
+ ind3 = creator.Ind([3])
+ ind4 = creator.Ind([4])
+ ind5 = creator.Ind([5])
+ ind1.output = tuple([0.1, 1.1, 1, 400])
+ ind2.output = tuple([0.1, 1.1, 1, 600])
+ ind3.output = tuple([0.1, 1.4, 1, 400])
+ ind4.output = tuple([0.1, 0.9, 1, 400])
+ ind5.output = tuple([0.1, 1.15, 2, 450])
+ pop = [ind1, ind2, ind3, ind4, ind5]
+ c = Constraints(test_output_dict, test_input_constraints, toolbox)
+ new_pop = c.apply_constraints(pop)
+ expected_pop = [ind1, ind5]
+ for ind in new_pop:
+ assert ind in expected_pop
+ assert len(new_pop) == len(pop)
diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py
new file mode 100644
index 00000000..a2e52889
--- /dev/null
+++ b/tests/test_evaluation.py
@@ -0,0 +1,117 @@
+import pytest
+import os
+import shutil
+from rollo.evaluation import Evaluation
+from collections import OrderedDict
+from deap import base, creator
+
+
+def test_eval_fn_generator():
+ os.chdir("./input_test_files")
+ if os.path.exists("./openmc_0_0"):
+ shutil.rmtree("./openmc_0_0")
+ if os.path.exists("./moltres_0_0"):
+ shutil.rmtree("./moltres_0_0")
+ ev = Evaluation()
+ ev.add_evaluator(
+ solver_name="openmc",
+ input_script="input_test_eval_fn_generator_openmc_template.py",
+ output_script="input_test_eval_fn_generator_openmc_output.py",
+ )
+ ev.add_evaluator(
+ solver_name="moltres",
+ input_script="input_test_render_jinja_template_python.py",
+ output_script="input_test_evaluation_get_output_vals_moltres.py",
+ )
+ eval_function = ev.eval_fn_generator(
+ control_dict=OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ ),
+ output_dict=OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "max_temp": "moltres",
+ "num_batches": "openmc",
+ }
+ ),
+ input_evaluators={
+ "openmc": {"keep_files": True},
+ "moltres": {"keep_files": True},
+ },
+ )
+
+ creator.create("obj", base.Fitness, weights=(-1.0,))
+ creator.create("Ind", list, fitness=creator.obj)
+ ind = creator.Ind([0.03, 1, 1, 1, 1])
+ ind.gen = 0
+ ind.num = 0
+ output_vals = eval_function(ind)
+ expected_output_vals = tuple([0.03, output_vals[1], 1000, 10])
+ shutil.rmtree("./openmc_0_0")
+ shutil.rmtree("./moltres_0_0")
+ os.chdir("../")
+ assert output_vals == expected_output_vals
+
+
+def test_get_output_vals():
+ os.chdir("./input_test_files")
+ ev = Evaluation()
+ ev.add_evaluator(
+ solver_name="openmc",
+ input_script="placeholder.py",
+ output_script="input_test_evaluation_get_output_vals.py",
+ )
+ output_vals = ev.get_output_vals(
+ output_vals=[None] * 4,
+ solver="openmc",
+ output_dict=OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "max_temp": "moltres",
+ "random": "openmc",
+ }
+ ),
+ control_vars={
+ "openmc": {"packing_fraction": 0.03},
+ "moltres": {"polynomial_triso": [1, 1, 1, 1]},
+ },
+ path="./test_evaluation/",
+ )
+ expected_output_vals = [0.03, 1.6331797843041689, None, 3]
+ os.remove("./test_evaluation/openmc_output.py")
+ os.chdir("../")
+ assert output_vals == expected_output_vals
+
+
+def test_name_ind():
+ ev = Evaluation()
+ control_vars = ev.name_ind(
+ ind=[0.01, 1, 1, 1, 1],
+ control_dict=OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["moltres", 4]}
+ ),
+ input_evaluators=["openmc", "moltres"],
+ )
+ expected_control_vars = {
+ "openmc": {"packing_fraction": 0.01},
+ "moltres": {"polynomial_triso": [1, 1, 1, 1]},
+ }
+ assert control_vars == expected_control_vars
+
+
+def test_render_jinja_template_python():
+ os.chdir("./input_test_files")
+ ev = Evaluation()
+ rendered_template = ev.render_jinja_template_python(
+ script="./input_test_render_jinja_template_python.py",
+ control_vars_solver={
+ "packing_fraction": 0.01,
+ "polynomial_triso": [1, 1, 1, 1],
+ },
+ )
+ print(rendered_template)
+ expected_rendered_template = "total_pf = 0.01\npoly_coeff = [1, 1, 1, 1]"
+ os.chdir("../")
+ assert rendered_template == expected_rendered_template
diff --git a/tests/test_executor.py b/tests/test_executor.py
new file mode 100644
index 00000000..aba189c7
--- /dev/null
+++ b/tests/test_executor.py
@@ -0,0 +1,158 @@
+import pytest
+import os
+import shutil
+from rollo.executor import Executor
+from collections import OrderedDict
+from deap import base, creator
+from rollo.constraints import Constraints
+
+test_input_dict = {
+ "control_variables": {
+ "packing_fraction": {"min": 0.005, "max": 0.1},
+ "polynomial_triso": {
+ "order": 3,
+ "min": 1,
+ "max": 1,
+ "radius": 4235e-5,
+ "volume": 10,
+ "slices": 10,
+ "height": 10,
+ },
+ },
+ "evaluators": {
+ "openmc": {
+ "input_script": "input_test_eval_fn_generator_openmc_template.py",
+ "inputs": ["packing_fraction", "polynomial_triso"],
+ "outputs": ["packing_fraction", "keff", "num_batches"],
+ "output_script": "input_test_eval_fn_generator_openmc_output.py",
+ "keep_files": True,
+ },
+ "moltres": {
+ "input_script": "input_test_render_jinja_template_python.py",
+ "inputs": [],
+ "outputs": ["max_temp"],
+ "output_script": "input_test_evaluation_get_output_vals_moltres.py",
+ "keep_files": True,
+ },
+ },
+ "constraints": {"keff": {"operator": [">"], "constrained_val": [1]}},
+ "algorithm": {
+ "objective": ["max", "min"],
+ "optimized_variable": ["keff", "packing_fraction"],
+ "pop_size": 100,
+ "generations": 10,
+ "mutation_probability": 0.5,
+ "mating_probability": 0.5,
+ "selection_operator": {"operator": "selBest", "inds": 1},
+ "mutation_operator": {
+ "operator": "mutGaussian",
+ "indpb": 0.5,
+ "mu": 0.5,
+ "sigma": 0.5,
+ },
+ "mating_operator": {"operator": "cxOnePoint"},
+ },
+}
+
+
+def test_organize_input_output():
+ e = Executor("input_file_placeholder")
+ ctrl_dict, output_dict = e.organize_input_output(test_input_dict)
+ expected_ctrl_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ )
+ expected_output_dict = OrderedDict(
+ {
+ "keff": "openmc",
+ "packing_fraction": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+ )
+ assert ctrl_dict == expected_ctrl_dict
+ assert output_dict == expected_output_dict
+
+
+def test_load_evaluator():
+ os.chdir("./input_test_files")
+ if os.path.exists("./openmc_0_0"):
+ shutil.rmtree("./openmc_0_0")
+ if os.path.exists("./moltres_0_0"):
+ shutil.rmtree("./moltres_0_0")
+ e = Executor("input_file_placeholder")
+ test_control_dict, test_output_dict = e.organize_input_output(test_input_dict)
+ eval_function = e.load_evaluator(
+ control_dict=test_control_dict,
+ output_dict=test_output_dict,
+ input_dict=test_input_dict,
+ )
+ creator.create(
+ "obj",
+ base.Fitness,
+ weights=(
+ 1.0,
+ -1.0,
+ ),
+ )
+ creator.create("Ind", list, fitness=creator.obj)
+ ind = creator.Ind([0.03, 1, 1, 1, 1])
+ ind.gen = 0
+ ind.num = 0
+ output_vals = eval_function(ind)
+ expected_output_vals = tuple([output_vals[0], 0.03, 10, 1000])
+ shutil.rmtree("./openmc_0_0")
+ shutil.rmtree("./moltres_0_0")
+ os.chdir("../")
+ assert output_vals == expected_output_vals
+
+
+def test_load_toolbox():
+ e = Executor("input_file_placeholder")
+ ctrl_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ )
+ output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+ )
+
+ def test_evaluator_fn():
+ return tuple([1, 1])
+
+ toolbox, creator = e.load_toolbox(
+ evaluator_fn=test_evaluator_fn,
+ input_algorithm=test_input_dict["algorithm"],
+ input_ctrl_vars=test_input_dict["control_variables"],
+ control_dict=ctrl_dict,
+ )
+
+ test_toolbox_ind = toolbox.individual()
+ assert type(test_toolbox_ind) == creator.Ind
+ test_toolbox_pop = toolbox.population(n=10)
+ assert type(test_toolbox_pop) == list
+ test_toolbox_eval = toolbox.evaluate()
+ assert test_toolbox_eval == tuple([1, 1])
+ assert toolbox.pop_size == 100
+ assert toolbox.ngen == 10
+ assert toolbox.mutpb == 0.5
+ assert toolbox.cxpb == 0.5
+
+
+def test_load_constraints():
+ output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+ )
+ e = Executor("input_file_placeholder")
+ constraint_obj = e.load_constraints(
+ output_dict, test_input_dict["constraints"], base.Toolbox()
+ )
+ assert type(constraint_obj) == Constraints
diff --git a/tests/test_openmc_evaluation.py b/tests/test_openmc_evaluation.py
new file mode 100644
index 00000000..acb781d0
--- /dev/null
+++ b/tests/test_openmc_evaluation.py
@@ -0,0 +1,13 @@
+import pytest
+import os
+from rollo.openmc_evaluation import OpenMCEvaluation
+
+
+def test_evaluate_keff():
+
+ oe = OpenMCEvaluation()
+ os.chdir("./input_test_files/test_evaluation/")
+ keff = oe.evaluate_keff()
+ expected_keff = 1.6331797843041689
+ os.chdir("../")
+ assert keff == expected_keff
diff --git a/tests/test_special_variables.py b/tests/test_special_variables.py
new file mode 100644
index 00000000..a9a29933
--- /dev/null
+++ b/tests/test_special_variables.py
@@ -0,0 +1,49 @@
+import pytest
+import numpy as np
+from rollo.special_variables import SpecialVariables
+
+poly_dict = {
+ "name": "triso",
+ "order": 3,
+ "min": 1,
+ "max": 1,
+ "radius": 4235e-5,
+ "volume": 10,
+ "slices": 10,
+ "height": 10,
+}
+
+
+def test_polynomial_triso_num():
+ sv = SpecialVariables()
+ assert sv.polynomial_triso_num(poly_dict) == 4
+
+
+def test_polynomial_triso_values():
+ poly_dict = {
+ "name": "triso",
+ "order": 3,
+ "min": -1,
+ "max": 1,
+ "radius": 4235e-5,
+ "volume": 10,
+ "slices": 10,
+ "height": 10,
+ }
+
+ var_dict = {"packing_fraction": 0.1}
+ sv = SpecialVariables()
+ dz_vals = np.linspace(0, poly_dict["height"], poly_dict["slices"])
+ vol_triso = 4 / 3 * np.pi * poly_dict["radius"] ** 3
+ no_trisos = var_dict["packing_fraction"] * poly_dict["volume"] / vol_triso
+ for i in range(100):
+ poly = sv.polynomial_triso_values(poly_dict, var_dict)
+ poly_val = (
+ poly[0] * dz_vals ** 3 +
+ poly[1] * dz_vals ** 2 +
+ poly[2] * dz_vals +
+ poly[3]
+ )
+ pf_z = poly_val / sum(poly_val) * no_trisos * vol_triso / poly_dict["volume"]
+ assert len([i for i in pf_z if i > 0.25]) == 0
+ assert len([i for i in poly_val if i < 0]) == 0
diff --git a/tests/test_toolbox_generator.py b/tests/test_toolbox_generator.py
new file mode 100644
index 00000000..130f9da8
--- /dev/null
+++ b/tests/test_toolbox_generator.py
@@ -0,0 +1,210 @@
+from rollo.toolbox_generator import ToolboxGenerator
+from deap import base, creator, tools
+import random
+import copy
+from collections import OrderedDict
+
+
+test_input_dict = {
+ "control_variables": {
+ "packing_fraction": {"min": 0.005, "max": 0.1},
+ "polynomial_triso": {
+ "order": 3,
+ "min": 1,
+ "max": 1,
+ "radius": 4235e-5,
+ "volume": 10,
+ "slices": 10,
+ "height": 10,
+ },
+ },
+ "evaluators": {
+ "openmc": {
+ "input_script": "input_test_eval_fn_generator_openmc_template.py",
+ "inputs": ["packing_fraction", "polynomial_triso"],
+ "outputs": ["packing_fraction", "keff", "num_batches"],
+ "output_script": "input_test_eval_fn_generator_openmc_output.py",
+ },
+ "moltres": {
+ "input_script": "input_test_render_jinja_template_python.py",
+ "inputs": [],
+ "outputs": ["max_temp"],
+ "output_script": "input_test_evaluation_get_output_vals_moltres.py",
+ },
+ },
+ "constraints": {"keff": {"operator": ">", "constrained_val": 1}},
+ "algorithm": {
+ "objective": ["max", "min"],
+ "optimized_variable": ["keff", "packing_fraction"],
+ "pop_size": 100,
+ "generations": 10,
+ "mutation_probability": 0.5,
+ "mating_probability": 0.5,
+ "selection_operator": {"operator": "selBest", "inds": 1},
+ "mutation_operator": {
+ "operator": "mutGaussian",
+ "indpb": 0.5,
+ "mu": 0.5,
+ "sigma": 0.5,
+ },
+ "mating_operator": {"operator": "cxOnePoint"},
+ },
+}
+
+
+def test_setup():
+ tg = ToolboxGenerator()
+ ctrl_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ )
+ output_dict = OrderedDict(
+ {
+ "packing_fraction": "openmc",
+ "keff": "openmc",
+ "num_batches": "openmc",
+ "max_temp": "moltres",
+ }
+ )
+
+ def test_evaluator_fn():
+ return tuple([1, 1])
+
+ toolbox, creator = tg.setup(
+ evaluator_fn=test_evaluator_fn,
+ input_algorithm=test_input_dict["algorithm"],
+ input_ctrl_vars=test_input_dict["control_variables"],
+ control_dict=ctrl_dict,
+ )
+
+ test_toolbox_ind = toolbox.individual()
+ assert type(test_toolbox_ind) == creator.Ind
+ test_toolbox_pop = toolbox.population(n=10)
+ assert type(test_toolbox_pop) == list
+ test_toolbox_eval = toolbox.evaluate()
+ assert test_toolbox_eval == tuple([1, 1])
+ assert toolbox.pop_size == 100
+ assert toolbox.ngen == 10
+ assert toolbox.mutpb == 0.5
+ assert toolbox.cxpb == 0.5
+ assert toolbox.objs == 2
+
+
+def test_individual_values():
+ tg = ToolboxGenerator()
+ ctrl_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ )
+ poly_dict = test_input_dict["control_variables"]["polynomial_triso"]
+ toolbox = base.Toolbox()
+ creator.create("obj", base.Fitness, weights=(-1.0,))
+ creator.create("Ind", list, fitness=creator.obj)
+ toolbox.register("packing_fraction", random.uniform, 0.005, 0.1)
+ toolbox.register("polynomial_triso", random.uniform, 1, 1)
+ ind_values = tg.individual_values(
+ test_input_dict["control_variables"], ctrl_dict, toolbox
+ )
+ assert type(ind_values) is creator.Ind
+ assert ind_values[0] >= 0.005
+ assert ind_values[0] <= 0.1
+ for i in range(1, 4):
+ ind_values[i] >= -1
+ ind_values[i] <= 1
+
+
+def test_min_max_list():
+ tg = ToolboxGenerator()
+ ctrl_dict = OrderedDict(
+ {"packing_fraction": ["openmc", 1], "polynomial_triso": ["openmc", 4]}
+ )
+ min_list, max_list = tg.min_max_list(
+ ctrl_dict, test_input_dict["control_variables"]
+ )
+ expected_min_list = [0.005, 1, 1, 1, 1]
+ expected_max_list = [0.1, 1, 1, 1, 1]
+
+
+def test_add_selection_operators():
+ tg = ToolboxGenerator()
+ selection_dict_list = [
+ {"operator": "selTournament", "inds": 5, "tournsize": 2},
+ {"operator": "selNSGA2", "inds": 5},
+ {"operator": "selBest", "inds": 5},
+ ]
+ creator.create("obj", base.Fitness, weights=(-1.0,))
+ creator.create("Ind", list, fitness=creator.obj)
+
+ def f_cycle():
+ return creator.Ind([toolbox.num()])
+
+ for i in range(100):
+ for selection_dict in selection_dict_list:
+ toolbox = base.Toolbox()
+ toolbox.register("num", random.uniform, -1, 1)
+ toolbox.register("individual", f_cycle)
+ toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+ pop = toolbox.population(n=10)
+ toolbox = tg.add_selection_operators(toolbox, selection_dict)
+ expected_inds = []
+ for i, ind in enumerate(pop):
+ if (i % 2) == 0:
+ ind.fitness.values = (1,)
+ else:
+ ind.fitness.values = (0,)
+ expected_inds.append(ind)
+ new_pop = toolbox.select(pop)
+ assert "select" in dir(toolbox)
+ if selection_dict["operator"] == "selBest":
+ assert new_pop == expected_inds
+ else:
+ assert len(new_pop) == len(expected_inds)
+
+
+def test_add_mutation_operators():
+ tg = ToolboxGenerator()
+ toolbox = base.Toolbox()
+ mutation_dict_list = [
+ {"operator": "mutPolynomialBounded", "eta": 0.5, "indpb": 0.5},
+ ]
+ creator.create("obj", base.Fitness, weights=(-1.0,))
+ creator.create("Ind", list, fitness=creator.obj)
+ min_list = [0.005, 0.1, 0.1, 0.1, 0.1]
+ max_list = [0.1, 1, 1, 1, 1]
+
+ for j in range(100):
+ for mutation_dict in mutation_dict_list:
+ ind = creator.Ind([0.05, 0.9, 0.9, 0.8, 0.7])
+ toolbox = tg.add_mutation_operators(
+ toolbox, mutation_dict, min_list, max_list
+ )
+ mutated = toolbox.mutate(ind)
+ assert "mutate" in dir(toolbox)
+ assert mutated != ind
+
+
+def test_add_mating_operators():
+ tg = ToolboxGenerator()
+ toolbox = base.Toolbox()
+ mating_dict_list = [
+ {"operator": "cxOnePoint"},
+ {"operator": "cxUniform", "indpb": 0.5},
+ {"operator": "cxBlend", "alpha": 0.5},
+ ]
+ creator.create("obj", base.Fitness, weights=(-1.0,))
+ creator.create("Ind", list, fitness=creator.obj)
+ toolbox.register("num", random.uniform, -1, 1)
+
+ def f_cycle():
+ return creator.Ind([toolbox.num(), toolbox.num(), toolbox.num(), toolbox.num()])
+
+ toolbox.register("individual", f_cycle)
+ toolbox.register("population", tools.initRepeat, list, toolbox.individual)
+ starting_pop = toolbox.population(n=2)
+ pop = copy.deepcopy(starting_pop)
+ for i in range(100):
+ for mating_dict in mating_dict_list:
+ new_pop = []
+ for child1, child2 in zip(pop[::2], pop[1::2]):
+ toolbox = tg.add_mating_operators(toolbox, mating_dict)
+ new_pop += toolbox.mate(child1, child2)
+ assert "mate" in dir(toolbox)
+ assert len(new_pop) == len(pop)