Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,51 @@ on:
workflow_call:

jobs:
numpy-windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.9"
numpy-requirement: "numpy==1.24.4"
numpy-label: "NumPy 1.24.4"
- python-version: "3.14"
numpy-requirement: "numpy"
numpy-label: "Latest NumPy"
name: Windows / Python ${{ matrix.python-version }} / ${{ matrix.numpy-label }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install PyGAD and NumPy
run: python -m pip install . pytest "${{ matrix.numpy-requirement }}"

- name: Verify Installed Versions
working-directory: ${{ runner.temp }}
run: python -c "import numpy, pygad; print('NumPy:', numpy.__version__); print('Testing installed package:', pygad.__file__)"

# Windows used 32-bit storage for dtype=int before NumPy 2.0.
# Run the same regression tests on older and current NumPy releases.
- name: Test Gene Storage and Integer Mutation
working-directory: ${{ runner.temp }}
run: >-
python -m pytest
"${{ github.workspace }}/tests/test_python_int_storage.py"
"${{ github.workspace }}/tests/test_gene_type_conversion.py"
"${{ github.workspace }}/tests/test_initial_population.py"
"${{ github.workspace }}/tests/test_mutation_integer_limits.py"
"${{ github.workspace }}/tests/test_nsga3_population_growth.py"
"${{ github.workspace }}/tests/test_sbx_polynomial.py"
"${{ github.workspace }}/tests/test_parent_selection_regressions.py"
"${{ github.workspace }}/tests/test_permutation_mutation.py"
-q -W error::DeprecationWarning

pytest:
# Use the latest Ubuntu runner for all jobs.
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The library is under active development and more features are added regularly. I

# Installation

The current release is [PyGAD 3.8.0](https://github.com/ahmedfgad/GeneticAlgorithmPython/releases/tag/3.8.0), dated October 9, 2026. Read the [release notes](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/3.8.0/docs/source/releases.md#pygad-380) for its new features, fixes, and compatibility changes. PyGAD requires Python 3.8 or newer.
The current release is [PyGAD 3.8.1](https://github.com/ahmedfgad/GeneticAlgorithmPython/releases/tag/3.8.1), dated October 11, 2026. Read the [release notes](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/3.8.1/docs/source/releases.md#pygad-381) for its fixes and compatibility changes. PyGAD requires Python 3.8 or newer.

Watch the [PyGAD 3.8.0 release video on YouTube](https://youtu.be/8pdIiMAMLUM).

Expand Down
3 changes: 3 additions & 0 deletions docs/source/gene_values.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,13 @@ Let us look at some examples.
PyGAD applies the same conversion rules to generated and supplied initial populations, mutation candidates, and custom operator outputs. `on_parents`, `on_crossover`, and `on_mutation` receive converted values; any replacements returned or made in place by these callbacks are converted again before use. These rules apply whether `allow_duplicate_genes` is `True` or `False`.

- Integer conversion truncates the fractional part towards zero. For example, `1.9` becomes `1` and `-1.9` becomes `-1`. Additive mutation adds the random value before converting the result.
- Random and adaptive mutation clip generated values to the limits of a NumPy integer gene type before casting. This applies to both additive and replacement mutation, including random candidates from `None` gene-space entries, constraint checks, and duplicate repair. For example, a `numpy.uint8` gene at `0` with an offset of `-1` stays at `0`, and a `numpy.int8` gene at `127` with an offset of `1` stays at `127`. Values within the type's limits keep the same conversion behavior. Python `int`, floating-point, and `object` genes keep their existing behavior.
- Floating-point precision rounds before casting to the requested type. This avoids rounding in a narrow type such as `numpy.float16` before the final conversion.
- Precision must be an integer or `None`. `None` keeps the value unrounded, `0` rounds to whole numbers, and a negative precision rounds to positions before the decimal point. For example, `[float, -1]` rounds to tens. Integer types may only be paired with `None`.
- Rounding uses NumPy's nearest-even rule at halfway points: `0.5` rounds to `0.0` and `1.5` rounds to `2.0` with precision `0`.

Python `int` conversion of candidate arrays and population columns uses explicit signed 64-bit storage (`numpy.int64`), with values from `-2**63` through `2**63 - 1`. Initialization ranges and bounded operators use these same limits. This avoids the 32-bit default used by NumPy 1.x on Windows. A population with one shared Python `int` type has `dtype=numpy.int64`; per-gene type specifications still use object arrays with Python `int` elements in those columns. Scalar conversion still returns a Python `int`, and explicitly requested NumPy types keep their own widths and limits. Supplied Python integers outside the signed 64-bit range raise `OverflowError` during array conversion; use `gene_type=object` to retain larger Python integers.

Floating-point types use binary representations, so a stored value can differ slightly from its decimal representation. Requested precision does not increase the accuracy or range of the selected type. For unusually large values or precisions where NumPy's intermediate decimal scaling overflows, PyGAD uses scalar rounding to preserve finite values before the final cast.

When types are specified per gene, population arrays use `dtype=object` so each column can retain its requested Python or NumPy scalar type. Saved best solutions retain these types too. This also preserves large integers when other genes are floating-point values. A NumPy array constructed without `dtype=object` can already lose integer precision through conversion to a shared floating-point type; use a list or an object array for mixed input values that must remain exact.
Expand Down
6 changes: 4 additions & 2 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

[PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) is an open-source Python library for building the genetic algorithm and optimizing machine learning algorithms. It works with [Keras](https://keras.io) and [PyTorch](https://pytorch.org).

Watch the [PyGAD 3.8.0 release video on YouTube](https://youtu.be/8pdIiMAMLUM)
and read the [release notes](releases.md#pygad-380).
The current release is PyGAD 3.8.1, dated October 11, 2026.
Read the [release notes](releases.md#pygad-381).

Watch the [PyGAD 3.8.0 release video on YouTube](https://youtu.be/8pdIiMAMLUM).

![PYGAD-LOGO](images/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)

Expand Down
4 changes: 3 additions & 1 deletion docs/source/pygad.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ You can set it to:

Integer conversion truncates towards zero. Floating-point values are rounded before casting, using nearest-even rounding at halfway points. Precision can be `None` to leave values unrounded, or an integer including `0` and negative values. For example, `[float, -1]` rounds to tens. Integer types may only be paired with `None`.

The same rules apply to initialization, mutation, custom operators, and the outputs of `on_parents`, `on_crossover`, and `on_mutation`, independently of `allow_duplicate_genes`. Per-gene type specifications and saved best solutions preserve mixed scalar types using object arrays. See [Conversion and Rounding Rules](https://pygad.readthedocs.io/en/latest/gene_values.html#conversion-and-rounding-rules).
The same rules apply to initialization, mutation, custom operators, and the outputs of `on_parents`, `on_crossover`, and `on_mutation`, independently of `allow_duplicate_genes`. Python `int` uses signed 64-bit conversion for candidate arrays and population columns on every platform, including Windows with NumPy 1.x. Explicit NumPy types keep their requested widths. Per-gene type specifications and saved best solutions preserve mixed scalar types using object arrays. See [Conversion and Rounding Rules](https://pygad.readthedocs.io/en/latest/gene_values.html#conversion-and-rounding-rules) for storage limits and handling larger Python integers.

Version history:

Expand Down Expand Up @@ -381,6 +381,8 @@ A bool that controls how `random` and `adaptive` mutation change a gene when dra
- `True`: replace the gene with the randomly generated value.
- `False` (default): add the random value to the gene.

For NumPy integer gene types, both modes clip the resulting value to the type's limits before casting. For example, a `numpy.uint8` gene at `0` stays at `0` when adding `-1`. See [Conversion and Rounding Rules](gene_values.md#conversion-and-rounding-rules).

Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. See the [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) release notes for an example.

</details>
Expand Down
8 changes: 8 additions & 0 deletions docs/source/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Release notes are listed from newest to oldest. Unreleased contains changes plan

No changes yet.

## PyGAD 3.8.1

Release Date: October 11, 2026.

1. Random and adaptive mutation clip generated values to NumPy integer type limits before casting, fixing overflow errors and wraparound with types such as `numpy.uint8` and `numpy.int8`. The same rule covers additive and replacement mutation, `None` gene-space entries, constraint candidates, and duplicate repair. Clipping applies only to explicitly requested NumPy integer types. See [issue #379](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/379) and [Conversion and Rounding Rules](gene_values.md#conversion-and-rounding-rules).
2. Python `int` conversion of candidate arrays and population columns uses explicit signed 64-bit storage across NumPy versions and platforms, fixing large-integer conversion and initialization failures on Windows with NumPy 1.x. Integer range sampling and bounded operators use the same storage limits. Parent selection and crossover share array allocation, preserving large integers throughout the lifecycle. Per-gene Python `int` elements keep their scalar type, and explicitly requested NumPy types keep their widths. Supplied Python `int` population values must fit in signed 64-bit storage; use `gene_type=object` for larger supplied integers. See [Conversion and Rounding Rules](gene_values.md#conversion-and-rounding-rules).
3. Regression tests cover integer mutation limits, exact large-integer storage, initialization, constraints, duplicate repair, parent selection, crossover, and NSGA-III population growth. Windows CI checks both NumPy 1.24.4 and the latest NumPy release without version-specific implementation branches. The 16 pre-existing Windows failures with NumPy 1.24.4 are fixed, and no higher NumPy minimum is required. The `pygad.helper` and `pygad.utils` submodule versions are `1.4.6` and `1.5.8`.

## PyGAD 3.8.0

Release Date: October 9, 2026.
Expand Down
2 changes: 1 addition & 1 deletion pygad/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.8.0"
__version__ = "3.8.1"
2 changes: 1 addition & 1 deletion pygad/helper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pygad.helper import unique
from pygad.helper import misc

__version__ = "1.4.4"
__version__ = "1.4.6"
51 changes: 36 additions & 15 deletions pygad/helper/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,9 @@ def print_params_summary():
def initialize_parents_array(self, shape):
"""
Allocate an empty parents (or offspring) array with the right
dtype. Uses the dtype of the first gene type when every gene
shares the same type, otherwise falls back to ``object``.
dtype. Uses the gene's storage dtype when every gene shares
the same type, otherwise falls back to ``object``. Python
``int`` uses signed 64-bit array storage.

Parameters
----------
Expand All @@ -282,10 +283,20 @@ def initialize_parents_array(self, shape):
An uninitialised array of the requested shape and dtype.
"""
if self.gene_type_single:
return numpy.empty(shape, dtype=self.gene_type[0])
return numpy.empty(shape, dtype=self._get_gene_storage_dtype(self.gene_type[0]))
else:
return numpy.empty(shape, dtype=object)

def _get_gene_storage_dtype(self, gene_dtype):
"""Return the array storage type for a declared gene type.

Python int uses explicit signed 64-bit storage so array conversion,
allocation, and range sampling agree across NumPy versions and
platforms. Explicit NumPy types keep their requested widths. This
does not change scalar conversion or the declared gene_type.
"""
return numpy.int64 if gene_dtype is int else gene_dtype

def change_population_dtype_and_round(self,
population):
"""
Expand Down Expand Up @@ -359,7 +370,7 @@ def _convert_gene_values(self, values, gene_type):
if precision is None:
if numpy.isscalar(values):
return values if dtype is object else dtype(values)
converted_values = numpy.array(values, dtype=dtype, copy=True)
converted_values = numpy.array(values, dtype=self._get_gene_storage_dtype(dtype), copy=True)
else:
rounded_values = self._round_gene_values(numpy.asarray(values, dtype=float), precision)
converted_values = numpy.asarray(rounded_values, dtype=dtype)
Expand Down Expand Up @@ -397,12 +408,13 @@ def mutation_change_gene_dtype_and_round(self,
Apply a random mutation value to a gene and cast / round the
result. If ``mutation_by_replacement`` is True, the random
value replaces the gene; otherwise it is added to the
existing value.
existing value. NumPy integer results are clipped to the
destination type's limits before casting.

Parameters
----------
random_value : numeric
The freshly drawn mutation value.
random_value : numeric or numpy.ndarray
The freshly drawn mutation value or array of candidates.
gene_index : int
Index of the gene being mutated.
gene_value : numeric
Expand All @@ -414,8 +426,9 @@ def mutation_change_gene_dtype_and_round(self,

Returns
-------
gene_value_new : numeric
The mutated value after casting and rounding.
gene_value_new : numeric or numpy.ndarray
The mutated value or candidates after clipping NumPy
integer values, casting, and rounding.
"""

if mutation_by_replacement:
Expand All @@ -431,6 +444,14 @@ def mutation_change_gene_dtype_and_round(self,
mutated_value = gene_value + random_value
else:
mutated_value = gene_value + numpy.asarray(random_value, dtype=object)
gene_dtype = self.get_gene_dtype(gene_index)[0]
if gene_dtype is not int and numpy.issubdtype(numpy.dtype(gene_dtype), numpy.integer):
type_limits = numpy.iinfo(gene_dtype)
# Object storage and Python bounds keep 64-bit integers exact.
# Clip the final sum or replacement, so an unsigned gene can
# still use negative offsets without wrapping or overflowing.
mutated_value = numpy.clip(numpy.asarray(mutated_value, dtype=object),
int(type_limits.min), int(type_limits.max))
return self.change_gene_dtype_and_round(gene_index, mutated_value)

def validate_gene_constraint_callable_output(self,
Expand Down Expand Up @@ -646,7 +667,7 @@ def convert_bounded_operator_gene_value(self, gene_index, value):
dtype = self.get_gene_dtype(gene_index)[0]
if numpy.issubdtype(numpy.dtype(dtype), numpy.integer):
value = value.item() if isinstance(value, numpy.generic) else value
type_limits = numpy.iinfo(dtype)
type_limits = numpy.iinfo(self._get_gene_storage_dtype(dtype))
first = max(math.ceil(lower), int(type_limits.min))
last = min(math.floor(upper), int(type_limits.max))
if first > last:
Expand Down Expand Up @@ -880,15 +901,15 @@ def _initial_population_range_values(self, gene_index, lower, upper, num_values,
if the gene type and precision cannot represent any valid value.
"""
lower, upper = sorted([lower, upper])
dtype = self.get_gene_dtype(gene_index)[0]
dtype = self._get_gene_storage_dtype(self.get_gene_dtype(gene_index)[0])
if numpy.issubdtype(numpy.dtype(dtype), numpy.integer):
first_value, last_value = self._initial_population_integer_bounds(gene_index, lower, upper)
if first_value > last_value:
raise ValueError(f"The initialization range [{lower}, {upper}) has no value representable by gene_type for the gene at index {gene_index}.")
if all_integer_values:
return numpy.arange(first_value, last_value + 1, dtype=dtype)
# RandomState interprets the Python int type as C long on
# Windows. Use NumPy's resolved dtype to match the population.
# Pass the explicit storage type to RandomState so its integer
# draws use the same width as conversion and allocation.
return self.numpy_random_generator.randint(first_value, last_value + 1,
size=num_values, dtype=numpy.dtype(dtype).type)

Expand All @@ -902,7 +923,7 @@ def _initial_population_integer_bounds(self, gene_index, lower, upper):
lower = lower.item() if isinstance(lower, numpy.generic) else lower
upper = upper.item() if isinstance(upper, numpy.generic) else upper
lower, upper = sorted([lower, upper])
type_limits = numpy.iinfo(self.get_gene_dtype(gene_index)[0])
type_limits = numpy.iinfo(self._get_gene_storage_dtype(self.get_gene_dtype(gene_index)[0]))
first_value = max(math.ceil(lower), int(type_limits.min))
last_value = min(math.ceil(upper) - 1, int(type_limits.max))
if lower == upper and lower == first_value:
Expand All @@ -911,7 +932,7 @@ def _initial_population_integer_bounds(self, gene_index, lower, upper):

def _initial_population_range_snapshot(self, gene_index, lower, upper, sample_size):
"""Create an inspection sample without allocating a range or drawing random values."""
dtype = self.get_gene_dtype(gene_index)[0]
dtype = self._get_gene_storage_dtype(self.get_gene_dtype(gene_index)[0])
if numpy.issubdtype(numpy.dtype(dtype), numpy.integer):
first_value, last_value = self._initial_population_integer_bounds(gene_index, lower, upper)
count = min(sample_size, last_value - first_value + 1)
Expand Down
2 changes: 1 addition & 1 deletion pygad/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
from pygad.utils import validation
from pygad.utils import engine

__version__ = "1.5.7"
__version__ = "1.5.8"
Loading
Loading