View Javadoc
1   package net.bmahe.genetics4j.neat;
2   
3   import java.util.Collection;
4   import java.util.Objects;
5   import java.util.Set;
6   import java.util.concurrent.ConcurrentHashMap;
7   import java.util.concurrent.atomic.AtomicLong;
8   
9   import org.apache.commons.lang3.Validate;
10  
11  import net.bmahe.genetics4j.neat.spec.NeatNodeLayout;
12  
13  /**
14   * Allocates collision-safe hidden-node IDs for one semantic node layout.
15   *
16   * <p>A manager is intended to be shared by every chromosome in an evolving population. It tracks IDs already present
17   * in chromosomes and assigns one stable hidden-node ID to each ancestral connection innovation that is split by an
18   * add-node mutation. Consequently, homologous mutations receive the same node ID even when they occur in different
19   * genomes.
20   *
21   * <p>Allocation and registration methods are synchronized so a manager can safely be used by concurrent mutation
22   * workers. IDs are drawn exclusively from the layout's hidden-node range and exhaustion is reported with an
23   * {@link IllegalStateException}.
24   *
25   * @see NodeIdManagerRegistry
26   * @see InnovationManager
27   */
28  public class NodeIdManager {
29  
30  	private final NeatNodeLayout nodeLayout;
31  	private final AtomicLong nextNodeId;
32  	private final Set<Integer> allocatedNodeIds = ConcurrentHashMap.newKeySet();
33  	private final ConcurrentHashMap<Integer, Integer> splitNodeIds = new ConcurrentHashMap<>();
34  	private final ConcurrentHashMap<Integer, Integer> splitInnovationsByNodeId = new ConcurrentHashMap<>();
35  
36  	/**
37  	 * Creates a manager for one node layout.
38  	 *
39  	 * @param _nodeLayout layout defining the external IDs and hidden-node range
40  	 * @throws NullPointerException if {@code _nodeLayout} is {@code null}
41  	 */
42  	public NodeIdManager(final NeatNodeLayout _nodeLayout) {
43  		nodeLayout = Objects.requireNonNull(_nodeLayout);
44  		nextNodeId = new AtomicLong(nodeLayout.hiddenNodeIdStartInclusive());
45  	}
46  
47  	/**
48  	 * Returns the hidden-node ID associated with splitting an ancestral connection.
49  	 *
50  	 * <p>The first call allocates an ID; subsequent calls with the same innovation return that ID.
51  	 *
52  	 * @param connectionInnovation non-negative connection innovation number
53  	 * @return the stable hidden-node ID for the split
54  	 * @throws IllegalArgumentException if the innovation number is negative
55  	 * @throws IllegalStateException    if the hidden-node range is exhausted
56  	 */
57  	public synchronized int nodeIdForSplit(final int connectionInnovation) {
58  		Validate.isTrue(connectionInnovation >= 0, "Connection innovation must be non-negative");
59  		return splitNodeIds.computeIfAbsent(connectionInnovation, ignored -> {
60  			final int nodeId = allocateNodeId();
61  			splitInnovationsByNodeId.put(nodeId, connectionInnovation);
62  			return nodeId;
63  		});
64  	}
65  
66  	/**
67  	 * Registers node IDs already present in a chromosome before new IDs are allocated.
68  	 *
69  	 * <p>External IDs are accepted but do not consume hidden-node allocation slots. Every other ID must belong to the
70  	 * layout's hidden-node range.
71  	 *
72  	 * @param nodeIds IDs referenced by an existing chromosome
73  	 * @throws NullPointerException     if the collection or one of its elements is {@code null}
74  	 * @throws IllegalArgumentException if an ID is outside the layout namespaces
75  	 */
76  	public synchronized void registerExistingNodeIds(final Collection<Integer> nodeIds) {
77  		Objects.requireNonNull(nodeIds);
78  		for (final Integer nodeId : nodeIds) {
79  			Objects.requireNonNull(nodeId);
80  			if (nodeLayout.isExternal(nodeId) == false) {
81  				Validate.isTrue(nodeLayout.isHidden(nodeId), "Node ID %d is outside the layout namespaces", nodeId);
82  				allocatedNodeIds.add(nodeId);
83  			}
84  		}
85  	}
86  
87  	/**
88  	 * Registers a known relationship between an ancestral connection and an existing hidden node.
89  	 *
90  	 * @param connectionInnovation non-negative connection innovation number
91  	 * @param nodeId               hidden-node ID assigned to the split
92  	 * @throws IllegalArgumentException if the ID is outside the hidden range or either value conflicts with a previous
93  	 *                                  registration
94  	 */
95  	public synchronized void registerSplit(final int connectionInnovation, final int nodeId) {
96  		Validate.isTrue(connectionInnovation >= 0, "Connection innovation must be non-negative");
97  		Validate.isTrue(nodeLayout.isHidden(nodeId), "Node ID %d is outside the hidden-node range", nodeId);
98  		final Integer existing = splitNodeIds.get(connectionInnovation);
99  		if (existing != null && existing != nodeId) {
100 			throw new IllegalArgumentException("Connection innovation %d is already mapped to hidden node %d"
101 					.formatted(connectionInnovation, existing));
102 		}
103 		final Integer existingInnovation = splitInnovationsByNodeId.get(nodeId);
104 		if (existingInnovation != null && existingInnovation != connectionInnovation) {
105 			throw new IllegalArgumentException(
106 					"Hidden node ID %d is already mapped to connection innovation %d".formatted(nodeId, existingInnovation));
107 		}
108 		splitNodeIds.put(connectionInnovation, nodeId);
109 		splitInnovationsByNodeId.put(nodeId, connectionInnovation);
110 		allocatedNodeIds.add(nodeId);
111 	}
112 
113 	private int allocateNodeId() {
114 		while (true) {
115 			final long candidate = nextNodeId.getAndIncrement();
116 			if (candidate >= nodeLayout.hiddenNodeIdEndExclusive()) {
117 				throw new IllegalStateException("Hidden-node ID range [%d, %d) is exhausted"
118 						.formatted(nodeLayout.hiddenNodeIdStartInclusive(), nodeLayout.hiddenNodeIdEndExclusive()));
119 			}
120 			final int nodeId = Math.toIntExact(candidate);
121 			if (allocatedNodeIds.add(nodeId)) {
122 				return nodeId;
123 			}
124 		}
125 	}
126 }