{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "(user_guide_optimization_ga_performance)=" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Performance and Advanced Usage" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This page continues the {doc}`bga` walkthrough and covers evaluating the objective function faster, choosing among the built-in representations, and posing objectives symbolically." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Vectorized and parallel evaluation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By default, `evaluate` calls the objective function once per individual in a plain Python loop. If your objective function can consume the whole `(nPop, dim)` array of phenotypes at once (e.g. because it's written with NumPy broadcasting), pass `vectorized=True` to avoid the per-individual Python-level call overhead:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sigmaepsilon.math.optimize import BinaryGeneticAlgorithm as BGA\n", "\n", "ranges = [[-2, 2], [-1, 3]]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "outputs": [], "source": [ "def Rosenbrock_vectorized(x, a=1, b=100):\n", " return (a - x[:, 0]) ** 2 + b * (x[:, 1] - x[:, 0] ** 2) ** 2\n", "\n", "\n", "bga = BGA(\n", " Rosenbrock_vectorized, ranges, length=12, nPop=100, minimize=True, seed=0, vectorized=True\n", ")\n", "result = bga.solve()\n", "result.phenotype, result.fitness" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively, if the objective function is expensive and must still be called once per individual, set `n_jobs` to a value other than `1` to evaluate the population in parallel worker processes (`-1` uses all available CPUs). This requires the objective function to be picklable (a module-level function, not a lambda or a notebook-local closure) so that a worker process can reconstruct it.\n\nIn a regular script this just works, since Python re-imports the script as `__main__` in each worker and finds the function there (guard the entry point with `if __name__ == \"__main__\":`). Inside a Jupyter kernel there is no such script to re-import, so the default `\"spawn\"` start method (macOS/Windows) can't find a notebook-local function in the worker. Switching to the `\"fork\"` start method sidesteps this: a forked worker is a copy of the already-running kernel process, so it already has the function in memory and needs to pickle only the arguments/results, not the function itself. `\"fork\"` is the default start method on Linux, and is used below purely so this example can run inside a notebook \u2014 a regular script doesn't need it.\n\nTo make the timing difference visible, the objective function below does a bit of deliberately wasteful pure-Python busywork per call, standing in for \"a real objective function that's actually expensive to evaluate\":" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "outputs": [], "source": [ "import time\n", "import multiprocessing as mp\n", "\n", "if mp.get_start_method(allow_none=True) != \"fork\":\n", " mp.set_start_method(\"fork\", force=True)\n", "\n", "\n", "def expensive_Rosenbrock(x, a=1, b=100):\n", " # deliberately wasteful pure-Python work, standing in for \"an objective\n", " # function that's actually expensive to evaluate\"\n", " acc = 0.0\n", " for i in range(200_000):\n", " acc += (i % 7) * 1e-9\n", " return (a - x[0]) ** 2 + b * (x[1] - x[0] ** 2) ** 2 + acc\n", "\n", "\n", "def time_evolution(n_jobs, n_generations=5, nPop=48):\n", " bga = BGA(\n", " expensive_Rosenbrock,\n", " ranges,\n", " length=12,\n", " nPop=nPop,\n", " minimize=True,\n", " seed=0,\n", " n_jobs=n_jobs,\n", " )\n", " t0 = time.perf_counter()\n", " bga.evolve(n_generations)\n", " return time.perf_counter() - t0\n", "\n", "\n", "t_serial = time_evolution(n_jobs=1)\n", "t_parallel = time_evolution(n_jobs=-1)\n", "\n", "print(f\"n_jobs=1 (serial): {t_serial:.2f} s\")\n", "print(f\"n_jobs=-1 (parallel): {t_parallel:.2f} s\")\n", "print(f\"speedup: {t_serial / t_parallel:.2f}x (on {mp.cpu_count()} CPUs)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Choosing a representation" ] }, { "cell_type": "markdown", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ "None of `BinaryGeneticAlgorithm`/`IntegerGeneticAlgorithm`/`RealValuedGeneticAlgorithm` will fit every problem \u2014 sometimes the natural representation for a candidate solution (e.g. a permutation) needs its own crossover/mutation operators. See {doc}`custom_ga` for a full, worked walkthrough of writing such a subclass from scratch on top of {py:class}`~sigmaepsilon.math.optimize.ga.GeneticAlgorithm`, reusing the framework's selection strategies, reproducible RNG, and evolution loop." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Symbolic objectives" ] }, { "cell_type": "markdown", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ "Just like for linear programming, you can use the {py:class}`~sigmaepsilon.math.function.function.Function` class to pose problems using symbolic functions." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sympy as sy\n", "from sigmaepsilon.math.function import Function\n", "\n", "variables = sy.symbols(\"x y\")\n", "obj = Function(\"(1-x)**2 + 100*(y-x**2)**2\", variables=variables)\n", "ranges = [[-2, 2], [-1, 3]]\n", "bga = BGA(obj, ranges, length=12, nPop=100, minimize=True)\n", "result = bga.solve()\n", "result.phenotype, result.fitness" ] } ], "metadata": { "kernelspec": { "display_name": "sigmaepsilon-math-NMVzm02N-py3.12", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }