001/* 002 * Java Genetic Algorithm Library (jenetics-8.3.0). 003 * Copyright (c) 2007-2025 Franz Wilhelmstötter 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 * Author: 018 * Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com) 019 */ 020package io.jenetics.prog; 021 022import static java.lang.String.format; 023import static java.util.Objects.requireNonNull; 024import static io.jenetics.internal.util.SerialIO.readInt; 025import static io.jenetics.internal.util.SerialIO.writeInt; 026 027import java.io.IOException; 028import java.io.InvalidObjectException; 029import java.io.ObjectInput; 030import java.io.ObjectInputStream; 031import java.io.ObjectOutput; 032import java.io.Serial; 033import java.io.Serializable; 034import java.util.function.Function; 035import java.util.function.Predicate; 036 037import io.jenetics.util.ISeq; 038import io.jenetics.util.MSeq; 039 040import io.jenetics.ext.AbstractTreeChromosome; 041import io.jenetics.ext.util.FlatTreeNode; 042import io.jenetics.ext.util.Tree; 043import io.jenetics.ext.util.TreeNode; 044 045import io.jenetics.prog.op.Op; 046import io.jenetics.prog.op.Program; 047 048/** 049 * Holds the nodes of the operation tree. 050 * {@snippet lang="java": 051 * final int depth = 6; 052 * final ISeq<Op<Double>> operations = ISeq.of(null); // @replace substring='null' replacement="..." 053 * final ISeq<Op<Double>> terminals = ISeq.of(null); // @replace substring='null' replacement="..." 054 * final ProgramChromosome<Double> ch = ProgramChromosome.of( 055 * depth, 056 * // If the program has more than 200 nodes, it is marked as "invalid". 057 * ch -> ch.length() <= 200, 058 * operations, 059 * terminals 060 * ); 061 * } 062 * 063 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a> 064 * @version 4.1 065 * @since 3.9 066 */ 067public class ProgramChromosome<A> 068 extends AbstractTreeChromosome<Op<A>, ProgramGene<A>> 069 implements Function<A[], A>, Serializable 070{ 071 072 @Serial 073 private static final long serialVersionUID = 2L; 074 075 private final Predicate<? super ProgramChromosome<A>> _validator; 076 private final ISeq<Op<A>> _operations; 077 private final ISeq<Op<A>> _terminals; 078 079 /** 080 * Create a new program chromosome from the given program genes. This 081 * constructor assumes that the given {@code program} is valid. Since the 082 * program validation is quite expensive, the validity check is skipped in 083 * this constructor. 084 * 085 * @param program the program. During the program evolution, newly created 086 * program trees have the same <em>depth</em> than this tree. 087 * @param validator the chromosome validator. A typical validator would 088 * check the size of the tree and if the tree is too large, mark it 089 * at <em>invalid</em>. The <em>validator</em> may be {@code null}. 090 * @param operations the allowed non-terminal operations 091 * @param terminals the allowed terminal operations 092 * @throws NullPointerException if one of the given arguments is {@code null} 093 * @throws IllegalArgumentException if either the {@code operations} or 094 * {@code terminals} sequence is empty 095 */ 096 protected ProgramChromosome( 097 final ISeq<ProgramGene<A>> program, 098 final Predicate<? super ProgramChromosome<A>> validator, 099 final ISeq<? extends Op<A>> operations, 100 final ISeq<? extends Op<A>> terminals 101 ) { 102 super(program); 103 _validator = requireNonNull(validator); 104 _operations = requireNonNull(ISeq.upcast(operations)); 105 _terminals = requireNonNull(ISeq.upcast(terminals)); 106 107 if (operations.isEmpty()) { 108 throw new IllegalArgumentException("No operations given."); 109 } 110 if (terminals.isEmpty()) { 111 throw new IllegalArgumentException("No terminals given"); 112 } 113 } 114 115 /** 116 * Return the allowed operations. 117 * 118 * @since 5.0 119 * 120 * @return the allowed operations 121 */ 122 public ISeq<Op<A>> operations() { 123 return _operations; 124 } 125 126 /** 127 * Return the allowed terminal operations. 128 * 129 * @since 5.0 130 * 131 * @return the allowed terminal operations 132 */ 133 public ISeq<Op<A>> terminals() { 134 return _terminals; 135 } 136 137 @Override 138 public boolean isValid() { 139 if (_valid == null) { 140 _valid = _validator.test(this); 141 } 142 143 return _valid; 144 } 145 146 private boolean isSuperValid() { 147 return super.isValid(); 148 } 149 150 /** 151 * Evaluates the root node of this chromosome. 152 * 153 * @see ProgramGene#apply(Object[]) 154 * @see ProgramChromosome#eval(Object[]) 155 * 156 * @param args the input variables 157 * @return the evaluated value 158 * @throws NullPointerException if the given variable array is {@code null} 159 */ 160 @Override 161 public A apply(final A[] args) { 162 return root().apply(args); 163 } 164 165 /** 166 * Evaluates the root node of this chromosome. 167 * 168 * @see ProgramGene#eval(Object[]) 169 * @see ProgramChromosome#apply(Object[]) 170 * 171 * @param args the function arguments 172 * @return the evaluated value 173 * @throws NullPointerException if the given variable array is {@code null} 174 */ 175 @SafeVarargs 176 public final A eval(final A... args) { 177 return root().eval(args); 178 } 179 180 @Override 181 public ProgramChromosome<A> newInstance(final ISeq<ProgramGene<A>> genes) { 182 return create(genes, _validator, _operations, _terminals); 183 } 184 185 @Override 186 public ProgramChromosome<A> newInstance() { 187 return create(root().depth(), _validator, _operations, _terminals); 188 } 189 190 /** 191 * Create a new chromosome from the given operation tree (program). 192 * 193 * @param program the operation tree 194 * @param validator the chromosome validator. A typical validator would 195 * check the size of the tree and if the tree is too large, mark it 196 * at <em>invalid</em>. The <em>validator</em> may be {@code null}. 197 * @param operations the allowed non-terminal operations 198 * @param terminals the allowed terminal operations 199 * @param <A> the operation type 200 * @return a new chromosome from the given operation tree 201 * @throws NullPointerException if one of the given arguments is {@code null} 202 * @throws IllegalArgumentException if the given operation tree is invalid, 203 * which means there is at least one node where the operation arity 204 * and the node child count differ. 205 */ 206 public static <A> ProgramChromosome<A> of( 207 final Tree<? extends Op<A>, ?> program, 208 final Predicate<? super ProgramChromosome<A>> validator, 209 final ISeq<? extends Op<A>> operations, 210 final ISeq<? extends Op<A>> terminals 211 ) { 212 Program.check(program); 213 checkOperations(operations); 214 checkTerminals(terminals); 215 216 return create(program, validator, operations, terminals); 217 } 218 219 // Create the chromosomes without checks. 220 private static <A> ProgramChromosome<A> create( 221 final Tree<? extends Op<A>, ?> program, 222 final Predicate<? super ProgramChromosome<A>> validator, 223 final ISeq<? extends Op<A>> operations, 224 final ISeq<? extends Op<A>> terminals 225 ) { 226 final ISeq<ProgramGene<A>> genes = FlatTreeNode.ofTree(program).stream() 227 .map(n -> new ProgramGene<>( 228 n.value(), n.childOffset(), operations, terminals)) 229 .collect(ISeq.toISeq()); 230 231 return new ProgramChromosome<>(genes, validator, operations, terminals); 232 } 233 234 private static void checkOperations(final ISeq<? extends Op<?>> operations) { 235 final ISeq<?> terminals = operations.stream() 236 .filter(Op::isTerminal) 237 .collect(ISeq.toISeq()); 238 239 if (!terminals.isEmpty()) { 240 throw new IllegalArgumentException(format( 241 "Operations must not contain terminals: %s", 242 terminals.toString(",") 243 )); 244 } 245 } 246 247 private static void checkTerminals(final ISeq<? extends Op<?>> terminals) { 248 final ISeq<?> operations = terminals.stream() 249 .filter(op -> !op.isTerminal()) 250 .collect(ISeq.toISeq()); 251 252 if (!operations.isEmpty()) { 253 throw new IllegalArgumentException(format( 254 "Terminals must not contain operations: %s", 255 operations.toString(",") 256 )); 257 } 258 } 259 260 /** 261 * Create a new chromosome from the given operation tree (program). 262 * 263 * @param program the operation tree 264 * @param operations the allowed non-terminal operations 265 * @param terminals the allowed terminal operations 266 * @param <A> the operation type 267 * @return a new chromosome from the given operation tree 268 * @throws NullPointerException if one of the given arguments is {@code null} 269 * @throws IllegalArgumentException if the given operation tree is invalid, 270 * which means there is at least one node where the operation arity 271 * and the node child count differ. 272 */ 273 public static <A> ProgramChromosome<A> of( 274 final Tree<? extends Op<A>, ?> program, 275 final ISeq<? extends Op<A>> operations, 276 final ISeq<? extends Op<A>> terminals 277 ) { 278 return of( 279 program, 280 (Predicate<? super ProgramChromosome<A>> & Serializable)ProgramChromosome::isSuperValid, 281 operations, 282 terminals 283 ); 284 } 285 286 /** 287 * Create a new program chromosome with the defined depth. This method will 288 * create a <em>full</em> program tree. 289 * 290 * @param depth the depth of the created program tree 291 * @param validator the chromosome validator. A typical validator would 292 * check the size of the tree and if the tree is too large, mark it 293 * at <em>invalid</em>. The <em>validator</em> may be {@code null}. 294 * @param operations the allowed non-terminal operations 295 * @param terminals the allowed terminal operations 296 * @param <A> the operation type 297 * @return a new program chromosome from the given (flattened) program tree 298 * @throws NullPointerException if one of the parameters is {@code null} 299 * @throws IllegalArgumentException if the {@code depth} is smaller than zero 300 */ 301 public static <A> ProgramChromosome<A> of( 302 final int depth, 303 final Predicate<? super ProgramChromosome<A>> validator, 304 final ISeq<? extends Op<A>> operations, 305 final ISeq<? extends Op<A>> terminals 306 ) { 307 checkOperations(operations); 308 checkTerminals(terminals); 309 310 return create(depth, validator, operations, terminals); 311 } 312 313 private static <A> ProgramChromosome<A> create( 314 final int depth, 315 final Predicate<? super ProgramChromosome<A>> validator, 316 final ISeq<? extends Op<A>> operations, 317 final ISeq<? extends Op<A>> terminals 318 ) { 319 return create( 320 Program.of(depth, operations, terminals), 321 validator, 322 operations, 323 terminals 324 ); 325 } 326 327 /** 328 * Create a new program chromosome with the defined depth. This method will 329 * create a <em>full</em> program tree. 330 * 331 * @param depth the depth of the created (full) program tree 332 * @param operations the allowed non-terminal operations 333 * @param terminals the allowed terminal operations 334 * @param <A> the operation type 335 * @return a new program chromosome from the given (flattened) program tree 336 * @throws NullPointerException if one of the parameters is {@code null} 337 * @throws IllegalArgumentException if the {@code depth} is smaller than zero 338 */ 339 public static <A> ProgramChromosome<A> of( 340 final int depth, 341 final ISeq<? extends Op<A>> operations, 342 final ISeq<? extends Op<A>> terminals 343 ) { 344 return of( 345 depth, 346 (Predicate<? super ProgramChromosome<A>> & Serializable) 347 ProgramChromosome::isSuperValid, 348 operations, 349 terminals 350 ); 351 } 352 353 /** 354 * Create a new program chromosome from the given (flattened) program tree. 355 * This method doesn't make any assumption about the validity of the given 356 * operation tree. If the tree is not valid, it will repair it. This 357 * behavior allows the <em>safe</em> usage of all existing alterers. 358 * {@snippet lang="java": 359 * final ProgramChromosome<Double> ch = ProgramChromosome.of( 360 * genes, 361 * // If the program has more that 200 nodes, it is marked as "invalid". 362 * ch -> ch.length() <= 200, 363 * operations, 364 * terminals 365 * ); 366 * } 367 * 368 * @param genes the program genes 369 * @param validator the chromosome validator to use 370 * @param operations the allowed non-terminal operations 371 * @param terminals the allowed terminal operations 372 * @param <A> the operation type 373 * @return a new program chromosome from the given (flattened) program tree 374 * @throws NullPointerException if one of the parameters is {@code null} 375 */ 376 public static <A> ProgramChromosome<A> of( 377 final ISeq<ProgramGene<A>> genes, 378 final Predicate<? super ProgramChromosome<A>> validator, 379 final ISeq<? extends Op<A>> operations, 380 final ISeq<? extends Op<A>> terminals 381 ) { 382 final TreeNode<Op<A>> program = Program.toTree(genes, terminals); 383 return of(program, validator, operations, terminals); 384 } 385 386 private static <A> ProgramChromosome<A> create( 387 final ISeq<ProgramGene<A>> genes, 388 final Predicate<? super ProgramChromosome<A>> validator, 389 final ISeq<? extends Op<A>> operations, 390 final ISeq<? extends Op<A>> terminals 391 ) { 392 final TreeNode<Op<A>> program = Program.toTree(genes, terminals); 393 return create(program, validator, operations, terminals); 394 } 395 396 public static <A> ProgramChromosome<A> of( 397 final ISeq<ProgramGene<A>> genes, 398 final ISeq<? extends Op<A>> operations, 399 final ISeq<? extends Op<A>> terminals 400 ) { 401 return of(genes, ProgramChromosome::isSuperValid, operations, terminals); 402 } 403 404 405 /* ************************************************************************* 406 * Java object serialization 407 * ************************************************************************/ 408 409 @Serial 410 private Object writeReplace() { 411 return new SerialProxy(SerialProxy.PROGRAM_CHROMOSOME, this); 412 } 413 414 @Serial 415 private void readObject(final ObjectInputStream stream) 416 throws InvalidObjectException 417 { 418 throw new InvalidObjectException("Serialization proxy required."); 419 } 420 421 void write(final ObjectOutput out) throws IOException { 422 writeInt(length(), out); 423 out.writeObject(_operations); 424 out.writeObject(_terminals); 425 426 for (ProgramGene<A> gene : _genes) { 427 out.writeObject(gene.allele()); 428 writeInt(gene.childOffset(), out); 429 } 430 } 431 432 @SuppressWarnings({"unchecked", "rawtypes"}) 433 static ProgramChromosome read(final ObjectInput in) 434 throws IOException, ClassNotFoundException 435 { 436 final var length = readInt(in); 437 final var operations = (ISeq)in.readObject(); 438 final var terminals = (ISeq)in.readObject(); 439 440 final MSeq genes = MSeq.ofLength(length); 441 for (int i = 0; i < genes.length(); ++i) { 442 final Op op = (Op)in.readObject(); 443 final int childOffset = readInt(in); 444 genes.set(i, new ProgramGene(op, childOffset, operations, terminals)); 445 } 446 447 return ProgramChromosome.of(genes.toISeq(), operations, terminals); 448 } 449 450}