NeatChromosomeSpec.java

package net.bmahe.genetics4j.neat.spec;

import org.apache.commons.lang3.Validate;
import org.immutables.value.Value;

import net.bmahe.genetics4j.core.spec.chromosome.ChromosomeSpec;

/**
 * Specification for NEAT (NeuroEvolution of Augmenting Topologies) neural network chromosomes.
 *
 * <p>A specification defines the stable identities and vector order of the network's input and output nodes, the
 * namespace available to hidden nodes, and the bounds applied to connection weights. Chromosome factories and genetic
 * operators use the same {@link NeatNodeLayout} throughout an evolutionary run so structural mutations do not relabel
 * existing nodes.
 *
 * <p>Contiguous layouts preserve the traditional NEAT numbering scheme. Inputs start at zero, outputs immediately
 * follow the inputs, and hidden-node IDs start after the outputs:
 *
 * <pre>{@code
 * NeatChromosomeSpec xorSpec = NeatChromosomeSpec.of(
 * 		2, // input IDs [0, 1]
 * 			1, // output ID [2]
 * 			-1.0f, // minimum connection weight
 * 			1.0f); // maximum connection weight
 * }</pre>
 *
 * <p>Explicit layouts support stable, ordered, non-contiguous external IDs and a dedicated hidden-node range:
 *
 * <pre>{@code
 * NeatNodeLayout layout = NeatNodeLayout.explicit(
 * 		List.of(10, 1_000, 1_001), // ordered input IDs
 * 			List.of(102_000), // ordered output IDs
 * 			1_000_000, // hidden range start, inclusive
 * 			2_000_000); // hidden range end, exclusive
 *
 * NeatChromosomeSpec sparseSpec = new NeatChromosomeSpec.Builder().nodeLayout(layout)
 * 		.minWeightValue(-2.0f)
 * 		.maxWeightValue(2.0f)
 * 		.build();
 * }</pre>
 *
 * <p>Declaration order is significant: it defines how input and output vectors map to node IDs. Node IDs themselves
 * are opaque identities and need not be contiguous. A valid layout contains at least one input and one output, uses
 * unique non-negative external IDs, keeps input and output IDs disjoint, and reserves a non-empty hidden-node range
 * that does not overlap external IDs.
 *
 * <p>The minimum connection weight must be strictly less than the maximum. Factories use these bounds when creating
 * initial connections, and mutation operators use them when changing connection weights.
 *
 * @see NeatNodeLayout
 * @see net.bmahe.genetics4j.neat.chromosomes.NeatChromosome
 * @see net.bmahe.genetics4j.neat.chromosomes.factory.NeatConnectedChromosomeFactory
 * @see net.bmahe.genetics4j.core.spec.EAConfiguration
 */
@Value.Immutable
public abstract class NeatChromosomeSpec implements ChromosomeSpec {

	/**
	 * Returns the stable external node identities and hidden-node namespace for chromosomes created from this
	 * specification.
	 *
	 * @return the node layout
	 */
	@Value.Parameter
	public abstract NeatNodeLayout nodeLayout();

	/**
	 * Returns the minimum allowed connection weight.
	 *
	 * @return the inclusive lower bound for connection weights
	 */
	@Value.Parameter
	public abstract float minWeightValue();

	/**
	 * Returns the maximum allowed connection weight.
	 *
	 * @return the inclusive upper bound for connection weights
	 */
	@Value.Parameter
	public abstract float maxWeightValue();

	/**
	 * Returns the number of input nodes declared by {@link #nodeLayout()}.
	 *
	 * @return the positive number of input nodes
	 */
	public int numInputs() {
		return nodeLayout().numInputs();
	}

	/**
	 * Returns the number of output nodes declared by {@link #nodeLayout()}.
	 *
	 * @return the positive number of output nodes
	 */
	public int numOutputs() {
		return nodeLayout().numOutputs();
	}

	@Value.Check
	protected void check() {
		Validate.notNull(nodeLayout());
		Validate.isTrue(minWeightValue() < maxWeightValue(), "Minimum weight must be less than maximum weight");
	}

	/** Builds immutable NEAT chromosome specifications. */
	public static class Builder extends ImmutableNeatChromosomeSpec.Builder {
	}

	/**
	 * Creates a specification using the supplied node layout and connection-weight bounds.
	 *
	 * @param nodeLayout     stable external IDs and hidden-node namespace
	 * @param minWeightValue minimum allowed connection weight
	 * @param maxWeightValue maximum allowed connection weight
	 * @return a validated immutable chromosome specification
	 * @throws NullPointerException     if {@code nodeLayout} is {@code null}
	 * @throws IllegalArgumentException if {@code minWeightValue} is not less than {@code maxWeightValue}
	 */
	public static NeatChromosomeSpec of(final NeatNodeLayout nodeLayout, final float minWeightValue,
			final float maxWeightValue) {
		return new Builder().nodeLayout(nodeLayout).minWeightValue(minWeightValue).maxWeightValue(maxWeightValue).build();
	}

	/**
	 * Creates a specification using the traditional contiguous node layout.
	 *
	 * <p>Input IDs are {@code [0, numInputs)}, output IDs are {@code [numInputs, numInputs + numOutputs)}, and the
	 * hidden-node namespace starts at {@code numInputs + numOutputs}.
	 *
	 * @param numInputs      number of input nodes; must be positive
	 * @param numOutputs     number of output nodes; must be positive
	 * @param minWeightValue minimum allowed connection weight
	 * @param maxWeightValue maximum allowed connection weight
	 * @return a validated immutable chromosome specification
	 * @throws IllegalArgumentException if either node count is not positive or the weight bounds are not ordered
	 */
	public static NeatChromosomeSpec of(final int numInputs, final int numOutputs, final float minWeightValue,
			final float maxWeightValue) {
		return of(NeatNodeLayout.contiguous(numInputs, numOutputs), minWeightValue, maxWeightValue);
	}
}