codecov-io: # [Codecov](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=h1) Report
> Merging [#27](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=desc) into [master](https://codecov.io/gh/evopy/evopy/commit/1ca150c0a4bd76cde3e989aafa114dc476928201?src=pr&el=desc) will **increase** coverage by `0.31%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/evopy/evopy/pull/27/graphs/tree.svg?width=650&token=XeKRLzWOmy&height=150&src=pr)](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #27      +/-   ##
==========================================
+ Coverage   94.95%   95.27%   +0.31%     
==========================================
  Files           7        7              
  Lines         119      127       +8     
  Branches       26       27       +1     
==========================================
+ Hits          113      121       +8     
  Misses          3        3              
  Partials        3        3
```


| [Impacted Files](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [evopy/evopy.py](https://codecov.io/gh/evopy/evopy/pull/27/diff?src=pr&el=tree#diff-ZXZvcHkvZXZvcHkucHk=) | `96% <100%> (+0.76%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=footer). Last update [1ca150c...81e87b4](https://codecov.io/gh/evopy/evopy/pull/27?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/.pylintrc b/.pylintrc
index d826f05..6490d05 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -527,10 +527,10 @@ valid-metaclass-classmethod-first-arg=cls
 [DESIGN]
 
 # Maximum number of arguments for function / method.
-max-args=15
+max-args=20
 
 # Maximum number of attributes for a class (see R0902).
-max-attributes=15
+max-attributes=20
 
 # Maximum number of boolean expressions in an if statement.
 max-bool-expr=5
@@ -539,7 +539,7 @@ max-bool-expr=5
 max-branches=12
 
 # Maximum number of locals for function / method body.
-max-locals=15
+max-locals=20
 
 # Maximum number of parents for a class (see R0901).
 max-parents=7
diff --git a/evopy/evopy.py b/evopy/evopy.py
index 292e875..de3a10d 100644
--- a/evopy/evopy.py
+++ b/evopy/evopy.py
@@ -1,4 +1,6 @@
 """Module used for the execution of the evolutionary algorithm."""
+import time
+
 import numpy as np
 
 from evopy.individual import Individual
@@ -12,7 +14,8 @@ class EvoPy:
 
     def __init__(self, fitness_function, individual_length, warm_start=None, generations=100,
                  population_size=30, num_children=1, mean=0, std=1, maximize=False,
-                 strategy=Strategy.SINGLE_VARIANCE, random_seed=None, reporter=None):
+                 strategy=Strategy.SINGLE_VARIANCE, random_seed=None, reporter=None,
+                 target_fitness_value=None, max_run_time=None):
         """Initializes an EvoPy instance.
 
         :param fitness_function: the fitness function on which the individuals are evaluated
@@ -28,6 +31,8 @@ class EvoPy:
                          information, check the Strategy enum
         :param random_seed: the seed to use for the random number generator
         :param reporter: callback to be invoked at each generation with a ProgressReport as argument
+        :param target_fitness_value: target fitness value for early stopping
+        :param max_run_time: maximum time allowed to run in seconds
         """
         self.fitness_function = fitness_function
         self.individual_length = individual_length
@@ -42,6 +47,21 @@ class EvoPy:
         self.random_seed = random_seed
         self.random = random_with_seed(self.random_seed)
         self.reporter = reporter
+        self.target_fitness_value = target_fitness_value
+        self.max_run_time = max_run_time
+
+    def _check_early_stop(self, start_time, best):
+        """Check whether the algorithm can stop early, based on time and fitness target.
+
+        :param start_time: the starting time to compare against
+        :param best: the current best individual
+        :return: whether the algorithm should be terminated early
+        """
+        return (self.max_run_time is not None
+                and (time.time() - start_time) > self.max_run_time) \
+               or \
+               (self.target_fitness_value is not None
+                and abs(best.fitness - self.target_fitness_value) < np.finfo(float).eps)
 
     def run(self):
         """Run the evolutionary strategy algorithm.
@@ -51,6 +71,8 @@ class EvoPy:
         if self.individual_length == 0:
             return None
 
+        start_time = time.time()
+
         population = self._init_population()
         best = sorted(population, reverse=self.maximize,
                       key=lambda individual: individual.evaluate(self.fitness_function))[0]
@@ -62,9 +84,13 @@ class EvoPy:
                                 key=lambda individual: individual.evaluate(self.fitness_function))
             population = population[:self.population_size]
             best = population[0]
+
             if self.reporter is not None:
                 self.reporter(ProgressReport(generation, best.genotype, best.fitness))
 
+            if self._check_early_stop(start_time, best):
+                break
+
         return best.genotype
 
     def _init_population(self):

Support setting a target fitness value for early stop
diff --git a/test/test_evopy.py b/test/test_evopy.py
index 9ddda48..56fa63c 100644
--- a/test/test_evopy.py
+++ b/test/test_evopy.py
@@ -3,12 +3,14 @@ import numpy as np
 
 from evopy import EvoPy
 
+
 def test_random_consistency():
     """Test whether the random state gives consistent runs when initialized."""
     x_first = EvoPy(lambda x: pow(x, 2), 1, random_seed=42).run()
     x_second = EvoPy(lambda x: pow(x, 2), 1, random_seed=42).run()
     assert x_first == x_second
 
+
 def test_random_consistency_multiple_runs():
     """"Test whether the random state is not re-used in sequential runs"""
     evopy = EvoPy(lambda x: pow(x, 2), 1, random_seed=42)
@@ -16,6 +18,7 @@ def test_random_consistency_multiple_runs():
     x_second = evopy.run()
     assert x_first != x_second
 
+
 def test_simple_use_case():
     """Test whether evopy can successfully run for a simple evaluation function."""
     evopy = EvoPy(lambda x: pow(x, 2), 1)
@@ -25,6 +28,32 @@ def test_simple_use_case():
     assert best_individual.size == 1
 
 
+def test_early_timed_stop():
+    """Test whether evopy can successfully stop early when given a specified time constraint."""
+    count = [0]
+
+    def increment_reporter(report):
+        count[0] = report.generation + 1
+
+    evopy = EvoPy(lambda x: pow(x, 2), 1, max_run_time=0, reporter=increment_reporter)
+    evopy.run()
+
+    assert count[0] == 1
+
+
+def test_early_target_value_stop():
+    """Test whether evopy can successfully stop after achieving a specified target value."""
+    count = [0]
+
+    def increment_reporter(report):
+        count[0] = report.generation + 1
+
+    evopy = EvoPy(lambda x: 0, 1, target_fitness_value=0, reporter=increment_reporter)
+    evopy.run()
+
+    assert count[0] == 1
+
+
 def test_empty_input_array():
     """Test whether evopy can successfully run for a simple evaluation function."""
     evopy = EvoPy(lambda x: 0, 0)

pip install -e .[dev]
parse_log_pytest
requirements.txt
nose pylint pytest
3.9
requirements.dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
astroid==3.3.9
babel==2.17.0
certifi==2025.1.31
charset-normalizer==3.4.1
codecov==2.1.13
coverage==7.8.0
dill==0.3.9
docutils==0.21.2
-e git+https://github.com/evopy/evopy.git@1ca150c0a4bd76cde3e989aafa114dc476928201#egg=evopy
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
MarkupSafe==3.0.2
mccabe==0.7.0
nose==1.3.7
numpy==2.0.2
packaging==24.2
parameterized==0.9.0
platformdirs==4.3.7
pluggy==1.5.0
Pygments==2.19.1
pylint==3.3.6
pytest==8.3.5
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
tomlkit==0.13.2
typing_extensions==4.13.0
urllib3==2.3.0
zipp==3.21.0

name: evopy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - astroid==3.3.9
      - babel==2.17.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - coverage==7.8.0
      - dill==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isort==6.0.1
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - nose==1.3.7
      - numpy==2.0.2
      - packaging==24.2
      - parameterized==0.9.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pygments==2.19.1
      - pylint==3.3.6
      - pytest==8.3.5
      - requests==2.32.3
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - tomlkit==0.13.2
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - zipp==3.21.0
prefix: /opt/conda/envs/evopy

test/test_evopy.py::test_early_timed_stop test/test_evopy.py::test_early_target_value_stop
test/test_evopy.py::test_random_consistency test/test_evopy.py::test_random_consistency_multiple_runs test/test_evopy.py::test_simple_use_case test/test_evopy.py::test_empty_input_array test/test_evopy.py::test_multi_dimensional_use_case
evopy/evopy.py .pylintrc
evopy/evopy.py .pylintrc