Repository: optimagic-dev/optimagic (language: python)

Issue:
Improve the results returned by minimize and maximize
### Current Situation

1. in the `results` dictionary the `status` value is always set to "success" when the optimization finishes. This results in unwanted behavior (e.g. status is set to "success" when scipy stops after the maximum number of iterations).

2. in `_process_scipy_results` entries like the Jacobian are saved twice, once under the unified name and once under the original name.

3. The results dictionary does not contain a path to the database. 

4. We still `fitness` as the name of the final criterion value and `x` for the parameter vector.

5. At the moment all internal columns that start with `_` are returned to the user in the final `params` DataFrame as well. Do we want this? 


### Desired Situation

1. status is only set to "success" if the optimizer claims to have successfully finished. In addition, we might want to check that the current function evaluation is the lowest one recorded in the database.

2. Not sure if this is the behavior we want. One the one hand people coming from scipy might appreciate that they can use the results object as before. One the other hand it is duplication and users might wonder if the entries refer to different concepts and just happen to be the same.

3. The results dictionary should include this.

4. I would suggest `criterion_value` and `final_internal_params`. 

5. I would propose to remove all private (i.e. starting with a `_`) columns before returning `params` to the user.



Discussion / hints:

Fix patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 000000000..eab911891
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,8 @@
+coverage:
+  status:
+    patch:
+      default:
+        target: 80%
+    project:
+      default:
+        target: 84%
diff --git a/.conda/meta.yaml b/.conda/meta.yaml
index cdd7b5ea1..42a71c8d5 100644
--- a/.conda/meta.yaml
+++ b/.conda/meta.yaml
@@ -20,15 +20,15 @@ requirements:
     - python
     - setuptools
   host:
-    - python >=3.6
+    - python >=3.8
 
   run:
-    - python >=3.6
+    - python >=3.8
     - click
     - cloudpickle
     - joblib
     - numpy >=1.16
-    - pandas >=0.24
+    - pandas >=1.0
     - bokeh >=1.3
     - scipy
     - fuzzywuzzy
diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml
index 2e328bf04..665de5379 100644
--- a/.github/workflows/continuous-integration-workflow.yml
+++ b/.github/workflows/continuous-integration-workflow.yml
@@ -18,7 +18,7 @@ jobs:
       fail-fast: false
       matrix:
         os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
-        python-version: ['3.6', '3.7', '3.8']
+        python-version: ['3.8', 3.9]
 
     steps:
       - uses: actions/checkout@v2
@@ -38,7 +38,7 @@ jobs:
 
       - name: Run pytest.
         shell: bash -l {0}
-        run: tox -e pytest -- -m "not slow" --cov-report=xml --cov=./ -n auto
+        run: tox -e pytest -- -m "not slow" --cov-report=xml --cov=./
 
       - name: Upload coverage report.
         if: runner.os == 'Linux' && matrix.python-version == '3.8'
diff --git a/.gitignore b/.gitignore
index 6a81f42ca..18aba498e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,6 +68,10 @@ instance/
 
 # Sphinx documentation
 docs/_build/
