View Javadoc
1   package net.bmahe.genetics4j.moo;
2   
3   import java.util.Collection;
4   import java.util.Comparator;
5   import java.util.List;
6   import java.util.Objects;
7   import java.util.stream.Collectors;
8   import java.util.stream.IntStream;
9   
10  import org.apache.commons.lang3.Validate;
11  
12  /**
13   * Represents a multi-objective fitness vector for multi-objective optimization (MOO).
14   * 
15   * <p>A FitnessVector encapsulates multiple fitness values, each representing the quality of a solution in a different
16   * objective dimension. This is essential for multi-objective evolutionary algorithms like NSGA-II and SPEA-II where
17   * solutions are evaluated against multiple, often conflicting objectives.
18   * 
19   * <p>The fitness vector provides:
20   * <ul>
21   * <li>Storage for multiple objective values</li>
22   * <li>Custom comparators for each objective (minimization/maximization)</li>
23   * <li>Pareto dominance comparison logic</li>
24   * <li>Type-safe access to individual objective values</li>
25   * </ul>
26   * 
27   * <p>Pareto dominance comparison rules:
28   * <ul>
29   * <li>Vector A dominates B if A is better in all objectives</li>
30   * <li>Vector A weakly dominates B if A is better or equal in all objectives and better in at least one</li>
31   * <li>Vectors are non-dominated if neither dominates the other</li>
32   * </ul>
33   * 
34   * @param <T> the type of the fitness values, must be comparable
35   * @see net.bmahe.genetics4j.moo.nsga2
36   * @see net.bmahe.genetics4j.moo.spea2
37   * @see ParetoUtils
38   */
39  public class FitnessVector<T extends Comparable<T>> implements Comparable<FitnessVector<T>> {
40  
41  	private final List<T> fitnesses;
42  	private final List<Comparator<T>> comparators;
43  
44  	/**
45  	 * Creates a fitness vector with the specified values and comparators.
46  	 * 
47  	 * @param _vector      the fitness values for each objective
48  	 * @param _comparators the comparators defining optimization direction for each objective
49  	 * @throws IllegalArgumentException if vectors are null, empty, or have different sizes
50  	 */
51  	public FitnessVector(final Collection<T> _vector, final Collection<Comparator<T>> _comparators) {
52  		Objects.requireNonNull(_vector);
53  		Validate.isTrue(_vector.isEmpty() == false, "Fitness vector must not be empty");
54  		Objects.requireNonNull(_comparators);
55  		Validate.isTrue(_vector.size() == _comparators.size());
56  
57  		fitnesses = List.copyOf(_vector);
58  		comparators = List.copyOf(_comparators);
59  	}
60  
61  	/**
62  	 * Creates a fitness vector with natural ordering for all objectives.
63  	 * 
64  	 * <p>All objectives will use natural ordering (ascending), which means smaller values are considered better
65  	 * (minimization).
66  	 * 
67  	 * @param _vector the fitness values for each objective
68  	 * @throws IllegalArgumentException if vector is null or empty
69  	 */
70  	public FitnessVector(final Collection<T> _vector) {
71  		this(_vector,
72  				IntStream.range(0, _vector.size())
73  						.boxed()
74  						.map(i -> Comparator.<T>naturalOrder())
75  						.collect(Collectors.toList()));
76  	}
77  
78  	/**
79  	 * Creates a fitness vector from variable arguments with natural ordering.
80  	 * 
81  	 * @param _vector the fitness values for each objective
82  	 * @throws IllegalArgumentException if vector is empty
83  	 */
84  	public FitnessVector(T... _vector) {
85  		this(List.of(_vector));
86  	}
87  
88  	/**
89  	 * Returns the number of objectives in this fitness vector.
90  	 * 
91  	 * @return the number of objectives
92  	 */
93  	public int dimensions() {
94  		return fitnesses.size();
95  	}
96  
97  	/**
98  	 * Returns the fitness value for the specified objective.
99  	 * 
100 	 * @param index the index of the objective (0-based)
101 	 * @return the fitness value for the specified objective
102 	 * @throws IllegalArgumentException if index is out of bounds
103 	 */
104 	public T get(final int index) {
105 		Validate.exclusiveBetween(-1, fitnesses.size(), index);
106 
107 		return fitnesses.get(index);
108 	}
109 
110 	/**
111 	 * Returns the comparator for the specified objective.
112 	 * 
113 	 * @param index the index of the objective (0-based)
114 	 * @return the comparator defining optimization direction for the objective
115 	 * @throws IllegalArgumentException if index is out of bounds
116 	 */
117 	public Comparator<T> getComparator(final int index) {
118 		Validate.exclusiveBetween(-1, fitnesses.size(), index);
119 
120 		return comparators.get(index);
121 	}
122 
123 	@Override
124 	public int hashCode() {
125 		final int prime = 31;
126 		int result = 1;
127 		result = prime * result + (comparators == null ? 0 : comparators.hashCode());
128 		return prime * result + (fitnesses == null ? 0 : fitnesses.hashCode());
129 	}
130 
131 	@Override
132 	public boolean equals(Object obj) {
133 		if (this == obj) {
134 			return true;
135 		}
136 		if (obj == null) {
137 			return false;
138 		}
139 		if (getClass() != obj.getClass()) {
140 			return false;
141 		}
142 		FitnessVector other = (FitnessVector) obj;
143 		if (comparators == null) {
144 			if (other.comparators != null) {
145 				return false;
146 			}
147 		} else if (!comparators.equals(other.comparators)) {
148 			return false;
149 		}
150 		if (fitnesses == null) {
151 			if (other.fitnesses != null) {
152 				return false;
153 			}
154 		} else if (!fitnesses.equals(other.fitnesses)) {
155 			return false;
156 		}
157 		return true;
158 	}
159 
160 	@Override
161 	public String toString() {
162 		return "FitnessVector [fitnesses=" + fitnesses + "]";
163 	}
164 
165 	@Override
166 	public int compareTo(final FitnessVector<T> o) {
167 		return compare(this, o);
168 	}
169 
170 	/**
171 	 * Compares two fitness vectors using Pareto dominance.
172 	 * 
173 	 * <p>This method implements Pareto dominance comparison:
174 	 * <ul>
175 	 * <li>Returns positive value if fv1 dominates fv2 (fv1 is better in all objectives)</li>
176 	 * <li>Returns negative value if fv2 dominates fv1 (fv2 is better in all objectives)</li>
177 	 * <li>Returns 0 if vectors are non-dominated (neither dominates the other)</li>
178 	 * </ul>
179 	 * 
180 	 * @param <U> the type of the fitness values
181 	 * @param fv1 the first fitness vector to compare
182 	 * @param fv2 the second fitness vector to compare
183 	 * @return positive if fv1 dominates fv2, negative if fv2 dominates fv1, 0 if non-dominated
184 	 * @throws IllegalArgumentException if vectors are null or have different dimensions
185 	 */
186 	public static <U extends Comparable<U>> int compare(final FitnessVector<U> fv1, final FitnessVector<U> fv2) {
187 		Objects.requireNonNull(fv1);
188 		Objects.requireNonNull(fv2);
189 
190 		if (fv1.dimensions() != fv2.dimensions()) {
191 			throw new IllegalArgumentException("Can't compare FitnessVector with different dimensions");
192 		}
193 
194 		int greater = 0;
195 		int lesser = 0;
196 
197 		for (int i = 0; i < fv1.dimensions(); i++) {
198 			final U d1 = fv1.get(i);
199 			final U d2 = fv2.get(i);
200 
201 			final int compared = fv1.comparators.get(i).compare(d1, d2);
202 
203 			if (compared < 0) {
204 				lesser++;
205 			} else if (compared > 0) {
206 				greater++;
207 			}
208 		}
209 
210 		if (lesser == 0 && greater > 0) {
211 			return 1;
212 		}
213 		if (greater == 0 && lesser > 0) {
214 			return -1;
215 		}
216 		return 0;
217 	}
218 }