001/* 002 * Java Genetic Algorithm Library (jenetics-9.1.0). 003 * Copyright (c) 2007-2026 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.op; 021 022import static java.lang.String.format; 023import static java.util.Objects.requireNonNull; 024 025import java.io.Serial; 026import java.io.Serializable; 027import java.util.Objects; 028import java.util.function.BiFunction; 029import java.util.function.Function; 030import java.util.random.RandomGenerator; 031 032import io.jenetics.util.ISeq; 033import io.jenetics.util.RandomRegistry; 034 035import io.jenetics.ext.util.FlatTree; 036import io.jenetics.ext.util.Tree; 037import io.jenetics.ext.util.TreeNode; 038 039/** 040 * This class composes a given operation tree to a new operation, which can 041 * serve as a sub <em>program</em> in another operation tree. 042 * 043 * @param <T> the argument type of the operation 044 * 045 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a> 046 * @version 4.1 047 * @since 3.9 048 */ 049public class Program<T> implements Op<T>, Serializable { 050 051 @Serial 052 private static final long serialVersionUID = 1L; 053 054 private final String _name; 055 private final Tree<? extends Op<T>, ?> _tree; 056 057 /** 058 * Create a new program with the given name and the given operation tree. 059 * The arity of the program is calculated from the given operation tree and 060 * set to the maximal arity of the operations of the tree. 061 * 062 * @param name the program name 063 * @param tree the operation tree 064 * @throws NullPointerException if one of the given arguments is {@code null} 065 * @throws IllegalArgumentException if the given operation tree is invalid, 066 * which means there is at least one node where the operation arity 067 * and the node child count differ. 068 */ 069 public Program(final String name, final Tree<? extends Op<T>, ?> tree) { 070 _name = requireNonNull(name); 071 _tree = requireNonNull(tree); 072 check(tree); 073 } 074 075 @Override 076 public String name() { 077 return _name; 078 } 079 080 @Override 081 public int arity() { 082 return 0; 083 } 084 085 /** 086 * Return the underlying expression tree. 087 * 088 * @since 4.1 089 * 090 * @return the underlying expression tree 091 */ 092 public Tree<Op<T>, ?> tree() { 093 return TreeNode.ofTree(_tree); 094 } 095 096 @Override 097 public T apply(final T[] args) { 098 if (args.length < arity()) { 099 throw new IllegalArgumentException(format( 100 "Arguments length is smaller than program arity: %d < %d", 101 args.length, arity() 102 )); 103 } 104 105 return eval(_tree, args); 106 } 107 108 /** 109 * Convenient method, which lets you apply the program function without 110 * explicitly create a wrapper array. 111 * 112 * @see #apply(Object[]) 113 * 114 * @param args the function arguments 115 * @return the evaluated value 116 * @throws NullPointerException if the given variable array is {@code null} 117 * @throws IllegalArgumentException if the length of the argument array 118 * is smaller than the program arity 119 */ 120 @SafeVarargs 121 public final T eval(final T... args) { 122 return apply(args); 123 } 124 125 @Override 126 public int hashCode() { 127 return Objects.hash(_name, _tree); 128 } 129 130 @Override 131 public boolean equals(final Object obj) { 132 return obj instanceof Program<?> other && 133 Objects.equals(other._name, _name) && 134 Objects.equals(other._tree, _tree); 135 } 136 137 @Override 138 public String toString() { 139 return _name; 140 } 141 142 143 /* ************************************************************************* 144 * Static helper methods. 145 * ************************************************************************/ 146 147 /** 148 * Evaluates the given operation tree with the given variables. This method 149 * is equivalent to 150 * {@snippet lang="java": 151 * final T result = tree.reduce(variables, Op::apply); 152 * } 153 * but handles the variable sized {@code variables} array more conveniently. 154 * 155 * @see Tree#reduce(Object[], BiFunction) 156 * 157 * @param <T> the argument type 158 * @param tree the operation tree 159 * @param variables the input variables 160 * @return the result of the operation tree evaluation 161 * @throws NullPointerException if one of the arguments is {@code null} 162 * @throws IllegalArgumentException if the length of the variable array 163 * is smaller than the program arity 164 */ 165 @SafeVarargs 166 public static <T> T eval( 167 final Tree<? extends Op<T>, ?> tree, 168 final T... variables 169 ) { 170 return tree.reduce(variables, Function::apply); 171 } 172 173 /** 174 * Validates the given program tree. 175 * 176 * @param program the program to validate 177 * @throws NullPointerException if the given {@code program} is {@code null} 178 * @throws IllegalArgumentException if the given operation tree is invalid, 179 * which means there is at least one node where the operation arity 180 * and the node child count differ. 181 */ 182 public static void check(final Tree<? extends Op<?>, ?> program) { 183 program.forEach(Program::checkArity); 184 } 185 186 private static void checkArity(final Tree<? extends Op<?>, ?> node) { 187 if (node.value() != null && 188 node.value().arity() != node.childCount()) 189 { 190 throw new IllegalArgumentException(format( 191 "Op arity != child count: %d != %d", 192 node.value().arity(), node.childCount() 193 )); 194 } 195 } 196 197 /** 198 * Create a new, random program from the given (non) terminal operations 199 * with the desired depth. The created program tree is a <em>full</em> tree. 200 * 201 * @since 4.1 202 * 203 * @param name the program name 204 * @param depth the desired depth of the program tree 205 * @param operations the list of <em>non</em>-terminal operations 206 * @param terminals the list of terminal operations 207 * @param <A> the operational type 208 * @return a new program 209 * @throws NullPointerException if one of the given operations is 210 * {@code null} 211 * @throws IllegalArgumentException if the given tree depth is smaller than 212 * zero 213 */ 214 public static <A> Program<A> of( 215 final String name, 216 final int depth, 217 final ISeq<? extends Op<A>> operations, 218 final ISeq<? extends Op<A>> terminals 219 ) { 220 return new Program<>(name, of(depth, operations, terminals)); 221 } 222 223 /** 224 * Create a new, random program from the given (non) terminal operations 225 * with the desired depth. The created program tree is a <em>full</em> tree. 226 * 227 * @since 4.1 228 * 229 * @param name the program name 230 * @param depth the desired depth of the program tree 231 * @param operations the list of <em>non</em>-terminal operations 232 * @param terminals the list of terminal operations 233 * @param random the random engine used for creating the program 234 * @param <A> the operational type 235 * @return a new program 236 * @throws NullPointerException if one of the given operations is 237 * {@code null} 238 * @throws IllegalArgumentException if the given tree depth is smaller than 239 * zero 240 */ 241 public static <A> Program<A> of( 242 final String name, 243 final int depth, 244 final ISeq<? extends Op<A>> operations, 245 final ISeq<? extends Op<A>> terminals, 246 final RandomGenerator random 247 ) { 248 return new Program<>(name, of(depth, operations, terminals, random)); 249 } 250 251 /** 252 * Create a new, random program tree from the given (non) terminal 253 * operations with the desired depth. The created program tree is a 254 * <em>full</em> tree. 255 * 256 * @param depth the desired depth of the program tree 257 * @param operations the list of <em>non</em>-terminal operations 258 * @param terminals the list of terminal operations 259 * @param <A> the operational type 260 * @return a new program tree 261 * @throws NullPointerException if one of the given operations is 262 * {@code null} 263 * @throws IllegalArgumentException if the given tree depth is smaller than 264 * zero 265 */ 266 public static <A> TreeNode<Op<A>> of( 267 final int depth, 268 final ISeq<? extends Op<A>> operations, 269 final ISeq<? extends Op<A>> terminals 270 ) { 271 return of(depth, operations, terminals, RandomRegistry.random()); 272 } 273 274 /** 275 * Create a new, random program tree from the given (non) terminal 276 * operations with the desired depth. The created program tree is a 277 * <em>full</em> tree. 278 * 279 * @since 4.1 280 * 281 * @param depth the desired depth of the program tree 282 * @param operations the list of <em>non</em>-terminal operations 283 * @param terminals the list of terminal operations 284 * @param random the random engine used for creating the program 285 * @param <A> the operational type 286 * @return a new program tree 287 * @throws NullPointerException if one of the given operations is 288 * {@code null} 289 * @throws IllegalArgumentException if the given tree depth is smaller than 290 * zero 291 */ 292 public static <A> TreeNode<Op<A>> of( 293 final int depth, 294 final ISeq<? extends Op<A>> operations, 295 final ISeq<? extends Op<A>> terminals, 296 final RandomGenerator random 297 ) { 298 if (depth < 0) { 299 throw new IllegalArgumentException( 300 "Tree depth is smaller than zero: " + depth 301 ); 302 } 303 if (!operations.forAll(o -> !o.isTerminal())) { 304 throw new IllegalArgumentException( 305 "Operation list contains terminal op." 306 ); 307 } 308 if (!terminals.forAll(Op::isTerminal)) { 309 throw new IllegalArgumentException( 310 "Terminal list contains non-terminal op." 311 ); 312 } 313 314 final TreeNode<Op<A>> root = TreeNode.of(); 315 fill(depth, root, operations, terminals, random); 316 return root; 317 } 318 319 private static <A> void fill( 320 final int level, 321 final TreeNode<Op<A>> tree, 322 final ISeq<? extends Op<A>> operations, 323 final ISeq<? extends Op<A>> terminals, 324 final RandomGenerator random 325 ) { 326 final Op<A> op = level == 0 327 ? terminals.get(random.nextInt(terminals.size())) 328 : operations.get(random.nextInt(operations.size())); 329 330 tree.value(op); 331 332 if (level > 1) { 333 for (int i = 0; i < op.arity(); ++i) { 334 final TreeNode<Op<A>> node = TreeNode.of(); 335 fill(level - 1, node, operations, terminals, random); 336 tree.attach(node); 337 } 338 } else { 339 for (int i = 0; i < op.arity(); ++i) { 340 final Op<A> term = terminals.get(random.nextInt(terminals.size())); 341 tree.attach(TreeNode.of(term)); 342 } 343 } 344 } 345 346 /** 347 * Creates a valid program tree from the given flattened sequence of 348 * op nodes. The given {@code operations} and {@code termination} nodes are 349 * used for <em>repairing</em> the program tree, if necessary. 350 * 351 * @param nodes the flattened, possible corrupt, program tree 352 * @param terminals the usable non-terminal operation nodes to use for 353 * reparation 354 * @param <A> the operation argument type 355 * @return a new valid program tree build from the flattened program tree 356 * @throws NullPointerException if one of the arguments is {@code null} 357 * @throws IllegalArgumentException if the {@code nodes} sequence is empty 358 */ 359 public static <A> TreeNode<Op<A>> toTree( 360 final ISeq<? extends FlatTree<? extends Op<A>, ?>> nodes, 361 final ISeq<? extends Op<A>> terminals 362 ) { 363 if (nodes.isEmpty()) { 364 throw new IllegalArgumentException("Tree nodes must not be empty."); 365 } 366 367 final Op<A> op = requireNonNull(nodes.get(0).value()); 368 final TreeNode<Op<A>> tree = TreeNode.of(op); 369 return toTree( 370 tree, 371 0, 372 nodes, 373 offsets(nodes), 374 terminals, 375 RandomRegistry.random() 376 ); 377 } 378 379 private static <A> TreeNode<Op<A>> toTree( 380 final TreeNode<Op<A>> root, 381 final int index, 382 final ISeq<? extends FlatTree<? extends Op<A>, ?>> nodes, 383 final int[] offsets, 384 final ISeq<? extends Op<A>> terminals, 385 final RandomGenerator random 386 ) { 387 if (index < nodes.size()) { 388 final FlatTree<? extends Op<A>, ?> node = nodes.get(index); 389 final Op<A> op = node.value(); 390 391 for (int i = 0; i < op.arity(); ++i) { 392 assert offsets[index] != -1; 393 394 final TreeNode<Op<A>> treeNode = TreeNode.of(); 395 if (offsets[index] + i < nodes.size()) { 396 treeNode.value(nodes.get(offsets[index] + i).value()); 397 } else { 398 treeNode.value(terminals.get(random.nextInt(terminals.size()))); 399 } 400 401 toTree( 402 treeNode, 403 offsets[index] + i, 404 nodes, 405 offsets, 406 terminals, 407 random 408 ); 409 root.attach(treeNode); 410 } 411 } 412 413 return root; 414 } 415 416 /** 417 * Create the offset array for the given nodes. The offsets are calculated 418 * using the arity of the stored operations. 419 * 420 * @param nodes the flattened tree nodes 421 * @return the offset array for the given nodes 422 */ 423 static int[] 424 offsets(final ISeq<? extends FlatTree<? extends Op<?>, ?>> nodes) { 425 final int[] offsets = new int[nodes.size()]; 426 427 int offset = 1; 428 for (int i = 0; i < offsets.length; ++i) { 429 final Op<?> op = nodes.get(i).value(); 430 431 offsets[i] = op.isTerminal() ? -1 : offset; 432 offset += op.arity(); 433 } 434 435 return offsets; 436 } 437 438}