Skip to content
Snippets Groups Projects
Commit b4710b7a authored by istkabra's avatar istkabra
Browse files

* Continuing massive refactor

  * Implementing the settings neatly at the right place
parent 009776c2
Branches
Tags
No related merge requests found
package Enums;
public enum InitializationMethods {
Curl,
Straight,
Random
}
package Enums;
public enum MutatorMethods {
SinglePoint,
Crossover
}
package Enums; package Enums;
public enum Selection { public enum SelectionMethods {
Proportional, Proportional,
Tournament, Tournament,
OnlyBest OnlyBest
......
package Enums;
public enum VisualizerMethods {
Console,
Image,
Video
}
package MainClasses; package MainClasses;
import Enums.Selection; import Enums.InitializationMethods;
import Enums.MutatorMethods;
import Enums.SelectionMethods;
import Enums.VisualizerMethods;
import java.awt.*; import java.awt.*;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
...@@ -13,18 +16,33 @@ public class Config { ...@@ -13,18 +16,33 @@ public class Config {
String propertyPath; String propertyPath;
Properties properties; Properties properties;
static String LOGFILE; static String ENCODING_VARIANT;
static int SEED;
static int POPULATION_SIZE; static int POPULATION_SIZE;
static int TOTAL_GENERATIONS; static int TOTAL_GENERATIONS;
static InitializationMethods INITIALIZATION_METHOD;
static SelectionMethods SELECTION_METHOD;
static int K; // Number of selected Candidates to face off in a tournament selection
static MutatorMethods[] MUTATOR_METHODS;
static int POINTS_PER_BOND; // Points per hydrophobic bond, default Evaluator will work the same with any value
static int MUTATION_ATTEMPTS_PER_CANDIDATE; static int MUTATION_ATTEMPTS_PER_CANDIDATE;
static double MUTATION_CHANCE; static double MUTATION_CHANCE;
static double MUTATION_MULTIPLIER; static double MUTATION_MULTIPLIER;
static int CROSSOVER_ATTEMPTS_PER_CANDIDATE; static int CROSSOVER_ATTEMPTS_PER_CANDIDATE;
static double CROSSOVER_CHANCE; static double CROSSOVER_CHANCE;
static double CROSSOVER_MULTIPLIER; static double CROSSOVER_MULTIPLIER;
static Selection SELECTION_VARIANT;
static int K; // Number of selected Candidates to face off in a tournament selection static String LOGFILE;
static VisualizerMethods[] VISUALIZERS;
static String IMAGE_SEQUENCE_PATH; static String IMAGE_SEQUENCE_PATH;
static String VIDEO_PATH_AND_FILE;
static int IMAGE_INTERVAL;
static double IMAGE_INTERVAL_DECLINE;
static int IMAGE_INTERVAL_MIN;
static boolean ZOOM;
// For images // For images
public static final Font font = new Font("Sans-Serif", Font.PLAIN, 15); public static final Font font = new Font("Sans-Serif", Font.PLAIN, 15);
...@@ -47,9 +65,6 @@ public class Config { ...@@ -47,9 +65,6 @@ public class Config {
public static final String consoleConnectionVertical = " | "; public static final String consoleConnectionVertical = " | ";
public static final String consoleConnectionHorizontal = "---"; public static final String consoleConnectionHorizontal = "---";
// Points per hydrophobic bond
static int POINTS_PER_BOND;
public Config(String propertyPath) { public Config(String propertyPath) {
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.properties = this.readProperties(); this.properties = this.readProperties();
...@@ -71,35 +86,76 @@ public class Config { ...@@ -71,35 +86,76 @@ public class Config {
} }
private void initializeProperties() { private void initializeProperties() {
LOGFILE = this.properties.getProperty("logfilePath");
// Basic Initialization settings
ENCODING_VARIANT = this.properties.getProperty("encodingVatiant");
SEED = Integer.parseInt(this.properties.getProperty("seed"));
// Algorithm settings
POPULATION_SIZE = Integer.parseInt(this.properties.getProperty("populationSize")); POPULATION_SIZE = Integer.parseInt(this.properties.getProperty("populationSize"));
TOTAL_GENERATIONS = Integer.parseInt(this.properties.getProperty("noGenerations")); TOTAL_GENERATIONS = Integer.parseInt(this.properties.getProperty("noGenerations"));
if (this.properties.getProperty("initializationMethod").equals("curl")) {
INITIALIZATION_METHOD = InitializationMethods.Curl;
} else if (this.properties.getProperty("initializationMethod").equals("straight")) {
INITIALIZATION_METHOD = InitializationMethods.Straight;
}else if (this.properties.getProperty("initializationMethod").equals("random")) {
INITIALIZATION_METHOD = InitializationMethods.Random;
}
if (this.properties.getProperty("selectionMethod").equals("proportional")) {
SELECTION_METHOD = SelectionMethods.Proportional;
} else if (this.properties.getProperty("selectionMethod").equals("tournament")) {
SELECTION_METHOD = SelectionMethods.Tournament;
} else if (this.properties.getProperty("selectionMethod").equals("onlybest")) {
SELECTION_METHOD = SelectionMethods.OnlyBest;
}
K = Integer.parseInt(this.properties.getProperty("k"));
String[] mutatorsToUse = this.properties.getProperty("mutatorMethods").split(",");
MUTATOR_METHODS = new MutatorMethods[mutatorsToUse.length];
for (int i = 0; i < mutatorsToUse.length; i++) {
if (mutatorsToUse[i].equals("singlePoint")) {
MUTATOR_METHODS[i] = MutatorMethods.SinglePoint;
} else if (mutatorsToUse[i].equals("crossover")) {
MUTATOR_METHODS[i] = MutatorMethods.Crossover;
}
}
POINTS_PER_BOND = Integer.parseInt(this.properties.getProperty("pointsPerBond"));
// Mutation settings
MUTATION_ATTEMPTS_PER_CANDIDATE = Integer.parseInt(this.properties.getProperty("mutationAttemptsPerCandidate")); MUTATION_ATTEMPTS_PER_CANDIDATE = Integer.parseInt(this.properties.getProperty("mutationAttemptsPerCandidate"));
MUTATION_CHANCE = Double.parseDouble(this.properties.getProperty("mutationChance")); MUTATION_CHANCE = Double.parseDouble(this.properties.getProperty("mutationChance"));
MUTATION_MULTIPLIER = Double.parseDouble(this.properties.getProperty("mutationDecline")); MUTATION_MULTIPLIER = Double.parseDouble(this.properties.getProperty("mutationMultiplier"));
CROSSOVER_ATTEMPTS_PER_CANDIDATE = Integer.parseInt(this.properties.getProperty("crossoverAttemptsPerCandidate")); CROSSOVER_ATTEMPTS_PER_CANDIDATE = Integer.parseInt(this.properties.getProperty("crossoverAttemptsPerCandidate"));
CROSSOVER_CHANCE = Double.parseDouble(this.properties.getProperty("crossoverChance")); CROSSOVER_CHANCE = Double.parseDouble(this.properties.getProperty("crossoverChance"));
CROSSOVER_MULTIPLIER = Double.parseDouble(this.properties.getProperty("crossoverDecline")); CROSSOVER_MULTIPLIER = Double.parseDouble(this.properties.getProperty("crossoverMultiplier"));
K = Integer.parseInt(this.properties.getProperty("k"));
try { // Output settings
if (this.properties.getProperty("selection").equals("proportional")) { LOGFILE = this.properties.getProperty("logfilePath");
SELECTION_VARIANT = Selection.Proportional;
} else if (this.properties.getProperty("selection").equals("tournament")) { String[] visualizersToUse = this.properties.getProperty("visualizerType").split(",");
SELECTION_VARIANT = Selection.Tournament; VISUALIZERS = new VisualizerMethods[visualizersToUse.length];
} else if (this.properties.getProperty("selection").equals("onlybest")) { for (int i = 0; i < visualizersToUse.length; i++) {
SELECTION_VARIANT = Selection.Tournament; if (visualizersToUse[i].equals("console")) {
} else { VISUALIZERS[i] = VisualizerMethods.Console;
throw new Exception("Selection variant not found!"); } else if (visualizersToUse[i].equals("image")) {
VISUALIZERS[i] = VisualizerMethods.Image;
} else if (visualizersToUse[i].equals("video")) {
VISUALIZERS[i] = VisualizerMethods.Video;
} }
} catch (Exception e) {
e.printStackTrace();
} }
IMAGE_SEQUENCE_PATH = properties.getProperty("imageSequencePath"); IMAGE_SEQUENCE_PATH = this.properties.getProperty("imageSequencePath");
POINTS_PER_BOND = Integer.parseInt(this.properties.getProperty("pointsPerBond")); VIDEO_PATH_AND_FILE = this.properties.getProperty("videoPathAndFile");
IMAGE_INTERVAL = Integer.parseInt(this.properties.getProperty("imgInterval"));
IMAGE_INTERVAL_DECLINE = Double.parseDouble(this.properties.getProperty("imgIntervalDecline"));
IMAGE_INTERVAL_MIN = Integer.parseInt(this.properties.getProperty("imgIntervalMin"));
ZOOM = this.properties.getProperty("zoom").equals("true");
} }
public Properties getProperties() { public Properties getProperties() {
......
package MainClasses; package MainClasses;
import Enums.DirectionNESW; import Enums.*;
import Evaluators.EvaluatorNESW; import Evaluators.EvaluatorNESW;
import InitialGenerationCreators.Curl; import InitialGenerationCreators.Curl;
import InitialGenerationCreators.RandomDirection;
import InitialGenerationCreators.StraightLine;
import Interfaces.*; import Interfaces.*;
import Mutators.Crossover; import Mutators.Crossover;
import Mutators.SinglePoint; import Mutators.SinglePoint;
import Selectors.FitnessProportional;
import Selectors.OnlyBest; import Selectors.OnlyBest;
import Selectors.Tournament;
import Visualization.Visualizers.VisualizerNESWtoConsole; import Visualization.Visualizers.VisualizerNESWtoConsole;
import Visualization.Visualizers.VisualizerNESWtoFile; import Visualization.Visualizers.VisualizerNESWtoFile;
...@@ -17,8 +21,7 @@ import java.nio.file.StandardOpenOption; ...@@ -17,8 +21,7 @@ import java.nio.file.StandardOpenOption;
import java.util.Random; import java.util.Random;
public class GeneticAlgorithm { public class GeneticAlgorithm {
Random rand = new Random(); Random rand;
Visualizer visualizer;
int[] isHydrophobic; int[] isHydrophobic;
Candidate[] population; Candidate[] population;
...@@ -32,32 +35,86 @@ public class GeneticAlgorithm { ...@@ -32,32 +35,86 @@ public class GeneticAlgorithm {
InitialGenerationCreator initialGenCreator; InitialGenerationCreator initialGenCreator;
Mutator[] mutators; Mutator[] mutators;
Selector selector; Selector selector;
Evaluator evaluator; Evaluator evaluator;
Visualizer[] visualizers;
// Initialize with protein // Initialize with protein
public GeneticAlgorithm (int[] protein) { public GeneticAlgorithm (int[] protein) {
this.isHydrophobic = protein; this.isHydrophobic = protein;
// this.visualizer = new VisualizerNESWtoConsole();
this.visualizer = new VisualizerNESWtoFile(Config.IMAGE_SEQUENCE_PATH);
// this.initialGenCreator = new RandomDirection<>(this.rand, DirectionNESW.class); this.initializeSettings();
// this.initialGenCreator = new StraightLine(); this.clearLog();
this.initialGenCreator = new Curl<>(DirectionNESW.class);
this.population = this.initialGenCreator.initializeDirections(Config.POPULATION_SIZE, this.isHydrophobic);
this.totalFitness = 0;
this.fitness = new double[Config.POPULATION_SIZE];
this.overallBestFitness = 0;
}
private void initializeSettings() {
if (Config.SEED != -1) {
this.rand = new Random(Config.SEED);
} else {
this.rand = new Random();
}
// Settings that are dependant on encoding
if (Config.ENCODING_VARIANT.equals("NESW")) {
int nullCount = 0;
for (int i = 0; i < Config.VISUALIZERS.length; i++) {
if (!Config.VISUALIZERS[i].equals(VisualizerMethods.Console)
&& !Config.VISUALIZERS[i].equals(VisualizerMethods.Image)) {
nullCount++;
}
}
this.visualizers = new Visualizer[Config.VISUALIZERS.length - nullCount];
int j = 0;
for (VisualizerMethods vm : Config.VISUALIZERS) {
if (vm.equals(VisualizerMethods.Console)) {
this.visualizers[j] = new VisualizerNESWtoConsole();
j++;
} else if (vm.equals(VisualizerMethods.Image)) {
this.visualizers[j] = new VisualizerNESWtoFile(Config.IMAGE_SEQUENCE_PATH);
j++;
}
}
if (Config.INITIALIZATION_METHOD.equals(InitializationMethods.Curl)) {
this.initialGenCreator = new Curl<>(DirectionNESW.class);
} else if (Config.INITIALIZATION_METHOD.equals(InitializationMethods.Straight)) {
this.initialGenCreator = new StraightLine();
} else if (Config.INITIALIZATION_METHOD.equals(InitializationMethods.Random)) {
this.initialGenCreator = new RandomDirection<>(DirectionNESW.class, this.rand);
}
this.mutators = new Mutator[Config.MUTATOR_METHODS.length];
for (int i = 0; i < Config.MUTATOR_METHODS.length; i++) {
if (Config.MUTATOR_METHODS[i].equals(MutatorMethods.SinglePoint)) {
this.mutators[i] = new SinglePoint<>(DirectionNESW.class, this.rand,
Config.MUTATION_ATTEMPTS_PER_CANDIDATE, Config.MUTATION_CHANCE, Config.MUTATION_MULTIPLIER);
this.mutators = new Mutator[2]; } else if (Config.MUTATOR_METHODS[i].equals(MutatorMethods.Crossover)) {
this.mutators[0] = new SinglePoint<>(DirectionNESW.class, this.rand, Config.MUTATION_ATTEMPTS_PER_CANDIDATE, this.mutators[i] = new Crossover<>(DirectionNESW.class, this.rand,
Config.MUTATION_CHANCE, Config.MUTATION_MULTIPLIER); Config.CROSSOVER_ATTEMPTS_PER_CANDIDATE, Config.CROSSOVER_CHANCE, Config.CROSSOVER_MULTIPLIER);
this.mutators[1] = new Crossover<>(DirectionNESW.class, this.rand, Config.CROSSOVER_ATTEMPTS_PER_CANDIDATE, }
Config.CROSSOVER_CHANCE, Config.CROSSOVER_MULTIPLIER); }
this.evaluator = new EvaluatorNESW(Config.POINTS_PER_BOND);
// this.selector = new FitnessProportional(this.rand, this.isHydrophobic); } else {
// this.selector = new Tournament(this.rand, this.isHydrophobic, this.k); // TODO: initialization for FRL settings
this.selector = new OnlyBest(this.isHydrophobic); }
this.evaluator = new EvaluatorNESW(Config.POINTS_PER_BOND); if (Config.SELECTION_METHOD.equals(SelectionMethods.Proportional)) {
this.selector = new FitnessProportional(this.rand, this.isHydrophobic);
} else if (Config.SELECTION_METHOD.equals(SelectionMethods.Tournament)) {
this.selector = new Tournament(this.rand, this.isHydrophobic, Config.K);
} else if (Config.SELECTION_METHOD.equals(SelectionMethods.OnlyBest)) {
this.selector = new OnlyBest(this.isHydrophobic);
}
}
// Clear log file private void clearLog() {
String content = "Generation\tAverage Fitness\tBest Fitness\tOverall Best Fitness\tBonds\tOverlaps\n"; String content = "Generation\tAverage Fitness\tBest Fitness\tOverall Best Fitness\tBonds\tOverlaps\n";
try { try {
Files.write(Paths.get(Config.LOGFILE), content.getBytes()); Files.write(Paths.get(Config.LOGFILE), content.getBytes());
...@@ -65,11 +122,6 @@ public class GeneticAlgorithm { ...@@ -65,11 +122,6 @@ public class GeneticAlgorithm {
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
this.population = this.initialGenCreator.initializeDirections(Config.POPULATION_SIZE, this.isHydrophobic);
this.totalFitness = 0;
this.fitness = new double[Config.POPULATION_SIZE];
this.overallBestFitness = 0;
} }
public void simulateGenerations() { public void simulateGenerations() {
...@@ -105,8 +157,10 @@ public class GeneticAlgorithm { ...@@ -105,8 +157,10 @@ public class GeneticAlgorithm {
int bonds = this.evaluator.evaluateBonds(this.population[bestIndex]); int bonds = this.evaluator.evaluateBonds(this.population[bestIndex]);
int overlaps = this.evaluator.evaluateOverlaps(this.population[bestIndex]); int overlaps = this.evaluator.evaluateOverlaps(this.population[bestIndex]);
this.visualizer.setFilename(String.format("gen_%07d.jpg",gen)); for (Visualizer v : this.visualizers) {
this.visualizer.drawProtein(this.population[bestIndex].getVertexList(), bestFitness, bonds, overlaps, gen); v.setFilename(String.format("gen_%07d.jpg", gen));
v.drawProtein(this.population[bestIndex].getVertexList(), bestFitness, bonds, overlaps, gen);
}
System.out.println("The fitness is: " + bestFitness System.out.println("The fitness is: " + bestFitness
+ " [hydrophobicBonds = " + bonds + " | overlaps = " + overlaps + "]"); + " [hydrophobicBonds = " + bonds + " | overlaps = " + overlaps + "]");
...@@ -135,10 +189,22 @@ public class GeneticAlgorithm { ...@@ -135,10 +189,22 @@ public class GeneticAlgorithm {
} }
public int getMaxH() { public int getMaxH() {
return this.visualizer.getMaxH(); int maxHAcrossVisualiszators = 0;
for (Visualizer v : visualizers) {
if (maxHAcrossVisualiszators < v.getMaxH()) {
maxHAcrossVisualiszators = v.getMaxH();
}
}
return maxHAcrossVisualiszators;
} }
public int getMaxW() { public int getMaxW() {
return this.visualizer.getMaxH(); int maxWAcrossVisualiszators = 0;
for (Visualizer v : visualizers) {
if (maxWAcrossVisualiszators < v.getMaxW()) {
maxWAcrossVisualiszators = v.getMaxW();
}
}
return maxWAcrossVisualiszators;
} }
} }
package MainClasses; package MainClasses;
import Enums.VisualizerMethods;
import Interfaces.Visualizer;
import Visualization.VideoCreator; import Visualization.VideoCreator;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
String propertyPath = "./src/main/resources/genetic.properties"; String propertyPath = "./src/main/resources/genetic.properties";
Config config = new Config (propertyPath); Config config = new Config(propertyPath);
int[] protein = Examples.convertStringToIntArray(Examples.SEQ20); int[] protein = Examples.convertStringToIntArray(Examples.SEQ20);
GeneticAlgorithm ga = new GeneticAlgorithm(protein); GeneticAlgorithm ga = new GeneticAlgorithm(protein);
ga.simulateGenerations(); ga.simulateGenerations();
// if (Config.isVidoable && Config.doVideo) // Create a new video if possible and desired
try { boolean imagesRefreshed = Arrays.stream(Config.VISUALIZERS).anyMatch(VisualizerMethods.Image::equals);
VideoCreator.createVideo(config.getProperties(), ga.getMaxH(), ga.getMaxW()); boolean videoEnabled = Arrays.stream(Config.VISUALIZERS).anyMatch(VisualizerMethods.Video::equals);
} catch (IOException e) { if (imagesRefreshed && videoEnabled){
e.printStackTrace(); VideoCreator.createVideo(Config.IMAGE_SEQUENCE_PATH, Config.VIDEO_PATH_AND_FILE,
Config.IMAGE_INTERVAL, ga.getMaxH(), ga.getMaxW());
} }
} }
} }
...@@ -87,27 +87,29 @@ public class VideoCreator{ ...@@ -87,27 +87,29 @@ public class VideoCreator{
} }
// MainClasses.Main function // MainClasses.Main function
public static void createVideo(Properties properties, int maxH, int maxW) throws IOException { public static void createVideo(String imagesPath, String videoPathAndFile,
String imagesPath = properties.getProperty("imageSequencePath"); int imgInterval, int maxH, int maxW) {
String videoPathAndFile = properties.getProperty("videoPathAndFile"); try {
int imgInterval = Integer.parseInt(properties.getProperty("imgInterval")); dir = new File(imagesPath);
dir = new File(imagesPath);
int[] widthHeight = VideoCreator.resizeImages(imagesPath, maxH, maxW); int[] widthHeight = VideoCreator.resizeImages(imagesPath, maxH, maxW);
File file = new File(videoPathAndFile); File file = new File(videoPathAndFile);
if (!file.exists()) { if (!file.exists()) {
file.createNewFile(); file.createNewFile();
} }
Vector<String> imgLst = new Vector<>(); Vector<String> imgLst = new Vector<>();
if (dir.isDirectory()) { if (dir.isDirectory()) {
int counter = 1; int counter = 1;
for (final File f : dir.listFiles(imageFilter)) { for (final File f : dir.listFiles(imageFilter)) {
imgLst.add(f.getAbsolutePath()); imgLst.add(f.getAbsolutePath());
}
} }
makeVideo("file:\\" + file.getAbsolutePath(), imgLst, widthHeight[0], widthHeight[1], imgInterval);
} catch (IOException e) {
e.printStackTrace();
} }
makeVideo("file:\\" + file.getAbsolutePath(), imgLst, widthHeight[0], widthHeight[1], imgInterval);
} }
public static void makeVideo(String fileName, Vector imgLst, int width, int height, int interval) throws MalformedURLException { public static void makeVideo(String fileName, Vector imgLst, int width, int height, int interval) throws MalformedURLException {
......
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
# Type of population initialization [curl | straight | random] # Type of population initialization [curl | straight | random]
initializationMethod = curl initializationMethod = curl
# Type of selection that should be used [proportional | tournament | onlybest] # Type of selection that should be used [proportional | tournament | onlybest]
selection = proportional selectionMethod = proportional
# Number of tournament participants, only relevant when selection is set to tournament # Number of tournament participants, only relevant when selection is set to tournament
k = 5 k = 5
# Type(s) of mutators to use in the algorithm, separated by comma [singlePoint / crossover] # Type(s) of mutators to use in the algorithm, separated by comma [singlePoint / crossover]
mutatorsToUse = singlePoint,crossover mutatorMethods = singlePoint,crossover
# Points each HH bond gives # Points each HH bond gives
pointsPerBond = 1 pointsPerBond = 1
...@@ -28,13 +28,13 @@ ...@@ -28,13 +28,13 @@
# Chance for a successful single point mutation [1.0 = 100%] # Chance for a successful single point mutation [1.0 = 100%]
mutationChance = 1.0 mutationChance = 1.0
# Multiplicand for mutation probability with each generation -> ex with 0.05: 1st 1.0, 2nd 0.95, 3rd 0.9025, 4th 0.857 # Multiplicand for mutation probability with each generation -> ex with 0.05: 1st 1.0, 2nd 0.95, 3rd 0.9025, 4th 0.857
mutationDecline = 0.001 mutationMultiplier = 0.001
# How often a crossover should be attempted per candidate # How often a crossover should be attempted per candidate
crossoverAttemptsPerCandidate = 1 crossoverAttemptsPerCandidate = 1
# Chance for a successful crossover [1.0 = 100%] # Chance for a successful crossover [1.0 = 100%]
crossoverChance = 0.6 crossoverChance = 0.6
# Multiplicand for mutation probability with each generation -> ex with -0.05: 1st 1.0, 2nd 1.05, 3rd 1,1025, 4th 1,157 # Multiplicand for mutation probability with each generation -> ex with -0.05: 1st 1.0, 2nd 1.05, 3rd 1,1025, 4th 1,157
crossoverDecline = 0.005 crossoverMultiplier = 0.005
### Output settings ### Output settings
...@@ -48,9 +48,9 @@ ...@@ -48,9 +48,9 @@
videoPathAndFile = ./visualization/video.mp4 videoPathAndFile = ./visualization/video.mp4
# ms between images in the beginning of the video # ms between images in the beginning of the video
imgInterval = 100 imgInterval = 100
# Decline of interval between images to speed up later generations # Decline of interval between images to speed up later generations TODO
imgIntervalDecline = 0.01 imgIntervalDecline = 0.01
# Minimum time between images # Minimum time between images TODO
imgIntervalMin = 10 imgIntervalMin = 10
# Zoom to the appropriate size, once no bigger proteins are going to follow # Zoom to the appropriate size, once no bigger proteins are going to follow TODO
zoom = false zoom = false
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment