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 this file 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)