diff --git a/docs/source/_static/images/optimagic_logo.svg b/docs/source/_static/images/optimagic_logo.svg new file mode 100644 index 000000000..1e83bcbf4 --- /dev/null +++ b/docs/source/_static/images/optimagic_logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/_static/images/optimagic_logo_dark_mode.svg b/docs/source/_static/images/optimagic_logo_dark_mode.svg new file mode 100644 index 000000000..93f55c402 --- /dev/null +++ b/docs/source/_static/images/optimagic_logo_dark_mode.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/conf.py b/docs/source/conf.py index f26665066..ca8e01219 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -198,8 +198,8 @@ html_theme_options = { "sidebar_hide_name": True, "navigation_with_keys": True, - "light_logo": "images/estimagic_logo.svg", - "dark_logo": "images/estimagic_logo_dark_mode.svg", + "light_logo": "images/optimagic_logo.svg", + "dark_logo": "images/optimagic_logo_dark_mode.svg", "light_css_variables": { "color-brand-primary": "#f04f43", "color-brand-content": "#f04f43", diff --git a/docs/source/how_to/how_to_bounds.ipynb b/docs/source/how_to/how_to_bounds.ipynb index a1939dd7f..3b8c1ee22 100644 --- a/docs/source/how_to/how_to_bounds.ipynb +++ b/docs/source/how_to/how_to_bounds.ipynb @@ -17,14 +17,14 @@ "id": "b3c135aa", "metadata": {}, "source": [ - "## Example criterion function\n", + "## Example objective function\n", "\n", "Let’s again look at the sphere function:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "ec477eb7", "metadata": {}, "outputs": [], @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "b0eb906d", "metadata": {}, "outputs": [], @@ -53,7 +53,7 @@ { "data": { "text/plain": [ - "array([ 0.00000000e+00, -1.33177530e-08, 7.18836679e-09])" + "array([ 0., -0., 0.])" ] }, "execution_count": 4, @@ -63,7 +63,7 @@ ], "source": [ "res = om.minimize(fun=fun, params=np.arange(3), algorithm=\"scipy_lbfgsb\")\n", - "res.params" + "res.params.round(5)" ] }, { @@ -97,7 +97,10 @@ ], "source": [ "res = om.minimize(\n", - " fun=fun, params=np.arange(3), lower_bounds=np.ones(3), algorithm=\"scipy_lbfgsb\"\n", + " fun=fun,\n", + " params=np.arange(3),\n", + " bounds=om.Bounds(lower=np.ones(3)),\n", + " algorithm=\"scipy_lbfgsb\",\n", ")\n", "res.params" ] @@ -112,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "26c5c0df", "metadata": {}, "outputs": [ @@ -122,7 +125,7 @@ "array([-1.00000000e+00, -3.57647467e-08, 1.00000000e+00])" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -132,8 +135,10 @@ " fun=fun,\n", " params=np.arange(3),\n", " algorithm=\"scipy_lbfgsb\",\n", - " lower_bounds=np.array([-2, -np.inf, 1]),\n", - " upper_bounds=np.array([-1, np.inf, np.inf]),\n", + " bounds=om.Bounds(\n", + " lower=np.array([-2, -np.inf, 1]),\n", + " upper=np.array([-1, np.inf, np.inf]),\n", + " ),\n", ")\n", "res.params" ] @@ -185,7 +190,7 @@ " fun=fun,\n", " params=params,\n", " algorithm=\"scipy_lbfgsb\",\n", - " lower_bounds={\"intercept\": -2},\n", + " bounds=om.Bounds(lower={\"intercept\": -2}),\n", ")\n", "res.params" ] @@ -295,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 10, "id": "34d59f01", "metadata": {}, "outputs": [], @@ -309,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 11, "id": "b284ad8a", "metadata": {}, "outputs": [ @@ -374,7 +379,7 @@ "intercept 0 -2.0 -2" ] }, - "execution_count": 25, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -387,6 +392,24 @@ ")\n", "res.params" ] + }, + { + "cell_type": "markdown", + "id": "d2a20601", + "metadata": {}, + "source": [ + "## Coming from scipy" + ] + }, + { + "cell_type": "markdown", + "id": "bd83f842", + "metadata": {}, + "source": [ + "If `params` is a flat numpy array, you can also provide bounds in any format that \n", + "is supported by [`scipy.optimize.minimize`](\n", + "https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html). " + ] } ], "metadata": { diff --git a/docs/source/how_to/how_to_slice_plot.ipynb b/docs/source/how_to/how_to_slice_plot.ipynb index 5407e59a8..e6d43b4e7 100644 --- a/docs/source/how_to/how_to_slice_plot.ipynb +++ b/docs/source/how_to/how_to_slice_plot.ipynb @@ -52,8 +52,10 @@ "outputs": [], "source": [ "params = {\"alpha\": 0, \"beta\": 0, \"gamma\": 0, \"delta\": 0}\n", - "lower_bounds = {name: -5 for name in params}\n", - "upper_bounds = {name: i + 2 for i, name in enumerate(params)}" + "bounds = om.Bounds(\n", + " lower={name: -5 for name in params},\n", + " upper={name: i + 2 for i, name in enumerate(params)},\n", + ")" ] }, { @@ -70,7 +72,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAgAElEQVR4XuzdB3gVxd7H8R8BAiShN+kdpRcB6b1IURQVG4ooqNgV9AqCylWxvFzBiiIgKiAoKAg2lF6kWOgtFOm9BhISSt5nFhMpITknu2dzynef5z5cye6Uzywz+e/uzGRKTExMFAcCCCCAAAIIIIAAAggg4IJAJgIQF5TJAgEEEEAAAQQQQAABBCwBAhBuBAQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAABBBBAAAECEO4BBBBAAAEEEEAAAQQQcE2AAMQ1ajJCAAEEEEAAAQQQQAABAhDuAQQQQAABBBBAAAEEEHBNgADENWoyQgABBBBAAAEEEEAAAQIQ7gEEEEAAAQQQQAABBBBwTYAAxDVqMkIAAQQQQAAB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QAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAABBBBAAAEEEHBAgADEAUSSQAABBBBAAAEEEEAAAc8ECEA8c+IsBBBAAAEEEEAAAQQQcECAAMQBRJJAAAEEEEAAAQQQQAABzwQIQDxz4iwEEEAAAQQQQAABBBBwQIAAxAFEkkAAAQQQQAABBBBAAAHPBAhAPHPiLAQQQAAB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qlULvXv3pgBxgzHKJpIACbyagNzvEBPaXbt2xZi8nj59Ws3p0SZYslkjd/XkuVel2AJEhMYHH3ygzLiik5QlZlzR5q5igtugQQOegHBQkoCVBChArATGx81NYOXKlZA/48aNg4eHB1KlShVzsVAWMll05MKi7HiJza/c45AjfS0FyLx581QdFixYoOycp06dit9//12d1NAEy9zjk60jARJ4PQFxuStmtPJHnH48ePBAecQSQRItQESkiMdA2UySu35yH082gN5++201n8YWIGLiWrVqVTXXyimKrAn9+/dH0qRJlXfDmzdvQk7N9+3bh1WrVvEEhIOTBKwgQAFiBSw+an4CspDIjpecakiSBUe8qsh9DblELh/78+fPVx6wxCxLTLLk6F1LASLvlvsechIjC+H777+v6iPuIClAzD9G2UISIIHXEzh16pQykxU3vGIKK2Ljl19+USIi+pK43BMRN7xyFy9lypQoV66c8ooVV4DIW2bMmKHMXcXUSjan5A6IzL/yHjHBEnNYKUvMumiCxZFJApYToACxnBWfNDkBcWfbvXt3tVgVKVJEtVbufXTr1g29evV6wWuKyVGweSRAAiRAAiRAAiTgNAIUIE5Dy4KNRmDu3Lk4duyYulwYO8kOmphZ8XTBaD3K+pIACZAACZAACeiRAAWIHnuFdXIJAbENluN0ceWYK1cuiD2xmFfJPY/ly5cjd+7cLqkXX0oCJEACJEACJEACZiJAAWKm3mRb7CYgpyBiKyyuGyWqrZhiyQmIeE1hIgESIAESIAESIAESsJ8ABYj9DFkCCZAACZAACZAACZAACZCAhQQoQCwExcdIgARIgARIgARIgARIgATsJ0ABYj9DlkACJEACJEACJEACJEACJGAhAQoQC0HxMRIgARIgARIgARIgARIgAfsJUIDYz5AlkAAJkAAJkAAJkAAJkAAJWEiAAsRCUHyMBEiABEiABEiABEiABEjAfgIUIPYzZAkkQAIkQAIkQAIkQAIkQAIWEqAAsRAUHyMBEiABEiABEiABEiABErCfAAWI/QxZAgmQAAmQAAmQAAmQAAmQgIUEKEAsBMXHSIAESIAESIAESIAESIAE7CdAAWI/Q5ZAAiRAAiRAAiRAAiRAAiRgIQEKEAtB8TESIAESIAESIAESIAESIAH7CVCA2M+QJZAACZAACZAACZAACZAACVhIgALEQlB8jARIgARIgARIgARIgARIwH4CFCD2M2QJJEACJEACJEACJEACJEACFhKgALEQFB8jARIgARIgARIgARIgARKwnwAFiP0MWQIJkAAJkAAJkAAJkAAJkICFBChALATFx0iABEiABEiABEiABEiABOwnQAFiP0OWQAIkQAIkQAIkQAIkQAIkYCEBChALQfExEiABEiABEiABEiABEiAB+wlQgNjPkCWQAAmQAAmQAAmQAAmQAAlYSIACxEJQfIwESIAESIAESIAESIAESMB+AhQg9jNkCSRAAiRAAiRAAiRAAiRAAhYSoACxEBQfIwESIAESIAESIAESIAESsJ8ABYj9DFkCCZAACZAACZAACZAACZCAhQQoQCwExcdIgARIgARIgARIgARIgATsJ0ABYj9DlkACJEACJEACJEACJEACJGAhgf8DKjGkyK7DXqoAAAAASUVORK5CYII=" + "image/png": "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r7/iwIEDqFChAjp37oxixYqp++bMmQNZ6MqXL49Zs2bh+vXrqFmzJr799lvMmDEDS5YsQUhICD766CO0adMGXl5e6r4//vgD8+bNUzZKkfr69u0bUa8DkPERJEACJKAbAmfOnFFzuMzVModnyZJF7X6Y74DIvDx+/Hg1F/v4+OD9999H165d4e7uDvMdkLJly6rdj4sXL6JgwYJqByVPnjwYOnQo517d9DgNMTIBChAj9x5ttxmBq1evolq1amrRat++PdKkSYMFCxaoRcq0eK1evRq7du1C8eLF1WIk2/LLli1TIqBkyZJ49OgRSpQooWyS7Xu5bunSpTh06JD6u2bNmqFAgQLq+levXkHqkzJixAhMnjxZLYZyzcuXLzF27Fj1b76+vmjSpAnu37+PKVOmqIWzdu3a6t9GjRql6hEhJP/7119/4fz589i+fTuSJUtmMzasiARIgAT0TuDWrVuoWLEikiZNig4dOiBjxoxYvHgxAgICIubwLVu2qI9GMj/LPHr48GE198pHok6dOkUSIH5+fuqjkXwU6tWrl1oTZH2oV68e5169DwbaZwgCFCCG6CYaaW8C8tXszz//VC/vGTJkUI+Lafs+PDwcwcHBuHv3LurWrYsvv/xSLWomAfLdd9+p3QopIj6aNm2K3377De+++676O9MiuG3bNrVIigBZuHCh+nuTn7IsoNeuXVMCx3R4UoRI4cKF8f3330fCERoaqgSK7LrIDogIJ9Puir25sX4SIAES0AMBObcxadIkbN68We18RDeHyxycPn16TJs2LcLk3r17IzAwUH0QinoGJDYXLM69euh52mBUAhQgRu052m1TArLV/vDhQ/XCbypRBci9e/cwfPhwrF27VrlgmYosYPKFzCRAzMXG5cuXUaNGDbVlX6VKFXXL8ePHlduWSSiIAJHFb+PGjRF1fvXVVzh9+rT6gmcq4iYgrlhSlxRxGRN7xN3AvMgXO3EfYCEBEiABVyEgO9fivmraWY4qQBImTIhChQqpXQz58GMqJpdaESGWChDOva4yqthOexKgALEnXdZtGALNmzeHLFBz5859Q4AcO3YMnp6eaidDBMU333yDd955R53rqF69utrtiEmAmFy7zAWILF7vvffeWwXIwIEDlcAwFyDdu3fHixcvlACRHZhSpUpFnPvInTu3ElANGjRQLgMUIIYZejSUBEjABgRkhzhlypSRdjfMPyLJboW4xcpcX6tWrUhPTJAggfpAZIkA4dxrg85iFSQAgAKEw4AEAOVGJS/74hOcJEkSxWT69On44YcflP+wafHq378/unTpEsFMXvS1ECBykFLctObPnx9x7kS+5MnhdQoQDmkSIAFXIyA70bL7cerUKSRKlEg1P+outszX8mfMmDGR8IhbrYiQqAJEzgCKYFm5ciXy5s2r7uHc62oji+21FwEKEHuRZb2GImA6qyHb8+IeFRQUhPXr16s2mA6hy8FFWdhErIggERcqWZje5oJlrx0QcQeThVQiuLRq1Qo3b95UCbdOnDhBAWKokUdjSYAEbEFgzZo1aie6Tp06aNmypYpeJUJD5krTHC4fZwYPHqyiEMp8LgE/RGTIuRE5FxJVgMi/i9uWuOiKEHn8+DFy5szJudcWHcY6XJ4ABYjLDwECMBGQrLkSSerOnTsoUqSI8hWWCClyFkPcs+TQuBwwF3EiRRYwOTPSp08flTlXFifZ4jc/AyIHySWjrixulStXVvfJFzoJ1SsHz01Jr1atWhXpDIi4eYmYMHfBkmeIC5ZEw5IirlgSLct0HkVcEOR6yfpbpkwZdiwJkAAJuAwBiQQoO9YSNl2KzN8SnVA+JJncaOUaCW0+evToSOf4RJB8/vnnKpS6uNeaEhFKPRKcZOLEiUrISHAP+fDEuddlhhUbakcCFCB2hMuqjU1AxIZEQTHthEhrZKv+woULKiSjt7e35g0UQSK7LJJvxJQfRHOjaAAJkAAJaERAzmjIn6xZs6oPR9EVmcclf5L8b9q0aSNctmIyWXa85cOUXOvm5qYu49yrUQfzsU5DgALEabqSDbGGgCwmsm0vyQBlkZFdCtlNkKRTsvXOQgIkQAIkQAIkQAIkYBsCFCC24chaDE5AwtvK2Q7JpCtiJFu2bOosiMSNl8OJLCRAAiRAAiRAAiRAArYhQAFiG46shQRIgARIgARIgARIgARIwAICFCAWQOIlJEACJEACJEACJEACJEACtiFAAWIbjqyFBEiABEiABEiABEiABEjAAgIUIBZA4iUkQAIkQAIkQAIkQAIkQAK2IUABYhuOrIUESIAESIAESIAESIAESMACAhQgFkDiJSRAAiRAAiRAAiRAAiRAArYhQAFiG46shQRIgARIgARIgARIgARIwAICFCAWQOIlJEACJEACJEACJEACJEACtiFAAWIbjqyFBEiABEiABEiABEiABEjAAgIUIBZA4iUkQAIkQAIkQAIkQAIkQAK2IUABYhuOrIUESIAESIAESIAESIAESMACAhQgFkDiJSRAAiRAAiRAAiRAAiRAArYhQAFiG46shQRIgARIgARIgARIgARIwAICFCAWQOIlJEACJEACJEACJEACJEACtiFAAWIbjqyFBEiABEiABEiABEiABEjAAgIUIBZA4iUkQAIkQAIkQAIkQAIkQAK2IUABYhuOrIUESIAESIAESIAESIAESMACAhQgFkDiJSRAAiRAAiRAAiRAAiRAArYhQAFiG46shQRIgARIgARIgARIgARIwAICFCAWQOIlJEACJEACJEACJEACJEACtiFAAWIbjqyFBEiABEiABEiABEiABEjAAgIUIBZA4iUkQAIkQAIkQAIkQAIkQAK2IUABYhuOrIUESIAESIAESIAESIAESMACAhQgFkDiJSRAAiRAAiRAAiRAAiRAArYhQAFiG46shQRIgARIgARIgARIgARIwAICFCAWQOIlJEACJEACJEACJEACJEACtiFAAWIbjqyFBEiABEiABEiABEiABEjAAgIUIBZA4iUkQAIkQAIkQAIkQAIkQAK2IUABYhuOrIUESIAESIAESIAESIAESMACAhQgFkDiJSRAAiRAAiRAAiRAAiRAArYh8H8T6DgEjaxT5QAAAABJRU5ErkJggg==" }, "metadata": {}, "output_type": "display_data" @@ -80,8 +82,7 @@ "fig = om.slice_plot(\n", " func=sphere,\n", " params=params,\n", - " lower_bounds=lower_bounds,\n", - " upper_bounds=upper_bounds,\n", + " bounds=bounds,\n", ")\n", "fig.show(renderer=\"png\")" ] @@ -115,7 +116,7 @@ "outputs": [ { "data": { - "image/png": "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" + "image/png": "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IIAAAggggEAgCBCABEItU0YEEEAAAQQQQAABBCwiQABikYogG9YXSExMlAULFkhSUpK0adNGQkNDkzN98eJFuffee+XJJ5+UG264wanCrFmzRt5880357LPPpFixYk6dw0EIIIAAAhkLrF27Vo4fP24OyJUrl+TNm1cqVKggZcqUgQ0BBCwkQABiocogK9YW0IChT58+JpNffPGFtGvXLjnD58+fl/r168vIkSOle/fuThVk4cKFMmDAAFm0aJGUKlXKqXM4CAEEEEAgY4HHH39c5s6de80Bd999twwZMsRpujvvvNMELvolEQkBBNwvQADiflOu6KcC+vKaPXu2KV3Tpk1l1KhRBCB+WtcUCwEE7CmgAcixY8fkxx9/NAU4c+aMvPXWW+bv8+fPl6ioKKcKpl82aQAyYsQIp47nIAQQyJ4AAUj2vDg6QAUuXbpkgo4HHnjACHz88ceyatUqCQ8PN39P2wNy4cIF6devn9x4443muGXLlklERIToy/H222835zh6QPTlOG3aNNm4caO0b99e+vfvL3Xq1DHHLF26VN5++23Zu3evxMXFSfXq1U2vSY8ePQK0Jig2AgggkLFA2gBEj/z555/lueeekz/++CN5KNbZs2flvffek3nz5snhw4elWbNm8vLLL0vNmjXNsFj9WYECBUybq+mll16SyMhIGTRokOzYsUNiY2PN37UtfvrppyUkJIRqQQCBbAgQgGQDi0MDV0C79PXF9ssvvxiEbt26ma55RzCRNgDRl1vDhg3NsXqMfpM2efJk2b9/v0yaNEkaNWqUHIDoMRrY6DdzX3/9tRQuXFh++uknc672uPz5559y3XXXmbHM+g3e9OnTk68RuDVCyRFAAIFrBbSd1gDhnXfeEZ23d+TIEfP/+gWSYzjVlStXpHfv3nLq1Ckzd0+/HPr2229l165dsnjxYlm/fr28+OKLUrx4cenVq5e5Sdu2bSUhIUHeffddE6wULVpUtm/fbnrCn332WXn00UepDgQQyIYAAUg2sDg0cAX0pbZv3z6ZOXOmQbj55pulYMGCMnHiRPP3jAKQ4cOHS9++fc0x586dM4GEdu2//vrryQHIf//7X/OtmyZHoLNkyRIpWbJkMrhOfD99+rScOHFCunTpIi+88ILpCSEhgAACCPwjkNkckKeeesoEDvpFjgYMOiyrQYMG5uRt27aZ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" }, "metadata": {}, "output_type": "display_data" @@ -125,8 +126,7 @@ "fig = om.slice_plot(\n", " func=sphere,\n", " params=params,\n", - " lower_bounds=lower_bounds,\n", - " upper_bounds=upper_bounds,\n", + " bounds=bounds,\n", " # selecting a subset of params\n", " selector=lambda x: [x[\"alpha\"], x[\"beta\"]],\n", " # evaluate func in parallel\n", @@ -157,7 +157,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/docs/source/how_to/how_to_visualize_histories.ipynb b/docs/source/how_to/how_to_visualize_histories.ipynb index 7d5091ff5..91fd96c45 100644 --- a/docs/source/how_to/how_to_visualize_histories.ipynb +++ b/docs/source/how_to/how_to_visualize_histories.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "id": "8675ff3f", "metadata": {}, "outputs": [], @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "5efb43c8", "metadata": {}, "outputs": [], @@ -60,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "32cf04a2", "metadata": {}, "outputs": [ @@ -87,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "d641708a", "metadata": {}, "outputs": [ @@ -114,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "72b6938c", "metadata": {}, "outputs": [ @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 6, "id": "45e853a5", "metadata": {}, "outputs": [ @@ -174,7 +174,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "id": "c09ded87", "metadata": {}, "outputs": [ @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 8, "id": "70099614", "metadata": {}, "outputs": [], @@ -219,8 +219,7 @@ "res = om.minimize(\n", " sphere,\n", " params=np.arange(10),\n", - " soft_lower_bounds=np.full(10, -3),\n", - " soft_upper_bounds=np.full(10, 10),\n", + " bounds=om.Bounds(soft_lower=np.full(10, -3), soft_upper=np.full(10, 10)),\n", " algorithm=\"scipy_neldermead\",\n", " multistart=True,\n", " multistart_options={\"n_samples\": 1000, \"convergence.max_discoveries\": 10},\n", @@ -229,7 +228,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "id": "e21dcd65", "metadata": {}, "outputs": [ diff --git a/docs/source/index.md b/docs/source/index.md index 6b8e1b9fb..20b5a20d5 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -4,9 +4,9 @@ ```{raw} html - + - + ```
diff --git a/docs/source/tutorials/numdiff_overview.ipynb b/docs/source/tutorials/numdiff_overview.ipynb index b034796ce..709f8dd27 100644 --- a/docs/source/tutorials/numdiff_overview.ipynb +++ b/docs/source/tutorials/numdiff_overview.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -411,54 +411,30 @@ "fd = om.first_derivative(\n", " func=sphere,\n", " params=params,\n", - " lower_bounds=params, # forces first_derivative to use forward differences\n", - " upper_bounds=params + 1,\n", + " # forces first_derivative to use forward differences\n", + " bounds=om.Bounds(lower=params, upper=params + 1),\n", ")\n", "\n", "fd[\"derivative\"]" ] }, { - "cell_type": "code", - "execution_count": 13, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[2.006, 0. , 0. , 0. , 0. ],\n", - " [0. , 2. , 0. , 0. , 0. ],\n", - " [0. , 0. , 2. , 0. , 0. ],\n", - " [0. , 0. , 0. , 2. , 0. ],\n", - " [0. , 0. , 0. , 0. , 2. ]])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "sd = om.second_derivative(\n", - " func=sphere,\n", - " params=params,\n", - " lower_bounds=params, # forces first_derivative to use forward differences\n", - " upper_bounds=params + 1,\n", - ")\n", - "\n", - "sd[\"derivative\"].round(3)" + "Of course, bounds also work in second_derivative." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Or use parallelized numerical derivatives" + "## You can parallelize" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -467,7 +443,7 @@ "array([0., 2., 4., 6., 8.])" ] }, - "execution_count": 14, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -484,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -497,7 +473,7 @@ " [0. , 0. , 0. , 0. , 2. ]])" ] }, - "execution_count": 15, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -529,7 +505,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.14" }, "vscode": { "interpreter": { diff --git a/docs/source/tutorials/optimization_overview.ipynb b/docs/source/tutorials/optimization_overview.ipynb index 51a4a4b66..07eb25bb3 100644 --- a/docs/source/tutorials/optimization_overview.ipynb +++ b/docs/source/tutorials/optimization_overview.ipynb @@ -231,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -240,18 +240,19 @@ "array([0., 0., 0., 1., 2.])" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "bounds = om.Bounds(lower=np.arange(5) - 2, upper=np.array([10, 10, 10, np.inf, np.inf]))\n", + "\n", "res = om.minimize(\n", " fun=sphere,\n", " params=np.arange(5),\n", " algorithm=\"scipy_lbfgsb\",\n", - " lower_bounds=np.arange(5) - 2,\n", - " upper_bounds=np.array([10, 10, 10, np.inf, np.inf]),\n", + " bounds=bounds,\n", ")\n", "\n", "res.params.round(5)" @@ -266,7 +267,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -275,7 +276,7 @@ "array([0., 1., 0., 3., 0.])" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -302,7 +303,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -311,7 +312,7 @@ "array([ 0.33333, 0.33333, 0.33334, -0. , 0. ])" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -347,7 +348,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -357,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -366,7 +367,7 @@ "array([ 0., -0., -0., 0., 0.])" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -397,7 +398,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -407,7 +408,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -416,7 +417,7 @@ "array([ 0., -0., -0., -0., -0.])" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -440,7 +441,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -449,7 +450,7 @@ "array([ 0., -0., -0., -0., -0.])" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -469,12 +470,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Turn local optimizers global with multistart" + "## Turn local optimizers global with multistart\n", + "\n", + "Multistart optimization requires finite soft bounds on all parameters. Those bounds will\n", + "be used for sampling but not enforced during optimization." ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -483,18 +487,19 @@ "array([ 0., 0., -0., -0., 0., -0., 0., -0., -0., -0.])" ] }, - "execution_count": 17, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "bounds = om.Bounds(soft_lower=np.full(10, -5), soft_upper=np.full(10, 15))\n", + "\n", "res = om.minimize(\n", " fun=sphere,\n", " params=np.arange(10),\n", " algorithm=\"scipy_neldermead\",\n", - " soft_lower_bounds=np.full(10, -5),\n", - " soft_upper_bounds=np.full(10, 15),\n", + " bounds=bounds,\n", " multistart=True,\n", " multistart_options={\"convergence.max_discoveries\": 5},\n", ")\n", @@ -510,7 +515,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -539,7 +544,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -559,7 +564,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -568,7 +573,7 @@ "array([-0., 0., 0., 0., -0.])" ] }, - "execution_count": 20, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -593,7 +598,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ @@ -615,7 +620,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "metadata": {}, "outputs": [ { @@ -624,7 +629,7 @@ "dict_keys(['params', 'criterion', 'runtime'])" ] }, - "execution_count": 22, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -654,7 +659,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -663,7 +668,7 @@ "array([ 0., -0., -0., -0., -0.])" ] }, - "execution_count": 23, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } diff --git a/pyproject.toml b/pyproject.toml index af4f20fef..b3bb726ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,8 @@ extend-ignore = [ "PLR5501", # For calls to warnings.warn(): No explicit `stacklevel` keyword argument found "B028", + # Incompatible with formatting + "ISC001", ] [tool.ruff.lint.per-file-ignores] @@ -246,8 +248,9 @@ module = [ "optimagic.optimization.optimize", "optimagic.optimization.process_multistart_sample", "optimagic.optimization.process_results", - "optimagic.optimization.tiktak", + "optimagic.optimization.multistart", "optimagic.optimization.scipy_aliases", + "optimagic.optimization.create_optimization_problem", "optimagic.optimizers._pounders", "optimagic.optimizers._pounders.pounders_auxiliary", @@ -281,8 +284,6 @@ module = [ "optimagic.parameters.conversion", "optimagic.parameters.kernel_transformations", "optimagic.parameters.nonlinear_constraints", - "optimagic.parameters.parameter_bounds", - "optimagic.parameters.parameter_groups", "optimagic.parameters.process_constraints", "optimagic.parameters.process_selectors", "optimagic.parameters.scale_conversion", diff --git a/src/estimagic/estimate_ml.py b/src/estimagic/estimate_ml.py index 81fde607f..4e292d8f0 100644 --- a/src/estimagic/estimate_ml.py +++ b/src/estimagic/estimate_ml.py @@ -37,6 +37,8 @@ check_optimization_options, ) from optimagic.utilities import get_rng, to_pickle +from optimagic.parameters.bounds import Bounds, pre_process_bounds +from optimagic.deprecations import replace_and_warn_about_deprecated_bounds def estimate_ml( @@ -44,8 +46,7 @@ def estimate_ml( params, optimize_options, *, - lower_bounds=None, - upper_bounds=None, + bounds=None, constraints=None, logging=False, log_options=None, @@ -56,6 +57,9 @@ def estimate_ml( hessian=None, hessian_kwargs=None, design_info=None, + # deprecated + lower_bounds=None, + upper_bounds=None, ): """Do a maximum likelihood (ml) estimation. @@ -85,11 +89,13 @@ def estimate_ml( you signal that ``params`` are already the optimal parameters and no numerical optimization is needed. If you pass a str as optimize_options it is used as the ``algorithm`` option. - lower_bounds (pytree): A pytree with the same structure as params with lower - bounds for the parameters. Can be ``-np.inf`` for parameters with no lower - bound. - upper_bounds (pytree): As lower_bounds. Can be ``np.inf`` for parameters with - no upper bound. + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. constraints (list, dict): List with constraint dictionaries or single dict. See :ref:`constraints`. logging (pathlib.Path, str or False): Path to sqlite3 file (which typically has @@ -132,9 +138,22 @@ def estimate_ml( LikelihoodResult: A LikelihoodResult object. """ + # ================================================================================== + # handle deprecations + # ================================================================================== + + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + ) + # ================================================================================== # Check and process inputs # ================================================================================== + + bounds = pre_process_bounds(bounds) + is_optimized = optimize_options is False if not is_optimized: @@ -169,8 +188,7 @@ def estimate_ml( fun=loglike, fun_kwargs=loglike_kwargs, params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, constraints=constraints, logging=logging, log_options=log_options, @@ -219,8 +237,7 @@ def estimate_ml( converter, internal_estimates = get_converter( params=estimates, constraints=constraints, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=loglike_eval, primary_key="contributions", scaling=False, @@ -247,8 +264,10 @@ def func(x): jac_res = first_derivative( func=func, params=internal_estimates.values, - lower_bounds=internal_estimates.lower_bounds, - upper_bounds=internal_estimates.upper_bounds, + bounds=Bounds( + lower=internal_estimates.lower_bounds, + upper=internal_estimates.upper_bounds, + ), **numdiff_options, ) @@ -290,8 +309,10 @@ def func(x): hess_res = second_derivative( func=func, params=internal_estimates.values, - lower_bounds=internal_estimates.lower_bounds, - upper_bounds=internal_estimates.upper_bounds, + bounds=Bounds( + lower=internal_estimates.lower_bounds, + upper=internal_estimates.upper_bounds, + ), **numdiff_options, ) int_hess = hess_res["derivative"] diff --git a/src/estimagic/estimate_msm.py b/src/estimagic/estimate_msm.py index 562dc6c47..2b85faa30 100644 --- a/src/estimagic/estimate_msm.py +++ b/src/estimagic/estimate_msm.py @@ -46,6 +46,8 @@ check_optimization_options, ) from optimagic.utilities import get_rng, to_pickle +from optimagic.deprecations import replace_and_warn_about_deprecated_bounds +from optimagic.parameters.bounds import Bounds, pre_process_bounds def estimate_msm( @@ -55,8 +57,7 @@ def estimate_msm( params, optimize_options, *, - lower_bounds=None, - upper_bounds=None, + bounds=None, constraints=None, logging=False, log_options=None, @@ -65,6 +66,9 @@ def estimate_msm( numdiff_options=None, jacobian=None, jacobian_kwargs=None, + # deprecated + lower_bounds=None, + upper_bounds=None, ): """Do a method of simulated moments or indirect inference estimation. @@ -101,11 +105,13 @@ def estimate_msm( ``optimize_options`` you signal that ``params`` are already the optimal parameters and no numerical optimization is needed. If you pass a str as optimize_options it is used as the ``algorithm`` option. - lower_bounds (pytree): A pytree with the same structure as params with lower - bounds for the parameters. Can be ``-np.inf`` for parameters with no lower - bound. - upper_bounds (pytree): As lower_bounds. Can be ``np.inf`` for parameters with - no upper bound. + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. simulate_moments_kwargs (dict): Additional keyword arguments for ``simulate_moments``. weights (str): One of "diagonal" (default), "identity" or "optimal". @@ -148,9 +154,20 @@ def estimate_msm( """ # ================================================================================== + # handle deprecations + # ================================================================================== + + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + ) + # ================================================================================== # Check and process inputs # ================================================================================== + bounds = pre_process_bounds(bounds) + if weights not in ["diagonal", "optimal", "identity"]: raise NotImplementedError("Custom weighting matrices are not yet implemented.") @@ -213,8 +230,7 @@ def estimate_msm( ) opt_res = minimize( - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, constraints=constraints, logging=logging, log_options=log_options, @@ -269,8 +285,7 @@ def helper(params): converter, internal_estimates = get_converter( params=estimates, constraints=constraints, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=func_eval, primary_key="contributions", scaling=False, @@ -296,8 +311,10 @@ def func(x): int_jac = first_derivative( func=func, params=internal_estimates.values, - lower_bounds=internal_estimates.lower_bounds, - upper_bounds=internal_estimates.upper_bounds, + bounds=Bounds( + lower=internal_estimates.lower_bounds, + upper=internal_estimates.upper_bounds, + ), **numdiff_options, )["derivative"] diff --git a/src/optimagic/__init__.py b/src/optimagic/__init__.py index 520d872ef..f9498cda9 100644 --- a/src/optimagic/__init__.py +++ b/src/optimagic/__init__.py @@ -14,6 +14,7 @@ from optimagic.visualization.history_plots import criterion_plot, params_plot from optimagic.visualization.profile_plot import profile_plot from optimagic.visualization.slice_plot import slice_plot +from optimagic.parameters.bounds import Bounds try: from ._version import version as __version__ @@ -43,5 +44,6 @@ "check_constraints", "OptimizeLogReader", "OptimizeResult", + "Bounds", "__version__", ] diff --git a/src/optimagic/benchmarking/cartis_roberts.py b/src/optimagic/benchmarking/cartis_roberts.py index 6a827ba05..c809bdf49 100644 --- a/src/optimagic/benchmarking/cartis_roberts.py +++ b/src/optimagic/benchmarking/cartis_roberts.py @@ -19,6 +19,7 @@ import numpy as np from optimagic.config import IS_NUMBA_INSTALLED +from optimagic.parameters.bounds import Bounds if IS_NUMBA_INSTALLED: from numba import njit @@ -4953,7 +4954,7 @@ def get_start_points_methanl8(): "solution_x": None, "start_criterion": 3.0935, "solution_criterion": 0, - "lower_bounds": np.concatenate([np.zeros(50), 1e-6 * np.ones(50)]), + "bounds": Bounds(lower=np.concatenate([np.zeros(50), 1e-6 * np.ones(50)])), }, "chemrctb": { "fun": chemrctb, @@ -4961,7 +4962,7 @@ def get_start_points_methanl8(): "solution_x": solution_x_chemrctb, "start_criterion": 1.446513, "solution_criterion": 1.404424e-3, - "lower_bounds": 1e-6 * np.ones(100), + "bounds": Bounds(lower=1e-6 * np.ones(100)), }, "chnrsbne": { "fun": chnrsbne, @@ -4997,7 +4998,7 @@ def get_start_points_methanl8(): "solution_x": [*np.arange(1, 11).tolist(), 1] + ([0] * 10 + [1]) * 9, "start_criterion": 285, "solution_criterion": 0, - "lower_bounds": np.zeros(110), + "bounds": Bounds(lower=np.zeros(110)), }, "eigenb": { "fun": partial( @@ -5255,8 +5256,10 @@ def get_start_points_methanl8(): "solution_x": solution_x_qr3d, "start_criterion": 1.2, "solution_criterion": 0, - "lower_bounds": [-np.inf] * 25 - + [0 if i == j else -np.inf for i in range(5) for j in range(5)], + "bounds": Bounds( + lower=[-np.inf] * 25 + + [0 if i == j else -np.inf for i in range(5) for j in range(5)] + ), }, "qr3dbd": { "fun": partial(qr3dbd, m=5), @@ -5264,8 +5267,10 @@ def get_start_points_methanl8(): "solution_x": solution_x_qr3dbd, "start_criterion": 1.2, "solution_criterion": 0, - "lower_bounds": [-np.inf] * 25 - + [0 if i == j else -np.inf for i in range(5) for j in range(5)], + "bounds": Bounds( + lower=[-np.inf] * 25 + + [0 if i == j else -np.inf for i in range(5) for j in range(5)] + ), }, "spmsqrt": { "fun": spmsqrt, @@ -5287,8 +5292,7 @@ def get_start_points_methanl8(): "solution_x": solution_x_semicon2, "start_criterion": 2.025037e4, "solution_criterion": 0, - "lower_bounds": -5 * np.ones(100), - "upper_bounds": 0.2 * 700 * np.ones(100), + "bounds": Bounds(lower=-5 * np.ones(100), upper=0.2 * 700 * np.ones(100)), }, "vardimne": { "fun": vardimne, diff --git a/src/optimagic/deprecations.py b/src/optimagic/deprecations.py index 1d5defe8e..43abea5c0 100644 --- a/src/optimagic/deprecations.py +++ b/src/optimagic/deprecations.py @@ -1,4 +1,5 @@ import warnings +from optimagic.parameters.bounds import Bounds def throw_criterion_future_warning(): @@ -95,3 +96,35 @@ def replace_and_warn_about_deprecated_algo_options(algo_options): out[replacements[k]] = algo_options[k] return out + + +def replace_and_warn_about_deprecated_bounds( + lower_bounds, + upper_bounds, + bounds, + soft_lower_bounds=None, + soft_upper_bounds=None, +): + old_bounds = { + "lower": lower_bounds, + "upper": upper_bounds, + "soft_lower": soft_lower_bounds, + "soft_upper": soft_upper_bounds, + } + + old_present = [k for k, v in old_bounds.items() if v is not None] + + if old_present: + substring = ", ".join(f"{b}_bound" for b in old_present) + substring = substring.replace(", ", ", and ", -1) + msg = ( + f"Specifying bounds via the arguments {substring} is " + "deprecated and will be removed in optimagic version 0.6.0 and later. " + "Please use the `bounds` argument instead." + ) + warnings.warn(msg, FutureWarning) + + if bounds is None and old_present: + bounds = Bounds(**old_bounds) + + return bounds diff --git a/src/optimagic/differentiation/derivatives.py b/src/optimagic/differentiation/derivatives.py index 9c7ca22ab..a4dae849b 100644 --- a/src/optimagic/differentiation/derivatives.py +++ b/src/optimagic/differentiation/derivatives.py @@ -15,8 +15,10 @@ from optimagic.differentiation.generate_steps import generate_steps from optimagic.differentiation.richardson_extrapolation import richardson_extrapolation from optimagic.parameters.block_trees import hessian_to_block_tree, matrix_to_block_tree -from optimagic.parameters.parameter_bounds import get_bounds +from optimagic.parameters.bounds import get_internal_bounds from optimagic.parameters.tree_registry import get_registry +from optimagic.deprecations import replace_and_warn_about_deprecated_bounds +from optimagic.parameters.bounds import Bounds, pre_process_bounds class Evals(NamedTuple): @@ -28,13 +30,12 @@ def first_derivative( func, params, *, + bounds=None, func_kwargs=None, method="central", n_steps=1, base_steps=None, scaling_factor=1, - lower_bounds=None, - upper_bounds=None, step_ratio=2, min_steps=None, f0=None, @@ -44,6 +45,9 @@ def first_derivative( return_func_value=False, return_info=False, key=None, + # deprecated + lower_bounds=None, + upper_bounds=None, ): """Evaluate first derivative of func at params according to method and step options. @@ -61,6 +65,13 @@ def first_derivative( Args: func (callable): Function of which the derivative is calculated. params (pytree): A pytree. See :ref:`params`. + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are not used during + numerical differentiation. Each bound type mirrors the structure of params. + Check our how-to guide on bounds for examples. If params is a flat numpy + array, you can also provide bounds via any format that is supported by + scipy.optimize.minimize. func_kwargs (dict): Additional keyword arguments for func, optional. method (str): One of ["central", "forward", "backward"], default "central". n_steps (int): Number of steps needed. For central methods, this is @@ -77,8 +88,6 @@ def first_derivative( scaling_factor is useful if you want to increase or decrease the base_step relative to the rule-of-thumb or user provided base_step, for example to benchmark the effect of the step size. Default 1. - lower_bounds (pytree): To be written. - upper_bounds (pytree): To be written. step_ratio (float, numpy.array): Ratio between two consecutive Richardson extrapolation steps in the same direction. default 2.0. Has to be larger than one. The step ratio is only used if n_steps > 1. @@ -130,10 +139,23 @@ def first_derivative( 1. """ + # ================================================================================== + # handle deprecations + # ================================================================================== + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + ) + + # ================================================================================== + + bounds = pre_process_bounds(bounds) + _is_fast_params = isinstance(params, np.ndarray) and params.ndim == 1 registry = get_registry(extended=True) - lower_bounds, upper_bounds = get_bounds(params, lower_bounds, upper_bounds) + internal_lb, internal_ub = get_internal_bounds(params, bounds=bounds) # handle keyword arguments func_kwargs = {} if func_kwargs is None else func_kwargs @@ -157,8 +179,7 @@ def first_derivative( target="first_derivative", base_steps=base_steps, scaling_factor=scaling_factor, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=Bounds(lower=internal_lb, upper=internal_ub), step_ratio=step_ratio, min_steps=min_steps, ) @@ -283,13 +304,12 @@ def second_derivative( func, params, *, + bounds=None, func_kwargs=None, method="central_cross", n_steps=1, base_steps=None, scaling_factor=1, - lower_bounds=None, - upper_bounds=None, step_ratio=2, min_steps=None, f0=None, @@ -299,6 +319,9 @@ def second_derivative( return_func_value=False, return_info=False, key=None, + # deprecated + lower_bounds=None, + upper_bounds=None, ): """Evaluate second derivative of func at params according to method and step options. @@ -321,6 +344,13 @@ def second_derivative( :class:`pandas.DataFrame` with parameters at which the derivative is calculated. If it is a DataFrame, it can contain the columns "lower_bound" and "upper_bound" for bounds. See :ref:`params`. + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are not used during + numerical differentiation. Each bound type mirrors the structure of params. + Check our how-to guide on bounds for examples. If params is a flat numpy + array, you can also provide bounds via any format that is supported by + scipy.optimize.minimize. func_kwargs (dict): Additional keyword arguments for func, optional. method (str): One of {"forward", "backward", "central_average", "central_cross"} These correspond to the finite difference approximations defined in @@ -341,12 +371,6 @@ def second_derivative( scaling_factor is useful if you want to increase or decrease the base_step relative to the rule-of-thumb or user provided base_step, for example to benchmark the effect of the step size. Default 1. - lower_bounds (numpy.ndarray): 1d array with lower bounds for each parameter. If - params is a DataFrame and has the columns "lower_bound", this will be taken - as lower_bounds if now lower_bounds have been provided explicitly. - upper_bounds (numpy.ndarray): 1d array with upper bounds for each parameter. If - params is a DataFrame and has the columns "upper_bound", this will be taken - as upper_bounds if no upper_bounds have been provided explicitly. step_ratio (float, numpy.array): Ratio between two consecutive Richardson extrapolation steps in the same direction. default 2.0. Has to be larger than one. The step ratio is only used if n_steps > 1. @@ -376,6 +400,7 @@ def second_derivative( key (str): If func returns a dictionary, take the derivative of func(params)[key]. + Returns: result (dict): Result dictionary with keys: - "derivative" (numpy.ndarray, pandas.Series or pandas.DataFrame): The @@ -407,7 +432,20 @@ def second_derivative( returned if return_info is True. """ - lower_bounds, upper_bounds = get_bounds(params, lower_bounds, upper_bounds) + + # ================================================================================== + # handle deprecations + # ================================================================================== + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + ) + # ================================================================================== + + bounds = pre_process_bounds(bounds) + + internal_lb, internal_ub = get_internal_bounds(params, bounds=bounds) # handle keyword arguments func_kwargs = {} if func_kwargs is None else func_kwargs @@ -433,8 +471,7 @@ def second_derivative( target="second_derivative", base_steps=base_steps, scaling_factor=scaling_factor, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=Bounds(lower=internal_lb, upper=internal_ub), step_ratio=step_ratio, min_steps=min_steps, ) diff --git a/src/optimagic/differentiation/generate_steps.py b/src/optimagic/differentiation/generate_steps.py index b1eb8ec83..da5c05632 100644 --- a/src/optimagic/differentiation/generate_steps.py +++ b/src/optimagic/differentiation/generate_steps.py @@ -16,8 +16,7 @@ def generate_steps( target, base_steps, scaling_factor, - lower_bounds, - upper_bounds, + bounds, step_ratio, min_steps, ): @@ -92,11 +91,11 @@ def generate_steps( min_steps = base_steps if min_steps is None else min_steps assert ( - upper_bounds - lower_bounds >= 2 * min_steps + bounds.upper - bounds.lower >= 2 * min_steps ).all(), "min_steps is too large to fit into bounds." - upper_step_bounds = upper_bounds - x - lower_step_bounds = lower_bounds - x + upper_step_bounds = bounds.upper - x + lower_step_bounds = bounds.lower - x pos = step_ratio ** np.arange(n_steps) * base_steps.reshape(-1, 1) neg = -pos.copy() @@ -106,7 +105,7 @@ def generate_steps( x, pos, neg, method, lower_step_bounds, upper_step_bounds ) - if np.isfinite(lower_bounds).any() or np.isfinite(upper_bounds).any(): + if np.isfinite(bounds.lower).any() or np.isfinite(bounds.upper).any(): pos, neg = _rescale_to_accomodate_bounds( base_steps, pos, neg, lower_step_bounds, upper_step_bounds, min_steps ) diff --git a/src/optimagic/optimization/create_optimization_problem.py b/src/optimagic/optimization/create_optimization_problem.py new file mode 100644 index 000000000..6d8461184 --- /dev/null +++ b/src/optimagic/optimization/create_optimization_problem.py @@ -0,0 +1,416 @@ +from dataclasses import dataclass +from typing import Callable, Literal, Any +from optimagic.typing import PyTree +from optimagic.parameters.bounds import Bounds, pre_process_bounds + +from pathlib import Path + +from optimagic.exceptions import ( + MissingInputError, + AliasError, +) +from optimagic.optimization.check_arguments import check_optimize_kwargs +from optimagic.optimization.get_algorithm import ( + process_user_algorithm, +) +from optimagic.shared.process_user_function import ( + process_func_of_params, + get_kwargs_from_args, +) +from optimagic.optimization.scipy_aliases import ( + map_method_to_algorithm, + split_fun_and_jac, +) +from optimagic import deprecations +from optimagic.deprecations import ( + replace_and_warn_about_deprecated_algo_options, + replace_and_warn_about_deprecated_bounds, +) +from optimagic.decorators import AlgoInfo + + +@dataclass(frozen=True) +class OptimizationProblem: + """Collect everything that defines the optimization problem. + + The attributes are very close to the arguments of `maximize` and `minimize` but they + are converted to stricter types. For example, the bounds argument that can be a + sequence of tuples, a scipy.optimize.Bounds object or an optimagic.Bounds when + calling `maximize` or `minimize` is converted to an optimagic.Bounds object. + + All deprecated arguments are removed and all scipy aliases are replaced by their + optimagic counterparts. + + All user provided functions are partialled if corresponding `kwargs` dictionaries + were provided. + + # TODO: Document attributes after other todos are resolved. + + """ + + fun: Callable[[PyTree], float | PyTree] + params: PyTree + # TODO: algorithm will become an Algorithm object; algo_options and algo_info will + # be removed and become part of Algorithm + algorithm: Callable + algo_options: dict[str, Any] | None + algo_info: AlgoInfo + bounds: Bounds + # TODO: constraints will become list[Constraint] | None + constraints: list[dict[str, Any]] + jac: Callable[[PyTree], PyTree] | None + fun_and_jac: Callable[[PyTree], tuple[float, PyTree]] | None + # TODO: numdiff_options will become NumDiffOptions + numdiff_options: dict[str, Any] | None + # TODO: logging will become None | Logger and log_options will be removed + logging: bool | Path | None + log_options: dict[str, Any] | None + # TODO: error_handling will become None | ErrorHandlingOptions and error_penalty + # will be removed + error_handling: Literal["raise", "continue"] + error_penalty: dict[str, Any] | None + # TODO: scaling will become None | ScalingOptions and scaling_options will be + # removed + scaling: bool + scaling_options: dict[str, Any] | None + # TODO: multistart will become None | MultistartOptions and multistart_options will + # be removed + multistart: bool + multistart_options: dict[str, Any] | None + collect_history: bool + skip_checks: bool + direction: Literal["minimize", "maximize"] + + +def create_optimization_problem( + direction, + fun, + params, + algorithm, + *, + bounds, + fun_kwargs, + constraints, + algo_options, + jac, + jac_kwargs, + fun_and_jac, + fun_and_jac_kwargs, + numdiff_options, + logging, + log_options, + error_handling, + error_penalty, + scaling, + scaling_options, + multistart, + multistart_options, + collect_history, + skip_checks, + # scipy aliases + x0, + method, + args, + # scipy arguments that are not yet supported + hess, + hessp, + callback, + # scipy arguments that will never be supported + options, + tol, + # deprecated arguments + criterion, + criterion_kwargs, + derivative, + derivative_kwargs, + criterion_and_derivative, + criterion_and_derivative_kwargs, + lower_bounds, + upper_bounds, + soft_lower_bounds, + soft_upper_bounds, +): + # ================================================================================== + # error handling needed as long as fun is an optional argument (i.e. until + # criterion is fully removed). + # ================================================================================== + + if fun is None and criterion is None: + msg = ( + "Missing objective function. Please provide an objective function as the " + "first positional argument or as the keyword argument `fun`." + ) + raise MissingInputError(msg) + + if params is None and x0 is None: + msg = ( + "Missing start parameters. Please provide start parameters as the second " + "positional argument or as the keyword argument `params`." + ) + raise MissingInputError(msg) + + if algorithm is None and method is None: + msg = ( + "Missing algorithm. Please provide an algorithm as the third positional " + "argument or as the keyword argument `algorithm`." + ) + raise MissingInputError(msg) + + # ================================================================================== + # deprecations + # ================================================================================== + + if criterion is not None: + deprecations.throw_criterion_future_warning() + fun = criterion if fun is None else fun + + if criterion_kwargs is not None: + deprecations.throw_criterion_kwargs_future_warning() + fun_kwargs = criterion_kwargs if fun_kwargs is None else fun_kwargs + + if derivative is not None: + deprecations.throw_derivative_future_warning() + jac = derivative if jac is None else jac + + if derivative_kwargs is not None: + deprecations.throw_derivative_kwargs_future_warning() + jac_kwargs = derivative_kwargs if jac_kwargs is None else jac_kwargs + + if criterion_and_derivative is not None: + deprecations.throw_criterion_and_derivative_future_warning() + fun_and_jac = criterion_and_derivative if fun_and_jac is None else fun_and_jac + + if criterion_and_derivative_kwargs is not None: + deprecations.throw_criterion_and_derivative_kwargs_future_warning() + fun_and_jac_kwargs = ( + criterion_and_derivative_kwargs + if fun_and_jac_kwargs is None + else fun_and_jac_kwargs + ) + + algo_options = replace_and_warn_about_deprecated_algo_options(algo_options) + + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + soft_lower_bounds=soft_lower_bounds, + soft_upper_bounds=soft_upper_bounds, + ) + + # ================================================================================== + # handle scipy aliases + # ================================================================================== + + if x0 is not None: + if params is not None: + msg = ( + "x0 is an alias for params (for better compatibility with scipy). " + "Do not use both x0 and params." + ) + raise AliasError(msg) + else: + params = x0 + + if method is not None: + if algorithm is not None: + msg = ( + "method is an alias for algorithm to select the scipy optimizers under " + "their original name. Do not use both method and algorithm." + ) + raise AliasError(msg) + else: + algorithm = map_method_to_algorithm(method) + + if args is not None: + if ( + fun_kwargs is not None + or jac_kwargs is not None + or fun_and_jac_kwargs is not None + ): + msg = ( + "args is an alternative to fun_kwargs, jac_kwargs and " + "fun_and_jac_kwargs that optimagic supports for compatibility " + "with scipy. Do not use args in conjunction with any of the other " + "arguments." + ) + raise AliasError(msg) + else: + kwargs = get_kwargs_from_args(args, fun, offset=1) + fun_kwargs, jac_kwargs, fun_and_jac_kwargs = kwargs, kwargs, kwargs + + # jac is not an alias but we need to handle the case where `jac=True`, i.e. fun is + # actually fun_and_jac. This is not recommended in optimagic because then optimizers + # cannot evaluate fun in isolation but we can easily support it for compatibility. + if jac is True: + jac = None + if fun_and_jac is None: + fun_and_jac = fun + fun = split_fun_and_jac(fun_and_jac, target="fun") + + bounds = pre_process_bounds(bounds) + + # ================================================================================== + # Handle scipy arguments that are not yet implemented + # ================================================================================== + + if hess is not None: + msg = ( + "The hess argument is not yet supported in optimagic. Creat an issue on " + "https://github.com/OpenSourceEconomics/optimagic/ if you have urgent need " + "for this feature." + ) + raise NotImplementedError(msg) + + if hessp is not None: + msg = ( + "The hessp argument is not yet supported in optimagic. Creat an issue on " + "https://github.com/OpenSourceEconomics/optimagic/ if you have urgent need " + "for this feature." + ) + raise NotImplementedError(msg) + + if callback is not None: + msg = ( + "The callback argument is not yet supported in optimagic. Creat an issue " + "on https://github.com/OpenSourceEconomics/optimagic/ if you have urgent " + "need for this feature." + ) + raise NotImplementedError(msg) + + # ================================================================================== + # Handle scipy arguments that will never be supported + # ================================================================================== + + if options is not None: + # TODO: Add link to a how-to guide or tutorial for this + msg = ( + "The options argument is not supported in optimagic. Please use the " + "algo_options argument instead." + ) + raise NotImplementedError(msg) + + if tol is not None: + # TODO: Add link to a how-to guide or tutorial for this + msg = ( + "The tol argument is not supported in optimagic. Please use " + "algo_options or configured algorithms instead to set convergence criteria " + "for your optimizer." + ) + raise NotImplementedError(msg) + + # ================================================================================== + # Set default values and check options + # ================================================================================== + fun_kwargs = {} if fun_kwargs is None else fun_kwargs + constraints = [] if constraints is None else constraints + algo_options = {} if algo_options is None else algo_options + jac_kwargs = {} if jac_kwargs is None else jac_kwargs + fun_and_jac_kwargs = {} if fun_and_jac_kwargs is None else fun_and_jac_kwargs + numdiff_options = {} if numdiff_options is None else numdiff_options + log_options = {} if log_options is None else log_options + error_penalty = {} if error_penalty is None else error_penalty + scaling_options = {} if scaling_options is None else scaling_options + multistart_options = {} if multistart_options is None else multistart_options + if logging: + logging = Path(logging) + + # ================================================================================== + # Check types of arguments + # ================================================================================== + # TODO: This should probably be inlined + + if not skip_checks: + check_optimize_kwargs( + direction=direction, + criterion=fun, + criterion_kwargs=fun_kwargs, + params=params, + algorithm=algorithm, + constraints=constraints, + algo_options=algo_options, + derivative=jac, + derivative_kwargs=jac_kwargs, + criterion_and_derivative=fun_and_jac, + criterion_and_derivative_kwargs=fun_and_jac_kwargs, + numdiff_options=numdiff_options, + logging=logging, + log_options=log_options, + error_handling=error_handling, + error_penalty=error_penalty, + scaling=scaling, + scaling_options=scaling_options, + multistart=multistart, + multistart_options=multistart_options, + ) + # ================================================================================== + # Get the algorithm info + # ================================================================================== + raw_algo, algo_info = process_user_algorithm(algorithm) + + if algo_info.primary_criterion_entry == "root_contributions": + if direction == "maximize": + msg = ( + "Optimizers that exploit a least squares structure like {} can only be " + "used for minimization." + ) + raise ValueError(msg.format(algo_info.name)) + + # ================================================================================== + # partial the kwargs into corresponding functions + # ================================================================================== + fun = process_func_of_params( + func=fun, + kwargs=fun_kwargs, + name="criterion", + skip_checks=skip_checks, + ) + if isinstance(jac, dict): + jac = jac.get(algo_info.primary_criterion_entry) + if jac is not None: + jac = process_func_of_params( + func=jac, + kwargs=jac_kwargs, + name="derivative", + skip_checks=skip_checks, + ) + if isinstance(fun_and_jac, dict): + fun_and_jac = fun_and_jac.get(algo_info.primary_criterion_entry) + + if fun_and_jac is not None: + fun_and_jac = process_func_of_params( + func=fun_and_jac, + kwargs=fun_and_jac_kwargs, + name="criterion_and_derivative", + skip_checks=skip_checks, + ) + + # ================================================================================== + # create the problem object + # ================================================================================== + + problem = OptimizationProblem( + fun=fun, + params=params, + algorithm=raw_algo, + algo_options=algo_options, + algo_info=algo_info, + bounds=bounds, + constraints=constraints, + jac=jac, + fun_and_jac=fun_and_jac, + numdiff_options=numdiff_options, + logging=logging, + log_options=log_options, + error_handling=error_handling, + error_penalty=error_penalty, + scaling=scaling, + scaling_options=scaling_options, + multistart=multistart, + multistart_options=multistart_options, + collect_history=collect_history, + skip_checks=skip_checks, + direction=direction, + ) + + return problem diff --git a/src/optimagic/optimization/get_algorithm.py b/src/optimagic/optimization/get_algorithm.py index b61e509d0..b94df217d 100644 --- a/src/optimagic/optimization/get_algorithm.py +++ b/src/optimagic/optimization/get_algorithm.py @@ -26,7 +26,6 @@ def process_user_algorithm(algorithm): Returns: callable: the raw internal algorithm AlgoInfo: Attributes of the algorithm - set: The free arguments of the algorithm. """ if isinstance(algorithm, str): diff --git a/src/optimagic/optimization/tiktak.py b/src/optimagic/optimization/multistart.py similarity index 100% rename from src/optimagic/optimization/tiktak.py rename to src/optimagic/optimization/multistart.py diff --git a/src/optimagic/optimization/optimize.py b/src/optimagic/optimization/optimize.py index 9140984f3..6c12b2977 100644 --- a/src/optimagic/optimization/optimize.py +++ b/src/optimagic/optimization/optimize.py @@ -1,3 +1,17 @@ +"""Public functions for optimization. + +This module defines the public functions `maximize` and `minimize` that will be called +by users. + +Internally, `maximize` and `minimize` just call `create_optimization_problem` with +all arguments and add the `direction`. In `create_optimization_problem`, the user input +is consolidated and converted to stricter types. The resulting `OptimizationProblem` +is then passed to `_optimize` which handles the optimization logic. + +`_optimize` processes the optimization problem and performs the actual optimization. + +""" + import functools import warnings from pathlib import Path @@ -6,8 +20,6 @@ from optimagic.exceptions import ( InvalidFunctionError, InvalidKwargsError, - MissingInputError, - AliasError, ) from optimagic.logging.create_tables import ( make_optimization_iteration_table, @@ -16,11 +28,9 @@ ) from optimagic.logging.load_database import load_database from optimagic.logging.write_to_database import append_row -from optimagic.optimization.check_arguments import check_optimize_kwargs from optimagic.optimization.error_penalty import get_error_penalty_function from optimagic.optimization.get_algorithm import ( get_final_algorithm, - process_user_algorithm, ) from optimagic.optimization.internal_criterion_template import ( internal_criterion_and_derivative_template, @@ -28,22 +38,22 @@ from optimagic.optimization.optimization_logging import log_scheduled_steps_and_get_ids from optimagic.optimization.process_multistart_sample import process_multistart_sample from optimagic.optimization.process_results import process_internal_optimizer_result -from optimagic.optimization.tiktak import WEIGHT_FUNCTIONS, run_multistart_optimization +from optimagic.optimization.multistart import ( + WEIGHT_FUNCTIONS, + run_multistart_optimization, +) from optimagic.parameters.conversion import ( aggregate_func_output_to_value, get_converter, ) from optimagic.parameters.nonlinear_constraints import process_nonlinear_constraints -from optimagic.shared.process_user_function import ( - process_func_of_params, - get_kwargs_from_args, -) -from optimagic.optimization.scipy_aliases import ( - map_method_to_algorithm, - split_fun_and_jac, + +from optimagic.parameters.bounds import Bounds +from optimagic.optimization.create_optimization_problem import ( + create_optimization_problem, + OptimizationProblem, ) -from optimagic import deprecations -from optimagic.deprecations import replace_and_warn_about_deprecated_algo_options +from optimagic.optimization.optimize_result import OptimizeResult def maximize( @@ -51,12 +61,9 @@ def maximize( params=None, algorithm=None, *, - lower_bounds=None, - upper_bounds=None, - soft_lower_bounds=None, - soft_upper_bounds=None, - fun_kwargs=None, + bounds=None, constraints=None, + fun_kwargs=None, algo_options=None, jac=None, jac_kwargs=None, @@ -91,17 +98,31 @@ def maximize( derivative_kwargs=None, criterion_and_derivative=None, criterion_and_derivative_kwargs=None, + lower_bounds=None, + upper_bounds=None, + soft_lower_bounds=None, + soft_upper_bounds=None, ): - """Maximize criterion using algorithm subject to constraints.""" - return _optimize( + """Maximize fun using algorithm subject to constraints. + + TODO: Write docstring after enhancement proposals are implemented. + + Args: + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. + + """ + problem = create_optimization_problem( direction="maximize", fun=fun, params=params, + bounds=bounds, algorithm=algorithm, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - soft_lower_bounds=soft_lower_bounds, - soft_upper_bounds=soft_upper_bounds, fun_kwargs=fun_kwargs, constraints=constraints, algo_options=algo_options, @@ -138,19 +159,21 @@ def maximize( derivative_kwargs=derivative_kwargs, criterion_and_derivative=criterion_and_derivative, criterion_and_derivative_kwargs=criterion_and_derivative_kwargs, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + soft_lower_bounds=soft_lower_bounds, + soft_upper_bounds=soft_upper_bounds, ) + return _optimize(problem) + def minimize( fun=None, params=None, algorithm=None, *, - lower_bounds=None, - upper_bounds=None, - soft_lower_bounds=None, - soft_upper_bounds=None, - fun_kwargs=None, + bounds=None, constraints=None, algo_options=None, jac=None, @@ -186,18 +209,33 @@ def minimize( derivative_kwargs=None, criterion_and_derivative=None, criterion_and_derivative_kwargs=None, + lower_bounds=None, + upper_bounds=None, + soft_lower_bounds=None, + soft_upper_bounds=None, + fun_kwargs=None, ): - """Minimize criterion using algorithm subject to constraints.""" + """Minimize criterion using algorithm subject to constraints. + + TODO: Write docstring after enhancement proposals are implemented. - return _optimize( + Args: + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. + + """ + + problem = create_optimization_problem( direction="minimize", fun=fun, params=params, algorithm=algorithm, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - soft_lower_bounds=soft_lower_bounds, - soft_upper_bounds=soft_upper_bounds, + bounds=bounds, fun_kwargs=fun_kwargs, constraints=constraints, algo_options=algo_options, @@ -234,349 +272,40 @@ def minimize( derivative_kwargs=derivative_kwargs, criterion_and_derivative=criterion_and_derivative, criterion_and_derivative_kwargs=criterion_and_derivative_kwargs, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + soft_lower_bounds=soft_lower_bounds, + soft_upper_bounds=soft_upper_bounds, ) + return _optimize(problem) -def _optimize( - direction, - fun, - params, - algorithm, - *, - lower_bounds, - upper_bounds, - soft_lower_bounds, - soft_upper_bounds, - fun_kwargs, - constraints, - algo_options, - jac, - jac_kwargs, - fun_and_jac, - fun_and_jac_kwargs, - numdiff_options, - logging, - log_options, - error_handling, - error_penalty, - scaling, - scaling_options, - multistart, - multistart_options, - collect_history, - skip_checks, - # scipy aliases - x0, - method, - args, - # scipy arguments that are not yet supported - hess, - hessp, - callback, - # scipy arguments that will never be supported - options, - tol, - # deprecated arguments - criterion, - criterion_kwargs, - derivative, - derivative_kwargs, - criterion_and_derivative, - criterion_and_derivative_kwargs, -): - """Minimize or maximize criterion using algorithm subject to constraints. - - Arguments are the same as in maximize and minimize, with an additional direction - argument. Direction is a string that can take the values "maximize" and "minimize". - - Returns are the same as in maximize and minimize. - - """ - # ================================================================================== - # error handling needed as long as fun is an optional argument (i.e. until - # criterion is fully removed). - # ================================================================================== - - if fun is None and criterion is None: - msg = ( - "Missing objective function. Please provide an objective function as the " - "first positional argument or as the keyword argument `fun`." - ) - raise MissingInputError(msg) - - if params is None and x0 is None: - msg = ( - "Missing start parameters. Please provide start parameters as the second " - "positional argument or as the keyword argument `params`." - ) - raise MissingInputError(msg) - - if algorithm is None and method is None: - msg = ( - "Missing algorithm. Please provide an algorithm as the third positional " - "argument or as the keyword argument `algorithm`." - ) - raise MissingInputError(msg) - - # ================================================================================== - # deprecations - # ================================================================================== - - if criterion is not None: - deprecations.throw_criterion_future_warning() - fun = criterion if fun is None else fun - - if criterion_kwargs is not None: - deprecations.throw_criterion_kwargs_future_warning() - fun_kwargs = criterion_kwargs if fun_kwargs is None else fun_kwargs - - if derivative is not None: - deprecations.throw_derivative_future_warning() - jac = derivative if jac is None else jac - - if derivative_kwargs is not None: - deprecations.throw_derivative_kwargs_future_warning() - jac_kwargs = derivative_kwargs if jac_kwargs is None else jac_kwargs - - if criterion_and_derivative is not None: - deprecations.throw_criterion_and_derivative_future_warning() - fun_and_jac = criterion_and_derivative if fun_and_jac is None else fun_and_jac - - if criterion_and_derivative_kwargs is not None: - deprecations.throw_criterion_and_derivative_kwargs_future_warning() - fun_and_jac_kwargs = ( - criterion_and_derivative_kwargs - if fun_and_jac_kwargs is None - else fun_and_jac_kwargs - ) - - algo_options = replace_and_warn_about_deprecated_algo_options(algo_options) - - # ================================================================================== - # handle scipy aliases - # ================================================================================== - - if x0 is not None: - if params is not None: - msg = ( - "x0 is an alias for params (for better compatibility with scipy). " - "Do not use both x0 and params." - ) - raise AliasError(msg) - else: - params = x0 - - if method is not None: - if algorithm is not None: - msg = ( - "method is an alias for algorithm to select the scipy optimizers under " - "their original name. Do not use both method and algorithm." - ) - raise AliasError(msg) - else: - algorithm = map_method_to_algorithm(method) - - if args is not None: - if ( - fun_kwargs is not None - or jac_kwargs is not None - or fun_and_jac_kwargs is not None - ): - msg = ( - "args is an alternative to fun_kwargs, jac_kwargs and " - "fun_and_jac_kwargs that optimagic supports for compatibility " - "with scipy. Do not use args in conjunction with any of the other " - "arguments." - ) - raise AliasError(msg) - else: - kwargs = get_kwargs_from_args(args, fun, offset=1) - fun_kwargs, jac_kwargs, fun_and_jac_kwargs = kwargs, kwargs, kwargs - - # jac is not an alias but we need to handle the case where `jac=True`, i.e. fun is - # actually fun_and_jac. This is not recommended in optimagic because then optimizers - # cannot evaluate fun in isolation but we can easily support it for compatibility. - if jac is True: - jac = None - if fun_and_jac is None: - fun_and_jac = fun - fun = split_fun_and_jac(fun_and_jac, target="fun") - - # ================================================================================== - # Handle scipy arguments that are not yet implemented - # ================================================================================== - - if hess is not None: - msg = ( - "The hess argument is not yet supported in optimagic. Creat an issue on " - "https://github.com/OpenSourceEconomics/optimagic/ if you have urgent need " - "for this feature." - ) - raise NotImplementedError(msg) - - if hessp is not None: - msg = ( - "The hessp argument is not yet supported in optimagic. Creat an issue on " - "https://github.com/OpenSourceEconomics/optimagic/ if you have urgent need " - "for this feature." - ) - raise NotImplementedError(msg) - - if callback is not None: - msg = ( - "The callback argument is not yet supported in optimagic. Creat an issue " - "on https://github.com/OpenSourceEconomics/optimagic/ if you have urgent " - "need for this feature." - ) - raise NotImplementedError(msg) - - # ================================================================================== - # Handle scipy arguments that will never be supported - # ================================================================================== - - if options is not None: - # TODO: Add link to a how-to guide or tutorial for this - msg = ( - "The options argument is not supported in optimagic. Please use the " - "algo_options argument instead." - ) - raise NotImplementedError(msg) - - if tol is not None: - # TODO: Add link to a how-to guide or tutorial for this - msg = ( - "The tol argument is not supported in optimagic. Please use " - "algo_options or configured algorithms instead to set convergence criteria " - "for your optimizer." - ) - raise NotImplementedError(msg) - - # ================================================================================== - # Set default values and check options - # ================================================================================== - fun_kwargs = _setdefault(fun_kwargs, {}) - constraints = _setdefault(constraints, []) - algo_options = _setdefault(algo_options, {}) - jac_kwargs = _setdefault(jac_kwargs, {}) - fun_and_jac_kwargs = _setdefault(fun_and_jac_kwargs, {}) - numdiff_options = _setdefault(numdiff_options, {}) - log_options = _setdefault(log_options, {}) - scaling_options = _setdefault(scaling_options, {}) - error_penalty = _setdefault(error_penalty, {}) - multistart_options = _setdefault(multistart_options, {}) - if logging: - logging = Path(logging) - - if not skip_checks: - check_optimize_kwargs( - direction=direction, - criterion=fun, - criterion_kwargs=fun_kwargs, - params=params, - algorithm=algorithm, - constraints=constraints, - algo_options=algo_options, - derivative=jac, - derivative_kwargs=jac_kwargs, - criterion_and_derivative=fun_and_jac, - criterion_and_derivative_kwargs=fun_and_jac_kwargs, - numdiff_options=numdiff_options, - logging=logging, - log_options=log_options, - error_handling=error_handling, - error_penalty=error_penalty, - scaling=scaling, - scaling_options=scaling_options, - multistart=multistart, - multistart_options=multistart_options, - ) - # ================================================================================== - # Get the algorithm info - # ================================================================================== - raw_algo, algo_info = process_user_algorithm(algorithm) - - algo_kwargs = set(algo_info.arguments) - - if algo_info.primary_criterion_entry == "root_contributions": - if direction == "maximize": - msg = ( - "Optimizers that exploit a least squares structure like {} can only be " - "used for minimization." - ) - raise ValueError(msg.format(algo_info.name)) - +def _optimize(problem: OptimizationProblem) -> OptimizeResult: + """Solve an optimization problem.""" # ================================================================================== # Split constraints into nonlinear and reparametrization parts # ================================================================================== + constraints = problem.constraints if isinstance(constraints, dict): constraints = [constraints] nonlinear_constraints = [c for c in constraints if c["type"] == "nonlinear"] + algo_kwargs = set(problem.algo_info.arguments) if nonlinear_constraints and "nonlinear_constraints" not in algo_kwargs: raise ValueError( - f"Algorithm {algo_info.name} does not support nonlinear constraints." + f"Algorithm {problem.algo_info.name} does not support nonlinear " + "constraints." ) # the following constraints will be handled via reparametrization constraints = [c for c in constraints if c["type"] != "nonlinear"] - # ================================================================================== - # prepare logging - # ================================================================================== - if logging: - problem_data = { - "direction": direction, - # "criterion"-criterion, - "criterion_kwargs": fun_kwargs, - "algorithm": algorithm, - "constraints": constraints, - "algo_options": algo_options, - # "derivative"-derivative, - "derivative_kwargs": jac_kwargs, - # "criterion_and_derivative"-criterion_and_derivative, - "criterion_and_derivative_kwargs": fun_and_jac_kwargs, - "numdiff_options": numdiff_options, - "log_options": log_options, - "error_handling": error_handling, - "error_penalty": error_penalty, - "params": params, - } - - # ================================================================================== - # partial the kwargs into corresponding functions - # ================================================================================== - fun = process_func_of_params( - func=fun, - kwargs=fun_kwargs, - name="criterion", - skip_checks=skip_checks, - ) - if isinstance(jac, dict): - jac = jac.get(algo_info.primary_criterion_entry) - if jac is not None: - jac = process_func_of_params( - func=jac, - kwargs=jac_kwargs, - name="derivative", - skip_checks=skip_checks, - ) - if isinstance(fun_and_jac, dict): - fun_and_jac = fun_and_jac.get(algo_info.primary_criterion_entry) - - if fun_and_jac is not None: - fun_and_jac = process_func_of_params( - func=fun_and_jac, - kwargs=fun_and_jac_kwargs, - name="criterion_and_derivative", - skip_checks=skip_checks, - ) - # ================================================================================== # Do first evaluation of user provided functions # ================================================================================== try: - first_crit_eval = fun(params) + first_crit_eval = problem.fun(problem.params) except (KeyboardInterrupt, SystemExit): raise except Exception as e: @@ -584,27 +313,27 @@ def _optimize( raise InvalidFunctionError(msg) from e # do first derivative evaluation (if given) - if jac is not None: + if problem.jac is not None: try: - first_deriv_eval = jac(params) + first_deriv_eval = problem.jac(problem.params) except (KeyboardInterrupt, SystemExit): raise except Exception as e: msg = "Error while evaluating derivative at start params." raise InvalidFunctionError(msg) from e - if fun_and_jac is not None: + if problem.fun_and_jac is not None: try: - first_crit_and_deriv_eval = fun_and_jac(params) + first_crit_and_deriv_eval = problem.fun_and_jac(problem.params) except (KeyboardInterrupt, SystemExit): raise except Exception as e: msg = "Error while evaluating criterion_and_derivative at start params." raise InvalidFunctionError(msg) from e - if jac is not None: + if problem.jac is not None: used_deriv = first_deriv_eval - elif fun_and_jac is not None: + elif problem.fun_and_jac is not None: used_deriv = first_crit_and_deriv_eval[1] else: used_deriv = None @@ -613,26 +342,33 @@ def _optimize( # Get the converter (for tree flattening, constraints and scaling) # ================================================================================== converter, internal_params = get_converter( - params=params, + params=problem.params, constraints=constraints, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=problem.bounds, func_eval=first_crit_eval, - primary_key=algo_info.primary_criterion_entry, - scaling=scaling, - scaling_options=scaling_options, + primary_key=problem.algo_info.primary_criterion_entry, + scaling=problem.scaling, + scaling_options=problem.scaling_options, derivative_eval=used_deriv, - soft_lower_bounds=soft_lower_bounds, - soft_upper_bounds=soft_upper_bounds, - add_soft_bounds=multistart, + add_soft_bounds=problem.multistart, ) # ================================================================================== # initialize the log database # ================================================================================== - if logging: - problem_data["free_mask"] = internal_params.free_mask - database = _create_and_initialize_database(logging, log_options, problem_data) + if problem.logging: + # TODO: We want to remove the optimization_problem table completely but we + # probably do need to store the start parameters in the database because it is + # used by the log reader. + problem_data = { + "direction": problem.direction, + "params": problem.params, + } + database = _create_and_initialize_database( + logging=problem.logging, + log_options=problem.log_options, + problem_data=problem_data, + ) else: database = None @@ -640,35 +376,35 @@ def _optimize( # Do some things that require internal parameters or bounds # ================================================================================== - if converter.has_transforming_constraints and multistart: + if converter.has_transforming_constraints and problem.multistart: raise NotImplementedError( "multistart optimizations are not yet compatible with transforming " "constraints." ) numdiff_options = _fill_numdiff_options_with_defaults( - numdiff_options=numdiff_options, + numdiff_options=problem.numdiff_options, lower_bounds=internal_params.lower_bounds, upper_bounds=internal_params.upper_bounds, ) # get error penalty function error_penalty_func = get_error_penalty_function( - error_handling=error_handling, + error_handling=problem.error_handling, start_x=internal_params.values, start_criterion=converter.func_to_internal(first_crit_eval), - error_penalty=error_penalty, - primary_key=algo_info.primary_criterion_entry, - direction=direction, + error_penalty=problem.error_penalty, + primary_key=problem.algo_info.primary_criterion_entry, + direction=problem.direction, ) # process nonlinear constraints: internal_constraints = process_nonlinear_constraints( nonlinear_constraints=nonlinear_constraints, - params=params, + params=problem.params, converter=converter, numdiff_options=numdiff_options, - skip_checks=skip_checks, + skip_checks=problem.skip_checks, ) x = internal_params.values @@ -676,31 +412,31 @@ def _optimize( # get the internal algorithm # ================================================================================== internal_algorithm = get_final_algorithm( - raw_algorithm=raw_algo, - algo_info=algo_info, + raw_algorithm=problem.algorithm, + algo_info=problem.algo_info, valid_kwargs=algo_kwargs, lower_bounds=internal_params.lower_bounds, upper_bounds=internal_params.upper_bounds, nonlinear_constraints=internal_constraints, - algo_options=algo_options, - logging=logging, + algo_options=problem.algo_options, + logging=problem.logging, database=database, - collect_history=collect_history, + collect_history=problem.collect_history, ) # ================================================================================== # partial arguments into the internal_criterion_and_derivative_template # ================================================================================== to_partial = { - "direction": direction, - "criterion": fun, + "direction": problem.direction, + "criterion": problem.fun, "converter": converter, - "derivative": jac, - "criterion_and_derivative": fun_and_jac, + "derivative": problem.jac, + "criterion_and_derivative": problem.fun_and_jac, "numdiff_options": numdiff_options, - "logging": logging, + "logging": problem.logging, "database": database, - "algo_info": algo_info, - "error_handling": error_handling, + "algo_info": problem.algo_info, + "error_handling": problem.error_handling, "error_penalty_func": error_penalty_func, } @@ -720,35 +456,35 @@ def _optimize( # ================================================================================== # Do actual optimization # ================================================================================== - if not multistart: + if not problem.multistart: steps = [{"type": "optimization", "name": "optimization"}] step_ids = log_scheduled_steps_and_get_ids( steps=steps, - logging=logging, + logging=problem.logging, database=database, ) raw_res = internal_algorithm(**problem_functions, x=x, step_id=step_ids[0]) else: multistart_options = _fill_multistart_options_with_defaults( - options=multistart_options, - params=params, + options=problem.multistart_options, + params=problem.params, x=x, params_to_internal=converter.params_to_internal, ) raw_res = run_multistart_optimization( local_algorithm=internal_algorithm, - primary_key=algo_info.primary_criterion_entry, + primary_key=problem.algo_info.primary_criterion_entry, problem_functions=problem_functions, x=x, lower_sampling_bounds=internal_params.soft_lower_bounds, upper_sampling_bounds=internal_params.soft_upper_bounds, options=multistart_options, - logging=logging, + logging=problem.logging, database=database, - error_handling=error_handling, + error_handling=problem.error_handling, ) # ================================================================================== @@ -757,23 +493,23 @@ def _optimize( _scalar_start_criterion = aggregate_func_output_to_value( converter.func_to_internal(first_crit_eval), - algo_info.primary_criterion_entry, + problem.algo_info.primary_criterion_entry, ) fixed_result_kwargs = { "start_fun": _scalar_start_criterion, - "start_params": params, - "algorithm": algo_info.name, - "direction": direction, + "start_params": problem.params, + "algorithm": problem.algo_info.name, + "direction": problem.direction, "n_free": internal_params.free_mask.sum(), } res = process_internal_optimizer_result( raw_res, converter=converter, - primary_key=algo_info.primary_criterion_entry, + primary_key=problem.algo_info.primary_criterion_entry, fixed_kwargs=fixed_result_kwargs, - skip_checks=skip_checks, + skip_checks=problem.skip_checks, ) return res @@ -863,8 +599,7 @@ def _fill_numdiff_options_with_defaults(numdiff_options, lower_bounds, upper_bou # only define the ones that deviate from the normal defaults default_numdiff_options = { "method": "forward", - "lower_bounds": lower_bounds, - "upper_bounds": upper_bounds, + "bounds": Bounds(lower=lower_bounds, upper=upper_bounds), "error_handling": default_error_handling, "return_info": False, } diff --git a/src/optimagic/parameters/bounds.py b/src/optimagic/parameters/bounds.py new file mode 100644 index 000000000..eb045a63a --- /dev/null +++ b/src/optimagic/parameters/bounds.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import numpy as np +from pybaum import leaf_names, tree_map +from pybaum import tree_just_flatten as tree_leaves + +from optimagic.exceptions import InvalidBoundsError +from optimagic.parameters.tree_registry import get_registry +from dataclasses import dataclass +from optimagic.typing import PyTree, PyTreeRegistry +from scipy.optimize import Bounds as ScipyBounds +from typing import Sequence +from numpy.typing import NDArray +from typing import Any + + +@dataclass(frozen=True) +class Bounds: + lower: PyTree | None = None + upper: PyTree | None = None + soft_lower: PyTree | None = None + soft_upper: PyTree | None = None + + +def pre_process_bounds( + bounds: None | Bounds | ScipyBounds | Sequence[tuple[float, float]], +) -> Bounds | None: + """Convert all valid types of specifying bounds to optimagic.Bounds. + + This just harmonizes multiple ways of specifying bounds into a single format. + It does not check that bounds are valid or compatible with params. + + Args: + bounds: The user provided bounds. + + Returns: + The bounds in the optimagic format. + + Raises: + InvalidBoundsError: If bounds cannot be processed, e.g. because they do not have + the correct type. + + """ + if isinstance(bounds, ScipyBounds): + bounds = Bounds(lower=bounds.lb, upper=bounds.ub) + elif isinstance(bounds, Bounds) or bounds is None: + pass + else: + try: + bounds = _process_bounds_sequence(bounds) + except (KeyboardInterrupt, SystemExit): + raise + except Exception as e: + raise InvalidBoundsError( + f"Invalid bounds of type: {type(bounds)}. Bounds must be " + "optimagic.Bounds, scipy.optimize.Bounds or a Sequence of tuples with " + "lower and upper bounds." + ) from e + return bounds + + +def _process_bounds_sequence(bounds: Sequence[tuple[float, float]]) -> Bounds: + lower = np.full(len(bounds), -np.inf) + upper = np.full(len(bounds), np.inf) + + for i, (lb, ub) in enumerate(bounds): + if lb is not None: + lower[i] = lb + if ub is not None: + upper[i] = ub + return Bounds(lower=lower, upper=upper) + + +def get_internal_bounds( + params: PyTree, + bounds: Bounds | None = None, + registry: PyTreeRegistry | None = None, + add_soft_bounds: bool = False, +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Create consolidated and flattened bounds for params. + + If params is a DataFrame with value column, the user provided bounds are + extended with bounds from the params DataFrame. + + If no bounds are available the entry is set to minus np.inf for the lower bound and + np.inf for the upper bound. + + The bounds provided in `bounds` override bounds provided in params if both are + specified (in the case where params is a DataFrame with bounds as a column). + + Args: + params: The parameter pytree. + bounds: The lower and upper bounds. + registry: pybaum registry. + add_soft_bounds: If True, the element-wise maximum (minimum) of the lower and + soft_lower (upper and soft_upper) bounds are taken. If False, the lower + (upper) bounds are returned. + + Returns: + Consolidated and flattened lower_bounds. + Consolidated and flattened upper_bounds. + + """ + bounds = Bounds() if bounds is None else bounds + + fast_path = _is_fast_path( + params=params, + bounds=bounds, + add_soft_bounds=add_soft_bounds, + ) + if fast_path: + return _get_fast_path_bounds( + params=params, + bounds=bounds, + ) + + registry = get_registry(extended=True) if registry is None else registry + n_params = len(tree_leaves(params, registry=registry)) + + # Fill leaves with np.nan. If params contains a data frame with bounds as a column, + # that column is NOT overwritten (as long as an extended registry is used). + nan_tree = tree_map(lambda leaf: np.nan, params, registry=registry) # noqa: ARG005 + + lower_flat = _update_bounds_and_flatten(nan_tree, bounds.lower, kind="lower_bound") + upper_flat = _update_bounds_and_flatten(nan_tree, bounds.upper, kind="upper_bound") + + if len(lower_flat) != n_params: + raise InvalidBoundsError("lower_bounds do not match dimension of params.") + if len(upper_flat) != n_params: + raise InvalidBoundsError("upper_bounds do not match dimension of params.") + + lower_flat[np.isnan(lower_flat)] = -np.inf + upper_flat[np.isnan(upper_flat)] = np.inf + + if add_soft_bounds: + lower_flat_soft = _update_bounds_and_flatten( + nan_tree, bounds.soft_lower, kind="soft_lower_bound" + ) + lower_flat_soft[np.isnan(lower_flat_soft)] = -np.inf + lower_flat = np.maximum(lower_flat, lower_flat_soft) + + upper_flat_soft = _update_bounds_and_flatten( + nan_tree, bounds.soft_upper, kind="soft_upper_bound" + ) + upper_flat_soft[np.isnan(upper_flat_soft)] = np.inf + upper_flat = np.minimum(upper_flat, upper_flat_soft) + + if (lower_flat > upper_flat).any(): + msg = "Invalid bounds. Some lower bounds are larger than upper bounds." + raise InvalidBoundsError(msg) + + return lower_flat, upper_flat + + +def _update_bounds_and_flatten( + nan_tree: PyTree, bounds: PyTree, kind: str +) -> NDArray[np.float64]: + """Flatten bounds array and update it with bounds from params. + + Args: + nan_tree: Pytree with the same structure as params, filled with nans. + bounds: The candidate bounds to be updated and flattened. + kind: One of "lower_bound", "upper_bound", "soft_lower_bound", + "soft_upper_bound". + + Returns: + np.ndarray: The updated and flattened bounds. + + """ + registry = get_registry(extended=True, data_col=kind) + flat_nan_tree = tree_leaves(nan_tree, registry=registry) + + if bounds is not None: + registry = get_registry(extended=True) + flat_bounds = tree_leaves(bounds, registry=registry) + + seperator = 10 * "$" + params_names = leaf_names(nan_tree, registry=registry, separator=seperator) + bounds_names = leaf_names(bounds, registry=registry, separator=seperator) + + flat_nan_dict = dict(zip(params_names, flat_nan_tree, strict=False)) + + invalid = {"names": [], "bounds": []} # type: ignore + for bounds_name, bounds_leaf in zip(bounds_names, flat_bounds, strict=False): + # if a bounds leaf is None we treat it as saying the the corresponding + # subtree of params has no bounds. + if bounds_leaf is not None: + if bounds_name in flat_nan_dict: + flat_nan_dict[bounds_name] = bounds_leaf + else: + invalid["names"].append(bounds_name) + invalid["bounds"].append(bounds_leaf) + + if invalid["bounds"]: + msg = ( + f"{kind} could not be matched to params pytree. The bounds " + f"{invalid['bounds']} with names {invalid['names']} are not part of " + "params." + ) + raise InvalidBoundsError(msg) + + flat_nan_tree = list(flat_nan_dict.values()) + + updated = np.array(flat_nan_tree, dtype=np.float64) + return updated + + +def _is_fast_path(params: PyTree, bounds: Bounds, add_soft_bounds: bool) -> bool: + out = True + if add_soft_bounds: + out = False + + if not _is_1d_array(params): + out = False + + for bound in bounds.lower, bounds.upper: + if not (_is_1d_array(bound) or bound is None): + out = False + return out + + +def _is_1d_array(candidate: Any) -> bool: + return isinstance(candidate, np.ndarray) and candidate.ndim == 1 + + +def _get_fast_path_bounds( + params: PyTree, bounds: Bounds +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + if bounds.lower is None: + # faster than np.full + lower_bounds = np.array([-np.inf] * len(params)) + else: + lower_bounds = bounds.lower.astype(float) + + if bounds.upper is None: + # faster than np.full + upper_bounds = np.array([np.inf] * len(params)) + else: + upper_bounds = bounds.upper.astype(float) + + if (lower_bounds > upper_bounds).any(): + msg = "Invalid bounds. Some lower bounds are larger than upper bounds." + raise InvalidBoundsError(msg) + + return lower_bounds, upper_bounds diff --git a/src/optimagic/parameters/constraint_tools.py b/src/optimagic/parameters/constraint_tools.py index 5c790a210..d2e32f7c8 100644 --- a/src/optimagic/parameters/constraint_tools.py +++ b/src/optimagic/parameters/constraint_tools.py @@ -1,25 +1,46 @@ from optimagic.parameters.conversion import get_converter +from optimagic.deprecations import replace_and_warn_about_deprecated_bounds +from optimagic.parameters.bounds import pre_process_bounds -def count_free_params(params, constraints=None, lower_bounds=None, upper_bounds=None): +def count_free_params( + params, + constraints=None, + bounds=None, + # deprecated + lower_bounds=None, + upper_bounds=None, +): """Count the (free) parameters of an optimization problem. Args: params (pytree): The parameters. constraints (list): The constraints for the optimization problem. If constraints are provided, only the free parameters are counted. - lower_bounds (pytree): Lower bounds for params. - upper_bounds (pytree): Upper bounds for params. + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. Returns: int: Number of (free) parameters """ + bounds = replace_and_warn_about_deprecated_bounds( + bounds=bounds, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + ) + + bounds = pre_process_bounds(bounds) + _, internal_params = get_converter( params=params, constraints=constraints, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=3, primary_key="value", scaling=False, @@ -29,26 +50,44 @@ def count_free_params(params, constraints=None, lower_bounds=None, upper_bounds= return int(internal_params.free_mask.sum()) -def check_constraints(params, constraints, lower_bounds=None, upper_bounds=None): +def check_constraints( + params, + constraints, + bounds=None, + # deprecated + lower_bounds=None, + upper_bounds=None, +): """Raise an error if constraints are invalid or not satisfied in params. Args: params (pytree): The parameters. constraints (list): The constraints for the optimization problem. - lower_bounds (pytree): Lower bounds for params. - upper_bounds (pytree): Upper bounds for params. - + bounds: Lower and upper bounds on the parameters. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are used for + sampling based optimizers but are not enforced during optimization. Each + bound type mirrors the structure of params. Check our how-to guide on bounds + for examples. If params is a flat numpy array, you can also provide bounds + via any format that is supported by scipy.optimize.minimize. Raises: InvalidParamsError: If constraints are valid but not satisfied. InvalidConstraintError: If constraints are invalid. """ + bounds = replace_and_warn_about_deprecated_bounds( + bounds=bounds, + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + ) + + bounds = pre_process_bounds(bounds) + get_converter( params=params, constraints=constraints, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=3, primary_key="value", scaling=False, diff --git a/src/optimagic/parameters/conversion.py b/src/optimagic/parameters/conversion.py index 9076b2bff..f9af25aa1 100644 --- a/src/optimagic/parameters/conversion.py +++ b/src/optimagic/parameters/conversion.py @@ -13,15 +13,12 @@ def get_converter( params, constraints, - lower_bounds, - upper_bounds, + bounds, func_eval, primary_key, scaling, scaling_options, derivative_eval=None, - soft_lower_bounds=None, - soft_upper_bounds=None, add_soft_bounds=False, ): """Get a converter between external and internal params and internal params. @@ -74,20 +71,16 @@ def get_converter( if fast_path: return _get_fast_path_converter( params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, primary_key=primary_key, ) tree_converter, internal_params = get_tree_converter( params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=func_eval, derivative_eval=derivative_eval, primary_key=primary_key, - soft_lower_bounds=soft_lower_bounds, - soft_upper_bounds=soft_upper_bounds, add_soft_bounds=add_soft_bounds, ) @@ -216,7 +209,7 @@ def _fast_params_from_internal(x, return_type="tree"): return x -def _get_fast_path_converter(params, lower_bounds, upper_bounds, primary_key): +def _get_fast_path_converter(params, bounds, primary_key): def _fast_derivative_to_internal( derivative_eval, x, # noqa: ARG001 @@ -233,15 +226,15 @@ def _fast_derivative_to_internal( has_transforming_constraints=False, ) - if lower_bounds is None: + if bounds is None or bounds.lower is None: lower_bounds = np.full(len(params), -np.inf) else: - lower_bounds = lower_bounds.astype(float) + lower_bounds = bounds.lower.astype(float) - if upper_bounds is None: + if bounds is None or bounds.upper is None: upper_bounds = np.full(len(params), np.inf) else: - upper_bounds = upper_bounds.astype(float) + upper_bounds = bounds.upper.astype(float) internal_params = InternalParams( values=params.astype(float), diff --git a/src/optimagic/parameters/nonlinear_constraints.py b/src/optimagic/parameters/nonlinear_constraints.py index 29cf25f8b..e51b2234a 100644 --- a/src/optimagic/parameters/nonlinear_constraints.py +++ b/src/optimagic/parameters/nonlinear_constraints.py @@ -89,8 +89,7 @@ def _process_nonlinear_constraint( # process numdiff_options for numerical derivative options = numdiff_options.copy() - options.pop("lower_bounds", None) - options.pop("upper_bounds", None) + options.pop("bounds", None) if "derivative" in c: if not callable(c["derivative"]): diff --git a/src/optimagic/parameters/parameter_bounds.py b/src/optimagic/parameters/parameter_bounds.py deleted file mode 100644 index f9286928a..000000000 --- a/src/optimagic/parameters/parameter_bounds.py +++ /dev/null @@ -1,165 +0,0 @@ -import numpy as np -from pybaum import leaf_names, tree_map -from pybaum import tree_just_flatten as tree_leaves - -from optimagic.exceptions import InvalidBoundsError -from optimagic.parameters.tree_registry import get_registry - - -def get_bounds( - params, - lower_bounds=None, - upper_bounds=None, - soft_lower_bounds=None, - soft_upper_bounds=None, - registry=None, - add_soft_bounds=False, -): - """Consolidate lower/upper bounds with bounds available in params. - - Updates bounds defined in params. If no bounds are available the entry is set to - -np.inf for the lower bound and np.inf for the upper bound. If a bound is defined in - params and lower_bounds or upper_bounds, the bound from lower_bounds or upper_bounds - will be used. - - Args: - params (pytree): The parameter pytree. - lower_bounds (pytree): Must be a subtree of params. - upper_bounds (pytree): Must be a subtree of params. - registry (dict): pybaum registry. - - Returns: - np.ndarray: Consolidated and flattened lower_bounds. - np.ndarray: Consolidated and flattened upper_bounds. - - """ - fast_path = _is_fast_path( - params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - add_soft_bounds=add_soft_bounds, - ) - if fast_path: - return _get_fast_path_bounds( - params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - ) - - registry = get_registry(extended=True) if registry is None else registry - n_params = len(tree_leaves(params, registry=registry)) - - # Fill leaves with np.nan. If params contains a data frame with bounds as a column, - # that column is NOT overwritten (as long as an extended registry is used). - nan_tree = tree_map(lambda leaf: np.nan, params, registry=registry) # noqa: ARG005 - - lower_flat = _update_bounds_and_flatten( - nan_tree, lower_bounds, direction="lower_bound" - ) - upper_flat = _update_bounds_and_flatten( - nan_tree, upper_bounds, direction="upper_bound" - ) - - if len(lower_flat) != n_params: - raise InvalidBoundsError("lower_bounds do not match dimension of params.") - if len(upper_flat) != n_params: - raise InvalidBoundsError("upper_bounds do not match dimension of params.") - - lower_flat[np.isnan(lower_flat)] = -np.inf - upper_flat[np.isnan(upper_flat)] = np.inf - - if add_soft_bounds: - lower_flat_soft = _update_bounds_and_flatten( - nan_tree, soft_lower_bounds, direction="soft_lower_bound" - ) - lower_flat_soft[np.isnan(lower_flat_soft)] = -np.inf - lower_flat = np.maximum(lower_flat, lower_flat_soft) - - upper_flat_soft = _update_bounds_and_flatten( - nan_tree, soft_upper_bounds, direction="soft_upper_bound" - ) - upper_flat_soft[np.isnan(upper_flat_soft)] = np.inf - upper_flat = np.minimum(upper_flat, upper_flat_soft) - - if (lower_flat > upper_flat).any(): - msg = "Invalid bounds. Some lower bounds are larger than upper bounds." - raise InvalidBoundsError(msg) - - return lower_flat, upper_flat - - -def _update_bounds_and_flatten(nan_tree, bounds, direction): - registry = get_registry(extended=True, data_col=direction) - flat_nan_tree = tree_leaves(nan_tree, registry=registry) - - if bounds is not None: - registry = get_registry(extended=True) - flat_bounds = tree_leaves(bounds, registry=registry) - - seperator = 10 * "$" - params_names = leaf_names(nan_tree, registry=registry, separator=seperator) - bounds_names = leaf_names(bounds, registry=registry, separator=seperator) - - flat_nan_dict = dict(zip(params_names, flat_nan_tree, strict=False)) - - invalid = {"names": [], "bounds": []} - for bounds_name, bounds_leaf in zip(bounds_names, flat_bounds, strict=False): - # if a bounds leaf is None we treat it as saying the the corresponding - # subtree of params has no bounds. - if bounds_leaf is not None: - if bounds_name in flat_nan_dict: - flat_nan_dict[bounds_name] = bounds_leaf - else: - invalid["names"].append(bounds_name) - invalid["bounds"].append(bounds_leaf) - - if invalid["bounds"]: - msg = ( - f"{direction} could not be matched to params pytree. The bounds " - f"{invalid['bounds']} with names {invalid['names']} are not part of " - "params." - ) - raise InvalidBoundsError(msg) - - flat_nan_tree = list(flat_nan_dict.values()) - - updated = np.array(flat_nan_tree, dtype=np.float64) - return updated - - -def _is_fast_path(params, lower_bounds, upper_bounds, add_soft_bounds): - out = True - if add_soft_bounds: - out = False - - if not _is_1d_array(params): - out = False - - for bound in lower_bounds, upper_bounds: - if not (_is_1d_array(bound) or bound is None): - out = False - return out - - -def _is_1d_array(candidate): - return isinstance(candidate, np.ndarray) and candidate.ndim == 1 - - -def _get_fast_path_bounds(params, lower_bounds, upper_bounds): - if lower_bounds is None: - # faster than np.full - lower_bounds = np.array([-np.inf] * len(params)) - else: - lower_bounds = lower_bounds.astype(float) - - if upper_bounds is None: - # faster than np.full - upper_bounds = np.array([np.inf] * len(params)) - else: - upper_bounds = upper_bounds.astype(float) - - if (lower_bounds > upper_bounds).any(): - msg = "Invalid bounds. Some lower bounds are larger than upper bounds." - raise InvalidBoundsError(msg) - - return lower_bounds, upper_bounds diff --git a/src/optimagic/parameters/tree_conversion.py b/src/optimagic/parameters/tree_conversion.py index 8640cb9ec..6c651b747 100644 --- a/src/optimagic/parameters/tree_conversion.py +++ b/src/optimagic/parameters/tree_conversion.py @@ -5,20 +5,17 @@ from optimagic.exceptions import InvalidFunctionError from optimagic.parameters.block_trees import block_tree_to_matrix -from optimagic.parameters.parameter_bounds import get_bounds +from optimagic.parameters.bounds import get_internal_bounds from optimagic.parameters.tree_registry import get_registry from optimagic.utilities import isscalar def get_tree_converter( params, - lower_bounds, - upper_bounds, + bounds, func_eval, primary_key, derivative_eval=None, - soft_lower_bounds=None, - soft_upper_bounds=None, add_soft_bounds=False, ): """Get flatten and unflatten functions for criterion and its derivative. @@ -55,21 +52,17 @@ def get_tree_converter( _registry = get_registry(extended=True) _params_vec, _params_treedef = tree_flatten(params, registry=_registry) _params_vec = np.array(_params_vec).astype(float) - _lower, _upper = get_bounds( + _lower, _upper = get_internal_bounds( params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, registry=_registry, ) if add_soft_bounds: - _soft_lower, _soft_upper = get_bounds( + _soft_lower, _soft_upper = get_internal_bounds( params=params, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, registry=_registry, - soft_lower_bounds=soft_lower_bounds, - soft_upper_bounds=soft_upper_bounds, add_soft_bounds=add_soft_bounds, ) else: diff --git a/src/optimagic/typing.py b/src/optimagic/typing.py index f3fb9a5c1..9b71a71ec 100644 --- a/src/optimagic/typing.py +++ b/src/optimagic/typing.py @@ -1,4 +1,5 @@ -from typing import Any +from typing import Any, Callable PyTree = Any +PyTreeRegistry = dict[type | str, dict[str, Callable[[Any], Any]]] diff --git a/src/optimagic/visualization/slice_plot.py b/src/optimagic/visualization/slice_plot.py index de8c4dbe1..144b958dc 100644 --- a/src/optimagic/visualization/slice_plot.py +++ b/src/optimagic/visualization/slice_plot.py @@ -11,13 +11,14 @@ from optimagic.parameters.conversion import get_converter from optimagic.parameters.tree_registry import get_registry from optimagic.visualization.plotting_utilities import combine_plots, get_layout_kwargs +from optimagic.deprecations import replace_and_warn_about_deprecated_bounds +from optimagic.parameters.bounds import pre_process_bounds def slice_plot( func, params, - lower_bounds=None, - upper_bounds=None, + bounds=None, func_kwargs=None, selector=None, n_cores=DEFAULT_N_CORES, @@ -33,22 +34,29 @@ def slice_plot( return_dict=False, make_subplot_kwargs=None, batch_evaluator="joblib", + # deprecated + lower_bounds=None, + upper_bounds=None, ): """Plot criterion along coordinates at given and random values. Generates plots for each parameter and optionally combines them into a figure with subplots. + # TODO: Use soft bounds to create the grid (if available). + Args: criterion (callable): criterion function that takes params and returns a scalar value or dictionary with the entry "value". params (pytree): A pytree with parameters. - lower_bounds (pytree): A pytree with same structure as params. Must be - specified and finite for all parameters unless params is a DataFrame - containing with "lower_bound" column. - upper_bounds (pytree): A pytree with same structure as params. Must be - specified and finite for all parameters unless params is a DataFrame - containing with "lower_bound" column. + bounds: Lower and upper bounds on the parameters. The bounds are used to create + a grid over which slice plots are drawn. The most general and preferred + way to specify bounds is an `optimagic.Bounds` object that collects lower, + upper, soft_lower and soft_upper bounds. The soft bounds are not used for + slice_plots. Each bound type mirrors the structure of params. Check our + how-to guide on bounds for examples. If params is a flat numpy array, you + can also provide bounds via any format that is supported by + scipy.optimize.minimize. selector (callable): Function that takes params and returns a subset of params for which we actually want to generate the plot. n_cores (int): Number of cores. @@ -84,6 +92,13 @@ def slice_plot( plots for each parameter or a plotly Figure combining the individual plots. """ + bounds = replace_and_warn_about_deprecated_bounds( + lower_bounds=lower_bounds, + upper_bounds=upper_bounds, + bounds=bounds, + ) + + bounds = pre_process_bounds(bounds) layout_kwargs = None if title is not None: @@ -99,8 +114,7 @@ def slice_plot( converter, internal_params = get_converter( params=params, constraints=None, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=func_eval, primary_key="value", scaling=False, diff --git a/tests/estimagic/test_estimate_ml.py b/tests/estimagic/test_estimate_ml.py index 2604b4de6..2fcec4d52 100644 --- a/tests/estimagic/test_estimate_ml.py +++ b/tests/estimagic/test_estimate_ml.py @@ -11,6 +11,7 @@ from numpy.testing import assert_array_equal from scipy.stats import multivariate_normal from statsmodels.base.model import GenericLikelihoodModel +from optimagic.parameters.bounds import Bounds def aaae(obj1, obj2, decimal=3): @@ -388,7 +389,7 @@ def test_estimate_ml_general_pytree(normal_inputs): params=start_params, loglike_kwargs=kwargs, optimize_options="scipy_lbfgsb", - lower_bounds={"sd": 0.0001}, + bounds=Bounds(lower={"sd": 0.0001}), jacobian_kwargs=kwargs, constraints=[{"selector": lambda p: p["sd"], "type": "sdcorr"}], ) @@ -415,7 +416,7 @@ def test_to_pickle(normal_inputs, tmp_path): params=start_params, loglike_kwargs=kwargs, optimize_options="scipy_lbfgsb", - lower_bounds={"sd": 0.0001}, + bounds=Bounds(lower={"sd": 0.0001}), jacobian_kwargs=kwargs, constraints=[{"selector": lambda p: p["sd"], "type": "sdcorr"}], ) @@ -433,7 +434,7 @@ def test_caching(normal_inputs): params=start_params, loglike_kwargs=kwargs, optimize_options="scipy_lbfgsb", - lower_bounds={"sd": 0.0001}, + bounds=Bounds(lower={"sd": 0.0001}), jacobian_kwargs=kwargs, constraints=[{"selector": lambda p: p["sd"], "type": "sdcorr"}], ) diff --git a/tests/optimagic/differentiation/test_derivatives.py b/tests/optimagic/differentiation/test_derivatives.py index 6272e752b..2eac8e8e5 100644 --- a/tests/optimagic/differentiation/test_derivatives.py +++ b/tests/optimagic/differentiation/test_derivatives.py @@ -29,6 +29,7 @@ from numpy.testing import assert_array_almost_equal as aaae from pandas.testing import assert_frame_equal from scipy.optimize._numdiff import approx_derivative +from optimagic.parameters.bounds import Bounds @pytest.fixture() @@ -47,14 +48,18 @@ def test_first_derivative_jacobian(binary_choice_inputs, method): fix = binary_choice_inputs func = partial(logit_loglikeobs, y=fix["y"], x=fix["x"]) + bounds = Bounds( + lower=np.full(fix["params_np"].shape, -np.inf), + upper=np.full(fix["params_np"].shape, np.inf), + ) + calculated = first_derivative( func=func, method=method, params=fix["params_np"], n_steps=1, base_steps=None, - lower_bounds=np.full(fix["params_np"].shape, -np.inf), - upper_bounds=np.full(fix["params_np"].shape, np.inf), + bounds=bounds, min_steps=1e-8, step_ratio=2.0, f0=func(fix["params_np"]), diff --git a/tests/optimagic/differentiation/test_generate_steps.py b/tests/optimagic/differentiation/test_generate_steps.py index ecd3862ee..164f5ca95 100644 --- a/tests/optimagic/differentiation/test_generate_steps.py +++ b/tests/optimagic/differentiation/test_generate_steps.py @@ -8,6 +8,7 @@ generate_steps, ) from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds def test_scalars_as_base_steps(): @@ -181,8 +182,7 @@ def test_generate_steps_binding_min_step(): n_steps=2, target="first_derivative", base_steps=np.array([0.1, 0.2, 0.3]), - lower_bounds=np.full(3, -np.inf), - upper_bounds=np.full(3, 2.5), + bounds=Bounds(lower=np.full(3, -np.inf), upper=np.full(3, 2.5)), step_ratio=2.0, min_steps=np.full(3, 1e-8), scaling_factor=1.0, @@ -202,8 +202,7 @@ def test_generate_steps_min_step_equals_base_step(): n_steps=2, target="first_derivative", base_steps=np.array([0.1, 0.2, 0.3]), - lower_bounds=np.full(3, -np.inf), - upper_bounds=np.full(3, 2.5), + bounds=Bounds(lower=np.full(3, -np.inf), upper=np.full(3, 2.5)), step_ratio=2.0, min_steps=None, scaling_factor=1.0, diff --git a/tests/optimagic/optimization/test_history_collection.py b/tests/optimagic/optimization/test_history_collection.py index e3d2409a6..d25463bee 100644 --- a/tests/optimagic/optimization/test_history_collection.py +++ b/tests/optimagic/optimization/test_history_collection.py @@ -8,6 +8,7 @@ from numpy.testing import assert_array_almost_equal as aaae from numpy.testing import assert_array_equal as aae from optimagic.decorators import mark_minimizer +from optimagic.parameters.bounds import Bounds OPTIMIZERS = [] BOUNDED = [] @@ -32,8 +33,7 @@ def test_history_collection_with_parallelization(algorithm, tmp_path): fun=lambda x: {"root_contributions": x, "value": x @ x}, params=np.arange(5), algorithm=algorithm, - lower_bounds=lb, - upper_bounds=ub, + bounds=Bounds(lower=lb, upper=ub), algo_options={"n_cores": 2, "stopping.max_iterations": 3}, logging=logging, log_options={"if_database_exists": "replace", "fast_logging": True}, diff --git a/tests/optimagic/optimization/test_internal_criterion_and_derivative_template.py b/tests/optimagic/optimization/test_internal_criterion_and_derivative_template.py index c529ce150..2314d30d7 100644 --- a/tests/optimagic/optimization/test_internal_criterion_and_derivative_template.py +++ b/tests/optimagic/optimization/test_internal_criterion_and_derivative_template.py @@ -69,8 +69,7 @@ def test_criterion_and_derivative_template( converter, _ = get_converter( params=base_inputs["params"], constraints=None, - lower_bounds=None, - upper_bounds=None, + bounds=None, func_eval=crit(base_inputs["params"]), primary_key="value", scaling=False, @@ -118,8 +117,7 @@ def test_internal_criterion_with_penalty(base_inputs, direction): converter, _ = get_converter( params=base_inputs["params"], constraints=None, - lower_bounds=None, - upper_bounds=None, + bounds=None, func_eval=sos_scalar_criterion(base_inputs["params"]), primary_key="value", scaling=False, diff --git a/tests/optimagic/optimization/test_many_algorithms.py b/tests/optimagic/optimization/test_many_algorithms.py index 429354d06..4185890d8 100644 --- a/tests/optimagic/optimization/test_many_algorithms.py +++ b/tests/optimagic/optimization/test_many_algorithms.py @@ -13,6 +13,7 @@ from optimagic.algorithms import AVAILABLE_ALGORITHMS, GLOBAL_ALGORITHMS from optimagic.optimization.optimize import minimize from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds LOCAL_ALGORITHMS = { key: value @@ -54,8 +55,9 @@ def test_algorithm_on_sum_of_squares_with_binding_bounds(algorithm): res = minimize( fun=sos, params=np.array([3, 2, -3]), - lower_bounds=np.array([1, -np.inf, -np.inf]), - upper_bounds=np.array([np.inf, np.inf, -1]), + bounds=Bounds( + lower=np.array([1, -np.inf, -np.inf]), upper=np.array([np.inf, np.inf, -1]) + ), algorithm=algorithm, collect_history=True, skip_checks=True, @@ -77,8 +79,7 @@ def test_global_algorithms_on_sum_of_squares(algorithm): res = minimize( fun=sos, params=np.array([0.35, 0.35]), - lower_bounds=np.array([0.2, -0.5]), - upper_bounds=np.array([1, 0.5]), + bounds=Bounds(lower=np.array([0.2, -0.5]), upper=np.array([1, 0.5])), algorithm=algorithm, collect_history=False, skip_checks=True, diff --git a/tests/optimagic/optimization/test_multistart.py b/tests/optimagic/optimization/test_multistart.py index fbcd5c92d..886f0c0e5 100644 --- a/tests/optimagic/optimization/test_multistart.py +++ b/tests/optimagic/optimization/test_multistart.py @@ -3,253 +3,183 @@ import numpy as np import pandas as pd import pytest -from optimagic.decorators import switch_sign -from optimagic.examples.criterion_functions import ( - sos_dict_criterion, - sos_scalar_criterion, +from optimagic.optimization.multistart import ( + _linear_weights, + _tiktak_weights, + draw_exploration_sample, + get_batched_optimization_sample, + run_explorations, + update_convergence_state, ) -from optimagic.logging.load_database import load_database -from optimagic.logging.read_from_database import read_new_rows -from optimagic.logging.read_log import read_steps_table -from optimagic.optimization.optimize import maximize, minimize -from optimagic.optimization.optimize_result import OptimizeResult from numpy.testing import assert_array_almost_equal as aaae -criteria = [sos_scalar_criterion, sos_dict_criterion] - @pytest.fixture() def params(): - params = pd.DataFrame() - params["value"] = np.arange(4) - params["soft_lower_bound"] = [-5] * 4 - params["soft_upper_bound"] = [10] * 4 - return params - - -test_cases = product(criteria, ["maximize", "minimize"]) - - -@pytest.mark.parametrize("criterion, direction", test_cases) -def test_multistart_minimize_with_sum_of_squares_at_defaults( - criterion, direction, params -): - if direction == "minimize": - res = minimize( - fun=criterion, - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - ) - else: - res = maximize( - fun=switch_sign(sos_dict_criterion), - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - ) + df = pd.DataFrame(index=["a", "b", "c"]) + df["value"] = [0, 1, 2.0] + df["soft_lower_bound"] = [-1, 0, np.nan] + df["upper_bound"] = [2, 2, np.nan] + return df - assert hasattr(res, "multistart_info") - ms_info = res.multistart_info - assert len(ms_info["exploration_sample"]) == 40 - assert len(ms_info["exploration_results"]) == 40 - assert all(isinstance(entry, float) for entry in ms_info["exploration_results"]) - assert all(isinstance(entry, OptimizeResult) for entry in ms_info["local_optima"]) - assert all(isinstance(entry, pd.DataFrame) for entry in ms_info["start_parameters"]) - assert np.allclose(res.fun, 0) - aaae(res.params["value"], np.zeros(4)) - - -def test_multistart_with_existing_sample(params): - sample = pd.DataFrame( - np.arange(20).reshape(5, 4) / 10, - columns=params.index, - ) - options = {"sample": sample} - - res = minimize( - fun=sos_dict_criterion, - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - multistart_options=options, - ) - - calc_sample = _params_list_to_aray(res.multistart_info["exploration_sample"]) - aaae(calc_sample, options["sample"]) - - -def test_convergence_via_max_discoveries_works(params): - options = { - "convergence_relative_params_tolerance": np.inf, - "convergence_max_discoveries": 2, - } - - res = maximize( - fun=switch_sign(sos_dict_criterion), - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - multistart_options=options, - ) - - assert len(res.multistart_info["local_optima"]) == 2 +@pytest.fixture() +def constraints(): + return [{"type": "fixed", "loc": "c", "value": 2}] + + +dim = 2 +distributions = ["uniform", "triangular"] +rules = ["sobol", "halton", "latin_hypercube", "random"] +lower = [np.zeros(dim), np.ones(dim) * 0.5, -np.ones(dim)] +upper = [np.ones(dim), np.ones(dim) * 0.75, np.ones(dim) * 2] +test_cases = list(product(distributions, rules, lower, upper)) + + +@pytest.mark.parametrize("dist, rule, lower, upper", test_cases) +def test_draw_exploration_sample(dist, rule, lower, upper): + results = [] + + for _ in range(2): + results.append( + draw_exploration_sample( + x=np.ones_like(lower) * 0.5, + lower=lower, + upper=upper, + n_samples=3, + sampling_distribution=dist, + sampling_method=rule, + seed=1234, + ) + ) -def test_steps_are_logged_as_skipped_if_convergence(params): - options = { - "convergence_relative_params_tolerance": np.inf, - "convergence_max_discoveries": 2, - } + aaae(results[0], results[1]) + calculated = results[0] + assert calculated.shape == (3, 2) + + +def test_run_explorations(): + def _dummy(x, **kwargs): + assert set(kwargs) == { + "task", + "algo_info", + "error_handling", + "fixed_log_data", + } + if x.sum() == 5: + out = np.nan + else: + out = -x.sum() + return out - minimize( - fun=sos_dict_criterion, - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - multistart_options=options, - logging="logging.db", + calculated = run_explorations( + func=_dummy, + primary_key="value", + sample=np.arange(6).reshape(3, 2), + batch_evaluator="joblib", + n_cores=1, + step_id=0, + error_handling="raise", ) - steps_table = read_steps_table("logging.db") - expected_status = ["complete", "complete", "complete", "skipped", "skipped"] - assert steps_table["status"].tolist() == expected_status - + exp_values = np.array([-9, -1]) + exp_sample = np.array([[4, 5], [0, 1]]) -def test_all_steps_occur_in_optimization_iterations_if_no_convergence(params): - options = {"convergence_max_discoveries": np.inf} + aaae(calculated["sorted_values"], exp_values) + aaae(calculated["sorted_sample"], exp_sample) - minimize( - fun=sos_dict_criterion, - params=params, - algorithm="scipy_lbfgsb", - multistart=True, - multistart_options=options, - logging="logging.db", - ) - database = load_database(path_or_database="logging.db") - iterations, _ = read_new_rows( - database=database, - table_name="optimization_iterations", - last_retrieved=0, - return_type="dict_of_lists", +def test_get_batched_optimization_sample(): + calculated = get_batched_optimization_sample( + sorted_sample=np.arange(12).reshape(6, 2), + n_optimizations=5, + batch_size=4, ) + expected = [[[0, 1], [2, 3], [4, 5], [6, 7]], [[8, 9]]] - present_steps = set(iterations["step"]) + assert len(calculated[0]) == 4 + assert len(calculated[1]) == 1 + assert len(calculated) == 2 - assert present_steps == {1, 2, 3, 4, 5} + for calc_batch, exp_batch in zip(calculated, expected, strict=False): + assert isinstance(calc_batch, list) + for calc_entry, exp_entry in zip(calc_batch, exp_batch, strict=False): + assert isinstance(calc_entry, np.ndarray) + assert calc_entry.tolist() == exp_entry -def test_with_non_transforming_constraints(params): - res = minimize( - fun=sos_dict_criterion, - params=params, - constraints=[{"loc": [0, 1], "type": "fixed", "value": [0, 1]}], - algorithm="scipy_lbfgsb", - multistart=True, - ) +def test_linear_weights(): + calculated = _linear_weights(5, 10, 0.4, 0.8) + expected = 0.6 + assert np.allclose(calculated, expected) - aaae(res.params["value"].to_numpy(), np.array([0, 1, 0, 0])) +def test_tiktak_weights(): + assert np.allclose(0.3, _tiktak_weights(0, 10, 0.3, 0.8)) + assert np.allclose(0.8, _tiktak_weights(10, 10, 0.3, 0.8)) -def test_error_is_raised_with_transforming_constraints(params): - with pytest.raises(NotImplementedError): - minimize( - fun=sos_dict_criterion, - params=params, - constraints=[{"loc": [0, 1], "type": "probability"}], - algorithm="scipy_lbfgsb", - multistart=True, - ) +@pytest.fixture() +def current_state(): + state = { + "best_x": np.ones(3), + "best_y": 5, + "best_res": None, + "x_history": [np.arange(3) - 1e-20, np.ones(3)], + "y_history": [6, 5], + "result_history": [], + "start_history": [], + } -def _params_list_to_aray(params_list): - data = [params["value"].tolist() for params in params_list] - return np.array(data) - + return state -def test_multistart_with_numpy_params(): - res = minimize( - fun=lambda params: params @ params, - params=np.arange(5), - algorithm="scipy_lbfgsb", - soft_lower_bounds=np.full(5, -10), - soft_upper_bounds=np.full(5, 10), - multistart=True, - ) - aaae(res.params, np.zeros(5)) +@pytest.fixture() +def starts(): + return [np.zeros(3)] -def test_with_invalid_bounds(): - with pytest.raises(ValueError): - minimize( - fun=lambda x: x @ x, - params=np.arange(5), - algorithm="scipy_neldermead", - multistart=True, - ) +@pytest.fixture() +def results(): + return [{"solution_x": np.arange(3) + 1e-10, "solution_criterion": 4}] -def test_with_scaling(): - def _crit(params): - x = params - np.arange(len(params)) - return x @ x +def test_update_state_converged(current_state, starts, results): + criteria = { + "xtol": 1e-3, + "max_discoveries": 2, + } - res = minimize( - fun=_crit, - params=np.full(5, 10), - soft_lower_bounds=np.full(5, -1), - soft_upper_bounds=np.full(5, 11), - algorithm="scipy_lbfgsb", - multistart=True, + new_state, is_converged = update_convergence_state( + current_state=current_state, + starts=starts, + results=results, + convergence_criteria=criteria, + primary_key="value", ) - aaae(res.params, np.arange(5)) + aaae(new_state["best_x"], np.arange(3)) + assert new_state["best_y"] == 4 + assert new_state["y_history"] == [6, 5, 4] + assert new_state["result_history"][0]["solution_criterion"] == 4 + aaae(new_state["start_history"][0], np.zeros(3)) + assert new_state["best_res"].keys() == results[0].keys() + assert is_converged -def test_with_ackley(): - def ackley(x): - out = ( - -20 * np.exp(-0.2 * np.sqrt(np.mean(x**2))) - - np.exp(np.mean(np.cos(2 * np.pi * x))) - + 20 - + np.exp(1) - ) - return out - - dim = 5 - kwargs = { - "fun": ackley, - "params": np.full(dim, -10), - "lower_bounds": np.full(dim, -32), - "upper_bounds": np.full(dim, 32), - "algo_options": {"stopping.maxfun": 1000}, +def test_update_state_not_converged(current_state, starts, results): + criteria = { + "xtol": 1e-3, + "max_discoveries": 5, } - minimize( - **kwargs, - algorithm="scipy_lbfgsb", - multistart=True, - multistart_options={ - "n_samples": 200, - "share_optimizations": 0.1, - "convergence_max_discoveries": 10, - }, - ) - - -def test_multistart_with_least_squares_optimizers(): - est = minimize( - fun=sos_dict_criterion, - params=np.array([-1, 1.0]), - lower_bounds=np.full(2, -10.0), - upper_bounds=np.full(2, 10.0), - algorithm="scipy_ls_trf", - multistart=True, - multistart_options={"n_samples": 3, "share_optimizations": 1.0}, + _, is_converged = update_convergence_state( + current_state=current_state, + starts=starts, + results=results, + convergence_criteria=criteria, + primary_key="value", ) - aaae(est.params, np.zeros(2)) + assert not is_converged diff --git a/tests/optimagic/optimization/test_tiktak.py b/tests/optimagic/optimization/test_tiktak.py deleted file mode 100644 index 122e513c3..000000000 --- a/tests/optimagic/optimization/test_tiktak.py +++ /dev/null @@ -1,185 +0,0 @@ -from itertools import product - -import numpy as np -import pandas as pd -import pytest -from optimagic.optimization.tiktak import ( - _linear_weights, - _tiktak_weights, - draw_exploration_sample, - get_batched_optimization_sample, - run_explorations, - update_convergence_state, -) -from numpy.testing import assert_array_almost_equal as aaae - - -@pytest.fixture() -def params(): - df = pd.DataFrame(index=["a", "b", "c"]) - df["value"] = [0, 1, 2.0] - df["soft_lower_bound"] = [-1, 0, np.nan] - df["upper_bound"] = [2, 2, np.nan] - return df - - -@pytest.fixture() -def constraints(): - return [{"type": "fixed", "loc": "c", "value": 2}] - - -dim = 2 -distributions = ["uniform", "triangular"] -rules = ["sobol", "halton", "latin_hypercube", "random"] -lower = [np.zeros(dim), np.ones(dim) * 0.5, -np.ones(dim)] -upper = [np.ones(dim), np.ones(dim) * 0.75, np.ones(dim) * 2] -test_cases = list(product(distributions, rules, lower, upper)) - - -@pytest.mark.parametrize("dist, rule, lower, upper", test_cases) -def test_draw_exploration_sample(dist, rule, lower, upper): - results = [] - - for _ in range(2): - results.append( - draw_exploration_sample( - x=np.ones_like(lower) * 0.5, - lower=lower, - upper=upper, - n_samples=3, - sampling_distribution=dist, - sampling_method=rule, - seed=1234, - ) - ) - - aaae(results[0], results[1]) - calculated = results[0] - assert calculated.shape == (3, 2) - - -def test_run_explorations(): - def _dummy(x, **kwargs): - assert set(kwargs) == { - "task", - "algo_info", - "error_handling", - "fixed_log_data", - } - if x.sum() == 5: - out = np.nan - else: - out = -x.sum() - return out - - calculated = run_explorations( - func=_dummy, - primary_key="value", - sample=np.arange(6).reshape(3, 2), - batch_evaluator="joblib", - n_cores=1, - step_id=0, - error_handling="raise", - ) - - exp_values = np.array([-9, -1]) - exp_sample = np.array([[4, 5], [0, 1]]) - - aaae(calculated["sorted_values"], exp_values) - aaae(calculated["sorted_sample"], exp_sample) - - -def test_get_batched_optimization_sample(): - calculated = get_batched_optimization_sample( - sorted_sample=np.arange(12).reshape(6, 2), - n_optimizations=5, - batch_size=4, - ) - expected = [[[0, 1], [2, 3], [4, 5], [6, 7]], [[8, 9]]] - - assert len(calculated[0]) == 4 - assert len(calculated[1]) == 1 - assert len(calculated) == 2 - - for calc_batch, exp_batch in zip(calculated, expected, strict=False): - assert isinstance(calc_batch, list) - for calc_entry, exp_entry in zip(calc_batch, exp_batch, strict=False): - assert isinstance(calc_entry, np.ndarray) - assert calc_entry.tolist() == exp_entry - - -def test_linear_weights(): - calculated = _linear_weights(5, 10, 0.4, 0.8) - expected = 0.6 - assert np.allclose(calculated, expected) - - -def test_tiktak_weights(): - assert np.allclose(0.3, _tiktak_weights(0, 10, 0.3, 0.8)) - assert np.allclose(0.8, _tiktak_weights(10, 10, 0.3, 0.8)) - - -@pytest.fixture() -def current_state(): - state = { - "best_x": np.ones(3), - "best_y": 5, - "best_res": None, - "x_history": [np.arange(3) - 1e-20, np.ones(3)], - "y_history": [6, 5], - "result_history": [], - "start_history": [], - } - - return state - - -@pytest.fixture() -def starts(): - return [np.zeros(3)] - - -@pytest.fixture() -def results(): - return [{"solution_x": np.arange(3) + 1e-10, "solution_criterion": 4}] - - -def test_update_state_converged(current_state, starts, results): - criteria = { - "xtol": 1e-3, - "max_discoveries": 2, - } - - new_state, is_converged = update_convergence_state( - current_state=current_state, - starts=starts, - results=results, - convergence_criteria=criteria, - primary_key="value", - ) - - aaae(new_state["best_x"], np.arange(3)) - assert new_state["best_y"] == 4 - assert new_state["y_history"] == [6, 5, 4] - assert new_state["result_history"][0]["solution_criterion"] == 4 - aaae(new_state["start_history"][0], np.zeros(3)) - assert new_state["best_res"].keys() == results[0].keys() - - assert is_converged - - -def test_update_state_not_converged(current_state, starts, results): - criteria = { - "xtol": 1e-3, - "max_discoveries": 5, - } - - _, is_converged = update_convergence_state( - current_state=current_state, - starts=starts, - results=results, - convergence_criteria=criteria, - primary_key="value", - ) - - assert not is_converged diff --git a/tests/optimagic/optimization/test_with_bounds.py b/tests/optimagic/optimization/test_with_bounds.py new file mode 100644 index 000000000..8bfea6808 --- /dev/null +++ b/tests/optimagic/optimization/test_with_bounds.py @@ -0,0 +1,39 @@ +from optimagic.optimization.optimize import minimize, maximize +from scipy.optimize import Bounds as ScipyBounds +import numpy as np + + +def test_minimize_with_scipy_bounds(): + minimize( + lambda x: x @ x, + np.arange(3), + bounds=ScipyBounds(np.full(3, -1), np.full(3, 5)), + algorithm="scipy_lbfgsb", + ) + + +def test_minimize_with_sequence_bounds(): + minimize( + lambda x: x @ x, + np.arange(3), + bounds=[(-1, 5)] * 3, + algorithm="scipy_lbfgsb", + ) + + +def test_maximize_with_scipy_bounds(): + maximize( + lambda x: -x @ x, + np.arange(3), + bounds=ScipyBounds(np.full(3, -1), np.full(3, 5)), + algorithm="scipy_lbfgsb", + ) + + +def test_maximize_with_sequence_bounds(): + maximize( + lambda x: -x @ x, + np.arange(3), + bounds=[(-1, 5)] * 3, + algorithm="scipy_lbfgsb", + ) diff --git a/tests/optimagic/optimization/test_with_constraints.py b/tests/optimagic/optimization/test_with_constraints.py index 733b29d14..cc1fe0253 100644 --- a/tests/optimagic/optimization/test_with_constraints.py +++ b/tests/optimagic/optimization/test_with_constraints.py @@ -28,6 +28,7 @@ from optimagic.exceptions import InvalidConstraintError, InvalidParamsError from optimagic.optimization.optimize import maximize, minimize from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds def logit_loglike(params, y, x): @@ -268,7 +269,7 @@ def return_all_but_working_hours(params): "type": "increasing", }, ], - lower_bounds={"work": {"hours": 0}}, + bounds=Bounds(lower={"work": {"hours": 0}}), ) assert np.allclose(res.params["work"]["hours"], start_params["time_budget"]) diff --git a/tests/optimagic/optimization/test_with_multistart.py b/tests/optimagic/optimization/test_with_multistart.py new file mode 100644 index 000000000..45692a1e9 --- /dev/null +++ b/tests/optimagic/optimization/test_with_multistart.py @@ -0,0 +1,252 @@ +from itertools import product + +import numpy as np +import pandas as pd +import pytest +from optimagic.decorators import switch_sign +from optimagic.examples.criterion_functions import ( + sos_dict_criterion, + sos_scalar_criterion, +) +from optimagic.logging.load_database import load_database +from optimagic.logging.read_from_database import read_new_rows +from optimagic.logging.read_log import read_steps_table +from optimagic.optimization.optimize import maximize, minimize +from optimagic.optimization.optimize_result import OptimizeResult +from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds + +criteria = [sos_scalar_criterion, sos_dict_criterion] + + +@pytest.fixture() +def params(): + params = pd.DataFrame() + params["value"] = np.arange(4) + params["soft_lower_bound"] = [-5] * 4 + params["soft_upper_bound"] = [10] * 4 + return params + + +test_cases = product(criteria, ["maximize", "minimize"]) + + +@pytest.mark.parametrize("criterion, direction", test_cases) +def test_multistart_minimize_with_sum_of_squares_at_defaults( + criterion, direction, params +): + if direction == "minimize": + res = minimize( + fun=criterion, + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + ) + else: + res = maximize( + fun=switch_sign(sos_dict_criterion), + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + ) + + assert hasattr(res, "multistart_info") + ms_info = res.multistart_info + assert len(ms_info["exploration_sample"]) == 40 + assert len(ms_info["exploration_results"]) == 40 + assert all(isinstance(entry, float) for entry in ms_info["exploration_results"]) + assert all(isinstance(entry, OptimizeResult) for entry in ms_info["local_optima"]) + assert all(isinstance(entry, pd.DataFrame) for entry in ms_info["start_parameters"]) + assert np.allclose(res.fun, 0) + aaae(res.params["value"], np.zeros(4)) + + +def test_multistart_with_existing_sample(params): + sample = pd.DataFrame( + np.arange(20).reshape(5, 4) / 10, + columns=params.index, + ) + options = {"sample": sample} + + res = minimize( + fun=sos_dict_criterion, + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + multistart_options=options, + ) + + calc_sample = _params_list_to_aray(res.multistart_info["exploration_sample"]) + aaae(calc_sample, options["sample"]) + + +def test_convergence_via_max_discoveries_works(params): + options = { + "convergence_relative_params_tolerance": np.inf, + "convergence_max_discoveries": 2, + } + + res = maximize( + fun=switch_sign(sos_dict_criterion), + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + multistart_options=options, + ) + + assert len(res.multistart_info["local_optima"]) == 2 + + +def test_steps_are_logged_as_skipped_if_convergence(params): + options = { + "convergence_relative_params_tolerance": np.inf, + "convergence_max_discoveries": 2, + } + + minimize( + fun=sos_dict_criterion, + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + multistart_options=options, + logging="logging.db", + ) + + steps_table = read_steps_table("logging.db") + expected_status = ["complete", "complete", "complete", "skipped", "skipped"] + assert steps_table["status"].tolist() == expected_status + + +def test_all_steps_occur_in_optimization_iterations_if_no_convergence(params): + options = {"convergence_max_discoveries": np.inf} + + minimize( + fun=sos_dict_criterion, + params=params, + algorithm="scipy_lbfgsb", + multistart=True, + multistart_options=options, + logging="logging.db", + ) + + database = load_database(path_or_database="logging.db") + iterations, _ = read_new_rows( + database=database, + table_name="optimization_iterations", + last_retrieved=0, + return_type="dict_of_lists", + ) + + present_steps = set(iterations["step"]) + + assert present_steps == {1, 2, 3, 4, 5} + + +def test_with_non_transforming_constraints(params): + res = minimize( + fun=sos_dict_criterion, + params=params, + constraints=[{"loc": [0, 1], "type": "fixed", "value": [0, 1]}], + algorithm="scipy_lbfgsb", + multistart=True, + ) + + aaae(res.params["value"].to_numpy(), np.array([0, 1, 0, 0])) + + +def test_error_is_raised_with_transforming_constraints(params): + with pytest.raises(NotImplementedError): + minimize( + fun=sos_dict_criterion, + params=params, + constraints=[{"loc": [0, 1], "type": "probability"}], + algorithm="scipy_lbfgsb", + multistart=True, + ) + + +def _params_list_to_aray(params_list): + data = [params["value"].tolist() for params in params_list] + return np.array(data) + + +def test_multistart_with_numpy_params(): + res = minimize( + fun=lambda params: params @ params, + params=np.arange(5), + algorithm="scipy_lbfgsb", + bounds=Bounds(soft_lower=np.full(5, -10), soft_upper=np.full(5, 10)), + multistart=True, + ) + + aaae(res.params, np.zeros(5)) + + +def test_with_invalid_bounds(): + with pytest.raises(ValueError): + minimize( + fun=lambda x: x @ x, + params=np.arange(5), + algorithm="scipy_neldermead", + multistart=True, + ) + + +def test_with_scaling(): + def _crit(params): + x = params - np.arange(len(params)) + return x @ x + + res = minimize( + fun=_crit, + params=np.full(5, 10), + bounds=Bounds(soft_lower=np.full(5, -1), soft_upper=np.full(5, 11)), + algorithm="scipy_lbfgsb", + multistart=True, + ) + + aaae(res.params, np.arange(5)) + + +def test_with_ackley(): + def ackley(x): + out = ( + -20 * np.exp(-0.2 * np.sqrt(np.mean(x**2))) + - np.exp(np.mean(np.cos(2 * np.pi * x))) + + 20 + + np.exp(1) + ) + return out + + dim = 5 + + kwargs = { + "fun": ackley, + "params": np.full(dim, -10), + "bounds": Bounds(lower=np.full(dim, -32), upper=np.full(dim, 32)), + "algo_options": {"stopping.maxfun": 1000}, + } + + minimize( + **kwargs, + algorithm="scipy_lbfgsb", + multistart=True, + multistart_options={ + "n_samples": 200, + "share_optimizations": 0.1, + "convergence_max_discoveries": 10, + }, + ) + + +def test_multistart_with_least_squares_optimizers(): + est = minimize( + fun=sos_dict_criterion, + params=np.array([-1, 1.0]), + bounds=Bounds(soft_lower=np.full(2, -10), soft_upper=np.full(2, 10)), + algorithm="scipy_ls_trf", + multistart=True, + multistart_options={"n_samples": 3, "share_optimizations": 1.0}, + ) + + aaae(est.params, np.zeros(2)) diff --git a/tests/optimagic/optimization/test_with_nonlinear_constraints.py b/tests/optimagic/optimization/test_with_nonlinear_constraints.py index a5fbe3d3c..2e85e7a19 100644 --- a/tests/optimagic/optimization/test_with_nonlinear_constraints.py +++ b/tests/optimagic/optimization/test_with_nonlinear_constraints.py @@ -7,6 +7,7 @@ from optimagic.config import IS_CYIPOPT_INSTALLED from optimagic.algorithms import AVAILABLE_ALGORITHMS from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds NLC_ALGORITHMS = [ name @@ -89,8 +90,7 @@ def constraint_jac(x): "criterion": criterion, "params": np.array([0, np.sqrt(2)]), "derivative": derivative, - "lower_bounds": np.zeros(2), - "upper_bounds": 2 * np.ones(2), + "bounds": Bounds(lower=np.zeros(2), upper=2 * np.ones(2)), } kwargs = { @@ -128,8 +128,7 @@ def test_nonlinear_optimization(nlc_2d_example, algorithm, constr_type): solution_x, kwargs = nlc_2d_example if algorithm == "scipy_cobyla": - del kwargs[constr_type]["lower_bounds"] - del kwargs[constr_type]["upper_bounds"] + del kwargs[constr_type]["bounds"] with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -160,13 +159,11 @@ def test_documentation_example(algorithm): pytest.skip(reason="Slow.") kwargs = { - "lower_bounds": np.zeros(6), - "upper_bounds": 2 * np.ones(6), + "bounds": Bounds(lower=np.zeros(6), upper=2 * np.ones(6)), } if algorithm == "scipy_cobyla": - del kwargs["lower_bounds"] - del kwargs["upper_bounds"] + del kwargs["bounds"] minimize( fun=criterion, diff --git a/tests/optimagic/parameters/test_parameter_bounds.py b/tests/optimagic/parameters/test_bounds.py similarity index 55% rename from tests/optimagic/parameters/test_parameter_bounds.py rename to tests/optimagic/parameters/test_bounds.py index 3f56e7af3..0727d4da7 100644 --- a/tests/optimagic/parameters/test_parameter_bounds.py +++ b/tests/optimagic/parameters/test_bounds.py @@ -2,7 +2,7 @@ import pandas as pd import pytest from optimagic.exceptions import InvalidBoundsError -from optimagic.parameters.parameter_bounds import get_bounds +from optimagic.parameters.bounds import get_internal_bounds, Bounds, pre_process_bounds from numpy.testing import assert_array_equal @@ -23,6 +23,28 @@ def array_params(): return np.arange(2) +def test_pre_process_bounds_trivial_case(): + got = pre_process_bounds(Bounds(lower=[0], upper=[1])) + expected = Bounds(lower=[0], upper=[1]) + assert got == expected + + +def test_pre_process_bounds_none_case(): + assert pre_process_bounds(None) is None + + +def test_pre_process_bounds_sequence(): + got = pre_process_bounds([(0, 1), (None, 1)]) + expected = Bounds(lower=[0, -np.inf], upper=[1, 1]) + assert_array_equal(got.lower, expected.lower) + assert_array_equal(got.upper, expected.upper) + + +def test_pre_process_bounds_invalid_type(): + with pytest.raises(InvalidBoundsError): + pre_process_bounds(1) + + def test_get_bounds_subdataframe(pytree_params): upper_bounds = { "utility": pd.DataFrame([[2]] * 2, index=["b", "c"], columns=["value"]), @@ -31,31 +53,31 @@ def test_get_bounds_subdataframe(pytree_params): "delta": 0, "utility": pd.DataFrame([[1]] * 2, index=["a", "b"], columns=["value"]), } - lb, ub = get_bounds( - pytree_params, lower_bounds=lower_bounds, upper_bounds=upper_bounds - ) + + bounds = Bounds(lower=lower_bounds, upper=upper_bounds) + + lb, ub = get_internal_bounds(pytree_params, bounds=bounds) assert np.all(lb[1:3] == np.ones(2)) assert np.all(ub[2:4] == 2 * np.ones(2)) TEST_CASES = [ - ({"selector": lambda p: p["delta"], "lower_bounds": 0}, None), - ({"delta": [0, -1]}, None), - ({"probs": 1}, None), - ({"probs": np.array([0, 1])}, None), # wrong size lower bounds - (None, {"probs": np.array([0, 1])}), # wrong size upper bounds + Bounds(lower={"delta": [0, -1]}, upper=None), + Bounds(lower={"probs": 1}, upper=None), + Bounds(lower={"probs": np.array([0, 1])}, upper=None), # wrong size lower bounds + Bounds(lower=None, upper={"probs": np.array([0, 1])}), # wrong size upper bounds ] -@pytest.mark.parametrize("lower_bounds, upper_bounds", TEST_CASES) -def test_get_bounds_error(pytree_params, lower_bounds, upper_bounds): +@pytest.mark.parametrize("bounds", TEST_CASES) +def test_get_bounds_error(pytree_params, bounds): with pytest.raises(InvalidBoundsError): - get_bounds(pytree_params, lower_bounds=lower_bounds, upper_bounds=upper_bounds) + get_internal_bounds(pytree_params, bounds=bounds) def test_get_bounds_no_arguments(pytree_params): - got_lower, got_upper = get_bounds(pytree_params) + got_lower, got_upper = get_internal_bounds(pytree_params) expected_lower = np.array([-np.inf] + 3 * [0] + 4 * [-np.inf]) expected_upper = np.full(8, np.inf) @@ -67,7 +89,9 @@ def test_get_bounds_no_arguments(pytree_params): def test_get_bounds_with_lower_bounds(pytree_params): lower_bounds = {"delta": 0.1} - got_lower, got_upper = get_bounds(pytree_params, lower_bounds=lower_bounds) + bounds = Bounds(lower=lower_bounds) + + got_lower, got_upper = get_internal_bounds(pytree_params, bounds=bounds) expected_lower = np.array([0.1] + 3 * [0] + 4 * [-np.inf]) expected_upper = np.full(8, np.inf) @@ -80,7 +104,8 @@ def test_get_bounds_with_upper_bounds(pytree_params): upper_bounds = { "utility": pd.DataFrame([[1]] * 3, index=["a", "b", "c"], columns=["value"]), } - got_lower, got_upper = get_bounds(pytree_params, upper_bounds=upper_bounds) + bounds = Bounds(upper=upper_bounds) + got_lower, got_upper = get_internal_bounds(pytree_params, bounds=bounds) expected_lower = np.array([-np.inf] + 3 * [0] + 4 * [-np.inf]) expected_upper = np.array([np.inf] + 3 * [1] + 4 * [np.inf]) @@ -90,7 +115,7 @@ def test_get_bounds_with_upper_bounds(pytree_params): def test_get_bounds_numpy(array_params): - got_lower, got_upper = get_bounds(array_params) + got_lower, got_upper = get_internal_bounds(array_params) expected = np.array([np.inf, np.inf]) @@ -99,10 +124,10 @@ def test_get_bounds_numpy(array_params): def test_get_bounds_numpy_error(array_params): + # lower bounds larger than upper bounds + bounds = Bounds(lower=np.ones_like(array_params), upper=np.zeros_like(array_params)) with pytest.raises(InvalidBoundsError): - get_bounds( + get_internal_bounds( array_params, - # lower bounds larger than upper bounds - lower_bounds=np.ones_like(array_params), - upper_bounds=np.zeros_like(array_params), + bounds=bounds, ) diff --git a/tests/optimagic/parameters/test_constraint_tools.py b/tests/optimagic/parameters/test_constraint_tools.py index 6e1ed1698..86402e5dc 100644 --- a/tests/optimagic/parameters/test_constraint_tools.py +++ b/tests/optimagic/parameters/test_constraint_tools.py @@ -11,7 +11,7 @@ def test_count_free_params_no_constraints(): def test_count_free_params_with_constraints(): params = {"a": 1, "b": 2, "c": [3, 3]} constraints = [{"selector": lambda x: x["c"], "type": "equality"}] - assert count_free_params(params, constraints) == 3 + assert count_free_params(params, constraints=constraints) == 3 def test_check_constraints(): @@ -19,4 +19,4 @@ def test_check_constraints(): constraints = [{"selector": lambda x: x["c"], "type": "equality"}] with pytest.raises(InvalidParamsError): - check_constraints(params, constraints) + check_constraints(params, constraints=constraints) diff --git a/tests/optimagic/parameters/test_conversion.py b/tests/optimagic/parameters/test_conversion.py index b24dfff23..5ecc28836 100644 --- a/tests/optimagic/parameters/test_conversion.py +++ b/tests/optimagic/parameters/test_conversion.py @@ -7,14 +7,14 @@ get_converter, ) from numpy.testing import assert_array_almost_equal as aaae +from optimagic.parameters.bounds import Bounds def test_get_converter_fast_case(): converter, internal = get_converter( params=np.arange(3), constraints=None, - lower_bounds=None, - upper_bounds=None, + bounds=None, func_eval=3, derivative_eval=2 * np.arange(3), primary_key="value", @@ -36,11 +36,14 @@ def test_get_converter_fast_case(): def test_get_converter_with_constraints_and_bounds(): + bounds = Bounds( + lower=np.array([-1, -np.inf, -np.inf]), + upper=np.array([np.inf, 10, np.inf]), + ) converter, internal = get_converter( params=np.arange(3), constraints=[{"loc": 2, "type": "fixed"}], - lower_bounds=np.array([-1, -np.inf, -np.inf]), - upper_bounds=np.array([np.inf, 10, np.inf]), + bounds=bounds, func_eval=3, derivative_eval=2 * np.arange(3), primary_key="value", @@ -62,11 +65,14 @@ def test_get_converter_with_constraints_and_bounds(): def test_get_converter_with_scaling(): + bounds = Bounds( + lower=np.arange(3) - 1, + upper=np.arange(3) + 1, + ) converter, internal = get_converter( params=np.arange(3), constraints=None, - lower_bounds=np.arange(3) - 1, - upper_bounds=np.arange(3) + 1, + bounds=bounds, func_eval=3, derivative_eval=2 * np.arange(3), primary_key="value", @@ -92,8 +98,7 @@ def test_get_converter_with_trees(): converter, internal = get_converter( params=params, constraints=None, - lower_bounds=None, - upper_bounds=None, + bounds=None, func_eval={"contributions": {"d": 1, "e": 2}}, derivative_eval={"a": 0, "b": 2, "c": 4}, primary_key="value", diff --git a/tests/optimagic/parameters/test_process_constraints.py b/tests/optimagic/parameters/test_process_constraints.py index aba4aac37..a2059d0cc 100644 --- a/tests/optimagic/parameters/test_process_constraints.py +++ b/tests/optimagic/parameters/test_process_constraints.py @@ -8,6 +8,7 @@ from optimagic.parameters.process_constraints import ( _replace_pairwise_equality_by_equality, ) +from optimagic.parameters.bounds import Bounds def test_replace_pairwise_equality_by_equality(): @@ -37,6 +38,6 @@ def test_to_many_bounds_in_increasing_constraint_raise_good_error(): with pytest.raises(InvalidConstraintError): check_constraints( params=np.arange(3), - lower_bounds=np.arange(3) - 1, + bounds=Bounds(lower=np.arange(3) - 1), constraints={"loc": [0, 1, 2], "type": "increasing"}, ) diff --git a/tests/optimagic/parameters/test_tree_conversion.py b/tests/optimagic/parameters/test_tree_conversion.py index 269c35237..0b2860d7f 100644 --- a/tests/optimagic/parameters/test_tree_conversion.py +++ b/tests/optimagic/parameters/test_tree_conversion.py @@ -3,6 +3,7 @@ import pytest from optimagic.parameters.tree_conversion import get_tree_converter from numpy.testing import assert_array_equal as aae +from optimagic.parameters.bounds import Bounds @pytest.fixture() @@ -31,10 +32,12 @@ def upper_bounds(): @pytest.mark.parametrize("func_eval", FUNC_EVALS) def test_tree_converter_primary_key_is_value(params, upper_bounds, func_eval): + bounds = Bounds( + upper=upper_bounds, + ) converter, flat_params = get_tree_converter( params=params, - lower_bounds=None, - upper_bounds=upper_bounds, + bounds=bounds, func_eval=func_eval, derivative_eval=params, primary_key="value", @@ -73,8 +76,7 @@ def test_tree_conversion_fast_path(primary_entry): converter, flat_params = get_tree_converter( params=np.arange(3), - lower_bounds=None, - upper_bounds=None, + bounds=None, func_eval=func_eval, derivative_eval=derivative_eval, primary_key=primary_entry, diff --git a/tests/test_deprecations.py b/tests/optimagic/test_deprecations.py similarity index 77% rename from tests/test_deprecations.py rename to tests/optimagic/test_deprecations.py index a492b9f5a..c674a8405 100644 --- a/tests/test_deprecations.py +++ b/tests/optimagic/test_deprecations.py @@ -25,7 +25,9 @@ from estimagic import OptimizeLogReader, OptimizeResult from estimagic import criterion_plot, params_plot import optimagic as om +import estimagic as em import warnings +from optimagic.parameters.bounds import Bounds # ====================================================================================== # Deprecated in 0.5.0, remove in 0.6.0 @@ -92,8 +94,7 @@ def test_estimagic_slice_plot_is_deprecated(): slice_plot( func=lambda x: x @ x, params=np.arange(3), - lower_bounds=np.zeros(3), - upper_bounds=np.ones(3) * 5, + bounds=Bounds(lower=np.zeros(3), upper=np.ones(3) * 5), ) @@ -388,3 +389,119 @@ def test_deprecated_attributes_of_optimize_result(): with pytest.warns(FutureWarning, match=msg): _ = res.start_criterion + + +BOUNDS_KWARGS = [ + {"lower_bounds": np.full(3, -1)}, + {"upper_bounds": np.full(3, 2)}, +] + +SOFT_BOUNDS_KWARGS = [ + {"soft_lower_bounds": np.full(3, -1)}, + {"soft_upper_bounds": np.full(3, 1)}, +] + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS + SOFT_BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_minimize(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.minimize( + lambda x: x @ x, + np.arange(3), + algorithm="scipy_lbfgsb", + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS + SOFT_BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_maximize(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.maximize( + lambda x: -x @ x, + np.arange(3), + algorithm="scipy_lbfgsb", + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_first_derivative(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.first_derivative( + lambda x: x @ x, + np.arange(3), + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_second_derivative(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.second_derivative( + lambda x: x @ x, + np.arange(3), + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_estimate_ml(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + em.estimate_ml( + loglike=lambda x: {"contributions": -(x**2), "value": -x @ x}, + params=np.arange(3), + optimize_options={"algorithm": "scipy_lbfgsb"}, + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_estimate_msm(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + em.estimate_msm( + simulate_moments=lambda x: x, + empirical_moments=np.zeros(3), + moments_cov=np.eye(3), + params=np.arange(3), + optimize_options={"algorithm": "scipy_lbfgsb"}, + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_count_free_params(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.count_free_params( + np.arange(3), + constraints=[{"loc": 0, "type": "fixed"}], + **bounds_kwargs, + ) + + +@pytest.mark.parametrize("bounds_kwargs", BOUNDS_KWARGS) +def test_old_bounds_are_deprecated_in_check_constraints(bounds_kwargs): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.check_constraints( + np.arange(3), + constraints=[{"loc": 0, "type": "fixed"}], + **bounds_kwargs, + ) + + +def test_old_bounds_are_deprecated_in_slice_plot(): + msg = "Specifying bounds via the arguments" + with pytest.warns(FutureWarning, match=msg): + om.slice_plot( + lambda x: x @ x, + np.arange(3), + lower_bounds=np.full(3, -1), + upper_bounds=np.full(3, 2), + ) diff --git a/tests/optimagic/visualization/test_history_plots.py b/tests/optimagic/visualization/test_history_plots.py index a31c7ecdc..26d7bb090 100644 --- a/tests/optimagic/visualization/test_history_plots.py +++ b/tests/optimagic/visualization/test_history_plots.py @@ -4,10 +4,12 @@ import pytest from optimagic.optimization.optimize import minimize from optimagic.visualization.history_plots import criterion_plot, params_plot +from optimagic.parameters.bounds import Bounds @pytest.fixture() def minimize_result(): + bounds = Bounds(soft_lower=np.full(5, -1), soft_upper=np.full(5, 6)) out = {} for multistart in [True, False]: res = [] @@ -16,8 +18,7 @@ def minimize_result(): fun=lambda x: x @ x, params=np.arange(5), algorithm=algorithm, - soft_lower_bounds=np.full(5, -1), - soft_upper_bounds=np.full(5, 6), + bounds=bounds, multistart=multistart, multistart_options={ "n_samples": 1000, @@ -94,13 +95,13 @@ def test_criterion_plot_wrong_results(): def test_criterion_plot_different_input_types(): + bounds = Bounds(soft_lower=np.full(5, -1), soft_upper=np.full(5, 6)) # logged result minimize( fun=lambda x: x @ x, params=np.arange(5), algorithm="scipy_lbfgsb", - soft_lower_bounds=np.full(5, -1), - soft_upper_bounds=np.full(5, 6), + bounds=bounds, multistart=True, multistart_options={"n_samples": 1000, "convergence.max_discoveries": 5}, log_options={"fast_logging": True}, @@ -111,8 +112,7 @@ def test_criterion_plot_different_input_types(): fun=lambda x: x @ x, params=np.arange(5), algorithm="scipy_lbfgsb", - soft_lower_bounds=np.full(5, -1), - soft_upper_bounds=np.full(5, 6), + bounds=bounds, multistart=True, multistart_options={"n_samples": 1000, "convergence.max_discoveries": 5}, ) diff --git a/tests/optimagic/visualization/test_slice_plot.py b/tests/optimagic/visualization/test_slice_plot.py index 71015f6e5..de812ba45 100644 --- a/tests/optimagic/visualization/test_slice_plot.py +++ b/tests/optimagic/visualization/test_slice_plot.py @@ -1,18 +1,20 @@ import numpy as np import pytest from optimagic.visualization.slice_plot import slice_plot +from optimagic.parameters.bounds import Bounds @pytest.fixture() def fixed_inputs(): params = {"alpha": 0, "beta": 0, "gamma": 0, "delta": 0} - lower_bounds = {name: -5 for name in params} - upper_bounds = {name: i + 2 for i, name in enumerate(params)} + bounds = Bounds( + lower={name: -5 for name in params}, + upper={name: i + 2 for i, name in enumerate(params)}, + ) out = { "params": params, - "lower_bounds": lower_bounds, - "upper_bounds": upper_bounds, + "bounds": bounds, } return out