{ "cells": [ { "cell_type": "raw", "metadata": { "editable": true, "raw_mimetype": "text/restructuredtext", "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ ".. _user_guide_function:" ] }, { "cell_type": "markdown", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ "# Functions and Relations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The library provides a set of classes to facilitate the definition of complex functions. These functions are mainly used to set up linear and nonlinear optimization functions, where derivative information or symbolic input is relevant. Also, they aim to provide a unified way to feed other classes and functions in the library." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Functions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The most important class is the `Function` class. It allows for symbolic and numerical definition of functions, where derivative information is important. For instance, symbolic definition of a function looks like this:" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [] }, "outputs": [], "source": [ "from sigmaepsilon.math.function import Function\n", "import sympy as sy\n", "\n", "x1, x2, x3, x4 = variables = sy.symbols(['x1', 'x2', 'x3', 'x4'])\n", "f = Function(3*x1 + 9*x3 + x2 + x4, variables=variables)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After defining the function, the first and second derivatives are determined automatically, and the symbolic expressions are turned into high-performance NumPy functions via SymPy. You can evaluate the function, its gradient and Hessian as well." ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10" ] }, "execution_count": 65, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f([0, 6, 0, 4])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To evaluate the gradient, call the `g` method of the instance." ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([3, 1, 9, 1])" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f.g([0, 6, 0, 4])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To evaluate the Hessian, call the `G` method (since the function is linear, the Hessian is zero now)." ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0, 0, 0, 0],\n", " [0, 0, 0, 0],\n", " [0, 0, 0, 0],\n", " [0, 0, 0, 0]])" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f.G([0, 6, 0, 4])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To define the same function numerically, we feed the `Function` class with custom function implementations." ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# implementation of the function f(x, y) = x^2 + y\n", "def f0(x, y):\n", " return x**2 + y\n", "\n", "# implementation of the gradient of f(x, y)\n", "def f1(x, y):\n", " return np.array([2*x, 1])\n", "\n", "# implementation of the hessian of f(x, y)\n", "def f2(x, y):\n", " return np.array([[0, 0], [0, 0]])\n", "\n", "f = Function(f0, f1, f2, dimension=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If the function instance is defined by custom callables, the way the arguments have to be passed depends on those callables. In the previous case, the functions `f0`, `f1` and `f2` expect two arguments, hence the inputs must be provided as such." ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0,\n", " array([0, 1]),\n", " array([[0, 0],\n", " [0, 0]]))" ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f(0, 0), f.g(0, 0), f.G(0, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's take a look at the next code block, where we reimplement the same function, with the exception that the callables now expect an iterable as the input instead of the coordinates separately." ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [], "source": [ "f0 = lambda x: x[0] ** 2 + x[1]\n", "f1 = lambda x: np.array([2*x[0], 1])\n", "f2 = lambda x: np.array([[0, 0], [0, 0]])\n", "f = Function(f0, f1, f2, dimension=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Consequently, the instance has to be called with vectorized inputs." ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0,\n", " array([0, 1]),\n", " array([[0, 0],\n", " [0, 0]]))" ] }, "execution_count": 71, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f([0, 0]), f.g([0, 0]), f.G([0, 0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Relations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Relations are like functions, but they either express an equality or an inequality of some sort and they are mainly used to express constraints when dealing with mathematical programming problems." ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [], "source": [ "from sigmaepsilon.math.function import Equality, InEquality, Relation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the following block, you see examples to create a computational representation of the equality\n", "\n", "$$\n", "x_1 + 2 x_3 + x_4 = 4\n", "$$" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [], "source": [ "x1, x2, x3, x4 = variables = sy.symbols(['x1', 'x2', 'x3', 'x4'])\n", "eq = Equality(lambda x: x[0] + 2*x[2] + x[3] - 4)\n", "eq = Equality(x1 + 2*x3 + x4 - 4, variables=variables)\n", "eq = Equality(\"x1 + 2*x3 + x4 = 4\", variables=variables)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, symbolic definitions have the advantage of the gradient and Hessian of the input expression being calculated automatically." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Both the `Equality` and the `Inequality` classes are subclasses of `Function`, hence they can be called similarly." ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(-4,\n", " array([1, 0, 2, 1]),\n", " array([[0, 0, 0, 0],\n", " [0, 0, 0, 0],\n", " [0, 0, 0, 0],\n", " [0, 0, 0, 0]]))" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eq([0, 0, 0, 0]), eq.g([0, 0, 0, 0]), eq.G([0, 0, 0, 0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Relations can also be related. The `relate` method returns `True` if the relation is satisfied and `False` if it is not." ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(True, False)" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eq.relate([4, 0, 0, 0]), eq.relate([0, 0, 0, 0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The definition and usage of inequalities is very similar, except that now you also have to specify the operator at instantiation." ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False\n", "True\n", "True\n", "False\n" ] } ], "source": [ "x, y = variables = sy.symbols(\"x y\")\n", "\n", "gt = InEquality('x + y', op='>', variables=variables)\n", "ge = InEquality('x + y >= 0', variables=variables)\n", "le = InEquality('x + y', op=lambda x, y: x <= y, variables=variables)\n", "lt = InEquality('x + y', op=lambda x, y: x < y, variables=variables)\n", "\n", "print(gt.relate([0, 0]))\n", "print(ge.relate([0, 0]))\n", "print(le.relate([0, 0]))\n", "print(lt.relate([0, 0]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From version 2.0, you don't necessarily have to use the classes `Equality` and `Inequality`, as the `Relation` class is able to decide the type itself." ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n" ] } ], "source": [ "ieq = Relation('x + y >= 2', variables=variables)\n", "eq = Relation('x + y = 2', variables=variables)\n", "\n", "print(type(ieq))\n", "print(type(eq))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.10.11" }, "vscode": { "interpreter": { "hash": "4e251a336b180e3c877fd4b81be72acfad98293ac2abcf90f00390a06765d313" } } }, "nbformat": 4, "nbformat_minor": 4 }