Skip to content

Commit 8496c99

Browse files
committed
Formatting issues, support PDF report, more tests
1 parent 1d8ed98 commit 8496c99

27 files changed

Lines changed: 2350 additions & 978 deletions

docs/source/pygad.md

Lines changed: 253 additions & 53 deletions
Large diffs are not rendered by default.

docs/source/releases.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,12 @@ Release Date ..., 2026
690690
22. Two new example folders under `/examples`: `examples/benchmarks/` has one runnable example per benchmark (classic, ZDT, DTLZ, knapsack, and TSP), and `examples/quality_indicators/` has one runnable example per quality indicator (hypervolume, IGD, GD, and spacing).
691691
23. `plot_pareto_front_curve()` now also supports 3 objectives (3D scatter). M >= 4 still raises and points to the new high-dimensional plots.
692692
24. Seven new plot methods on `pygad.GA`. The first three work on the final population (no extra flag needed): `plot_pareto_front_pcp()` (parallel coordinates, any M >= 2), `plot_pareto_front_scatter_matrix()` (M-by-M pairwise scatter, best for M >= 4), and `plot_pareto_front_heatmap()` (solutions-by-objectives heatmap). The other four require `save_solutions=True`: `plot_fitness_band()` (per-generation min / mean / max with a shaded band), `plot_non_dominated_hypervolume()` (hypervolume of the non-dominated set per generation), `plot_population_diversity()` (mean pairwise distance per generation), and `plot_pareto_front_evolution()` (non-dominated set overlaid every k generations).
693-
25. Fix a latent divide-by-zero in `NSGA3.normalise_fitness()`. The safeguard for near-zero denominators used to collapse to `0` for tiny negative values (the realistic case under PyGAD-max), which silently produced wrong normalised values. The safeguard now keeps the negative sign.
693+
25. Fix a latent divide-by-zero in `NSGA3.nsga3_normalize_fitness()`. The safeguard for near-zero denominators used to collapse to `0` for tiny negative values (the realistic case under PyGAD-max), which silently produced wrong normalized values. The safeguard now keeps the negative sign.
694+
26. Refactor the NSGA classes to keep each script focused. A new module `pygad/utils/nsga.py` hosts the `NSGA` mixin with `non_dominated_sorting()` and `get_non_dominated_set()`, which are shared between NSGA-II and NSGA-III. `nsga2.py` now only carries NSGA-II specific code (`crowding_distance`, `sort_solutions_nsga2`). `nsga3.py` now only carries the NSGA-III algorithm primitives. The `nsga3_selection()` and `tournament_selection_nsga3()` methods have moved to `pygad/utils/parent_selection.py` next to their NSGA-II counterparts. The engine-time helpers `_bootstrap_nsga3_reference_points()`, `_nsga3_grow_population()`, `_nsga3_generate_extra_random_solutions()`, and `_nsga3_generate_single_random_gene()` now live in `pygad/utils/engine.py`.
695+
27. Rename NSGA-III novel names to start with `nsga3_` so the algorithm-specific surface is easy to spot. Algorithm primitives become `nsga3_generate_reference_points`, `nsga3_compute_ideal_point`, `nsga3_find_extreme_points`, `nsga3_compute_intercepts`, `nsga3_normalize_fitness`, `nsga3_associate_to_reference_points`, and `nsga3_niching_select`. Module-level helpers gain the same prefix (`_nsga3_pick_target_reference_point`, `_nsga3_pick_candidate_at_reference`, `_nsga3_enumerate_compositions`, `_nsga3_validate_multi_objective_fitness`, `_nsga3_accumulate_fronts`). The constants are renamed `NSGA3_ASF_EPSILON` and `NSGA3_INTERCEPT_NEAR_ZERO`. Names that already had NSGA-II parallels (`tournament_selection_nsga3`, `pareto_fronts`, `non_dominated_sorting`) keep their original spelling.
696+
28. Spell every name and docstring in American English (`normalize`, `maximize`, `behavior`, `color`, `optimization`, ...) so the library stays consistent.
697+
29. Expand abbreviated names introduced by the NSGA-III refactor: `fl_indices` to `critical_front_indices`, `fl_assoc` to `critical_front_associations`, `fl_dist` to `critical_front_distances`, `st_indices` to `selection_pool_indices`, `st_fitness` to `selection_pool_fitness`, `accepted_assoc` to `accepted_associations`, `K` to `num_to_select` (in `nsga3_niching_select`).
698+
30. The NSGA-III population auto-growth path now respects every initial-population rule: `init_range_low`/`init_range_high`, `gene_space`, `gene_type` (single dtype or nested per-gene `[type, precision]`), `gene_constraint`, and `allow_duplicate_genes=False`. Previously, only the gene-space / init-range sampling step was applied; gene constraints and duplicate resolution were skipped, which could leave the grown rows in an invalid state.
699+
31. A new `Report` mixin in `pygad/utils/report.py` adds `ga_instance.generate_report(filename, ...)` to build a PDF report of the run. The report bundles a configuration table, a run-summary table, the best solution, and every applicable plot (auto-selected based on the run's properties: SOO vs MOO, number of objectives, `save_solutions`, `save_best_solutions`). The report uses `reportlab` and `matplotlib`, both available through the new optional dependency extra `pip install pygad[report]`.
700+
32. A new example `examples/example_generate_report.py` shows how to build a PDF report after running a multi-objective GA.
701+
33. The `pygad.md`, `releases.md`, `visualize.md`, and `utils.md` documentation pages were updated to reflect the new module layout, the renamed methods, the new `generate_report()` entry point, and the new NSGA-III instance attributes (`nsga3_num_divisions`, `nsga3_reference_points`). The "Other Instance Attributes & Methods" section in `pygad.md` is now grouped by area (Lifecycle, Population, Fitness, Parent Selection, NSGA-II, NSGA-III, Crossover, Mutation, Elitism, Gene Constraints, Saving) so each method or attribute appears under its topic.

docs/source/utils.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -320,28 +320,45 @@ Selects the parents for the NSGA-III algorithm to solve multi-objective optimiza
320320

321321
Selects the parents for the NSGA-III algorithm to solve multi-objective optimization problems. It selects the parents using the tournament selection technique where the within-front comparison is based on the niche count (instead of the crowding distance used by `tournament_selection_nsga2()`). Requires the `nsga3_num_divisions` parameter to be set.
322322

323+
## `pygad.utils.nsga` Submodule
324+
325+
The `pygad.utils.nsga` module has a class named `NSGA` that holds the building blocks shared by NSGA-II and NSGA-III. The methods inside this class are:
326+
327+
1. `non_dominated_sorting()`: Returns all the Pareto fronts by applying non-dominated sorting over the solutions.
328+
2. `get_non_dominated_set()`: Returns the two sets of non-dominated and dominated solutions from the passed solutions. The Pareto front is the non-dominated set.
329+
323330
## `pygad.utils.nsga2` Submodule
324331

325-
The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II. The methods inside this class are:
332+
The `pygad.utils.nsga2` module has a class named `NSGA2` that implements the NSGA-II-specific primitives. The methods inside this class are:
326333

327-
1. `non_dominated_sorting()`: Returns all the pareto fronts by applying non-dominated sorting over the solutions.
328-
2. `get_non_dominated_set()`: Returns the 2 sets of non-dominated solutions and dominated solutions from the passed solutions. Note that the Pareto front consists of the solutions in the non-dominated set.
329-
3. `crowding_distance()`: Calculates the crowding distance for all solutions in the current pareto front.
330-
4. `sort_solutions_nsga2()`: Sort the solutions. If the problem is single-objective, then the solutions are sorted by sorting the fitness values of the population. If it is multi-objective, then non-dominated sorting and crowding distance are applied to sort the solutions.
334+
1. `crowding_distance()`: Calculates the crowding distance for all solutions in the current Pareto front.
335+
2. `sort_solutions_nsga2()`: Sort the solutions. If the problem is single-objective, the solutions are sorted by their fitness values. If it is multi-objective, non-dominated sorting and crowding distance are applied to sort the solutions.
331336

332337
## `pygad.utils.nsga3` Submodule
333338

334-
The `pygad.utils.nsga3` module has a class named `NSGA3` that implements NSGA-III. The methods inside this class are:
335-
336-
1. `generate_reference_points()`: Build the structured grid of reference points on the unit simplex using the Das-Dennis (stars-and-bars) method.
337-
2. `compute_ideal_point()`: Return the ideal point (column maximum under PyGAD's maximisation convention).
338-
3. `find_extreme_points()`: For each objective axis, return the solution that best represents the corner of that axis based on the Achievement Scalarising Function (ASF).
339-
4. `compute_intercepts()`: Fit a hyperplane through the M extreme points and return the per-axis intercept point used as the normalisation denominator. Falls back to the nadir (worst per objective) when the hyperplane cannot be fitted or when the intercept is degenerate.
340-
5. `normalise_fitness()`: Scale each fitness row to the [0, 1] range using the ideal point and the intercepts.
341-
6. `associate_to_reference_points()`: For every normalised solution, return the nearest reference index and the perpendicular distance to that reference line.
342-
7. `niching_select()`: Pick K survivors from the critical front using niche counts and per-niche tie-breaking rules.
343-
8. `nsga3_selection()`: Top-level NSGA-III parent selection routine.
344-
9. `tournament_selection_nsga3()`: Tournament-style NSGA-III parent selection routine.
339+
The `pygad.utils.nsga3` module has a class named `NSGA3` that implements the NSGA-III algorithm primitives. NSGA-III novel names start with `nsga3_` to make the algorithm surface easy to spot.
340+
341+
1. `nsga3_generate_reference_points()`: Build the structured grid of reference points on the unit simplex using the Das-Dennis (stars-and-bars) method.
342+
2. `nsga3_compute_ideal_point()`: Return the ideal point (column maximum under PyGAD's maximization convention).
343+
3. `nsga3_find_extreme_points()`: For each objective axis, return the solution that best represents the corner of that axis based on the Achievement Scalarizing Function (ASF).
344+
4. `nsga3_compute_intercepts()`: Fit a hyperplane through the M extreme points and return the per-axis intercept point used as the normalization denominator. Falls back to the nadir (worst per objective) when the hyperplane cannot be fitted or when the intercept is degenerate.
345+
5. `nsga3_normalize_fitness()`: Scale each fitness row to the `[0, 1]` range using the ideal point and the intercepts.
346+
6. `nsga3_associate_to_reference_points()`: For every normalized solution, return the nearest reference index and the perpendicular distance to that reference line.
347+
7. `nsga3_niching_select()`: Pick `num_to_select` survivors from the critical front using niche counts and per-niche tie-breaking rules.
348+
349+
The selection methods `nsga3_selection()` and `tournament_selection_nsga3()` live in `pygad.utils.parent_selection`. The engine-time helpers (`_bootstrap_nsga3_reference_points()`, `_nsga3_grow_population()`, `_nsga3_generate_extra_random_solutions()`, `_nsga3_generate_single_random_gene()`) live in `pygad.utils.engine`.
350+
351+
Two module-level constants in `pygad.utils.nsga3` control the numerical safeguards: `NSGA3_ASF_EPSILON` (default `1e-6`) and `NSGA3_INTERCEPT_NEAR_ZERO` (default `1e-12`).
352+
353+
## `pygad.utils.report` Submodule
354+
355+
The `pygad.utils.report` module has a class named `Report` that adds the `generate_report()` method to the `pygad.GA` class. It builds a PDF report of the GA run, bundling the configuration table, a run summary, the best solution, and every applicable plot. Requires the optional dependencies `reportlab` and `matplotlib`:
356+
357+
```
358+
pip install pygad[report]
359+
```
360+
361+
See [`generate_report()`](https://pygad.readthedocs.io/en/latest/pygad.html#generate-report) and the runnable example at [`examples/example_generate_report.py`](https://github.com/ahmedfgad/GeneticAlgorithmPython/tree/master/examples/example_generate_report.py).
345362

346363
## `pygad.utils.quality_indicators` Submodule
347364

docs/source/visualize.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ For M=3 (NSGA-III on DTLZ2):
8080

8181
## `plot_pareto_front_pcp()`
8282

83-
Parallel-coordinates view of the final non-dominated set. Each objective is a vertical axis. Each non-dominated solution becomes a polyline that crosses every axis. Values are normalised per objective so very different scales remain comparable. Useful for any M >= 2 and especially for M >= 4.
83+
Parallel-coordinates view of the final non-dominated set. Each objective is a vertical axis. Each non-dominated solution becomes a polyline that crosses every axis. Values are normalized per objective so very different scales remain comparable. Useful for any M >= 2 and especially for M >= 4.
8484

8585
Parameters: `title`, `xlabel`, `ylabel`, `linewidth`, `font_size`, `color`, `alpha`, `grid`, `save_dir`.
8686

@@ -104,7 +104,7 @@ ga_instance.plot_pareto_front_scatter_matrix()
104104

105105
## `plot_pareto_front_heatmap()`
106106

107-
Heatmap of the final non-dominated set. Rows are solutions, columns are objectives, colour is the raw objective value. Rows are sorted by objective `sort_by` (default `0`); pass `sort_by=None` to keep the original order.
107+
Heatmap of the final non-dominated set. Rows are solutions, columns are objectives, color is the raw objective value. Rows are sorted by objective `sort_by` (default `0`); pass `sort_by=None` to keep the original order.
108108

109109
Parameters: `title`, `xlabel`, `ylabel`, `font_size`, `cmap`, `sort_by`, `save_dir`.
110110

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Build a PDF report for a small multi-objective GA run.
3+
4+
Requires the optional ``report`` extra: pip install pygad[report].
5+
The output file ``pygad_report.pdf`` is written next to this script.
6+
"""
7+
8+
import numpy
9+
10+
import pygad
11+
12+
13+
def fitness_func(ga_instance, solution, solution_idx):
14+
return [float(numpy.sum(solution)),
15+
-float(numpy.sum(numpy.asarray(solution) ** 2))]
16+
17+
18+
def main():
19+
ga_instance = pygad.GA(
20+
num_generations=30,
21+
num_parents_mating=8,
22+
fitness_func=fitness_func,
23+
sol_per_pop=20,
24+
num_genes=4,
25+
parent_selection_type="nsga2",
26+
save_solutions=True,
27+
random_seed=42,
28+
suppress_warnings=True,
29+
)
30+
ga_instance.run()
31+
output_path = ga_instance.generate_report(
32+
filename="pygad_report",
33+
title="PyGAD multi-objective demo",
34+
notes="A short two-objective example with 30 generations.",
35+
)
36+
print(f"Report written to: {output_path}")
37+
38+
39+
if __name__ == "__main__":
40+
main()

pygad/benchmarks/classic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Each class is callable with the (ga, solution, sol_idx) signature
55
and returns a single fitness value. Minimisation values are negated
6-
so PyGAD can maximise them.
6+
so PyGAD can maximize them.
77
"""
88

99
import math

pygad/benchmarks/dtlz.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
number of decision variables is M + k - 1. Defaults: k = 5 for
66
DTLZ1, k = 10 for DTLZ2, DTLZ3, and DTLZ4.
77
8-
Fitness values are negated so PyGAD can maximise them.
8+
Fitness values are negated so PyGAD can maximize them.
99
"""
1010

1111
import math

pygad/benchmarks/tsp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Travelling Salesman Problem (TSP) benchmark.
33
44
A solution is a permutation of city indices. Fitness is the
5-
negative tour length so PyGAD can maximise it.
5+
negative tour length so PyGAD can maximize it.
66
77
Build the problem from a 2D array of coordinates or from a
88
precomputed distance matrix. The class exposes gene_space,

pygad/helper/activations.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,20 @@ def relu(sop):
5454

5555
def softmax(layer_outputs):
5656
"""
57-
Apply a sum-normalised softmax: divide each value by the sum of
57+
Apply a sum-normalized softmax: divide each value by the sum of
5858
all values plus a tiny constant to avoid division by zero.
5959
6060
Note that this is not the canonical softmax (which uses
61-
exponentials); it just normalises the inputs so they sum to one.
61+
exponentials); it just normalizes the inputs so they sum to one.
6262
6363
Parameters
6464
----------
6565
layer_outputs : numpy.ndarray
66-
The values to normalise.
66+
The values to normalize.
6767
6868
Returns
6969
-------
7070
activated : numpy.ndarray
71-
The normalised values.
71+
The normalized values.
7272
"""
7373
return layer_outputs / (numpy.sum(layer_outputs) + 0.000001)

pygad/kerasga/kerasga.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def __init__(self, model, num_solutions):
124124
Parameters
125125
----------
126126
model : tensorflow.keras.Model
127-
The Keras model to optimise. Its current weights are used
127+
The Keras model to optimize. Its current weights are used
128128
as the seed for the first solution.
129129
num_solutions : int
130130
Number of solutions in the population. Each solution is a

0 commit comments

Comments
 (0)