+docs/build/
+docs/source/_build/
+docs/source/getting_started/*.db
+docs/source/refs.bib.bak
 
 # PyBuilder
 target/
diff --git a/CHANGES.rst b/CHANGES.rst
index 02c2b7c5e..d169b8402 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,5 @@
 Changes
-=======
+^^^^^^^
 
 This is a record of all past estimagic releases and what went into them in reverse
 chronological order. We follow `semantic versioning <https://semver.org/>`_ and all
@@ -15,10 +15,13 @@ Add a lot of new functionality with a few minor breaking changes. We have more
 optimizers, better error handling, bootstrap and inference for method of simulated
 moments. The breaking changes are:
 - logging is disabled by default during optimization.
+- the log_option "if_exists" was renamed to "if_table_exists"
 - The comparison plot function is removed.
-- first_derivative now returns a dictionary instead, independent of arguments.
-
+- first_derivative now returns a dictionary, independent of arguments.
+- structure of the logging database has changed
 
+- :gh:`228` Restructures testing and makes changes to log_options.
+- :gh:`149` Adds ``estimate_msm`` functionality (:ghuser:`janosg` and :ghuser:`loikein`)
 - :gh:`219` Several enhancements by (:ghuser:`tobiasraabe`)
 - :gh:`218` Improve documentation by (:ghuser:`sofyaakimova`) and (:ghuser:`effieHan`)
 - :gh:`214` Fix bug with overlapping "fixed" and "linear" constraints (:ghuser:`janosg`)
diff --git a/README.rst b/README.rst
index 06c66224a..0e6e5b1c5 100644
--- a/README.rst
+++ b/README.rst
@@ -61,6 +61,17 @@ Optimization
   :scale: 21 %
 
 
+Estimation and Inference
+------------------------
+
+- You can estimate a model using method of simulated moments (MSM), calculate standard
+  errors and do sensitivity analysis with just one function call.
+- Asymptotic standard errors for maximum likelihood estimation.
+- estimagic also provides bootstrap confidence intervals and standard errors.
+  Of course the bootstrap procedures are parallelized.
+
+
+
 Numerical differentiation
 -------------------------
 
@@ -70,13 +81,6 @@ Numerical differentiation
   with pre-implemented or user provided batch evaluators.
 
 
-Statistical Inference
----------------------
-
-- estimagic provides asymptotic standard errors for maximum likelihood and method
-  of simulated moments.
-- estimagic also provides bootstrap confidence intervals and standard errors.
-  Of course the bootstrap procedures are parallelized.
 
 
 Installation
diff --git a/docs/environment.yml b/docs/environment.yml
index c1ed6d927..b2dd6d6f3 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -1,7 +1,7 @@
 channels:
   - defaults
 dependencies:
-  - python=3.7
+  - python=3.8
   - ipython
   - pip:
     - sphinx
diff --git a/docs/source/getting_started/first_msm_estimation_with_estimagic.ipynb b/docs/source/getting_started/first_msm_estimation_with_estimagic.ipynb
index adc753d5e..29e335478 100644
--- a/docs/source/getting_started/first_msm_estimation_with_estimagic.ipynb
+++ b/docs/source/getting_started/first_msm_estimation_with_estimagic.ipynb
@@ -2,8 +2,6 @@
  "cells": [
   {
    "cell_type": "markdown",
-   "id": "mounted-exchange",
-   "metadata": {},
    "source": [
     "# First Method of Simulated Moments (MSM) estimation with estimagic\n",
     "\n",
@@ -17,46 +15,48 @@
     "## Steps of MSM estimation\n",
     "\n",
     "- load empirical data \n",
-    "- define a function calculate estimation moments on the data \n",
+    "- define a function to calculate estimation moments on the data \n",
     "- calculate the covariance matrix of the empirical moments (with ``get_moments_cov``)\n",
     "- define a function to simulate moments from the model \n",
     "- estimate the model, calculate standard errors, do sensitivity analysis (with ``estimate_msm``)\n",
     "\n",
     "## Example: Estimating the parameters of a truncated normal distribution\n",
     "\n",
-    "The model we consiter is a truncated normal distribution. The goal is to estimate its parameters from a dataset of grades observed in a macroeconomics class. \n",
+    "The model we consider is a truncated normal distribution. The goal is to estimate its parameters from a dataset of grades observed in a macroeconomics class. \n",
     "\n",
     "The estimation mechanics are exactly the same for more complicated models. A model is always defined by a function that can take parameters (here: the mean, variance and lower_cutoff and upper_cutoff) and returns a number of simulated moments (mean, variance, soft_min and soft_max of simulated exam points)."
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 1,
-   "id": "mathematical-diesel",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "import numpy as np\n",
     "import pandas as pd\n",
     "\n",
     "from estimagic.config import EXAMPLE_DIR"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "greatest-skirt",
-   "metadata": {},
    "source": [
     "## Load data"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 2,
-   "id": "double-hunter",
-   "metadata": {},
+   "source": [
+    "data = pd.read_csv(EXAMPLE_DIR / \"exam_points.csv\")\n",
+    "data.head(3).append(data.tail(3))"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -119,20 +119,14 @@
        "160   60.20"
       ]
      },
-     "execution_count": 2,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 2
     }
    ],
-   "source": [
-    "data = pd.read_csv(EXAMPLE_DIR / \"exam_points.csv\")\n",
-    "data.head(3).append(data.tail(3))"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "concerned-robin",
-   "metadata": {},
    "source": [
     "## Define function to calculate moments\n",
     "\n",
@@ -149,15 +143,13 @@
     "\n",
     "Note that such a direct equivalence of moments and parameters is not strictly needed, but it is a good way to think about the problem:\n",
     "\n",
-    "We can also estimate a version where we only use the first two moments or we could include even more moments (like shares that fall into a certain bin of points, as in Rick's original example. In general, more moments are better. "
-   ]
+    "We can also estimate a version where we only use the first two moments or we could include even more moments (like shares that fall into a certain bin of points, as in Rick's original example). In general, more moments are better. "
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 3,
-   "id": "affiliated-following",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "def calculate_moments(sample, targets=\"all\"):\n",
     "    points = sample[\"points\"]\n",
@@ -171,15 +163,20 @@
     "        moments = {k: v for k, v in moments.items() if k in targets}\n",
     "    moments = pd.Series(moments)\n",
     "    return moments"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 4,
-   "id": "treated-favor",
-   "metadata": {},
+   "source": [
+    "empirical_moments = calculate_moments(data)\n",
+    "empirical_moments"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/plain": [
        "mean    341.908696\n",
@@ -189,20 +186,14 @@
        "dtype: float64"
       ]
      },
-     "execution_count": 4,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 4
     }
    ],
-   "source": [
-    "empirical_moments = calculate_moments(data)\n",
-    "empirical_moments"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "above-given",
-   "metadata": {},
    "source": [
     "## Calculate the covariance matrix of empirical moments\n",
     "\n",
@@ -212,25 +203,30 @@
     "3. to calculate sensitivity measures\n",
     "\n",
     "We will calculate ``moments_cov`` via a bootstrap. Depending on your problem there can be other ways to do it."
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 5,
-   "id": "toxic-sailing",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "from estimagic import get_moments_cov"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 6,
-   "id": "touched-passport",
-   "metadata": {},
+   "source": [
+    "moments_cov = get_moments_cov(\n",
+    "    data, calculate_moments, bootstrap_kwargs={\"n_draws\": 5_000, \"seed\": 1234}\n",
+    ")\n",
+    "moments_cov"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -298,46 +294,46 @@
        "max    2.128968  -0.559551    2.427182  11.929751"
       ]
      },
-     "execution_count": 6,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 6
     }
    ],
-   "source": [
-    "moments_cov = get_moments_cov(\n",
-    "    data, calculate_moments, bootstrap_kwargs={\"n_draws\": 5_000, \"seed\": 1234}\n",
-    ")\n",
-    "moments_cov"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "spiritual-salvation",
-   "metadata": {},
    "source": [
     "``get_moments_cov`` mainly just calls estimagic's bootstrap function. See our [bootstrap_tutorial](../how_to_guides/inference/how_to_do_bootstrap_inference.ipynb) for background information. \n",
     "\n"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "tropical-extra",
-   "metadata": {},
    "source": [
     "## Define a function to calculate simulated moments\n",
     "\n",
-    "In real application, this is the step that takes most of the time. However, in our very simple case, all the work is already done by scipy.\n",
+    "In a real application, this is the step that takes most of the time. However, in our very simple example, all the work is already done by scipy.\n",
     "\n",
     "To test our function let's first set up a parameter vector (which will also serve as start parameters for the numerical optimization). "
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 7,
-   "id": "million-blocking",
-   "metadata": {},
+   "source": [
+    "params = pd.DataFrame(\n",
+    "    [500, 100, 0, 450],\n",
+    "    index=[\"mean\", \"sd\", \"lower_trunc\", \"upper_trunc\"],\n",
+    "    columns=[\"value\"],\n",
+    ")\n",
+    "params[\"lower_bound\"] = [-np.inf, 0, -np.inf, -np.inf]\n",
+    "params"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -395,37 +391,24 @@
        "upper_trunc    450         -inf"
       ]
      },
-     "execution_count": 7,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 7
     }
    ],
-   "source": [
-    "params = pd.DataFrame(\n",
-    "    [500, 100, 0, 450],\n",
-    "    index=[\"mean\", \"sd\", \"lower_trunc\", \"upper_trunc\"],\n",
-    "    columns=[\"value\"],\n",
-    ")\n",
-    "params[\"lower_bound\"] = [-np.inf, 0, -np.inf, -np.inf]\n",
-    "params"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 8,
-   "id": "limiting-benjamin",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "from scipy.stats import truncnorm"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 9,
-   "id": "early-essence",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "def simulate_moments(params, n_draws=1_000, seed=5471):\n",
     "    np.random.seed(seed)\n",
@@ -443,15 +426,19 @@
     "    sim_moments = calculate_moments(sim_data)\n",
     "\n",
     "    return sim_moments"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 10,
-   "id": "focused-average",
-   "metadata": {},
+   "source": [
+    "simulate_moments(params)"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/plain": [
        "mean    384.237035\n",
@@ -461,49 +448,41 @@
        "dtype: float64"
       ]
      },
-     "execution_count": 10,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 10
     }
    ],
-   "source": [
-    "simulate_moments(params)"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "noted-patio",
-   "metadata": {},
    "source": [
     "## Estimate the model parameters\n",
     "\n",
-    "Estimating a model means entails the following steps:\n",
+    "Estimating a model consists of the following steps:\n",
     "\n",
     "- Building a criterion function that measures a distance between simulated and empirical moments\n",
-    "- Minimizing this criterion function.\n",
-    "- Calculating the jacobian of the model\n",
-    "- Calculating standard errors, confidence intervals and p_values\n",
+    "- Minimizing this criterion function\n",
+    "- Calculating the Jacobian of the model\n",
+    "- Calculating standard errors, confidence intervals and p values\n",
     "- Calculating sensitivity measures\n",
     "\n",
-    "This can all be done in one go with the ``estimate_msm`` function. This function has good default values, so you only need a minimum number of inputs. However you can configure almost every aspect of the workflow via optional arguments. If you need even more control, you can call the low level functions ``estimate_msm`` is built on directly. "
-   ]
+    "This can all be done in one go with the ``estimate_msm`` function. This function has good default values, so you only need a minimum number of inputs. However, you can configure almost every aspect of the workflow via optional arguments. If you need even more control, you can call the low level functions ``estimate_msm`` is built on directly. "
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 11,
-   "id": "relative-implement",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "from estimagic import estimate_msm"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 12,
-   "id": "compact-leisure",
-   "metadata": {},
-   "outputs": [],
    "source": [
     "res = estimate_msm(\n",
     "    simulate_moments,\n",
@@ -512,15 +491,19 @@
     "    params,\n",
     "    minimize_options={\"algorithm\": \"scipy_lbfgsb\"},\n",
     ")"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 13,
-   "id": "special-senegal",
-   "metadata": {},
+   "source": [
+    "res[\"summary\"]"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -604,50 +587,43 @@
        "upper_trunc   ***  "
       ]
      },
-     "execution_count": 13,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 13
     }
    ],
-   "source": [
-    "res[\"summary\"]"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "associate-walker",
-   "metadata": {},
    "source": [
     "## What's in the result?\n",
     "\n",
     "The result is a dictionary with the following entries:"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 14,
-   "id": "assigned-decision",
-   "metadata": {},
+   "source": [
+    "res.keys()"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/plain": [
        "dict_keys(['summary', 'cov', 'minimize_res', 'numdiff_info', 'jacobian', 'sensitivity'])"
       ]
      },
-     "execution_count": 14,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 14
     }
    ],
-   "source": [
-    "res.keys()"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "micro-meaning",
-   "metadata": {},
    "source": [
     "- summary: A DataFrame with estimated parameters, standard errors, p_values and confidence intervals. This is ideal for use with our `estimation_table` function. \n",
     "- cov: A DataFrame with the full covariance matrix of the estimated parameters. You can use this for hypothesis tests. \n",
@@ -655,64 +631,78 @@
     "- numdiff_info: A dictionary with the complete output of the numerical differentiation (if one was performed)\n",
     "- jacobian: A DataFrame with the jacobian of `simulate_moments` with respect to the free parameters. \n",
     "- sensitivity: A dictionary of DataFrames with the six sensitivity measures from Honore, Jorgensen and DePaula (2020)"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "eleven-bread",
-   "metadata": {},
    "source": [
     "## How to visualize sensitivity measures\n",
     "\n",
     "For more background on the sensitivity measures and their interpretation, check out the [how to guide](../how_to_guides/miscellaneous/how_to_visualize_and_interpret_sensitivity_measures.ipynb) on sensitivity measures. \n",
     "\n",
     "Here we are just showing you how to plot them:"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 15,
-   "id": "handed-constitutional",
-   "metadata": {},
+   "source": [
+    "from estimagic.visualization.lollipop_plot import lollipop_plot\n",
+    "\n",
+    "lollipop_plot(res[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T);"
+   ],
    "outputs": [
     {
+     "output_type": "display_data",
      "data": {
-      "image/png": "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\n",
+      "image/png": "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",
       "text/plain": [
        "<Figure size 360x180 with 4 Axes>"
       ]
      },
-     "metadata": {},
-     "output_type": "display_data"
+     "metadata": {}
     }
    ],
-   "source": [
-    "from estimagic.visualization.lollipop_plot import lollipop_plot\n",
-    "\n",
-    "lollipop_plot(res[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T);"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "moved-programmer",
-   "metadata": {},
    "source": [
     "## How to deal with calibrated parameters\n",
     "\n",
-    "In many MSM applications, not all parameters are estimated freely. Instead, some of them are calibrated outside of the model. Thus they are fixed during the estimation and no standard errors can be calculated for them. \n",
+    "In many MSM applications, not all parameters are estimated freely. Instead, some of them are calibrated outside of the model. Thus, they are fixed during the estimation and no standard errors can be calculated for them. \n",
     "\n",
     "To do this in `estimate_msm`, you can use the same constraint syntax you already know from `maximize` and `minimize`. \n",
     "\n",
     "For example, in Rick's Tutorial, the cutoffs are fixed to 0 and 450, respectively. This can be expressed as follows:"
-   ]
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 16,
-   "id": "permanent-batman",
-   "metadata": {},
+   "source": [
+    "constraints = [\n",
+    "    {\"loc\": \"lower_trunc\", \"type\": \"fixed\", \"value\": 0},\n",
+    "    {\"loc\": \"upper_trunc\", \"type\": \"fixed\", \"value\": 450},\n",
+    "]\n",
+    "\n",
+    "res_small = estimate_msm(\n",
+    "    simulate_moments,\n",
+    "    empirical_moments,\n",
+    "    moments_cov,\n",
+    "    params,\n",
+    "    minimize_options={\"algorithm\": \"scipy_lbfgsb\"},\n",
+    "    constraints=constraints,\n",
+    ")\n",
+    "\n",
+    "res_small[\"summary\"]"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -796,80 +786,71 @@
        "upper_trunc   NaN  "
       ]
      },
-     "execution_count": 16,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 16
     }
    ],
-   "source": [
-    "constraints = [\n",
-    "    {\"loc\": \"lower_trunc\", \"type\": \"fixed\", \"value\": 0},\n",
-    "    {\"loc\": \"upper_trunc\", \"type\": \"fixed\", \"value\": 450},\n",
-    "]\n",
-    "\n",
-    "res_small = estimate_msm(\n",
-    "    simulate_moments,\n",
-    "    empirical_moments,\n",
-    "    moments_cov,\n",
-    "    params,\n",
-    "    minimize_options={\"algorithm\": \"scipy_lbfgsb\"},\n",
-    "    constraints=constraints,\n",
-    ")\n",
-    "\n",
-    "res_small[\"summary\"]"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 17,
-   "id": "severe-namibia",
-   "metadata": {},
+   "source": [
+    "lollipop_plot(\n",
+    "    [\n",
+    "        res[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T,\n",
+    "        res_small[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T,\n",
+    "    ]\n",
+    ");"
+   ],
    "outputs": [
     {
+     "output_type": "display_data",
      "data": {
-      "image/png": "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\n",
+      "image/png": "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",
       "text/plain": [
        "<Figure size 360x180 with 4 Axes>"
       ]
      },
-     "metadata": {},
-     "output_type": "display_data"
+     "metadata": {}
     }
    ],
-   "source": [
-    "lollipop_plot(\n",
-    "    [\n",
-    "        res[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T,\n",
-    "        res_small[\"sensitivity\"][\"sensitivity_to_bias\"].abs().T,\n",
-    "    ]\n",
-    ");"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "massive-maintenance",
-   "metadata": {},
    "source": [
-    "Calculating standard errors, p_values for the constrained model works out of the box. The sensitivity measures for free parameters also work. Showing sensitivity of estimated parameters with respect to calibrated ones does not work yet but is planned soon. If you can't wait, check out Thomas Jorgensen's [Paper](https://arxiv.org/abs/2004.12100) on the topic and make a pull request ;-)."
-   ]
+    "Calculating standard errors, p values for the constrained model works out of the box. The sensitivity measures for free parameters also work. Showing sensitivity of estimated parameters with respect to calibrated ones does not work yet but is planned soon. If you can't wait, check out Thomas Jorgensen's [Paper](https://arxiv.org/abs/2004.12100) on the topic and make a pull request ;-)"
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "devoted-hearing",
-   "metadata": {},
    "source": [
     "## Comparison with Rick's results\n",
     "\n",
-    "We can compare point estimates and standard errors Rick's Maximum Likelihood and MSM estimates. Note that he fixed the lower and upper truncation points. "
-   ]
+    "We can compare point estimates and standard errors to Rick's Maximum Likelihood and MSM estimates. Note that he fixed the lower and upper truncation points. "
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 18,
-   "id": "surface-algebra",
-   "metadata": {},
+   "source": [
+    "comparison = pd.DataFrame()\n",
+    "comparison[\"estimagic_free\"] = res[\"minimize_res\"][\"solution_params\"][[\"value\"]].round(\n",
+    "    1\n",
+    ")\n",
+    "comparison[\"estimagic_fixed\"] = res_small[\"minimize_res\"][\"solution_params\"][\n",
+    "    [\"value\"]\n",
+    "].round(1)\n",
+    "comparison[\"rick_mle\"] = [622.3, 198.8, 0, 450]\n",
+    "comparison[\"rick_msm\"] = [612.3, 197.3, 0, 450]\n",
+    "comparison"
+   ],
    "outputs": [
     {
+     "output_type": "execute_result",
      "data": {
       "text/html": [
        "<div>\n",
@@ -937,39 +918,27 @@
        "upper_trunc           449.9            450.0     450.0     450.0"
       ]
      },
-     "execution_count": 18,
      "metadata": {},
-     "output_type": "execute_result"
+     "execution_count": 18
     }
    ],
-   "source": [
-    "comparison = pd.DataFrame()\n",
-    "comparison[\"estimagic_free\"] = res[\"minimize_res\"][\"solution_params\"][[\"value\"]].round(\n",
-    "    1\n",
-    ")\n",
-    "comparison[\"estimagic_fixed\"] = res_small[\"minimize_res\"][\"solution_params\"][\n",
-    "    [\"value\"]\n",
-    "].round(1)\n",
-    "comparison[\"rick_mle\"] = [622.3, 198.8, 0, 450]\n",
-    "comparison[\"rick_msm\"] = [612.3, 197.3, 0, 450]\n",
-    "comparison"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "markdown",
-   "id": "celtic-clinic",
-   "metadata": {},
    "source": [
-    "Interestingly, the estimates where all parameters are free are closer to the maximum likelihood estimates than the other ones. Overall, given the very small sample and the differnt estimation moments, we can conclude that the results are similar enough."
-   ]
+    "Interestingly, the estimates where all parameters are free are closer to the maximum likelihood estimates than the other ones.\n",
+    "\n",
+    "Note that we used different estimation moments, so the results are not really comparable. We will do an exact replication soon. "
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "located-brooks",
-   "metadata": {},
+   "source": [],
    "outputs": [],
-   "source": []
+   "metadata": {}
   }
  ],
  "metadata": {
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 09e380ec0..7292b631e 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -33,15 +33,26 @@ Optimization
   interactive dashboard. See :ref:`dashboard`.
 
 
-
 .. _How to use logging: how_to_guides/optimization/how_to_use_logging.ipynb
 
 
-
   .. image:: _static/images/dashboard.gif
     :scale: 80 %
     :align: center
 
+Estimation and Inference
+------------------------
+
+- You can estimate a model using method of simulated moments (MSM), calculate standard
+  errors and do sensitivity analysis with just one function call.
+  See `MSM Tutorial`_
+- Asymptotic standard errors for maximum likelihood estimation.
+- estimagic also provides bootstrap confidence intervals and standard errors.
+  Of course the bootstrap procedures are parallelized.
+
+.. _MSM Tutorial: getting_started/first_msm_estimation_with_estimagic.ipynb
+
+
 Numerical differentiation
 -------------------------
 - estimagic can calculate precise numerical derivatives using `Richardson extrapolations
@@ -49,13 +60,6 @@ Numerical differentiation
 - Function evaluations needed for numerical derivatives can be done in parallel
   with pre-implemented or user provided batch evaluators.
 
-Statistical inference
----------------------
-- estimagic provides asymptotic standard errors for maximum likelihood and method
-  of simulated moments.
-- estimagic also provides bootstrap confidence intervals and standard errors.
-  Of course the bootstrap procedures are parallelized.
-
 
 Structure of the documentation
 ==============================
diff --git a/estimagic/dashboard/monitoring_callbacks.py b/estimagic/dashboard/monitoring_callbacks.py
index 17d4522a8..2df821d95 100644
--- a/estimagic/dashboard/monitoring_callbacks.py
+++ b/estimagic/dashboard/monitoring_callbacks.py
@@ -192,7 +192,7 @@ def _create_params_data_for_update(data, param_ids, clip_bound):
 
     Args:
         data
-        param_ids (list): list of the length of the arrays in data["external_params"]
+        param_ids (list): list of the length of the arrays in data["params"]
         clip_bound (float)
 
     Returns:
@@ -201,8 +201,7 @@ def _create_params_data_for_update(data, param_ids, clip_bound):
 
     """
     params_data = [
-        np.clip(arr, -clip_bound, clip_bound).tolist()
-        for arr in data["external_params"]
+        np.clip(arr, -clip_bound, clip_bound).tolist() for arr in data["params"]
     ]
     params_data = transpose_nested_list(params_data)
     params_data = dict(zip(param_ids, params_data))
diff --git a/estimagic/decorators.py b/estimagic/decorators.py
index ec8982408..169d00938 100644
--- a/estimagic/decorators.py
+++ b/estimagic/decorators.py
@@ -179,3 +179,27 @@ def wrapper_unpack(arg):
         return decorator_unpack(func)
     else:
         return decorator_unpack
+
+
+def switch_sign(func):
+    """Switch sign of all outputs of a function."""
+
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        unswitched = func(*args, **kwargs)
+        if isinstance(unswitched, dict):
+            switched = {key: -val for key, val in unswitched.items()}
+        elif isinstance(unswitched, (tuple, list)):
+            switched = []
+            for entry in unswitched:
+                if isinstance(entry, dict):
+                    switched.append({key: -val for key, val in entry.items()})
+                else:
+                    switched.append(-entry)
+            if isinstance(unswitched, tuple):
+                switched = tuple(switched)
+        else:
+            switched = -unswitched
+        return switched
+
+    return wrapper
diff --git a/estimagic/estimation/estimate_msm.py b/estimagic/estimation/estimate_msm.py
index 20791fd93..6c09c301f 100644
--- a/estimagic/estimation/estimate_msm.py
+++ b/estimagic/estimation/estimate_msm.py
@@ -81,10 +81,6 @@ def estimate_msm(
             disable logging completely by setting it to False, but we highly recommend
             not to do so. The dashboard can only be used when logging is used.
         log_options (dict): Additional keyword arguments to configure the logging.
-            - "suffix": A string that is appended to the default table names, separated
-            by an underscore. You can use this if you want to write the log into an
-            existing database where the default names "optimization_iterations",
-            "optimization_status" and "optimization_problem" are already in use.
             - "fast_logging": A boolean that determines if "unsafe" settings are used
             to speed up write processes to the database. This should only be used for
             very short running criterion functions where the main purpose of the log
@@ -92,10 +88,10 @@ def estimate_msm(
             corrupted database in case of a sudden system shutdown. If one evaluation
             of the criterion function (and gradient if applicable) takes more than
             100 ms, the logging overhead is negligible.
-            - "if_exists": (str) One of "extend", "replace", "raise"
-            - "save_all_arguments": (bool). If True, all arguments to maximize
-              that can be pickled are saved in the log file. Otherwise, only the
-              information needed by the dashboard is saved. Default False.
+            - "if_table_exists": (str) One of "extend", "replace", "raise". What to
+            do if the tables we want to write to already exist. Default "extend".
+            - "if_database_exists": (str): One of "extend", "replace", "raise". What to
+            do if the database we want to write to already exists. Default "extend".
         minimize_options (dict or False): Keyword arguments that govern the numerical
             optimization. Valid entries are all arguments of
             :func:`~estimagic.optimization.optimize.minimize` except for criterion,
diff --git a/estimagic/examples/criterion_functions_optimization_tests.py b/estimagic/examples/criterion_functions.py
similarity index 66%
rename from estimagic/examples/criterion_functions_optimization_tests.py
rename to estimagic/examples/criterion_functions.py
index 8f0ec521a..359360cc4 100644
--- a/estimagic/examples/criterion_functions_optimization_tests.py
+++ b/estimagic/examples/criterion_functions.py
@@ -39,24 +39,6 @@ def trid_gradient(params):
     return 2 * (x - 1) - l1 - l2
 
 
-def trid_pandas_gradient(params):
-    """Calculate gradient of trid function.
-
-    Args:
-        params(pandas.DataFrame): Must have the column "value" containing
-        input values for parameters. Accepts arbitrary numbers of input values.
-
-    Returns:
-        pd.Series: gradient of trid function.
-    """
-    x = params["value"].to_numpy()
-    l1 = np.insert(x, 0, 0)
-    l1 = np.delete(l1, [-1])
-    l2 = np.append(x, 0)
-    l2 = np.delete(l2, [0])
-    return pd.Series(2 * (x - 1) - l1 - l2)
-
-
 def trid_criterion_and_gradient(params):
     """Implement Trid function and calculate gradient.
 
@@ -68,14 +50,9 @@ def trid_criterion_and_gradient(params):
         int: trid function output.
         np.ndarray: gradient of trid function.
     """
-    x = params["value"].to_numpy()
-    l1 = np.insert(x, 0, 0)
-    l1 = np.delete(l1, [-1])
-    l2 = np.append(x, 0)
-    l2 = np.delete(l2, [0])
-    return ((params["value"] - 1) ** 2).sum() - (
-        params["value"][1:] * x[:-1]
-    ).sum(), 2 * (x - 1) - l1 - l2
+    val = trid_scalar_criterion(params)
+    grad = trid_gradient(params)
+    return val, grad
 
 
 def trid_dict_criterion(params):
@@ -106,10 +83,7 @@ def rotated_hyper_ellipsoid_scalar_criterion(params):
     Returns:
         int: Rotated Hyper Ellipsoid function output.
     """
-    val = 0
-    for i in range(len(params["value"])):
-        val += (params["value"][: i + 1] ** 2).sum()
-    return val
+    return rotated_hyper_ellipsoid_contributions(params).sum()
 
 
 def rotated_hyper_ellipsoid_gradient(params):
@@ -126,20 +100,6 @@ def rotated_hyper_ellipsoid_gradient(params):
     return np.arange(2 * len(x), 0, -2) * x
 
 
-def rotated_hyper_ellipsoid_pandas_gradient(params):
-    """Calculate gradient of rotated_hyper_ellipsoid function.
-
-    Args:
-        params(pandas.DataFrame): Must have the column "value" containing
-        input values for parameters. Accepts arbitrary numbers of input values.
-
-    Returns:
-        pd.Series: gradient of rotated hyper ellipsoid function.
-    """
-    x = params["value"].to_numpy()
-    return pd.Series(np.arange(2 * len(x), 0, -2) * x)
-
-
 def rotated_hyper_ellipsoid_criterion_and_gradient(params):
     """Implement Rotated Hyper Ellipsoid function and calculate gradient.
 
@@ -151,11 +111,9 @@ def rotated_hyper_ellipsoid_criterion_and_gradient(params):
         int: Rotated Hyper Ellipsoid function output.
         np.ndarray: gradient of rotated hyper ellipsoid function.
     """
-    val = 0
-    for i in range(len(params["value"])):
-        val += (params["value"][: i + 1] ** 2).sum()
-    x = params["value"].to_numpy()
-    return val, np.arange(2 * len(x), 0, -2) * x
+    val = rotated_hyper_ellipsoid_scalar_criterion(params)
+    grad = rotated_hyper_ellipsoid_gradient(params)
+    return val, grad
 
 
 def rotated_hyper_ellipsoid_contributions(params):
@@ -192,11 +150,8 @@ def rotated_hyper_ellipsoid_dict_criterion(params):
         "root_contributions" (np.ndarray): array with root of contributions of
         function output as elements.
     """
-    out = {
-        "value": rotated_hyper_ellipsoid_scalar_criterion(params),
-        "contributions": rotated_hyper_ellipsoid_contributions(params),
-        "root_contributions": np.sqrt(rotated_hyper_ellipsoid_contributions(params)),
-    }
+    contribs = rotated_hyper_ellipsoid_contributions(params)
+    out = _out_dict_from_contribs(contribs)
     return out
 
 
@@ -211,10 +166,7 @@ def rosenbrock_scalar_criterion(params):
     Returns:
         int: Rosenbrock function output.
     """
-    x = params["value"].to_numpy()
-    r1 = ((x[1:] - x[:-1] ** 2) ** 2).sum() * 100
-    r2 = ((x[:-1] - 1) ** 2).sum()
-    return r1 + r2
+    return rosenbrock_contributions(params).sum()
 
 
 def rosenbrock_gradient(params):
@@ -241,19 +193,6 @@ def rosenbrock_gradient(params):
     return 100 * (4 * (l1 ** 3) + 2 * l2 - 2 * (l3 ** 2) - 4 * (l4 * x)) + 2 * l1 - l5
 
 
-def rosenbrock_pandas_gradient(params):
-    """Calculate gradient of rosenbrock function.
-
-    Args:
-        params(pandas.DataFrame): Must have the column "value" containing
-        input values for parameters. Accepts arbitrary numbers of input values.
-
-    Returns:
-        pd.Series: gradient of rosenbrock function.
-    """
-    return pd.Series(rosenbrock_gradient(params))
-
-
 def rosenbrock_criterion_and_gradient(params):
     """Implement rosenbrock function and calculate gradient.
 
@@ -296,15 +235,145 @@ def rosenbrock_dict_criterion(params):
 
     Returns:
         Dictionary with the following entries:
-        "value" (a scalar float): Rosenbrock function output.
+        "value" (a scalar float): Rosenbrock function value.
+        "contributions" (np.ndarray): array with contributions of function output
+        as elements.
+        "root_contributions" (np.ndarray): array with root of contributions of
+        function output as elements.
+    """
+    contribs = rosenbrock_contributions(params)
+    out = _out_dict_from_contribs(contribs)
+    return out
+
+
+def sos_dict_criterion(params):
+    """Calculate the sum of squares function and compute
+        contributions and root_contributions.
+
+    Args:
+        params(pandas.DataFrame): Must have the column "value" containing
+        input values for parameters. Accepts arbitrary numbers of input values.
+
+    Returns:
+        Dictionary with the following entries:
+        "value" (a scalar float): sum of squares function value.
+        "contributions" (np.ndarray): array with contributions of function output
+        as elements.
+        "root_contributions" (np.ndarray): array with root of contributions of
+        function output as elements.
+
+    """
+    root_contribs = params["value"].to_numpy()
+    out = _out_dict_from_root_contribs(root_contribs)
+    return out
+
+
+def sos_dict_criterion_with_pd_objects(params):
+    """Calculate the sum of squares function and compute
+        contributions and root_contributions as pandas objects.
+
+    Args:
+        params(pandas.DataFrame): Must have the column "value" containing
+        input values for parameters. Accepts arbitrary numbers of input values.
+
+    Returns:
+        Dictionary with the following pandas object entries:
+        "value" (a scalar float): sum of squares function value.
         "contributions" (np.ndarray): array with contributions of function output
         as elements.
         "root_contributions" (np.ndarray): array with root of contributions of
         function output as elements.
+
     """
+    out = sos_dict_criterion(params)
+    out["contributions"] = pd.Series(out["contributions"])
+    out["root_contributions"] = pd.Series(out["root_contributions"])
+
+    return out
+
+
+def sos_scalar_criterion(params):
+    """Calculate the sum of squares."""
+    return (params["value"].to_numpy() ** 2).sum()
+
+
+def sos_gradient(params):
+    """Calculate the gradient of the sum of squares function."""
+    return 2 * params["value"].to_numpy()
+
+
+def sos_jacobian(params):
+    """Calculate the Jacobian of the sum of squares function."""
+    return np.diag(2 * params["value"])
+
+
+def sos_ls_jacobian(params):
+    return np.eye(len(params))
+
+
+def sos_pandas_gradient(params):
+    """Calculate the gradient of the sum of squares function."""
+    return 2 * params["value"]
+
+
+def sos_pandas_jacobian(params):
+    """Calculate the Jacobian of the sum of squares function."""
+    return pd.DataFrame(np.diag(2 * params["value"]))
+
+
+def sos_criterion_and_gradient(params):
+    """Calculate sum of squares criterion value and gradient."""
+    x = params["value"].to_numpy()
+    return (x ** 2).sum(), 2 * x
+
+
+def sos_criterion_and_jacobian(params):
+    """Calculate sum of squares criterion value and Jacobian."""
+    x = params["value"].to_numpy()
+    return {"contributions": x ** 2, "value": (x ** 2).sum()}, np.diag(2 * x)
+
+
+def sos_dict_derivative(params):
+    x = params["value"].to_numpy()
+
+    out = {
+        "value": 2 * x,
+        "contributions": np.diag(2 * x),
+        "root_contributions": np.eye(len(x)),
+    }
+    return out
+
+
+def sos_dict_derivative_with_pd_objects(params):
+    dict_np = sos_dict_derivative(params)
+    out = {
+        "value": pd.Series(dict_np["value"]),
+        "contributions": pd.DataFrame(dict_np["contributions"]),
+        "root_contributions": pd.DataFrame(dict_np["root_contributions"]),
+    }
+    return out
+
+
+def sos_double_dict_criterion_and_derivative_with_pd_objects(params):
+    val = sos_dict_criterion_with_pd_objects(params)
+    deriv = sos_dict_derivative_with_pd_objects(params)
+    return val, deriv
+
+
+def _out_dict_from_root_contribs(root_contribs):
+    contribs = root_contribs ** 2
+    out = {
+        "value": contribs.sum(),
+        "contributions": contribs,
+        "root_contributions": root_contribs,
+    }
+    return out
+
+
+def _out_dict_from_contribs(contribs):
     out = {
-        "value": rosenbrock_scalar_criterion(params),
-        "contributions": rosenbrock_contributions(params),
-        "root_contributions": np.sqrt(rosenbrock_contributions(params)),
+        "value": contribs.sum(),
+        "contributions": contribs,
+        "root_contributions": np.sqrt(contribs),
     }
     return out
diff --git a/estimagic/examples/numdiff_example_functions_np.py b/estimagic/examples/numdiff_functions.py
similarity index 100%
rename from estimagic/examples/numdiff_example_functions_np.py
rename to estimagic/examples/numdiff_functions.py
diff --git a/estimagic/logging/database_utilities.py b/estimagic/logging/database_utilities.py
index 292786068..d38bcff77 100644
--- a/estimagic/logging/database_utilities.py
+++ b/estimagic/logging/database_utilities.py
@@ -104,12 +104,9 @@ def make_optimization_iteration_table(database, first_eval, if_exists="extend"):
 
     columns = [
         Column("rowid", Integer, primary_key=True),
-        Column("external_params", PickleType(pickler=RobustPickler)),
-        Column("internal_params", PickleType(pickler=RobustPickler)),
+        Column("params", PickleType(pickler=RobustPickler)),
         Column("internal_derivative", PickleType(pickler=RobustPickler)),
         Column("timestamp", DATETIME),
-        Column("distance_origin", Float),
-        Column("distance_ones", Float),
         Column("exceptions", String),
         Column("valid", Boolean),
         Column("hash", String),
@@ -143,9 +140,7 @@ def make_optimization_status_table(database, if_exists="extend"):
     database.create_all(database.bind)
 
 
-def make_optimization_problem_table(
-    database, if_exists="extend", save_all_arguments=True
-):
+def make_optimization_problem_table(database, if_exists="extend"):
     table_name = "optimization_problem"
     _handle_existing_table(database, table_name, if_exists)
 
@@ -160,21 +155,9 @@ def make_optimization_problem_table(
         Column("error_handling", String),
         Column("error_penalty", PickleType(pickler=RobustPickler)),
         Column("cache_size", Integer),
+        Column("constraints", PickleType(pickler=RobustPickler)),
     ]
 
-    if save_all_arguments:
-        columns += [
-            Column("criterion", PickleType(pickler=RobustPickler)),
-            Column("criterion_kwargs", PickleType(pickler=RobustPickler)),
-            Column("constraints", PickleType(pickler=RobustPickler)),
-            Column("derivative", PickleType(pickler=RobustPickler)),
-            Column("derivative_kwargs", PickleType(pickler=RobustPickler)),
-            Column("criterion_and_derivative", PickleType(pickler=RobustPickler)),
-            Column(
-                "criterion_and_derivative_kwargs", PickleType(pickler=RobustPickler)
-            ),
-        ]
-
     Table(
         table_name, database, *columns, extend_existing=True, sqlite_autoincrement=True
     )
diff --git a/estimagic/logging/read_log.py b/estimagic/logging/read_log.py
index 85db8f822..b3f84e168 100644
--- a/estimagic/logging/read_log.py
+++ b/estimagic/logging/read_log.py
@@ -64,7 +64,7 @@ def read_optimization_iteration(path_or_database, iteration, include_internals=F
         data = data[0]
 
     params = start_params.copy()
-    params["value"] = data.pop("external_params")
+    params["value"] = data.pop("params")
     data["params"] = params
 
     to_remove = ["distance_origin", "distance_ones"]
diff --git a/estimagic/optimization/check_arguments.py b/estimagic/optimization/check_arguments.py
index 941e46f68..7be6d7c99 100644
--- a/estimagic/optimization/check_arguments.py
+++ b/estimagic/optimization/check_arguments.py
@@ -1,4 +1,5 @@
 import typing
+import warnings
 from pathlib import Path
 
 import pandas as pd
@@ -35,8 +36,17 @@ def check_argument(argument):
     if argument["direction"] not in ["minimize", "maximize"]:
         raise ValueError("diretion must be 'minimize' or 'maximize'")
 
-    if "value" not in argument["params"].columns:
+    parcols = argument["params"].columns
+    if "value" not in parcols:
         raise ValueError("The params DataFrame must contain a 'value' column.")
 
+    if "lower" in parcols and "lower_bounds" not in parcols:
+        msg = "There is a column 'lower' in params. Did you mean 'lower_bounds'?"
+        warnings.warn(msg)
+
+    if "upper" in parcols and "upper_bounds" not in parcols:
+        msg = "There is a column 'upper' in in params. Did you mean 'upper_bounds'?"
+        warnings.warn(msg)
+
     if argument["error_handling"] not in ["raise", "continue"]:
         raise ValueError("error_handling must be 'raise' or 'continue'")
diff --git a/estimagic/optimization/internal_criterion_template.py b/estimagic/optimization/internal_criterion_template.py
index 859c86ddb..bb0edc742 100644
--- a/estimagic/optimization/internal_criterion_template.py
+++ b/estimagic/optimization/internal_criterion_template.py
@@ -134,7 +134,7 @@ def internal_criterion_and_derivative_template(
     to_dos = _determine_to_dos(task, cache_entry, derivative, criterion_and_derivative)
 
     caught_exceptions = []
-    new_criterion, new_derivative = None, None
+    new_criterion, new_derivative, new_external_derivative = None, None, None
     current_params = params.copy()
     external_x = reparametrize_from_internal(x)
     current_params["value"] = external_x
@@ -156,7 +156,9 @@ def func(x):
 
         try:
             derivative_dict = first_derivative(func, x, **options)
-            new_derivative = derivative_dict["derivative"]
+            new_derivative = {
+                algorithm_info["primary_criterion_entry"]: derivative_dict["derivative"]
+            }
             new_criterion = derivative_dict["func_value"]
         except (KeyboardInterrupt, SystemExit):
             raise
@@ -170,9 +172,6 @@ def func(x):
             new_criterion, new_external_derivative = criterion_and_derivative(
                 current_params
             )
-            new_derivative = convert_derivative(
-                external_derivative=new_external_derivative, internal_values=x
-            )
         except (KeyboardInterrupt, SystemExit):
             raise
         except Exception as e:
@@ -194,9 +193,6 @@ def func(x):
         if "derivative" in to_dos:
             try:
                 new_external_derivative = derivative(current_params)
-                new_derivative = convert_derivative(
-                    external_derivative=new_external_derivative, internal_values=x
-                )
             except (KeyboardInterrupt, SystemExit):
                 raise
             except Exception as e:
@@ -204,6 +200,17 @@ def func(x):
                 if "criterion" in cache_entry:
                     raise Exception(DERIVATIVE_ERROR_MESSAGE) from e
 
+    if new_derivative is None and new_external_derivative is not None:
+        if not isinstance(new_external_derivative, dict):
+            new_external_derivative = {
+                algorithm_info["primary_criterion_entry"]: new_external_derivative
+            }
+
+        new_derivative = {
+            k: convert_derivative(v, internal_values=x)
+            for k, v in new_external_derivative.items()
+        }
+
     if caught_exceptions:
         if error_handling == "continue":
             new_criterion, new_derivative = _penalty_and_derivative(
@@ -226,15 +233,13 @@ def func(x):
 
     if (new_criterion is not None or new_derivative is not None) and database:
         _log_new_evaluations(
-            new_criterion,
-            new_derivative,
-            x,
-            external_x,
-            caught_exceptions,
-            x_hash,
-            database,
-            database_path,
-            log_options,
+            new_criterion=new_criterion,
+            new_derivative=new_derivative,
+            external_x=external_x,
+            caught_exceptions=caught_exceptions,
+            database=database,
+            database_path=database_path,
+            log_options=log_options,
         )
 
     res = _get_output_for_optimizer(
@@ -367,44 +372,59 @@ def _check_and_harmonize_criterion_output(output, algorithm_info):
         if not isinstance(output, dict):
             raise ValueError("The output of criterion must be a scalar or dict.")
 
-        if primary not in output:
-            raise ValueError(
-                NO_PRIMARY_MESSAGE.format(algo_name, primary, list(output))
-            )
+        if "contributions" not in output and "root_contributions" in output:
+            output["contributions"] = output["root_contributions"] ** 2
 
         if "value" not in output and "contributions" in output:
             output["value"] = output["contributions"].sum()
 
-        elif "value" not in output and "root_contributions" in output:
-            output["value"] = (output["root_contributions"] ** 2).sum()
-
+        if primary not in output:
+            raise ValueError(
+                NO_PRIMARY_MESSAGE.format(algo_name, primary, list(output))
+            )
     return output
 
 
 def _check_and_harmonize_derivative(derivative, algorithm_info):
     primary = algorithm_info["primary_criterion_entry"]
 
+    if not isinstance(derivative, dict) and derivative is not None:
+        derivative = {primary: derivative}
+
     if derivative is None:
         pass
-    elif primary == "value" and np.atleast_2d(derivative).shape[0] != 1:
-        raise ValueError("The derivative of a scalar optimizer must be a 1d array.")
+    else:
 
-    elif primary in ("contributions", "root_contributions"):
-        if len(derivative.shape) != 2:
+        if primary not in derivative:
             raise ValueError(
-                "The derivative of a n optimizer that exploits a sum or sum of squares "
-                "structure must be a 2d array."
+                "If derivative returns a dict and you use an optimizer that works with "
+                f"{primary}, the derivative dictionary also must contain {primary} "
+                "as a key."
             )
+
+        if "value" in derivative and np.atleast_2d(derivative["value"]).shape[0] != 1:
+            raise ValueError("The derivative of a scalar optimizer must be a 1d array.")
+        if "contributions" in derivative:
+            if len(derivative["contributions"].shape) != 2:
+                raise ValueError(
+                    "The derivative of an optimizer that exploits a sum "
+                    "structure must be a 2d array."
+                )
+        if "root_contributions" in derivative:
+            if len(derivative["root_contributions"].shape) != 2:
+                raise ValueError(
+                    "The derivative of an optimizer that exploits a least squares "
+                    "structure must be a 2d array."
+                )
+
     return derivative
 
 
 def _log_new_evaluations(
     new_criterion,
     new_derivative,
-    x,
     external_x,
     caught_exceptions,
-    x_hash,
     database,
     database_path,
     log_options,
@@ -417,11 +437,8 @@ def _log_new_evaluations(
 
     """
     data = {
-        "external_params": external_x,
-        "internal_params": x,
+        "params": external_x,
         "timestamp": datetime.datetime.now(),
-        "distance_origin": float(np.linalg.norm(x)),
-        "distance_ones": float(np.linalg.norm(x - 1)),
         "valid": True,
     }
 
@@ -458,7 +475,7 @@ def _get_output_for_optimizer(
         crit = crit if direction == "minimize" else -crit
 
     if "derivative" in task:
-        deriv = np.array(new_derivative)
+        deriv = np.array(new_derivative[primary])
         deriv = deriv if direction == "minimize" else -deriv
 
     if task == "criterion_and_derivative":
diff --git a/estimagic/optimization/optimize.py b/estimagic/optimization/optimize.py
index 5130368e3..5cf8bae7c 100644
--- a/estimagic/optimization/optimize.py
+++ b/estimagic/optimization/optimize.py
@@ -1,6 +1,7 @@
 import functools
 import inspect
 import warnings
+from pathlib import Path
 
 import numpy as np
 
@@ -103,10 +104,6 @@ def maximize(
             disable logging completely by setting it to False, but we highly recommend
             not to do so. The dashboard can only be used when logging is used.
         log_options (dict): Additional keyword arguments to configure the logging.
-            - "suffix": A string that is appended to the default table names, separated
-            by an underscore. You can use this if you want to write the log into an
-            existing database where the default names "optimization_iterations",
-            "optimization_status" and "optimization_problem" are already in use.
             - "fast_logging": A boolean that determines if "unsafe" settings are used
             to speed up write processes to the database. This should only be used for
             very short running criterion functions where the main purpose of the log
@@ -114,10 +111,10 @@ def maximize(
             corrupted database in case of a sudden system shutdown. If one evaluation
             of the criterion function (and gradient if applicable) takes more than
             100 ms, the logging overhead is negligible.
-            - "if_exists": (str) One of "extend", "replace", "raise"
-            - "save_all_arguments": (bool). If True, all arguments to maximize
-              that can be pickled are saved in the log file. Otherwise, only the
-              information needed by the dashboard is saved. Default False.
+            - "if_table_exists": (str) One of "extend", "replace", "raise". What to
+            do if the tables we want to write to already exist. Default "extend".
+            - "if_database_exists": (str): One of "extend", "replace", "raise". What to
+            do if the database we want to write to already exists. Default "extend".
         error_handling (str): Either "raise" or "continue". Note that "continue" does
             not absolutely guarantee that no error is raised but we try to handle as
             many errors as possible in that case without aborting the optimization.
@@ -240,10 +237,6 @@ def minimize(
             disable logging completely by setting it to False, but we highly recommend
             not to do so. The dashboard can only be used when logging is used.
         log_options (dict): Additional keyword arguments to configure the logging.
-            - "suffix": A string that is appended to the default table names, separated
-            by an underscore. You can use this if you want to write the log into an
-            existing database where the default names "optimization_iterations",
-            "optimization_status" and "optimization_problem" are already in use.
             - "fast_logging": A boolean that determines if "unsafe" settings are used
             to speed up write processes to the database. This should only be used for
             very short running criterion functions where the main purpose of the log
@@ -251,10 +244,10 @@ def minimize(
             corrupted database in case of a sudden system shutdown. If one evaluation
             of the criterion function (and gradient if applicable) takes more than
             100 ms, the logging overhead is negligible.
-            - "if_exists": (str) One of "extend", "replace", "raise"
-            - "save_all_arguments": (bool). If True, all arguments to minimize
-              that can be pickled are saved in the log file. Otherwise, only the
-              information needed by the dashboard is saved. Default False.
+            - "if_table_exists": (str) One of "extend", "replace", "raise". What to
+            do if the tables we want to write to already exist. Default "extend".
+            - "if_database_exists": (str): One of "extend", "replace", "raise". What to
+            do if the database we want to write to already exists. Default "extend".
         error_handling (str): Either "raise" or "continue". Note that "continue" does
             not absolutely guarantee that no error is raised but we try to handle as
             many errors as possible in that case without aborting the optimization.
@@ -379,10 +372,6 @@ def _optimize(
             disable logging completely by setting it to False, but we highly recommend
             not to do so. The dashboard can only be used when logging is used.
         log_options (dict): Additional keyword arguments to configure the logging.
-            - "suffix": A string that is appended to the default table names, separated
-            by an underscore. You can use this if you want to write the log into an
-            existing database where the default names "optimization_iterations",
-            "optimization_status" and "optimization_problem" are already in use.
             - "fast_logging": A boolean that determines if "unsafe" settings are used
             to speed up write processes to the database. This should only be used for
             very short running criterion functions where the main purpose of the log
@@ -390,10 +379,10 @@ def _optimize(
             corrupted database in case of a sudden system shutdown. If one evaluation
             of the criterion function (and gradient if applicable) takes more than
             100 ms, the logging overhead is negligible.
-            - "if_exists": (str) One of "extend", "replace", "raise"
-            - "save_all_arguments": (bool). If True, all arguments to
-              optimize that can be pickled are saved in the log file. Otherwise, only
-              the information needed by the dashboard is saved. Default False.
+            - "if_table_exists": (str) One of "extend", "replace", "raise". What to
+            do if the tables we want to write to already exist. Default "extend".
+            - "if_database_exists": (str): One of "extend", "replace", "raise". What to
+            do if the database we want to write to already exists. Default "extend".
         error_handling (str): Either "raise" or "continue". Note that "continue" does
             not absolutely guarantee that no error is raised but we try to handle as
             many errors as possible in that case without aborting the optimization.
@@ -645,6 +634,9 @@ def _single_optimize(
     if "solution_criterion" not in res:
         res["solution_criterion"] = criterion(p)
 
+    if direction == "maximize":
+        res["solution_criterion"] = -res["solution_criterion"]
+
     # in the long run we can get some of those from the database if logging was used.
     optional_entries = [
         "solution_derivative",
@@ -701,42 +693,55 @@ def _fill_error_penalty_with_defaults(error_penalty, first_eval, direction):
 
 
 def _create_and_initialize_database(logging, log_options, first_eval, problem_data):
-
     # extract information
-    path = logging
+    path = Path(logging)
     fast_logging = log_options.get("fast_logging", False)
-    if_exists = log_options.get("if_exists", "extend")
-    save_all_arguments = log_options.get("save_all_arguments", False)
+    if_table_exists = log_options.get("if_table_exists", "extend")
+    if_database_exists = log_options.get("if_database_exists", "extend")
+
+    if "if_exists" in log_options and "if_table_exists" not in log_options:
+        warnings.warn("The log_option 'if_exists' was renamed to 'if_table_exists'.")
+
+    if logging.exists():
+        if if_database_exists == "raise":
+            raise FileExistsError(
+                f"The database {logging} already exists and the log_option "
+                "'if_database_exists' is set to 'raise'"
+            )
+        elif if_database_exists == "replace":
+            logging.unlink()
+
     database = load_database(path=path, fast_logging=fast_logging)
 
     # create the optimization_iterations table
     make_optimization_iteration_table(
         database=database,
         first_eval=first_eval,
-        if_exists=if_exists,
+        if_exists=if_table_exists,
     )
 
     # create and initialize the optimization_status table
-    make_optimization_status_table(database, if_exists)
+    make_optimization_status_table(database, if_exists=if_table_exists)
     append_row(
         {"status": "running"}, "optimization_status", database, path, fast_logging
     )
 
     # create_and_initialize the optimization_problem table
-    make_optimization_problem_table(database, if_exists, save_all_arguments)
-    if not save_all_arguments:
-        not_saved = [
-            "criterion",
-            "criterion_kwargs",
-            "constraints",
-            "derivative",
-            "derivative_kwargs",
-            "criterion_and_derivative",
-            "criterion_and_derivative_kwargs",
-        ]
-        problem_data = {
-            key: val for key, val in problem_data.items() if key not in not_saved
-        }
+    make_optimization_problem_table(database, if_exists=if_table_exists)
+
+    not_saved = [
+        "criterion",
+        "criterion_kwargs",
+        "constraints",
+        "derivative",
+        "derivative_kwargs",
+        "criterion_and_derivative",
+        "criterion_and_derivative_kwargs",
+    ]
+    problem_data = {
+        key: val for key, val in problem_data.items() if key not in not_saved
+    }
+
     append_row(problem_data, "optimization_problem", database, path, fast_logging)
 
     return database
diff --git a/estimagic/optimization/scipy_optimizers.py b/estimagic/optimization/scipy_optimizers.py
index bc08af062..ff7c58632 100644
--- a/estimagic/optimization/scipy_optimizers.py
+++ b/estimagic/optimization/scipy_optimizers.py
@@ -412,8 +412,6 @@ def scipy_powell(
 def scipy_bfgs(
     criterion_and_derivative,
     x,
-    lower_bounds,
-    upper_bounds,
     *,
     convergence_absolute_gradient_tolerance=CONVERGENCE_ABSOLUTE_GRADIENT_TOLERANCE,
     stopping_max_iterations=STOPPING_MAX_ITERATIONS,
@@ -1182,6 +1180,7 @@ def _scipy_least_squares(
 
     algo_info = DEFAULT_ALGO_INFO.copy()
     algo_info["name"] = f"scipy_ls_{method}"
+    algo_info["primary_criterion_entry"] = "root_contributions"
     func = functools.partial(
         criterion_and_derivative,
         task="criterion",
diff --git a/estimagic/parameters/consolidate_constraints.py b/estimagic/parameters/consolidate_constraints.py
index 1f3754939..58271ee34 100644
--- a/estimagic/parameters/consolidate_constraints.py
+++ b/estimagic/parameters/consolidate_constraints.py
@@ -509,7 +509,7 @@ def _rescale_linear_constraints(weights, rhs):
         new_rhs (pd.DataFrame)
 
     """
-    first_nonzero = weights.replace(0, np.nan).bfill(1).iloc[:, 0]
+    first_nonzero = weights.replace(0, np.nan).bfill(axis=1).iloc[:, 0]
     scaling_factor = 1 / first_nonzero.to_numpy().reshape(-1, 1)
     new_weights = scaling_factor * weights
     scaled_rhs = scaling_factor * rhs
diff --git a/tox.ini b/tox.ini
index 19fe52de8..f1baa7703 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,6 @@ conda_deps =
     joblib
     cloudpickle
     numpy
-    petsc4py >= 3.11
     pandas
     pytest
     pytest-cov


Test patch:
diff --git a/estimagic/tests/dashboard/db1.db b/estimagic/tests/dashboard/db1.db
index ccf900be7..529f3ead9 100644
Binary files a/estimagic/tests/dashboard/db1.db and b/estimagic/tests/dashboard/db1.db differ
diff --git a/estimagic/tests/dashboard/db2.db b/estimagic/tests/dashboard/db2.db
index ccf900be7..b96ca0a16 100644
Binary files a/estimagic/tests/dashboard/db2.db and b/estimagic/tests/dashboard/db2.db differ
diff --git a/estimagic/tests/dashboard/test_monitoring_app.py b/estimagic/tests/dashboard/test_monitoring_app.py
index 42caf432f..db9fbb056 100644
--- a/estimagic/tests/dashboard/test_monitoring_app.py
+++ b/estimagic/tests/dashboard/test_monitoring_app.py
@@ -9,6 +9,7 @@
 from bokeh.models import ColumnDataSource
 
 import estimagic.dashboard.monitoring_app as monitoring
+from estimagic.optimization.optimize import minimize
 
 
 def test_monitoring_app():
@@ -16,9 +17,22 @@ def test_monitoring_app():
     doc = Document()
     database_name = "test_db"
     current_dir_path = Path(__file__).resolve().parent
+    db_path = current_dir_path / "db2.db"
+
+    needs_update = False
+    if needs_update:
+        params = pd.DataFrame()
+        params["value"] = [1, 2, 3]
+        minimize(
+            criterion=lambda params: params["value"] @ params["value"],
+            params=params,
+            algorithm="scipy_neldermead",
+            logging=db_path,
+        )
+
     session_data = {
         "last_retrieved": 0,
-        "database_path": current_dir_path / "db1.db",
+        "database_path": db_path,
     }
     updating_options = {
         "rollover": 10_000,
diff --git a/estimagic/tests/dashboard/test_monitoring_callbacks.py b/estimagic/tests/dashboard/test_monitoring_callbacks.py
index 0f784bec9..1a820f37a 100644
--- a/estimagic/tests/dashboard/test_monitoring_callbacks.py
+++ b/estimagic/tests/dashboard/test_monitoring_callbacks.py
@@ -1,7 +1,4 @@
-from pathlib import Path
-
 import numpy as np
-import pandas as pd
 from bokeh.models import ColumnDataSource
 from bokeh.models import Toggle
 from bokeh.plotting import figure
@@ -10,8 +7,7 @@
 from estimagic.dashboard.monitoring_callbacks import _reset_column_data_sources
 from estimagic.dashboard.monitoring_callbacks import _switch_to_linear_scale
 from estimagic.dashboard.monitoring_callbacks import _switch_to_log_scale
-from estimagic.dashboard.monitoring_callbacks import _update_monitoring_tab
-from estimagic.logging.database_utilities import load_database
+
 
 PARAM_IDS = ["a", "b", "c", "d", "e"]
 
@@ -44,74 +40,12 @@ def test_switch_to_linear_scale():
     assert button.button_type == "default"
 
 
-def test_update_monitoring_tab():
-    # note: this test database does not include None in the value column.
-    # it has only 7 entries.
-    db_path = Path(__file__).parent / "db1.db"
-    database = load_database(metadata=None, path=db_path)
-
-    crit_data = {"iteration": [3, 5], "criterion": [-10, -10]}
-    criterion_cds = ColumnDataSource(crit_data)
-
-    param_data = {f"p{i}": [i, i, i] for i in range(6)}
-    param_data["iteration"] = [3, 4, 5]
-    plotted_param_data = {
-        k: v for k, v in param_data.items() if k in ["p0", "p2", "p4", "iteration"]
-    }
-    param_cds = ColumnDataSource(plotted_param_data)
-
-    start_params = pd.DataFrame()
-    start_params["group"] = ["g1", "g1", None, None, "g2", "g2"]
-    start_params["id"] = [f"p{i}" for i in range(6)]
-
-    session_data = {"last_retrieved": 5}
-    tables = []  # not used
-    rollover = 500
-    update_chunk = 5
-
-    expected_crit_data = {
-        "iteration": [3, 5, 6, 7],
-        "criterion": [-10, -10] + [3.371916994681647e-18, 3.3306686770405823e-18],
-    }
-
-    expected_param_data = plotted_param_data.copy()
-    expected_param_data["iteration"] += [6, 7]
-    expected_param_data["p0"] += [
-        -7.82732387e-10,
-        -7.45058016e-10,
-    ]
-    expected_param_data["p2"] += [
-        -7.50570405e-10,
-        -7.45058015e-10,
-    ]
-    expected_param_data["p4"] += [
-        -7.44958198e-10,
-        -7.45058015e-10,
-    ]
-
-    _update_monitoring_tab(
-        database=database,
-        criterion_cds=criterion_cds,
-        param_cds=param_cds,
-        session_data=session_data,
-        tables=tables,
-        rollover=rollover,
-        start_params=start_params,
-        update_chunk=update_chunk,
-        stride=1,
-    )
-
-    assert session_data["last_retrieved"] == 7
-    assert criterion_cds.data == expected_crit_data
-    assert param_cds.data == expected_param_data
-
-
 def test_create_params_data_for_update():
     param_ids = PARAM_IDS
 
     data = {
         "rowid": [1, 2, 3, 4, 5],
-        "external_params": [
+        "params": [
             np.array([0.47, 0.22, -0.46, 0.0, 2.0]),
             np.array([0.56, 0.26, 0.48, -0.30, 1.69]),
             np.array([0.50, 0.24, -0.15, -0.10, 1.89]),
diff --git a/estimagic/tests/dashboard/test_plot_functions.py b/estimagic/tests/dashboard/test_plot_functions.py
deleted file mode 100644
index c629f9c67..000000000
--- a/estimagic/tests/dashboard/test_plot_functions.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Test helper functions for the dashboard."""
-import webbrowser
-
-import pytest
-from bokeh.io import output_file
-from bokeh.io import save
-from bokeh.models import ColumnDataSource
-
-import estimagic.dashboard.plot_functions as plot_functions
-
-
-def test_create_styled_figure():
-    plot_functions.create_styled_figure("Hello World")
-
-
-# not testing find_free_port
-
-
-@pytest.mark.slow
-def test_plot_time_series_with_large_initial_values():
-    cds = ColumnDataSource({"y": [2e17, 1e16, 1e5], "x": [1, 2, 3]})
-    title = "Are large initial values shown?"
-    fig = plot_functions.plot_time_series(
-        data=cds, y_keys=["y"], x_name="x", title=title, plot_width=1000
-    )
-    title = "Test _plot_time_series can handle large initial values."
-    output_file("time_series_initial_value.html", title=title)
-    path = save(obj=fig)
-    webbrowser.open_new_tab("file://" + path)
diff --git a/estimagic/tests/differentiation/test_derivatives.py b/estimagic/tests/differentiation/test_derivatives.py
index 88064d059..f71b6d9c0 100644
--- a/estimagic/tests/differentiation/test_derivatives.py
+++ b/estimagic/tests/differentiation/test_derivatives.py
@@ -16,10 +16,10 @@
 from estimagic.differentiation.derivatives import _nan_skipping_batch_evaluator
 from estimagic.differentiation.derivatives import _select_minimizer_along_axis
 from estimagic.differentiation.derivatives import first_derivative
-from estimagic.examples.numdiff_example_functions_np import logit_loglike
-from estimagic.examples.numdiff_example_functions_np import logit_loglike_gradient
-from estimagic.examples.numdiff_example_functions_np import logit_loglikeobs
-from estimagic.examples.numdiff_example_functions_np import logit_loglikeobs_jacobian
+from estimagic.examples.numdiff_functions import logit_loglike
+from estimagic.examples.numdiff_functions import logit_loglike_gradient
+from estimagic.examples.numdiff_functions import logit_loglikeobs
+from estimagic.examples.numdiff_functions import logit_loglikeobs_jacobian
 from estimagic.utilities import namedtuple_from_kwargs
 
 
diff --git a/estimagic/tests/examples/test_criterion_functions_optimization.py b/estimagic/tests/examples/test_criterion_functions.py
similarity index 69%
rename from estimagic/tests/examples/test_criterion_functions_optimization.py
rename to estimagic/tests/examples/test_criterion_functions.py
index 68e66290e..45391d071 100644
--- a/estimagic/tests/examples/test_criterion_functions_optimization.py
+++ b/estimagic/tests/examples/test_criterion_functions.py
@@ -4,47 +4,38 @@
 from numpy.testing import assert_allclose
 from numpy.testing import assert_array_equal
 
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rosenbrock_criterion_and_gradient,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rosenbrock_dict_criterion,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rosenbrock_gradient,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rosenbrock_scalar_criterion,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rotated_hyper_ellipsoid_criterion_and_gradient,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rotated_hyper_ellipsoid_dict_criterion,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rotated_hyper_ellipsoid_gradient,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     rotated_hyper_ellipsoid_scalar_criterion,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     trid_criterion_and_gradient,
 )
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import (
     trid_dict_criterion,
 )
-from estimagic.examples.criterion_functions_optimization_tests import trid_gradient
-from estimagic.examples.criterion_functions_optimization_tests import (
-    trid_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
+from estimagic.examples.criterion_functions import trid_gradient
+from estimagic.examples.criterion_functions import (
     trid_scalar_criterion,
 )
 
@@ -69,12 +60,6 @@ def input_params():
     "rosenbrock": rosenbrock_gradient,
 }
 
-criterion_pandas_gradient = {
-    "trid": trid_pandas_gradient,
-    "rhe": rotated_hyper_ellipsoid_pandas_gradient,
-    "rosenbrock": rosenbrock_pandas_gradient,
-}
-
 criterion_and_gradient = {
     "trid": trid_criterion_and_gradient,
     "rhe": rotated_hyper_ellipsoid_criterion_and_gradient,
@@ -130,13 +115,6 @@ def test_criterion_gradient(input_params, crit):
     assert_array_equal(out, gradient_output[crit])
 
 
-@pytest.mark.parametrize("crit", crit_list)
-def test_criterion_pandas_gradient(input_params, crit):
-    out = criterion_pandas_gradient[crit](input_params)
-    assert isinstance(out, pd.Series)
-    assert_array_equal(out, gradient_output[crit])
-
-
 @pytest.mark.parametrize("crit", crit_list)
 def test_criterion_and_gradient(input_params, crit):
     out = criterion_and_gradient[crit](input_params)
diff --git a/estimagic/tests/inference/test_msm_covs.py b/estimagic/tests/inference/test_msm_covs.py
index a55da62fa..e4c12042b 100644
--- a/estimagic/tests/inference/test_msm_covs.py
+++ b/estimagic/tests/inference/test_msm_covs.py
@@ -22,7 +22,7 @@
 
 
 @pytest.mark.parametrize("jac, moments_cov", test_cases)
-def test_cov_sandwich_and_cov_optimal_are_equivalent_in_special_case(jac, moments_cov):
+def test_cov_robust_and_cov_optimal_are_equivalent_in_special_case(jac, moments_cov):
     weights = np.linalg.inv(moments_cov)
     if isinstance(moments_cov, pd.DataFrame):
         weights = pd.DataFrame(
diff --git a/estimagic/tests/logging/test_database_utilities.py b/estimagic/tests/logging/test_database_utilities.py
index 03c3ba8bb..86dd1628a 100644
--- a/estimagic/tests/logging/test_database_utilities.py
+++ b/estimagic/tests/logging/test_database_utilities.py
@@ -21,11 +21,8 @@
 @pytest.fixture
 def iteration_data():
     data = {
-        "external_params": np.ones(1),
-        "internal_params": np.ones(1),
+        "params": np.ones(1),
         "timestamp": datetime(year=2020, month=4, day=9, hour=12, minute=41, second=1),
-        "distance_origin": 1.0,
-        "distance_ones": 0.0,
         "value": 5.0,
     }
     return data
@@ -35,15 +32,10 @@ def iteration_data():
 def problem_data():
     data = {
         "direction": "maximize",
-        "criterion": np.sum,
         "params": np.arange(3),
         "algorithm": "bla",
         "constraints": [{"type": "bla"}],
         "algo_options": None,
-        "derivative": None,
-        "derivative_kwargs": None,
-        "criterion_and_derivative": None,
-        "criterion_and_derivative_kwargs": None,
         "numdiff_options": {},
         "log_options": {"fast_logging": False},
     }
@@ -84,11 +76,9 @@ def test_optimization_iteration_table_scalar(tmp_path, iteration_data):
     assert isinstance(res, list) and isinstance(res[0], dict)
     res = res[0]
     assert res["rowid"] == 1
-    assert res["internal_derivative"] is None
-    for key in ["internal_params", "external_params"]:
-        assert_array_equal(res[key], iteration_data[key])
+    assert_array_equal(res["params"], iteration_data["params"])
 
-    for key in ["distance_ones", "distance_origin", "value", "timestamp"]:
+    for key in ["value", "timestamp"]:
         assert res[key] == iteration_data[key]
 
 
diff --git a/estimagic/tests/logging/test_read_log.py b/estimagic/tests/logging/test_read_log.py
index 86fca6e39..cb168f940 100644
--- a/estimagic/tests/logging/test_read_log.py
+++ b/estimagic/tests/logging/test_read_log.py
@@ -29,9 +29,9 @@ def test_read_optimization_iteration(tmp_path):
     # add the optimization_iterations table
     make_optimization_iteration_table(database, first_eval={"output": 0.5})
     iteration_data = [
-        {"external_params": np.array([0])},
-        {"external_params": np.array([1])},
-        {"external_params": np.array([2])},
+        {"params": np.array([0])},
+        {"params": np.array([1])},
+        {"params": np.array([2])},
     ]
 
     for data in iteration_data:
diff --git a/estimagic/tests/optimization/test_all_algorithms_with_sum_of_squares.py b/estimagic/tests/optimization/test_all_algorithms_with_sum_of_squares.py
deleted file mode 100644
index 14c3f8f30..000000000
--- a/estimagic/tests/optimization/test_all_algorithms_with_sum_of_squares.py
+++ /dev/null
@@ -1,729 +0,0 @@
-"""Test the external interface for optimization with for all algorithms sos.
-
-sum of squares is abbreviated as sos throughout the module.
-
-"""
-import functools
-import warnings
-from itertools import product
-
-import numpy as np
-import pandas as pd
-import pytest
-from numpy.testing import assert_allclose
-
-from estimagic.config import IS_DFOLS_INSTALLED
-from estimagic.config import IS_PETSC4PY_INSTALLED
-from estimagic.config import IS_PYBOBYQA_INSTALLED
-from estimagic.optimization import AVAILABLE_ALGORITHMS
-from estimagic.optimization.optimize import maximize
-from estimagic.optimization.optimize import minimize
-
-BOUNDS_FREE_ALGORITHMS = [
-    "scipy_neldermead",
-    "scipy_conjugate_gradient",
-    "scipy_bfgs",
-    "scipy_newton_cg",
-    "scipy_cobyla",
-]
-
-BOUNDS_SUPPORTING_ALGORITHMS = [
-    alg for alg in AVAILABLE_ALGORITHMS if alg not in BOUNDS_FREE_ALGORITHMS
-]
-
-IMPRECISE_ALGOS = [
-    "scipy_powell",
-    "scipy_truncated_newton",
-    "scipy_trust_constr",
-    "scipy_ls_trf",
-    "scipy_ls_dogbox",
-]
-
-
-def _skip_tests_with_missing_dependencies(test_cases):
-    """Skip tests involving optimizers whose dependencies could not be found."""
-    new_test_cases = []
-    for test_case in test_cases:
-        needs_skipping, reason = _get_skipping_info(test_case)
-        if needs_skipping:
-            test_case = pytest.param(
-                *test_case,
-                marks=pytest.mark.skip(reason=reason),
-            )
-
-        new_test_cases.append(test_case)
-
-    return new_test_cases
-
-
-def _get_skipping_info(test_case):
-    installation_info = {
-        "tao_": IS_PETSC4PY_INSTALLED,
-        "nag_pybobyqa": IS_PYBOBYQA_INSTALLED,
-        "nag_dfols": IS_DFOLS_INSTALLED,
-    }
-
-    reasons = {
-        "tao_": "petsc4py is not installed",
-        "nag_pybobyqa": "pybobyqa is not installed",
-        "nag_dfols": "dfols is not installed",
-    }
-
-    algo_name = test_case[0]
-    needs_skipping = False
-    reason = None
-    for substring, is_installed in installation_info.items():
-        if algo_name.startswith(substring) and not is_installed:
-            needs_skipping = True
-            reason = reasons[substring]
-
-    return needs_skipping, reason
-
-
-# ======================================================================================
-# Define example functions
-# ======================================================================================
-
-
-def sos_dict_criterion(params):
-    out = {
-        "value": (params["value"].to_numpy() ** 2).sum(),
-        "contributions": params["value"].to_numpy() ** 2,
-        "root_contributions": params["value"].to_numpy(),
-    }
-    return out
-
-
-def sos_dict_criterion_with_pd_objects(params):
-    out = {
-        "value": (params["value"] ** 2).sum(),
-        "contributions": params["value"] ** 2,
-        "root_contributions": params["value"],
-    }
-    return out
-
-
-def sos_scalar_criterion(params):
-    return (params["value"].to_numpy() ** 2).sum()
-
-
-def sos_gradient(params):
-    return 2 * params["value"].to_numpy()
-
-
-def sos_jacobian(params):
-    return np.diag(2 * params["value"])
-
-
-def sos_pandas_gradient(params):
-    return 2 * params["value"]
-
-
-def sos_pandas_jacobian(params):
-    return pd.DataFrame(np.diag(2 * params["value"]))
-
-
-def sos_criterion_and_gradient(params):
-    x = params["value"].to_numpy()
-    return (x ** 2).sum(), 2 * x
-
-
-def sos_criterion_and_jacobian(params):
-    x = params["value"].to_numpy()
-    return {"contributions": x ** 2, "value": (x ** 2).sum()}, np.diag(2 * x)
-
-
-# ======================================================================================
-# Other helper functions for tests
-# ======================================================================================
-
-
-def get_test_cases_for_algorithm(algorithm):
-    """Generate list of all possible argument combinations for algorithm."""
-    is_least_squares = algorithm in [
-        "tao_pounders",
-        "nag_dfols",
-        "scipy_ls_trf",
-        "scipy_ls_dogbox",
-    ]
-    is_sum = algorithm in ["bhhh"]
-    is_scalar = not (is_least_squares or is_sum)
-
-    directions = ["minimize"] if is_least_squares else ["maximize", "minimize"]
-
-    crit_funcs = [sos_dict_criterion]
-    if is_scalar:
-        crit_funcs.append(sos_scalar_criterion)
-
-    if is_scalar:
-        derivatives = [sos_gradient, sos_pandas_gradient, None]
-    elif is_sum:
-        derivatives = [sos_jacobian, sos_pandas_jacobian, None]
-    else:
-        derivatives = [None]
-
-    if is_scalar:
-        crit_and_derivs = [sos_criterion_and_gradient, None]
-    elif is_sum:
-        crit_and_derivs = [sos_criterion_and_jacobian, None]
-    else:
-        crit_and_derivs = [None]
-
-    prod_list = [directions, crit_funcs, derivatives, crit_and_derivs]
-
-    test_cases = []
-    for direction, crit, deriv, c_and_d in product(*prod_list):
-        if direction == "maximize":
-            case = (
-                algorithm,
-                direction,
-                switch_sign(crit),
-                switch_sign(deriv),
-                switch_sign(c_and_d),
-            )
-        else:
-            case = (algorithm, direction, crit, deriv, c_and_d)
-        test_cases.append(case)
-    return test_cases
-
-
-def switch_sign(func):
-    if func is None:
-        wrapper = None
-    else:
-
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            unswitched = func(*args, **kwargs)
-            if isinstance(unswitched, dict):
-                switched = {key: -val for key, val in unswitched.items()}
-            elif isinstance(unswitched, tuple):
-                switched = []
-                for entry in unswitched:
-                    if isinstance(entry, dict):
-                        switched.append({key: -val for key, val in entry.items()})
-                    else:
-                        switched.append(-entry)
-                switched = tuple(switched)
-            else:
-                switched = -unswitched
-            return switched
-
-    return wrapper
-
-
-# ======================================================================================
-# Actual tests
-# ======================================================================================
-
-test_cases = []
-for alg in AVAILABLE_ALGORITHMS:
-    test_cases += get_test_cases_for_algorithm(alg)
-test_cases = _skip_tests_with_missing_dependencies(test_cases)
-
-
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_without_constraints(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=np.ones((2, 1)), columns=["value"])
-    params["lower_bound"] = -np.inf
-    params["upper_bound"] = np.inf
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        log_options={"save_all_arguments": False},
-    )
-
-    assert res["success"], f"{algo} did not converge."
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(),
-        np.zeros(2),
-        atol=atol,
-        rtol=0,
-    )
-
-
-# constraints are only applicable to algorithms that support bounds
-bound_cases = []
-for alg in BOUNDS_SUPPORTING_ALGORITHMS:
-    bound_cases += get_test_cases_for_algorithm(alg)
-bound_cases = _skip_tests_with_missing_dependencies(bound_cases)
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_binding_bounds(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=np.array([5, 8, 8, 8, -5]), columns=["value"])
-    # the truncated_newton's line search fails if the lower bound of the first
-    # parameter is set to 1.0. With 2.0 truncated_newton also converges.
-    params["lower_bound"] = [2, -10, -10, -10, -10]
-    params["upper_bound"] = [10, 10, 10, 10, -1]
-    expected = np.array([2, 0, 0, 0, -1])
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_fixed_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [7.5], [-1], [-2], [1]], columns=["value"])
-    params["lower_bound"] = [-10, -10, -10, -10, -10]
-    params["upper_bound"] = [10, 10, 10, 10, 10]
-
-    constraints = [{"loc": [1, 3], "type": "fixed", "value": [7.5, -2]}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0, 7.5, 0, -2, 0.0])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_equality_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [7.5], [-1], [-2], [1]], columns=["value"])
-    params["lower_bound"] = [-10, -10, -10, -10, -10]
-    params["upper_bound"] = -params["lower_bound"]
-
-    constraints = [{"loc": [0, 4], "type": "equality"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_pairwise_equality_constraint(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[1], [2], [1], [2], [1]], columns=["value"])
-    params["lower_bound"] = [-10, -10, -10, -10, -10.0]
-    params["upper_bound"] = [10, 10, 10, 10, 10]
-
-    constraints = [{"locs": [[0, 1], [2, 3]], "type": "pairwise_equality"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_increasing_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [2], [3], [2], [1]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2], "type": "increasing"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_decreasing_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [2], [3], [2], [1]], columns=["value"])
-
-    constraints = [{"loc": [2, 3, 4], "type": "decreasing"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_linear_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [2], [0.1], [0.3], [0.6]], columns=["value"])
-
-    constraints = [{"loc": [2, 3, 4], "type": "linear", "value": 1, "weights": 1}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    with warnings.catch_warnings():
-        # in the case of the trust_constr algorithm we want to ignore the warning
-        # that the approximated function appears linear for the tests.
-        warnings.simplefilter(action="ignore", category=UserWarning)
-
-        res = optimize_func(
-            criterion=crit,
-            params=params,
-            algorithm=algo,
-            derivative=deriv,
-            criterion_and_derivative=crit_and_deriv,
-            constraints=constraints,
-        )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0, 0, 1 / 3, 1 / 3, 1 / 3])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_probability_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[0.3], [0.0], [0.6], [0.1], [5]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2, 3], "type": "probability"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0.25, 0.25, 0.25, 0.25, 0])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_covariance_constraint_no_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[1], [0.1], [2], [3], [2]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2], "type": "covariance"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_covariance_constraint_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    # Note: Robust bounds only have an effect for 3x3 covariance matrices or larger
-    params = pd.DataFrame(data=[[1], [0.1], [2], [0.2], [0.3], [3]], columns=["value"])
-
-    constraints = [
-        {
-            "loc": [0, 1, 2, 3, 4, 5],
-            "type": "covariance",
-            "bounds_distance": 0.1,
-            "robust_bounds": True,
-        }
-    ]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0.1, 0, 0.1, 0, 0, 0.1])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_sdcorr_constraint_no_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[1], [2], [0.1], [3], [2]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2], "type": "sdcorr"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.zeros(5)
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", bound_cases)
-def test_with_sdcorr_constraint_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    # Note: Robust bounds only have an effect for 3x3 sdcorr matrices or larger
-    params = pd.DataFrame(data=[[1], [2], [3], [0.1], [0.2], [0.3]], columns=["value"])
-
-    constraints = [
-        {
-            "loc": [0, 1, 2, 3, 4, 5],
-            "type": "sdcorr",
-            "bounds_distance": 0.1,
-            "robust_bounds": True,
-        }
-    ]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    # filter ignored algo_options warnings to pass the same tolerances to all algorithms
-    with warnings.catch_warnings():
-        hess_warn_msg = (
-            "delta_grad == 0.0. Check if the approximated function is linear. "
-            + "If the function is linear better results can be obtained by defining "
-            + "the Hessian as zero instead of using quasi-Newton approximations."
-        )
-        warnings.filterwarnings("ignore", message=hess_warn_msg)
-
-        res = optimize_func(
-            criterion=crit,
-            params=params,
-            algorithm=algo,
-            derivative=deriv,
-            criterion_and_derivative=crit_and_deriv,
-            constraints=constraints,
-        )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0.1, 0.1, 0.1, 0, 0, 0.0])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize(
-    "algo, direction, crit, deriv, crit_and_deriv", bound_cases
-)  # ================================
-def test_with_decreasing_constraint_and_fixes(
-    algo, direction, crit, deriv, crit_and_deriv
-):
-    params = pd.DataFrame(data=[[1], [4], [4], [2], [1]], columns=["value"])
-
-    constraints = [
-        {"loc": [1, 2, 3, 4], "type": "decreasing"},
-        {"loc": 2, "type": "fixed", "value": 4},
-    ]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0, 4, 4, 0, 0])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize(
-    "algo, direction, crit, deriv, crit_and_deriv", bound_cases
-)  # ================================
-def test_with_increasing_constraint_and_fixes(
-    algo, direction, crit, deriv, crit_and_deriv
-):
-    params = pd.DataFrame(data=[[1], [2], [3], [4], [1]], columns=["value"])
-
-    constraints = [
-        {"loc": [0, 1, 2, 3], "type": "increasing"},
-        {"loc": 2, "type": "fixed", "value": 3},
-    ]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-    )
-
-    assert res["success"], f"{algo} did not converge."
-
-    expected = np.array([0, 0, 3, 3, 0])
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-def test_scipy_lbfgsb_actually_calls_criterion_and_derivative():
-    params = pd.DataFrame(data=np.ones((10, 1)), columns=["value"])
-
-    def raising_crit_and_deriv(params):
-        raise Exception()
-
-    with pytest.raises(Exception):
-        minimize(
-            criterion=sos_scalar_criterion,
-            params=params,
-            algorithm="scipy_lbfgsb",
-            criterion_and_derivative=raising_crit_and_deriv,
-        )
diff --git a/estimagic/tests/optimization/test_criterion_versions.py b/estimagic/tests/optimization/test_criterion_versions.py
new file mode 100644
index 000000000..72d51bc1a
--- /dev/null
+++ b/estimagic/tests/optimization/test_criterion_versions.py
@@ -0,0 +1,67 @@
+"""Test different versions of specifying a criterion functions.
+
+Here we want to take:
+- Few representative algorithms (derivative based, derivative free, least squares)
+- One basic criterion function (sum of squares)
+- Many criterion versions (dict output, pandas output, scalar output)
+
+"""
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.config import IS_DFOLS_INSTALLED
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.examples.criterion_functions import sos_dict_criterion_with_pd_objects
+from estimagic.examples.criterion_functions import sos_scalar_criterion
+from estimagic.optimization.optimize import minimize
+
+algorithms = ["scipy_lbfgsb", "scipy_neldermead"]
+if IS_DFOLS_INSTALLED:
+    algorithms.append("nag_dfols")
+
+ls_algorithms = {"nag_dfols"}
+
+
+criterion_functions = {
+    "sos_dict_criterion": sos_dict_criterion,
+    "sos_scalar_criterion": sos_scalar_criterion,
+    "sos_dict_criterion_with_pd_objects": sos_dict_criterion_with_pd_objects,
+}
+
+
+valid_cases = []
+invalid_cases = []
+for algo in algorithms:
+    for name, crit in criterion_functions.items():
+        if algo in ls_algorithms and "dict" not in name:
+            invalid_cases.append((crit, algo))
+        else:
+            valid_cases.append((crit, algo))
+
+
+@pytest.mark.parametrize("criterion, algorithm", valid_cases)
+def test_valid_criterion_versions(criterion, algorithm):
+    start_params = pd.DataFrame()
+    start_params["value"] = [1, 2, 3]
+    res = minimize(
+        criterion=criterion,
+        params=start_params,
+        algorithm=algorithm,
+    )
+
+    aaae(res["solution_params"]["value"].to_numpy(), np.zeros(3), decimal=4)
+
+
+@pytest.mark.parametrize("criterion, algorithm", invalid_cases)
+def test_invalid_criterion_versions(criterion, algorithm):
+    start_params = pd.DataFrame()
+    start_params["value"] = [1, 2, 3]
+
+    with pytest.raises(ValueError):
+        minimize(
+            criterion=criterion,
+            params=start_params,
+            algorithm=algorithm,
+        )
diff --git a/estimagic/tests/optimization/test_derivative_versions.py b/estimagic/tests/optimization/test_derivative_versions.py
new file mode 100644
index 000000000..770d5d48a
--- /dev/null
+++ b/estimagic/tests/optimization/test_derivative_versions.py
@@ -0,0 +1,145 @@
+"""Test different versions of specifying a criterion functions.
+
+Here we want to take:
+- Few representative algorithms (derivative based, derivative free, least squares)
+- One basic criterion function (sum of squares)
+- Many ways of specifying derivatives or not.
+- Maximize and minimize since this might make a difference
+
+"""
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.decorators import switch_sign
+from estimagic.examples.criterion_functions import sos_criterion_and_gradient
+from estimagic.examples.criterion_functions import sos_criterion_and_jacobian
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.examples.criterion_functions import sos_dict_derivative
+from estimagic.examples.criterion_functions import sos_dict_derivative_with_pd_objects
+from estimagic.examples.criterion_functions import (
+    sos_double_dict_criterion_and_derivative_with_pd_objects,
+)
+from estimagic.examples.criterion_functions import sos_gradient
+from estimagic.examples.criterion_functions import sos_jacobian
+from estimagic.examples.criterion_functions import sos_pandas_gradient
+from estimagic.examples.criterion_functions import sos_pandas_jacobian
+from estimagic.optimization.optimize import maximize
+from estimagic.optimization.optimize import minimize
+
+algorithms = ["scipy_lbfgsb", "scipy_ls_dogbox", "scipy_neldermead"]
+
+ls_algorithms = {"scipy_ls_dogbox"}
+
+
+scalar_derivatives = [None, sos_gradient, sos_pandas_gradient]
+
+scalar_criterion_and_derivtives = [None, sos_criterion_and_gradient]
+
+ls_derivatives = [None, sos_jacobian, sos_pandas_jacobian]
+
+
+ls_criterion_and_derivatives = [sos_criterion_and_jacobian]
+
+dict_derivatives = [sos_dict_derivative, None, sos_dict_derivative_with_pd_objects]
+
+dict_criterion_and_derivatives = [
+    None,
+    sos_double_dict_criterion_and_derivative_with_pd_objects,
+]
+
+
+valid_cases = []
+invalid_cases = []
+for algo in algorithms:
+    if algo in ls_algorithms:
+        for deriv in ls_derivatives:
+            for crit_and_deriv in ls_criterion_and_derivatives:
+                valid_cases.append(("minimize", algo, deriv, crit_and_deriv))
+                invalid_cases.append(("maximize", algo, deriv, crit_and_deriv))
+    else:
+        for deriv in scalar_derivatives:
+            for crit_and_deriv in scalar_criterion_and_derivtives:
+                for direction in ["maximize", "minimize"]:
+                    valid_cases.append((direction, algo, deriv, crit_and_deriv))
+
+    for deriv in dict_derivatives:
+        for crit_and_deriv in dict_criterion_and_derivatives:
+            if algo in ls_algorithms:
+                valid_cases.append(("minimize", algo, deriv, crit_and_deriv))
+            else:
+                for direction in ["maximize", "minimize"]:
+                    valid_cases.append((direction, algo, deriv, crit_and_deriv))
+
+
+@pytest.mark.parametrize(
+    "direction, algorithm, derivative, criterion_and_derivative", valid_cases
+)
+def test_valid_derivative_versions(
+    direction, algorithm, derivative, criterion_and_derivative
+):
+    start_params = pd.DataFrame()
+    start_params["value"] = [1, 2, 3]
+
+    if direction == "minimize":
+        res = minimize(
+            criterion=sos_dict_criterion,
+            params=start_params,
+            algorithm=algorithm,
+            derivative=derivative,
+            criterion_and_derivative=criterion_and_derivative,
+            error_handling="raise",
+        )
+    else:
+        deriv = derivative if derivative is None else switch_sign(derivative)
+        crit_and_deriv = (
+            criterion_and_derivative
+            if criterion_and_derivative is None
+            else switch_sign(criterion_and_derivative)
+        )
+        res = maximize(
+            criterion=switch_sign(sos_dict_criterion),
+            params=start_params,
+            algorithm=algorithm,
+            derivative=deriv,
+            criterion_and_derivative=crit_and_deriv,
+            error_handling="raise",
+        )
+
+    aaae(res["solution_params"]["value"].to_numpy(), np.zeros(3), decimal=4)
+
+
+@pytest.mark.parametrize(
+    "direction, algorithm, derivative, criterion_and_derivative", invalid_cases
+)
+def test_invalid_derivative_versions(
+    direction, algorithm, derivative, criterion_and_derivative
+):
+    start_params = pd.DataFrame()
+    start_params["value"] = [1, 2, 3]
+
+    if direction == "minimize":
+        with pytest.raises(ValueError):
+            minimize(
+                criterion=sos_dict_criterion,
+                params=start_params,
+                algorithm=algorithm,
+                derivative=derivative,
+                criterion_and_derivative=criterion_and_derivative,
+            )
+    else:
+        deriv = derivative if derivative is None else switch_sign(derivative)
+        crit_and_deriv = (
+            criterion_and_derivative
+            if criterion_and_derivative is None
+            else switch_sign(criterion_and_derivative)
+        )
+        with pytest.raises(ValueError):
+            maximize(
+                criterion=switch_sign(sos_dict_criterion),
+                params=start_params,
+                algorithm=algorithm,
+                derivative=deriv,
+                criterion_and_derivative=crit_and_deriv,
+            )
diff --git a/estimagic/tests/optimization/test_internal_criterion_and_derivative_template.py b/estimagic/tests/optimization/test_internal_criterion_and_derivative_template.py
index fa61cb54c..edcd46df9 100644
--- a/estimagic/tests/optimization/test_internal_criterion_and_derivative_template.py
+++ b/estimagic/tests/optimization/test_internal_criterion_and_derivative_template.py
@@ -7,6 +7,12 @@
 from numpy.testing import assert_array_almost_equal as aaae
 
 from estimagic.differentiation.derivatives import first_derivative
+from estimagic.examples.criterion_functions import sos_criterion_and_gradient
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.examples.criterion_functions import sos_dict_criterion_with_pd_objects
+from estimagic.examples.criterion_functions import sos_gradient
+from estimagic.examples.criterion_functions import sos_pandas_gradient
+from estimagic.examples.criterion_functions import sos_scalar_criterion
 from estimagic.optimization.internal_criterion_template import _penalty_contributions
 from estimagic.optimization.internal_criterion_template import (
     _penalty_contributions_derivative,
@@ -23,24 +29,6 @@
     internal_criterion_and_derivative_template,
 )
 from estimagic.optimization.optimize import _fill_error_penalty_with_defaults
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_criterion_and_gradient,
-)
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_dict_criterion,
-)
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_dict_criterion_with_pd_objects,
-)
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_gradient,
-)
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_pandas_gradient,
-)
-from estimagic.tests.optimization.test_all_algorithms_with_sum_of_squares import (
-    sos_scalar_criterion,
-)
 
 
 def no_second_call(func):
diff --git a/estimagic/tests/optimization/test_many_algorithms.py b/estimagic/tests/optimization/test_many_algorithms.py
new file mode 100644
index 000000000..c1a3e785b
--- /dev/null
+++ b/estimagic/tests/optimization/test_many_algorithms.py
@@ -0,0 +1,53 @@
+"""Test all availabl algorithms on a simple sum of squares function.
+
+- only minimize
+- only numerical derivative
+
+"""
+import inspect
+
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.optimization import AVAILABLE_ALGORITHMS
+from estimagic.optimization.optimize import minimize
+
+
+BOUNDED_ALGORITHMS = []
+for name, func in AVAILABLE_ALGORITHMS.items():
+    arguments = list(inspect.signature(func).parameters)
+    if "lower_bounds" in arguments and "upper_bounds" in arguments:
+        BOUNDED_ALGORITHMS.append(name)
+
+
+@pytest.mark.parametrize("algorithm", AVAILABLE_ALGORITHMS)
+def test_algorithm_on_sum_of_squares(algorithm):
+    params = pd.DataFrame()
+    params["value"] = [1, 2, 3]
+
+    res = minimize(
+        criterion=sos_dict_criterion,
+        params=params,
+        algorithm=algorithm,
+    )
+
+    aaae(res["solution_params"]["value"].to_numpy(), np.zeros(3), decimal=4)
+
+
+@pytest.mark.parametrize("algorithm", BOUNDED_ALGORITHMS)
+def test_algorithm_on_sum_of_squares_with_binding_bounds(algorithm):
+    params = pd.DataFrame()
+    params["value"] = [3, 2, -3]
+    params["lower_bound"] = [1, np.nan, np.nan]
+    params["upper_bound"] = [np.nan, np.nan, -1]
+
+    res = minimize(
+        criterion=sos_dict_criterion,
+        params=params,
+        algorithm=algorithm,
+    )
+
+    aaae(res["solution_params"]["value"].to_numpy(), np.array([1, 0, -1]), decimal=3)
diff --git a/estimagic/tests/optimization/test_optimize.py b/estimagic/tests/optimization/test_optimize.py
new file mode 100644
index 000000000..813ab0c42
--- /dev/null
+++ b/estimagic/tests/optimization/test_optimize.py
@@ -0,0 +1,50 @@
+"""Tests for (almost) algorithm independent properties of maximize and minimize."""
+import numpy as np
+import pandas as pd
+import pytest
+
+from estimagic.examples.criterion_functions import sos_scalar_criterion
+from estimagic.optimization.optimize import maximize
+from estimagic.optimization.optimize import minimize
+
+
+def test_sign_is_switched_back_after_maximization():
+    params = pd.DataFrame()
+    params["value"] = [1, 2, 3]
+    res = maximize(
+        lambda params: 1 - params["value"] @ params["value"],
+        params=params,
+        algorithm="scipy_lbfgsb",
+    )
+
+    assert np.allclose(res["solution_criterion"], 1)
+
+
+def test_warnings_with_old_bounds_names():
+    base_params = pd.DataFrame()
+    base_params["value"] = [1, 2, 3]
+
+    for wrong_name in "lower", "upper":
+        params = base_params.copy()
+        params[wrong_name] = 0
+        with pytest.warns(UserWarning):
+            maximize(
+                lambda params: 1 - params["value"] @ params["value"],
+                params=params,
+                algorithm="scipy_lbfgsb",
+            )
+
+
+def test_scipy_lbfgsb_actually_calls_criterion_and_derivative():
+    params = pd.DataFrame(data=np.ones((10, 1)), columns=["value"])
+
+    def raising_crit_and_deriv(params):
+        raise Exception()
+
+    with pytest.raises(Exception):
+        minimize(
+            criterion=sos_scalar_criterion,
+            params=params,
+            algorithm="scipy_lbfgsb",
+            criterion_and_derivative=raising_crit_and_deriv,
+        )
diff --git a/estimagic/tests/optimization/test_other_criterion_functions.py b/estimagic/tests/optimization/test_other_criterion_functions.py
deleted file mode 100644
index bf91ee63c..000000000
--- a/estimagic/tests/optimization/test_other_criterion_functions.py
+++ /dev/null
@@ -1,639 +0,0 @@
-""" Test the external interface for optimization with different implementations
-of three criterion functions: trid, rotated_hyper_ellipsoid, and rosenbrock,
-for a representative subset of available algorithms.
-
-"""
-import functools
-from itertools import product
-
-import numpy as np
-import pandas as pd
-import pytest
-from numpy.testing import assert_allclose
-
-from estimagic.config import IS_DFOLS_INSTALLED
-from estimagic.config import IS_PETSC4PY_INSTALLED
-from estimagic.config import IS_PYBOBYQA_INSTALLED
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_criterion_and_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_dict_criterion,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rosenbrock_scalar_criterion,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_criterion_and_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_dict_criterion,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    rotated_hyper_ellipsoid_scalar_criterion,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    trid_criterion_and_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    trid_dict_criterion,
-)
-from estimagic.examples.criterion_functions_optimization_tests import trid_gradient
-from estimagic.examples.criterion_functions_optimization_tests import (
-    trid_pandas_gradient,
-)
-from estimagic.examples.criterion_functions_optimization_tests import (
-    trid_scalar_criterion,
-)
-from estimagic.optimization.optimize import maximize
-from estimagic.optimization.optimize import minimize
-
-
-# Take a representative subset of algorithms for running tests - one least_squares
-# algorithm (nag_dfols) and few from the scipy library.
-rep_algo_list = [
-    "scipy_lbfgsb",
-    "scipy_slsqp",
-    "nag_pybobyqa",
-    "nag_dfols",
-    "scipy_powell",
-    "scipy_trust_constr",
-]
-
-# These algorithms require atol = 1e-02
-IMPRECISE_ALGOS = ["scipy_powell", "scipy_trust_constr"]
-
-
-# Helper functions for tests
-def _skip_tests_with_missing_dependencies(test_cases):
-    """Skip tests involving optimizers whose dependencies could not be found."""
-    new_test_cases = []
-    for test_case in test_cases:
-        needs_skipping, reason = _get_skipping_info(test_case)
-        if needs_skipping:
-            test_case = pytest.param(
-                *test_case,
-                marks=pytest.mark.skip(reason=reason),
-            )
-
-        new_test_cases.append(test_case)
-
-    return new_test_cases
-
-
-def _get_skipping_info(test_case):
-    installation_info = {
-        "tao_": IS_PETSC4PY_INSTALLED,
-        "nag_pybobyqa": IS_PYBOBYQA_INSTALLED,
-        "nag_dfols": IS_DFOLS_INSTALLED,
-    }
-
-    reasons = {
-        "tao_": "petsc4py is not installed",
-        "nag_pybobyqa": "pybobyqa is not installed",
-        "nag_dfols": "dfols is not installed",
-    }
-
-    algo_name = test_case[0]
-    needs_skipping = False
-    reason = None
-    for substring, is_installed in installation_info.items():
-        if algo_name.startswith(substring) and not is_installed:
-            needs_skipping = True
-            reason = reasons[substring]
-
-    return needs_skipping, reason
-
-
-# Define dictionaries with different implementations of each criterion function as keys
-rosenbrock_criterion_functions = {
-    "scalar_criterion": rosenbrock_scalar_criterion,
-    "dict_criterion": rosenbrock_dict_criterion,
-    "criterion_and_derivative": rosenbrock_criterion_and_gradient,
-    "gradient": rosenbrock_gradient,
-    "pandas_gradient": rosenbrock_pandas_gradient,
-}
-
-rhe_criterion_functions = {
-    "scalar_criterion": rotated_hyper_ellipsoid_scalar_criterion,
-    "dict_criterion": rotated_hyper_ellipsoid_dict_criterion,
-    "criterion_and_derivative": rotated_hyper_ellipsoid_criterion_and_gradient,
-    "gradient": rotated_hyper_ellipsoid_gradient,
-    "pandas_gradient": rotated_hyper_ellipsoid_pandas_gradient,
-}
-
-
-def get_test_cases_for_algorithm_and_criterion(algorithm, criterion_functions):
-    """Generate list of all possible argument combinations for each criterion
-    function (specified as a dictionary) and algorithm.
-    """
-    is_least_squares = algorithm in ["nag_dfols"]
-    is_scalar = not (is_least_squares)
-
-    directions = ["minimize"] if is_least_squares else ["maximize", "minimize"]
-
-    crit_funcs = [criterion_functions["dict_criterion"]]
-    if is_scalar:
-        crit_funcs.append(criterion_functions["scalar_criterion"])
-
-    if is_scalar:
-        derivatives = [
-            criterion_functions["gradient"],
-            criterion_functions["pandas_gradient"],
-            None,
-        ]
-    else:
-        derivatives = [None]
-
-    if is_scalar:
-        crit_and_derivs = [criterion_functions["criterion_and_derivative"], None]
-    else:
-        crit_and_derivs = [None]
-
-    prod_list = [directions, crit_funcs, derivatives, crit_and_derivs]
-
-    test_cases = []
-    for direction, crit, deriv, c_and_d in product(*prod_list):
-        if direction == "maximize":
-            case = (
-                algorithm,
-                direction,
-                switch_sign(crit),
-                switch_sign(deriv),
-                switch_sign(c_and_d),
-            )
-        else:
-            case = (algorithm, direction, crit, deriv, c_and_d)
-        test_cases.append(case)
-    return test_cases
-
-
-# Trid function cannot be represented as a least squares problem. Hence we skip
-# testcases involving least_squares algorithms
-def get_trid_test_cases_for_algorithm(algorithm):
-    """Given trid function, generate list of all possible argument combinations
-    for each algorithm."""
-    is_least_squares = algorithm in ["nag_dfols"]
-    is_scalar = not (is_least_squares)
-
-    if is_scalar:
-        directions = ["maximize", "minimize"]
-    else:
-        pass
-
-    crit_funcs = [trid_dict_criterion]
-    if is_scalar:
-        crit_funcs.append(trid_scalar_criterion)
-
-    if is_scalar:
-        derivatives = [trid_gradient, trid_pandas_gradient, None]
-    else:
-        pass
-
-    if is_scalar:
-        crit_and_derivs = [trid_criterion_and_gradient, None]
-    else:
-        pass
-
-    test_cases = []
-
-    if is_scalar:
-
-        prod_list = [directions, crit_funcs, derivatives, crit_and_derivs]
-
-        for direction, crit, deriv, c_and_d in product(*prod_list):
-            if direction == "maximize":
-                case = (
-                    algorithm,
-                    direction,
-                    switch_sign(crit),
-                    switch_sign(deriv),
-                    switch_sign(c_and_d),
-                )
-            else:
-                case = (algorithm, direction, crit, deriv, c_and_d)
-            test_cases.append(case)
-    else:
-        pass
-    return test_cases
-
-
-def switch_sign(func):
-    if func is None:
-        wrapper = None
-    else:
-
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            unswitched = func(*args, **kwargs)
-            if isinstance(unswitched, dict):
-                switched = {key: -val for key, val in unswitched.items()}
-            elif isinstance(unswitched, tuple):
-                switched = []
-                for entry in unswitched:
-                    if isinstance(entry, dict):
-                        switched.append({key: -val for key, val in entry.items()})
-                    else:
-                        switched.append(-entry)
-                switched = tuple(switched)
-            else:
-                switched = -unswitched
-            return switched
-
-    return wrapper
-
-
-trid_test_cases = []
-rhe_test_cases = []
-rosenbrock_test_cases = []
-for alg in rep_algo_list:
-    trid_test_cases += get_trid_test_cases_for_algorithm(alg)
-    rhe_test_cases += get_test_cases_for_algorithm_and_criterion(
-        alg, rhe_criterion_functions
-    )
-    rosenbrock_test_cases += get_test_cases_for_algorithm_and_criterion(
-        alg, rosenbrock_criterion_functions
-    )
-
-
-test_cases = trid_test_cases + rhe_test_cases + rosenbrock_test_cases
-test_cases = _skip_tests_with_missing_dependencies(test_cases)
-
-# Define test functions - one for each constraint type
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_without_constraints(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=np.array([1, 2, 3]), columns=["value"])
-    params["lower_bound"] = -np.inf
-    params["upper_bound"] = np.inf
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([3, 4, 3])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.zeros(3)
-    else:
-        expected = np.ones(3)
-
-    assert res["success"], f"{algo} did not converge."
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(),
-        expected,
-        atol=atol,
-        rtol=0,
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_fixed_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [3], [2]], columns=["value"])
-    params["lower_bound"] = [-5, -5, -5]
-    params["upper_bound"] = [5, 5, 5]
-
-    constraints = [{"loc": [0], "type": "fixed", "value": [1]}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([1, 2.666666667, 2.333333333])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([1, 0, 0])
-    else:
-        expected = np.ones(3)
-
-    assert res["success"], f"{algo} did not converge."
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_equality_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[2], [2], [2]], columns=["value"])
-    params["lower_bound"] = [-5, -5, -5]
-    params["upper_bound"] = -params["lower_bound"]
-
-    constraints = [{"loc": [0, 1, 2], "type": "equality"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([3, 3, 3])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    elif (crit.__name__.startswith("rosenbrock")) & (algo == "scipy_lbfgsb"):
-        pytest.xfail(
-            "scipy_L-BFGS-B fail rosenbrock criterion \
-                with equality constraint."
-        )
-    else:
-        expected = np.ones(3)
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_pairwise_equality_constraint(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[2], [2], [3]], columns=["value"])
-    params["lower_bound"] = [-5, -5, -5]
-    params["upper_bound"] = [5, 5, 5]
-
-    constraints = [{"locs": [0, 1], "type": "pairwise_equality"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([3.333333333, 3.333333333, 2.666666667])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    else:
-        expected = np.ones(3)
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_increasing_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[1], [2], [3]], columns=["value"])
-
-    constraints = [{"loc": [1, 2], "type": "increasing"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("rosenbrock"):
-        expected = np.ones(3)
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    elif (crit.__name__.startswith("trid")) & (algo == "scipy_powell"):
-        pytest.xfail(
-            "scipy_powell fails trid criterion \
-                with increasing constraint."
-        )
-    else:
-        expected = np.array([2.666666667, 3.3333333, 3.3333333])
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_decreasing_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[2], [1], [3]], columns=["value"])
-
-    constraints = [{"loc": [0, 1], "type": "decreasing"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        pytest.skip("Analytical solution not known.")
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    else:
-        expected = np.ones(3)
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_linear_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[2], [1], [3]], columns=["value"])
-
-    constraints = [{"loc": [0, 1], "type": "linear", "value": 4, "weights": [1, 2]}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([1.185185185, 1.4074074069999998, 1.703703704])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0.571428571, 1.714285714, 0])
-    else:
-        pytest.skip("Analytical solution not known.")
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_probability_constraint(algo, direction, crit, deriv, crit_and_deriv):
-    params = pd.DataFrame(data=[[0.5], [0.5], [3]], columns=["value"])
-
-    constraints = [{"loc": [0, 1], "type": "probability"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        expected = np.array([0.272727273, 0.727272727, 1.363636364])
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0.4, 0.6, 0])
-    else:
-        pytest.skip("Analytical solution not known.")
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_covariance_constraint_no_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[1], [1], [1]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2], "type": "covariance"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        pytest.skip("Analytical solution not known.")
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    else:
-        expected = np.ones(3)
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
-
-
-@pytest.mark.slow
-@pytest.mark.parametrize("algo, direction, crit, deriv, crit_and_deriv", test_cases)
-def test_with_sdcorr_constraint_no_bounds_distance(
-    algo,
-    direction,
-    crit,
-    deriv,
-    crit_and_deriv,
-):
-    params = pd.DataFrame(data=[[1], [1], [1]], columns=["value"])
-
-    constraints = [{"loc": [0, 1, 2], "type": "sdcorr"}]
-
-    optimize_func = minimize if direction == "minimize" else maximize
-    res = optimize_func(
-        criterion=crit,
-        params=params,
-        algorithm=algo,
-        derivative=deriv,
-        criterion_and_derivative=crit_and_deriv,
-        constraints=constraints,
-        logging=False,
-    )
-
-    if crit.__name__.startswith("trid"):
-        pytest.skip("Analytical solution not known.")
-    elif crit.__name__.startswith("rotated_hyper_ellipsoid"):
-        expected = np.array([0, 0, 0])
-    else:
-        expected = np.ones(3)
-    assert res["success"], f"{algo} did not converge."
-
-    atol = 1e-02 if algo in IMPRECISE_ALGOS else 1e-04
-    assert_allclose(
-        res["solution_params"]["value"].to_numpy(), expected, atol=atol, rtol=0
-    )
diff --git a/estimagic/tests/optimization/test_with_advanced_constraints.py b/estimagic/tests/optimization/test_with_advanced_constraints.py
new file mode 100644
index 000000000..0dbb6d8b3
--- /dev/null
+++ b/estimagic/tests/optimization/test_with_advanced_constraints.py
@@ -0,0 +1,85 @@
+"""Tests using constraints with optional entries or combination of constraints.
+
+- Only sum of squares
+- Only scipy_lbfgsb
+- Only minimize
+
+"""
+import itertools
+
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.examples.criterion_functions import sos_gradient
+from estimagic.examples.criterion_functions import sos_scalar_criterion
+from estimagic.optimization.optimize import minimize
+
+
+CONSTR_INFO = {
+    "cov_bounds_distance": [
+        {
+            "loc": [0, 1, 2, 3, 4, 5],
+            "type": "covariance",
+            "bounds_distance": 0.1,
+            "robust_bounds": True,
+        }
+    ],
+    "sdcorr_bounds_distance": [
+        {
+            "loc": [0, 1, 2, 3, 4, 5],
+            "type": "sdcorr",
+            "bounds_distance": 0.1,
+            "robust_bounds": True,
+        }
+    ],
+    "fixed_and_decreasing": [
+        {"loc": [1, 2, 3, 4], "type": "decreasing"},
+        {"loc": 2, "type": "fixed", "value": 4},
+    ],
+    "fixed_and_increasing": [
+        {"loc": [0, 1, 2, 3], "type": "increasing"},
+        {"loc": 2, "type": "fixed", "value": 3},
+    ],
+}
+
+
+START_INFO = {
+    "cov_bounds_distance": [1, 0.1, 2, 0.2, 0.3, 3],
+    "sdcorr_bounds_distance": [1, 2, 3, 0.1, 0.2, 0.3],
+    "fixed_and_decreasing": [1, 4, 4, 2, 1],
+    "fixed_and_increasing": [1, 2, 3, 4, 1],
+}
+
+RES_INFO = {
+    "cov_bounds_distance": [0.1, 0, 0.1, 0, 0, 0.1],
+    "sdcorr_bounds_distance": [0.1, 0.1, 0.1, 0, 0, 0.0],
+    "fixed_and_decreasing": [0, 4, 4, 0, 0],
+    "fixed_and_increasing": [0, 0, 3, 3, 0],
+}
+
+
+derivatives = [sos_gradient, None]
+constr_names = list(CONSTR_INFO.keys())
+
+
+test_cases = list(itertools.product(derivatives, constr_names))
+
+
+@pytest.mark.parametrize("derivative, constr_name", test_cases)
+def test_with_covariance_constraint_bounds_distance(derivative, constr_name):
+    params = pd.Series(START_INFO[constr_name], name="value").to_frame()
+
+    res = minimize(
+        criterion=sos_scalar_criterion,
+        params=params,
+        algorithm="scipy_lbfgsb",
+        derivative=derivative,
+        constraints=CONSTR_INFO[constr_name],
+    )
+
+    assert res["success"], "scipy_lbfgsb did not converge."
+
+    expected = np.array(RES_INFO[constr_name])
+    aaae(res["solution_params"]["value"].to_numpy(), expected, decimal=4)
diff --git a/estimagic/tests/optimization/test_with_constraints.py b/estimagic/tests/optimization/test_with_constraints.py
new file mode 100644
index 000000000..1d5a935b8
--- /dev/null
+++ b/estimagic/tests/optimization/test_with_constraints.py
@@ -0,0 +1,147 @@
+"""Test many different criterion functions and many sets of constraints.
+
+- only minimize
+- only gradient based algorithms scipy_lbfgsb (scalar) and scipy_ls_dogbox (least
+  squares)
+- closed form and numerical derivatives
+
+"""
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.examples.criterion_functions import rosenbrock_dict_criterion
+from estimagic.examples.criterion_functions import rosenbrock_gradient
+from estimagic.examples.criterion_functions import (
+    rotated_hyper_ellipsoid_dict_criterion,
+)
+from estimagic.examples.criterion_functions import rotated_hyper_ellipsoid_gradient
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.examples.criterion_functions import sos_gradient
+from estimagic.examples.criterion_functions import sos_jacobian
+from estimagic.examples.criterion_functions import sos_ls_jacobian
+from estimagic.examples.criterion_functions import trid_gradient
+from estimagic.examples.criterion_functions import trid_scalar_criterion
+from estimagic.optimization.optimize import minimize
+
+
+FUNC_INFO = {
+    "sos": {
+        "criterion": sos_dict_criterion,
+        "gradient": sos_gradient,
+        "jacobian": sos_jacobian,
+        "ls_jacobian": sos_ls_jacobian,
+        "default_result": np.zeros(3),
+        "fixed_result": [1, 0, 0],
+        "entries": ["value", "contributions", "root_contributions"],
+        "linear_result": [0.8, 1.6, 0],
+        "probability_result": [0.5, 0.5, 0],
+    },
+    "rotated_hyper_ellipsoid": {
+        "criterion": rotated_hyper_ellipsoid_dict_criterion,
+        "gradient": rotated_hyper_ellipsoid_gradient,
+        "entries": ["value", "contributions", "root_contributions"],
+        "default_result": np.zeros(3),
+        "fixed_result": [1, 0, 0],
+        "linear_result": [0.571428571, 1.714285714, 0],
+        "probability_result": [0.4, 0.6, 0],
+    },
+    "rosenbrock": {
+        "criterion": rosenbrock_dict_criterion,
+        "gradient": rosenbrock_gradient,
+        "entries": ["value", "contributions"],
+        "default_result": np.ones(3),
+        "linear_result": "unknown",
+        "probability_result": "unknown",
+    },
+    "trid": {
+        "criterion": trid_scalar_criterion,
+        "gradient": trid_gradient,
+        "entries": ["value"],
+        "default_result": [3, 4, 3],
+        "fixed_result": [1, 2.666666667, 2.333333333],
+        "equality_result": [3, 3, 3],
+        "pairwise_equality_result": [3.333333333, 3.333333333, 2.666666667],
+        "increasing_result": [2.666666667, 3.3333333, 3.3333333],
+        "decreasing_result": "unknown",
+        "linear_result": [1.185185185, 1.4074074069999998, 1.703703704],
+        "probability_result": [0.272727273, 0.727272727, 1.363636364],
+        "covariance_result": "unknown",
+        "sdcorr_result": "unknown",
+    },
+}
+
+
+CONSTR_INFO = {
+    "fixed": [{"loc": [0], "type": "fixed", "value": 1}],
+    "equality": [{"loc": [0, 1, 2], "type": "equality"}],
+    "pairwise_equality": [{"locs": [0, 1], "type": "pairwise_equality"}],
+    "increasing": [{"loc": [1, 2], "type": "increasing"}],
+    "decreasing": [{"loc": [0, 1], "type": "decreasing"}],
+    "linear": [{"loc": [0, 1], "type": "linear", "value": 4, "weights": [1, 2]}],
+    "probability": [{"loc": [0, 1], "type": "probability"}],
+    "covariance": [{"loc": [0, 1, 2], "type": "covariance"}],
+    "sdcorr": [{"loc": [0, 1, 2], "type": "sdcorr"}],
+}
+
+
+START_INFO = {
+    "fixed": [1, 1.5, 4.5],
+    "equality": [1, 1, 1],
+    "pairwise_equality": [2, 2, 3],
+    "increasing": [1, 2, 3],
+    "decreasing": [3, 2, 1],
+    "linear": [2, 1, 3],
+    "probability": [0.8, 0.2, 3],
+    "covariance": [2, 1, 2],
+    "sdcorr": [2, 2, 0.5],
+}
+
+KNOWN_FAILURES = {
+    ("rosenbrock", "equality"),
+    ("rosenbrock", "decreasing"),  # imprecise
+}
+
+test_cases = []
+for crit_name in FUNC_INFO:
+    for constr_name in CONSTR_INFO:
+        unknown_res = FUNC_INFO[crit_name].get(f"{constr_name}_result") == "unknown"
+        known_failure = (crit_name, constr_name) in KNOWN_FAILURES
+        if not any([unknown_res, known_failure]):
+            for deriv in None, FUNC_INFO[crit_name]["gradient"]:
+                test_cases.append((crit_name, "scipy_lbfgsb", deriv, constr_name))
+
+            if "root_contributions" in FUNC_INFO[crit_name]["entries"]:
+                for deriv in list({FUNC_INFO[crit_name].get("ls_jacobian"), None}):
+                    test_cases.append(
+                        (crit_name, "scipy_ls_dogbox", deriv, constr_name)
+                    )
+
+
+@pytest.mark.parametrize(
+    "criterion_name, algorithm, derivative, constraint_name", test_cases
+)
+def test_constrained_minimization(
+    criterion_name, algorithm, derivative, constraint_name
+):
+
+    constraints = CONSTR_INFO[constraint_name]
+    criterion = FUNC_INFO[criterion_name]["criterion"]
+    params = pd.Series(START_INFO[constraint_name], name="value").to_frame()
+
+    res = minimize(
+        criterion=criterion,
+        params=params,
+        algorithm=algorithm,
+        derivative=derivative,
+        constraints=constraints,
+    )
+
+    calculated = res["solution_params"]["value"].to_numpy()
+
+    expected = FUNC_INFO[criterion_name].get(
+        f"{constraint_name}_result", FUNC_INFO[criterion_name]["default_result"]
+    )
+
+    aaae(calculated, expected, decimal=4)
diff --git a/estimagic/tests/optimization/test_with_logging.py b/estimagic/tests/optimization/test_with_logging.py
new file mode 100644
index 000000000..3a23a9b2c
--- /dev/null
+++ b/estimagic/tests/optimization/test_with_logging.py
@@ -0,0 +1,74 @@
+"""Test optimizations with logging in a temporary database.
+
+- Only minimize
+- Only dict criterion
+- scipy_lbfgsb and scipy_ls_dogbox
+- with and without derivatives
+
+"""
+import itertools
+
+import numpy as np
+import pandas as pd
+import pytest
+from numpy.testing import assert_array_almost_equal as aaae
+
+from estimagic.examples.criterion_functions import sos_dict_criterion
+from estimagic.examples.criterion_functions import sos_dict_derivative
+from estimagic.exceptions import TableExistsError
+from estimagic.optimization.optimize import minimize
+
+
+algorithms = ["scipy_lbfgsb", "scipy_ls_dogbox"]
+derivatives = [None, sos_dict_derivative]
+test_cases = list(itertools.product(algorithms, derivatives))
+
+
+@pytest.mark.parametrize("algorithm, derivative", test_cases)
+def test_optimization_with_valid_logging(algorithm, derivative):
+    res = minimize(
+        sos_dict_criterion,
+        pd.Series([1, 2, 3], name="value").to_frame(),
+        algorithm=algorithm,
+        derivative=derivative,
+        logging="logging.db",
+    )
+    aaae(res["solution_params"]["value"].to_numpy(), np.zeros(3))
+
+
+def test_optimization_with_existing_exsting_database():
+    minimize(
+        sos_dict_criterion,
+        pd.Series([1, 2, 3], name="value").to_frame(),
+        algorithm="scipy_lbfgsb",
+        logging="logging.db",
+        log_options={"if_database_exists": "raise"},
+    )
+
+    with pytest.raises(FileExistsError):
+        minimize(
+            sos_dict_criterion,
+            pd.Series([1, 2, 3], name="value").to_frame(),
+            algorithm="scipy_lbfgsb",
+            logging="logging.db",
+            log_options={"if_database_exists": "raise"},
+        )
+
+
+def test_optimization_with_existing_exsting_table():
+    minimize(
+        sos_dict_criterion,
+        pd.Series([1, 2, 3], name="value").to_frame(),
+        algorithm="scipy_lbfgsb",
+        logging="logging.db",
+        log_options={"if_database_exists": "raise"},
+    )
+
+    with pytest.raises(TableExistsError):
+        minimize(
+            sos_dict_criterion,
+            pd.Series([1, 2, 3], name="value").to_frame(),
+            algorithm="scipy_lbfgsb",
+            logging="logging.db",
+            log_options={"if_table_exists": "raise"},
+        )
diff --git a/estimagic/tests/parameters/test_kernel_transformations.py b/estimagic/tests/parameters/test_kernel_transformations.py
index 0f223f6b0..6eec28536 100644
--- a/estimagic/tests/parameters/test_kernel_transformations.py
+++ b/estimagic/tests/parameters/test_kernel_transformations.py
@@ -9,7 +9,7 @@
 from estimagic.differentiation.derivatives import first_derivative
 from estimagic.parameters.kernel_transformations import cov_matrix_to_sdcorr_params
 
-to_test = list(product(range(10, 30), range(5)))
+to_test = list(product(range(10, 30, 5), [1234, 5471]))
 
 
 def get_internal_cholesky(dim, seed=0):