Repository: dynamicslab/pysindy (language: python)

Issue:
Simpler `inputs_per_library` in GeneralizedLibrary?
A `GeneralizedLibrary.inputs_per_library` describes which variables to use for which input library.  Rather than having a list (for each library...) of lists (...use these input variables indices) or a numpy array of True/False (0-axis: library, 1-axis: input variables), we have a numpy array of indices.  I'd like to make a case for either of the first two alternatives over the current system, with a slight preference for the former.

The way the numpy array of indices works is that repeat indices are ignored (`np.unique(self.inputs_per_library_[i, :])`), but still needed because numpy arrays cannot be ragged.  For instance, a `GeneralizedLibrary` containing a `PolynomialLibrary` for `x` and `y` with a `FourierLibrary` for `z` has any of the following `inputs_per_library`:

```python
# Most simple, but weird repeat of number 2.
np.array([
    [0, 1],
    [2, 2]
])

# most common in code, makes some sense because number of columns is number of indices
np.array([
    [0, 1, 0], # or [1, 0, 0], or [0, 1, 1], &c
    [2, 2, 2]
])

# silly but works.
np.array([
    [0, 1, 0, 1, 0, 1, 0, 0, 0],
    [2, 2, 2, 2, 2, 2, 2, 2, 2]
])
```

Compare that to the alternatives:
```python
# removing duplicates becomes set(inputs_per_library[i])
[[0, 1], [2]]

# similar to how TensoredLibrary uses booleans to indicate inclusion in a tensor product
np.array([
    [True, True, False],
    [False, False, True]
])
```
The documentation in `GeneralizedLibrary` isn't totally clear, and the current convention requires code that serves no purpose than padding the array with values (and breaks if padded with the wrong value).  Here's an excerpt from the new `ParametrizedLibrary`:

```python
feature_input = np.zeros(num_features + num_parameters, dtype=np.int32)
feature_input[:num_features] = np.arange(num_features)

parameter_input = (
    np.ones(num_features + num_parameters, dtype=np.int32) * num_features
)
parameter_input[-num_parameters:] = num_features + np.arange(num_parameters)

inputs_per_libraries = np.array([parameter_input, feature_input])
```

Here's the two alternatives:
```python
# instantly understandable
inputs_per_libraries = [range(num_features, num_parameters+num_features), range(num_features)]

# or, still pretty understandable
inputs_per_libraries = np.zeros((2, num_parameters+num_features), dtype=bool)
inputs_per_libraries[0, range(num_features, num_features+num_parameters)] = True
inputs_per_libraries[1, range(num_features)] = True
```

The former method also has the advantage of being distinguishable from existing uses, making deprecation simpler.  I'm not advocating any particular timeline, but I'd like to hear how other people feel about the matter.  Not the world's biggest issue, but I feel like we get a lot of issues on how to do generalized libraries, and every time I troubleshoot bugs in a PR I have a moment of confusion looking it up.  What are people's thoughts?


Discussion / hints:
Hey @Jacob-Stevens-Haas, totally agree that the numpy array format for the generalized library input is pretty terrible. I like the list of lists approach as well, and would be very happy to see it implemented. Since the generalized library is integrated into a few different examples and other libraries, could you start up a new branch if you decide to try it? I think I will have some time to help too.

Fix patch:
diff --git a/examples/1_feature_overview/example.ipynb b/examples/1_feature_overview/example.ipynb
index b9632f30b..bb3d3a46e 100644
--- a/examples/1_feature_overview/example.ipynb
+++ b/examples/1_feature_overview/example.ipynb
@@ -2,8 +2,10 @@
  "cells": [
   {
    "cell_type": "markdown",
-   "id": "6834a12c",
-   "metadata": {},
+   "id": "73fd3bf6",
+   "metadata": {
+    "lines_to_next_cell": 0
+   },
    "source": [
     "# Feature Overview\n",
     "\n",
@@ -14,8 +16,10 @@
   },
   {
    "cell_type": "markdown",
-   "id": "03c83c1e",
-   "metadata": {},
+   "id": "439fe3d9",
+   "metadata": {
+    "lines_to_next_cell": 0
+   },
    "source": [
     "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/dynamicslab/pysindy/v1.7.3?filepath=examples/1_feature_overview.ipynb)"
    ]
@@ -23,15 +27,15 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "id": "90bf9e19",
+   "id": "5f517c29",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:18.994850Z",
-     "iopub.status.busy": "2023-06-20T21:46:18.992735Z",
-     "iopub.status.idle": "2023-06-20T21:46:19.926905Z",
-     "shell.execute_reply": "2023-06-20T21:46:19.926070Z"
+     "iopub.execute_input": "2023-07-10T22:16:22.611211Z",
+     "iopub.status.busy": "2023-07-10T22:16:22.609870Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.510725Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.509899Z"
     },
-    "lines_to_next_cell": 1
+    "lines_to_next_cell": 0
    },
    "outputs": [],
    "source": [
@@ -42,24 +46,26 @@
     "\n",
     "import matplotlib.pyplot as plt\n",
     "import numpy as np\n",
+    "from scipy.integrate import solve_ivp\n",
+    "from scipy.linalg import LinAlgWarning\n",
+    "from sklearn.exceptions import ConvergenceWarning\n",
+    "from sklearn.linear_model import Lasso\n",
+    "\n",
     "import pysindy as ps\n",
     "from pysindy.utils import enzyme\n",
     "from pysindy.utils import lorenz\n",
     "from pysindy.utils import lorenz_control\n",
-    "from scipy.integrate import solve_ivp\n",
-    "from scipy.linalg import LinAlgWarning\n",
-    "from sklearn.linear_model import Lasso\n",
-    "from sklearn.exceptions import ConvergenceWarning\n",
     "\n",
     "if __name__ != \"testing\":\n",
     "    t_end_train = 10\n",
     "    t_end_test = 15\n",
     "else:\n",
-    "    t_end_train = .04\n",
-    "    t_end_test = .04\n",
+    "    t_end_train = 0.04\n",
+    "    t_end_test = 0.04\n",
     "\n",
     "data = (Path() / \"../data\").resolve()\n",
     "\n",
+    "\n",
     "@contextmanager\n",
     "def ignore_specific_warnings():\n",
     "    filters = copy(warnings.filters)\n",
@@ -67,19 +73,24 @@
     "    warnings.filterwarnings(\"ignore\", category=LinAlgWarning)\n",
     "    warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
     "    yield\n",
-    "    warnings.filters = filters"
+    "    warnings.filters = filters\n",
+    "\n",
+    "if __name__ == \"testing\":\n",
+    "    import sys\n",
+    "    import os\n",
+    "    sys.stdout = open(os.devnull, \"w\")"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 2,
-   "id": "52058c1f",
+   "id": "4bbd43f3",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:19.929543Z",
-     "iopub.status.busy": "2023-06-20T21:46:19.929095Z",
-     "iopub.status.idle": "2023-06-20T21:46:19.932758Z",
-     "shell.execute_reply": "2023-06-20T21:46:19.932073Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.513154Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.512816Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.516189Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.515486Z"
     }
    },
    "outputs": [],
@@ -90,7 +101,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "54ee6855",
+   "id": "774cbaed",
    "metadata": {},
    "source": [
     "### A note on solve_ivp vs odeint before we continue\n",
@@ -100,27 +111,27 @@
   {
    "cell_type": "code",
    "execution_count": 3,
-   "id": "4f7a3878",
+   "id": "96aa7f0d",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:19.935278Z",
-     "iopub.status.busy": "2023-06-20T21:46:19.935024Z",
-     "iopub.status.idle": "2023-06-20T21:46:19.938623Z",
-     "shell.execute_reply": "2023-06-20T21:46:19.938149Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.518638Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.518389Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.521697Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.521118Z"
     }
    },
    "outputs": [],
    "source": [
     "# Initialize integrator keywords for solve_ivp to replicate the odeint defaults\n",
     "integrator_keywords = {}\n",
-    "integrator_keywords['rtol'] = 1e-12\n",
-    "integrator_keywords['method'] = 'LSODA'\n",
-    "integrator_keywords['atol'] = 1e-12"
+    "integrator_keywords[\"rtol\"] = 1e-12\n",
+    "integrator_keywords[\"method\"] = \"LSODA\"\n",
+    "integrator_keywords[\"atol\"] = 1e-12"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "f3964aa7",
+   "id": "d81deb1d",
    "metadata": {},
    "source": [
     "## Basic usage\n",
@@ -132,7 +143,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "5ed29e93",
+   "id": "0796936b",
    "metadata": {},
    "source": [
     "### Train the model"
@@ -141,37 +152,38 @@
   {
    "cell_type": "code",
    "execution_count": 4,
-   "id": "40668652",
+   "id": "7d6cd62e",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:19.941312Z",
-     "iopub.status.busy": "2023-06-20T21:46:19.940803Z",
-     "iopub.status.idle": "2023-06-20T21:46:20.104078Z",
-     "shell.execute_reply": "2023-06-20T21:46:20.103268Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.524335Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.524062Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.703919Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.703067Z"
     }
    },
    "outputs": [],
    "source": [
     "# Generate measurement data\n",
-    "dt = .002\n",
+    "dt = 0.002\n",
     "\n",
     "t_train = np.arange(0, t_end_train, dt)\n",
     "x0_train = [-8, 8, 27]\n",
     "t_train_span = (t_train[0], t_train[-1])\n",
-    "x_train = solve_ivp(lorenz, t_train_span, x0_train, \n",
-    "                    t_eval=t_train, **integrator_keywords).y.T"
+    "x_train = solve_ivp(\n",
+    "    lorenz, t_train_span, x0_train, t_eval=t_train, **integrator_keywords\n",
+    ").y.T"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 5,
-   "id": "17f837ea",
+   "id": "d647c368",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:20.106820Z",
-     "iopub.status.busy": "2023-06-20T21:46:20.106324Z",
-     "iopub.status.idle": "2023-06-20T21:46:20.128373Z",
-     "shell.execute_reply": "2023-06-20T21:46:20.127304Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.706673Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.706362Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.731568Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.730381Z"
     }
    },
    "outputs": [
@@ -194,7 +206,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "48642a17",
+   "id": "acc4aa28",
    "metadata": {},
    "source": [
     "### Assess results on a test trajectory"
@@ -203,13 +215,13 @@
   {
    "cell_type": "code",
    "execution_count": 6,
-   "id": "7e6bad95",
+   "id": "d81dedc1",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:20.132406Z",
-     "iopub.status.busy": "2023-06-20T21:46:20.132014Z",
-     "iopub.status.idle": "2023-06-20T21:46:20.412285Z",
-     "shell.execute_reply": "2023-06-20T21:46:20.411652Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.735912Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.735425Z",
+     "iopub.status.idle": "2023-07-10T22:16:23.996323Z",
+     "shell.execute_reply": "2023-07-10T22:16:23.995668Z"
     }
    },
    "outputs": [
@@ -226,16 +238,17 @@
     "t_test = np.arange(0, t_end_test, dt)\n",
     "x0_test = np.array([8, 7, 15])\n",
     "t_test_span = (t_test[0], t_test[-1])\n",
-    "x_test = solve_ivp(lorenz, t_test_span, x0_test, \n",
-    "                   t_eval=t_test, **integrator_keywords).y.T  \n",
+    "x_test = solve_ivp(\n",
+    "    lorenz, t_test_span, x0_test, t_eval=t_test, **integrator_keywords\n",
+    ").y.T\n",
     "\n",
     "# Compare SINDy-predicted derivatives with finite difference derivatives\n",
-    "print('Model score: %f' % model.score(x_test, t=dt))"
+    "print(\"Model score: %f\" % model.score(x_test, t=dt))"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "0f208297",
+   "id": "b00719f8",
    "metadata": {},
    "source": [
     "### Predict derivatives with learned model"
@@ -244,13 +257,13 @@
   {
    "cell_type": "code",
    "execution_count": 7,
-   "id": "a64025ad",
+   "id": "f258c463",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:20.414658Z",
-     "iopub.status.busy": "2023-06-20T21:46:20.414457Z",
-     "iopub.status.idle": "2023-06-20T21:46:21.360581Z",
-     "shell.execute_reply": "2023-06-20T21:46:21.359880Z"
+     "iopub.execute_input": "2023-07-10T22:16:23.998704Z",
+     "iopub.status.busy": "2023-07-10T22:16:23.998306Z",
+     "iopub.status.idle": "2023-07-10T22:16:24.798958Z",
+     "shell.execute_reply": "2023-07-10T22:16:24.798261Z"
     }
    },
    "outputs": [
@@ -267,25 +280,23 @@
    ],
    "source": [
     "# Predict derivatives using the learned model\n",
-    "x_dot_test_predicted = model.predict(x_test)  \n",
+    "x_dot_test_predicted = model.predict(x_test)\n",
     "\n",
     "# Compute derivatives with a finite difference method, for comparison\n",
     "x_dot_test_computed = model.differentiate(x_test, t=dt)\n",
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(7, 9))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test, x_dot_test_computed[:, i],\n",
-    "                'k', label='numerical derivative')\n",
-    "    axs[i].plot(t_test, x_dot_test_predicted[:, i],\n",
-    "                'r--', label='model prediction')\n",
+    "    axs[i].plot(t_test, x_dot_test_computed[:, i], \"k\", label=\"numerical derivative\")\n",
+    "    axs[i].plot(t_test, x_dot_test_predicted[:, i], \"r--\", label=\"model prediction\")\n",
     "    axs[i].legend()\n",
-    "    axs[i].set(xlabel='t', ylabel='$\\dot x_{}$'.format(i))\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=r\"$\\dot x_{}$\".format(i))\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "9023a971",
+   "id": "a9a60864",
    "metadata": {},
    "source": [
     "### Simulate forward in time"
@@ -294,13 +305,13 @@
   {
    "cell_type": "code",
    "execution_count": 8,
-   "id": "814e95fb",
+   "id": "9ba377ae",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:21.363633Z",
-     "iopub.status.busy": "2023-06-20T21:46:21.363396Z",
-     "iopub.status.idle": "2023-06-20T21:46:25.943768Z",
-     "shell.execute_reply": "2023-06-20T21:46:25.943088Z"
+     "iopub.execute_input": "2023-07-10T22:16:24.801488Z",
+     "iopub.status.busy": "2023-07-10T22:16:24.801077Z",
+     "iopub.status.idle": "2023-07-10T22:16:30.361779Z",
+     "shell.execute_reply": "2023-07-10T22:16:30.360806Z"
     }
    },
    "outputs": [
@@ -331,28 +342,26 @@
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(7, 9))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test, x_test[:, i], 'k', label='true simulation')\n",
-    "    axs[i].plot(t_test, x_test_sim[:, i], 'r--', label='model simulation')\n",
+    "    axs[i].plot(t_test, x_test[:, i], \"k\", label=\"true simulation\")\n",
+    "    axs[i].plot(t_test, x_test_sim[:, i], \"r--\", label=\"model simulation\")\n",
     "    axs[i].legend()\n",
-    "    axs[i].set(xlabel='t', ylabel='$x_{}$'.format(i))\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=\"$x_{}$\".format(i))\n",
     "\n",
     "fig = plt.figure(figsize=(10, 4.5))\n",
-    "ax1 = fig.add_subplot(121, projection='3d')\n",
-    "ax1.plot(x_test[:, 0], x_test[:, 1], x_test[:, 2], 'k')\n",
-    "ax1.set(xlabel='$x_0$', ylabel='$x_1$',\n",
-    "        zlabel='$x_2$', title='true simulation')\n",
+    "ax1 = fig.add_subplot(121, projection=\"3d\")\n",
+    "ax1.plot(x_test[:, 0], x_test[:, 1], x_test[:, 2], \"k\")\n",
+    "ax1.set(xlabel=\"$x_0$\", ylabel=\"$x_1$\", zlabel=\"$x_2$\", title=\"true simulation\")\n",
     "\n",
-    "ax2 = fig.add_subplot(122, projection='3d')\n",
-    "ax2.plot(x_test_sim[:, 0], x_test_sim[:, 1], x_test_sim[:, 2], 'r--')\n",
-    "ax2.set(xlabel='$x_0$', ylabel='$x_1$',\n",
-    "        zlabel='$x_2$', title='model simulation')\n",
+    "ax2 = fig.add_subplot(122, projection=\"3d\")\n",
+    "ax2.plot(x_test_sim[:, 0], x_test_sim[:, 1], x_test_sim[:, 2], \"r--\")\n",
+    "ax2.set(xlabel=\"$x_0$\", ylabel=\"$x_1$\", zlabel=\"$x_2$\", title=\"model simulation\")\n",
     "\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "a63ce651",
+   "id": "3a7241be",
    "metadata": {},
    "source": [
     "## Different forms of input data\n",
@@ -362,7 +371,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "e544cdd9",
+   "id": "e1c99378",
    "metadata": {},
    "source": [
     "### Single trajectory, pass in collection times"
@@ -371,13 +380,13 @@
   {
    "cell_type": "code",
    "execution_count": 9,
-   "id": "37fa9de9",
+   "id": "51c94b17",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:25.946525Z",
-     "iopub.status.busy": "2023-06-20T21:46:25.946042Z",
-     "iopub.status.idle": "2023-06-20T21:46:25.965291Z",
-     "shell.execute_reply": "2023-06-20T21:46:25.964377Z"
+     "iopub.execute_input": "2023-07-10T22:16:30.364645Z",
+     "iopub.status.busy": "2023-07-10T22:16:30.364310Z",
+     "iopub.status.idle": "2023-07-10T22:16:30.401790Z",
+     "shell.execute_reply": "2023-07-10T22:16:30.401117Z"
     }
    },
    "outputs": [
@@ -399,7 +408,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "60dc424a",
+   "id": "5c0a4988",
    "metadata": {},
    "source": [
     "### Single trajectory, set default timestep\n",
@@ -409,13 +418,13 @@
   {
    "cell_type": "code",
    "execution_count": 10,
-   "id": "a2d4d693",
+   "id": "d4f37f20",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:25.969477Z",
-     "iopub.status.busy": "2023-06-20T21:46:25.969006Z",
-     "iopub.status.idle": "2023-06-20T21:46:25.990431Z",
-     "shell.execute_reply": "2023-06-20T21:46:25.989020Z"
+     "iopub.execute_input": "2023-07-10T22:16:30.405613Z",
+     "iopub.status.busy": "2023-07-10T22:16:30.405165Z",
+     "iopub.status.idle": "2023-07-10T22:16:30.433935Z",
+     "shell.execute_reply": "2023-07-10T22:16:30.433308Z"
     }
    },
    "outputs": [
@@ -437,7 +446,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "3d6dc256",
+   "id": "586dfadc",
    "metadata": {},
    "source": [
     "### Single trajectory, pass in pre-computed derivatives"
@@ -446,13 +455,13 @@
   {
    "cell_type": "code",
    "execution_count": 11,
-   "id": "ae98bb7c",
+   "id": "54fb8544",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:25.994772Z",
-     "iopub.status.busy": "2023-06-20T21:46:25.994286Z",
-     "iopub.status.idle": "2023-06-20T21:46:26.047804Z",
-     "shell.execute_reply": "2023-06-20T21:46:26.046782Z"
+     "iopub.execute_input": "2023-07-10T22:16:30.437622Z",
+     "iopub.status.busy": "2023-07-10T22:16:30.437271Z",
+     "iopub.status.idle": "2023-07-10T22:16:30.499006Z",
+     "shell.execute_reply": "2023-07-10T22:16:30.498083Z"
     }
    },
    "outputs": [
@@ -470,7 +479,7 @@
     "x_dot_true = np.zeros(x_train.shape)\n",
     "for i in range(t_train.size):\n",
     "    x_dot_true[i] = lorenz(t_train[i], x_train[i])\n",
-    "    \n",
+    "\n",
     "model = ps.SINDy()\n",
     "model.fit(x_train, t=t_train, x_dot=x_dot_true)\n",
     "model.print()"
@@ -478,7 +487,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "85468c8a",
+   "id": "a041d0f4",
    "metadata": {},
    "source": [
     "### Multiple trajectories\n",
@@ -488,13 +497,13 @@
   {
    "cell_type": "code",
    "execution_count": 12,
-   "id": "805c3725",
+   "id": "7b8100d4",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:26.052053Z",
-     "iopub.status.busy": "2023-06-20T21:46:26.051474Z",
-     "iopub.status.idle": "2023-06-20T21:46:28.991302Z",
-     "shell.execute_reply": "2023-06-20T21:46:28.989841Z"
+     "iopub.execute_input": "2023-07-10T22:16:30.505088Z",
+     "iopub.status.busy": "2023-07-10T22:16:30.504393Z",
+     "iopub.status.idle": "2023-07-10T22:16:34.241408Z",
+     "shell.execute_reply": "2023-07-10T22:16:34.240353Z"
     }
    },
    "outputs": [
@@ -515,13 +524,16 @@
     "else:\n",
     "    n_trajectories = 2\n",
     "    sample_range = (10, 15)\n",
-    "x0s = np.array([36, 48, 41]) * (\n",
-    "    np.random.rand(n_trajectories, 3) - 0.5\n",
-    ") + np.array([0, 0, 25])\n",
+    "x0s = np.array([36, 48, 41]) * (np.random.rand(n_trajectories, 3) - 0.5) + np.array(\n",
+    "    [0, 0, 25]\n",
+    ")\n",
     "x_train_multi = []\n",
     "for i in range(n_trajectories):\n",
-    "    x_train_multi.append(solve_ivp(lorenz, t_train_span, x0s[i], \n",
-    "                                   t_eval=t_train, **integrator_keywords).y.T)\n",
+    "    x_train_multi.append(\n",
+    "        solve_ivp(\n",
+    "            lorenz, t_train_span, x0s[i], t_eval=t_train, **integrator_keywords\n",
+    "        ).y.T\n",
+    "    )\n",
     "\n",
     "model = ps.SINDy()\n",
     "model.fit(x_train_multi, t=dt, multiple_trajectories=True)\n",
@@ -530,7 +542,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "0f5f1ca4",
+   "id": "23392e7a",
    "metadata": {},
    "source": [
     "### Multiple trajectories, different lengths of time\n",
@@ -540,15 +552,14 @@
   {
    "cell_type": "code",
    "execution_count": 13,
-   "id": "3ad8fe34",
+   "id": "d28cdad8",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:28.995749Z",
-     "iopub.status.busy": "2023-06-20T21:46:28.995280Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.610991Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.609495Z"
-    },
-    "lines_to_next_cell": 1
+     "iopub.execute_input": "2023-07-10T22:16:34.248580Z",
+     "iopub.status.busy": "2023-07-10T22:16:34.246613Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.115252Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.114207Z"
+    }
    },
    "outputs": [
     {
@@ -562,17 +573,18 @@
     }
    ],
    "source": [
-    "x0s = np.array([36, 48, 41]) * (\n",
-    "    np.random.rand(n_trajectories, 3) - 0.5\n",
-    ") + np.array([0, 0, 25])\n",
+    "x0s = np.array([36, 48, 41]) * (np.random.rand(n_trajectories, 3) - 0.5) + np.array(\n",
+    "    [0, 0, 25]\n",
+    ")\n",
     "x_train_multi = []\n",
     "t_train_multi = []\n",
     "for i in range(n_trajectories):\n",
     "    n_samples = np.random.randint(*sample_range)\n",
     "    t = np.arange(0, n_samples * dt, dt)\n",
     "    t_span = (t[0], t[-1])\n",
-    "    x_train_multi.append(solve_ivp(lorenz, t_span, x0s[i], \n",
-    "                                   t_eval=t, **integrator_keywords).y.T)\n",
+    "    x_train_multi.append(\n",
+    "        solve_ivp(lorenz, t_span, x0s[i], t_eval=t, **integrator_keywords).y.T\n",
+    "    )\n",
     "    t_train_multi.append(t)\n",
     "\n",
     "model = ps.SINDy()\n",
@@ -582,8 +594,10 @@
   },
   {
    "cell_type": "markdown",
-   "id": "da3688c6",
-   "metadata": {},
+   "id": "ce972125",
+   "metadata": {
+    "lines_to_next_cell": 2
+   },
    "source": [
     "### Discrete time dynamical system (map)"
    ]
@@ -591,13 +605,13 @@
   {
    "cell_type": "code",
    "execution_count": 14,
-   "id": "1bdabeca",
+   "id": "cae87021",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.615455Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.614973Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.632784Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.631776Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.119870Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.119206Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.138823Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.137649Z"
     }
    },
    "outputs": [
@@ -613,6 +627,7 @@
     "def f(x):\n",
     "    return 3.6 * x * (1 - x)\n",
     "\n",
+    "\n",
     "if __name__ != \"testing\":\n",
     "    n_steps = 1000\n",
     "else:\n",
@@ -630,7 +645,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "a1bea77d",
+   "id": "5929323d",
    "metadata": {},
    "source": [
     "### Pandas DataFrame"
@@ -639,13 +654,13 @@
   {
    "cell_type": "code",
    "execution_count": 15,
-   "id": "156bdb6c",
+   "id": "d16389a1",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.637272Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.636625Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.818949Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.817831Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.145266Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.143642Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.363014Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.360940Z"
     }
    },
    "outputs": [
@@ -664,7 +679,7 @@
     "\n",
     "# Create a dataframe with entries corresponding to measurements and\n",
     "# indexed by the time at which the measurements were taken\n",
-    "df = pd.DataFrame(data=x_train, columns=['x', 'y', 'z'], index=t_train)\n",
+    "df = pd.DataFrame(data=x_train, columns=[\"x\", \"y\", \"z\"], index=t_train)\n",
     "\n",
     "# The column names can be used as feature names\n",
     "model = ps.SINDy(feature_names=df.columns)\n",
@@ -676,7 +691,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "2badd60b",
+   "id": "3022b955",
    "metadata": {},
    "source": [
     "## Optimization options\n",
@@ -685,7 +700,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "3c7ab55c",
+   "id": "6e7c2419",
    "metadata": {},
    "source": [
     "### STLSQ - change parameters"
@@ -694,13 +709,13 @@
   {
    "cell_type": "code",
    "execution_count": 16,
-   "id": "e6ecce63",
+   "id": "4fd7896d",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.823587Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.823105Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.846739Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.845721Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.372097Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.371642Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.407926Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.406898Z"
     }
    },
    "outputs": [
@@ -715,7 +730,7 @@
     }
    ],
    "source": [
-    "stlsq_optimizer = ps.STLSQ(threshold=.01, alpha=.5)\n",
+    "stlsq_optimizer = ps.STLSQ(threshold=0.01, alpha=0.5)\n",
     "\n",
     "model = ps.SINDy(optimizer=stlsq_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
@@ -724,7 +739,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "d41c3235",
+   "id": "c688e7dd",
    "metadata": {},
    "source": [
     "### STLSQ - verbose (print out optimization terms at each iteration)\n",
@@ -735,13 +750,13 @@
   {
    "cell_type": "code",
    "execution_count": 17,
-   "id": "fe45ea18",
+   "id": "de86919c",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.851064Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.850376Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.874284Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.873269Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.413670Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.412605Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.450835Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.449223Z"
     }
    },
    "outputs": [
@@ -760,7 +775,7 @@
     }
    ],
    "source": [
-    "stlsq_optimizer = ps.STLSQ(threshold=.01, alpha=.5, verbose=True)\n",
+    "stlsq_optimizer = ps.STLSQ(threshold=0.01, alpha=0.5, verbose=True)\n",
     "\n",
     "model = ps.SINDy(optimizer=stlsq_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
@@ -769,7 +784,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "d12c35ad",
+   "id": "ac551054",
    "metadata": {},
    "source": [
     "### SR3"
@@ -778,13 +793,13 @@
   {
    "cell_type": "code",
    "execution_count": 18,
-   "id": "00872262",
+   "id": "3b22808e",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.878764Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.878107Z",
-     "iopub.status.idle": "2023-06-20T21:46:29.896052Z",
-     "shell.execute_reply": "2023-06-20T21:46:29.895041Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.459690Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.458990Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.489737Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.488242Z"
     }
    },
    "outputs": [
@@ -799,7 +814,7 @@
     }
    ],
    "source": [
-    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder='l1')\n",
+    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder=\"l1\")\n",
     "\n",
     "model = ps.SINDy(optimizer=sr3_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
@@ -808,7 +823,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "4b6c58db",
+   "id": "c96bf8f2",
    "metadata": {},
    "source": [
     "### SR3 - with trimming\n",
@@ -818,13 +833,13 @@
   {
    "cell_type": "code",
    "execution_count": 19,
-   "id": "ea5ee9ad",
+   "id": "b5a39675",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:29.900501Z",
-     "iopub.status.busy": "2023-06-20T21:46:29.899866Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.088774Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.087759Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.500708Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.499391Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.813808Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.811465Z"
     }
    },
    "outputs": [
@@ -872,23 +887,23 @@
   },
   {
    "cell_type": "markdown",
-   "id": "f8709bd5",
+   "id": "5bd39437",
    "metadata": {},
    "source": [
-    "### SR3 regularizers \n",
+    "### SR3 regularizers\n",
     "The default regularizer with SR3 is the L0 norm, but L1 and L2 are allowed. Note that the pure L2 option is notably less sparse than the other options."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 20,
-   "id": "0fba59ff",
+   "id": "ff14fd01",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.093335Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.092644Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.141282Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.140263Z"
+     "iopub.execute_input": "2023-07-10T22:16:35.820776Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.819768Z",
+     "iopub.status.idle": "2023-07-10T22:16:35.989212Z",
+     "shell.execute_reply": "2023-07-10T22:16:35.988116Z"
     }
    },
    "outputs": [
@@ -903,13 +918,7 @@
       "L1 regularizer: \n",
       "(x0)' = -9.999 x0 + 9.999 x1\n",
       "(x1)' = 27.992 x0 + -0.999 x1 + -1.000 x0 x2\n",
-      "(x2)' = -2.666 x2 + 1.000 x0 x1\n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
+      "(x2)' = -2.666 x2 + 1.000 x0 x1\n",
       "L2 regularizer: \n",
       "(x0)' = -0.001 1 + -10.005 x0 + 10.003 x1\n",
       "(x1)' = -0.015 1 + 27.991 x0 + -0.998 x1 + 0.002 x2 + -1.000 x0 x2\n",
@@ -918,7 +927,7 @@
     }
    ],
    "source": [
-    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder='l0')\n",
+    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder=\"l0\")\n",
     "with ignore_specific_warnings():\n",
     "    model = ps.SINDy(optimizer=sr3_optimizer).fit(x_train, t=dt)\n",
     "print(\"L0 regularizer: \")\n",
@@ -939,7 +948,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "0bde9a47",
+   "id": "b5d58e43",
    "metadata": {},
    "source": [
     "### SR3 - with variable thresholding\n",
@@ -949,13 +958,13 @@
   {
    "cell_type": "code",
    "execution_count": 21,
-   "id": "f092e78e",
+   "id": "f5fd35f6",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.145542Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.144892Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.186019Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.185010Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.000019Z",
+     "iopub.status.busy": "2023-07-10T22:16:35.997909Z",
+     "iopub.status.idle": "2023-07-10T22:16:36.093835Z",
+     "shell.execute_reply": "2023-07-10T22:16:36.092470Z"
     }
    },
    "outputs": [
@@ -966,7 +975,13 @@
       "Threshold = 0.1 for all terms\n",
       "(x0)' = -9.999 x0 + 9.999 x1\n",
       "(x1)' = 27.992 x0 + -0.999 x1 + -1.000 x0 x2\n",
-      "(x2)' = -2.666 x2 + 1.000 x0 x1\n",
+      "(x2)' = -2.666 x2 + 1.000 x0 x1\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
       "Threshold = 0.1 for quadratic terms, else threshold = 1\n",
       "(x0)' = -9.999 x0 + 9.999 x1\n",
       "(x1)' = 25.563 x0 + -0.952 x0 x2\n",
@@ -976,7 +991,7 @@
    ],
    "source": [
     "# Without thresholds matrix\n",
-    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder='l0')\n",
+    "sr3_optimizer = ps.SR3(threshold=0.1, thresholder=\"l0\")\n",
     "model = ps.SINDy(optimizer=sr3_optimizer).fit(x_train, t=dt)\n",
     "print(\"Threshold = 0.1 for all terms\")\n",
     "model.print()\n",
@@ -992,10 +1007,10 @@
   },
   {
    "cell_type": "markdown",
-   "id": "72202b8c",
+   "id": "c764abd4",
    "metadata": {},
    "source": [
-    "It can be seen that the x1 term in the second equation correctly gets truncated with these thresholds. \n",
+    "It can be seen that the x1 term in the second equation correctly gets truncated with these thresholds.\n",
     "\n",
     "### ConstrainedSR3\n",
     "We can impose linear equality and inequality constraints on the coefficients in the `SINDy` model using the `ConstrainedSR3` class. Below we constrain the x0 coefficient in the second equation to be exactly 28 and the x0 and x1 coefficients in the first equations to be negatives of one another. See this [notebook](https://github.com/dynamicslab/pysindy/blob/master/examples/7_plasma_examples.ipynb) for examples."
@@ -1004,13 +1019,13 @@
   {
    "cell_type": "code",
    "execution_count": 22,
-   "id": "0d3b513f",
+   "id": "45744433",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.190519Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.189864Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.197509Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.196534Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.101008Z",
+     "iopub.status.busy": "2023-07-10T22:16:36.099178Z",
+     "iopub.status.idle": "2023-07-10T22:16:36.111986Z",
+     "shell.execute_reply": "2023-07-10T22:16:36.110879Z"
     }
    },
    "outputs": [
@@ -1025,7 +1040,7 @@
    "source": [
     "# Figure out how many library features there will be\n",
     "library = ps.PolynomialLibrary()\n",
-    "library.fit([ps.AxesArray(x_train,{\"ax_sample\":0,\"ax_coord\":1})])\n",
+    "library.fit([ps.AxesArray(x_train, {\"ax_sample\": 0, \"ax_coord\": 1})])\n",
     "n_features = library.n_output_features_\n",
     "print(f\"Features ({n_features}):\", library.get_feature_names())"
    ]
@@ -1033,13 +1048,13 @@
   {
    "cell_type": "code",
    "execution_count": 23,
-   "id": "68f43444",
+   "id": "d4f988e7",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.201840Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.201204Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.236673Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.235652Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.120068Z",
+     "iopub.status.busy": "2023-07-10T22:16:36.118320Z",
+     "iopub.status.idle": "2023-07-10T22:16:36.146422Z",
+     "shell.execute_reply": "2023-07-10T22:16:36.145335Z"
     }
    },
    "outputs": [
@@ -1068,7 +1083,9 @@
     "# 1 * (x0 coefficient) = 28\n",
     "constraint_lhs[1, 1 + n_features] = 1\n",
     "\n",
-    "optimizer = ps.ConstrainedSR3(constraint_rhs=constraint_rhs, constraint_lhs=constraint_lhs)\n",
+    "optimizer = ps.ConstrainedSR3(\n",
+    "    constraint_rhs=constraint_rhs, constraint_lhs=constraint_lhs\n",
+    ")\n",
     "model = ps.SINDy(optimizer=optimizer, feature_library=library).fit(x_train, t=dt)\n",
     "model.print()"
    ]
@@ -1076,13 +1093,13 @@
   {
    "cell_type": "code",
    "execution_count": 24,
-   "id": "074e2c85",
+   "id": "0e487dea",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.241040Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.240398Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.271001Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.269978Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.153208Z",
+     "iopub.status.busy": "2023-07-10T22:16:36.151704Z",
+     "iopub.status.idle": "2023-07-10T22:16:36.196802Z",
+     "shell.execute_reply": "2023-07-10T22:16:36.195534Z"
     }
    },
    "outputs": [
@@ -1098,10 +1115,12 @@
    ],
    "source": [
     "# Try with normalize columns (doesn't work with constraints!!!)\n",
-    "optimizer = ps.ConstrainedSR3(constraint_rhs=constraint_rhs, \n",
-    "                              constraint_lhs=constraint_lhs,\n",
-    "                              normalize_columns=True, \n",
-    "                              threshold=10)\n",
+    "optimizer = ps.ConstrainedSR3(\n",
+    "    constraint_rhs=constraint_rhs,\n",
+    "    constraint_lhs=constraint_lhs,\n",
+    "    normalize_columns=True,\n",
+    "    threshold=10,\n",
+    ")\n",
     "with ignore_specific_warnings():\n",
     "    model = ps.SINDy(optimizer=optimizer, feature_library=library).fit(x_train, t=dt)\n",
     "\n",
@@ -1111,36 +1130,37 @@
   {
    "cell_type": "code",
    "execution_count": 25,
-   "id": "0dd167dd",
+   "id": "2b92fbee",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.292033Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.291568Z",
-     "iopub.status.idle": "2023-06-20T21:46:30.297224Z",
-     "shell.execute_reply": "2023-06-20T21:46:30.296191Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.204658Z",
+     "iopub.status.busy": "2023-07-10T22:16:36.202447Z",
+     "iopub.status.idle": "2023-07-10T22:16:36.217833Z",
+     "shell.execute_reply": "2023-07-10T22:16:36.216518Z"
     }
    },
    "outputs": [],
    "source": [
     "# Repeat with inequality constraints, need CVXPY installed\n",
     "try:\n",
-    "    import cvxpy\n",
+    "    import cvxpy  # noqa: F401\n",
+    "\n",
     "    run_cvxpy = True\n",
     "except ImportError:\n",
     "    run_cvxpy = False\n",
-    "    print('No CVXPY package is installed')"
+    "    print(\"No CVXPY package is installed\")"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 26,
-   "id": "9db21c89",
+   "id": "69d1327b",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:30.301505Z",
-     "iopub.status.busy": "2023-06-20T21:46:30.300789Z",
-     "iopub.status.idle": "2023-06-20T21:46:39.716658Z",
-     "shell.execute_reply": "2023-06-20T21:46:39.715592Z"
+     "iopub.execute_input": "2023-07-10T22:16:36.226261Z",
+     "iopub.status.busy": "2023-07-10T22:16:36.224103Z",
+     "iopub.status.idle": "2023-07-10T22:16:56.423455Z",
+     "shell.execute_reply": "2023-07-10T22:16:56.422760Z"
     }
    },
    "outputs": [
@@ -1182,23 +1202,22 @@
     "        thresholder=\"l1\",\n",
     "        tol=1e-7,\n",
     "        threshold=10,\n",
-    "        max_iter=10000\n",
+    "        max_iter=10000,\n",
     "    )\n",
-    "    model = ps.SINDy(optimizer=optimizer, \n",
-    "                     feature_library=library).fit(x_train, t=dt)\n",
+    "    model = ps.SINDy(optimizer=optimizer, feature_library=library).fit(x_train, t=dt)\n",
     "    model.print()\n",
     "    print(optimizer.coef_[0, 1], optimizer.coef_[0, 2])"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "5bbd4e36",
+   "id": "ab8789a5",
    "metadata": {},
    "source": [
     "### MIOSR\n",
-    "Mixed-integer optimized sparse regression (MIOSR) is an optimizer which solves the NP-hard subset selection problem to provable optimality using techniques from mixed-integer optimization. This optimizer is expected to be most performant compared to heuristics in settings with limited data or on systems with small coefficents. See Bertsimas, Dimitris, and Wes Gurnee. \"Learning Sparse Nonlinear Dynamics via Mixed-Integer Optimization.\" arXiv preprint arXiv:2206.00176 (2022). for details.\n",
+    "Mixed-integer optimized sparse regression (MIOSR) is an optimizer which solves the NP-hard subset selection problem to provable optimality using techniques from mixed-integer optimization. This optimizer is expected to be most performant compared to heuristics in settings with limited data or on systems with small coefficients. See Bertsimas, Dimitris, and Wes Gurnee. \"Learning Sparse Nonlinear Dynamics via Mixed-Integer Optimization.\" arXiv preprint arXiv:2206.00176 (2022). for details.\n",
     "\n",
-    "Note, MIOSR requires `gurobipy` as a dependency. Please either `pip install gurobipy` or `pip install pysindy[miosr]`. \n",
+    "Note, MIOSR requires `gurobipy` as a dependency. Please either `pip install gurobipy` or `pip install pysindy[miosr]`.\n",
     "\n",
     "Gurobi is a private company, but a limited academic license is available when you pip install. If you have previously installed `gurobipy` but your license has expired, `import gurobipy` will work but using the functionality will throw a `GurobiError`. See [here](https://support.gurobi.com/hc/en-us/articles/360038967271-How-do-I-renew-my-free-individual-academic-or-free-trial-license-) for how to renew a free academic license."
    ]
@@ -1206,19 +1225,20 @@
   {
    "cell_type": "code",
    "execution_count": 27,
-   "id": "71015afc",
+   "id": "90b5a4c4",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:39.721010Z",
-     "iopub.status.busy": "2023-06-20T21:46:39.720562Z",
-     "iopub.status.idle": "2023-06-20T21:46:39.727634Z",
-     "shell.execute_reply": "2023-06-20T21:46:39.726575Z"
+     "iopub.execute_input": "2023-07-10T22:16:56.430278Z",
+     "iopub.status.busy": "2023-07-10T22:16:56.429670Z",
+     "iopub.status.idle": "2023-07-10T22:16:56.442115Z",
+     "shell.execute_reply": "2023-07-10T22:16:56.439989Z"
     }
    },
    "outputs": [],
    "source": [
     "try:\n",
     "    import gurobipy\n",
+    "\n",
     "    run_miosr = True\n",
     "    GurobiError = gurobipy.GurobiError\n",
     "except ImportError:\n",
@@ -1227,7 +1247,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "e7b2cf08",
+   "id": "380f4e4a",
    "metadata": {},
    "source": [
     "MIOSR can handle sparsity constraints in two ways: dimensionwise sparsity where the algorithm is fit once per each dimension, and global sparsity, where all dimensions are fit jointly to respect the overall sparsity constraint. Additionally, MIOSR can handle constraints and can be adapted to implement custom constraints by advanced users.\n",
@@ -1238,13 +1258,13 @@
   {
    "cell_type": "code",
    "execution_count": 28,
-   "id": "24795b5c",
+   "id": "66384243",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:39.732015Z",
-     "iopub.status.busy": "2023-06-20T21:46:39.731413Z",
-     "iopub.status.idle": "2023-06-20T21:46:39.793251Z",
-     "shell.execute_reply": "2023-06-20T21:46:39.792231Z"
+     "iopub.execute_input": "2023-07-10T22:16:56.446891Z",
+     "iopub.status.busy": "2023-07-10T22:16:56.446497Z",
+     "iopub.status.idle": "2023-07-10T22:16:56.559429Z",
+     "shell.execute_reply": "2023-07-10T22:16:56.558363Z"
     }
    },
    "outputs": [
@@ -1273,19 +1293,19 @@
     "        model.fit(x_train, t=dt)\n",
     "        model.print()\n",
     "    except GurobiError:\n",
-    "        print('User has an expired gurobi license')"
+    "        print(\"User has an expired gurobi license\")"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 29,
-   "id": "f182c920",
+   "id": "3bcdd95b",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:39.797497Z",
-     "iopub.status.busy": "2023-06-20T21:46:39.796934Z",
-     "iopub.status.idle": "2023-06-20T21:46:39.888024Z",
-     "shell.execute_reply": "2023-06-20T21:46:39.887060Z"
+     "iopub.execute_input": "2023-07-10T22:16:56.566643Z",
+     "iopub.status.busy": "2023-07-10T22:16:56.565273Z",
+     "iopub.status.idle": "2023-07-10T22:16:56.788284Z",
+     "shell.execute_reply": "2023-07-10T22:16:56.787086Z"
     }
    },
    "outputs": [
@@ -1308,12 +1328,12 @@
     "        model.fit(x_train, t=dt)\n",
     "        model.print()\n",
     "    except GurobiError:\n",
-    "        print('User does not have a gurobi license')"
+    "        print(\"User does not have a gurobi license\")"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "3642849e",
+   "id": "9d349b85",
    "metadata": {},
    "source": [
     "### MIOSR (verbose) with equality constraints"
@@ -1322,13 +1342,13 @@
   {
    "cell_type": "code",
    "execution_count": 30,
-   "id": "bef3b652",
+   "id": "b80bd8ef",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:39.892358Z",
-     "iopub.status.busy": "2023-06-20T21:46:39.891791Z",
-     "iopub.status.idle": "2023-06-20T21:46:39.981400Z",
-     "shell.execute_reply": "2023-06-20T21:46:39.980372Z"
+     "iopub.execute_input": "2023-07-10T22:16:56.795979Z",
+     "iopub.status.busy": "2023-07-10T22:16:56.793203Z",
+     "iopub.status.idle": "2023-07-10T22:16:56.999445Z",
+     "shell.execute_reply": "2023-07-10T22:16:56.998234Z"
     }
    },
    "outputs": [
@@ -1637,7 +1657,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Explored 23 nodes (166 simplex iterations) in 0.05 seconds (0.00 work units)\n"
+      "Explored 23 nodes (166 simplex iterations) in 0.10 seconds (0.00 work units)\n"
      ]
     },
     {
@@ -1721,19 +1741,19 @@
     "            constraint_rhs=constraint_rhs,\n",
     "            constraint_lhs=constraint_lhs,\n",
     "            verbose=True,  # print the full gurobi log\n",
-    "            target_sparsity=7\n",
+    "            target_sparsity=7,\n",
     "        )\n",
     "        model = ps.SINDy(optimizer=miosr_optimizer, feature_library=library)\n",
     "        model.fit(x_train, t=dt)\n",
     "        model.print()\n",
     "        print(optimizer.coef_[0, 1], optimizer.coef_[0, 2])\n",
     "    except GurobiError:\n",
-    "        print('User does not have a gurobi license')"
+    "        print(\"User does not have a gurobi license\")"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "7ac63f13",
+   "id": "1ff22310",
    "metadata": {},
    "source": [
     "See the [gurobi documentation](https://www.gurobi.com/documentation/9.5/refman/mip_logging.html) for more information on how to read the log output and this [tutorial](https://www.gurobi.com/resource/mip-basics/) on the basics of mixed-integer optimization."
@@ -1741,7 +1761,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "797e7674",
+   "id": "ccd42080",
    "metadata": {},
    "source": [
     "### SSR (greedy algorithm)\n",
@@ -1751,13 +1771,13 @@
   {
    "cell_type": "code",
    "execution_count": 31,
-   "id": "93cc2d86",
+   "id": "c4dba59c",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:39.985902Z",
-     "iopub.status.busy": "2023-06-20T21:46:39.985344Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.039639Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.038605Z"
+     "iopub.execute_input": "2023-07-10T22:16:57.004933Z",
+     "iopub.status.busy": "2023-07-10T22:16:57.004425Z",
+     "iopub.status.idle": "2023-07-10T22:16:57.282098Z",
+     "shell.execute_reply": "2023-07-10T22:16:57.276010Z"
     }
    },
    "outputs": [
@@ -1772,7 +1792,7 @@
     }
    ],
    "source": [
-    "ssr_optimizer = ps.SSR(alpha=.05)\n",
+    "ssr_optimizer = ps.SSR(alpha=0.05)\n",
     "\n",
     "model = ps.SINDy(optimizer=ssr_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
@@ -1781,7 +1801,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "cd8b355f",
+   "id": "7473172a",
    "metadata": {},
    "source": [
     "The alpha parameter is the same here as in the STLSQ optimizer. It determines the amount of L2 regularization to use, so if alpha is nonzero, this is doing Ridge regression rather than least-squares regression."
@@ -1790,13 +1810,13 @@
   {
    "cell_type": "code",
    "execution_count": 32,
-   "id": "f449c648",
+   "id": "8b4c7f42",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.043921Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.043355Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.255697Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.254737Z"
+     "iopub.execute_input": "2023-07-10T22:16:57.289360Z",
+     "iopub.status.busy": "2023-07-10T22:16:57.287576Z",
+     "iopub.status.idle": "2023-07-10T22:16:57.961689Z",
+     "shell.execute_reply": "2023-07-10T22:16:57.960191Z"
     }
    },
    "outputs": [
@@ -1811,7 +1831,7 @@
     }
    ],
    "source": [
-    "ssr_optimizer = ps.SSR(alpha=.05, criteria=\"model_residual\")\n",
+    "ssr_optimizer = ps.SSR(alpha=0.05, criteria=\"model_residual\")\n",
     "model = ps.SINDy(optimizer=ssr_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
@@ -1819,7 +1839,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "c8475693",
+   "id": "d926ac07",
    "metadata": {},
    "source": [
     "The kappa parameter determines how sparse a solution is desired."
@@ -1828,13 +1848,13 @@
   {
    "cell_type": "code",
    "execution_count": 33,
-   "id": "e4dcb82f",
+   "id": "1498fd1a",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.259993Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.259429Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.469482Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.468551Z"
+     "iopub.execute_input": "2023-07-10T22:16:57.976617Z",
+     "iopub.status.busy": "2023-07-10T22:16:57.973295Z",
+     "iopub.status.idle": "2023-07-10T22:16:58.477001Z",
+     "shell.execute_reply": "2023-07-10T22:16:58.476119Z"
     }
    },
    "outputs": [
@@ -1849,7 +1869,7 @@
     }
    ],
    "source": [
-    "ssr_optimizer = ps.SSR(alpha=.05, criteria=\"model_residual\", kappa=1e-3)\n",
+    "ssr_optimizer = ps.SSR(alpha=0.05, criteria=\"model_residual\", kappa=1e-3)\n",
     "model = ps.SINDy(optimizer=ssr_optimizer)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
@@ -1857,7 +1877,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "996b7f27",
+   "id": "d712dc8e",
    "metadata": {},
    "source": [
     "### FROLS (greedy algorithm)\n",
@@ -1867,13 +1887,13 @@
   {
    "cell_type": "code",
    "execution_count": 34,
-   "id": "e8de1f3e",
+   "id": "9019e37f",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.473748Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.473195Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.563518Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.562539Z"
+     "iopub.execute_input": "2023-07-10T22:16:58.490423Z",
+     "iopub.status.busy": "2023-07-10T22:16:58.483344Z",
+     "iopub.status.idle": "2023-07-10T22:16:58.659625Z",
+     "shell.execute_reply": "2023-07-10T22:16:58.657314Z"
     }
    },
    "outputs": [
@@ -1888,7 +1908,7 @@
     }
    ],
    "source": [
-    "optimizer = ps.FROLS(alpha=.05)\n",
+    "optimizer = ps.FROLS(alpha=0.05)\n",
     "model = ps.SINDy(optimizer=optimizer)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
@@ -1896,7 +1916,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "48057870",
+   "id": "6f0c8cea",
    "metadata": {},
    "source": [
     "The kappa parameter determines how sparse a solution is desired."
@@ -1905,13 +1925,13 @@
   {
    "cell_type": "code",
    "execution_count": 35,
-   "id": "ee5bb175",
+   "id": "d2cba441",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.567832Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.567264Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.657014Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.656504Z"
+     "iopub.execute_input": "2023-07-10T22:16:58.665642Z",
+     "iopub.status.busy": "2023-07-10T22:16:58.664869Z",
+     "iopub.status.idle": "2023-07-10T22:16:58.801656Z",
+     "shell.execute_reply": "2023-07-10T22:16:58.799647Z"
     }
    },
    "outputs": [
@@ -1926,7 +1946,7 @@
     }
    ],
    "source": [
-    "optimizer = ps.FROLS(alpha=.05, kappa=1e-7)\n",
+    "optimizer = ps.FROLS(alpha=0.05, kappa=1e-7)\n",
     "model = ps.SINDy(optimizer=optimizer)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
@@ -1934,7 +1954,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "4df21047",
+   "id": "1b825aed",
    "metadata": {},
    "source": [
     "### LASSO\n",
@@ -1944,13 +1964,13 @@
   {
    "cell_type": "code",
    "execution_count": 36,
-   "id": "ff3689c2",
+   "id": "8ce86588",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.659795Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.659343Z",
-     "iopub.status.idle": "2023-06-20T21:46:40.890585Z",
-     "shell.execute_reply": "2023-06-20T21:46:40.889620Z"
+     "iopub.execute_input": "2023-07-10T22:16:58.806909Z",
+     "iopub.status.busy": "2023-07-10T22:16:58.806374Z",
+     "iopub.status.idle": "2023-07-10T22:16:59.441026Z",
+     "shell.execute_reply": "2023-07-10T22:16:59.439929Z"
     }
    },
    "outputs": [
@@ -1974,7 +1994,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "dee9cb93",
+   "id": "e1f4e16a",
    "metadata": {},
    "source": [
     "### Ensemble methods\n",
@@ -1984,16 +2004,35 @@
   {
    "cell_type": "code",
    "execution_count": 37,
-   "id": "d225a814",
+   "id": "a12783a0",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:40.894946Z",
-     "iopub.status.busy": "2023-06-20T21:46:40.894375Z",
-     "iopub.status.idle": "2023-06-20T21:46:41.715164Z",
-     "shell.execute_reply": "2023-06-20T21:46:41.714389Z"
+     "iopub.execute_input": "2023-07-10T22:16:59.450448Z",
+     "iopub.status.busy": "2023-07-10T22:16:59.449508Z",
+     "iopub.status.idle": "2023-07-10T22:17:02.319193Z",
+     "shell.execute_reply": "2023-07-10T22:17:02.318049Z"
     }
    },
    "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[Text(0, 0, '$1$'),\n",
+       " Text(1, 0, '$x$'),\n",
+       " Text(2, 0, '$y$'),\n",
+       " Text(3, 0, '$z$'),\n",
+       " Text(4, 0, '$x^2$'),\n",
+       " Text(5, 0, '$x y$'),\n",
+       " Text(6, 0, '$x z$'),\n",
+       " Text(7, 0, '$y^2$'),\n",
+       " Text(8, 0, '$y z$'),\n",
+       " Text(9, 0, '$z^2$')]"
+      ]
+     },
+     "execution_count": 37,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
     {
      "data": {
       "image/png": "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nVrneOihh6AoCqZOnYr//e9/FtdwVqV79+5o06YNXn/9dVy7dq3S47XtCwHgjz/+qNRmOspk6g9HjRqFnJwcq33D9evXUVBQUOv1VqekpATvvvuuebq4uBjvvvsuAgMD0b17d5uuCyjbrwMHDsSWLVtw9uxZc/uJEyewc+dOm6+PHINHgqjReOWVV5CWloYePXrgiSeeQEREBP744w8cOXIEn3/+udU38qrs3bsXkyZNwl//+lfcdtttKCkpwdq1a83FFlD2iduCBQswc+ZMnDp1CsOHD4ePjw+ysrKwefNmPPnkk3j22Wdt+jtmZWXhgQcewODBg3HgwAGsW7cODz/8MLp27WrT9Zg89dRTePvtt/HQQw9h6tSpCAkJwUcffWS+2RzvjUBEjU1AQAB69eqFxx57DOfOncOSJUvQtm3bGg0I07NnTzRt2hRjx47FlClToCgK1q5dW6fTqgIDAzF48GB88skn8Pf3x/3333/TZVxcXPDBBx8gNjYWt99+Ox577DG0aNECOTk5SEtLg6+vL7Zt21arHPPmzUNGRgbuv/9+tGrVCufPn8c777wDnU6HXr16AQAeffRRfPzxx3j66aeRlpaGe++9F0ajEb/88gs+/vhj7N69G3feeWett0FVQkND8eqrr+LUqVO47bbb8O9//xuZmZl47733oNVqbbae8l566SXs2bMH9957LyZMmACj0Yi3334bnTp1QmZmpl3WSfbFIogajebNm+M///kP5s2bh5SUFLzzzjto1qwZbr/9dvP9Dmqqa9euiImJwbZt25CTkwNPT0907doVO3fuxD333GOe77nnnsNtt92GN998E3PnzgVQdu5zdHS0xYhqtvLvf/8bs2fPxnPPPQdXV1dMmjSp0n0nbMl0j4vJkycjKSkJ3t7eGDNmDHr27IkRI0bwzttE1Og8//zz+P7775GYmIirV69iwIABeOedd+Dp6XnTZZs1a4bt27fjmWeewaxZs9C0aVM88sgjGDBgAGJiYmqdZcyYMdi+fTtGjRoFd3f3Gi0TFRWFAwcOYP78+Xj77bdx7do1BAcHo0ePHhYjqtXUAw88gFOnTuHDDz/EhQsXcMstt6Bv376YO3eueeACFxcXbNmyBW+++SbWrFmDzZs3w9PTE7feeiumTp1q81PGmzZtitWrV2Py5Ml4//330bx5c7z99tt2Hbm0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",
@@ -2006,17 +2045,17 @@
     }
    ],
    "source": [
-    "# Default is to sample the entire time series with replacement, generating 10 models on roughly \n",
-    "# 60% of the total data, with duplicates. \n",
+    "# Default is to sample the entire time series with replacement, generating 10 models on roughly\n",
+    "# 60% of the total data, with duplicates.\n",
     "\n",
     "# Custom feature names\n",
     "np.random.seed(100)\n",
-    "feature_names = ['x', 'y', 'z']\n",
+    "feature_names = [\"x\", \"y\", \"z\"]\n",
     "\n",
     "ensemble_optimizer = ps.EnsembleOptimizer(\n",
-    "    ps.STLSQ(threshold=1e-3,normalize_columns=False),\n",
+    "    ps.STLSQ(threshold=1e-3, normalize_columns=False),\n",
     "    bagging=True,\n",
-    "    n_subset=int(0.6*x_train.shape[0])\n",
+    "    n_subset=int(0.6 * x_train.shape[0]),\n",
     ")\n",
     "\n",
     "model = ps.SINDy(optimizer=ensemble_optimizer, feature_names=feature_names)\n",
@@ -2026,8 +2065,8 @@
     "std_ensemble = np.std(ensemble_coefs, axis=0)\n",
     "\n",
     "# Now we sub-sample the library. The default is to omit a single library term.\n",
-    "library_ensemble_optimizer=ps.EnsembleOptimizer(\n",
-    "    ps.STLSQ(threshold=1e-3,normalize_columns=False),library_ensemble=True\n",
+    "library_ensemble_optimizer = ps.EnsembleOptimizer(\n",
+    "    ps.STLSQ(threshold=1e-3, normalize_columns=False), library_ensemble=True\n",
     ")\n",
     "model = ps.SINDy(optimizer=library_ensemble_optimizer, feature_names=feature_names)\n",
     "\n",
@@ -2039,35 +2078,45 @@
     "# Plot results\n",
     "xticknames = model.get_feature_names()\n",
     "for i in range(10):\n",
-    "    xticknames[i] = '$' + xticknames[i] + '$'\n",
+    "    xticknames[i] = \"$\" + xticknames[i] + \"$\"\n",
     "plt.figure(figsize=(10, 4))\n",
-    "colors = ['b', 'r', 'k']\n",
+    "colors = [\"b\", \"r\", \"k\"]\n",
     "plt.subplot(1, 2, 1)\n",
-    "plt.title('ensembling')\n",
+    "plt.title(\"ensembling\")\n",
     "for i in range(3):\n",
-    "    plt.errorbar(range(10), mean_ensemble[i, :], yerr=std_ensemble[i, :], \n",
-    "                 fmt='o', color=colors[i],\n",
-    "                 label=r'Equation for $\\dot{' + feature_names[i] + r'}$')\n",
+    "    plt.errorbar(\n",
+    "        range(10),\n",
+    "        mean_ensemble[i, :],\n",
+    "        yerr=std_ensemble[i, :],\n",
+    "        fmt=\"o\",\n",
+    "        color=colors[i],\n",
+    "        label=r\"Equation for $\\dot{\" + feature_names[i] + r\"}$\",\n",
+    "    )\n",
     "ax = plt.gca()\n",
     "plt.grid(True)\n",
     "ax.set_xticks(range(10))\n",
-    "ax.set_xticklabels(xticknames, verticalalignment='top')\n",
+    "ax.set_xticklabels(xticknames, verticalalignment=\"top\")\n",
     "plt.subplot(1, 2, 2)\n",
-    "plt.title('library ensembling')\n",
+    "plt.title(\"library ensembling\")\n",
     "for i in range(3):\n",
-    "    plt.errorbar(range(10), mean_library_ensemble[i, :], yerr=std_library_ensemble[i, :], \n",
-    "                 fmt='o', color=colors[i], \n",
-    "                 label=r'Equation for $\\dot{' + feature_names[i] + r'}$')\n",
+    "    plt.errorbar(\n",
+    "        range(10),\n",
+    "        mean_library_ensemble[i, :],\n",
+    "        yerr=std_library_ensemble[i, :],\n",
+    "        fmt=\"o\",\n",
+    "        color=colors[i],\n",
+    "        label=r\"Equation for $\\dot{\" + feature_names[i] + r\"}$\",\n",
+    "    )\n",
     "ax = plt.gca()\n",
     "plt.grid(True)\n",
     "plt.legend()\n",
     "ax.set_xticks(range(10))\n",
-    "ax.set_xticklabels(xticknames, verticalalignment='top');"
+    "ax.set_xticklabels(xticknames, verticalalignment=\"top\")"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "598e3d76",
+   "id": "e9e66c5a",
    "metadata": {},
    "source": [
     "## Differentiation options"
@@ -2075,7 +2124,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "74899504",
+   "id": "0bbd99bd",
    "metadata": {},
    "source": [
     "### Pass in pre-computed derivatives\n",
@@ -2085,13 +2134,13 @@
   {
    "cell_type": "code",
    "execution_count": 38,
-   "id": "aaf84c81",
+   "id": "d1eb6eef",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:41.717222Z",
-     "iopub.status.busy": "2023-06-20T21:46:41.717013Z",
-     "iopub.status.idle": "2023-06-20T21:46:41.737138Z",
-     "shell.execute_reply": "2023-06-20T21:46:41.736068Z"
+     "iopub.execute_input": "2023-07-10T22:17:02.324288Z",
+     "iopub.status.busy": "2023-07-10T22:17:02.323414Z",
+     "iopub.status.idle": "2023-07-10T22:17:02.399135Z",
+     "shell.execute_reply": "2023-07-10T22:17:02.396198Z"
     }
    },
    "outputs": [
@@ -2107,7 +2156,7 @@
    ],
    "source": [
     "x_dot_precomputed = ps.FiniteDifference()._differentiate(x_train, t_train)\n",
-    "    \n",
+    "\n",
     "model = ps.SINDy()\n",
     "model.fit(x_train, t=t_train, x_dot=x_dot_precomputed)\n",
     "model.print()"
@@ -2115,7 +2164,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "7ba66d4b",
+   "id": "b7282b3a",
    "metadata": {},
    "source": [
     "### Drop end points from finite difference computation\n",
@@ -2125,13 +2174,13 @@
   {
    "cell_type": "code",
    "execution_count": 39,
-   "id": "db88be22",
+   "id": "85e5e50b",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:41.741452Z",
-     "iopub.status.busy": "2023-06-20T21:46:41.741044Z",
-     "iopub.status.idle": "2023-06-20T21:46:41.767288Z",
-     "shell.execute_reply": "2023-06-20T21:46:41.766373Z"
+     "iopub.execute_input": "2023-07-10T22:17:02.414665Z",
+     "iopub.status.busy": "2023-07-10T22:17:02.413568Z",
+     "iopub.status.idle": "2023-07-10T22:17:02.506468Z",
+     "shell.execute_reply": "2023-07-10T22:17:02.502118Z"
     }
    },
    "outputs": [
@@ -2147,7 +2196,7 @@
    ],
    "source": [
     "fd_drop_endpoints = ps.FiniteDifference(drop_endpoints=True)\n",
-    "    \n",
+    "\n",
     "model = ps.SINDy(differentiation_method=fd_drop_endpoints)\n",
     "model.fit(x_train, t=t_train)\n",
     "model.print()"
@@ -2155,7 +2204,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "af069a42",
+   "id": "f8ab1227",
    "metadata": {},
    "source": [
     "### Differentiation along specific array axis\n",
@@ -2165,13 +2214,13 @@
   {
    "cell_type": "code",
    "execution_count": 40,
-   "id": "9ddb97b4",
+   "id": "1bb8c457",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:41.772027Z",
-     "iopub.status.busy": "2023-06-20T21:46:41.771565Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.299962Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.299255Z"
+     "iopub.execute_input": "2023-07-10T22:17:02.521878Z",
+     "iopub.status.busy": "2023-07-10T22:17:02.521476Z",
+     "iopub.status.idle": "2023-07-10T22:17:03.781133Z",
+     "shell.execute_reply": "2023-07-10T22:17:03.779960Z"
     }
    },
    "outputs": [
@@ -2200,31 +2249,31 @@
     "from scipy.io import loadmat\n",
     "\n",
     "# Load the data stored in a matlab .mat file\n",
-    "kdV = loadmat(data / 'kdv.mat')\n",
-    "t = np.ravel(kdV['t'])\n",
-    "X = np.ravel(kdV['x'])\n",
-    "x = np.real(kdV['usol'])\n",
+    "kdV = loadmat(data / \"kdv.mat\")\n",
+    "t = np.ravel(kdV[\"t\"])\n",
+    "X = np.ravel(kdV[\"x\"])\n",
+    "x = np.real(kdV[\"usol\"])\n",
     "dt_kdv = t[1] - t[0]\n",
     "\n",
     "# Plot x and x_dot\n",
     "plt.figure()\n",
     "plt.pcolormesh(t, X, x)\n",
-    "plt.xlabel('t', fontsize=16)\n",
-    "plt.ylabel('X', fontsize=16)\n",
-    "plt.title(r'$u(x, t)$', fontsize=16)\n",
+    "plt.xlabel(\"t\", fontsize=16)\n",
+    "plt.ylabel(\"X\", fontsize=16)\n",
+    "plt.title(r\"$u(x, t)$\", fontsize=16)\n",
     "plt.figure()\n",
     "x_dot = ps.FiniteDifference(axis=1)._differentiate(x, t=dt_kdv)\n",
     "\n",
     "plt.pcolormesh(t, X, x_dot)\n",
-    "plt.xlabel('t', fontsize=16)\n",
-    "plt.ylabel('x', fontsize=16)\n",
-    "plt.title(r'$\\dot{u}(x, t)$', fontsize=16)\n",
+    "plt.xlabel(\"t\", fontsize=16)\n",
+    "plt.ylabel(\"x\", fontsize=16)\n",
+    "plt.title(r\"$\\dot{u}(x, t)$\", fontsize=16)\n",
     "plt.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "255f91e1",
+   "id": "a8bbab58",
    "metadata": {},
    "source": [
     "### Smoothed finite difference\n",
@@ -2234,13 +2283,13 @@
   {
    "cell_type": "code",
    "execution_count": 41,
-   "id": "ab6fc9a6",
+   "id": "5198a508",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.302763Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.302515Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.325238Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.324174Z"
+     "iopub.execute_input": "2023-07-10T22:17:03.788558Z",
+     "iopub.status.busy": "2023-07-10T22:17:03.787514Z",
+     "iopub.status.idle": "2023-07-10T22:17:03.831002Z",
+     "shell.execute_reply": "2023-07-10T22:17:03.829773Z"
     }
    },
    "outputs": [
@@ -2256,7 +2305,7 @@
    ],
    "source": [
     "smoothed_fd = ps.SmoothedFiniteDifference()\n",
-    "    \n",
+    "\n",
     "model = ps.SINDy(differentiation_method=smoothed_fd)\n",
     "model.fit(x_train, t=t_train)\n",
     "model.print()"
@@ -2264,7 +2313,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "f0a1d04a",
+   "id": "2e2e9de2",
    "metadata": {},
    "source": [
     "### More differentiation options\n",
@@ -2276,13 +2325,13 @@
   {
    "cell_type": "code",
    "execution_count": 42,
-   "id": "de627167",
+   "id": "cb9dc338",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.329515Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.329050Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.547182Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.545771Z"
+     "iopub.execute_input": "2023-07-10T22:17:03.842575Z",
+     "iopub.status.busy": "2023-07-10T22:17:03.841837Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.472261Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.470783Z"
     }
    },
    "outputs": [
@@ -2306,7 +2355,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "f285f10c",
+   "id": "c819a9d3",
    "metadata": {},
    "source": [
     "## Feature libraries"
@@ -2314,7 +2363,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "86f64b38",
+   "id": "339bcdfd",
    "metadata": {},
    "source": [
     "### Custom feature names"
@@ -2323,13 +2372,13 @@
   {
    "cell_type": "code",
    "execution_count": 43,
-   "id": "f1179eec",
+   "id": "37bac5fb",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.551719Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.551312Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.573958Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.572953Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.481487Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.480419Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.555542Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.552341Z"
     }
    },
    "outputs": [
@@ -2344,7 +2393,7 @@
     }
    ],
    "source": [
-    "feature_names = ['x', 'y', 'z']\n",
+    "feature_names = [\"x\", \"y\", \"z\"]\n",
     "model = ps.SINDy(feature_names=feature_names)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
@@ -2352,7 +2401,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "c6be7ef5",
+   "id": "ef557633",
    "metadata": {},
    "source": [
     "### Custom left-hand side when printing the model"
@@ -2361,13 +2410,13 @@
   {
    "cell_type": "code",
    "execution_count": 44,
-   "id": "6c98c0c5",
+   "id": "c763350d",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.578348Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.577528Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.598620Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.597615Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.566558Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.562733Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.632744Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.631557Z"
     }
    },
    "outputs": [
@@ -2384,12 +2433,12 @@
    "source": [
     "model = ps.SINDy()\n",
     "model.fit(x_train, t=dt)\n",
-    "model.print(lhs=['dx0/dt', 'dx1/dt', 'dx2/dt'])"
+    "model.print(lhs=[\"dx0/dt\", \"dx1/dt\", \"dx2/dt\"])"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "685d5da4",
+   "id": "ad61f1f8",
    "metadata": {},
    "source": [
     "### Customize polynomial library\n",
@@ -2399,13 +2448,13 @@
   {
    "cell_type": "code",
    "execution_count": 45,
-   "id": "7c048fdf",
+   "id": "7edcbe2b",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.602807Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.602230Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.624511Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.623519Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.641139Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.639647Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.686456Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.683982Z"
     }
    },
    "outputs": [
@@ -2422,15 +2471,14 @@
    "source": [
     "poly_library = ps.PolynomialLibrary(include_interaction=False)\n",
     "\n",
-    "model = ps.SINDy(feature_library=poly_library,            \n",
-    "                 optimizer=ps.STLSQ(threshold=0.5))\n",
+    "model = ps.SINDy(feature_library=poly_library, optimizer=ps.STLSQ(threshold=0.5))\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "53016663",
+   "id": "6a0005b1",
    "metadata": {},
    "source": [
     "### Fourier library"
@@ -2439,13 +2487,13 @@
   {
    "cell_type": "code",
    "execution_count": 46,
-   "id": "c7566d99",
+   "id": "3f4ae1b6",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.628662Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.628109Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.661330Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.660331Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.696298Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.693099Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.762238Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.760532Z"
     }
    },
    "outputs": [
@@ -2462,15 +2510,14 @@
    "source": [
     "fourier_library = ps.FourierLibrary(n_frequencies=3)\n",
     "\n",
-    "model = ps.SINDy(feature_library=fourier_library, \n",
-    "                 optimizer=ps.STLSQ(threshold=4))\n",
+    "model = ps.SINDy(feature_library=fourier_library, optimizer=ps.STLSQ(threshold=4))\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "c7613445",
+   "id": "84b54762",
    "metadata": {},
    "source": [
     "### Fully custom library\n",
@@ -2480,13 +2527,13 @@
   {
    "cell_type": "code",
    "execution_count": 47,
-   "id": "fb58dbd3",
+   "id": "6e194b98",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.665744Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.665119Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.717420Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.716385Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.770259Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.769558Z",
+     "iopub.status.idle": "2023-07-10T22:17:04.894807Z",
+     "shell.execute_reply": "2023-07-10T22:17:04.893259Z"
     }
    },
    "outputs": [
@@ -2494,7 +2541,14 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "(x0)' = -9.999 x0 + 9.999 x1\n",
+      "(x0)' = -9.999 x0 + 9.999 x1"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
       "(x1)' = 1.197 1/x0 + -50.011 1/x2 + -12.462 x0 + 9.291 x1 + 0.383 x2 + 0.882 sin(x0,x1) + 1.984 sin(x0,x2) + -0.464 sin(x1,x2)\n",
       "(x2)' = 0.874 1/x0 + -8.545 1/x2 + 0.114 x0 + 0.147 x1 + 3.659 sin(x0,x1) + -3.302 sin(x0,x2) + -3.094 sin(x1,x2)\n"
      ]
@@ -2502,16 +2556,16 @@
    ],
    "source": [
     "library_functions = [\n",
-    "    lambda x : np.exp(x),\n",
-    "    lambda x : 1. / x,\n",
-    "    lambda x : x,\n",
-    "    lambda x,y : np.sin(x + y)\n",
+    "    lambda x: np.exp(x),\n",
+    "    lambda x: 1.0 / x,\n",
+    "    lambda x: x,\n",
+    "    lambda x, y: np.sin(x + y),\n",
     "]\n",
     "library_function_names = [\n",
-    "    lambda x : 'exp(' + x + ')',\n",
-    "    lambda x : '1/' + x,\n",
-    "    lambda x : x,\n",
-    "    lambda x,y : 'sin(' + x + ',' + y + ')'\n",
+    "    lambda x: \"exp(\" + x + \")\",\n",
+    "    lambda x: \"1/\" + x,\n",
+    "    lambda x: x,\n",
+    "    lambda x, y: \"sin(\" + x + \",\" + y + \")\",\n",
     "]\n",
     "custom_library = ps.CustomLibrary(\n",
     "    library_functions=library_functions, function_names=library_function_names\n",
@@ -2525,7 +2579,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "0717d4d3",
+   "id": "831ad420",
    "metadata": {},
    "source": [
     "### Fully custom library, default function names\n",
@@ -2535,13 +2589,13 @@
   {
    "cell_type": "code",
    "execution_count": 48,
-   "id": "48275943",
+   "id": "bcde4b2f",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.721517Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.721193Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.772430Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.771369Z"
+     "iopub.execute_input": "2023-07-10T22:17:04.906615Z",
+     "iopub.status.busy": "2023-07-10T22:17:04.903507Z",
+     "iopub.status.idle": "2023-07-10T22:17:05.043447Z",
+     "shell.execute_reply": "2023-07-10T22:17:05.042301Z"
     }
    },
    "outputs": [
@@ -2557,10 +2611,10 @@
    ],
    "source": [
     "library_functions = [\n",
-    "    lambda x : np.exp(x),\n",
-    "    lambda x : 1./x,\n",
-    "    lambda x : x,\n",
-    "    lambda x,y : np.sin(x+y)\n",
+    "    lambda x: np.exp(x),\n",
+    "    lambda x: 1.0 / x,\n",
+    "    lambda x: x,\n",
+    "    lambda x, y: np.sin(x + y),\n",
     "]\n",
     "custom_library = ps.CustomLibrary(library_functions=library_functions)\n",
     "\n",
@@ -2572,7 +2626,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "77667e78",
+   "id": "3d763791",
    "metadata": {},
    "source": [
     "### Identity library\n",
@@ -2582,13 +2636,13 @@
   {
    "cell_type": "code",
    "execution_count": 49,
-   "id": "37157ef7",
+   "id": "115773c7",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.776676Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.776226Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.808324Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.807291Z"
+     "iopub.execute_input": "2023-07-10T22:17:05.055322Z",
+     "iopub.status.busy": "2023-07-10T22:17:05.049641Z",
+     "iopub.status.idle": "2023-07-10T22:17:05.151687Z",
+     "shell.execute_reply": "2023-07-10T22:17:05.150548Z"
     }
    },
    "outputs": [
@@ -2612,7 +2666,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "5cddf2d5",
+   "id": "6c2f90b2",
    "metadata": {},
    "source": [
     "### Concatenate two libraries\n",
@@ -2622,13 +2676,13 @@
   {
    "cell_type": "code",
    "execution_count": 50,
-   "id": "fde91671",
+   "id": "c8224049",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.812495Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.812044Z",
-     "iopub.status.idle": "2023-06-20T21:46:42.861259Z",
-     "shell.execute_reply": "2023-06-20T21:46:42.860272Z"
+     "iopub.execute_input": "2023-07-10T22:17:05.167482Z",
+     "iopub.status.busy": "2023-07-10T22:17:05.164502Z",
+     "iopub.status.idle": "2023-07-10T22:17:05.315227Z",
+     "shell.execute_reply": "2023-07-10T22:17:05.313835Z"
     }
    },
    "outputs": [
@@ -2665,8 +2719,7 @@
     "fourier_library = ps.FourierLibrary()\n",
     "combined_library = identity_library + fourier_library\n",
     "\n",
-    "model = ps.SINDy(feature_library=combined_library,\n",
-    "                 feature_names=feature_names)\n",
+    "model = ps.SINDy(feature_library=combined_library, feature_names=feature_names)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()\n",
     "model.get_feature_names()"
@@ -2674,7 +2727,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "38492a67",
+   "id": "d6464ffd",
    "metadata": {},
    "source": [
     "### Tensor two libraries together\n",
@@ -2684,13 +2737,13 @@
   {
    "cell_type": "code",
    "execution_count": 51,
-   "id": "292e936d",
+   "id": "831b1c57",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:42.865676Z",
-     "iopub.status.busy": "2023-06-20T21:46:42.865041Z",
-     "iopub.status.idle": "2023-06-20T21:46:43.010097Z",
-     "shell.execute_reply": "2023-06-20T21:46:43.009098Z"
+     "iopub.execute_input": "2023-07-10T22:17:05.321968Z",
+     "iopub.status.busy": "2023-07-10T22:17:05.320909Z",
+     "iopub.status.idle": "2023-07-10T22:17:05.733237Z",
+     "shell.execute_reply": "2023-07-10T22:17:05.728710Z"
     }
    },
    "outputs": [
@@ -2708,8 +2761,7 @@
     "fourier_library = ps.FourierLibrary()\n",
     "combined_library = identity_library * fourier_library\n",
     "\n",
-    "model = ps.SINDy(feature_library=combined_library,\n",
-    "                 feature_names=feature_names)\n",
+    "model = ps.SINDy(feature_library=combined_library, feature_names=feature_names)\n",
     "model.fit(x_train, t=dt)\n",
     "# model.print()  # prints out long and unobvious model\n",
     "print(\"Feature names:\\n\", model.get_feature_names())"
@@ -2718,13 +2770,13 @@
   {
    "cell_type": "code",
    "execution_count": 52,
-   "id": "40dfd1de",
+   "id": "5c6ce9ab",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:43.016579Z",
-     "iopub.status.busy": "2023-06-20T21:46:43.014121Z",
-     "iopub.status.idle": "2023-06-20T21:46:43.666671Z",
-     "shell.execute_reply": "2023-06-20T21:46:43.665939Z"
+     "iopub.execute_input": "2023-07-10T22:17:05.739305Z",
+     "iopub.status.busy": "2023-07-10T22:17:05.738798Z",
+     "iopub.status.idle": "2023-07-10T22:17:07.632252Z",
+     "shell.execute_reply": "2023-07-10T22:17:07.631082Z"
     }
    },
    "outputs": [
@@ -2740,27 +2792,25 @@
     }
    ],
    "source": [
-    "# the model prediction is quite bad of course \n",
+    "# the model prediction is quite bad of course\n",
     "# because the library has mostly useless terms\n",
-    "x_dot_test_predicted = model.predict(x_test)  \n",
+    "x_dot_test_predicted = model.predict(x_test)\n",
     "\n",
     "# Compute derivatives with a finite difference method, for comparison\n",
     "x_dot_test_computed = model.differentiate(x_test, t=dt)\n",
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(7, 9))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test, x_dot_test_computed[:, i],\n",
-    "                'k', label='numerical derivative')\n",
-    "    axs[i].plot(t_test, x_dot_test_predicted[:, i],\n",
-    "                'r--', label='model prediction')\n",
+    "    axs[i].plot(t_test, x_dot_test_computed[:, i], \"k\", label=\"numerical derivative\")\n",
+    "    axs[i].plot(t_test, x_dot_test_predicted[:, i], \"r--\", label=\"model prediction\")\n",
     "    axs[i].legend()\n",
-    "    axs[i].set(xlabel='t', ylabel='$\\dot x_{}$'.format(i))\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=r\"$\\dot x_{}$\".format(i))\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "9ea0ba0d",
+   "id": "82b971a6",
    "metadata": {},
    "source": [
     "### Generalized library\n",
@@ -2769,9 +2819,12 @@
     "One can specify:\n",
     "<br>\n",
     "1. N different libraries to add together\n",
-    "2. A list of inputs to use for each library. For two libraries with four inputs this would look like inputs_per_library = [[0, 1, 2, 3], [0, 1, 2, 3]] and to avoid using the first two input variables in the second library, you would change it to something like inputs_per_library = [[0, 1, 2, 3], [2, 2, 2, 3]], since duplicates are thrown out and [2, 2, 2, 3] will reduce to [2, 3]. \n",
-    "        \n",
-    "3. A list of libraries to tensor together and add to the overall library. For four libraries, we could make three tensor libraries by using tensor_array = [[1, 0, 1, 1], [1, 1, 1, 1], [0, 0, 1, 1]]. The first sub-array takes the tensor product of libraries 0, 2, 3, the second takes the tensor product of all of them, and the last takes the tensor product of the libraries 2 and 3. This is a silly example since the [1, 1, 1, 1] tensor product already contains all the possible terms. \n",
+    "2. A list of inputs to use for each library. For two libraries with four inputs this\n",
+    " would look like inputs_per_library = [[0, 1, 2, 3], [0, 1, 2, 3]] and to avoid using\n",
+    " the first two input variables in the second library, you would change it to\n",
+    " inputs_per_library = [[0, 1, 2, 3], [2, 3]].\n",
+    "\n",
+    "3. A list of libraries to tensor together and add to the overall library. For four libraries, we could make three tensor libraries by using tensor_array = [[1, 0, 1, 1], [1, 1, 1, 1], [0, 0, 1, 1]]. The first sub-array takes the tensor product of libraries 0, 2, 3, the second takes the tensor product of all of them, and the last takes the tensor product of the libraries 2 and 3. This is a silly example since the [1, 1, 1, 1] tensor product already contains all the possible terms.\n",
     "4. A list of library indices to exclude from the overall library. The first N libraries correspond to the input libraries and the subsequent indices correspond to the tensored libraries. For two libraries, exclude_libraries=[0,1] and tensor_array=[[1,1]] would result in a library consisting of only the tensor product.\n",
     "<br>\n",
     "<br>\n",
@@ -2781,15 +2834,14 @@
   {
    "cell_type": "code",
    "execution_count": 53,
-   "id": "559fb313",
+   "id": "ddf03db6",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:43.669466Z",
-     "iopub.status.busy": "2023-06-20T21:46:43.669196Z",
-     "iopub.status.idle": "2023-06-20T21:46:43.711305Z",
-     "shell.execute_reply": "2023-06-20T21:46:43.710280Z"
-    },
-    "lines_to_next_cell": 1
+     "iopub.execute_input": "2023-07-10T22:17:07.639595Z",
+     "iopub.status.busy": "2023-07-10T22:17:07.638708Z",
+     "iopub.status.idle": "2023-07-10T22:17:07.756180Z",
+     "shell.execute_reply": "2023-07-10T22:17:07.754231Z"
+    }
    },
    "outputs": [
     {
@@ -2809,25 +2861,23 @@
     "poly_library = ps.PolynomialLibrary(include_bias=False)\n",
     "fourier_library = ps.FourierLibrary()\n",
     "\n",
-    "# Initialize the default inputs, i.e. each library\n",
-    "# uses all the input variables\n",
-    "inputs_temp = np.tile([0, 1, 2], 2)\n",
-    "inputs_per_library = np.reshape(inputs_temp, (2, 3))\n",
-    "\n",
-    "# Don't use the x0 input for generating the Fourier library\n",
-    "inputs_per_library[1, 0] = 1\n",
+    "# Initialize the default inputs, but\n",
+    "# don't use the x0 input for generating the Fourier library\n",
+    "inputs_per_library = [(0, 1, 2), (1, 2)]\n",
     "\n",
     "# Tensor all the polynomial and Fourier library terms together\n",
     "tensor_array = [[1, 1]]\n",
     "\n",
     "# Initialize this generalized library, all the work hidden from the user!\n",
-    "generalized_library = ps.GeneralizedLibrary([poly_library, fourier_library], \n",
-    "                                            tensor_array=tensor_array, exclude_libraries=[1],\n",
-    "                                            inputs_per_library=inputs_per_library)\n",
+    "generalized_library = ps.GeneralizedLibrary(\n",
+    "    [poly_library, fourier_library],\n",
+    "    tensor_array=tensor_array,\n",
+    "    exclude_libraries=[1],\n",
+    "    inputs_per_library=inputs_per_library,\n",
+    ")\n",
     "\n",
     "# Fit the model and print the library feature names to check success\n",
-    "model = ps.SINDy(feature_library=generalized_library, \n",
-    "                 feature_names=feature_names)\n",
+    "model = ps.SINDy(feature_library=generalized_library, feature_names=feature_names)\n",
     "model.fit(x_train, t=dt)\n",
     "model.print()\n",
     "print(\"Feature names:\\n\", model.get_feature_names())"
@@ -2835,7 +2885,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "2b23f39d",
+   "id": "a9e4af07",
    "metadata": {},
    "source": [
     "## SINDy with control (SINDYc)\n",
@@ -2847,8 +2897,10 @@
   },
   {
    "cell_type": "markdown",
-   "id": "9eeb519b",
-   "metadata": {},
+   "id": "82555928",
+   "metadata": {
+    "lines_to_next_cell": 2
+   },
    "source": [
     "### Train the model"
    ]
@@ -2856,42 +2908,49 @@
   {
    "cell_type": "code",
    "execution_count": 54,
-   "id": "a60c6776",
+   "id": "728495d3",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:43.715521Z",
-     "iopub.status.busy": "2023-06-20T21:46:43.714728Z",
-     "iopub.status.idle": "2023-06-20T21:46:44.051175Z",
-     "shell.execute_reply": "2023-06-20T21:46:44.049281Z"
+     "iopub.execute_input": "2023-07-10T22:17:07.767568Z",
+     "iopub.status.busy": "2023-07-10T22:17:07.765716Z",
+     "iopub.status.idle": "2023-07-10T22:17:08.491457Z",
+     "shell.execute_reply": "2023-07-10T22:17:08.490669Z"
     }
    },
    "outputs": [],
    "source": [
     "# Control input\n",
     "def u_fun(t):\n",
-    "    return np.column_stack([np.sin(2 * t), t ** 2])\n",
+    "    return np.column_stack([np.sin(2 * t), t**2])\n",
+    "\n",
     "\n",
     "# Generate measurement data\n",
-    "dt = .002\n",
+    "dt = 0.002\n",
     "\n",
     "t_train = np.arange(0, t_end_train, dt)\n",
     "t_train_span = (t_train[0], t_train[-1])\n",
     "x0_train = [-8, 8, 27]\n",
-    "x_train = solve_ivp(lorenz_control, t_train_span, x0_train, \n",
-    "                    t_eval=t_train, args=(u_fun,), **integrator_keywords).y.T\n",
+    "x_train = solve_ivp(\n",
+    "    lorenz_control,\n",
+    "    t_train_span,\n",
+    "    x0_train,\n",
+    "    t_eval=t_train,\n",
+    "    args=(u_fun,),\n",
+    "    **integrator_keywords,\n",
+    ").y.T\n",
     "u_train = u_fun(t_train)"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 55,
-   "id": "e9f12f6a",
+   "id": "99c156f8",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:44.056194Z",
-     "iopub.status.busy": "2023-06-20T21:46:44.055773Z",
-     "iopub.status.idle": "2023-06-20T21:46:44.086777Z",
-     "shell.execute_reply": "2023-06-20T21:46:44.085707Z"
+     "iopub.execute_input": "2023-07-10T22:17:08.497095Z",
+     "iopub.status.busy": "2023-07-10T22:17:08.495650Z",
+     "iopub.status.idle": "2023-07-10T22:17:08.549956Z",
+     "shell.execute_reply": "2023-07-10T22:17:08.549040Z"
     }
    },
    "outputs": [
@@ -2914,7 +2973,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "374b0275",
+   "id": "61224adb",
    "metadata": {},
    "source": [
     "### Assess results on a test trajectory"
@@ -2923,13 +2982,13 @@
   {
    "cell_type": "code",
    "execution_count": 56,
-   "id": "96a9846a",
+   "id": "faefdfd8",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:44.091076Z",
-     "iopub.status.busy": "2023-06-20T21:46:44.090589Z",
-     "iopub.status.idle": "2023-06-20T21:46:44.577485Z",
-     "shell.execute_reply": "2023-06-20T21:46:44.576432Z"
+     "iopub.execute_input": "2023-07-10T22:17:08.564223Z",
+     "iopub.status.busy": "2023-07-10T22:17:08.554715Z",
+     "iopub.status.idle": "2023-07-10T22:17:09.630626Z",
+     "shell.execute_reply": "2023-07-10T22:17:09.628283Z"
     }
    },
    "outputs": [
@@ -2947,17 +3006,23 @@
     "t_test_span = (t_test[0], t_test[-1])\n",
     "u_test = u_fun(t_test)\n",
     "x0_test = np.array([8, 7, 15])\n",
-    "x_test = solve_ivp(lorenz_control, t_test_span, x0_test, \n",
-    "                   t_eval=t_test, args=(u_fun,), **integrator_keywords).y.T\n",
+    "x_test = solve_ivp(\n",
+    "    lorenz_control,\n",
+    "    t_test_span,\n",
+    "    x0_test,\n",
+    "    t_eval=t_test,\n",
+    "    args=(u_fun,),\n",
+    "    **integrator_keywords,\n",
+    ").y.T\n",
     "u_test = u_fun(t_test)\n",
     "\n",
     "# Compare SINDy-predicted derivatives with finite difference derivatives\n",
-    "print('Model score: %f' % model.score(x_test, u=u_test, t=dt))"
+    "print(\"Model score: %f\" % model.score(x_test, u=u_test, t=dt))"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "40b42d10",
+   "id": "4b5c25cd",
    "metadata": {},
    "source": [
     "### Predict derivatives with learned model"
@@ -2966,13 +3031,13 @@
   {
    "cell_type": "code",
    "execution_count": 57,
-   "id": "8ff231e6",
+   "id": "93baf235",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:44.581836Z",
-     "iopub.status.busy": "2023-06-20T21:46:44.581371Z",
-     "iopub.status.idle": "2023-06-20T21:46:45.070459Z",
-     "shell.execute_reply": "2023-06-20T21:46:45.069740Z"
+     "iopub.execute_input": "2023-07-10T22:17:09.637832Z",
+     "iopub.status.busy": "2023-07-10T22:17:09.635997Z",
+     "iopub.status.idle": "2023-07-10T22:17:10.947908Z",
+     "shell.execute_reply": "2023-07-10T22:17:10.946564Z"
     }
    },
    "outputs": [
@@ -2989,25 +3054,23 @@
    ],
    "source": [
     "# Predict derivatives using the learned model\n",
-    "x_dot_test_predicted = model.predict(x_test, u=u_test)  \n",
+    "x_dot_test_predicted = model.predict(x_test, u=u_test)\n",
     "\n",
     "# Compute derivatives with a finite difference method, for comparison\n",
     "x_dot_test_computed = model.differentiate(x_test, t=dt)\n",
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(7, 9))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test, x_dot_test_computed[:, i],\n",
-    "                'k', label='numerical derivative')\n",
-    "    axs[i].plot(t_test, x_dot_test_predicted[:, i],\n",
-    "                'r--', label='model prediction')\n",
+    "    axs[i].plot(t_test, x_dot_test_computed[:, i], \"k\", label=\"numerical derivative\")\n",
+    "    axs[i].plot(t_test, x_dot_test_predicted[:, i], \"r--\", label=\"model prediction\")\n",
     "    axs[i].legend()\n",
-    "    axs[i].set(xlabel='t', ylabel='$\\dot x_{}$'.format(i))\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=r\"$\\dot x_{}$\".format(i))\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "b6fc9f79",
+   "id": "bf404344",
    "metadata": {},
    "source": [
     "### Simulate forward in time (control input function known)\n",
@@ -3017,13 +3080,13 @@
   {
    "cell_type": "code",
    "execution_count": 58,
-   "id": "e0610f3e",
+   "id": "17eaf892",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:45.072807Z",
-     "iopub.status.busy": "2023-06-20T21:46:45.072502Z",
-     "iopub.status.idle": "2023-06-20T21:46:53.911182Z",
-     "shell.execute_reply": "2023-06-20T21:46:53.910524Z"
+     "iopub.execute_input": "2023-07-10T22:17:10.953529Z",
+     "iopub.status.busy": "2023-07-10T22:17:10.952845Z",
+     "iopub.status.idle": "2023-07-10T22:17:26.283982Z",
+     "shell.execute_reply": "2023-07-10T22:17:26.283284Z"
     }
    },
    "outputs": [
@@ -3054,28 +3117,26 @@
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(7, 9))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test, x_test[:, i], 'k', label='true simulation')\n",
-    "    axs[i].plot(t_test, x_test_sim[:, i], 'r--', label='model simulation')\n",
+    "    axs[i].plot(t_test, x_test[:, i], \"k\", label=\"true simulation\")\n",
+    "    axs[i].plot(t_test, x_test_sim[:, i], \"r--\", label=\"model simulation\")\n",
     "    axs[i].legend()\n",
-    "    axs[i].set(xlabel='t', ylabel='$x_{}$'.format(i))\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=\"$x_{}$\".format(i))\n",
     "\n",
     "fig = plt.figure(figsize=(10, 4.5))\n",
-    "ax1 = fig.add_subplot(121, projection='3d')\n",
-    "ax1.plot(x_test[:, 0], x_test[:, 1], x_test[:, 2], 'k')\n",
-    "ax1.set(xlabel='$x_0$', ylabel='$x_1$',\n",
-    "        zlabel='$x_2$', title='true simulation')\n",
+    "ax1 = fig.add_subplot(121, projection=\"3d\")\n",
+    "ax1.plot(x_test[:, 0], x_test[:, 1], x_test[:, 2], \"k\")\n",
+    "ax1.set(xlabel=\"$x_0$\", ylabel=\"$x_1$\", zlabel=\"$x_2$\", title=\"true simulation\")\n",
     "\n",
-    "ax2 = fig.add_subplot(122, projection='3d')\n",
-    "ax2.plot(x_test_sim[:, 0], x_test_sim[:, 1], x_test_sim[:, 2], 'r--')\n",
-    "ax2.set(xlabel='$x_0$', ylabel='$x_1$',\n",
-    "        zlabel='$x_2$', title='model simulation')\n",
+    "ax2 = fig.add_subplot(122, projection=\"3d\")\n",
+    "ax2.plot(x_test_sim[:, 0], x_test_sim[:, 1], x_test_sim[:, 2], \"r--\")\n",
+    "ax2.set(xlabel=\"$x_0$\", ylabel=\"$x_1$\", zlabel=\"$x_2$\", title=\"model simulation\")\n",
     "\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "73dec62b",
+   "id": "c302f378",
    "metadata": {},
    "source": [
     "### Simulate forward in time (unknown control input function)\n",
@@ -3085,13 +3146,13 @@
   {
    "cell_type": "code",
    "execution_count": 59,
-   "id": "80c8b4e3",
+   "id": "a6dffe97",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:53.914039Z",
-     "iopub.status.busy": "2023-06-20T21:46:53.913526Z",
-     "iopub.status.idle": "2023-06-20T21:46:53.917110Z",
-     "shell.execute_reply": "2023-06-20T21:46:53.916364Z"
+     "iopub.execute_input": "2023-07-10T22:17:26.286808Z",
+     "iopub.status.busy": "2023-07-10T22:17:26.286543Z",
+     "iopub.status.idle": "2023-07-10T22:17:26.289956Z",
+     "shell.execute_reply": "2023-07-10T22:17:26.289377Z"
     }
    },
    "outputs": [],
@@ -3102,13 +3163,13 @@
   {
    "cell_type": "code",
    "execution_count": 60,
-   "id": "e2f3712b",
+   "id": "418644da",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:46:53.919814Z",
-     "iopub.status.busy": "2023-06-20T21:46:53.919351Z",
-     "iopub.status.idle": "2023-06-20T21:47:02.889374Z",
-     "shell.execute_reply": "2023-06-20T21:47:02.888663Z"
+     "iopub.execute_input": "2023-07-10T22:17:26.292788Z",
+     "iopub.status.busy": "2023-07-10T22:17:26.292195Z",
+     "iopub.status.idle": "2023-07-10T22:17:37.415443Z",
+     "shell.execute_reply": "2023-07-10T22:17:37.414831Z"
     }
    },
    "outputs": [
@@ -3135,31 +3196,31 @@
     "x_test_sim = model.simulate(x0_test, t_test, u=u_test)\n",
     "\n",
     "# Note that the output is one example short of the length of t_test\n",
-    "print('Length of t_test:', len(t_test))\n",
-    "print('Length of simulation:', len(x_test_sim))\n",
+    "print(\"Length of t_test:\", len(t_test))\n",
+    "print(\"Length of simulation:\", len(x_test_sim))\n",
     "\n",
     "fig, axs = plt.subplots(x_test.shape[1], 1, sharex=True, figsize=(12, 4))\n",
     "for i in range(x_test.shape[1]):\n",
-    "    axs[i].plot(t_test[:-1], x_test[:-1, i], 'k', label='true simulation')\n",
-    "    axs[i].plot(t_test[:-1], x_test_sim[:, i], 'r--', label='model simulation')\n",
-    "    axs[i].set(xlabel='t', ylabel='$x_{}$'.format(i))\n",
+    "    axs[i].plot(t_test[:-1], x_test[:-1, i], \"k\", label=\"true simulation\")\n",
+    "    axs[i].plot(t_test[:-1], x_test_sim[:, i], \"r--\", label=\"model simulation\")\n",
+    "    axs[i].set(xlabel=\"t\", ylabel=\"$x_{}$\".format(i))\n",
     "\n",
     "fig.show()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "8c58e04e",
+   "id": "976306e8",
    "metadata": {},
    "source": [
-    "## Implicit ODEs \n",
-    "How would we use SINDy to solve an implicit ODE? In other words, now the LHS can depend on x and x_dot, \n",
+    "## Implicit ODEs\n",
+    "How would we use SINDy to solve an implicit ODE? In other words, now the LHS can depend on x and x_dot,\n",
     "$$\\dot{\\mathbf{x}} = \\mathbf{f}(\\mathbf{x}, \\dot{\\mathbf{x}})$$\n",
     "\n",
     "\n",
     "In order to deal with this, we need a library $\\Theta(\\mathbf{x}, \\dot{\\mathbf{x}})$. SINDy parallel implicit (SINDy-PI) is geared to solve these problems. It solves the optimization problem,\n",
     "$$argmin_\\mathbf{\\Xi} (\\|\\Theta(\\mathbf{X}, \\dot{\\mathbf{X}}) - \\Theta(\\mathbf{X}, \\dot{\\mathbf{X}})\\mathbf{\\Xi}\\| + \\lambda \\|\\mathbf{\\Xi}\\|_1)$$\n",
-    "such that diag$(\\mathbf{\\Xi}) = 0$. So for every candidate library term it generates a different model. With N state variables, we need to choose N of the equations to solve for the system evolution. See the [SINDy-PI notebook](https://github.com/dynamicslab/pysindy/blob/master/examples/9_sindypi_with_sympy.ipynb) for more details. \n",
+    "such that diag$(\\mathbf{\\Xi}) = 0$. So for every candidate library term it generates a different model. With N state variables, we need to choose N of the equations to solve for the system evolution. See the [SINDy-PI notebook](https://github.com/dynamicslab/pysindy/blob/master/examples/9_sindypi_with_sympy.ipynb) for more details.\n",
     "\n",
     "Here we illustrate successful identification of the 1D Michelson-Menten enzyme model\n",
     "$$\\dot{x} = 0.6 - \\frac{1.5 x}{0.3 + x}.$$\n",
@@ -3172,13 +3233,13 @@
   {
    "cell_type": "code",
    "execution_count": 61,
-   "id": "e1a7d2aa",
+   "id": "d1956aff",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:02.891570Z",
-     "iopub.status.busy": "2023-06-20T21:47:02.891309Z",
-     "iopub.status.idle": "2023-06-20T21:47:13.454814Z",
-     "shell.execute_reply": "2023-06-20T21:47:13.453390Z"
+     "iopub.execute_input": "2023-07-10T22:17:37.418352Z",
+     "iopub.status.busy": "2023-07-10T22:17:37.417790Z",
+     "iopub.status.idle": "2023-07-10T22:17:48.802073Z",
+     "shell.execute_reply": "2023-07-10T22:17:48.800314Z"
     }
    },
    "outputs": [
@@ -3278,30 +3339,29 @@
     "    if __name__ != \"testing\":\n",
     "        T = 4\n",
     "    else:\n",
-    "        T = .02\n",
+    "        T = 0.02\n",
     "    t = np.arange(0, T + dt, dt)\n",
     "    t_span = (t[0], t[-1])\n",
     "    x0_train = [0.55]\n",
-    "    x_train = solve_ivp(enzyme, t_span, x0_train, \n",
-    "                        t_eval=t, **integrator_keywords).y.T\n",
+    "    x_train = solve_ivp(enzyme, t_span, x0_train, t_eval=t, **integrator_keywords).y.T\n",
     "\n",
-    "    # Initialize custom SINDy library so that we can have \n",
-    "    # x_dot inside it. \n",
+    "    # Initialize custom SINDy library so that we can have\n",
+    "    # x_dot inside it.\n",
     "    x_library_functions = [\n",
     "        lambda x: x,\n",
     "        lambda x, y: x * y,\n",
-    "        lambda x: x ** 2,\n",
+    "        lambda x: x**2,\n",
     "        lambda x, y, z: x * y * z,\n",
-    "        lambda x, y: x * y ** 2,\n",
-    "        lambda x: x ** 3,\n",
+    "        lambda x, y: x * y**2,\n",
+    "        lambda x: x**3,\n",
     "        lambda x, y, z, w: x * y * z * w,\n",
-    "        lambda x, y, z: x * y * z ** 2,\n",
-    "        lambda x, y: x * y ** 3,\n",
-    "        lambda x: x ** 4,\n",
+    "        lambda x, y, z: x * y * z**2,\n",
+    "        lambda x, y: x * y**3,\n",
+    "        lambda x: x**4,\n",
     "    ]\n",
     "    x_dot_library_functions = [lambda x: x]\n",
     "\n",
-    "    # library function names includes both the x_library_functions \n",
+    "    # library function names includes both the x_library_functions\n",
     "    # and x_dot_library_functions names\n",
     "    library_function_names = [\n",
     "        lambda x: x,\n",
@@ -3346,25 +3406,25 @@
   },
   {
    "cell_type": "markdown",
-   "id": "e5fc16ac",
+   "id": "3b311fe8",
    "metadata": {},
    "source": [
     "## SINDy with control parameters (SINDyCP)\n",
     "The control input in PySINDy can be used to discover equations parameterized by control parameters in conjunction with the `ParameterizedLibrary`. We demonstrate on the logistic map\n",
     "$$ x_{n+1} = r x_n(1-x_n)$$\n",
-    "which depends on a single parameter $r$.  "
+    "which depends on a single parameter $r$."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 62,
-   "id": "428b9767",
+   "id": "db1da0fb",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:13.459275Z",
-     "iopub.status.busy": "2023-06-20T21:47:13.458793Z",
-     "iopub.status.idle": "2023-06-20T21:47:15.906187Z",
-     "shell.execute_reply": "2023-06-20T21:47:15.905504Z"
+     "iopub.execute_input": "2023-07-10T22:17:48.806933Z",
+     "iopub.status.busy": "2023-07-10T22:17:48.806315Z",
+     "iopub.status.idle": "2023-07-10T22:17:51.395983Z",
+     "shell.execute_reply": "2023-07-10T22:17:51.395307Z"
     }
    },
    "outputs": [
@@ -3380,7 +3440,7 @@
     }
    ],
    "source": [
-    "#Iterate the map and drop the initial 500-step transient. The behavior is chaotic for r>3.6.\n",
+    "# Iterate the map and drop the initial 500-step transient. The behavior is chaotic for r>3.6.\n",
     "if __name__ != \"testing\":\n",
     "    num = 1000\n",
     "    N = 1000\n",
@@ -3401,7 +3461,7 @@
     "        x = r * x * (1 - x)\n",
     "    xss = xss + [xs]\n",
     "\n",
-    "plt.figure(figsize=(4, 4),dpi=100)\n",
+    "plt.figure(figsize=(4, 4), dpi=100)\n",
     "for ind in range(num):\n",
     "    plt.plot(np.ones(N) * rs[ind], xss[ind], \",\", alpha=0.1, c=\"black\", rasterized=True)\n",
     "plt.xlabel(\"$r$\")\n",
@@ -3411,7 +3471,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "e8d70b5e",
+   "id": "c19c2653",
    "metadata": {},
    "source": [
     "We construct a `parameter_library` and a `feature_library` to act on the input data `x` and the control input `u` independently. The `ParameterizedLibrary` is composed of products of the two libraries output features. This enables fine control over the library features, which is especially useful in the case of PDEs like those arising in pattern formation modeling. See this [notebook](https://github.com/dynamicslab/pysindy/blob/master/examples/17_parameterized_pattern_formation/17_parameterized_pattern_formation.ipynb) for examples."
@@ -3420,13 +3480,13 @@
   {
    "cell_type": "code",
    "execution_count": 63,
-   "id": "4b8ab2c2",
+   "id": "5f9ce4e5",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:15.908753Z",
-     "iopub.status.busy": "2023-06-20T21:47:15.908544Z",
-     "iopub.status.idle": "2023-06-20T21:47:15.925393Z",
-     "shell.execute_reply": "2023-06-20T21:47:15.924689Z"
+     "iopub.execute_input": "2023-07-10T22:17:51.398754Z",
+     "iopub.status.busy": "2023-07-10T22:17:51.398148Z",
+     "iopub.status.idle": "2023-07-10T22:17:51.420170Z",
+     "shell.execute_reply": "2023-07-10T22:17:51.418909Z"
     }
    },
    "outputs": [
@@ -3439,9 +3499,9 @@
     }
    ],
    "source": [
-    "#use four parameter values as training data\n",
+    "# use four parameter values as training data\n",
     "rs_train = [3.6, 3.7, 3.8, 3.9]\n",
-    "xs_train=[np.array(xss[np.where(np.array(rs)==r)[0][0]]) for r in rs_train]\n",
+    "xs_train = [np.array(xss[np.where(np.array(rs) == r)[0][0]]) for r in rs_train]\n",
     "\n",
     "feature_lib = ps.PolynomialLibrary(degree=3, include_bias=True)\n",
     "parameter_lib = ps.PolynomialLibrary(degree=1, include_bias=True)\n",
@@ -3461,7 +3521,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "310133df",
+   "id": "545c1b5c",
    "metadata": {},
    "source": [
     "## PDEFIND Feature Overview\n",
@@ -3474,13 +3534,13 @@
   {
    "cell_type": "code",
    "execution_count": 64,
-   "id": "4be3b235",
+   "id": "d84ae809",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:15.928208Z",
-     "iopub.status.busy": "2023-06-20T21:47:15.927882Z",
-     "iopub.status.idle": "2023-06-20T21:47:16.432984Z",
-     "shell.execute_reply": "2023-06-20T21:47:16.432271Z"
+     "iopub.execute_input": "2023-07-10T22:17:51.424896Z",
+     "iopub.status.busy": "2023-07-10T22:17:51.424367Z",
+     "iopub.status.idle": "2023-07-10T22:17:51.987307Z",
+     "shell.execute_reply": "2023-07-10T22:17:51.986612Z"
     }
    },
    "outputs": [
@@ -3533,7 +3593,7 @@
     "plt.colorbar()\n",
     "u = np.reshape(u, (len(x), len(t), 1))\n",
     "\n",
-    "# Define quadratic library with up to third order derivatives \n",
+    "# Define quadratic library with up to third order derivatives\n",
     "# on a uniform spatial grid\n",
     "library_functions = [lambda x: x, lambda x: x * x]\n",
     "library_function_names = [lambda x: x, lambda x: x + x]\n",
@@ -3542,7 +3602,7 @@
     "    function_names=library_function_names,\n",
     "    derivative_order=3,\n",
     "    spatial_grid=x,\n",
-    "    diff_kwargs={'is_uniform':True,'periodic':True},\n",
+    "    diff_kwargs={\"is_uniform\": True, \"periodic\": True},\n",
     ")\n",
     "\n",
     "optimizer = ps.STLSQ(threshold=0.1, alpha=1e-5, normalize_columns=True)\n",
@@ -3555,7 +3615,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "e453c95e",
+   "id": "84951678",
    "metadata": {},
    "source": [
     "### Weak formulation system identification improves robustness to noise.\n",
@@ -3565,13 +3625,13 @@
   {
    "cell_type": "code",
    "execution_count": 65,
-   "id": "e502b37f",
+   "id": "58786876",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:16.435660Z",
-     "iopub.status.busy": "2023-06-20T21:47:16.435408Z",
-     "iopub.status.idle": "2023-06-20T21:47:16.480477Z",
-     "shell.execute_reply": "2023-06-20T21:47:16.479695Z"
+     "iopub.execute_input": "2023-07-10T22:17:51.989440Z",
+     "iopub.status.busy": "2023-07-10T22:17:51.989216Z",
+     "iopub.status.idle": "2023-07-10T22:17:52.031639Z",
+     "shell.execute_reply": "2023-07-10T22:17:52.030859Z"
     }
    },
    "outputs": [],
@@ -3593,13 +3653,13 @@
   {
    "cell_type": "code",
    "execution_count": 66,
-   "id": "0113a0c8",
+   "id": "8281b695",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:16.483044Z",
-     "iopub.status.busy": "2023-06-20T21:47:16.482654Z",
-     "iopub.status.idle": "2023-06-20T21:47:16.545202Z",
-     "shell.execute_reply": "2023-06-20T21:47:16.544543Z"
+     "iopub.execute_input": "2023-07-10T22:17:52.034080Z",
+     "iopub.status.busy": "2023-07-10T22:17:52.033877Z",
+     "iopub.status.idle": "2023-07-10T22:17:52.101252Z",
+     "shell.execute_reply": "2023-07-10T22:17:52.100596Z"
     }
    },
    "outputs": [
@@ -3622,7 +3682,7 @@
   },
   {
    "cell_type": "markdown",
-   "id": "b2c30ee7",
+   "id": "58b82c65",
    "metadata": {},
    "source": [
     "### GeneralizedLibrary\n",
@@ -3644,13 +3704,13 @@
   {
    "cell_type": "code",
    "execution_count": 67,
-   "id": "4867a8e7",
+   "id": "b9fad5d8",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:16.547877Z",
-     "iopub.status.busy": "2023-06-20T21:47:16.547618Z",
-     "iopub.status.idle": "2023-06-20T21:47:17.436597Z",
-     "shell.execute_reply": "2023-06-20T21:47:17.435899Z"
+     "iopub.execute_input": "2023-07-10T22:17:52.103650Z",
+     "iopub.status.busy": "2023-07-10T22:17:52.103316Z",
+     "iopub.status.idle": "2023-07-10T22:17:53.020175Z",
+     "shell.execute_reply": "2023-07-10T22:17:53.019466Z"
     }
    },
    "outputs": [
@@ -3680,7 +3740,7 @@
     "    nx = 50\n",
     "    ny = 100\n",
     "else:\n",
-    "    nx = 6 # must be even\n",
+    "    nx = 6  # must be even\n",
     "    ny = 10\n",
     "Lx = 1\n",
     "Ly = 1\n",
@@ -3691,15 +3751,15 @@
     "X, Y = np.meshgrid(x, y, indexing=\"ij\")\n",
     "\n",
     "# Define rho\n",
-    "rho = X ** 2 + Y ** 2\n",
+    "rho = X**2 + Y**2\n",
     "plt.figure(figsize=(20, 3))\n",
     "plt.subplot(1, 5, 1)\n",
     "plt.imshow(rho, aspect=\"auto\", origin=\"lower\")\n",
     "plt.title(r\"$\\rho(x, y)$\")\n",
     "plt.colorbar()\n",
     "\n",
-    "# Generate the PDE data for phi by fourier transforms \n",
-    "# since this is homogenous PDE\n",
+    "# Generate the PDE data for phi by fourier transforms\n",
+    "# since this is homogeneous PDE\n",
     "# and we assume periodic boundary conditions\n",
     "nx2 = int(nx / 2)\n",
     "ny2 = int(ny / 2)\n",
@@ -3713,7 +3773,7 @@
     "\n",
     "# Get 2D mesh in (kx, ky)\n",
     "KX, KY = np.meshgrid(kx, ky, indexing=\"ij\")\n",
-    "K2 = KX ** 2 + KY ** 2\n",
+    "K2 = KX**2 + KY**2\n",
     "K2[0, 0] = 1e-5\n",
     "\n",
     "# Generate phi data by solving the PDE and plot results\n",
@@ -3746,8 +3806,7 @@
     "fourier_library = ps.FourierLibrary()\n",
     "X_mesh, Y_mesh = np.meshgrid(x, y)\n",
     "pde_library = ps.PDELibrary(\n",
-    "    derivative_order=1, \n",
-    "    spatial_grid=np.asarray([X_mesh, Y_mesh]).T\n",
+    "    derivative_order=1, spatial_grid=np.asarray([X_mesh, Y_mesh]).T\n",
     ")\n",
     "\n",
     "# Inputs are going to be all the variables [phi, X, Y].\n",
@@ -3763,21 +3822,18 @@
     "tensor_array = [[1, 0, 1]]\n",
     "\n",
     "# Remove X and Y from PDE library terms because why would we take these derivatives\n",
-    "inputs_temp = np.tile([0, 1, 2], 3)\n",
-    "inputs_per_library = np.reshape(inputs_temp, (3, 3))\n",
-    "inputs_per_library[2, 1] = 0\n",
-    "inputs_per_library[2, 2] = 0\n",
-    "\n",
-    "# Fit a generalized library of 3 feature libraries + 1 internally \n",
-    "# generated tensored library and only use the input variable phi \n",
-    "# for the PDELibrary. Note that this holds true both for the \n",
+    "inputs_per_library = [(0, 1, 2), (0, 1, 2), (0,)]\n",
+    "\n",
+    "# Fit a generalized library of 3 feature libraries + 1 internally\n",
+    "# generated tensored library and only use the input variable phi\n",
+    "# for the PDELibrary. Note that this holds true both for the\n",
     "# individual PDELibrary and any tensored libraries constructed from it.\n",
     "generalized_library = ps.GeneralizedLibrary(\n",
     "    [poly_library, fourier_library, pde_library],\n",
     "    tensor_array=tensor_array,\n",
     "    inputs_per_library=inputs_per_library,\n",
     ")\n",
-    "optimizer = ps.STLSQ(threshold=8,alpha=1e-3, normalize_columns=True)\n",
+    "optimizer = ps.STLSQ(threshold=8, alpha=1e-3, normalize_columns=True)\n",
     "model = ps.SINDy(feature_library=generalized_library, optimizer=optimizer)\n",
     "model.fit(data, x_dot=data_dot)\n",
     "\n",
@@ -3789,13 +3845,13 @@
   {
    "cell_type": "code",
    "execution_count": 68,
-   "id": "92ddf760",
+   "id": "917e920e",
    "metadata": {
     "execution": {
-     "iopub.execute_input": "2023-06-20T21:47:17.439194Z",
-     "iopub.status.busy": "2023-06-20T21:47:17.438936Z",
-     "iopub.status.idle": "2023-06-20T21:47:18.268823Z",
-     "shell.execute_reply": "2023-06-20T21:47:18.268127Z"
+     "iopub.execute_input": "2023-07-10T22:17:53.023024Z",
+     "iopub.status.busy": "2023-07-10T22:17:53.022803Z",
+     "iopub.status.idle": "2023-07-10T22:17:53.735852Z",
+     "shell.execute_reply": "2023-07-10T22:17:53.735216Z"
     }
    },
    "outputs": [
@@ -3820,22 +3876,22 @@
    ],
    "source": [
     "# Get prediction of rho and plot results\n",
-    "#predict expects a time axis...so add one and ignore it...\n",
-    "data_shaped=data.reshape((data.shape[0],data.shape[1],1,data.shape[2]))\n",
-    "rho_pred = model.predict(data_shaped)[:,:,0,:]\n",
+    "# predict expects a time axis...so add one and ignore it...\n",
+    "data_shaped = data.reshape((data.shape[0], data.shape[1], 1, data.shape[2]))\n",
+    "rho_pred = model.predict(data_shaped)[:, :, 0, :]\n",
     "if __name__ != \"testing\":\n",
     "    plt.figure(figsize=(16, 4))\n",
     "    plt.subplot(1, 3, 1)\n",
-    "    plt.title(r'True $\\rho$')\n",
-    "    plt.imshow(rho, aspect='auto', origin='lower')\n",
+    "    plt.title(r\"True $\\rho$\")\n",
+    "    plt.imshow(rho, aspect=\"auto\", origin=\"lower\")\n",
     "    plt.colorbar()\n",
     "    plt.subplot(1, 3, 2)\n",
-    "    plt.title(r'Predicted $\\rho_p$')\n",
-    "    plt.imshow(rho_pred[:, :, 0], aspect='auto', origin='lower')\n",
+    "    plt.title(r\"Predicted $\\rho_p$\")\n",
+    "    plt.imshow(rho_pred[:, :, 0], aspect=\"auto\", origin=\"lower\")\n",
     "    plt.colorbar()\n",
     "    plt.subplot(1, 3, 3)\n",
-    "    plt.title(r'Residual errors $\\rho - \\rho_p$')\n",
-    "    plt.imshow(rho - rho_pred[:, :, 0], aspect='auto', origin='lower')\n",
+    "    plt.title(r\"Residual errors $\\rho - \\rho_p$\")\n",
+    "    plt.imshow(rho - rho_pred[:, :, 0], aspect=\"auto\", origin=\"lower\")\n",
     "    plt.colorbar()\n",
     "    print(\"Feature names:\\n\", model.get_feature_names())"
    ]
diff --git a/examples/1_feature_overview/example.py b/examples/1_feature_overview/example.py
index 7c43cc70c..20b3f5c20 100644
--- a/examples/1_feature_overview/example.py
+++ b/examples/1_feature_overview/example.py
@@ -44,6 +44,11 @@ def ignore_specific_warnings():
     warnings.filters = filters
 
 
+if __name__ == "testing":
+    import sys
+    import os
+
+    sys.stdout = open(os.devnull, "w")
 # %%
 # Seed the random number generators for reproducibility
 np.random.seed(100)
@@ -895,7 +900,10 @@ def f(x):
 # One can specify:
 # <br>
 # 1. N different libraries to add together
-# 2. A list of inputs to use for each library. For two libraries with four inputs this would look like inputs_per_library = [[0, 1, 2, 3], [0, 1, 2, 3]] and to avoid using the first two input variables in the second library, you would change it to something like inputs_per_library = [[0, 1, 2, 3], [2, 2, 2, 3]], since duplicates are thrown out and [2, 2, 2, 3] will reduce to [2, 3].
+# 2. A list of inputs to use for each library. For two libraries with four inputs this
+#  would look like inputs_per_library = [[0, 1, 2, 3], [0, 1, 2, 3]] and to avoid using
+#  the first two input variables in the second library, you would change it to
+#  inputs_per_library = [[0, 1, 2, 3], [2, 3]].
 #
 # 3. A list of libraries to tensor together and add to the overall library. For four libraries, we could make three tensor libraries by using tensor_array = [[1, 0, 1, 1], [1, 1, 1, 1], [0, 0, 1, 1]]. The first sub-array takes the tensor product of libraries 0, 2, 3, the second takes the tensor product of all of them, and the last takes the tensor product of the libraries 2 and 3. This is a silly example since the [1, 1, 1, 1] tensor product already contains all the possible terms.
 # 4. A list of library indices to exclude from the overall library. The first N libraries correspond to the input libraries and the subsequent indices correspond to the tensored libraries. For two libraries, exclude_libraries=[0,1] and tensor_array=[[1,1]] would result in a library consisting of only the tensor product.
@@ -908,13 +916,9 @@ def f(x):
 poly_library = ps.PolynomialLibrary(include_bias=False)
 fourier_library = ps.FourierLibrary()
 
-# Initialize the default inputs, i.e. each library
-# uses all the input variables
-inputs_temp = np.tile([0, 1, 2], 2)
-inputs_per_library = np.reshape(inputs_temp, (2, 3))
-
-# Don't use the x0 input for generating the Fourier library
-inputs_per_library[1, 0] = 1
+# Initialize the default inputs, but
+# don't use the x0 input for generating the Fourier library
+inputs_per_library = [(0, 1, 2), (1, 2)]
 
 # Tensor all the polynomial and Fourier library terms together
 tensor_array = [[1, 1]]
@@ -1385,10 +1389,7 @@ def u_fun(t):
 tensor_array = [[1, 0, 1]]
 
 # Remove X and Y from PDE library terms because why would we take these derivatives
-inputs_temp = np.tile([0, 1, 2], 3)
-inputs_per_library = np.reshape(inputs_temp, (3, 3))
-inputs_per_library[2, 1] = 0
-inputs_per_library[2, 2] = 0
+inputs_per_library = [(0, 1, 2), (0, 1, 2), (0,)]
 
 # Fit a generalized library of 3 feature libraries + 1 internally
 # generated tensored library and only use the input variable phi
diff --git a/pysindy/feature_library/base.py b/pysindy/feature_library/base.py
index 5a65a65a9..16149b27c 100644
--- a/pysindy/feature_library/base.py
+++ b/pysindy/feature_library/base.py
@@ -4,6 +4,8 @@
 import abc
 import warnings
 from functools import wraps
+from itertools import repeat
+from typing import Optional
 from typing import Sequence
 
 import numpy as np
@@ -178,9 +180,6 @@ class ConcatLibrary(BaseFeatureLibrary):
 
     Attributes
     ----------
-    libraries_ : list of libraries
-        Library instances to be applied to the input matrix.
-
     n_features_in_ : int
         The total number of input features.
 
@@ -207,8 +206,7 @@ def __init__(
         self,
         libraries: list,
     ):
-        super().__init__()
-        self.libraries_ = libraries
+        self.libraries = libraries
 
     @x_sequence_or_item
     def fit(self, x_full, y=None):
@@ -228,13 +226,13 @@ def fit(self, x_full, y=None):
         self.n_features_in_ = n_features
 
         # First fit all libs provided below
-        fitted_libs = [lib.fit(x_full, y) for lib in self.libraries_]
+        fitted_libs = [lib.fit(x_full, y) for lib in self.libraries]
 
         # Calculate the sum of output features
         self.n_output_features_ = sum([lib.n_output_features_ for lib in fitted_libs])
 
         # Save fitted libs
-        self.libraries_ = fitted_libs
+        self.libraries = fitted_libs
 
         return self
 
@@ -254,12 +252,12 @@ def transform(self, x_full):
             generated from applying the custom functions to the inputs.
 
         """
-        for lib in self.libraries_:
+        for lib in self.libraries:
             check_is_fitted(lib)
 
         xp_full = []
         for x in x_full:
-            feature_sets = [lib.transform([x])[0] for lib in self.libraries_]
+            feature_sets = [lib.transform([x])[0] for lib in self.libraries]
             xp = np.concatenate(feature_sets, axis=feature_sets[0].ax_coord)
 
             xp = AxesArray(xp, comprehend_axes(xp))
@@ -280,13 +278,13 @@ def get_feature_names(self, input_features=None):
         output_feature_names : list of string, length n_output_features
         """
         feature_names = list()
-        for lib in self.libraries_:
+        for lib in self.libraries:
             lib_feat_names = lib.get_feature_names(input_features)
             feature_names += lib_feat_names
         return feature_names
 
     def calc_trajectory(self, diff_method, x, t):
-        return self.libraries_[0].calc_trajectory(diff_method, x, t)
+        return self.libraries[0].calc_trajectory(diff_method, x, t)
 
 
 class TensoredLibrary(BaseFeatureLibrary):
@@ -298,16 +296,17 @@ class TensoredLibrary(BaseFeatureLibrary):
     libraries : list of libraries
         Library instances to be applied to the input matrix.
 
+    inputs_per_library_ : Sequence of Sequences of ints (default None)
+        list that specifies which input indexes should be passed as
+        inputs for each of the individual feature libraries.
+        length must equal the number of feature libraries.  Default is
+        that all inputs are used for every library.
+
     Attributes
     ----------
     libraries_ : list of libraries
         Library instances to be applied to the input matrix.
 
-    inputs_per_library_ : numpy nd.array
-        Array that specifies which inputs should be used for each of the
-        libraries you are going to tensor together. Used for building
-        GeneralizedLibrary objects.
-
     n_features_in_ : int
         The total number of input features.
 
@@ -333,11 +332,10 @@ class TensoredLibrary(BaseFeatureLibrary):
     def __init__(
         self,
         libraries: list,
-        inputs_per_library=None,
+        inputs_per_library: Optional[Sequence[Sequence[int]]] = None,
     ):
-        super().__init__()
-        self.libraries_ = libraries
-        self.inputs_per_library_ = inputs_per_library
+        self.libraries = libraries
+        self.inputs_per_library = inputs_per_library
 
     def _combinations(self, lib_i, lib_j):
         """
@@ -377,7 +375,7 @@ def _set_inputs_per_library(self, inputs_per_library):
         """
         Extra function to make building a GeneralizedLibrary object easier
         """
-        self.inputs_per_library_ = inputs_per_library
+        self.inputs_per_library = inputs_per_library
 
     @x_sequence_or_item
     def fit(self, x_full, y=None):
@@ -398,18 +396,15 @@ def fit(self, x_full, y=None):
         self.n_features_in_ = n_features
 
         # If parameter is not set, use all the inputs
-        if self.inputs_per_library_ is None:
-            temp_inputs = np.tile(range(n_features), len(self.libraries_))
-            self.inputs_per_library_ = np.reshape(
-                temp_inputs, (len(self.libraries_), n_features)
+        if self.inputs_per_library is None:
+            self.inputs_per_library = list(
+                repeat(list(range(n_features)), len(self.libraries))
             )
 
         # First fit all libs provided below
         fitted_libs = [
-            lib.fit(
-                [x[..., np.unique(self.inputs_per_library_[i, :])] for x in x_full], y
-            )
-            for i, lib in enumerate(self.libraries_)
+            lib.fit([x[..., _unique(self.inputs_per_library[i])] for x in x_full], y)
+            for i, lib in enumerate(self.libraries)
         ]
 
         # Calculate the sum of output features
@@ -419,7 +414,7 @@ def fit(self, x_full, y=None):
             self.n_output_features_ *= osize
 
         # Save fitted libs
-        self.libraries_ = fitted_libs
+        self.libraries = fitted_libs
 
         return self
 
@@ -444,19 +439,19 @@ def transform(self, x_full):
         xp_full = []
         for x in x_full:
             xp = []
-            for i in range(len(self.libraries_)):
-                lib_i = self.libraries_[i]
-                if self.inputs_per_library_ is None:
+            for i in range(len(self.libraries)):
+                lib_i = self.libraries[i]
+                if self.inputs_per_library is None:
                     xp_i = lib_i.transform([x])[0]
                 else:
                     xp_i = lib_i.transform(
-                        [x[..., np.unique(self.inputs_per_library_[i, :])]]
+                        [x[..., _unique(self.inputs_per_library[i])]]
                     )[0]
 
-                for j in range(i + 1, len(self.libraries_)):
-                    lib_j = self.libraries_[j]
+                for j in range(i + 1, len(self.libraries)):
+                    lib_j = self.libraries[j]
                     xp_j = lib_j.transform(
-                        [x[..., np.unique(self.inputs_per_library_[j, :])]]
+                        [x[..., _unique(self.inputs_per_library[j])]]
                     )[0]
 
                     xp.append(self._combinations(xp_i, xp_j))
@@ -480,26 +475,26 @@ def get_feature_names(self, input_features=None):
         output_feature_names : list of string, length n_output_features
         """
         feature_names = list()
-        for i in range(len(self.libraries_)):
-            lib_i = self.libraries_[i]
+        for i in range(len(self.libraries)):
+            lib_i = self.libraries[i]
             if input_features is None:
                 input_features_i = [
-                    "x%d" % k for k in np.unique(self.inputs_per_library_[i, :])
+                    "x%d" % k for k in _unique(self.inputs_per_library[i])
                 ]
             else:
                 input_features_i = np.asarray(input_features)[
-                    np.unique(self.inputs_per_library_[i, :])
+                    _unique(self.inputs_per_library[i])
                 ].tolist()
             lib_i_feat_names = lib_i.get_feature_names(input_features_i)
-            for j in range(i + 1, len(self.libraries_)):
-                lib_j = self.libraries_[j]
+            for j in range(i + 1, len(self.libraries)):
+                lib_j = self.libraries[j]
                 if input_features is None:
                     input_features_j = [
-                        "x%d" % k for k in np.unique(self.inputs_per_library_[j, :])
+                        "x%d" % k for k in _unique(self.inputs_per_library[j])
                     ]
                 else:
                     input_features_j = np.asarray(input_features)[
-                        np.unique(self.inputs_per_library_[j, :])
+                        _unique(self.inputs_per_library[j])
                     ].tolist()
                 lib_j_feat_names = lib_j.get_feature_names(input_features_j)
                 feature_names += self._name_combinations(
@@ -508,4 +503,9 @@ def get_feature_names(self, input_features=None):
         return feature_names
 
     def calc_trajectory(self, diff_method, x, t):
-        return self.libraries_[0].calc_trajectory(diff_method, x, t)
+        return self.libraries[0].calc_trajectory(diff_method, x, t)
+
+
+def _unique(s: Sequence) -> Sequence:
+    """Remove duplicates, preserving order when python > 3.7"""
+    return list(dict.fromkeys(s))
diff --git a/pysindy/feature_library/custom_library.py b/pysindy/feature_library/custom_library.py
index 8065ba9c3..b385478f4 100644
--- a/pysindy/feature_library/custom_library.py
+++ b/pysindy/feature_library/custom_library.py
@@ -87,7 +87,6 @@ def __init__(
         interaction_only=True,
         include_bias=False,
     ):
-        super().__init__()
         self.functions = library_functions
         self.function_names = function_names
         if function_names and (
diff --git a/pysindy/feature_library/fourier_library.py b/pysindy/feature_library/fourier_library.py
index a6a61331d..accc5450b 100644
--- a/pysindy/feature_library/fourier_library.py
+++ b/pysindy/feature_library/fourier_library.py
@@ -55,7 +55,6 @@ def __init__(
         include_sin=True,
         include_cos=True,
     ):
-        super().__init__()
         if not (include_sin or include_cos):
             raise ValueError("include_sin and include_cos cannot both be False")
         if n_frequencies < 1 or not isinstance(n_frequencies, int):
diff --git a/pysindy/feature_library/generalized_library.py b/pysindy/feature_library/generalized_library.py
index c0f776306..3e5e24055 100644
--- a/pysindy/feature_library/generalized_library.py
+++ b/pysindy/feature_library/generalized_library.py
@@ -1,7 +1,13 @@
+from itertools import repeat
+from typing import Optional
+from typing import Sequence
+from warnings import warn
+
 import numpy as np
 from sklearn.utils.validation import check_is_fitted
 
 from ..utils import AxesArray
+from .base import _unique
 from .base import BaseFeatureLibrary
 from .base import x_sequence_or_item
 from .weak_pde_library import WeakPDELibrary
@@ -30,36 +36,16 @@ class GeneralizedLibrary(BaseFeatureLibrary):
         tensored libraries from tensoring libraries 0 and 3 and libraries
         1, 3, and 4.
 
-    inputs_per_library : 2D np.ndarray, optional (default None)
-        Shape should be equal to # feature libraries by # variable input.
-        Can be used to specify a subset of the variables to use to generate
-        a feature library. If number of feature libraries > 1, then can be
-        used to generate a large number of libraries, each using their own
-        subsets of the input variables. Note that this must be specified for
-        all the individual feature libraries.
+    inputs_per_library : Sequence of Seqeunces of ints (default None)
+        list that specifies which input indexes should be passed as
+        inputs for each of the individual feature libraries.
+        length must equal the number of feature libraries.  Default is
+        that all inputs are used for every library.
 
     Attributes
     ----------
-    libraries_ : list of libraries
-        Library instances to be applied to the input matrix.
-
-    tensor_array_ : 2D list of booleans (default None)
-        Indicates which pairs of libraries to tensor product together and
-        add to the overall library. For instance if you have 5 libraries,
-        and want to do two tensor products, you could use the list
-        [[1, 0, 0, 1, 0], [0, 1, 0, 1, 1]] to indicate that you want two
-        tensored libraries from tensoring libraries 0 and 3 and libraries
-        1, 3, and 4. Shape equal to # of tensor libraries to make
-        by the # feature libraries.
-
-    inputs_per_library_ : 2D np.ndarray, (default None)
-        Default is that all inputs are used for every library.
-        Can be used to specify a subset of the variables to use to generate
-        a feature library. If number of feature libraries > 1, then can be
-        use to generate a large number of libraries, each using their own
-        subsets of the input variables. Note that this must be specified for
-        all the individual feature libraries. The shape is equal to
-        # feature libraries, # variable inputs.
+    self.libraries_full_: list[BaseFeatureLibrary]
+        The fitted libraries
 
     n_features_in_ : int
         The total number of input features.
@@ -87,12 +73,11 @@ def __init__(
         self,
         libraries: list,
         tensor_array=None,
-        inputs_per_library=None,
+        inputs_per_library: Optional[Sequence[Sequence[int]]] = None,
         exclude_libraries=[],
     ):
-        super().__init__()
         if len(libraries) > 0:
-            self.libraries_ = libraries
+            self.libraries = libraries
 
             if has_weak(self) and has_nonweak(self):
                 raise ValueError(
@@ -106,15 +91,19 @@ def __init__(
                 "Empty or nonsensical library list passed to this library."
             )
         if inputs_per_library is not None:
-            if inputs_per_library.ndim != 2:
-                raise ValueError("Input libraries array should form a 2D numpy array.")
-            if inputs_per_library.shape[0] != len(libraries):
+            if len(inputs_per_library) != len(libraries):
                 raise ValueError(
                     "If specifying different inputs for each library, then "
                     "first dimension of inputs_per_library must be equal to "
                     "the number of libraries being used."
                 )
-            if np.any(inputs_per_library < 0):
+            if isinstance(inputs_per_library, np.ndarray):
+                warn(
+                    "inputs_per_library should no longer be passed as a numpy array",
+                    UserWarning,
+                )
+                inputs_per_library = [list(row) for row in inputs_per_library]
+            if any(x_ind < 0 for inputs in inputs_per_library for x_ind in inputs):
                 raise ValueError(
                     "The inputs_per_library parameter must be a numpy array "
                     "of integers with values between 0 and "
@@ -142,10 +131,9 @@ def __init__(
                         "specify at least two libraries (there should be at "
                         "least two entries with value 1 in the tensor_array)."
                     )
-        self.tensor_array_ = tensor_array
-        self.inputs_per_library_ = inputs_per_library
-        self.libraries_full_ = self.libraries_
-        self.exclude_libs_ = exclude_libraries
+        self.tensor_array = tensor_array
+        self.inputs_per_library = inputs_per_library
+        self.exclude_libraries = exclude_libraries
 
     @x_sequence_or_item
     def fit(self, x_full, y=None):
@@ -165,14 +153,17 @@ def fit(self, x_full, y=None):
         self.n_features_in_ = n_features
 
         # If parameter is not set, use all the inputs
-        if self.inputs_per_library_ is None:
-            temp_inputs = np.tile(range(n_features), len(self.libraries_))
-            self.inputs_per_library_ = np.reshape(
-                temp_inputs, (len(self.libraries_), n_features)
+        if self.inputs_per_library is None:
+            self.inputs_per_library = list(
+                repeat(list(range(n_features)), len(self.libraries))
             )
         else:
             # Check that the numbers in inputs_per_library are sensible
-            if np.any(self.inputs_per_library_ >= n_features):
+            if any(
+                input_ind >= n_features
+                for input_list in self.inputs_per_library
+                for input_ind in input_list
+            ):
                 raise ValueError(
                     "Each row in inputs_per_library must consist of integers "
                     "between 0 and the number of total input features - 1. "
@@ -180,21 +171,19 @@ def fit(self, x_full, y=None):
 
         # First fit all libraries separately below, with subset of the inputs
         fitted_libs = [
-            lib.fit(
-                [x[..., np.unique(self.inputs_per_library_[i, :])] for x in x_full], y
-            )
-            for i, lib in enumerate(self.libraries_)
+            lib.fit([x[..., _unique(self.inputs_per_library[i])] for x in x_full], y)
+            for i, lib in enumerate(self.libraries)
         ]
 
         # Next, tensor some libraries and append them to the list
-        if self.tensor_array_ is not None:
-            num_tensor_prods = np.shape(self.tensor_array_)[0]
+        if self.tensor_array is not None:
+            num_tensor_prods = np.shape(self.tensor_array)[0]
             for i in range(num_tensor_prods):
-                lib_inds = np.ravel(np.where(self.tensor_array_[i]))
+                lib_inds = np.ravel(np.where(self.tensor_array[i]))
                 library_subset = np.asarray(fitted_libs)[lib_inds]
                 library_full = np.prod(library_subset)
                 library_full._set_inputs_per_library(
-                    self.inputs_per_library_[lib_inds, :]
+                    [self.inputs_per_library[lib_ind] for lib_ind in lib_inds]
                 )
                 library_full.fit(x_full, y)
                 fitted_libs.append(library_full)
@@ -203,7 +192,7 @@ def fit(self, x_full, y=None):
         self.n_output_features_ = sum(
             lib.n_output_features_
             for lib in fitted_libs
-            if lib not in self.exclude_libs_
+            if lib not in self.exclude_libraries
         )
 
         # Save fitted libs
@@ -238,11 +227,11 @@ def transform(self, x_full):
 
             xps = []
             for i, lib in enumerate(self.libraries_full_):
-                if i < self.inputs_per_library_.shape[0]:
-                    if i not in self.exclude_libs_:
+                if i < len(self.inputs_per_library):
+                    if i not in self.exclude_libraries:
                         xps.append(
                             lib.transform(
-                                [x[..., np.unique(self.inputs_per_library_[i, :])]]
+                                [x[..., _unique(self.inputs_per_library[i])]]
                             )[0]
                         )
                 else:
@@ -265,35 +254,34 @@ def get_feature_names(self, input_features=None):
         -------
         output_feature_names : list of string, length n_output_features
         """
+        check_is_fitted(self)
         feature_names = list()
         for i, lib in enumerate(self.libraries_full_):
-            if i not in self.exclude_libs_:
-                if i < self.inputs_per_library_.shape[0]:
+            if i not in self.exclude_libraries:
+                if i < len(self.libraries):
                     if input_features is None:
                         input_features_i = [
-                            "x%d" % k for k in np.unique(self.inputs_per_library_[i, :])
+                            "x%d" % k for k in _unique(self.inputs_per_library[i])
                         ]
                     else:
                         input_features_i = np.asarray(input_features)[
-                            np.unique(self.inputs_per_library_[i, :])
+                            _unique(self.inputs_per_library[i])
                         ].tolist()
                 else:
                     # Tensor libraries need all the inputs and then internally
                     # handle the subsampling of the input variables
                     if input_features is None:
-                        input_features_i = [
-                            "x%d" % k for k in range(self.inputs_per_library_.shape[1])
-                        ]
+                        input_features_i = ["x{k}" for k in range(self.n_features_in_)]
                     else:
                         input_features_i = input_features
                 feature_names += lib.get_feature_names(input_features_i)
         return feature_names
 
     def calc_trajectory(self, diff_method, x, t):
-        return self.libraries_[0].calc_trajectory(diff_method, x, t)
+        return self.libraries[0].calc_trajectory(diff_method, x, t)
 
     def get_spatial_grid(self):
-        for lib_k in self.libraries_:
+        for lib_k in self.libraries:
             spatial_grid = lib_k.get_spatial_grid()
             if spatial_grid is not None:
                 return spatial_grid
diff --git a/pysindy/feature_library/parameterized_library.py b/pysindy/feature_library/parameterized_library.py
index 05b302657..3b1edf13b 100644
--- a/pysindy/feature_library/parameterized_library.py
+++ b/pysindy/feature_library/parameterized_library.py
@@ -6,47 +6,29 @@
 
 
 class ParameterizedLibrary(GeneralizedLibrary):
-    """Construct a SINDyCP library to fit multiple trajectories with variable
-    control parameters. The library is composed of a tensor product of a
-    feature library, applied to the input data, and a parameter library,
-    applied to the input control. If the input libraries are weak, the temporal
-    derivatives are automatically rescaled by the appropriate domain volumes.
+    """A tensor product of two libraries with different inputs.
+    Typically, this is a feature library of the input data and a
+    parameter library of input control, making the SINDyCP method.  If
+    the input libraries are weak, the temporal derivatives are
+    automatically rescaled by the appropriate domain volumes.
 
     Parameters
     ----------
-    feature_library : BaseFeatureLibrary, optional (default PolynomialLibrary).
-    Specifies the library function to apply to the input data features.
-
     parameter_library : BaseFeatureLibrary, optional (default PolynomialLibrary).
     Specifies the library function to apply to the input control features.
 
-    num_features : int, optional (default 3)
-    Specifies the number of features in the input data.
+    feature_library : BaseFeatureLibrary, optional (default PolynomialLibrary).
+    Specifies the library function to apply to the input data features.
 
     num_parameters : int, optional (default 3)
     Specifies the number of features in the input control.
 
+    num_features : int, optional (default 3)
+    Specifies the number of features in the input data.
+
     Attributes
     ----------
-    libraries_ : list of libraries
-        Library instances to be applied to the input matrix.
-        Equal to [parameter_library,feature_library].
-
-    tensor_array_ : 2D list of booleans
-        Indicates which pairs of libraries to tensor product together and
-        add to the overall library. Equal to [0,1]
-
-    inputs_per_library_ : 2D np.ndarray
-        Can be used to specify a subset of the variables to use to generate
-        a feature library. Value determined by num_parameters and num_features.
-
-    n_features_in_ : int
-        The total number of input features.
-
-    n_output_features_ : int
-        The total number of output features. The number of output features
-        is the sum of the numbers of output features for each of the
-        concatenated libraries.
+    see GeneralizedLibrary
 
     Examples
     ----------
@@ -66,48 +48,32 @@ class ParameterizedLibrary(GeneralizedLibrary):
 
     def __init__(
         self,
-        parameter_library=PolynomialLibrary(degree=1, include_bias=True),
-        feature_library=PolynomialLibrary(),
-        num_parameters=3,
-        num_features=3,
+        parameter_library: BaseFeatureLibrary = PolynomialLibrary(degree=1),
+        feature_library: BaseFeatureLibrary = PolynomialLibrary(),
+        num_parameters: int = 3,
+        num_features: int = 3,
     ):
-        if not isinstance(feature_library, BaseFeatureLibrary) or not isinstance(
-            parameter_library, BaseFeatureLibrary
-        ):
-            raise ValueError(
-                "Both feature_library and parameter_library must be instances of \
-                 BaseFeatureLibrary."
-            )
-
         if num_parameters <= 0 or num_features <= 0:
             raise ValueError("Both num_parameter and num_feature must be positive.")
-        libraries = [parameter_library, feature_library]
-        tensor_array = [[1, 1]]
-
-        feature_input = np.zeros(num_features + num_parameters, dtype=np.int32)
-        feature_input[:num_features] = np.arange(num_features)
-
-        parameter_input = (
-            np.ones(num_features + num_parameters, dtype=np.int32) * num_features
-        )
-        parameter_input[-num_parameters:] = num_features + np.arange(num_parameters)
-
-        inputs_per_libraries = np.array([parameter_input, feature_input])
-
-        super(ParameterizedLibrary, self).__init__(
-            libraries,
-            tensor_array=tensor_array,
+        inputs_per_library = [
+            range(num_features, num_parameters + num_features),
+            range(num_features),
+        ]
+
+        super().__init__(
+            libraries=[parameter_library, feature_library],
+            tensor_array=[[1, 1]],
             exclude_libraries=[0, 1],
-            inputs_per_library=inputs_per_libraries,
+            inputs_per_library=inputs_per_library,
         )
 
     def calc_trajectory(self, diff_method, x, t):
         # if tensoring weak libraries, add the correction
-        if hasattr(self.libraries_[0], "K"):
-            constants_final = np.ones(self.libraries_[0].K)
-            for k in range(self.libraries_[0].K):
-                constants_final[k] = np.sum(self.libraries_[0].fullweights0[k])
-            x, x_int = self.libraries_[0].calc_trajectory(diff_method, x, t)
+        if hasattr(self.libraries[0], "K"):
+            constants_final = np.ones(self.libraries[0].K)
+            for k in range(self.libraries[0].K):
+                constants_final[k] = np.sum(self.libraries[0].fullweights0[k])
+            x, x_int = self.libraries[0].calc_trajectory(diff_method, x, t)
             return x, x_int * constants_final[:, np.newaxis]
         else:
-            return self.libraries_[0].calc_trajectory(diff_method, x, t)
+            return self.libraries[0].calc_trajectory(diff_method, x, t)
diff --git a/pysindy/feature_library/pde_library.py b/pysindy/feature_library/pde_library.py
index 4716ac73d..d6c8666c9 100644
--- a/pysindy/feature_library/pde_library.py
+++ b/pysindy/feature_library/pde_library.py
@@ -118,7 +118,6 @@ def __init__(
         is_uniform=None,
         periodic=None,
     ):
-        super().__init__()
         self.functions = library_functions
         self.derivative_order = derivative_order
         self.function_names = function_names
diff --git a/pysindy/feature_library/sindy_pi_library.py b/pysindy/feature_library/sindy_pi_library.py
index 04535369a..8d5f054a7 100644
--- a/pysindy/feature_library/sindy_pi_library.py
+++ b/pysindy/feature_library/sindy_pi_library.py
@@ -139,7 +139,6 @@ def __init__(
         differentiation_method=None,
         include_bias=False,
     ):
-        super().__init__()
         self.x_functions = library_functions
         self.x_dot_functions = x_dot_library_functions
         self.function_names = function_names
diff --git a/pysindy/feature_library/weak_pde_library.py b/pysindy/feature_library/weak_pde_library.py
index 3c7266b31..5aa3cbbb6 100644
--- a/pysindy/feature_library/weak_pde_library.py
+++ b/pysindy/feature_library/weak_pde_library.py
@@ -171,7 +171,6 @@ def __init__(
         is_uniform=None,
         periodic=None,
     ):
-        super().__init__()
         self.functions = library_functions
         self.derivative_order = derivative_order
         self.function_names = function_names


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index a53294fbd..285446f40 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -341,12 +341,9 @@ def data_quadratic_library():
 @pytest.fixture
 def data_generalized_library():
     tensor_array = [[1, 1]]
-    inputs_temp = np.tile([0, 1, 2], 2)
-    inputs_per_library = np.reshape(inputs_temp, (2, 3))
     return GeneralizedLibrary(
         [PolynomialLibrary(), FourierLibrary()],
         tensor_array=tensor_array,
-        inputs_per_library=inputs_per_library,
     )
 
 
diff --git a/test/test_feature_library.py b/test/test_feature_library.py
index 7e551cf3a..5bbc232bb 100644
--- a/test/test_feature_library.py
+++ b/test/test_feature_library.py
@@ -170,23 +170,19 @@ def test_weak_pde_library_bad_parameters(params):
         ),
         dict(
             libraries=[PolynomialLibrary(), PolynomialLibrary()],
-            inputs_per_library=np.array([[0, 1], [0, 100]]),
+            inputs_per_library=[[0, 1], [0, 100]],
         ),
         dict(
             libraries=[PolynomialLibrary(), PolynomialLibrary()],
-            inputs_per_library=np.array([0, 0]),
+            inputs_per_library=[[0, 1]],
         ),
         dict(
             libraries=[PolynomialLibrary(), PolynomialLibrary()],
-            inputs_per_library=np.array([[0, 1]]),
+            inputs_per_library=[[0, 1, 2], [0, 1, 2], [0, 1, 2]],
         ),
         dict(
             libraries=[PolynomialLibrary(), PolynomialLibrary()],
-            inputs_per_library=np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2]]),
-        ),
-        dict(
-            libraries=[PolynomialLibrary(), PolynomialLibrary()],
-            inputs_per_library=np.array([[0, 1, 2], [0, 1, -1]]),
+            inputs_per_library=[[0, 1, 2], [0, 1, -1]],
         ),
     ],
 )
@@ -447,12 +443,7 @@ def test_generalized_library(data_lorenz):
 
     tensor_array = [[0, 1, 1], [1, 0, 1]]
 
-    inputs_per_library = np.tile([0, 1, 2], 3)
-    inputs_per_library = np.reshape(inputs_per_library, (3, 3))
-    inputs_per_library[0, 0] = 1
-    inputs_per_library[1, 1] = 0
-    inputs_per_library[2, 1] = 0
-    inputs_per_library[2, 2] = 0
+    inputs_per_library = [[1, 2], [0, 2], [0]]
 
     # First try without tensor libraries and subset of the input variables
     sindy_library = GeneralizedLibrary(