From 18cc84e77778ef527c5482c7840805b28bddc195 Mon Sep 17 00:00:00 2001
From: "Brandon T. Willard" <brandonwillard@users.noreply.github.com>
Date: Sun, 7 Mar 2021 14:18:17 -0600
Subject: [PATCH 01/48] Replace custom memoize module with cachetools

---
 pymc3/distributions/distribution.py       |   6 +-
 pymc3/memoize.py                          | 113 ----------------------
 pymc3/model.py                            |   9 +-
 pymc3/tests/test_memo.py                  |  68 -------------
 pymc3/tests/test_util.py                  |  52 ++++++++++
 pymc3/tests/test_variational_inference.py |   7 +-
 pymc3/util.py                             |  75 ++++++++++++++
 pymc3/variational/flows.py                |   2 +-
 pymc3/variational/opvi.py                 |  27 +++---
 pymc3/variational/stein.py                |   5 +-
 requirements.txt                          |   1 +
 11 files changed, 154 insertions(+), 211 deletions(-)
 delete mode 100644 pymc3/memoize.py
 delete mode 100644 pymc3/tests/test_memo.py

diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py
index c24a9d9df6..3ba2d0a040 100644
--- a/pymc3/distributions/distribution.py
+++ b/pymc3/distributions/distribution.py
@@ -32,6 +32,7 @@
 import theano.graph.basic
 import theano.tensor as tt
 
+from cachetools import LRUCache, cached
 from theano import function
 
 from pymc3.distributions.shape_utils import (
@@ -39,7 +40,6 @@
     get_broadcastable_dist_samples,
     to_tuple,
 )
-from pymc3.memoize import memoize
 from pymc3.model import (
     ContextMeta,
     FreeRV,
@@ -48,7 +48,7 @@
     ObservedRV,
     build_named_node_tree,
 )
-from pymc3.util import get_repr_for_variable, get_var_name
+from pymc3.util import get_repr_for_variable, get_var_name, hash_key
 from pymc3.vartypes import string_types, theano_constant
 
 __all__ = [
@@ -840,7 +840,7 @@ def draw_values(params, point=None, size=None):
     return [evaluated[j] for j in params]  # set the order back
 
 
-@memoize
+@cached(LRUCache(128), key=hash_key)
 def _compile_theano_function(param, vars, givens=None):
     """Compile theano function for a given parameter and input variables.
 
diff --git a/pymc3/memoize.py b/pymc3/memoize.py
deleted file mode 100644
index cbe791f10c..0000000000
--- a/pymc3/memoize.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#   Copyright 2020 The PyMC Developers
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-
-import collections
-import functools
-
-import dill
-
-from pymc3.util import biwrap
-
-CACHE_REGISTRY = []
-
-
-@biwrap
-def memoize(obj, bound=False):
-    """
-    Decorator to apply memoization to expensive functions.
-    It uses a custom `hashable` helper function to hash typically unhashable Python objects.
-
-    Parameters
-    ----------
-    obj : callable
-        the function to apply the caching to
-    bound : bool
-        indicates if the [obj] is a bound method (self as first argument)
-        For bound methods, the cache is kept in a `_cache` attribute on [self].
-    """
-    # this is declared not to be a bound method, so just attach new attr to obj
-    if not bound:
-        obj.cache = {}
-        CACHE_REGISTRY.append(obj.cache)
-
-    @functools.wraps(obj)
-    def memoizer(*args, **kwargs):
-        if not bound:
-            key = (hashable(args), hashable(kwargs))
-            cache = obj.cache
-        else:
-            # bound methods have self as first argument, remove it to compute key
-            key = (hashable(args[1:]), hashable(kwargs))
-            if not hasattr(args[0], "_cache"):
-                setattr(args[0], "_cache", collections.defaultdict(dict))
-                # do not add to cache registry
-            cache = getattr(args[0], "_cache")[obj.__name__]
-        if key not in cache:
-            cache[key] = obj(*args, **kwargs)
-
-        return cache[key]
-
-    return memoizer
-
-
-def clear_cache(obj=None):
-    if obj is None:
-        for c in CACHE_REGISTRY:
-            c.clear()
-    else:
-        if isinstance(obj, WithMemoization):
-            for v in getattr(obj, "_cache", {}).values():
-                v.clear()
-        else:
-            obj.cache.clear()
-
-
-class WithMemoization:
-    def __hash__(self):
-        return hash(id(self))
-
-    def __getstate__(self):
-        state = self.__dict__.copy()
-        state.pop("_cache", None)
-        return state
-
-    def __setstate__(self, state):
-        self.__dict__.update(state)
-
-
-def hashable(a) -> int:
-    """
-    Hashes many kinds of objects, including some that are unhashable through the builtin `hash` function.
-    Lists and tuples are hashed based on their elements.
-    """
-    if isinstance(a, dict):
-        # first hash the keys and values with hashable
-        # then hash the tuple of int-tuples with the builtin
-        return hash(tuple((hashable(k), hashable(v)) for k, v in a.items()))
-    if isinstance(a, (tuple, list)):
-        # lists are mutable and not hashable by default
-        # for memoization, we need the hash to depend on the items
-        return hash(tuple(hashable(i) for i in a))
-    try:
-        return hash(a)
-    except TypeError:
-        pass
-    # Not hashable >>>
-    try:
-        return hash(dill.dumps(a))
-    except Exception:
-        if hasattr(a, "__dict__"):
-            return hashable(a.__dict__)
-        else:
-            return id(a)
diff --git a/pymc3/model.py b/pymc3/model.py
index 349affcfa0..dff1e3b78b 100644
--- a/pymc3/model.py
+++ b/pymc3/model.py
@@ -27,6 +27,7 @@
 import theano.sparse as sparse
 import theano.tensor as tt
 
+from cachetools import LRUCache, cachedmethod
 from pandas import Series
 from theano.compile import SharedVariable
 from theano.graph.basic import Apply
@@ -36,10 +37,8 @@
 
 from pymc3.blocking import ArrayOrdering, DictToArrayBijection
 from pymc3.exceptions import ImputationWarning
-from pymc3.math import flatten_list
-from pymc3.memoize import WithMemoization, memoize
 from pymc3.theanof import floatX, generator, gradient, hessian, inputvars
-from pymc3.util import get_transformed_name, get_var_name
+from pymc3.util import WithMemoization, get_transformed_name, get_var_name, hash_key
 from pymc3.vartypes import continuous_types, discrete_types, isgenerator, typefilter
 
 __all__ = [
@@ -944,7 +943,9 @@ def isroot(self):
         return self.parent is None
 
     @property  # type: ignore
-    @memoize(bound=True)
+    @cachedmethod(
+        lambda self: self.__dict__.setdefault("_bijection_cache", LRUCache(128)), key=hash_key
+    )
     def bijection(self):
         vars = inputvars(self.vars)
 
diff --git a/pymc3/tests/test_memo.py b/pymc3/tests/test_memo.py
deleted file mode 100644
index 6653662e32..0000000000
--- a/pymc3/tests/test_memo.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#   Copyright 2020 The PyMC Developers
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-import numpy as np
-
-import pymc3 as pm
-
-from pymc3 import memoize
-
-
-def test_memo():
-    def fun(inputs, suffix="_a"):
-        return str(inputs) + str(suffix)
-
-    inputs = ["i1", "i2"]
-    assert fun(inputs) == "['i1', 'i2']_a"
-    assert fun(inputs, "_b") == "['i1', 'i2']_b"
-
-    funmem = memoize.memoize(fun)
-    assert hasattr(fun, "cache")
-    assert isinstance(fun.cache, dict)
-    assert len(fun.cache) == 0
-
-    # call the memoized function with a list input
-    # and check the size of the cache!
-    assert funmem(inputs) == "['i1', 'i2']_a"
-    assert funmem(inputs) == "['i1', 'i2']_a"
-    assert len(fun.cache) == 1
-    assert funmem(inputs, "_b") == "['i1', 'i2']_b"
-    assert funmem(inputs, "_b") == "['i1', 'i2']_b"
-    assert len(fun.cache) == 2
-
-    # add items to the inputs list (the list instance remains identical !!)
-    inputs.append("i3")
-    assert funmem(inputs) == "['i1', 'i2', 'i3']_a"
-    assert funmem(inputs) == "['i1', 'i2', 'i3']_a"
-    assert len(fun.cache) == 3
-
-
-def test_hashing_of_rv_tuples():
-    obs = np.random.normal(-1, 0.1, size=10)
-    with pm.Model() as pmodel:
-        mu = pm.Normal("mu", 0, 1)
-        sd = pm.Gamma("sd", 1, 2)
-        dd = pm.DensityDist(
-            "dd",
-            pm.Normal.dist(mu, sd).logp,
-            random=pm.Normal.dist(mu, sd).random,
-            observed=obs,
-        )
-        for freerv in [mu, sd, dd] + pmodel.free_RVs:
-            for structure in [
-                freerv,
-                {"alpha": freerv, "omega": None},
-                [freerv, []],
-                (freerv, []),
-            ]:
-                assert isinstance(memoize.hashable(structure), int)
diff --git a/pymc3/tests/test_util.py b/pymc3/tests/test_util.py
index adb334fb8a..05b6bdf52d 100644
--- a/pymc3/tests/test_util.py
+++ b/pymc3/tests/test_util.py
@@ -15,12 +15,14 @@
 import numpy as np
 import pytest
 
+from cachetools import cached
 from numpy.testing import assert_almost_equal
 
 import pymc3 as pm
 
 from pymc3.distributions.transforms import Transform
 from pymc3.tests.helpers import SeededTest
+from pymc3.util import hash_key, hashable, locally_cachedmethod
 
 
 class TestTransformName:
@@ -167,3 +169,53 @@ def test_dtype_error(self):
             raise pm.exceptions.DtypeError("With types.", actual=int, expected=str)
         assert "int" in exinfo.value.args[0] and "str" in exinfo.value.args[0]
         pass
+
+
+def test_hashing_of_rv_tuples():
+    obs = np.random.normal(-1, 0.1, size=10)
+    with pm.Model() as pmodel:
+        mu = pm.Normal("mu", 0, 1)
+        sd = pm.Gamma("sd", 1, 2)
+        dd = pm.DensityDist(
+            "dd",
+            pm.Normal.dist(mu, sd).logp,
+            random=pm.Normal.dist(mu, sd).random,
+            observed=obs,
+        )
+        for freerv in [mu, sd, dd] + pmodel.free_RVs:
+            for structure in [
+                freerv,
+                {"alpha": freerv, "omega": None},
+                [freerv, []],
+                (freerv, []),
+            ]:
+                assert isinstance(hashable(structure), int)
+
+
+def test_hash_key():
+    class Bad1:
+        def __hash__(self):
+            return 329
+
+    class Bad2:
+        def __hash__(self):
+            return 329
+
+    b1 = Bad1()
+    b2 = Bad2()
+
+    assert b1 != b2
+
+    @cached({}, key=hash_key)
+    def some_func(x):
+        return x
+
+    assert some_func(b1) != some_func(b2)
+
+    class TestClass:
+        @locally_cachedmethod
+        def some_method(self, x):
+            return x
+
+    tc = TestClass()
+    assert tc.some_method(b1) != tc.some_method(b2)
diff --git a/pymc3/tests/test_variational_inference.py b/pymc3/tests/test_variational_inference.py
index 1ef9b61629..1f1e035277 100644
--- a/pymc3/tests/test_variational_inference.py
+++ b/pymc3/tests/test_variational_inference.py
@@ -22,7 +22,6 @@
 import theano.tensor as tt
 
 import pymc3 as pm
-import pymc3.memoize
 import pymc3.util
 
 from pymc3.tests import models
@@ -757,14 +756,12 @@ def test_remove_scan_op():
 def test_clear_cache():
     import pickle
 
-    pymc3.memoize.clear_cache()
-    assert all(len(c) == 0 for c in pymc3.memoize.CACHE_REGISTRY)
     with pm.Model():
         pm.Normal("n", 0, 1)
         inference = ADVI()
         inference.fit(n=10)
         assert any(len(c) != 0 for c in inference.approx._cache.values())
-        pymc3.memoize.clear_cache(inference.approx)
+        inference.approx._cache.clear()
         # should not be cleared at this call
         assert all(len(c) == 0 for c in inference.approx._cache.values())
         new_a = pickle.loads(pickle.dumps(inference.approx))
@@ -772,7 +769,7 @@ def test_clear_cache():
         inference_new = pm.KLqp(new_a)
         inference_new.fit(n=10)
         assert any(len(c) != 0 for c in inference_new.approx._cache.values())
-        pymc3.memoize.clear_cache(inference_new.approx)
+        inference_new.approx._cache.clear()
         assert all(len(c) == 0 for c in inference_new.approx._cache.values())
 
 
diff --git a/pymc3/util.py b/pymc3/util.py
index 84b4f6c3e5..dbd6219a1e 100644
--- a/pymc3/util.py
+++ b/pymc3/util.py
@@ -19,9 +19,11 @@
 from typing import Dict, List, Tuple, Union
 
 import arviz
+import dill
 import numpy as np
 import xarray
 
+from cachetools import LRUCache, cachedmethod
 from theano.tensor import TensorVariable
 
 from pymc3.exceptions import SamplingError
@@ -304,3 +306,76 @@ def chains_and_samples(data: Union[xarray.Dataset, arviz.InferenceData]) -> Tupl
     nchains = coords["chain"].sizes["chain"]
     nsamples = coords["draw"].sizes["draw"]
     return nchains, nsamples
+
+
+def hashable(a=None) -> int:
+    """
+    Hashes many kinds of objects, including some that are unhashable through the builtin `hash` function.
+    Lists and tuples are hashed based on their elements.
+    """
+    if isinstance(a, dict):
+        # first hash the keys and values with hashable
+        # then hash the tuple of int-tuples with the builtin
+        return hash(tuple((hashable(k), hashable(v)) for k, v in a.items()))
+    if isinstance(a, (tuple, list)):
+        # lists are mutable and not hashable by default
+        # for memoization, we need the hash to depend on the items
+        return hash(tuple(hashable(i) for i in a))
+    try:
+        return hash(a)
+    except TypeError:
+        pass
+    # Not hashable >>>
+    try:
+        return hash(dill.dumps(a))
+    except Exception:
+        if hasattr(a, "__dict__"):
+            return hashable(a.__dict__)
+        else:
+            return id(a)
+
+
+def hash_key(*args, **kwargs):
+    return tuple(HashableWrapper(a) for a in args + tuple(kwargs.items()))
+
+
+class HashableWrapper:
+    __slots__ = ("obj",)
+
+    def __init__(self, obj):
+        self.obj = obj
+
+    def __hash__(self):
+        return hashable(self.obj)
+
+    def __eq__(self, other):
+        return self.obj == other
+
+    def __repr__(self):
+        return f"{type(self).__name__}({self.obj})"
+
+
+class WithMemoization:
+    def __hash__(self):
+        return hash(id(self))
+
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        state.pop("_cache", None)
+        return state
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+
+
+def locally_cachedmethod(f):
+
+    from collections import defaultdict
+
+    def self_cache_fn(f_name):
+        def cf(self):
+            return self.__dict__.setdefault("_cache", defaultdict(lambda: LRUCache(128)))[f_name]
+
+        return cf
+
+    return cachedmethod(self_cache_fn(f.__name__), key=hash_key)(f)
diff --git a/pymc3/variational/flows.py b/pymc3/variational/flows.py
index 601c7351fa..bc87af5eba 100644
--- a/pymc3/variational/flows.py
+++ b/pymc3/variational/flows.py
@@ -18,7 +18,7 @@
 from theano import tensor as tt
 
 from pymc3.distributions.dist_math import rho2sigma
-from pymc3.memoize import WithMemoization
+from pymc3.util import WithMemoization
 from pymc3.variational import opvi
 from pymc3.variational.opvi import collect_shared_to_list, node_property
 
diff --git a/pymc3/variational/opvi.py b/pymc3/variational/opvi.py
index ebf4a9cda8..814f49b363 100644
--- a/pymc3/variational/opvi.py
+++ b/pymc3/variational/opvi.py
@@ -57,10 +57,14 @@
 
 from pymc3.backends import NDArray
 from pymc3.blocking import ArrayOrdering, DictToArrayBijection, VarMap
-from pymc3.memoize import WithMemoization, memoize
 from pymc3.model import modelcontext
 from pymc3.theanof import identity, tt_rng
-from pymc3.util import get_default_varnames, get_transformed
+from pymc3.util import (
+    WithMemoization,
+    get_default_varnames,
+    get_transformed,
+    locally_cachedmethod,
+)
 from pymc3.variational.updates import adagrad_window
 
 __all__ = ["ObjectiveFunction", "Operator", "TestFunction", "Group", "Approximation"]
@@ -111,21 +115,18 @@ def inner(*args, **kwargs):
 
 def node_property(f):
     """A shortcut for wrapping method to accessible tensor"""
+
     if isinstance(f, str):
 
         def wrapper(fn):
-            return property(
-                memoize(
-                    theano.config.change_flags(compute_test_value="off")(append_name(f)(fn)),
-                    bound=True,
-                )
-            )
+            ff = append_name(f)(fn)
+            f_ = theano.config.change_flags(compute_test_value="off")(ff)
+            return property(locally_cachedmethod(f_))
 
         return wrapper
     else:
-        return property(
-            memoize(theano.config.change_flags(compute_test_value="off")(f), bound=True)
-        )
+        f_ = theano.config.change_flags(compute_test_value="off")(f)
+        return property(locally_cachedmethod(f_))
 
 
 @theano.config.change_flags(compute_test_value="ignore")
@@ -1586,9 +1587,7 @@ def vars_names(vs):
             raise KeyError("%r not found" % name)
         return found
 
-    @property
-    @memoize(bound=True)
-    @theano.config.change_flags(compute_test_value="off")
+    @node_property
     def sample_dict_fn(self):
         s = tt.iscalar()
         names = [v.name for v in self.model.free_RVs]
diff --git a/pymc3/variational/stein.py b/pymc3/variational/stein.py
index ca9a924910..216a9d8276 100644
--- a/pymc3/variational/stein.py
+++ b/pymc3/variational/stein.py
@@ -15,8 +15,8 @@
 import theano
 import theano.tensor as tt
 
-from pymc3.memoize import WithMemoization, memoize
 from pymc3.theanof import floatX
+from pymc3.util import WithMemoization, locally_cachedmethod
 from pymc3.variational.opvi import node_property
 from pymc3.variational.test_functions import rbf
 
@@ -90,7 +90,6 @@ def logp_norm(self):
             )
         return sized_symbolic_logp / self.approx.symbolic_normalizing_constant
 
-    @memoize
-    @theano.config.change_flags(compute_test_value="off")
+    @locally_cachedmethod
     def _kernel(self):
         return self._kernel_f(self.input_joint_matrix)
diff --git a/requirements.txt b/requirements.txt
index 93cb80ebc1..a103801473 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 arviz>=0.11.0
+cachetools>=4.2.1
 dill
 fastprogress>=0.2.0
 numpy>=1.15.0

From 9218f336994b41fc998a6740f87c2bb0a8aa6fcc Mon Sep 17 00:00:00 2001
From: "Brandon T. Willard" <brandonwillard@users.noreply.github.com>
Date: Sun, 7 Mar 2021 15:07:57 -0600
Subject: [PATCH 02/48] Add comment to RELEASE-NOTES.md

---
 RELEASE-NOTES.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 15dd545a19..71d0b35085 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -9,7 +9,8 @@
 + ...
 
 ### Maintenance
-+ ...
+- ⚠ Our memoization mechanism wasn't robust against hash collisions (#4506), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util`.
+- ...
 
 ## PyMC3 3.11.1 (12 February 2021)
 

From 03448f7e87f8f53cf4ce52e21c276cf4998fd440 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Wed, 10 Mar 2021 18:02:01 +0100
Subject: [PATCH 03/48] Keep broadcasting information in
 make_shared_replacements (#4526)

---
 RELEASE-NOTES.md            |  4 ++--
 pymc3/tests/test_theanof.py | 25 +++++++++++++++++++++++++
 pymc3/theanof.py            |  7 ++++++-
 3 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 71d0b35085..388ca345e4 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -9,8 +9,8 @@
 + ...
 
 ### Maintenance
-- ⚠ Our memoization mechanism wasn't robust against hash collisions (#4506), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util`.
-- ...
+- ⚠ Our memoization mechanism wasn't robust against hash collisions (#4506), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util` (see #4525).
+- `pm.make_shared_replacements` now retains broadcasting information which fixes issues with Metropolis samplers (see [#4492](https://github.com/pymc-devs/pymc3/pull/4492)).
 
 ## PyMC3 3.11.1 (12 February 2021)
 
diff --git a/pymc3/tests/test_theanof.py b/pymc3/tests/test_theanof.py
index d54aed680d..bf2ff61fce 100644
--- a/pymc3/tests/test_theanof.py
+++ b/pymc3/tests/test_theanof.py
@@ -19,6 +19,8 @@
 import theano
 import theano.tensor as tt
 
+import pymc3 as pm
+
 from pymc3.theanof import _conversion_map, take_along_axis
 from pymc3.vartypes import int_types
 
@@ -26,6 +28,29 @@
 INTX = str(_conversion_map[FLOATX])
 
 
+class TestBroadcasting:
+    def test_make_shared_replacements(self):
+        """Check if pm.make_shared_replacements preserves broadcasting."""
+
+        with pm.Model() as test_model:
+            test1 = pm.Normal("test1", mu=0.0, sigma=1.0, shape=(1, 10))
+            test2 = pm.Normal("test2", mu=0.0, sigma=1.0, shape=(10, 1))
+
+        # Replace test1 with a shared variable, keep test 2 the same
+        replacement = pm.make_shared_replacements([test_model.test2], test_model)
+        assert test_model.test1.broadcastable == replacement[test_model.test1].broadcastable
+
+    def test_metropolis_sampling(self):
+        """Check if the Metropolis sampler can handle broadcasting."""
+        with pm.Model() as test_model:
+            test1 = pm.Normal("test1", mu=0.0, sigma=1.0, shape=(1, 10))
+            test2 = pm.Normal("test2", mu=test1, sigma=1.0, shape=(10, 10))
+
+            step = pm.Metropolis()
+            # This should fail immediately if broadcasting does not work.
+            pm.sample(tune=5, draws=7, cores=1, step=step, compute_convergence_checks=False)
+
+
 def _make_along_axis_idx(arr_shape, indices, axis):
     # compute dimensions to iterate over
     if str(indices.dtype) not in int_types:
diff --git a/pymc3/theanof.py b/pymc3/theanof.py
index c40311da6e..50ab8afdaf 100644
--- a/pymc3/theanof.py
+++ b/pymc3/theanof.py
@@ -235,7 +235,12 @@ def make_shared_replacements(vars, model):
     Dict of variable -> new shared variable
     """
     othervars = set(model.vars) - set(vars)
-    return {var: theano.shared(var.tag.test_value, var.name + "_shared") for var in othervars}
+    return {
+        var: theano.shared(
+            var.tag.test_value, var.name + "_shared", broadcastable=var.broadcastable
+        )
+        for var in othervars
+    }
 
 
 def join_nonshared_inputs(xs, vars, shared, make_shared=False):

From 3079983d33b88be6e2eb757ad9919711b52528cb Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 14 Mar 2021 17:24:30 +0100
Subject: [PATCH 04/48] Bring back stats and plotting aliases (#4536)

* Restore plots and stats wrappers (see #4528)
* Alias stats/plots from ArviZ and add deprecation warnings in old wrappers
* Link to ArviZ docs, improve warnings UX
* Use intersphinx for docs linking

Co-authored-by: Oriol Abril-Pla <oriol.abril.pla@gmail.com>
---
 RELEASE-NOTES.md          |  12 ++++-
 docs/source/api/plots.rst |   7 ++-
 docs/source/api/stats.rst |   9 +++-
 pymc3/__init__.py         |   1 +
 pymc3/plots/__init__.py   | 110 +++++++++++++++++++++++++++++++++++---
 pymc3/stats/__init__.py   |  31 +++++++++++
 6 files changed, 155 insertions(+), 15 deletions(-)
 create mode 100644 pymc3/stats/__init__.py

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 388ca345e4..fbb68dada1 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,12 +1,20 @@
 # Release Notes
 
-## PyMC3 vNext (TBD)
+## PyMC3 3.11.2 (TBD)
 ### Breaking Changes
 + ...
 
 ### New Features
 + `pm.math.cartesian` can now handle inputs that are themselves >1D (see [#4482](https://github.com/pymc-devs/pymc3/pull/4482)).
-+ ...
++ Statistics and plotting functions that were removed in `3.11.0` were brought back, albeit with deprecation warnings if an old naming scheme is used (see [#4536](https://github.com/pymc-devs/pymc3/pull/4536)). In order to future proof your code, rename these function calls:
+  + `pm.traceplot` → `pm.plot_trace`
+  + `pm.compareplot` → `pm.plot_compare` (here you might need to rename some columns in the input according to the [`arviz.plot_compare` documentation](https://arviz-devs.github.io/arviz/api/generated/arviz.plot_compare.html))
+  + `pm.autocorrplot` → `pm.plot_autocorr`
+  + `pm.forestplot` → `pm.plot_forest`
+  + `pm.kdeplot` → `pm.plot_kde`
+  + `pm.energyplot` → `pm.plot_energy`
+  + `pm.densityplot` → `pm.plot_density`
+  + `pm.pairplot` → `pm.plot_pair`
 
 ### Maintenance
 - ⚠ Our memoization mechanism wasn't robust against hash collisions (#4506), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util` (see #4525).
diff --git a/docs/source/api/plots.rst b/docs/source/api/plots.rst
index 0dec109b7f..1e2b2e0139 100644
--- a/docs/source/api/plots.rst
+++ b/docs/source/api/plots.rst
@@ -7,8 +7,7 @@ Plots
 Plots are delegated to the
 `ArviZ <https://arviz-devs.github.io/arviz/index.html>`_.
 library, a general purpose library for
-"exploratory analysis of Bayesian models."
-Refer to its documentation to use the plotting functions directly.
+"exploratory analysis of Bayesian models".
 
-.. automodule:: pymc3.plots.posteriorplot
-   :members:
+Functions from the `arviz.plots` module are available through ``pymc3.<function>`` or ``pymc3.plots.<function>``,
+but for their API documentation please refer to the :ref:`ArviZ documentation <arviz:plot_api>`.
diff --git a/docs/source/api/stats.rst b/docs/source/api/stats.rst
index bf85bb596d..4cd23c3446 100644
--- a/docs/source/api/stats.rst
+++ b/docs/source/api/stats.rst
@@ -1,8 +1,13 @@
 *****
 Stats
 *****
+
+.. currentmodule:: pymc3.stats
+
 Statistics and diagnostics are delegated to the
 `ArviZ <https://arviz-devs.github.io/arviz/index.html>`_.
 library, a general purpose library for
-"exploratory analysis of Bayesian models."
-Refer to its documentation to use the diagnostics functions directly.
+"exploratory analysis of Bayesian models".
+
+Functions from the `arviz.stats` module are available through ``pymc3.<function>`` or ``pymc3.stats.<function>``,
+but for their API documentation please refer to the :ref:`ArviZ documentation <arviz:stats_api>`.
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 1e51deeb64..1378d0d993 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -61,6 +61,7 @@ def __set_compiler_flags():
 from pymc3.plots import *
 from pymc3.sampling import *
 from pymc3.smc import *
+from pymc3.stats import *
 from pymc3.step_methods import *
 from pymc3.tests import test
 from pymc3.theanof import *
diff --git a/pymc3/plots/__init__.py b/pymc3/plots/__init__.py
index 5923114cee..03c7715c24 100644
--- a/pymc3/plots/__init__.py
+++ b/pymc3/plots/__init__.py
@@ -1,4 +1,4 @@
-#   Copyright 2020 The PyMC Developers
+#   Copyright 2021 The PyMC Developers
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -12,12 +12,11 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-"""PyMC3 Plotting.
+"""Alias for the `plots` submodule from ArviZ.
 
-Plots are delegated to the `ArviZ <https://arviz-devs.github.io/arviz/>`_ library, a general purpose library for
-exploratory analysis of Bayesian models. For more details, see https://arviz-devs.github.io/arviz/.
-
-Only `plot_posterior_predictive_glm` is kept in the PyMC code base for now, but it will move to ArviZ once the latter adds features for regression plots.
+Plots are delegated to the ArviZ library, a general purpose library for
+"exploratory analysis of Bayesian models."
+See https://arviz-devs.github.io/arviz/ for details on plots.
 """
 import functools
 import sys
@@ -25,6 +24,103 @@
 
 import arviz as az
 
+# Makes this module as identical to arviz.plots as possible
+for attr in az.plots.__all__:
+    obj = getattr(az.plots, attr)
+    if not attr.startswith("__"):
+        setattr(sys.modules[__name__], attr, obj)
+
+
+def map_args(func, alias: str):
+    @functools.wraps(func)
+    def wrapped(*args, **kwargs):
+        if "varnames" in kwargs:
+            raise DeprecationWarning(
+                f"The `varnames` kwarg was renamed to `var_names`.", stacklevel=2
+            )
+        original = func.__name__
+        warnings.warn(
+            f"The function `{alias}` from PyMC3 is just an alias for `{original}` from ArviZ. "
+            f"Please switch to `pymc3.{original}` or `arviz.{original}`.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return func(*args, **kwargs)
+
+    return wrapped
+
+
+# Always show the DeprecationWarnings
+warnings.filterwarnings("once", category=DeprecationWarning, module="pymc3.plots")
+
+
+# Aliases of ArviZ functions
+autocorrplot = map_args(az.plot_autocorr, alias="autocorrplot")
+forestplot = map_args(az.plot_forest, alias="forestplot")
+kdeplot = map_args(az.plot_kde, alias="kdeplot")
+energyplot = map_args(az.plot_energy, alias="energyplot")
+densityplot = map_args(az.plot_density, alias="densityplot")
+pairplot = map_args(az.plot_pair, alias="pairplot")
+traceplot = map_args(az.plot_trace, alias="traceplot")
+
+
+# Customized with kwarg reformatting
+@functools.wraps(az.plot_compare)
+def compareplot(*args, **kwargs):
+    warnings.warn(
+        f"The function `compareplot` from PyMC3 is an alias for `plot_compare` from ArviZ. "
+        "It also applies some kwarg replacements. Nevertheless, please switch "
+        f"to `pymc3.plot_compare` or `arviz.plot_compare`.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    if "comp_df" in kwargs:
+        comp_df = kwargs["comp_df"].copy()
+    else:
+        args = list(args)
+        comp_df = args[0].copy()
+    if "WAIC" in comp_df.columns:
+        comp_df = comp_df.rename(
+            index=str,
+            columns={
+                "WAIC": "waic",
+                "pWAIC": "p_waic",
+                "dWAIC": "d_waic",
+                "SE": "se",
+                "dSE": "dse",
+                "var_warn": "warning",
+            },
+        )
+    elif "LOO" in comp_df.columns:
+        comp_df = comp_df.rename(
+            index=str,
+            columns={
+                "LOO": "loo",
+                "pLOO": "p_loo",
+                "dLOO": "d_loo",
+                "SE": "se",
+                "dSE": "dse",
+                "shape_warn": "warning",
+            },
+        )
+    if "comp_df" in kwargs:
+        kwargs["comp_df"] = comp_df
+    else:
+        args[0] = comp_df
+    return az.plot_compare(*args, **kwargs)
+
+
 from pymc3.plots.posteriorplot import plot_posterior_predictive_glm
 
-__all__ = ["plot_posterior_predictive_glm"]
+__all__ = tuple(az.plots.__all__) + (
+    "autocorrplot",
+    "compareplot",
+    "forestplot",
+    "kdeplot",
+    "plot_posterior",
+    "traceplot",
+    "energyplot",
+    "densityplot",
+    "pairplot",
+    "plot_posterior_predictive_glm",
+)
diff --git a/pymc3/stats/__init__.py b/pymc3/stats/__init__.py
new file mode 100644
index 0000000000..0880ca8b52
--- /dev/null
+++ b/pymc3/stats/__init__.py
@@ -0,0 +1,31 @@
+#   Copyright 2021 The PyMC Developers
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""Alias for the `stats` submodule from ArviZ.
+
+Diagnostics and auxiliary statistical functions are delegated to the ArviZ library, a general
+purpose library for "exploratory analysis of Bayesian models."
+See https://arviz-devs.github.io/arviz/ for details.
+"""
+import sys
+
+import arviz as az
+
+for attr in az.stats.__all__:
+    obj = getattr(az.stats, attr)
+    if not attr.startswith("__"):
+        setattr(sys.modules[__name__], attr, obj)
+
+
+__all__ = tuple(az.stats.__all__)

From 33c0e3555e3fc6a6e8bf5a13ef5b8e9b0dbda022 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 14 Mar 2021 17:10:00 +0100
Subject: [PATCH 05/48] Print informative warning when too old Theano version
 is imported

Closes #4527
---
 pymc3/__init__.py | 15 +++++++++++++--
 requirements.txt  |  1 +
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 1378d0d993..670f40341a 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -19,6 +19,9 @@
 import multiprocessing as mp
 import platform
 
+import semver
+import theano
+
 _log = logging.getLogger("pymc3")
 
 if not logging.root.handlers:
@@ -28,10 +31,18 @@
         _log.addHandler(handler)
 
 
+if not semver.match(theano.__version__, ">=1.1.2"):
+    print(
+        "!" * 60
+        + f"\nThe installed Theano(-PyMC) version ({theano.__version__}) does not match the PyMC3 requirements."
+        + "\nFor PyMC3 to work, Theano must be uninstalled and replaced with Theano-PyMC."
+        + "\nSee https://github.com/pymc-devs/pymc3/wiki for installation instructions.\n"
+        + "!" * 60
+    )
+
+
 def __set_compiler_flags():
     # Workarounds for Theano compiler problems on various platforms
-    import theano
-
     current = theano.config.gcc__cxxflags
     theano.config.gcc__cxxflags = f"{current} -Wno-c++11-narrowing"
 
diff --git a/requirements.txt b/requirements.txt
index a103801473..79eb591a84 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,5 +6,6 @@ numpy>=1.15.0
 pandas>=0.24.0
 patsy>=0.5.1
 scipy>=1.2.0
+semver
 theano-pymc==1.1.2
 typing-extensions>=3.7.4

From c1efb7ad51e34e6bd8b292ef226a7e191adbdb82 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 14 Mar 2021 22:26:20 +0100
Subject: [PATCH 06/48] Prepare 3.11.2 release

---
 RELEASE-NOTES.md  | 8 ++++----
 pymc3/__init__.py | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index fbb68dada1..46e334f60c 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,8 +1,6 @@
 # Release Notes
 
-## PyMC3 3.11.2 (TBD)
-### Breaking Changes
-+ ...
+## PyMC3 3.11.2 (14 March 2021)
 
 ### New Features
 + `pm.math.cartesian` can now handle inputs that are themselves >1D (see [#4482](https://github.com/pymc-devs/pymc3/pull/4482)).
@@ -17,9 +15,11 @@
   + `pm.pairplot` → `pm.plot_pair`
 
 ### Maintenance
-- ⚠ Our memoization mechanism wasn't robust against hash collisions (#4506), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util` (see #4525).
+- ⚠ Our memoization mechanism wasn't robust against hash collisions ([#4506](https://github.com/pymc-devs/pymc3/issues/4506)), sometimes resulting in incorrect values in, for example, posterior predictives. The `pymc3.memoize` module was removed and replaced with `cachetools`.  The `hashable` function and `WithMemoization` class were moved to `pymc3.util` (see [#4525](https://github.com/pymc-devs/pymc3/pull/4525)).
 - `pm.make_shared_replacements` now retains broadcasting information which fixes issues with Metropolis samplers (see [#4492](https://github.com/pymc-devs/pymc3/pull/4492)).
 
+**Release manager** for 3.11.2: Michael Osthege ([@michaelosthege](https://github.com/michaelosthege))
+
 ## PyMC3 3.11.1 (12 February 2021)
 
 ### New Features
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 670f40341a..f90b9f55bc 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -13,7 +13,7 @@
 #   limitations under the License.
 
 # pylint: disable=wildcard-import
-__version__ = "3.11.1"
+__version__ = "3.11.2"
 
 import logging
 import multiprocessing as mp

From 25e9476848eae5a2526594567bd63c5998e8530e Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sat, 20 Mar 2021 11:52:00 +0100
Subject: [PATCH 07/48] Deal with deprecation warning (#4547)

* Make backend compatibility check more robust

And deal with deprecation warning (closes #4546).

* Remove undefined plot_posterior from __all__ because it's already in az.plots.__all__

Pylint complained about it.
---
 pymc3/__init__.py        | 32 ++++++++++++++++++++++++--------
 pymc3/plots/__init__.py  |  1 -
 pymc3/tests/test_util.py | 22 ++++++++++++++++++++++
 requirements.txt         |  2 +-
 4 files changed, 47 insertions(+), 10 deletions(-)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index f90b9f55bc..79c52c9679 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -31,14 +31,29 @@
         _log.addHandler(handler)
 
 
-if not semver.match(theano.__version__, ">=1.1.2"):
-    print(
-        "!" * 60
-        + f"\nThe installed Theano(-PyMC) version ({theano.__version__}) does not match the PyMC3 requirements."
-        + "\nFor PyMC3 to work, Theano must be uninstalled and replaced with Theano-PyMC."
-        + "\nSee https://github.com/pymc-devs/pymc3/wiki for installation instructions.\n"
-        + "!" * 60
-    )
+def _check_backend_version():
+    backend_paths = theano.__spec__.submodule_search_locations
+    try:
+        backend_version = theano.__version__
+    except:
+        print(
+            "!" * 60
+            + f"\nThe imported Theano(-PyMC) module is broken."
+            + f"\nIt was imported from {backend_paths}"
+            + "\nTry to uninstall/reinstall it after closing all active sessions/notebooks."
+            + "\nAlso see https://github.com/pymc-devs/pymc3/wiki for installation instructions.\n"
+            + "!" * 60
+        )
+        return
+    if not semver.VersionInfo.parse(backend_version).match(">=1.1.2"):
+        print(
+            "!" * 60
+            + f"\nThe installed Theano(-PyMC) version ({theano.__version__}) does not match the PyMC3 requirements."
+            + f"\nIt was imported from {backend_paths}"
+            + "\nFor PyMC3 to work, a compatible Theano-PyMC backend version must be installed."
+            + "\nSee https://github.com/pymc-devs/pymc3/wiki for installation instructions.\n"
+            + "!" * 60
+        )
 
 
 def __set_compiler_flags():
@@ -47,6 +62,7 @@ def __set_compiler_flags():
     theano.config.gcc__cxxflags = f"{current} -Wno-c++11-narrowing"
 
 
+_check_backend_version()
 __set_compiler_flags()
 
 from pymc3 import gp, ode, sampling
diff --git a/pymc3/plots/__init__.py b/pymc3/plots/__init__.py
index 03c7715c24..3a5f8bed11 100644
--- a/pymc3/plots/__init__.py
+++ b/pymc3/plots/__init__.py
@@ -117,7 +117,6 @@ def compareplot(*args, **kwargs):
     "compareplot",
     "forestplot",
     "kdeplot",
-    "plot_posterior",
     "traceplot",
     "energyplot",
     "densityplot",
diff --git a/pymc3/tests/test_util.py b/pymc3/tests/test_util.py
index 05b6bdf52d..f2f3693a35 100644
--- a/pymc3/tests/test_util.py
+++ b/pymc3/tests/test_util.py
@@ -14,6 +14,7 @@
 
 import numpy as np
 import pytest
+import theano
 
 from cachetools import cached
 from numpy.testing import assert_almost_equal
@@ -25,6 +26,27 @@
 from pymc3.util import hash_key, hashable, locally_cachedmethod
 
 
+class TestBackendVersionCheck:
+    def test_warn_on_incompatible_backend(self, capsys):
+        assert not "!!!!!" in capsys.readouterr().out
+        pm._check_backend_version()
+        assert not "!!!!!" in capsys.readouterr().out
+
+        # Mock an incorrect backend version
+        original = theano.__version__
+
+        theano.__version__ = "1.1.0"
+        pm._check_backend_version()
+        assert "does not match" in capsys.readouterr().out
+
+        del theano.__version__
+        pm._check_backend_version()
+        assert "is broken" in capsys.readouterr().out
+
+        theano.__version__ = original
+        pass
+
+
 class TestTransformName:
     cases = [("var", "var_test__"), ("var_test_", "var_test__test__")]
     transform_name = "test"
diff --git a/requirements.txt b/requirements.txt
index 79eb591a84..d0900a22d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,6 @@ numpy>=1.15.0
 pandas>=0.24.0
 patsy>=0.5.1
 scipy>=1.2.0
-semver
+semver>=2.13.0
 theano-pymc==1.1.2
 typing-extensions>=3.7.4

From 94b0c2ef2bf3e938281b869d645df3b14fe198d4 Mon Sep 17 00:00:00 2001
From: Michael Osthege <m.osthege@fz-juelich.de>
Date: Wed, 31 Mar 2021 10:39:03 +0200
Subject: [PATCH 08/48] Hotfix theano.printing upon import

A workaround for https://github.com/pymc-devs/aesara/issues/309
---
 pymc3/__init__.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 79c52c9679..671fcbe048 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -62,8 +62,22 @@ def __set_compiler_flags():
     theano.config.gcc__cxxflags = f"{current} -Wno-c++11-narrowing"
 
 
+def _hotfix_theano_printing():
+    """ This is a workaround for https://github.com/pymc-devs/aesara/issues/309 """
+    try:
+        import pydot
+        import theano.printing
+
+        if theano.printing.Node != pydot.Node:
+            theano.printing.Node = pydot.Node
+    except ImportError:
+        # pydot is not installed
+        pass
+
+
 _check_backend_version()
 __set_compiler_flags()
+_hotfix_theano_printing()
 
 from pymc3 import gp, ode, sampling
 from pymc3.backends import load_trace, save_trace

From 106cd8fc79bab3dd2cca4810811a651fd8d74dbb Mon Sep 17 00:00:00 2001
From: Michael Osthege <m.osthege@fz-juelich.de>
Date: Wed, 31 Mar 2021 10:47:13 +0200
Subject: [PATCH 09/48] Add section for 3.11.3 patch release

---
 RELEASE-NOTES.md | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 46e334f60c..43c52c62b2 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,5 +1,10 @@
 # Release Notes
 
+## PyMC3 3.11.3 (TBD)
+### Maintenance
++ A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
++ `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
+
 ## PyMC3 3.11.2 (14 March 2021)
 
 ### New Features

From 34476191145d7234e399cd480da1d16480be28fb Mon Sep 17 00:00:00 2001
From: Maxim Kochurov <maxim.v.kochurov@gmail.com>
Date: Thu, 22 Apr 2021 09:55:10 +0300
Subject: [PATCH 10/48] Add a type guard for `intX` (#4569)

* add type guard for inX

* fix test for pandas

* fix posterior test, ints passed for float data

* release notes

* Update RELEASE-NOTES.md

* Update RELEASE-NOTES.md
---
 RELEASE-NOTES.md                   | 1 +
 pymc3/tests/test_data_container.py | 2 +-
 pymc3/tests/test_model_helpers.py  | 5 +++--
 pymc3/theanof.py                   | 3 +++
 4 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 43c52c62b2..b6f270b00a 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -24,6 +24,7 @@
 - `pm.make_shared_replacements` now retains broadcasting information which fixes issues with Metropolis samplers (see [#4492](https://github.com/pymc-devs/pymc3/pull/4492)).
 
 **Release manager** for 3.11.2: Michael Osthege ([@michaelosthege](https://github.com/michaelosthege))
+- `pm.intX` no longer downcasts integers unnecessarily (see [#4569](https://github.com/pymc-devs/pymc3/pull/4569))
 
 ## PyMC3 3.11.1 (12 February 2021)
 
diff --git a/pymc3/tests/test_data_container.py b/pymc3/tests/test_data_container.py
index 966ce47cd6..e1914f1e57 100644
--- a/pymc3/tests/test_data_container.py
+++ b/pymc3/tests/test_data_container.py
@@ -78,7 +78,7 @@ def test_sample_posterior_predictive_after_set_data(self):
             trace = pm.sample(1000, tune=1000, chains=1)
         # Predict on new data.
         with model:
-            x_test = [5, 6, 9]
+            x_test = [5.0, 6.0, 9.0]
             pm.set_data(new_data={"x": x_test})
             y_test = pm.sample_posterior_predictive(trace)
             y_test1 = pm.fast_sample_posterior_predictive(trace)
diff --git a/pymc3/tests/test_model_helpers.py b/pymc3/tests/test_model_helpers.py
index 20745febad..d4198270a4 100644
--- a/pymc3/tests/test_model_helpers.py
+++ b/pymc3/tests/test_model_helpers.py
@@ -82,7 +82,7 @@ def test_pandas_to_array(self, input_dtype):
         assert isinstance(theano_output, theano.graph.basic.Variable)
         npt.assert_allclose(theano_output.eval(), theano_graph_input.eval())
         intX = pm.theanof._conversion_map[theano.config.floatX]
-        if dense_input.dtype == intX or dense_input.dtype == theano.config.floatX:
+        if "int" in str(dense_input.dtype) or dense_input.dtype == theano.config.floatX:
             assert theano_output.owner is None  # func should not have added new nodes
             assert theano_output.name == input_name
         else:
@@ -92,7 +92,8 @@ def test_pandas_to_array(self, input_dtype):
         if "float" in input_dtype:
             assert theano_output.dtype == theano.config.floatX
         else:
-            assert theano_output.dtype == intX
+            # only cast floats, leave ints as is
+            assert theano_output.dtype == input_dtype
 
         # Check function behavior with generator data
         generator_output = func(square_generator)
diff --git a/pymc3/theanof.py b/pymc3/theanof.py
index 50ab8afdaf..7f22ce6f38 100644
--- a/pymc3/theanof.py
+++ b/pymc3/theanof.py
@@ -93,6 +93,9 @@ def intX(X):
     """
     Convert a theano tensor or numpy array to theano.tensor.int32 type.
     """
+    # check value is already int, do nothing in this case
+    if (hasattr(X, "dtype") and "int" in str(X.dtype)) or isinstance(X, int):
+        return X
     intX = _conversion_map[theano.config.floatX]
     try:
         return X.astype(intX)

From 842a69666744399f828e3429de4ea3035fcbc7a8 Mon Sep 17 00:00:00 2001
From: Osvaldo Martin <aloctavodia@gmail.com>
Date: Wed, 5 May 2021 08:15:49 +0300
Subject: [PATCH 11/48] BART with non-gaussian likelihoods (#4675)

* allow unbounded likelihoods, add inv_link and small refactor

* add test

* fix test

* set jitter optional, update realease notes
---
 RELEASE-NOTES.md             |   3 +
 pymc3/distributions/bart.py  |  86 ++++++++++++++++++++------
 pymc3/distributions/tree.py  |  10 ++--
 pymc3/step_methods/pgbart.py |  38 ++++++------
 pymc3/tests/test_bart.py     | 113 +++++++++++++++++++++++++++++++++++
 5 files changed, 208 insertions(+), 42 deletions(-)
 create mode 100644 pymc3/tests/test_bart.py

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index b6f270b00a..6e61222a7c 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -5,6 +5,9 @@
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
 
+### New Features
++ BART with non-gaussian likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675)).
+
 ## PyMC3 3.11.2 (14 March 2021)
 
 ### New Features
diff --git a/pymc3/distributions/bart.py b/pymc3/distributions/bart.py
index 4914844555..02f3fdb4cf 100644
--- a/pymc3/distributions/bart.py
+++ b/pymc3/distributions/bart.py
@@ -23,11 +23,24 @@
 
 
 class BaseBART(NoDistribution):
-    def __init__(self, X, Y, m=200, alpha=0.25, split_prior=None, *args, **kwargs):
-
+    def __init__(
+        self,
+        X,
+        Y,
+        m=200,
+        alpha=0.25,
+        split_prior=None,
+        scale=None,
+        inv_link=None,
+        jitter=False,
+        *args,
+        **kwargs,
+    ):
+
+        self.jitter = jitter
         self.X, self.Y, self.missing_data = self.preprocess_XY(X, Y)
 
-        super().__init__(shape=X.shape[0], dtype="float64", testval=0, *args, **kwargs)
+        super().__init__(shape=X.shape[0], dtype="float64", testval=self.Y.mean(), *args, **kwargs)
 
         if self.X.ndim != 2:
             raise ValueError("The design matrix X must have two dimensions")
@@ -48,13 +61,24 @@ def __init__(self, X, Y, m=200, alpha=0.25, split_prior=None, *args, **kwargs):
                 "The value for the alpha parameter for the tree structure "
                 "must be in the interval (0, 1)"
             )
+        self.m = m
+        self.alpha = alpha
+        self.y_std = Y.std()
+
+        if scale is None:
+            self.leaf_scale = NormalSampler(sigma=None)
+        elif isinstance(scale, (int, float)):
+            self.leaf_scale = NormalSampler(sigma=Y.std() / self.m ** scale)
+
+        if inv_link is None:
+            self.inv_link = lambda x: x
+        else:
+            self.inv_link = inv_link
 
         self.num_observations = X.shape[0]
         self.num_variates = X.shape[1]
         self.available_predictors = list(range(self.num_variates))
         self.ssv = SampleSplittingVariable(split_prior, self.num_variates)
-        self.m = m
-        self.alpha = alpha
         self.trees = self.init_list_of_trees()
         self.all_trees = []
         self.mean = fast_mean()
@@ -66,7 +90,9 @@ def preprocess_XY(self, X, Y):
         if isinstance(X, (Series, DataFrame)):
             X = X.to_numpy()
         missing_data = np.any(np.isnan(X))
-        X = np.random.normal(X, np.std(X, 0) / 100)
+        if self.jitter:
+            X = np.random.normal(X, np.nanstd(X, 0) / 100000)
+        Y = Y.astype(float)
         return X, Y, missing_data
 
     def init_list_of_trees(self):
@@ -155,18 +181,14 @@ def get_new_idx_data_points(self, current_split_node, idx_data_points):
 
     def get_residuals(self):
         """Compute the residuals."""
-        R_j = self.Y - self.sum_trees_output
-        return R_j
+        R_j = self.Y - self.inv_link(self.sum_trees_output)
 
-    def get_residuals_loo(self, tree):
-        """Compute the residuals without leaving the passed tree out."""
-        R_j = self.Y - (self.sum_trees_output - tree.predict_output(self.num_observations))
         return R_j
 
     def draw_leaf_value(self, idx_data_points):
-        """ Draw the residual mean."""
+        """Draw the residual mean."""
         R_j = self.get_residuals()[idx_data_points]
-        draw = self.mean(R_j)
+        draw = self.mean(R_j) + self.leaf_scale.random()
         return draw
 
     def predict(self, X_new):
@@ -174,13 +196,12 @@ def predict(self, X_new):
         trees = self.all_trees
         num_observations = X_new.shape[0]
         pred = np.zeros((len(trees), num_observations))
-        np.random.randint(len(trees))
         for draw, trees_to_sum in enumerate(trees):
             new_Y = np.zeros(num_observations)
             for tree in trees_to_sum:
                 new_Y += [tree.predict_out_of_sample(x) for x in X_new]
             pred[draw] = new_Y
-        return pred
+        return self.inv_link(pred)
 
 
 def compute_prior_probability(alpha):
@@ -257,6 +278,24 @@ def rvs(self):
                     return i
 
 
+class NormalSampler:
+    def __init__(self, sigma):
+        self.size = 5000
+        self.cache = []
+        self.sigma = sigma
+
+    def random(self):
+        if self.sigma is None:
+            return 0
+        else:
+            if not self.cache:
+                self.update()
+        return self.cache.pop()
+
+    def update(self):
+        self.cache = np.random.normal(loc=0.0, scale=self.sigma, size=self.size).tolist()
+
+
 class BART(BaseBART):
     """
     BART distribution.
@@ -278,10 +317,23 @@ class BART(BaseBART):
         Each element of split_prior should be in the [0, 1] interval and the elements should sum
         to 1. Otherwise they will be normalized.
         Defaults to None, all variable have the same a prior probability
+    scale : float
+        Controls the variance of the proposed leaf value. The leaf values are computed as a
+        Gaussian with mean equal to the conditional residual mean and variance proportional to
+        the variance of the response variable, and inversely proportional to the number of trees
+        and the scale parameter. Defaults to None, i.e the variance is 0.
+    inv_link : numpy function
+        Inverse link function defaults to None, i.e. the identity function.
+    jitter : bool
+        Whether to jitter the X values or not. Defaults to False. When values of X are repeated,
+        jittering X has the effect of increasing the number of effective spliting variables,
+        otherwise it does not have any effect.
     """
 
-    def __init__(self, X, Y, m=200, alpha=0.25, split_prior=None):
-        super().__init__(X, Y, m, alpha, split_prior)
+    def __init__(
+        self, X, Y, m=200, alpha=0.25, split_prior=None, scale=None, inv_link=None, jitter=False
+    ):
+        super().__init__(X, Y, m, alpha, split_prior, scale, inv_link)
 
     def _str_repr(self, name=None, dist=None, formatting="plain"):
         if dist is None:
diff --git a/pymc3/distributions/tree.py b/pymc3/distributions/tree.py
index 8e84bd9a7c..12f7dc6285 100644
--- a/pymc3/distributions/tree.py
+++ b/pymc3/distributions/tree.py
@@ -45,12 +45,13 @@ class Tree:
     tree_id : int, optional
     """
 
-    def __init__(self, tree_id=0):
+    def __init__(self, tree_id=0, num_observations=0):
         self.tree_structure = {}
         self.num_nodes = 0
         self.idx_leaf_nodes = []
         self.idx_prunable_split_nodes = []
         self.tree_id = tree_id
+        self.num_observations = num_observations
 
     def __getitem__(self, index):
         return self.get_node(index)
@@ -77,11 +78,12 @@ def delete_node(self, index):
         del self.tree_structure[index]
         self.num_nodes -= 1
 
-    def predict_output(self, num_observations):
-        output = np.zeros(num_observations)
+    def predict_output(self):
+        output = np.zeros(self.num_observations)
         for node_index in self.idx_leaf_nodes:
             current_node = self.get_node(node_index)
             output[current_node.idx_data_points] = current_node.value
+
         return output
 
     def predict_out_of_sample(self, x):
@@ -163,7 +165,7 @@ def init_tree(tree_id, leaf_node_value, idx_data_points):
         -------
 
         """
-        new_tree = Tree(tree_id)
+        new_tree = Tree(tree_id, len(idx_data_points))
         new_tree[0] = LeafNode(index=0, value=leaf_node_value, idx_data_points=idx_data_points)
         return new_tree
 
diff --git a/pymc3/step_methods/pgbart.py b/pymc3/step_methods/pgbart.py
index c3bac3ade9..cfd38c62c8 100644
--- a/pymc3/step_methods/pgbart.py
+++ b/pymc3/step_methods/pgbart.py
@@ -86,12 +86,13 @@ def __init__(self, vars=None, num_particles=10, max_stages=5000, chunk="auto", m
 
     def astep(self, _):
         bart = self.bart
+        inv_link = bart.inv_link
         num_observations = bart.num_observations
         variable_inclusion = np.zeros(bart.num_variates, dtype="int")
 
         # For the tunning phase we restrict max_stages to a low number, otherwise it is almost sure
         # we will reach max_stages given that our first set of m trees is not good at all.
-        # Can set max_stages as a function of the number of variables/dimensions?
+        # Can set max_stages as a function of the number of variables/dimensions? XXX
         if self.tune:
             max_stages = 5
         else:
@@ -105,10 +106,11 @@ def astep(self, _):
                 break
             self.idx += 1
             tree = bart.trees[idx]
-            R_j = bart.get_residuals_loo(tree)
+            old_prediction = tree.predict_output()
+            bart.sum_trees_output -= old_prediction
             # Generate an initial set of SMC particles
             # at the end of the algorithm we return one of these particles as the new tree
-            particles = self.init_particles(tree.tree_id, R_j, num_observations)
+            particles = self.init_particles(tree.tree_id, num_observations, inv_link)
 
             for t in range(1, max_stages):
                 # Get old particle at stage t
@@ -119,13 +121,12 @@ def astep(self, _):
                 # Update weights. Since the prior is used as the proposal,the weights
                 # are updated additively as the ratio of the new and old log_likelihoods
                 for p_idx, p in enumerate(particles):
-                    new_likelihood = self.likelihood_logp(p.tree.predict_output(num_observations))
+                    new_likelihood = self.likelihood_logp(inv_link(p.tree.predict_output()))
                     p.log_weight += new_likelihood - p.old_likelihood_logp
                     p.old_likelihood_logp = new_likelihood
 
                 # Normalize weights
                 W, normalized_weights = self.normalize(particles)
-
                 # Resample all but first particle
                 re_n_w = normalized_weights[1:] / normalized_weights[1:].sum()
                 new_indices = np.random.choice(self.indices, size=len(self.indices), p=re_n_w)
@@ -148,8 +149,8 @@ def astep(self, _):
             new_tree = np.random.choice(particles, p=normalized_weights)
             self.old_trees_particles_list[tree.tree_id] = new_tree
             bart.trees[idx] = new_tree.tree
-            new_prediction = new_tree.tree.predict_output(num_observations)
-            bart.sum_trees_output = bart.Y - R_j + new_prediction
+            new_prediction = new_tree.tree.predict_output()
+            bart.sum_trees_output += new_prediction
 
             if not self.tune:
                 self.iter += 1
@@ -161,8 +162,7 @@ def astep(self, _):
                     variable_inclusion[index] += 1
 
         stats = {"variable_inclusion": variable_inclusion}
-
-        return bart.sum_trees_output, [stats]
+        return inv_link(bart.sum_trees_output), [stats]
 
     @staticmethod
     def competence(var, has_grad):
@@ -194,31 +194,26 @@ def get_old_tree_particle(self, tree_id, t):
         old_tree_particle.set_particle_to_step(t)
         return old_tree_particle
 
-    def init_particles(self, tree_id, R_j, num_observations):
+    def init_particles(self, tree_id, num_observations, inv_link):
         """
         Initialize particles
         """
         # The first particle is from the tree we are trying to replace
         prev_tree = self.get_old_tree_particle(tree_id, 0)
-        likelihood = self.likelihood_logp(prev_tree.tree.predict_output(num_observations))
+        likelihood = self.likelihood_logp(inv_link(prev_tree.tree.predict_output()))
         prev_tree.old_likelihood_logp = likelihood
         prev_tree.log_weight = likelihood - self.log_num_particles
         particles = [prev_tree]
 
         # The rest of the particles are identically initialized
-        initial_value_leaf_nodes = R_j.mean()
         initial_idx_data_points_leaf_nodes = np.arange(num_observations, dtype="int32")
         new_tree = Tree.init_tree(
             tree_id=tree_id,
-            leaf_node_value=initial_value_leaf_nodes,
+            leaf_node_value=0,
             idx_data_points=initial_idx_data_points_leaf_nodes,
         )
-        likelihood_logp = self.likelihood_logp(new_tree.predict_output(num_observations))
-        log_weight = likelihood_logp - self.log_num_particles
         for i in range(1, self.num_particles):
-            particles.append(
-                ParticleTree(new_tree, self.bart.prior_prob_leaf_node, log_weight, likelihood_logp)
-            )
+            particles.append(ParticleTree(new_tree, self.bart.prior_prob_leaf_node, 0, 0))
 
         return np.array(particles)
 
@@ -237,10 +232,10 @@ class ParticleTree:
 
     def __init__(self, tree, prior_prob_leaf_node, log_weight=0, likelihood=0):
         self.tree = tree.copy()  # keeps the tree that we care at the moment
-        self.expansion_nodes = tree.idx_leaf_nodes.copy()  # This should be the array [0]
+        self.expansion_nodes = [0]
         self.tree_history = [self.tree]
         self.expansion_nodes_history = [self.expansion_nodes]
-        self.log_weight = 0
+        self.log_weight = log_weight
         self.prior_prob_leaf_node = prior_prob_leaf_node
         self.old_likelihood_logp = likelihood
         self.used_variates = []
@@ -253,7 +248,8 @@ def sample_tree_sequential(self, bart):
 
             if prob_leaf < np.random.random():
                 grow_successful, index_selected_predictor = bart.grow_tree(
-                    self.tree, index_leaf_node
+                    self.tree,
+                    index_leaf_node,
                 )
                 if grow_successful:
                     # Add new leaf nodes indexes
diff --git a/pymc3/tests/test_bart.py b/pymc3/tests/test_bart.py
new file mode 100644
index 0000000000..d1bb958007
--- /dev/null
+++ b/pymc3/tests/test_bart.py
@@ -0,0 +1,113 @@
+import numpy as np
+
+from scipy.special import expit
+
+import pymc3 as pm
+
+
+def test_split_node():
+    split_node = pm.distributions.bart.SplitNode(index=5, idx_split_variable=2, split_value=3.0)
+    assert split_node.index == 5
+    assert split_node.idx_split_variable == 2
+    assert split_node.split_value == 3.0
+    assert split_node.depth == 2
+    assert split_node.get_idx_parent_node() == 2
+    assert split_node.get_idx_left_child() == 11
+    assert split_node.get_idx_right_child() == 12
+
+
+def test_leaf_node():
+    leaf_node = pm.distributions.bart.LeafNode(index=5, value=3.14, idx_data_points=[1, 2, 3])
+    assert leaf_node.index == 5
+    assert np.array_equal(leaf_node.idx_data_points, [1, 2, 3])
+    assert leaf_node.value == 3.14
+    assert leaf_node.get_idx_parent_node() == 2
+    assert leaf_node.get_idx_left_child() == 11
+    assert leaf_node.get_idx_right_child() == 12
+
+
+def test_bart():
+    X = np.random.sample(size=(100, 4))
+    Y = np.random.sample(size=(100))
+
+    with pm.Model():
+        b = pm.BART("b", X=X, Y=Y)
+
+    bart = b.distribution
+    assert bart.num_observations == 100
+    assert bart.num_variates == 4
+    assert len(bart.trees) == bart.m
+
+    assert np.array_equal(bart.trees[0][0].idx_data_points, np.array(range(100), dtype="int32"))
+
+
+def test_available_splitting_rules():
+    X = np.array([[1.0, 2.0, 3.0, np.nan], [2.0, 2.0, 3.0, 99.9], [3.0, 4.0, 3.0, -3.3]])
+    Y = np.random.sample(size=(3))
+    with pm.Model():
+        b = pm.BART("b", X=X, Y=Y)
+
+    bart = b.distribution
+
+    idx_split_variable = 0
+    idx_data_points = np.array(range(bart.num_observations), dtype="int32")
+    available_splitting_rules = bart.get_available_splitting_rules(
+        idx_data_points, idx_split_variable
+    )
+    assert available_splitting_rules.size == 2
+    np.testing.assert_almost_equal(available_splitting_rules, np.array([1.0, 2.0]), 1)
+
+    idx_split_variable = 1
+    idx_data_points = np.array(range(bart.num_observations), dtype="int32")
+    available_splitting_rules = bart.get_available_splitting_rules(
+        idx_data_points, idx_split_variable
+    )
+    assert available_splitting_rules.size == 1
+    np.testing.assert_almost_equal(
+        available_splitting_rules,
+        np.array(
+            [
+                2.0,
+            ]
+        ),
+        1,
+    )
+
+    idx_split_variable = 2
+    idx_data_points = np.array(range(bart.num_observations), dtype="int32")
+    available_splitting_rules = bart.get_available_splitting_rules(
+        idx_data_points, idx_split_variable
+    )
+    assert available_splitting_rules.size == 0
+    np.testing.assert_almost_equal(available_splitting_rules, np.array([]))
+
+    idx_split_variable = 3
+    idx_data_points = np.array(range(bart.num_observations), dtype="int32")
+    available_splitting_rules = bart.get_available_splitting_rules(
+        idx_data_points, idx_split_variable
+    )
+    assert available_splitting_rules.size == 1
+    np.testing.assert_almost_equal(available_splitting_rules, np.array([-3.3]), 1)
+
+
+def test_model():
+    np.random.seed(212480)
+    X = np.linspace(7, 15, 100)
+    Y = np.sin(np.random.normal(X, 0.2)) + 3
+    X = X[:, None]
+
+    with pm.Model() as model:
+        sigma = pm.HalfNormal("sigma", 1)
+        mu = pm.BART("mu", X, Y, m=50)
+        y = pm.Normal("y", mu, sigma, observed=Y)
+        trace = pm.sample(1000, random_seed=212480)
+
+    np.testing.assert_allclose(trace[mu].mean(0), Y, 0.5)
+
+    Y = np.repeat([0, 1], 50)
+    with pm.Model() as model:
+        mu = pm.BART("mu", X, Y, m=50, inv_link=expit, scale=0.25)
+        y = pm.Bernoulli("y", mu, observed=Y)
+        trace = pm.sample(1000, random_seed=212480)
+
+    np.testing.assert_allclose(trace[mu].mean(0), Y, atol=0.5)

From 0ff9e9b0f244c118b26d762239bd3feb0243c6dc Mon Sep 17 00:00:00 2001
From: Osvaldo Martin <aloctavodia@gmail.com>
Date: Wed, 5 May 2021 10:40:54 +0300
Subject: [PATCH 12/48] Fix bug in pseudo_likelihood computation (#4672)

* fix bug in pseudo_likelihood computation

* add test

* update release notes
---
 RELEASE-NOTES.md        |  1 +
 pymc3/smc/smc.py        |  4 +++-
 pymc3/tests/test_smc.py | 13 +++++++++++++
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 6e61222a7c..73aae4122f 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -4,6 +4,7 @@
 ### Maintenance
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
++ Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
 
 ### New Features
 + BART with non-gaussian likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675)).
diff --git a/pymc3/smc/smc.py b/pymc3/smc/smc.py
index 2e7e369ad3..3512072081 100644
--- a/pymc3/smc/smc.py
+++ b/pymc3/smc/smc.py
@@ -190,8 +190,10 @@ def resample(self):
         self.prior_logp = self.prior_logp[resampling_indexes]
         self.likelihood_logp = self.likelihood_logp[resampling_indexes]
         self.posterior_logp = self.prior_logp + self.likelihood_logp * self.beta
-        if self.save_sim_data:
+        if self.kernel == "abc" and self.save_sim_data:
             self.sim_data = self.sim_data[resampling_indexes]
+        if self.kernel == "abc" and self.save_log_pseudolikelihood:
+            self.log_pseudolikelihood = self.log_pseudolikelihood[resampling_indexes]
 
     def update_proposal(self):
         """Update proposal based on the covariance matrix from tempered posterior."""
diff --git a/pymc3/tests/test_smc.py b/pymc3/tests/test_smc.py
index 695ea461f7..0e94424a06 100644
--- a/pymc3/tests/test_smc.py
+++ b/pymc3/tests/test_smc.py
@@ -141,6 +141,13 @@ def abs_diff(eps, obs_data, sim_data):
                 "s", normal_sim, params=(a, b), sum_stat="sort", epsilon=1, observed=self.data
             )
 
+        with pm.Model() as self.SMABC_log_pseudolikelihood:
+            a = pm.Normal("a", mu=0, sigma=1)
+            b = pm.HalfNormal("b", sigma=10)
+            s = pm.Simulator(
+                "s", normal_sim, params=(a, b), sum_stat="mean", epsilon=0.2, observed=self.data
+            )
+
     def test_one_gaussian(self):
         with self.SMABC_test:
             trace = pm.sample_smc(draws=1000, kernel="ABC")
@@ -210,3 +217,9 @@ def normal_sim(a, b):
             )
             with pytest.raises(NotImplementedError, match="named models"):
                 pm.sample_smc(draws=10, kernel="ABC")
+
+    def test_log_pseudolikelihood(self):
+        with self.SMABC_log_pseudolikelihood:
+            trace = pm.sample_smc(draws=1000, chains=2, kernel="ABC")
+            assert trace.report.log_pseudolikelihood[0].var() < 1
+            assert trace.report.log_pseudolikelihood[1].var() < 1

From fc31e00668f2874c1cc0d91d22e98aa5424bce23 Mon Sep 17 00:00:00 2001
From: Osvaldo Martin <aloctavodia@gmail.com>
Date: Mon, 10 May 2021 13:15:16 +0300
Subject: [PATCH 13/48] glm module deprecation warning (#4677)

* add warning message about glm module deprecation

* update deprecation message with version
---
 pymc3/glm/linear.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/pymc3/glm/linear.py b/pymc3/glm/linear.py
index 81c916c118..4e3112ae61 100644
--- a/pymc3/glm/linear.py
+++ b/pymc3/glm/linear.py
@@ -11,6 +11,7 @@
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
+import logging
 
 import numpy as np
 import theano.tensor as tt
@@ -20,6 +21,7 @@
 from pymc3.glm.utils import any_to_tensor_and_labels
 from pymc3.model import Deterministic, Model
 
+_log = logging.getLogger("pymc3")
 __all__ = ["LinearComponent", "GLM"]
 
 
@@ -216,6 +218,10 @@ def from_formula(
             See `patsy.dmatrix` and `patsy.EvalEnvironment` for details.
         Other arguments are documented in the constructor.
         """
+        _log.warning(
+            "The glm module is deprecated and will be removed in version 4.0\nWe recommend to"
+            + " instead use Bambi https://bambinos.github.io/bambi/"
+        )
         import patsy
 
         eval_env = patsy.EvalEnvironment.capture(eval_env, reference=1)

From 8cb87fe44aec3051704a353de9d0d1bc1d8090ff Mon Sep 17 00:00:00 2001
From: Osvaldo Martin <aloctavodia@gmail.com>
Date: Tue, 25 May 2021 08:04:56 +0300
Subject: [PATCH 14/48] Robust generalized bart inference (#4709)

* robust generalized bart inference

* update docstring

* update test

* clarify role of link function

* revert few changes

* raise a ValueError if inv_link string is not valid

* update release notes
---
 RELEASE-NOTES.md             |  2 +-
 pymc3/distributions/bart.py  | 71 ++++++++++++++----------------------
 pymc3/step_methods/pgbart.py | 58 +++++++++++++++--------------
 pymc3/tests/test_bart.py     |  4 +-
 4 files changed, 61 insertions(+), 74 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 73aae4122f..bbc56eed3e 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -7,7 +7,7 @@
 + Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
 
 ### New Features
-+ BART with non-gaussian likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675)).
++ BART with non-gaussian likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675) and [#4709](https://github.com/pymc-devs/pymc3/pull/4709)).
 
 ## PyMC3 3.11.2 (14 March 2021)
 
diff --git a/pymc3/distributions/bart.py b/pymc3/distributions/bart.py
index 02f3fdb4cf..0c3007557f 100644
--- a/pymc3/distributions/bart.py
+++ b/pymc3/distributions/bart.py
@@ -15,6 +15,7 @@
 import numpy as np
 
 from pandas import DataFrame, Series
+from scipy.special import expit
 
 from pymc3.distributions.distribution import NoDistribution
 from pymc3.distributions.tree import LeafNode, SplitNode, Tree
@@ -30,7 +31,6 @@ def __init__(
         m=200,
         alpha=0.25,
         split_prior=None,
-        scale=None,
         inv_link=None,
         jitter=False,
         *args,
@@ -63,22 +63,32 @@ def __init__(
             )
         self.m = m
         self.alpha = alpha
-        self.y_std = Y.std()
-
-        if scale is None:
-            self.leaf_scale = NormalSampler(sigma=None)
-        elif isinstance(scale, (int, float)):
-            self.leaf_scale = NormalSampler(sigma=Y.std() / self.m ** scale)
 
         if inv_link is None:
-            self.inv_link = lambda x: x
+            self.inv_link = self.link = lambda x: x
+        elif isinstance(inv_link, str):
+            # The link function is just a rough approximation in order to allow the PGBART sampler
+            # to propose reasonable values for the leaf nodes.
+            if inv_link == "logistic":
+                self.inv_link = expit
+                self.link = lambda x: (x - 0.5) * 10
+            elif inv_link == "exp":
+                self.inv_link = np.exp
+                self.link = np.log
+                self.Y[self.Y == 0] += 0.0001
+            else:
+                raise ValueError("Accepted strings are 'logistic' or 'exp'")
         else:
-            self.inv_link = inv_link
+            self.inv_link, self.link = inv_link
+
+        self.init_mean = self.link(self.Y.mean())
+        self.Y_un = self.link(self.Y)
 
         self.num_observations = X.shape[0]
         self.num_variates = X.shape[1]
         self.available_predictors = list(range(self.num_variates))
         self.ssv = SampleSplittingVariable(split_prior, self.num_variates)
+        self.initial_value_leaf_nodes = self.init_mean / self.m
         self.trees = self.init_list_of_trees()
         self.all_trees = []
         self.mean = fast_mean()
@@ -96,7 +106,7 @@ def preprocess_XY(self, X, Y):
         return X, Y, missing_data
 
     def init_list_of_trees(self):
-        initial_value_leaf_nodes = self.Y.mean() / self.m
+        initial_value_leaf_nodes = self.initial_value_leaf_nodes
         initial_idx_data_points_leaf_nodes = np.array(range(self.num_observations), dtype="int32")
         list_of_trees = []
         for i in range(self.m):
@@ -110,7 +120,7 @@ def init_list_of_trees(self):
         # bartMachine: A Powerful Tool for Machine Learning in R. ArXiv e-prints, 2013
         # The sum_trees_output will contain the sum of the predicted output for all trees.
         # When R_j is needed we subtract the current predicted output for tree T_j.
-        self.sum_trees_output = np.full_like(self.Y, self.Y.mean())
+        self.sum_trees_output = np.full_like(self.Y, self.init_mean)
 
         return list_of_trees
 
@@ -181,14 +191,13 @@ def get_new_idx_data_points(self, current_split_node, idx_data_points):
 
     def get_residuals(self):
         """Compute the residuals."""
-        R_j = self.Y - self.inv_link(self.sum_trees_output)
-
+        R_j = self.Y_un - self.sum_trees_output
         return R_j
 
     def draw_leaf_value(self, idx_data_points):
         """Draw the residual mean."""
         R_j = self.get_residuals()[idx_data_points]
-        draw = self.mean(R_j) + self.leaf_scale.random()
+        draw = self.mean(R_j)
         return draw
 
     def predict(self, X_new):
@@ -278,24 +287,6 @@ def rvs(self):
                     return i
 
 
-class NormalSampler:
-    def __init__(self, sigma):
-        self.size = 5000
-        self.cache = []
-        self.sigma = sigma
-
-    def random(self):
-        if self.sigma is None:
-            return 0
-        else:
-            if not self.cache:
-                self.update()
-        return self.cache.pop()
-
-    def update(self):
-        self.cache = np.random.normal(loc=0.0, scale=self.sigma, size=self.size).tolist()
-
-
 class BART(BaseBART):
     """
     BART distribution.
@@ -317,23 +308,17 @@ class BART(BaseBART):
         Each element of split_prior should be in the [0, 1] interval and the elements should sum
         to 1. Otherwise they will be normalized.
         Defaults to None, all variable have the same a prior probability
-    scale : float
-        Controls the variance of the proposed leaf value. The leaf values are computed as a
-        Gaussian with mean equal to the conditional residual mean and variance proportional to
-        the variance of the response variable, and inversely proportional to the number of trees
-        and the scale parameter. Defaults to None, i.e the variance is 0.
-    inv_link : numpy function
-        Inverse link function defaults to None, i.e. the identity function.
+    inv_link : str or tuple of functions
+        Inverse link function defaults to None, i.e. the identity function. Accepted strings are
+        ``logistic`` or ``exp``.
     jitter : bool
         Whether to jitter the X values or not. Defaults to False. When values of X are repeated,
         jittering X has the effect of increasing the number of effective spliting variables,
         otherwise it does not have any effect.
     """
 
-    def __init__(
-        self, X, Y, m=200, alpha=0.25, split_prior=None, scale=None, inv_link=None, jitter=False
-    ):
-        super().__init__(X, Y, m, alpha, split_prior, scale, inv_link)
+    def __init__(self, X, Y, m=200, alpha=0.25, split_prior=None, inv_link=None, jitter=False):
+        super().__init__(X, Y, m, alpha, split_prior, inv_link)
 
     def _str_repr(self, name=None, dist=None, formatting="plain"):
         if dist is None:
diff --git a/pymc3/step_methods/pgbart.py b/pymc3/step_methods/pgbart.py
index cfd38c62c8..46f52bbfa5 100644
--- a/pymc3/step_methods/pgbart.py
+++ b/pymc3/step_methods/pgbart.py
@@ -75,29 +75,29 @@ def __init__(self, vars=None, num_particles=10, max_stages=5000, chunk="auto", m
         self.log_num_particles = np.log(num_particles)
         self.indices = list(range(1, num_particles))
         self.max_stages = max_stages
-        self.old_trees_particles_list = []
-        for i in range(self.bart.m):
-            p = ParticleTree(self.bart.trees[i], self.bart.prior_prob_leaf_node)
-            self.old_trees_particles_list.append(p)
 
         shared = make_shared_replacements(vars, model)
         self.likelihood_logp = logp([model.datalogpt], vars, shared)
+        self.init_leaf_nodes = self.bart.initial_value_leaf_nodes
+        self.init_likelihood = self.likelihood_logp(self.bart.inv_link(self.bart.sum_trees_output))
+        self.init_log_weight = self.init_likelihood - self.log_num_particles
+        self.old_trees_particles_list = []
+        for i in range(self.bart.m):
+            p = ParticleTree(
+                self.bart.trees[i],
+                self.bart.prior_prob_leaf_node,
+                self.init_log_weight,
+                self.init_likelihood,
+            )
+            self.old_trees_particles_list.append(p)
         super().__init__(vars, shared)
 
     def astep(self, _):
         bart = self.bart
+
         inv_link = bart.inv_link
-        num_observations = bart.num_observations
         variable_inclusion = np.zeros(bart.num_variates, dtype="int")
 
-        # For the tunning phase we restrict max_stages to a low number, otherwise it is almost sure
-        # we will reach max_stages given that our first set of m trees is not good at all.
-        # Can set max_stages as a function of the number of variables/dimensions? XXX
-        if self.tune:
-            max_stages = 5
-        else:
-            max_stages = self.max_stages
-
         if self.idx == bart.m:
             self.idx = 0
 
@@ -110,9 +110,9 @@ def astep(self, _):
             bart.sum_trees_output -= old_prediction
             # Generate an initial set of SMC particles
             # at the end of the algorithm we return one of these particles as the new tree
-            particles = self.init_particles(tree.tree_id, num_observations, inv_link)
+            particles = self.init_particles(tree.tree_id)
 
-            for t in range(1, max_stages):
+            for t in range(1, self.max_stages):
                 # Get old particle at stage t
                 particles[0] = self.get_old_tree_particle(tree.tree_id, t)
                 # sample each particle (try to grow each tree)
@@ -120,8 +120,10 @@ def astep(self, _):
                     particles[c].sample_tree_sequential(bart)
                 # Update weights. Since the prior is used as the proposal,the weights
                 # are updated additively as the ratio of the new and old log_likelihoods
-                for p_idx, p in enumerate(particles):
-                    new_likelihood = self.likelihood_logp(inv_link(p.tree.predict_output()))
+                for p in particles:
+                    new_likelihood = self.likelihood_logp(
+                        inv_link(bart.sum_trees_output + p.tree.predict_output())
+                    )
                     p.log_weight += new_likelihood - p.old_likelihood_logp
                     p.old_likelihood_logp = new_likelihood
 
@@ -129,6 +131,7 @@ def astep(self, _):
                 W, normalized_weights = self.normalize(particles)
                 # Resample all but first particle
                 re_n_w = normalized_weights[1:] / normalized_weights[1:].sum()
+
                 new_indices = np.random.choice(self.indices, size=len(self.indices), p=re_n_w)
                 particles[1:] = particles[new_indices]
 
@@ -149,8 +152,7 @@ def astep(self, _):
             new_tree = np.random.choice(particles, p=normalized_weights)
             self.old_trees_particles_list[tree.tree_id] = new_tree
             bart.trees[idx] = new_tree.tree
-            new_prediction = new_tree.tree.predict_output()
-            bart.sum_trees_output += new_prediction
+            bart.sum_trees_output += new_tree.tree.predict_output()
 
             if not self.tune:
                 self.iter += 1
@@ -194,26 +196,28 @@ def get_old_tree_particle(self, tree_id, t):
         old_tree_particle.set_particle_to_step(t)
         return old_tree_particle
 
-    def init_particles(self, tree_id, num_observations, inv_link):
+    def init_particles(self, tree_id):
         """
         Initialize particles
         """
-        # The first particle is from the tree we are trying to replace
         prev_tree = self.get_old_tree_particle(tree_id, 0)
-        likelihood = self.likelihood_logp(inv_link(prev_tree.tree.predict_output()))
+        likelihood = self.likelihood_logp(self.bart.inv_link(prev_tree.tree.predict_output()))
         prev_tree.old_likelihood_logp = likelihood
         prev_tree.log_weight = likelihood - self.log_num_particles
         particles = [prev_tree]
 
-        # The rest of the particles are identically initialized
-        initial_idx_data_points_leaf_nodes = np.arange(num_observations, dtype="int32")
+        initial_idx_data_points_leaf_nodes = np.arange(self.bart.num_observations, dtype="int32")
         new_tree = Tree.init_tree(
             tree_id=tree_id,
-            leaf_node_value=0,
+            leaf_node_value=self.init_leaf_nodes,
             idx_data_points=initial_idx_data_points_leaf_nodes,
         )
-        for i in range(1, self.num_particles):
-            particles.append(ParticleTree(new_tree, self.bart.prior_prob_leaf_node, 0, 0))
+
+        prior_prob = self.bart.prior_prob_leaf_node
+        for _ in range(1, self.num_particles):
+            particles.append(
+                ParticleTree(new_tree, prior_prob, self.init_log_weight, self.init_likelihood)
+            )
 
         return np.array(particles)
 
diff --git a/pymc3/tests/test_bart.py b/pymc3/tests/test_bart.py
index d1bb958007..10b3828390 100644
--- a/pymc3/tests/test_bart.py
+++ b/pymc3/tests/test_bart.py
@@ -1,7 +1,5 @@
 import numpy as np
 
-from scipy.special import expit
-
 import pymc3 as pm
 
 
@@ -106,7 +104,7 @@ def test_model():
 
     Y = np.repeat([0, 1], 50)
     with pm.Model() as model:
-        mu = pm.BART("mu", X, Y, m=50, inv_link=expit, scale=0.25)
+        mu = pm.BART("mu", X, Y, m=50, inv_link="logistic")
         y = pm.Bernoulli("y", mu, observed=Y)
         trace = pm.sample(1000, random_seed=212480)
 

From 9e4c7f9d9b5df13ecbeb0ee1357836fa2aaaecda Mon Sep 17 00:00:00 2001
From: Osvaldo Martin <aloctavodia@gmail.com>
Date: Thu, 27 May 2021 09:09:53 +0300
Subject: [PATCH 15/48] regularize binomial bart (#4720)

* regularize binomial bart

* update release notes
---
 RELEASE-NOTES.md            |  3 ++-
 pymc3/distributions/bart.py | 15 ++++++++-------
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index bbc56eed3e..83784ac17e 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -7,7 +7,8 @@
 + Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
 
 ### New Features
-+ BART with non-gaussian likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675) and [#4709](https://github.com/pymc-devs/pymc3/pull/4709)).
++ Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
+[#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
 
 ## PyMC3 3.11.2 (14 March 2021)
 
diff --git a/pymc3/distributions/bart.py b/pymc3/distributions/bart.py
index 0c3007557f..b621453142 100644
--- a/pymc3/distributions/bart.py
+++ b/pymc3/distributions/bart.py
@@ -28,7 +28,7 @@ def __init__(
         self,
         X,
         Y,
-        m=200,
+        m=50,
         alpha=0.25,
         split_prior=None,
         inv_link=None,
@@ -67,11 +67,12 @@ def __init__(
         if inv_link is None:
             self.inv_link = self.link = lambda x: x
         elif isinstance(inv_link, str):
-            # The link function is just a rough approximation in order to allow the PGBART sampler
-            # to propose reasonable values for the leaf nodes.
             if inv_link == "logistic":
                 self.inv_link = expit
-                self.link = lambda x: (x - 0.5) * 10
+                # The link function is just a rough approximation in order to allow the PGBART
+                # sampler to propose reasonable values for the leaf nodes. The regularizing term
+                # 2 * self.m ** 0.5 is inspired by Chipman's DOI: 10.1214/09-AOAS285
+                self.link = lambda x: (x - 0.5) * 2 * self.m ** 0.5
             elif inv_link == "exp":
                 self.inv_link = np.exp
                 self.link = np.log
@@ -302,8 +303,8 @@ class BART(BaseBART):
     m : int
         Number of trees
     alpha : float
-        Control the prior probability over the depth of the trees. Must be in the interval (0, 1),
-        altought it is recomenned to be in the interval (0, 0.5].
+        Control the prior probability over the depth of the trees. Even when it can takes values in
+        the interval (0, 1), it is recommended to be in the interval (0, 0.5].
     split_prior : array-like
         Each element of split_prior should be in the [0, 1] interval and the elements should sum
         to 1. Otherwise they will be normalized.
@@ -317,7 +318,7 @@ class BART(BaseBART):
         otherwise it does not have any effect.
     """
 
-    def __init__(self, X, Y, m=200, alpha=0.25, split_prior=None, inv_link=None, jitter=False):
+    def __init__(self, X, Y, m=50, alpha=0.25, split_prior=None, inv_link=None, jitter=False):
         super().__init__(X, Y, m, alpha, split_prior, inv_link)
 
     def _str_repr(self, name=None, dist=None, formatting="plain"):

From 576a733ffeccba7ade9bbacd6e5201f515c0dc9e Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Thu, 17 Jun 2021 19:54:48 +0200
Subject: [PATCH 16/48] Create dedicated environment YML for Windows

The install installation guide for Windows was recently updated after it was discovered that the combination of numba+scipy dependencies leads to the correct installation of BLAS dependencies.
---
 .github/workflows/windows.yml               |  4 ++--
 conda-envs/windows-environment-dev-py38.yml | 26 +++++++++++++++++++++
 scripts/generate_pip_deps_from_conda.py     | 12 +++++++++-
 3 files changed, 39 insertions(+), 3 deletions(-)
 create mode 100644 conda-envs/windows-environment-dev-py38.yml

diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 8a81e97b21..c0ecbe8c64 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -31,7 +31,7 @@ jobs:
         with:
           path: ~/conda_pkgs_dir
           key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{
-            hashFiles('conda-envs/environment-dev-py38.yml') }}
+            hashFiles('conda-envs/windows-environment-dev-py38.yml') }}
       - name: Cache multiple paths
         uses: actions/cache@v2
         env:
@@ -48,7 +48,7 @@ jobs:
         with:
           activate-environment: pymc3-dev-py38
           channel-priority: strict
-          environment-file: conda-envs/environment-dev-py38.yml
+          environment-file: conda-envs/windows-environment-dev-py38.yml
           use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly!
       - name: Install-pymc3
         run: |
diff --git a/conda-envs/windows-environment-dev-py38.yml b/conda-envs/windows-environment-dev-py38.yml
new file mode 100644
index 0000000000..ea8b0622c0
--- /dev/null
+++ b/conda-envs/windows-environment-dev-py38.yml
@@ -0,0 +1,26 @@
+name: pymc3-dev-py38
+channels:
+- conda-forge
+- defaults
+dependencies:
+ # base dependencies (see install guide for Windows)
+- h5py>=2.7
+- libpython
+- mkl-service
+- m2w64-toolchain
+- numba
+- pip
+- python=3.8
+- python-graphviz
+- scipy
+# Extra stuff for dev, testing and docs build
+- ipython>=7.16
+- nbsphinx>=0.4
+- numpydoc>=0.9
+- pre-commit>=2.8.0
+- pytest-cov>=2.5
+- pytest>=3.0
+- recommonmark>=0.4
+- sphinx-autobuild>=0.7
+- sphinx>=1.5
+- watermark
diff --git a/scripts/generate_pip_deps_from_conda.py b/scripts/generate_pip_deps_from_conda.py
index 04a764d57e..28047f44a3 100755
--- a/scripts/generate_pip_deps_from_conda.py
+++ b/scripts/generate_pip_deps_from_conda.py
@@ -41,7 +41,17 @@
 
 import yaml
 
-EXCLUDE = {"python", "libblas", "mkl-service", "python-graphviz"}
+EXCLUDE = {
+    "pip",
+    "python",
+    "libblas",
+    "libpython",
+    "m2w64-toolchain",
+    "mkl-service",
+    "numba",
+    "scipy",
+    "python-graphviz",
+}
 RENAME = {}
 
 

From 5e8063dc2055b40184f68a2c132e96ffaaa3085c Mon Sep 17 00:00:00 2001
From: Sayam753 <sayamkumar753@yahoo.in>
Date: Fri, 18 Jun 2021 02:57:19 +0530
Subject: [PATCH 17/48] Fix LKJCorr.random

---
 pymc3/distributions/multivariate.py      |  2 +-
 pymc3/tests/test_distributions_random.py | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/pymc3/distributions/multivariate.py b/pymc3/distributions/multivariate.py
index 3fcdb8dbda..946aeb45cc 100755
--- a/pymc3/distributions/multivariate.py
+++ b/pymc3/distributions/multivariate.py
@@ -1541,7 +1541,7 @@ def random(self, point=None, size=None):
         """
         n, eta = draw_values([self.n, self.eta], point=point, size=size)
         size = 1 if size is None else size
-        samples = generate_samples(self._random, n, eta, broadcast_shape=(size,))
+        samples = generate_samples(self._random, n, eta, broadcast_shape=to_tuple(size))
         return samples
 
     def logp(self, x):
diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py
index a56f3f3b7b..972e84cda7 100644
--- a/pymc3/tests/test_distributions_random.py
+++ b/pymc3/tests/test_distributions_random.py
@@ -1823,6 +1823,21 @@ def test_matrix_normal_random_with_random_variables():
     assert prior["mu"].shape == (2, D, K)
 
 
+@pytest.mark.parametrize("n", [2, 5])
+def test_lkj_corr_with_prior_predictive(n):
+    """
+    This test checks for shape correctness when using pm.sample_prior_predictive
+    on LKJCorr distribution.
+    Originally reported - https://github.com/pymc-devs/pymc3/issues/4778
+    """
+    with pm.Model():
+        lkj = pm.LKJCorr("lkj", eta=1, n=n)
+        prior = pm.sample_prior_predictive(500)
+
+    upper_triangular_shape = n * (n - 1) // 2
+    assert prior["lkj"].shape == (500, upper_triangular_shape)
+
+
 class TestMvGaussianRandomWalk(SeededTest):
     @pytest.mark.parametrize(
         ["sample_shape", "dist_shape", "mu_shape", "param"],

From ea13746e2dd0f8f99c78229cda041169c8ef6a9f Mon Sep 17 00:00:00 2001
From: Sayam753 <sayamkumar753@yahoo.in>
Date: Fri, 18 Jun 2021 03:25:00 +0530
Subject: [PATCH 18/48] Give a mention in release notes

---
 RELEASE-NOTES.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 83784ac17e..aab5e1bb1f 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -5,6 +5,7 @@
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
 + Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
++ Fix `LKJCorr.random` method to work with `pm.sample_prior_predictive`. (see [#4780](https://github.com/pymc-devs/pymc3/pull/4780))
 
 ### New Features
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and

From 5112bad95bf7d76ee4055d73e2c4f2d37fb58ee7 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sat, 19 Jun 2021 20:46:39 +0200
Subject: [PATCH 19/48] Include deps sorting like done on main

---
 scripts/generate_pip_deps_from_conda.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/generate_pip_deps_from_conda.py b/scripts/generate_pip_deps_from_conda.py
index 28047f44a3..0413754af6 100755
--- a/scripts/generate_pip_deps_from_conda.py
+++ b/scripts/generate_pip_deps_from_conda.py
@@ -127,7 +127,7 @@ def main(conda_fname, pip_fname):
         f"# This file is auto-generated by scripts/generate_pip_deps_from_conda.py, "
         "do not modify.\n# See that file for comments about the need/usage of each dependency.\n\n"
     )
-    pip_content = header + "\n".join(pip_deps) + "\n"
+    pip_content = header + "\n".join(sorted(pip_deps)) + "\n"
 
     with open(pip_fname, "w") as pip_fd:
         pip_fd.write(pip_content)

From b49aa1f8a49fd6877bb9454eef360bdaf9a9f11e Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sat, 19 Jun 2021 20:48:09 +0200
Subject: [PATCH 20/48] Run CI jobs on v3 branch

---
 .github/workflows/arviz_compat.yml | 2 +-
 .github/workflows/jaxtests.yml     | 2 +-
 .github/workflows/pre-commit.yml   | 2 +-
 .github/workflows/pytest.yml       | 2 +-
 .github/workflows/windows.yml      | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/arviz_compat.yml b/.github/workflows/arviz_compat.yml
index 2bbf076205..f4709169ac 100644
--- a/.github/workflows/arviz_compat.yml
+++ b/.github/workflows/arviz_compat.yml
@@ -3,7 +3,7 @@ name: arviz-compatibility
 on:
   pull_request:
   push:
-    branches: [master]
+    branches: [v3]
 
 jobs:
   pytest:
diff --git a/.github/workflows/jaxtests.yml b/.github/workflows/jaxtests.yml
index c5b3f23963..5bd89e3a27 100644
--- a/.github/workflows/jaxtests.yml
+++ b/.github/workflows/jaxtests.yml
@@ -3,7 +3,7 @@ name: jax-sampling
 on:
   pull_request:
   push:
-    branches: [master]
+    branches: [v3]
 
 jobs:
   pytest:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 6131de7400..88ea331924 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -3,7 +3,7 @@ name: pre-commit
 on:
   pull_request:
   push:
-    branches: [master]
+    branches: [v3]
 
 jobs:
   pre-commit:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index e492c7e705..338847271a 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -3,7 +3,7 @@ name: pytest
 on:
   pull_request:
   push:
-    branches: [master]
+    branches: [v3]
 
 jobs:
   pytest:
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index c0ecbe8c64..be9df7ff4b 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -3,7 +3,7 @@ name: windows
 on:
   pull_request:
   push:
-    branches: [master]
+    branches: [v3]
 
 jobs:
   pytest:

From bf83ae6692ec00b394408afd122dac8df72eb4d7 Mon Sep 17 00:00:00 2001
From: Elbert Fliek <elbert.fliek@objectivepartners.com>
Date: Wed, 5 May 2021 22:00:51 +0200
Subject: [PATCH 21/48] Replace deprecated np.bool by native bool

---
 pymc3/step_methods/hmc/hmc.py       |  6 +++---
 pymc3/step_methods/hmc/nuts.py      |  4 ++--
 pymc3/step_methods/metropolis.py    | 14 +++++++-------
 pymc3/step_methods/mlda.py          |  2 +-
 pymc3/tests/backend_fixtures.py     |  8 ++++----
 pymc3/tests/test_ndarray_backend.py |  2 +-
 6 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/pymc3/step_methods/hmc/hmc.py b/pymc3/step_methods/hmc/hmc.py
index 613160c27e..0fcda73ab2 100644
--- a/pymc3/step_methods/hmc/hmc.py
+++ b/pymc3/step_methods/hmc/hmc.py
@@ -39,14 +39,14 @@ class HamiltonianMC(BaseHMC):
         {
             "step_size": np.float64,
             "n_steps": np.int64,
-            "tune": np.bool,
+            "tune": bool,
             "step_size_bar": np.float64,
             "accept": np.float64,
-            "diverging": np.bool,
+            "diverging": bool,
             "energy_error": np.float64,
             "energy": np.float64,
             "path_length": np.float64,
-            "accepted": np.bool,
+            "accepted": bool,
             "model_logp": np.float64,
             "process_time_diff": np.float64,
             "perf_counter_diff": np.float64,
diff --git a/pymc3/step_methods/hmc/nuts.py b/pymc3/step_methods/hmc/nuts.py
index 4a00ec9873..dbd1f1c903 100644
--- a/pymc3/step_methods/hmc/nuts.py
+++ b/pymc3/step_methods/hmc/nuts.py
@@ -94,11 +94,11 @@ class NUTS(BaseHMC):
         {
             "depth": np.int64,
             "step_size": np.float64,
-            "tune": np.bool,
+            "tune": bool,
             "mean_tree_accept": np.float64,
             "step_size_bar": np.float64,
             "tree_size": np.float64,
-            "diverging": np.bool,
+            "diverging": bool,
             "energy_error": np.float64,
             "energy": np.float64,
             "max_energy_error": np.float64,
diff --git a/pymc3/step_methods/metropolis.py b/pymc3/step_methods/metropolis.py
index 76804db2f8..d26f7207bf 100644
--- a/pymc3/step_methods/metropolis.py
+++ b/pymc3/step_methods/metropolis.py
@@ -104,8 +104,8 @@ class Metropolis(ArrayStepShared):
     stats_dtypes = [
         {
             "accept": np.float64,
-            "accepted": np.bool,
-            "tune": np.bool,
+            "accepted": bool,
+            "tune": bool,
             "scaling": np.float64,
         }
     ]
@@ -295,7 +295,7 @@ class BinaryMetropolis(ArrayStep):
     stats_dtypes = [
         {
             "accept": np.float64,
-            "tune": np.bool,
+            "tune": bool,
             "p_jump": np.float64,
         }
     ]
@@ -589,8 +589,8 @@ class DEMetropolis(PopulationArrayStepShared):
     stats_dtypes = [
         {
             "accept": np.float64,
-            "accepted": np.bool,
-            "tune": np.bool,
+            "accepted": bool,
+            "tune": bool,
             "scaling": np.float64,
             "lambda": np.float64,
         }
@@ -730,8 +730,8 @@ class DEMetropolisZ(ArrayStepShared):
     stats_dtypes = [
         {
             "accept": np.float64,
-            "accepted": np.bool,
-            "tune": np.bool,
+            "accepted": bool,
+            "tune": bool,
             "scaling": np.float64,
             "lambda": np.float64,
         }
diff --git a/pymc3/step_methods/mlda.py b/pymc3/step_methods/mlda.py
index 559f894f30..c000af1e34 100644
--- a/pymc3/step_methods/mlda.py
+++ b/pymc3/step_methods/mlda.py
@@ -667,7 +667,7 @@ def __init__(
 
         else:
             # otherwise, set it up from scratch.
-            self.stats_dtypes = [{"accept": np.float64, "accepted": np.bool, "tune": np.bool}]
+            self.stats_dtypes = [{"accept": np.float64, "accepted": bool, "tune": bool}]
 
             if isinstance(self.step_method_below, MetropolisMLDA):
                 self.stats_dtypes.append({"base_scaling": np.float64})
diff --git a/pymc3/tests/backend_fixtures.py b/pymc3/tests/backend_fixtures.py
index 6fd0b1318c..5735ba5b4d 100644
--- a/pymc3/tests/backend_fixtures.py
+++ b/pymc3/tests/backend_fixtures.py
@@ -61,11 +61,11 @@ def test_append_invalid(self):
             with pytest.raises(ValueError):
                 self.strace.setup(self.draws, self.chain)
             with pytest.raises(ValueError):
-                vars = self.sampler_vars + [{"a": np.bool}]
+                vars = self.sampler_vars + [{"a": bool}]
                 self.strace.setup(self.draws, self.chain, vars)
         else:
             with pytest.raises((ValueError, TypeError)):
-                self.strace.setup(self.draws, self.chain, [{"a": np.bool}])
+                self.strace.setup(self.draws, self.chain, [{"a": bool}])
 
     def test_append(self):
         if self.sampler_vars is None:
@@ -103,7 +103,7 @@ def setup_method(self):
         self.draws, self.chain = 3, 0
 
     def test_bad_dtype(self):
-        bad_vars = [{"a": np.float64}, {"a": np.bool}]
+        bad_vars = [{"a": np.float64}, {"a": bool}]
         good_vars = [{"a": np.float64}, {"a": np.float64}]
         with self.model:
             strace = self.backend(self.name)
@@ -185,7 +185,7 @@ def setup_class(cls):
                 cls.expected_stats[0].append(stats)
                 cls.expected_stats[1].append(stats)
                 for key, dtype in vars.items():
-                    if dtype == np.bool:
+                    if dtype == bool:
                         stats[key] = np.zeros(cls.draws, dtype=dtype)
                     else:
                         stats[key] = np.arange(cls.draws, dtype=dtype)
diff --git a/pymc3/tests/test_ndarray_backend.py b/pymc3/tests/test_ndarray_backend.py
index 1b13aa0b0f..f13a99e6f8 100644
--- a/pymc3/tests/test_ndarray_backend.py
+++ b/pymc3/tests/test_ndarray_backend.py
@@ -21,7 +21,7 @@
 from pymc3.backends import base, ndarray
 from pymc3.tests import backend_fixtures as bf
 
-STATS1 = [{"a": np.float64, "b": np.bool}]
+STATS1 = [{"a": np.float64, "b": bool}]
 
 STATS2 = [
     {"a": np.float64},

From 46cd3235b65b5f0cf3278deb50c1159042d8fd46 Mon Sep 17 00:00:00 2001
From: Sayam Kumar <sayamkumar753@yahoo.in>
Date: Sun, 4 Jul 2021 03:18:55 +0530
Subject: [PATCH 22/48] Enable ReadTheDocs on v3 (#4805)

* Add readthedocs yml file
* Manually invoke building gallery instead of using it as an extension
* Bring back the gallery extension. Just create the required directories for sphinx to build tutorials and examples section
* Fixup a bug when expected static directories are already present; Simplified to use existing directories path
* Simplified relative urls handling
* Given a mention in release notes
* Add installation links to landing page
* Update pymc-examples submodule
* Fix relative links for in-depth guides
---
 .readthedocs.yml                           | 16 ++++++++
 RELEASE-NOTES.md                           |  1 +
 docs/source/conf.py                        |  5 +++
 docs/source/index.rst                      | 44 +++++++++++-----------
 docs/source/pymc-examples                  |  2 +-
 docs/source/sphinxext/gallery_generator.py | 11 +++++-
 6 files changed, 54 insertions(+), 25 deletions(-)
 create mode 100644 .readthedocs.yml

diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000000..70d4c5250a
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,16 @@
+version: 2
+
+sphinx:
+    configuration: docs/source/conf.py
+
+python:
+   version: 3.7
+   install:
+   - requirements: requirements-dev.txt
+   - method: pip
+     path: .
+
+
+submodules:
+    include: all
+    recursive: true
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index aab5e1bb1f..6c47a4751f 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -6,6 +6,7 @@
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
 + Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
 + Fix `LKJCorr.random` method to work with `pm.sample_prior_predictive`. (see [#4780](https://github.com/pymc-devs/pymc3/pull/4780))
++ Enable documentation generation via ReadTheDocs for upcoming v3 releases. (see [#4805](https://github.com/pymc-devs/pymc3/pull/4805)).
 
 ### New Features
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 9b23a323af..6fc8780e8b 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -15,6 +15,8 @@
 import os
 import sys
 
+from pathlib import Path
+
 import pymc3
 
 # If extensions (or modules to document with autodoc) are in another directory,
@@ -330,3 +332,6 @@
 def setup(app):
     app.add_css_file("https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css")
     app.add_css_file("default.css")
+    dir_root = Path(__file__).parent
+    for directory in html_static_path:
+        os.makedirs(str(dir_root / directory), exist_ok=True)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 053e1962f1..205f8ee902 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -44,28 +44,28 @@
 
         <div class="ui container">
 
-        <h2 class="ui dividing header">Installation</h2>
-
-            <div class="row">
-                <div class="ui text container">
-                    <h3 class="ui header">Via conda-forge:</h3>
-
-.. code-block:: bash
-
-    conda install -c conda-forge pymc3
-
-
-.. raw:: html
-
-                    <h3 class="ui header">Latest (unstable):</h3>
+        <div class="ui vertical segment">
+            <h2 class="ui dividing header">Installation</h2>
+            <div class="ui three stackable cards">
 
-.. code-block:: bash
+                <a class="ui link card" href="https://github.com/pymc-devs/pymc3/wiki/Installation-Guide-(Linux)">
+                    <div class="content">
+                        <div class="header">Instructions for Linux</div>
+                    </div>
+                </a>
 
-    pip install git+https://github.com/pymc-devs/pymc3
+                <a class="ui link card" href="https://github.com/pymc-devs/pymc3/wiki/Installation-Guide-(MacOS)">
+                    <div class="content">
+                        <div class="header">Instructions for MacOS</div>
+                    </div>
+                </a>
 
-.. raw:: html
+                <a class="ui link card" href="https://github.com/pymc-devs/pymc3/wiki/Installation-Guide-(Windows)">
+                    <div class="content">
+                        <div class="header">Instructions for Windows</div>
+                    </div>
+                </a>
 
-                </div>
             </div>
         </div>
 
@@ -73,7 +73,7 @@
             <h2 class="ui dividing header">In-Depth Guides</h2>
             <div class="ui four stackable cards">
 
-                <a class="ui link card" href="/Probability_Distributions.html">
+                <a class="ui link card" href="Probability_Distributions.html">
                     <div class="content">
                         <div class="header">Probability Distributions</div>
                         <div class="description">PyMC3 includes a comprehensive set of pre-defined statistical distributions that can be used as model building blocks.
@@ -81,7 +81,7 @@
                     </div>
                 </a>
 
-                <a class="ui link card" href="/Gaussian_Processes.html">
+                <a class="ui link card" href="Gaussian_Processes.html">
                     <div class="content">
                         <div class="header">Gaussian Processes</div>
                         <div class="description">Sometimes an unknown parameter or variable in a model is not a scalar value or a fixed-length vector, but a function. A Gaussian process (GP) can be used as a prior probability distribution whose support is over the space of continuous functions. PyMC3 provides rich support for defining and using GPs.
@@ -89,7 +89,7 @@
                     </div>
                 </a>
 
-                <a class="ui link card" href="/pymc-examples/examples/variational_inference/variational_api_quickstart.html">
+                <a class="ui link card" href="pymc-examples/examples/variational_inference/variational_api_quickstart.html">
                     <div class="content">
                         <div class="header">Variational Inference</div>
                         <div class="description">Variational inference saves computational cost by turning a problem of integration into one of optimization. PyMC3's variational API supports a number of cutting edge algorithms, as well as minibatch for scaling to large datasets.
@@ -97,7 +97,7 @@
                     </div>
                 </a>
 
-                <a class="ui link card" href="/PyMC3_and_Theano.html">
+                <a class="ui link card" href="PyMC3_and_Theano.html">
                     <div class="content">
                         <div class="header">PyMC3 and Theano</div>
                         <div class="description">Theano is the deep-learning library PyMC3 uses to construct probability distributions and then access the gradient in order to implement cutting edge inference algorithms. More advanced models may be built by understanding this layer.
diff --git a/docs/source/pymc-examples b/docs/source/pymc-examples
index a093bcc5aa..0bbf199c46 160000
--- a/docs/source/pymc-examples
+++ b/docs/source/pymc-examples
@@ -1 +1 @@
-Subproject commit a093bcc5aa828e9317bdf9f936f968f76acdb4f7
+Subproject commit 0bbf199c462a65e3c9a7b5155f7b47c69eae2581
diff --git a/docs/source/sphinxext/gallery_generator.py b/docs/source/sphinxext/gallery_generator.py
index caf4beb450..46273e04d6 100644
--- a/docs/source/sphinxext/gallery_generator.py
+++ b/docs/source/sphinxext/gallery_generator.py
@@ -170,16 +170,23 @@ def build_gallery(srcdir, gallery):
         if basename.find(".rst") < 1:
             filename = os.path.join(source_dir, basename + ".ipynb")
             ex = NotebookGenerator(filename, target_dir)
+            url = Path(os.path.join(os.sep, gallery, ex.output_html))
+            # Need to chop off "/${gallery}/../" so as redirection works in multi versioned docs.
+            url = str(Path("..", *url.parts[3:]))
             data[basename] = {
                 "title": ex.pagetitle,
-                "url": os.path.join(os.sep, gallery, ex.output_html),
+                "url": url,
                 "thumb": os.path.basename(ex.png_path),
             }
+
         else:
             filename = basename.split(".")[0]
+            url = Path(os.path.join(os.sep, gallery, "../" + filename + ".html"))
+            # Need to chop off "/${gallery}/../" so as redirection works in multi versioned docs.
+            url = str(Path("..", *url.parts[3:]))
             data[basename] = {
                 "title": " ".join(filename.split("_")),
-                "url": os.path.join(os.sep, gallery, "../" + filename + ".html"),
+                "url": url,
                 "thumb": os.path.basename(default_png_path),
             }
 

From 135e5105f9ec2d4fc48444a168744ed7547cb0ed Mon Sep 17 00:00:00 2001
From: Ben Mares <15216687+maresb@users.noreply.github.com>
Date: Sat, 26 Jun 2021 13:17:32 +0200
Subject: [PATCH 23/48] Fix deprecation warning for np.int
 https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations

---
 pymc3/distributions/bound.py | 2 +-
 pymc3/gp/cov.py              | 2 +-
 pymc3/gp/util.py             | 2 +-
 pymc3/tests/test_mixture.py  | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/pymc3/distributions/bound.py b/pymc3/distributions/bound.py
index 074a575eba..7d2000c5ed 100644
--- a/pymc3/distributions/bound.py
+++ b/pymc3/distributions/bound.py
@@ -84,7 +84,7 @@ def _random(self, lower, upper, point=None, size=None):
             raise ValueError(
                 "Drawing samples from distributions with array-valued bounds is not supported."
             )
-        total_size = np.prod(size).astype(np.int)
+        total_size = np.prod(size).astype(int)
         samples = []
         s = 0
         while s < total_size:
diff --git a/pymc3/gp/cov.py b/pymc3/gp/cov.py
index 7a01a9eec5..a696c30b8a 100644
--- a/pymc3/gp/cov.py
+++ b/pymc3/gp/cov.py
@@ -61,7 +61,7 @@ def __init__(self, input_dim, active_dims=None):
         if active_dims is None:
             self.active_dims = np.arange(input_dim)
         else:
-            self.active_dims = np.asarray(active_dims, np.int)
+            self.active_dims = np.asarray(active_dims, int)
 
     def __call__(self, X, Xs=None, diag=False):
         r"""
diff --git a/pymc3/gp/util.py b/pymc3/gp/util.py
index 698c300564..b0875b905a 100644
--- a/pymc3/gp/util.py
+++ b/pymc3/gp/util.py
@@ -29,7 +29,7 @@
 def infer_shape(X, n_points=None):
     if n_points is None:
         try:
-            n_points = np.int(X.shape[0])
+            n_points = int(X.shape[0])
         except TypeError:
             raise TypeError("Cannot infer 'shape', provide as an argument")
     return n_points
diff --git a/pymc3/tests/test_mixture.py b/pymc3/tests/test_mixture.py
index 94b272bf43..acee36acfa 100644
--- a/pymc3/tests/test_mixture.py
+++ b/pymc3/tests/test_mixture.py
@@ -343,7 +343,7 @@ def build_toy_dataset(N, K):
             mus = [[1, 1, 1], [-1, -1, -1], [2, -2, 0]]
             stds = [[0.1, 0.1, 0.1], [0.1, 0.2, 0.2], [0.2, 0.3, 0.3]]
             x = np.zeros((N, 3), dtype=np.float32)
-            y = np.zeros((N,), dtype=np.int)
+            y = np.zeros((N,), dtype=int)
             for n in range(N):
                 k = np.argmax(np.random.multinomial(1, pi))
                 x[n, :] = np.random.multivariate_normal(mus[k], np.diag(stds[k]))

From d2b1ed25266aa38878b8c9129e7fa40c58a654ad Mon Sep 17 00:00:00 2001
From: Ben Mares <15216687+maresb@users.noreply.github.com>
Date: Sat, 26 Jun 2021 13:47:07 +0200
Subject: [PATCH 24/48] Fix return_inferencedata warnings in tests

---
 benchmarks/benchmarks/benchmarks.py       | 22 +++++-
 pymc3/tests/sampler_fixtures.py           |  8 +-
 pymc3/tests/test_bart.py                  |  4 +-
 pymc3/tests/test_data_container.py        | 18 ++---
 pymc3/tests/test_distributions.py         |  2 +-
 pymc3/tests/test_distributions_random.py  | 12 +--
 pymc3/tests/test_examples.py              | 26 +++---
 pymc3/tests/test_hmc.py                   |  4 +-
 pymc3/tests/test_mixture.py               |  2 +-
 pymc3/tests/test_model.py                 |  8 +-
 pymc3/tests/test_ndarray_backend.py       |  4 +-
 pymc3/tests/test_ode.py                   |  8 +-
 pymc3/tests/test_parallel_sampling.py     | 41 ++++++++--
 pymc3/tests/test_posterior_predictive.py  |  2 +-
 pymc3/tests/test_quadpotential.py         | 16 +++-
 pymc3/tests/test_sampling.py              | 96 ++++++++++++++++-------
 pymc3/tests/test_shared.py                |  2 +-
 pymc3/tests/test_theanof.py               |  9 ++-
 pymc3/tests/test_variational_inference.py |  4 +-
 19 files changed, 205 insertions(+), 83 deletions(-)

diff --git a/benchmarks/benchmarks/benchmarks.py b/benchmarks/benchmarks/benchmarks.py
index 489befbefc..8adc06107b 100644
--- a/benchmarks/benchmarks/benchmarks.py
+++ b/benchmarks/benchmarks/benchmarks.py
@@ -98,6 +98,7 @@ def time_overhead_sample(self, step):
                 random_seed=1,
                 progressbar=False,
                 compute_convergence_checks=False,
+                return_inferencedata=False,
             )
 
 
@@ -150,13 +151,23 @@ def time_drug_evaluation(self):
                 "effect size", diff_of_means / np.sqrt((group1_std ** 2 + group2_std ** 2) / 2)
             )
             pm.sample(
-                draws=20000, cores=4, chains=4, progressbar=False, compute_convergence_checks=False
+                draws=20000,
+                cores=4,
+                chains=4,
+                progressbar=False,
+                compute_convergence_checks=False,
+                return_inferencedata=False,
             )
 
     def time_glm_hierarchical(self):
         with glm_hierarchical_model():
             pm.sample(
-                draws=20000, cores=4, chains=4, progressbar=False, compute_convergence_checks=False
+                draws=20000,
+                cores=4,
+                chains=4,
+                progressbar=False,
+                compute_convergence_checks=False,
+                return_inferencedata=False,
             )
 
 
@@ -190,6 +201,7 @@ def track_glm_hierarchical_ess(self, init):
                 random_seed=100,
                 progressbar=False,
                 compute_convergence_checks=False,
+                return_inferencedata=False,
             )
             tot = time.time() - t0
         ess = float(az.ess(trace, var_names=["mu_a"])["mu_a"].values)
@@ -212,6 +224,7 @@ def track_marginal_mixture_model_ess(self, init):
                 random_seed=100,
                 progressbar=False,
                 compute_convergence_checks=False,
+                return_inferencedata=False,
             )
             tot = time.time() - t0
         ess = az.ess(trace, var_names=["mu"])["mu"].values.min()  # worst case
@@ -243,6 +256,7 @@ def track_glm_hierarchical_ess(self, step):
                 random_seed=100,
                 progressbar=False,
                 compute_convergence_checks=False,
+                return_inferencedata=False,
             )
             tot = time.time() - t0
         ess = float(az.ess(trace, var_names=["mu_a"])["mu_a"].values)
@@ -302,7 +316,9 @@ def freefall(y, t, p):
             Y = pm.Normal("Y", mu=ode_solution, sd=sigma, observed=y)
 
             t0 = time.time()
-            trace = pm.sample(500, tune=1000, chains=2, cores=2, random_seed=0)
+            trace = pm.sample(
+                500, tune=1000, chains=2, cores=2, random_seed=0, return_inferencedata=False
+            )
             tot = time.time() - t0
         ess = az.ess(trace)
         return np.mean([ess.sigma, ess.gamma]) / tot
diff --git a/pymc3/tests/sampler_fixtures.py b/pymc3/tests/sampler_fixtures.py
index fcf66f1556..a79c7cb0bf 100644
--- a/pymc3/tests/sampler_fixtures.py
+++ b/pymc3/tests/sampler_fixtures.py
@@ -140,7 +140,13 @@ def setup_class(cls):
         cls.model = cls.make_model()
         with cls.model:
             cls.step = cls.make_step()
-            cls.trace = pm.sample(cls.n_samples, tune=cls.tune, step=cls.step, cores=cls.chains)
+            cls.trace = pm.sample(
+                cls.n_samples,
+                tune=cls.tune,
+                step=cls.step,
+                cores=cls.chains,
+                return_inferencedata=False,
+            )
         cls.samples = {}
         for var in cls.model.unobserved_RVs:
             cls.samples[get_var_name(var)] = cls.trace.get_values(var, burn=cls.burn)
diff --git a/pymc3/tests/test_bart.py b/pymc3/tests/test_bart.py
index 10b3828390..8858fefe92 100644
--- a/pymc3/tests/test_bart.py
+++ b/pymc3/tests/test_bart.py
@@ -98,7 +98,7 @@ def test_model():
         sigma = pm.HalfNormal("sigma", 1)
         mu = pm.BART("mu", X, Y, m=50)
         y = pm.Normal("y", mu, sigma, observed=Y)
-        trace = pm.sample(1000, random_seed=212480)
+        trace = pm.sample(1000, random_seed=212480, return_inferencedata=False)
 
     np.testing.assert_allclose(trace[mu].mean(0), Y, 0.5)
 
@@ -106,6 +106,6 @@ def test_model():
     with pm.Model() as model:
         mu = pm.BART("mu", X, Y, m=50, inv_link="logistic")
         y = pm.Bernoulli("y", mu, observed=Y)
-        trace = pm.sample(1000, random_seed=212480)
+        trace = pm.sample(1000, random_seed=212480, return_inferencedata=False)
 
     np.testing.assert_allclose(trace[mu].mean(0), Y, atol=0.5)
diff --git a/pymc3/tests/test_data_container.py b/pymc3/tests/test_data_container.py
index e1914f1e57..41c091b09b 100644
--- a/pymc3/tests/test_data_container.py
+++ b/pymc3/tests/test_data_container.py
@@ -44,7 +44,7 @@ def test_sample(self):
             pm.Normal("obs", b * x_shared, np.sqrt(1e-2), observed=y)
 
             prior_trace0 = pm.sample_prior_predictive(1000)
-            trace = pm.sample(1000, init=None, tune=1000, chains=1)
+            trace = pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
             pp_trace0 = pm.sample_posterior_predictive(trace, 1000)
             pp_trace01 = pm.fast_sample_posterior_predictive(trace, 1000)
 
@@ -75,7 +75,7 @@ def test_sample_posterior_predictive_after_set_data(self):
             y = pm.Data("y", [1.0, 2.0, 3.0])
             beta = pm.Normal("beta", 0, 10.0)
             pm.Normal("obs", beta * x, np.sqrt(1e-2), observed=y)
-            trace = pm.sample(1000, tune=1000, chains=1)
+            trace = pm.sample(1000, tune=1000, chains=1, return_inferencedata=False)
         # Predict on new data.
         with model:
             x_test = [5.0, 6.0, 9.0]
@@ -94,13 +94,13 @@ def test_sample_after_set_data(self):
             y = pm.Data("y", [1.0, 2.0, 3.0])
             beta = pm.Normal("beta", 0, 10.0)
             pm.Normal("obs", beta * x, np.sqrt(1e-2), observed=y)
-            pm.sample(1000, init=None, tune=1000, chains=1)
+            pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
         # Predict on new data.
         new_x = [5.0, 6.0, 9.0]
         new_y = [5.0, 6.0, 9.0]
         with model:
             pm.set_data(new_data={"x": new_x, "y": new_y})
-            new_trace = pm.sample(1000, init=None, tune=1000, chains=1)
+            new_trace = pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
             pp_trace = pm.sample_posterior_predictive(new_trace, 1000)
             pp_tracef = pm.fast_sample_posterior_predictive(new_trace, 1000)
 
@@ -121,7 +121,7 @@ def test_shared_data_as_index(self):
             pm.Normal("obs", alpha[index], np.sqrt(1e-2), observed=y)
 
             prior_trace = pm.sample_prior_predictive(1000, var_names=["alpha"])
-            trace = pm.sample(1000, init=None, tune=1000, chains=1)
+            trace = pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
 
         # Predict on new data
         new_index = np.array([0, 1, 2])
@@ -146,14 +146,14 @@ def test_shared_data_as_rv_input(self):
         with pm.Model() as m:
             x = pm.Data("x", [1.0, 2.0, 3.0])
             _ = pm.Normal("y", mu=x, shape=3)
-            trace = pm.sample(chains=1)
+            trace = pm.sample(chains=1, return_inferencedata=False)
 
         np.testing.assert_allclose(np.array([1.0, 2.0, 3.0]), x.get_value(), atol=1e-1)
         np.testing.assert_allclose(np.array([1.0, 2.0, 3.0]), trace["y"].mean(0), atol=1e-1)
 
         with m:
             pm.set_data({"x": np.array([2.0, 4.0, 6.0])})
-            trace = pm.sample(chains=1)
+            trace = pm.sample(chains=1, return_inferencedata=False)
 
         np.testing.assert_allclose(np.array([2.0, 4.0, 6.0]), x.get_value(), atol=1e-1)
         np.testing.assert_allclose(np.array([2.0, 4.0, 6.0]), trace["y"].mean(0), atol=1e-1)
@@ -189,7 +189,7 @@ def test_set_data_to_non_data_container_variables(self):
             y = np.array([1.0, 2.0, 3.0])
             beta = pm.Normal("beta", 0, 10.0)
             pm.Normal("obs", beta * x, np.sqrt(1e-2), observed=y)
-            pm.sample(1000, init=None, tune=1000, chains=1)
+            pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
         with pytest.raises(TypeError) as error:
             pm.set_data({"beta": [1.1, 2.2, 3.3]}, model=model)
         error.match("defined as `pymc3.Data` inside the model")
@@ -201,7 +201,7 @@ def test_model_to_graphviz_for_model_with_data_container(self):
             beta = pm.Normal("beta", 0, 10.0)
             obs_sigma = floatX(np.sqrt(1e-2))
             pm.Normal("obs", beta * x, obs_sigma, observed=y)
-            pm.sample(1000, init=None, tune=1000, chains=1)
+            pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
 
         for formatting in {"latex", "latex_with_params"}:
             with pytest.raises(ValueError, match="Unsupported formatting"):
diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py
index 06efc90b8d..98baccb28f 100644
--- a/pymc3/tests/test_distributions.py
+++ b/pymc3/tests/test_distributions.py
@@ -2596,7 +2596,7 @@ def func(x):
     with pm.Model():
         pm.Normal("x")
         y = pm.DensityDist("y", func)
-        pm.sample(draws=5, tune=1, mp_ctx="spawn")
+        pm.sample(draws=5, tune=1, mp_ctx="spawn", return_inferencedata=False)
 
     import pickle
 
diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py
index 972e84cda7..1214cb8d56 100644
--- a/pymc3/tests/test_distributions_random.py
+++ b/pymc3/tests/test_distributions_random.py
@@ -1296,7 +1296,7 @@ def test_density_dist_with_random_sampleable(self, shape):
                 shape=shape,
                 random=normal_dist.random,
             )
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         size = 100
@@ -1319,7 +1319,7 @@ def test_density_dist_with_random_sampleable_failure(self, shape):
                 random=normal_dist.random,
                 wrap_random_with_dist_shape=False,
             )
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         with pytest.raises(RuntimeError):
@@ -1342,7 +1342,7 @@ def test_density_dist_with_random_sampleable_hidden_error(self, shape):
                 wrap_random_with_dist_shape=False,
                 check_shape_in_random=False,
             )
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         ppc = pm.sample_posterior_predictive(trace, samples=samples, model=model)
@@ -1365,7 +1365,7 @@ def test_density_dist_with_random_sampleable_handcrafted_success(self):
                 random=rvs,
                 wrap_random_with_dist_shape=False,
             )
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         size = 100
@@ -1385,7 +1385,7 @@ def test_density_dist_with_random_sampleable_handcrafted_success_fast(self):
                 random=rvs,
                 wrap_random_with_dist_shape=False,
             )
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         size = 100
@@ -1398,7 +1398,7 @@ def test_density_dist_without_random_not_sampleable(self):
             mu = pm.Normal("mu", 0, 1)
             normal_dist = pm.Normal.dist(mu, 1)
             pm.DensityDist("density_dist", normal_dist.logp, observed=np.random.randn(100))
-            trace = pm.sample(100, cores=1)
+            trace = pm.sample(100, cores=1, return_inferencedata=False)
 
         samples = 500
         with pytest.raises(ValueError):
diff --git a/pymc3/tests/test_examples.py b/pymc3/tests/test_examples.py
index d79093b392..eb3d36a6bd 100644
--- a/pymc3/tests/test_examples.py
+++ b/pymc3/tests/test_examples.py
@@ -75,7 +75,7 @@ def build_model(self):
     def test_run(self):
         model = self.build_model()
         with model:
-            pm.sample(50, tune=50)
+            pm.sample(50, tune=50, return_inferencedata=False)
 
 
 class TestARM12_6(SeededTest):
@@ -112,7 +112,7 @@ def too_slow(self):
                 vars=[model["groupmean"], model["sd_interval__"], model["floor_m"]],
             )
             step = pm.NUTS(model.vars, scaling=start)
-            pm.sample(50, step=step, start=start)
+            pm.sample(50, step=step, start=start, return_inferencedata=False)
 
 
 class TestARM12_6Uranium(SeededTest):
@@ -159,7 +159,7 @@ def too_slow(self):
             h = np.diag(H(start))
 
             step = pm.HamiltonianMC(model.vars, h)
-            pm.sample(50, step=step, start=start)
+            pm.sample(50, step=step, start=start, return_inferencedata=False)
 
 
 def build_disaster_model(masked=False):
@@ -202,7 +202,9 @@ def test_disaster_model(self):
             start = {"early_mean": 2.0, "late_mean": 3.0}
             # Use slice sampler for means (other variables auto-selected)
             step = pm.Slice([model.early_mean_log__, model.late_mean_log__])
-            tr = pm.sample(500, tune=50, start=start, step=step, chains=2)
+            tr = pm.sample(
+                500, tune=50, start=start, step=step, chains=2, return_inferencedata=False
+            )
             az.summary(tr)
 
     def test_disaster_model_missing(self):
@@ -212,7 +214,9 @@ def test_disaster_model_missing(self):
             start = {"early_mean": 2.0, "late_mean": 3.0}
             # Use slice sampler for means (other variables auto-selected)
             step = pm.Slice([model.early_mean_log__, model.late_mean_log__])
-            tr = pm.sample(500, tune=50, start=start, step=step, chains=2)
+            tr = pm.sample(
+                500, tune=50, start=start, step=step, chains=2, return_inferencedata=False
+            )
             az.summary(tr)
 
 
@@ -231,7 +235,7 @@ def build_model(self):
     def test_run(self):
         with self.build_model():
             start = pm.find_MAP(method="Powell")
-            pm.sample(50, pm.Slice(), start=start)
+            pm.sample(50, pm.Slice(), start=start, return_inferencedata=False)
 
 
 class TestLatentOccupancy(SeededTest):
@@ -290,7 +294,9 @@ def test_run(self):
             }
             step_one = pm.Metropolis([model.theta_interval__, model.psi_logodds__])
             step_two = pm.BinaryMetropolis([model.z])
-            pm.sample(50, step=[step_one, step_two], start=start, chains=1)
+            pm.sample(
+                50, step=[step_one, step_two], start=start, chains=1, return_inferencedata=False
+            )
 
 
 @pytest.mark.xfail(
@@ -332,7 +338,7 @@ def build_model(self):
 
     def test_run(self):
         with self.build_model():
-            pm.sample(50, step=[pm.NUTS(), pm.Metropolis()])
+            pm.sample(50, step=[pm.NUTS(), pm.Metropolis()], return_inferencedata=False)
 
 
 class TestMultilevelNormal(SeededTest):
@@ -375,9 +381,9 @@ def test_run(self):
 
         with model:
             step = pm.MLDA(subsampling_rates=2, coarse_models=coarse_models)
-            pm.sample(draws=50, chains=2, tune=50, step=step)
+            pm.sample(draws=50, chains=2, tune=50, step=step, return_inferencedata=False)
 
             step = pm.MLDA(
                 subsampling_rates=2, coarse_models=coarse_models, base_sampler="Metropolis"
             )
-            pm.sample(draws=50, chains=2, tune=50, step=step)
+            pm.sample(draws=50, chains=2, tune=50, step=step, return_inferencedata=False)
diff --git a/pymc3/tests/test_hmc.py b/pymc3/tests/test_hmc.py
index 057c317075..5e372af5f8 100644
--- a/pymc3/tests/test_hmc.py
+++ b/pymc3/tests/test_hmc.py
@@ -53,7 +53,9 @@ def test_nuts_tuning():
     with model:
         pymc3.Normal("mu", mu=0, sigma=1)
         step = pymc3.NUTS()
-        trace = pymc3.sample(10, step=step, tune=5, progressbar=False, chains=1)
+        trace = pymc3.sample(
+            10, step=step, tune=5, progressbar=False, chains=1, return_inferencedata=False
+        )
 
     assert not step.tune
     assert np.all(trace["step_size"][5:] == trace["step_size"][5])
diff --git a/pymc3/tests/test_mixture.py b/pymc3/tests/test_mixture.py
index acee36acfa..dd14e0052c 100644
--- a/pymc3/tests/test_mixture.py
+++ b/pymc3/tests/test_mixture.py
@@ -375,7 +375,7 @@ def build_toy_dataset(N, K):
 
             pm.Mixture("x_obs", pi, comp_dist, observed=X)
         with model:
-            trace = pm.sample(30, tune=10, chains=1)
+            trace = pm.sample(30, tune=10, chains=1, return_inferencedata=False)
 
         n_samples = 20
         with model:
diff --git a/pymc3/tests/test_model.py b/pymc3/tests/test_model.py
index 2e5a83c1c3..9152f0174f 100644
--- a/pymc3/tests/test_model.py
+++ b/pymc3/tests/test_model.py
@@ -194,7 +194,13 @@ def test_matrix_multiplication():
         rv_det = pm.Deterministic("rv_det", matrix @ rv_rv)
         det_rv = pm.Deterministic("det_rv", rv_rv @ transformed)
 
-        posterior = pm.sample(10, tune=0, compute_convergence_checks=False, progressbar=False)
+        posterior = pm.sample(
+            10,
+            tune=0,
+            compute_convergence_checks=False,
+            progressbar=False,
+            return_inferencedata=False,
+        )
         decimal = select_by_precision(7, 5)
         for point in posterior.points():
             npt.assert_almost_equal(
diff --git a/pymc3/tests/test_ndarray_backend.py b/pymc3/tests/test_ndarray_backend.py
index f13a99e6f8..40a0d62d77 100644
--- a/pymc3/tests/test_ndarray_backend.py
+++ b/pymc3/tests/test_ndarray_backend.py
@@ -219,7 +219,7 @@ def model():
     @classmethod
     def setup_class(cls):
         with TestSaveLoad.model():
-            cls.trace = pm.sample()
+            cls.trace = pm.sample(return_inferencedata=False)
 
     def test_save_new_model(self, tmpdir_factory):
         directory = str(tmpdir_factory.mktemp("data"))
@@ -228,7 +228,7 @@ def test_save_new_model(self, tmpdir_factory):
         assert save_dir == directory
         with pm.Model() as model:
             w = pm.Normal("w", 0, 1)
-            new_trace = pm.sample()
+            new_trace = pm.sample(return_inferencedata=False)
 
         with pytest.raises(OSError):
             _ = pm.save_trace(new_trace, directory)
diff --git a/pymc3/tests/test_ode.py b/pymc3/tests/test_ode.py
index 1d336bfba6..799ed57186 100644
--- a/pymc3/tests/test_ode.py
+++ b/pymc3/tests/test_ode.py
@@ -286,7 +286,7 @@ def system(y, t, p):
             sigma = pm.HalfCauchy("sigma", 1)
             forward = ode_model(theta=[alpha], y0=[y0])
             y = pm.Lognormal("y", mu=pm.math.log(forward), sd=sigma, observed=yobs)
-            trace = pm.sample(100, tune=0, chains=1)
+            trace = pm.sample(100, tune=0, chains=1, return_inferencedata=False)
 
         assert trace["alpha"].size > 0
         assert trace["y0"].size > 0
@@ -316,7 +316,7 @@ def system(y, t, p):
             forward = ode_model(theta=[alpha, beta], y0=[y0])
             y = pm.Lognormal("y", mu=pm.math.log(forward), sd=sigma, observed=yobs)
 
-            trace = pm.sample(100, tune=0, chains=1)
+            trace = pm.sample(100, tune=0, chains=1, return_inferencedata=False)
 
         assert trace["alpha"].size > 0
         assert trace["beta"].size > 0
@@ -357,7 +357,7 @@ def system(y, t, p):
             forward = ode_model(theta=[R], y0=[0.99, 0.01])
             y = pm.Lognormal("y", mu=pm.math.log(forward), sd=sigma, observed=yobs)
 
-            trace = pm.sample(100, tune=0, chains=1)
+            trace = pm.sample(100, tune=0, chains=1, return_inferencedata=False)
 
         assert trace["R"].size > 0
         assert trace["sigma"].size > 0
@@ -397,7 +397,7 @@ def system(y, t, p):
             forward = ode_model(theta=[beta, gamma], y0=[0.99, 0.01])
             y = pm.Lognormal("y", mu=pm.math.log(forward), sd=sigma, observed=yobs)
 
-            trace = pm.sample(100, tune=0, chains=1)
+            trace = pm.sample(100, tune=0, chains=1, return_inferencedata=False)
 
         assert trace["beta"].size > 0
         assert trace["gamma"].size > 0
diff --git a/pymc3/tests/test_parallel_sampling.py b/pymc3/tests/test_parallel_sampling.py
index e458c609a8..0b534a8871 100644
--- a/pymc3/tests/test_parallel_sampling.py
+++ b/pymc3/tests/test_parallel_sampling.py
@@ -29,7 +29,7 @@ def test_context():
     with pm.Model():
         pm.Normal("x")
         ctx = multiprocessing.get_context("spawn")
-        pm.sample(tune=2, draws=2, chains=2, cores=2, mp_ctx=ctx)
+        pm.sample(tune=2, draws=2, chains=2, cores=2, mp_ctx=ctx, return_inferencedata=False)
 
 
 class NoUnpickle:
@@ -56,6 +56,7 @@ def test_bad_unpickle():
                 cores=2,
                 chains=2,
                 compute_convergence_checks=False,
+                return_inferencedata=False,
             )
         assert "could not be unpickled" in str(exc_info.getrepr(style="short"))
 
@@ -73,7 +74,15 @@ def _crash_remote_process(a, master_pid):
 def test_dill():
     with pm.Model():
         pm.Normal("x")
-        pm.sample(tune=1, draws=1, chains=2, cores=2, pickle_backend="dill", mp_ctx="spawn")
+        pm.sample(
+            tune=1,
+            draws=1,
+            chains=2,
+            cores=2,
+            pickle_backend="dill",
+            mp_ctx="spawn",
+            return_inferencedata=False,
+        )
 
 
 def test_remote_pipe_closed():
@@ -85,7 +94,15 @@ def test_remote_pipe_closed():
 
         step = pm.Metropolis()
         with pytest.raises(RuntimeError, match="Chain [0-9] failed"):
-            pm.sample(step=step, mp_ctx="spawn", tune=2, draws=2, cores=2, chains=2)
+            pm.sample(
+                step=step,
+                mp_ctx="spawn",
+                tune=2,
+                draws=2,
+                cores=2,
+                chains=2,
+                return_inferencedata=False,
+            )
 
 
 def test_abort():
@@ -172,7 +189,14 @@ def func(x):
             return -2 * (x ** 2).sum()
 
         obs = pm.DensityDist("density_dist", func, observed=np.random.randn(100))
-        trace = pm.sample(draws=10, tune=10, step=pm.Metropolis(), cores=2, mp_ctx="spawn")
+        trace = pm.sample(
+            draws=10,
+            tune=10,
+            step=pm.Metropolis(),
+            cores=2,
+            mp_ctx="spawn",
+            return_inferencedata=False,
+        )
 
 
 def test_spawn_densitydist_bound_method():
@@ -182,7 +206,14 @@ def test_spawn_densitydist_bound_method():
         obs = pm.DensityDist("density_dist", normal_dist.logp, observed=np.random.randn(100))
         msg = "logp for DensityDist is a bound method, leading to RecursionError while serializing"
         with pytest.raises(ValueError, match=msg):
-            trace = pm.sample(draws=10, tune=10, step=pm.Metropolis(), cores=2, mp_ctx="spawn")
+            trace = pm.sample(
+                draws=10,
+                tune=10,
+                step=pm.Metropolis(),
+                cores=2,
+                mp_ctx="spawn",
+                return_inferencedata=False,
+            )
 
 
 def test_spawn_densitydist_syswarning(monkeypatch):
diff --git a/pymc3/tests/test_posterior_predictive.py b/pymc3/tests/test_posterior_predictive.py
index 7a19ac4a59..8d2270bb4c 100644
--- a/pymc3/tests/test_posterior_predictive.py
+++ b/pymc3/tests/test_posterior_predictive.py
@@ -20,7 +20,7 @@ def test_build_TraceDict():
     with pm.Model() as model:
         mu = pm.Normal("mu", 0.0, 1.0)
         a = pm.Normal("a", mu=mu, sigma=1, observed=np.array([0.5, 0.2]))
-        trace = pm.sample(chains=2, draws=500)
+        trace = pm.sample(chains=2, draws=500, return_inferencedata=False)
         dict = _TraceDict(multi_trace=trace)
         assert isinstance(dict, _TraceDict)
         assert len(dict) == 1000
diff --git a/pymc3/tests/test_quadpotential.py b/pymc3/tests/test_quadpotential.py
index d91a80b5e9..0a5f49cd70 100644
--- a/pymc3/tests/test_quadpotential.py
+++ b/pymc3/tests/test_quadpotential.py
@@ -150,7 +150,7 @@ def energy(self, x, velocity=None):
     pot = Potential(floatX([1]))
     with model:
         step = pymc3.NUTS(potential=pot)
-        pymc3.sample(10, init=None, step=step, chains=1)
+        pymc3.sample(10, init=None, step=step, chains=1, return_inferencedata=False)
     assert called
 
 
@@ -275,10 +275,20 @@ def test_full_adapt_sampling(seed=289586):
 
         pot = quadpotential.QuadPotentialFullAdapt(model.ndim, np.zeros(model.ndim))
         step = pymc3.NUTS(model=model, potential=pot)
-        pymc3.sample(draws=10, tune=1000, random_seed=seed, step=step, cores=1, chains=1)
+        pymc3.sample(
+            draws=10,
+            tune=1000,
+            random_seed=seed,
+            step=step,
+            cores=1,
+            chains=1,
+            return_inferencedata=False,
+        )
 
 
 def test_issue_3965():
     with pymc3.Model():
         pymc3.Normal("n")
-        pymc3.sample(100, tune=300, chains=1, init="advi+adapt_diag_grad")
+        pymc3.sample(
+            100, tune=300, chains=1, init="advi+adapt_diag_grad", return_inferencedata=False
+        )
diff --git a/pymc3/tests/test_sampling.py b/pymc3/tests/test_sampling.py
index f3f2872c44..38dcb8c580 100644
--- a/pymc3/tests/test_sampling.py
+++ b/pymc3/tests/test_sampling.py
@@ -47,7 +47,7 @@ def test_sample_does_not_set_seed(self):
         for _ in range(2):
             np.random.seed(1)
             with self.model:
-                pm.sample(1, tune=0, chains=1)
+                pm.sample(1, tune=0, chains=1, return_inferencedata=False)
                 random_numbers.append(np.random.random())
         assert random_numbers[0] == random_numbers[1]
 
@@ -58,7 +58,7 @@ def test_parallel_sample_does_not_reuse_seed(self):
         for _ in range(2):
             np.random.seed(1)  # seeds in other processes don't effect main process
             with self.model:
-                trace = pm.sample(100, tune=0, cores=cores)
+                trace = pm.sample(100, tune=0, cores=cores, return_inferencedata=False)
             # numpy thread mentioned race condition.  might as well check none are equal
             for first, second in combinations(range(cores), 2):
                 first_chain = trace.get_values("x", chains=first)
@@ -82,6 +82,7 @@ def test_sample(self):
                         step=self.step,
                         cores=cores,
                         random_seed=self.random_seed,
+                        return_inferencedata=False,
                     )
 
     def test_sample_init(self):
@@ -93,20 +94,21 @@ def test_sample_init(self):
                     n_init=1000,
                     draws=50,
                     random_seed=self.random_seed,
+                    return_inferencedata=False,
                 )
 
     def test_sample_args(self):
         with self.model:
             with pytest.raises(ValueError) as excinfo:
-                pm.sample(50, tune=0, init=None, foo=1)
+                pm.sample(50, tune=0, init=None, foo=1, return_inferencedata=False)
             assert "'foo'" in str(excinfo.value)
 
             with pytest.raises(ValueError) as excinfo:
-                pm.sample(50, tune=0, init=None, foo={})
+                pm.sample(50, tune=0, init=None, foo={}, return_inferencedata=False)
             assert "foo" in str(excinfo.value)
 
             with pytest.raises(ValueError) as excinfo:
-                pm.sample(10, tune=0, init=None, target_accept=0.9)
+                pm.sample(10, tune=0, init=None, target_accept=0.9, return_inferencedata=False)
             assert "target_accept" in str(excinfo.value)
 
     def test_iter_sample(self):
@@ -136,11 +138,13 @@ def test_parallel_start(self):
 
     def test_sample_tune_len(self):
         with self.model:
-            trace = pm.sample(draws=100, tune=50, cores=1)
+            trace = pm.sample(draws=100, tune=50, cores=1, return_inferencedata=False)
             assert len(trace) == 100
-            trace = pm.sample(draws=100, tune=50, cores=1, discard_tuned_samples=False)
+            trace = pm.sample(
+                draws=100, tune=50, cores=1, discard_tuned_samples=False, return_inferencedata=False
+            )
             assert len(trace) == 150
-            trace = pm.sample(draws=100, tune=50, cores=4)
+            trace = pm.sample(draws=100, tune=50, cores=4, return_inferencedata=False)
             assert len(trace) == 100
 
     def test_reset_tuning(self):
@@ -148,7 +152,15 @@ def test_reset_tuning(self):
             tune = 50
             chains = 2
             start, step = pm.sampling.init_nuts(chains=chains)
-            pm.sample(draws=2, tune=tune, chains=chains, step=step, start=start, cores=1)
+            pm.sample(
+                draws=2,
+                tune=tune,
+                chains=chains,
+                step=step,
+                start=start,
+                cores=1,
+                return_inferencedata=False,
+            )
             assert step.potential._n_samples == tune
             assert step.step_adapt._count == tune + 1
 
@@ -159,7 +171,12 @@ def test_trace_report(self, step_cls, discard):
             # add more variables, because stats are 2D with CompoundStep!
             pm.Uniform("uni")
             trace = pm.sample(
-                draws=100, tune=50, cores=1, discard_tuned_samples=discard, step=step_cls()
+                draws=100,
+                tune=50,
+                cores=1,
+                discard_tuned_samples=discard,
+                step=step_cls(),
+                return_inferencedata=False,
             )
             assert trace.report.n_tune == 50
             assert trace.report.n_draws == 100
@@ -175,7 +192,7 @@ def test_trace_report_bart(self):
             mu = pm.BART("mu", X, Y, m=20)
             sigma = pm.HalfNormal("sigma", 1)
             y = pm.Normal("y", mu, sigma, observed=Y)
-            trace = pm.sample(500, tune=100, random_seed=3415)
+            trace = pm.sample(500, tune=100, random_seed=3415, return_inferencedata=False)
         var_imp = trace.report.variable_importance
         assert var_imp[0] > var_imp[1:].sum()
         npt.assert_almost_equal(var_imp.sum(), 1)
@@ -224,7 +241,12 @@ def test_return_inferencedata(self, monkeypatch):
     def test_sampler_stat_tune(self, cores):
         with self.model:
             tune_stat = pm.sample(
-                tune=5, draws=7, cores=cores, discard_tuned_samples=False, step=pm.Metropolis()
+                tune=5,
+                draws=7,
+                cores=cores,
+                discard_tuned_samples=False,
+                step=pm.Metropolis(),
+                return_inferencedata=False,
             ).get_sampler_stats("tune", chains=1)
             assert list(tune_stat).count(True) == 5
             assert list(tune_stat).count(False) == 7
@@ -262,6 +284,7 @@ def test_sample_callback(self):
                         cores=cores,
                         random_seed=self.random_seed,
                         callback=callback,
+                        return_inferencedata=False,
                     )
                     assert callback.called
 
@@ -281,6 +304,7 @@ def callback(trace, draw):
                 cores=1,
                 random_seed=self.random_seed,
                 callback=callback,
+                return_inferencedata=False,
             )
             assert len(trace) == trace_cancel_length
 
@@ -297,12 +321,26 @@ def test_sample_find_MAP_does_not_modify_start():
 
         # make sure sample does not modify the start dict
         start = {"untransformed": 0.2}
-        pm.sample(draws=10, step=pm.Metropolis(), tune=5, start=start, chains=3)
+        pm.sample(
+            draws=10,
+            step=pm.Metropolis(),
+            tune=5,
+            start=start,
+            chains=3,
+            return_inferencedata=False,
+        )
         assert start == {"untransformed": 0.2}
 
         # make sure sample does not modify the start when passes as list of dict
         start = [{"untransformed": 2}, {"untransformed": 0.2}]
-        pm.sample(draws=10, step=pm.Metropolis(), tune=5, start=start, chains=2)
+        pm.sample(
+            draws=10,
+            step=pm.Metropolis(),
+            tune=5,
+            start=start,
+            chains=2,
+            return_inferencedata=False,
+        )
         assert start == [{"untransformed": 2}, {"untransformed": 0.2}]
 
 
@@ -310,7 +348,7 @@ def test_empty_model():
     with pm.Model():
         pm.Normal("a", observed=1)
         with pytest.raises(ValueError) as error:
-            pm.sample()
+            pm.sample(return_inferencedata=False)
         error.match("any free variables")
 
 
@@ -318,7 +356,7 @@ def test_partial_trace_sample():
     with pm.Model() as model:
         a = pm.Normal("a", mu=0, sigma=1)
         b = pm.Normal("b", mu=0, sigma=1)
-        trace = pm.sample(trace=[a])
+        trace = pm.sample(trace=[a], return_inferencedata=False)
 
 
 @pytest.mark.parametrize(
@@ -420,7 +458,7 @@ def test_normal_scalar(self):
         with pm.Model() as model:
             mu = pm.Normal("mu", 0.0, 1.0)
             a = pm.Normal("a", mu=mu, sigma=1, observed=0.0)
-            trace = pm.sample(draws=ndraws, chains=nchains)
+            trace = pm.sample(draws=ndraws, chains=nchains, return_inferencedata=False)
 
         with model:
             # test list input
@@ -472,7 +510,7 @@ def test_normal_vector(self, caplog):
         with pm.Model() as model:
             mu = pm.Normal("mu", 0.0, 1.0)
             a = pm.Normal("a", mu=mu, sigma=1, observed=np.array([0.5, 0.2]))
-            trace = pm.sample()
+            trace = pm.sample(return_inferencedata=False)
 
         with model:
             # test list input
@@ -527,7 +565,7 @@ def test_exceptions(self, caplog):
         with pm.Model() as model:
             mu = pm.Normal("mu", 0.0, 1.0)
             a = pm.Normal("a", mu=mu, sigma=1, observed=np.array([0.5, 0.2]))
-            trace = pm.sample()
+            trace = pm.sample(return_inferencedata=False)
 
         with model:
             with pytest.raises(IncorrectArgumentsError):
@@ -549,7 +587,7 @@ def test_vector_observed(self):
         with pm.Model() as model:
             mu = pm.Normal("mu", mu=0, sigma=1)
             a = pm.Normal("a", mu=mu, sigma=1, observed=np.array([0.0, 1.0]))
-            trace = pm.sample()
+            trace = pm.sample(return_inferencedata=False)
 
         with model:
             # test list input
@@ -577,7 +615,7 @@ def test_sum_normal(self):
         with pm.Model() as model:
             a = pm.Normal("a", sigma=0.2)
             b = pm.Normal("b", mu=a)
-            trace = pm.sample()
+            trace = pm.sample(return_inferencedata=False)
 
         with model:
             # test list input
@@ -606,7 +644,7 @@ def test_model_not_drawable_prior(self):
         with model:
             mu = pm.HalfFlat("sigma")
             pm.Poisson("foo", mu=mu, observed=data)
-            trace = pm.sample(tune=1000)
+            trace = pm.sample(tune=1000, return_inferencedata=False)
 
         with model:
             with pytest.raises(ValueError) as excinfo:
@@ -628,7 +666,7 @@ def test_model_shared_variable(self):
             logistic = pm.Deterministic("p", pm.math.sigmoid(coeff * x_shared))
 
             obs = pm.Bernoulli("obs", p=logistic, observed=y_shared)
-            trace = pm.sample(100)
+            trace = pm.sample(100, return_inferencedata=False)
 
         x_shared.set_value([-1, 0, 1.0])
         y_shared.set_value([0, 0, 0])
@@ -669,7 +707,7 @@ def test_deterministic_of_observed(self):
             out_diff = in_1 + in_2
             pm.Deterministic("out", out_diff)
 
-            trace = pm.sample(100, chains=nchains)
+            trace = pm.sample(100, chains=nchains, return_inferencedata=False)
             np.random.seed(0)
             rtol = 1e-5 if theano.config.floatX == "float64" else 1e-4
 
@@ -707,7 +745,7 @@ def test_deterministic_of_observed_modified_interface(self):
             out_diff = in_1 + in_2
             pm.Deterministic("out", out_diff)
 
-            trace = pm.sample(100)
+            trace = pm.sample(100, return_inferencedata=False)
             ppc_trace = pm.trace_to_dataframe(
                 trace, varnames=[n for n in trace.varnames if n != "out"]
             ).to_dict("records")
@@ -736,7 +774,7 @@ def test_variable_type(self):
             mu = pm.HalfNormal("mu", 1)
             a = pm.Normal("a", mu=mu, sigma=2, observed=np.array([1, 2]))
             b = pm.Poisson("b", mu, observed=np.array([1, 2]))
-            trace = pm.sample()
+            trace = pm.sample(return_inferencedata=False)
 
         with model:
             ppc = pm.sample_posterior_predictive(trace, samples=1)
@@ -878,7 +916,7 @@ def _mocked_init_nuts(*args, **kwargs):
     with pm.Model() as m:
         x = pm.HalfNormal("x", transform=None)
         with expectation:
-            pm.sample(tune=1, draws=0, chains=1, init=init, start=start)
+            pm.sample(tune=1, draws=0, chains=1, init=init, start=start, return_inferencedata=False)
 
 
 @pytest.mark.parametrize(
@@ -907,7 +945,7 @@ def test_init_jitter(testval, jitter_max_retries, expectation):
 def point_list_arg_bug_fixture() -> Tuple[pm.Model, pm.backends.base.MultiTrace]:
     with pm.Model() as pmodel:
         n = pm.Normal("n")
-        trace = pm.sample()
+        trace = pm.sample(return_inferencedata=False)
 
     with pmodel:
         d = pm.Deterministic("d", n * 4)
@@ -959,7 +997,7 @@ def test_multivariate2(self):
         with pm.Model() as dm_model:
             probs = pm.Dirichlet("probs", a=np.ones(6), shape=6)
             obs = pm.Multinomial("obs", n=100, p=probs, observed=mn_data)
-            burned_trace = pm.sample(20, tune=10, cores=1)
+            burned_trace = pm.sample(20, tune=10, cores=1, return_inferencedata=False)
         sim_priors = pm.sample_prior_predictive(samples=20, model=dm_model)
         sim_ppc = pm.sample_posterior_predictive(burned_trace, samples=20, model=dm_model)
         assert sim_priors["probs"].shape == (20, 6)
diff --git a/pymc3/tests/test_shared.py b/pymc3/tests/test_shared.py
index 723216362f..d88ad743f4 100644
--- a/pymc3/tests/test_shared.py
+++ b/pymc3/tests/test_shared.py
@@ -41,7 +41,7 @@ def test_sample(self):
             pm.Normal("obs", b * x_shared, np.sqrt(1e-2), observed=y)
             prior_trace0 = pm.sample_prior_predictive(1000)
 
-            trace = pm.sample(1000, init=None, tune=1000, chains=1)
+            trace = pm.sample(1000, init=None, tune=1000, chains=1, return_inferencedata=False)
             pp_trace0 = pm.sample_posterior_predictive(trace, 1000)
             pp_trace01 = pm.fast_sample_posterior_predictive(trace, 1000)
 
diff --git a/pymc3/tests/test_theanof.py b/pymc3/tests/test_theanof.py
index bf2ff61fce..b38afbdf7d 100644
--- a/pymc3/tests/test_theanof.py
+++ b/pymc3/tests/test_theanof.py
@@ -48,7 +48,14 @@ def test_metropolis_sampling(self):
 
             step = pm.Metropolis()
             # This should fail immediately if broadcasting does not work.
-            pm.sample(tune=5, draws=7, cores=1, step=step, compute_convergence_checks=False)
+            pm.sample(
+                tune=5,
+                draws=7,
+                cores=1,
+                step=step,
+                compute_convergence_checks=False,
+                return_inferencedata=False,
+            )
 
 
 def _make_along_axis_idx(arr_shape, indices, axis):
diff --git a/pymc3/tests/test_variational_inference.py b/pymc3/tests/test_variational_inference.py
index 1f1e035277..486db7d291 100644
--- a/pymc3/tests/test_variational_inference.py
+++ b/pymc3/tests/test_variational_inference.py
@@ -981,10 +981,10 @@ def test_var_replacement():
 def test_empirical_from_trace(another_simple_model):
     with another_simple_model:
         step = pm.Metropolis()
-        trace = pm.sample(100, step=step, chains=1, tune=0)
+        trace = pm.sample(100, step=step, chains=1, tune=0, return_inferencedata=False)
         emp = Empirical(trace)
         assert emp.histogram.shape[0].eval() == 100
-        trace = pm.sample(100, step=step, chains=4, tune=0)
+        trace = pm.sample(100, step=step, chains=4, tune=0, return_inferencedata=False)
         emp = Empirical(trace)
         assert emp.histogram.shape[0].eval() == 400
 

From e07a71a13535f9ca1715e20f473141ebc15f3415 Mon Sep 17 00:00:00 2001
From: Chris Fonnesbeck <fonnesbeck@gmail.com>
Date: Sun, 4 Jul 2021 23:28:39 -0500
Subject: [PATCH 25/48] Refactored pymc3_random_discrete to use histograms
 instead of raw counts (#4840)

* Refactored pymc3_random_discrete to use histograms instead of raw counts

* Run pre-commit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 pymc3/tests/test_distributions_random.py | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py
index 1214cb8d56..fff795c1fb 100644
--- a/pymc3/tests/test_distributions_random.py
+++ b/pymc3/tests/test_distributions_random.py
@@ -107,15 +107,12 @@ def pymc3_random_discrete(
             e = ref_rand(size=size, **pt)
             o = np.atleast_1d(o).flatten()
             e = np.atleast_1d(e).flatten()
-            observed = dict(zip(*np.unique(o, return_counts=True)))
-            expected = dict(zip(*np.unique(e, return_counts=True)))
-            for e in expected.keys():
-                expected[e] = (observed.get(e, 0), expected[e])
-            k = np.array([v for v in expected.values()])
-            if np.all(k[:, 0] == k[:, 1]):
+            observed, _ = np.histogram(o, bins=min(7, len(set(o))))
+            expected, _ = np.histogram(e, bins=min(7, len(set(o))))
+            if np.all(observed == expected):
                 p = 1.0
             else:
-                _, p = st.chisquare(k[:, 0], k[:, 1])
+                _, p = st.chisquare(observed + 1, expected + 1)
             f -= 1
         assert p > alpha, str(pt)
 

From 97adc6224925c2ff9ffecab18a710cfcbb808a01 Mon Sep 17 00:00:00 2001
From: Thomas Wiecki <thomas.wiecki@gmail.com>
Date: Tue, 9 Mar 2021 11:21:42 +0100
Subject: [PATCH 26/48] Remove float128 dtype

Backport of #4514.

Removes float128 dtype which does not exist on Windows and newer OSX versions and furthermore does not to be properly supported even if it does exist.
---
 RELEASE-NOTES.md                 |  3 ++-
 pymc3/distributions/dist_math.py |  9 +--------
 pymc3/tests/test_dist_math.py    | 12 ++----------
 3 files changed, 5 insertions(+), 19 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 6c47a4751f..242dad15d6 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -5,8 +5,9 @@
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
 + Fix bug in the computation of the log pseudolikelihood values (SMC-ABC). (see [#4672](https://github.com/pymc-devs/pymc3/pull/4672)).
-+ Fix `LKJCorr.random` method to work with `pm.sample_prior_predictive`. (see [#4780](https://github.com/pymc-devs/pymc3/pull/4780))
++ Fix `LKJCorr.random` method to work with `pm.sample_prior_predictive`. (see [#4780](https://github.com/pymc-devs/pymc3/pull/4780)).
 + Enable documentation generation via ReadTheDocs for upcoming v3 releases. (see [#4805](https://github.com/pymc-devs/pymc3/pull/4805)).
++ Remove `float128` dtype support (see [#4834](https://github.com/pymc-devs/pymc3/pull/4834)).
 
 ### New Features
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
diff --git a/pymc3/distributions/dist_math.py b/pymc3/distributions/dist_math.py
index 7087772227..e155ae32ea 100644
--- a/pymc3/distributions/dist_math.py
+++ b/pymc3/distributions/dist_math.py
@@ -1,4 +1,4 @@
-#   Copyright 2020 The PyMC Developers
+#   Copyright 2021 The PyMC Developers
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@
 
 @author: johnsalvatier
 """
-import platform
-
 import numpy as np
 import scipy.linalg
 import scipy.stats
@@ -44,11 +42,6 @@
     dtype: (np.nextafter(0, 1, dtype=dtype), np.nextafter(1, 0, dtype=dtype))
     for dtype in ["float16", "float32", "float64"]
 }
-if platform.system() in ["Linux", "Darwin"]:
-    _beta_clip_values["float128"] = (
-        np.nextafter(0, 1, dtype="float128"),
-        np.nextafter(1, 0, dtype="float128"),
-    )
 
 
 def bound(logp, *conditions, **kwargs):
diff --git a/pymc3/tests/test_dist_math.py b/pymc3/tests/test_dist_math.py
index de9bbd5b7e..adcc5cb953 100644
--- a/pymc3/tests/test_dist_math.py
+++ b/pymc3/tests/test_dist_math.py
@@ -1,4 +1,4 @@
-#   Copyright 2020 The PyMC Developers
+#   Copyright 2021 The PyMC Developers
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -10,9 +10,6 @@
 #   distributed under the License is distributed on an "AS IS" BASIS,
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
-#   limitations under the License.
-import sys
-
 import numpy as np
 import numpy.testing as npt
 import pytest
@@ -237,12 +234,7 @@ def test_grad(self):
         verify_grad(i0e, [[[0.5, -2.0]]])
 
 
-@pytest.mark.parametrize(
-    "dtype",
-    ["float16", "float32", "float64", "float128"]
-    if sys.platform != "win32"
-    else ["float16", "float32", "float64"],
-)
+@pytest.mark.parametrize("dtype", ["float16", "float32", "float64"])
 def test_clipped_beta_rvs(dtype):
     # Verify that the samples drawn from the beta distribution are never
     # equal to zero or one (issue #3898)

From 14b617564ebbe47a9292b95024d2bfddddc63947 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Mon, 5 Jul 2021 16:41:05 +0200
Subject: [PATCH 27/48] Expose `plot_gp_dist` at package level (#4835)

* Expose `plot_gp_dist` at package level

This is a super useful helper function that works for many things beyond Gaussian processes.

* Mention plot_gp_dist API shortcut

* Run pre-commit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 RELEASE-NOTES.md  | 1 +
 pymc3/__init__.py | 1 +
 2 files changed, 2 insertions(+)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 242dad15d6..03b4bcc0bf 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -12,6 +12,7 @@
 ### New Features
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
 [#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
++ `plot_gp_dist` is now available at the package level: `pm.plot_gp_dist` ([see #4835](https://github.com/pymc-devs/pymc3/pull/4835)).
 
 ## PyMC3 3.11.2 (14 March 2021)
 
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 671fcbe048..8987101fac 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -88,6 +88,7 @@ def _hotfix_theano_printing():
 from pymc3.distributions import transforms
 from pymc3.exceptions import *
 from pymc3.glm import *
+from pymc3.gp.util import plot_gp_dist
 from pymc3.math import (
     expand_packed_triangular,
     invlogit,

From 9cd79ba6ccbbfd5426d656c7c1710f015075a266 Mon Sep 17 00:00:00 2001
From: Sayam753 <sayamkumar753@yahoo.in>
Date: Sun, 11 Jul 2021 12:25:27 +0530
Subject: [PATCH 28/48] Fix param:type rendering in v3 docs

---
 docs/source/_static/main.css | 5 +++++
 docs/source/conf.py          | 1 +
 2 files changed, 6 insertions(+)
 create mode 100644 docs/source/_static/main.css

diff --git a/docs/source/_static/main.css b/docs/source/_static/main.css
new file mode 100644
index 0000000000..16fffc59dc
--- /dev/null
+++ b/docs/source/_static/main.css
@@ -0,0 +1,5 @@
+.classifier:before {
+    font-style: normal;
+    margin: 0.5em;
+    content: ":";
+}
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 6fc8780e8b..c799c5a977 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -332,6 +332,7 @@
 def setup(app):
     app.add_css_file("https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css")
     app.add_css_file("default.css")
+    app.add_css_file("main.css")
     dir_root = Path(__file__).parent
     for directory in html_static_path:
         os.makedirs(str(dir_root / directory), exist_ok=True)

From 10c914b92c649b018be09e6dab5a5bc905d4f41e Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Thu, 15 Jul 2021 10:39:35 +0200
Subject: [PATCH 29/48] Remove JAX sampling on v3 in favor of v4 (#4866)

* Remove JAX sampling in favor of v4

Closes #4865

* Use newer link

Co-authored-by: Ricardo Vieira <28983449+ricardoV94@users.noreply.github.com>

* Rephrase

Co-authored-by: Thomas Wiecki <thomas.wiecki@gmail.com>

Co-authored-by: Ricardo Vieira <28983449+ricardoV94@users.noreply.github.com>
Co-authored-by: Thomas Wiecki <thomas.wiecki@gmail.com>
---
 .github/workflows/jaxtests.yml         |  64 -------
 .github/workflows/pytest.yml           |   1 -
 README.rst                             |  10 +-
 RELEASE-NOTES.md                       |   1 +
 pymc3/sampling_jax.py                  | 225 -------------------------
 pymc3/tests/test_sampling_jax.py       |  19 ---
 scripts/check_all_tests_are_covered.py |   2 +-
 7 files changed, 6 insertions(+), 316 deletions(-)
 delete mode 100644 .github/workflows/jaxtests.yml
 delete mode 100644 pymc3/sampling_jax.py
 delete mode 100644 pymc3/tests/test_sampling_jax.py

diff --git a/.github/workflows/jaxtests.yml b/.github/workflows/jaxtests.yml
deleted file mode 100644
index 5bd89e3a27..0000000000
--- a/.github/workflows/jaxtests.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: jax-sampling
-
-on:
-  pull_request:
-  push:
-    branches: [v3]
-
-jobs:
-  pytest:
-    strategy:
-      matrix:
-        os: [ubuntu-latest]
-        floatx: [float64]
-        test-subset:
-          - pymc3/tests/test_sampling_jax.py
-      fail-fast: false
-    runs-on: ${{ matrix.os }}
-    env:
-      TEST_SUBSET: ${{ matrix.test-subset }}
-      THEANO_FLAGS: floatX=${{ matrix.floatx }},gcc__cxxflags='-march=native'
-    defaults:
-      run:
-        shell: bash -l {0}
-    steps:
-      - uses: actions/checkout@v2
-      - name: Cache conda
-        uses: actions/cache@v1
-        env:
-          # Increase this value to reset cache if environment-dev-py39.yml has not changed
-          CACHE_NUMBER: 0
-        with:
-          path: ~/conda_pkgs_dir
-          key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{
-            hashFiles('conda-envs/environment-dev-py39.yml') }}
-      - name: Cache multiple paths
-        uses: actions/cache@v2
-        env:
-          # Increase this value to reset cache if requirements.txt has not changed
-          CACHE_NUMBER: 0
-        with:
-          path: |
-            ~/.cache/pip
-            $RUNNER_TOOL_CACHE/Python/*
-            ~\AppData\Local\pip\Cache
-          key: ${{ runner.os }}-build-${{ matrix.python-version }}-${{
-            hashFiles('requirements.txt') }}
-      - uses: conda-incubator/setup-miniconda@v2
-        with:
-          activate-environment: pymc3-dev-py39
-          channel-priority: strict
-          environment-file: conda-envs/environment-dev-py39.yml
-          use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly!
-      - name: Install pymc3
-        run: |
-          conda activate pymc3-dev-py39
-          pip install -e .
-          python --version
-      - name: Install jax specific dependencies
-        run: |
-          conda activate pymc3-dev-py39
-          pip install numpyro tensorflow_probability
-      - name: Run tests
-        run: |
-          python -m pytest -vv --cov=pymc3 --cov-report=xml --cov-report term --durations=50 $TEST_SUBSET
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 338847271a..c578820dc9 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -27,7 +27,6 @@ jobs:
             --ignore=pymc3/tests/test_quadpotential.py
             --ignore=pymc3/tests/test_random.py
             --ignore=pymc3/tests/test_sampling.py
-            --ignore=pymc3/tests/test_sampling_jax.py
             --ignore=pymc3/tests/test_shape_handling.py
             --ignore=pymc3/tests/test_shared.py
             --ignore=pymc3/tests/test_smc.py
diff --git a/README.rst b/README.rst
index cc2c5fba08..db80a72117 100644
--- a/README.rst
+++ b/README.rst
@@ -19,13 +19,11 @@ The future of PyMC3 & Theano
 ============================
 
 There have been many questions and uncertainty around the future of PyMC3 since Theano
-stopped getting developed by the original authors, and we started experiments with PyMC4.
+stopped getting developed by the original authors, and we started experiments with a PyMC version based on tensorflow probability.
 
-We are happy to announce that PyMC3 on Theano (which we are `developing further <https://github.com/pymc-devs/Theano-PyMC>`__)
-with a new JAX backend is the future. PyMC4 will not be developed further.
-
-See the `full announcement <https://pymc-devs.medium.com/the-future-of-pymc3-or-theano-is-dead-long-live-theano-d8005f8a0e9b>`__
-for more details.
+Since then many things changed and we are happy to announce that PyMC3 will continue to rely on Theano,
+or rather its successors Theano-PyMC (`pymc3 <4)` and Aesara (`pymc3 >=4`).
+Check out <https://github.com/aesara-devs/aesara>`__) and specifically the latest developments on the `PyMC3 `main` branch <https://github.com/pymc-devs/pymc3/>`.
 
 Features
 ========
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 03b4bcc0bf..6223ad496f 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -13,6 +13,7 @@
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
 [#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
 + `plot_gp_dist` is now available at the package level: `pm.plot_gp_dist` ([see #4835](https://github.com/pymc-devs/pymc3/pull/4835)).
++ The experimental JAX sampling was removed. It will be included in the upcoming `v4` release ([see #4866](https://github.com/pymc-devs/pymc3/pull/4866)).
 
 ## PyMC3 3.11.2 (14 March 2021)
 
diff --git a/pymc3/sampling_jax.py b/pymc3/sampling_jax.py
deleted file mode 100644
index 522bca7b12..0000000000
--- a/pymc3/sampling_jax.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# pylint: skip-file
-import os
-import re
-import warnings
-
-from collections import defaultdict
-
-xla_flags = os.getenv("XLA_FLAGS", "").lstrip("--")
-xla_flags = re.sub(r"xla_force_host_platform_device_count=.+\s", "", xla_flags).split()
-os.environ["XLA_FLAGS"] = " ".join(["--xla_force_host_platform_device_count={}".format(100)])
-
-import arviz as az
-import jax
-import numpy as np
-import pandas as pd
-import theano.graph.fg
-
-from theano.link.jax.jax_dispatch import jax_funcify
-
-import pymc3 as pm
-
-from pymc3 import modelcontext
-
-warnings.warn("This module is experimental.")
-
-# Disable C compilation by default
-# theano.config.cxx = ""
-# This will make the JAX Linker the default
-# theano.config.mode = "JAX"
-
-
-def sample_tfp_nuts(
-    draws=1000,
-    tune=1000,
-    chains=4,
-    target_accept=0.8,
-    random_seed=10,
-    model=None,
-    num_tuning_epoch=2,
-    num_compute_step_size=500,
-):
-    import jax
-
-    from tensorflow_probability.substrates import jax as tfp
-
-    model = modelcontext(model)
-
-    seed = jax.random.PRNGKey(random_seed)
-
-    fgraph = theano.graph.fg.FunctionGraph(model.free_RVs, [model.logpt])
-    fns = jax_funcify(fgraph)
-    logp_fn_jax = fns[0]
-
-    rv_names = [rv.name for rv in model.free_RVs]
-    init_state = [model.test_point[rv_name] for rv_name in rv_names]
-    init_state_batched = jax.tree_map(lambda x: np.repeat(x[None, ...], chains, axis=0), init_state)
-
-    @jax.pmap
-    def _sample(init_state, seed):
-        def gen_kernel(step_size):
-            hmc = tfp.mcmc.NoUTurnSampler(target_log_prob_fn=logp_fn_jax, step_size=step_size)
-            return tfp.mcmc.DualAveragingStepSizeAdaptation(
-                hmc, tune // num_tuning_epoch, target_accept_prob=target_accept
-            )
-
-        def trace_fn(_, pkr):
-            return pkr.new_step_size
-
-        def get_tuned_stepsize(samples, step_size):
-            return step_size[-1] * jax.numpy.std(samples[-num_compute_step_size:])
-
-        step_size = jax.tree_map(jax.numpy.ones_like, init_state)
-        for i in range(num_tuning_epoch - 1):
-            tuning_hmc = gen_kernel(step_size)
-            init_samples, tuning_result, kernel_results = tfp.mcmc.sample_chain(
-                num_results=tune // num_tuning_epoch,
-                current_state=init_state,
-                kernel=tuning_hmc,
-                trace_fn=trace_fn,
-                return_final_kernel_results=True,
-                seed=seed,
-            )
-
-            step_size = jax.tree_multimap(get_tuned_stepsize, list(init_samples), tuning_result)
-            init_state = [x[-1] for x in init_samples]
-
-        # Run inference
-        sample_kernel = gen_kernel(step_size)
-        mcmc_samples, leapfrog_num = tfp.mcmc.sample_chain(
-            num_results=draws,
-            num_burnin_steps=tune // num_tuning_epoch,
-            current_state=init_state,
-            kernel=sample_kernel,
-            trace_fn=lambda _, pkr: pkr.inner_results.leapfrogs_taken,
-            seed=seed,
-        )
-
-        return mcmc_samples, leapfrog_num
-
-    print("Compiling...")
-    tic2 = pd.Timestamp.now()
-    map_seed = jax.random.split(seed, chains)
-    mcmc_samples, leapfrog_num = _sample(init_state_batched, map_seed)
-
-    # map_seed = jax.random.split(seed, chains)
-    # mcmc_samples = _sample(init_state_batched, map_seed)
-    # tic4 = pd.Timestamp.now()
-    # print("Sampling time = ", tic4 - tic3)
-
-    posterior = {k: v for k, v in zip(rv_names, mcmc_samples)}
-
-    az_trace = az.from_dict(posterior=posterior)
-    tic3 = pd.Timestamp.now()
-    print("Compilation + sampling time = ", tic3 - tic2)
-    return az_trace  # , leapfrog_num, tic3 - tic2
-
-
-def sample_numpyro_nuts(
-    draws=1000,
-    tune=1000,
-    chains=4,
-    target_accept=0.8,
-    random_seed=10,
-    model=None,
-    progress_bar=True,
-    keep_untransformed=False,
-):
-    from numpyro.infer import MCMC, NUTS
-
-    from pymc3 import modelcontext
-
-    model = modelcontext(model)
-
-    seed = jax.random.PRNGKey(random_seed)
-
-    fgraph = theano.graph.fg.FunctionGraph(model.free_RVs, [model.logpt])
-    fns = jax_funcify(fgraph)
-    logp_fn_jax = fns[0]
-
-    rv_names = [rv.name for rv in model.free_RVs]
-    init_state = [model.test_point[rv_name] for rv_name in rv_names]
-    init_state_batched = jax.tree_map(lambda x: np.repeat(x[None, ...], chains, axis=0), init_state)
-
-    @jax.jit
-    def _sample(current_state, seed):
-        step_size = jax.tree_map(jax.numpy.ones_like, init_state)
-        nuts_kernel = NUTS(
-            potential_fn=lambda x: -logp_fn_jax(*x),
-            # model=model,
-            target_accept_prob=target_accept,
-            adapt_step_size=True,
-            adapt_mass_matrix=True,
-            dense_mass=False,
-        )
-
-        pmap_numpyro = MCMC(
-            nuts_kernel,
-            num_warmup=tune,
-            num_samples=draws,
-            num_chains=chains,
-            postprocess_fn=None,
-            chain_method="parallel",
-            progress_bar=progress_bar,
-        )
-
-        pmap_numpyro.run(seed, init_params=current_state, extra_fields=("num_steps",))
-        samples = pmap_numpyro.get_samples(group_by_chain=True)
-        leapfrogs_taken = pmap_numpyro.get_extra_fields(group_by_chain=True)["num_steps"]
-        return samples, leapfrogs_taken
-
-    print("Compiling...")
-    tic2 = pd.Timestamp.now()
-    map_seed = jax.random.split(seed, chains)
-    mcmc_samples, leapfrogs_taken = _sample(init_state_batched, map_seed)
-    # map_seed = jax.random.split(seed, chains)
-    # mcmc_samples = _sample(init_state_batched, map_seed)
-    # tic4 = pd.Timestamp.now()
-    # print("Sampling time = ", tic4 - tic3)
-
-    posterior = {k: v for k, v in zip(rv_names, mcmc_samples)}
-    tic3 = pd.Timestamp.now()
-    posterior = _transform_samples(posterior, model, keep_untransformed=keep_untransformed)
-    tic4 = pd.Timestamp.now()
-
-    az_trace = az.from_dict(posterior=posterior)
-    print("Compilation + sampling time = ", tic3 - tic2)
-    print("Transformation time = ", tic4 - tic3)
-
-    return az_trace  # , leapfrogs_taken, tic3 - tic2
-
-
-def _transform_samples(samples, model, keep_untransformed=False):
-
-    # Find out which RVs we need to compute:
-    free_rv_names = {x.name for x in model.free_RVs}
-    unobserved_names = {x.name for x in model.unobserved_RVs}
-
-    names_to_compute = unobserved_names - free_rv_names
-    ops_to_compute = [x for x in model.unobserved_RVs if x.name in names_to_compute]
-
-    # Create function graph for these:
-    fgraph = theano.graph.fg.FunctionGraph(model.free_RVs, ops_to_compute)
-
-    # Jaxify, which returns a list of functions, one for each op
-    jax_fns = jax_funcify(fgraph)
-
-    # Put together the inputs
-    inputs = [samples[x.name] for x in model.free_RVs]
-
-    for cur_op, cur_jax_fn in zip(ops_to_compute, jax_fns):
-
-        # We need a function taking a single argument to run vmap, while the
-        # jax_fn takes a list, so:
-        result = jax.vmap(jax.vmap(cur_jax_fn))(*inputs)
-
-        # Add to sample dict
-        samples[cur_op.name] = result
-
-    # Discard unwanted transformed variables, if desired:
-    vars_to_keep = set(
-        pm.util.get_default_varnames(list(samples.keys()), include_transformed=keep_untransformed)
-    )
-    samples = {x: y for x, y in samples.items() if x in vars_to_keep}
-
-    return samples
diff --git a/pymc3/tests/test_sampling_jax.py b/pymc3/tests/test_sampling_jax.py
deleted file mode 100644
index 46a406833c..0000000000
--- a/pymc3/tests/test_sampling_jax.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import numpy as np
-
-import pymc3 as pm
-
-from pymc3.sampling_jax import sample_numpyro_nuts
-
-
-def test_transform_samples():
-
-    with pm.Model() as model:
-
-        sigma = pm.HalfNormal("sigma")
-        b = pm.Normal("b", sigma=sigma)
-        trace = sample_numpyro_nuts(keep_untransformed=True)
-
-    log_vals = trace.posterior["sigma_log__"].values
-    trans_vals = trace.posterior["sigma"].values
-
-    assert np.allclose(np.exp(log_vals), trans_vals)
diff --git a/scripts/check_all_tests_are_covered.py b/scripts/check_all_tests_are_covered.py
index f02f90d509..058a867d02 100644
--- a/scripts/check_all_tests_are_covered.py
+++ b/scripts/check_all_tests_are_covered.py
@@ -12,7 +12,7 @@
 from pathlib import Path
 
 if __name__ == "__main__":
-    testing_workflows = ["jaxtests.yml", "pytest.yml"]
+    testing_workflows = ["pytest.yml"]
     ignored = set()
     non_ignored = set()
     for wfyml in testing_workflows:

From 791c53c46ad4f5d541025216d4b36a06e95f3063 Mon Sep 17 00:00:00 2001
From: Sayam Kumar <sayamkumar753@yahoo.in>
Date: Sat, 14 Aug 2021 17:49:05 +0530
Subject: [PATCH 30/48] Use `to_tuple` function in
 pm.fast_sample_posterior_predictive (#4927)

Closes #4854
---
 RELEASE-NOTES.md                            |  1 +
 pymc3/distributions/posterior_predictive.py |  3 ++-
 pymc3/tests/test_posterior_predictive.py    | 23 +++++++++++++++++++++
 3 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 6223ad496f..cb3b995b67 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -8,6 +8,7 @@
 + Fix `LKJCorr.random` method to work with `pm.sample_prior_predictive`. (see [#4780](https://github.com/pymc-devs/pymc3/pull/4780)).
 + Enable documentation generation via ReadTheDocs for upcoming v3 releases. (see [#4805](https://github.com/pymc-devs/pymc3/pull/4805)).
 + Remove `float128` dtype support (see [#4834](https://github.com/pymc-devs/pymc3/pull/4834)).
++ Use `to_tuple` function in `pm.fast_sample_posterior_predictive` to pass shape assertions (see [#4927](https://github.com/pymc-devs/pymc3/pull/4927)).
 
 ### New Features
 + Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
diff --git a/pymc3/distributions/posterior_predictive.py b/pymc3/distributions/posterior_predictive.py
index 31aa3e40f5..470f0f02a7 100644
--- a/pymc3/distributions/posterior_predictive.py
+++ b/pymc3/distributions/posterior_predictive.py
@@ -26,6 +26,7 @@
     is_fast_drawable,
     vectorized_ppc,
 )
+from pymc3.distributions.shape_utils import to_tuple
 from pymc3.exceptions import IncorrectArgumentsError
 from pymc3.model import (
     Model,
@@ -551,7 +552,7 @@ def random_sample(
         ) -> np.ndarray:
             val = meth(point=point, size=size)
             try:
-                assert val.shape == (size,) + shape, (
+                assert val.shape == to_tuple(size) + to_tuple(shape), (
                     "Sampling from random of %s yields wrong shape" % param
                 )
             # error-quashing here is *extremely* ugly, but it seems to be what the logic in DensityDist wants.
diff --git a/pymc3/tests/test_posterior_predictive.py b/pymc3/tests/test_posterior_predictive.py
index 8d2270bb4c..84b5038935 100644
--- a/pymc3/tests/test_posterior_predictive.py
+++ b/pymc3/tests/test_posterior_predictive.py
@@ -37,3 +37,26 @@ def test_build_TraceDict_point_list():
         assert len(dict) == 1
         assert len(dict["mu"]) == 1
         assert dict["mu"][0] == 0.0
+
+
+def test_fast_sample_posterior_predictive_shape_assertions():
+    """
+    This test checks the shape assertions in pm.fast_sample_posterior_predictive.
+    Originally reported - https://github.com/pymc-devs/pymc3/issues/4778
+    """
+    with pm.Model():
+        p = pm.Beta("p", 2, 2)
+        trace = pm.sample(
+            tune=30, draws=50, chains=1, return_inferencedata=True, compute_convergence_checks=False
+        )
+
+    with pm.Model() as m_forward:
+        p = pm.Beta("p", 2, 2)
+        b2 = pm.Binomial("b2", n=1, p=p)
+        b3 = pm.Binomial("b3", n=1, p=p * b2)
+
+    with m_forward:
+        trace_forward = pm.fast_sample_posterior_predictive(trace, var_names=["p", "b2", "b3"])
+
+    for free_rv in trace_forward.values():
+        assert free_rv.shape[0] == 50

From 03be0d804d612bc88df77be0a82484be12fb3762 Mon Sep 17 00:00:00 2001
From: Ravin Kumar <ravinsdrive@gmail.com>
Date: Thu, 19 Aug 2021 17:43:56 -0700
Subject: [PATCH 31/48] Release PyMC3 v3.11.3 (#4941)

* Release PyMC3 v3.11.3

* Update RELEASE-NOTES.md
---
 RELEASE-NOTES.md  | 8 ++++++--
 pymc3/__init__.py | 2 +-
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index cb3b995b67..a5e2f11141 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,6 +1,6 @@
 # Release Notes
 
-## PyMC3 3.11.3 (TBD)
+## PyMC3 3.11.3 (19 August 2021)
 ### Maintenance
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
@@ -15,6 +15,11 @@
 [#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
 + `plot_gp_dist` is now available at the package level: `pm.plot_gp_dist` ([see #4835](https://github.com/pymc-devs/pymc3/pull/4835)).
 + The experimental JAX sampling was removed. It will be included in the upcoming `v4` release ([see #4866](https://github.com/pymc-devs/pymc3/pull/4866)).
++ `pm.intX` no longer downcasts integers unnecessarily (see [#4569](https://github.com/pymc-devs/pymc3/pull/4569))
+
+
+**Release manager** for 3.11.3: Ravin Kumar ([@canyon289](https://github.com/canyon289))
+
 
 ## PyMC3 3.11.2 (14 March 2021)
 
@@ -35,7 +40,6 @@
 - `pm.make_shared_replacements` now retains broadcasting information which fixes issues with Metropolis samplers (see [#4492](https://github.com/pymc-devs/pymc3/pull/4492)).
 
 **Release manager** for 3.11.2: Michael Osthege ([@michaelosthege](https://github.com/michaelosthege))
-- `pm.intX` no longer downcasts integers unnecessarily (see [#4569](https://github.com/pymc-devs/pymc3/pull/4569))
 
 ## PyMC3 3.11.1 (12 February 2021)
 
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 8987101fac..82f2cb1900 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -13,7 +13,7 @@
 #   limitations under the License.
 
 # pylint: disable=wildcard-import
-__version__ = "3.11.2"
+__version__ = "3.11.3"
 
 import logging
 import multiprocessing as mp

From 94d892c377d89fda4597bfee8fd0c6a3d783b2a9 Mon Sep 17 00:00:00 2001
From: Sayam Kumar <sayamkumar753@yahoo.in>
Date: Sun, 22 Aug 2021 16:52:32 +0530
Subject: [PATCH 32/48] Fixup broken doc links (#4958)

---
 docs/source/index.rst     | 10 ----------
 docs/source/learn.rst     |  2 +-
 docs/source/pymc-examples |  2 +-
 3 files changed, 2 insertions(+), 12 deletions(-)

diff --git a/docs/source/index.rst b/docs/source/index.rst
index 205f8ee902..acf33506dc 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -134,16 +134,6 @@
                         <img class="ui image" height="120" src="https://www.numfocus.org/wp-content/uploads/2017/07/NumFocus_LRG.png"/>
                     </a>
                 </div>
-                <div class="column">
-                    <a href="https://quantopian.com">
-                        <img class="ui image" height="120" src="https://raw.githubusercontent.com/pymc-devs/pymc3/master/docs/quantopianlogo.jpg"/>
-                    </a>
-                </div>
-                <div class="column">
-                    <a href="https://odsc.com/">
-                        <img class="ui image" height="120" src="https://raw.githubusercontent.com/pymc-devs/pymc3/master/docs/odsc_logo.png"/>
-                    </a>
-                </div>
             </div>
         </div>
     </div>
diff --git a/docs/source/learn.rst b/docs/source/learn.rst
index 9d0d60d8bc..b8bcaee389 100644
--- a/docs/source/learn.rst
+++ b/docs/source/learn.rst
@@ -104,7 +104,7 @@
 
         <div class="card">
             <div class="image">
-                <img src="https://lh5.googleusercontent.com/Ms2ssellxl7cM6OEL_kpiKRojcj2E4ZaUWDXOa8zEwi-v9orJGYuhjczbwFSDJNsEb_ruiwtCJONNjoo7T1c7qorZm3LsAnroMAm4S5WzNT_PVqWz9aE=w1280">
+                <img src="https://jkkweb.sitehost.iu.edu/DoingBayesianDataAnalysis/DBDA2Ecover.png">
             </div>
             <div class="content">
                 <div class="header">Doing Bayesian Data Analysis</div>
diff --git a/docs/source/pymc-examples b/docs/source/pymc-examples
index 0bbf199c46..411c286fd9 160000
--- a/docs/source/pymc-examples
+++ b/docs/source/pymc-examples
@@ -1 +1 @@
-Subproject commit 0bbf199c462a65e3c9a7b5155f7b47c69eae2581
+Subproject commit 411c286fd93799dd887b9b9e5c9b5f88607e8807

From bfc3813367592e62cc25be5abf0484d417972d84 Mon Sep 17 00:00:00 2001
From: Ravin Kumar <ravinsdrive@gmail.com>
Date: Fri, 20 Aug 2021 09:19:55 -0700
Subject: [PATCH 33/48] Update __init__.py

Update RELEASE-NOTES.md

Mark 3.11.3 release as broken per discussion
---
 RELEASE-NOTES.md  | 22 ++++++++++++++--------
 pymc3/__init__.py |  2 +-
 2 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index a5e2f11141..7f67343eea 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,6 +1,14 @@
 # Release Notes
 
-## PyMC3 3.11.3 (19 August 2021)
+## PyMC3 3.11.4 (20 August 2021)
+
+### New Features
++ Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
+[#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
++ `plot_gp_dist` is now available at the package level: `pm.plot_gp_dist` ([see #4835](https://github.com/pymc-devs/pymc3/pull/4835)).
++ The experimental JAX sampling was removed. It will be included in the upcoming `v4` release ([see #4866](https://github.com/pymc-devs/pymc3/pull/4866)).
++ `pm.intX` no longer downcasts integers unnecessarily (see [#4569](https://github.com/pymc-devs/pymc3/pull/4569))
+
 ### Maintenance
 + A deprecation warning from the `semver` package we use for checking backend compatibility was dealt with (see [#4547](https://github.com/pymc-devs/pymc3/pull/4547)).
 + `theano.printing.pydotprint` is now hotfixed upon import (see [#4594](https://github.com/pymc-devs/pymc3/pull/4594)).
@@ -10,16 +18,14 @@
 + Remove `float128` dtype support (see [#4834](https://github.com/pymc-devs/pymc3/pull/4834)).
 + Use `to_tuple` function in `pm.fast_sample_posterior_predictive` to pass shape assertions (see [#4927](https://github.com/pymc-devs/pymc3/pull/4927)).
 
-### New Features
-+ Generalized BART, bounded distributions like Binomial and Poisson can now be used as likelihoods (see [#4675](https://github.com/pymc-devs/pymc3/pull/4675), [#4709](https://github.com/pymc-devs/pymc3/pull/4709) and
-[#4720](https://github.com/pymc-devs/pymc3/pull/4720)).
-+ `plot_gp_dist` is now available at the package level: `pm.plot_gp_dist` ([see #4835](https://github.com/pymc-devs/pymc3/pull/4835)).
-+ The experimental JAX sampling was removed. It will be included in the upcoming `v4` release ([see #4866](https://github.com/pymc-devs/pymc3/pull/4866)).
-+ `pm.intX` no longer downcasts integers unnecessarily (see [#4569](https://github.com/pymc-devs/pymc3/pull/4569))
+**Release manager** for 3.11.4: Ravin Kumar ([@canyon289](https://github.com/canyon289))
 
+## PyMC3 3.11.3 (19 August 2021)
 
-**Release manager** for 3.11.3: Ravin Kumar ([@canyon289](https://github.com/canyon289))
+### Maintenance
+* Broken release. Refer to 3.11.4
 
+**Release manager** for 3.11.3: Ravin Kumar ([@canyon289](https://github.com/canyon289))
 
 ## PyMC3 3.11.2 (14 March 2021)
 
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 82f2cb1900..684efd5c71 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -13,7 +13,7 @@
 #   limitations under the License.
 
 # pylint: disable=wildcard-import
-__version__ = "3.11.3"
+__version__ = "3.11.4"
 
 import logging
 import multiprocessing as mp

From 916c068cbe6dd4f600db25d4e38db9aa214a4692 Mon Sep 17 00:00:00 2001
From: Sayam Kumar <sayamkumar753@yahoo.in>
Date: Sat, 16 Oct 2021 14:19:50 +0530
Subject: [PATCH 34/48] Fix logo urls on v3 (#5081)

---
 README.rst                              | 2 +-
 docs/source/semantic_sphinx/layout.html | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.rst b/README.rst
index db80a72117..b09e641106 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,4 @@
-.. image:: https://cdn.rawgit.com/pymc-devs/pymc3/master/docs/logos/svg/PyMC3_banner.svg
+.. image:: https://cdn.rawgit.com/pymc-devs/pymc/v3/docs/logos/svg/PyMC3_banner.svg
     :height: 100px
     :alt: PyMC3 logo
     :align: center
diff --git a/docs/source/semantic_sphinx/layout.html b/docs/source/semantic_sphinx/layout.html
index 25d0c62d69..5ae7732a2c 100644
--- a/docs/source/semantic_sphinx/layout.html
+++ b/docs/source/semantic_sphinx/layout.html
@@ -54,7 +54,7 @@
     <div class="ui container">
         <div class="ui large secondary pointing menu">
             <a class="item" href="/">
-                <img class="ui bottom aligned tiny image" src="https://cdn.rawgit.com/pymc-devs/pymc3/master/docs/logos/svg/PyMC3_banner.svg" />
+                <img class="ui bottom aligned tiny image" src="https://cdn.rawgit.com/pymc-devs/pymc/v3/docs/logos/svg/PyMC3_banner.svg" />
             </a>
             {% if
             theme_navbar_links %} {%- for link in theme_navbar_links %} <a href="{{ pathto(*link[1:]) }}" class="item">{{
@@ -74,7 +74,7 @@
     </div>
     {% if pagename == 'index' %}
     <div class="ui center aligned text container">
-        <img src="https://cdn.rawgit.com/pymc-devs/pymc3/master/docs/logos/svg/PyMC3_banner.svg" />
+        <img src="https://cdn.rawgit.com/pymc-devs/pymc/v3/docs/logos/svg/PyMC3_banner.svg" />
         <h2>Probabilistic Programming in Python</h2>
         <a href="pymc-examples/examples/pymc3_howto/api_quickstart.html">
             <div class="ui huge primary button">Quickstart <i class="right arrow icon"></i></div>

From fc1acebd0b6bd48a6e129c990f5615d5f06fd3f5 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 17 Oct 2021 18:29:13 +0200
Subject: [PATCH 35/48] Add backports module and backport logp syntax (#5083)

* Add backports module and backport logp syntax

Closes #5052

Co-authored-by: Ravin Kumar <ravinsdrive@gmail.com>
---
 .github/workflows/pytest.yml    |  2 ++
 RELEASE-NOTES.md                |  4 +++
 docs/source/developer_guide.rst |  2 +-
 pymc3/__init__.py               |  1 +
 pymc3/backports.py              | 43 +++++++++++++++++++++++++++++++++
 pymc3/tests/test_backports.py   | 32 ++++++++++++++++++++++++
 6 files changed, 83 insertions(+), 1 deletion(-)
 create mode 100644 pymc3/backports.py
 create mode 100644 pymc3/tests/test_backports.py

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index c578820dc9..9f8b5f82e0 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -13,6 +13,7 @@ jobs:
         floatx: [float32, float64]
         test-subset:
           - |
+            --ignore=pymc3/tests/test_backports.py
             --ignore=pymc3/tests/test_dist_math.py
             --ignore=pymc3/tests/test_distribution_defaults.py
             --ignore=pymc3/tests/test_distributions.py
@@ -42,6 +43,7 @@ jobs:
             pymc3/tests/test_shared.py
             pymc3/tests/test_smc.py
           - |
+            pymc3/tests/test_backports.py
             pymc3/tests/test_examples.py
             pymc3/tests/test_mixture.py
             pymc3/tests/test_ode.py
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 7f67343eea..98b349cc03 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,5 +1,9 @@
 # Release Notes
 
+## PyMC 3.11.5 (TBD)
+### Backports
++ The `pm.logp(rv, x)` syntax is now available and recommended to make your model code `v4`-ready. Note that this backport is just an alias and much less capable than what's available with `pymc >=4` (see [#5083](https://github.com/pymc-devs/pymc/pulls/5083)).
+
 ## PyMC3 3.11.4 (20 August 2021)
 
 ### New Features
diff --git a/docs/source/developer_guide.rst b/docs/source/developer_guide.rst
index 64463cd5b4..5c2c89e663 100644
--- a/docs/source/developer_guide.rst
+++ b/docs/source/developer_guide.rst
@@ -158,7 +158,7 @@ explicit about the conversion. For example:
     with pm.Model() as model:
         z = pm.Normal('z', mu=0., sigma=5.)             # ==> pymc3.model.FreeRV, or theano.tensor with logp
         x = pm.Normal('x', mu=z, sigma=1., observed=5.) # ==> pymc3.model.ObservedRV, also has logp properties
-    x.logp({'z': 2.5})                                  # ==> -4.0439386
+    pm.logp(x, {'z': 2.5})                              # ==> -4.0439386
     model.logp({'z': 2.5})                              # ==> -6.6973152
 
 **TFP**
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 684efd5c71..131bd5b394 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -82,6 +82,7 @@ def _hotfix_theano_printing():
 from pymc3 import gp, ode, sampling
 from pymc3.backends import load_trace, save_trace
 from pymc3.backends.tracetab import *
+from pymc3.backports import logp
 from pymc3.blocking import *
 from pymc3.data import *
 from pymc3.distributions import *
diff --git a/pymc3/backports.py b/pymc3/backports.py
new file mode 100644
index 0000000000..cc56f5b332
--- /dev/null
+++ b/pymc3/backports.py
@@ -0,0 +1,43 @@
+#   Copyright 2021 The PyMC Developers
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from typing import Union
+
+import numpy as np
+
+from theano.tensor import TensorVariable
+
+from pymc3.distributions.distribution import Distribution
+from pymc3.model import Factor
+
+
+def logp(
+    rv: Union[Factor, Distribution], value: Union[TensorVariable, np.ndarray]
+) -> Union[TensorVariable, np.ndarray]:
+    """
+    Calculate log-probability of a distribution at specified value.
+
+    This function is a limited functionality backported version of PyMC >=4.0 like capabilities.
+
+    Parameters
+    ----------
+    value : numeric
+        Value(s) for which log-probability is calculated. If the log-probabilities for multiple
+        values are desired the values must be provided in a numpy array or theano tensor
+
+    Returns
+    -------
+    logp : TensorVariable or np.ndarray
+    """
+    return rv.logp(value)
diff --git a/pymc3/tests/test_backports.py b/pymc3/tests/test_backports.py
new file mode 100644
index 0000000000..172f2418ff
--- /dev/null
+++ b/pymc3/tests/test_backports.py
@@ -0,0 +1,32 @@
+#   Copyright 2021 The PyMC Developers
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import numpy as np
+
+import pymc3 as pm
+
+
+class TestLogpSyntax:
+    def test_equivalence(self):
+        with pm.Model():
+            rv = pm.Normal("n")
+        input = {"n": 2}
+        np.testing.assert_array_equal(rv.logp(input), pm.logp(rv, input))
+
+    def test_equivalence_dist(self):
+        rv = pm.Normal.dist()
+        assert rv.logp(2).eval() == pm.logp(rv, 2).eval()
+        np.testing.assert_array_equal(
+            rv.logp(np.arange(3)).eval(), pm.logp(rv, np.arange(3)).eval()
+        )

From efbaccee94159de0a15b04baf84b1250047f132a Mon Sep 17 00:00:00 2001
From: ariepratama <arie.pratama.s@gmail.com>
Date: Sat, 30 Oct 2021 19:17:12 +0800
Subject: [PATCH 36/48] 4801 fix indentation and remove extra backslash on n
 variable

---
 pymc3/distributions/multivariate.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pymc3/distributions/multivariate.py b/pymc3/distributions/multivariate.py
index 946aeb45cc..702ce857c1 100755
--- a/pymc3/distributions/multivariate.py
+++ b/pymc3/distributions/multivariate.py
@@ -680,8 +680,8 @@ class DirichletMultinomial(Discrete):
 
     .. math::
 
-    f(x \mid n, a) = \frac{\Gamma(n + 1)\Gamma(\sum a_k)}
-                              {\Gamma(\n + \sum a_k)}
+        f(x \mid n, a) = \frac{\Gamma(n + 1)\Gamma(\sum a_k)}
+                              {\Gamma(n + \sum a_k)}
                          \prod_{k=1}^K
                          \frac{\Gamma(x_k +  a_k)}
                               {\Gamma(x_k + 1)\Gamma(a_k)}

From c332f8c957bb2ebaf346832f440e5ee94b65026a Mon Sep 17 00:00:00 2001
From: Meenal Jhajharia <meenal@mjhajharia.com>
Date: Mon, 8 Nov 2021 14:00:34 +0530
Subject: [PATCH 37/48] fix broken links

---
 docs/source/semantic_sphinx/layout.html | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/docs/source/semantic_sphinx/layout.html b/docs/source/semantic_sphinx/layout.html
index 5ae7732a2c..a193ab250c 100644
--- a/docs/source/semantic_sphinx/layout.html
+++ b/docs/source/semantic_sphinx/layout.html
@@ -68,7 +68,7 @@
                         <i class="search link icon"></i>
                     </form>
                 </div>
-                <a class="item" href="https://github.com/pymc-devs/pymc3"><i class="github blue icon large"></i></a>
+                <a class="item" href="https://github.com/pymc-devs/pymc"><i class="github blue icon large"></i></a>
             </div>
         </div>
     </div>
@@ -86,17 +86,17 @@ <h2>Probabilistic Programming in Python</h2>
 <div class="ui container" role="main">
     {% if pagename == 'index' %}
     <div class="ui vertical segment">
-        <a class="ui image" href="https://github.com/pymc-devs/pymc3/actions">
-            <img src="https://github.com/pymc-devs/pymc3/workflows/pytest/badge.svg">
+        <a class="ui image" href="https://github.com/pymc-devs/pymc/actions">
+            <img src="https://github.com/pymc-devs/pymc/workflows/pytest/badge.svg">
         </a>
-        <a class="ui image" href="https://coveralls.io/github/pymc-devs/pymc3?branch=master">
-            <img src="https://coveralls.io/repos/github/pymc-devs/pymc3/badge.svg?branch=master">
+        <a class="ui image" href="https://coveralls.io/github/pymc-devs/pymc?branch=v3">
+            <img src="https://coveralls.io/repos/github/pymc-devs/pymc/badge.svg?branch=v3">
         </a>
         <a class="ui image" href="https://tidelift.com/subscription/pkg/pypi-pymc3?utm_source=pypi-pymc3&utm_medium=referral&utm_campaign=enterprise">
         <a class="ui image" href="https://www.numfocus.org/">
             <img src="https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A">
         </a>
-        <a class="ui image" href="https://mybinder.org/v2/gh/pymc-devs/pymc3/master?filepath=%2Fdocs%2Fsource%2Fnotebooks">
+        <a class="ui image" href="https://mybinder.org/v2/gh/pymc-devs/pymc/v3?filepath=%2Fdocs%2Fsource%2Fnotebooks">
             <img src="https://mybinder.org/badge_logo.svg">
         </a>
         <a class="ui image" href="https://hub.docker.com/r/pymc/pymc3">
@@ -114,7 +114,7 @@ <h2>Probabilistic Programming in Python</h2>
 {%- block footer %}
 <div class="ui vertical footer segment">
     <div class="ui center aligned container">
-        <a href="https://github.com/pymc-devs/pymc3"><i class="github icon large"></i></a>
+        <a href="https://github.com/pymc-devs/pymc"><i class="github icon large"></i></a>
         <a href="https://twitter.com/pymc_devs"><i class="twitter icon large"></i></a>
         <a href="https://discourse.pymc.io/"><i class="discourse icon large"></i></a>
     </div>

From 6a580cc6b535412ddb0309b2c363795a56c5d645 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 12 Dec 2021 01:12:43 +0100
Subject: [PATCH 38/48] Deprecate those kwargs in `v3` that will break in `v4`
 (#5226)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Upgrade pre-commit from main and apply automated code style

Except the check against print statements.

* XFAIL KroneckerNormal test on Windows because of #5253

* Add deprecat dependency

For more automation in kwarg deprecations.

* Deprecation of testval→initval and related start kwarg

* Partially add admonition style for rendering warnings

With css taken from pydata-sphinx-theme.

Co-authored-by: Oriol (ZBook) <oriol.abril.pla@gmail.com>
---
 .pre-commit-config.yaml                     |  27 ++---
 RELEASE-NOTES.md                            |   2 +
 conda-envs/environment-dev-py37.yml         |   3 +
 conda-envs/environment-dev-py38.yml         |   3 +
 conda-envs/environment-dev-py39.yml         |   3 +
 conda-envs/windows-environment-dev-py38.yml |   2 +
 docs/source/_static/main.css                | 127 ++++++++++++++++++++
 docs/source/sphinxext/gallery_generator.py  |   2 +-
 pymc3/__init__.py                           |   2 +-
 pymc3/backends/base.py                      |   2 +-
 pymc3/distributions/distribution.py         |  70 +++++++++--
 pymc3/distributions/posterior_predictive.py |   2 +-
 pymc3/gp/cov.py                             |   2 +-
 pymc3/gp/util.py                            |   4 +-
 pymc3/model.py                              |   2 +-
 pymc3/model_graph.py                        |   2 +-
 pymc3/ode/ode.py                            |   4 +-
 pymc3/sampling.py                           |  30 +++--
 pymc3/step_methods/hmc/quadpotential.py     |  12 +-
 pymc3/step_methods/mlda.py                  |  20 +--
 pymc3/tests/test_distributions.py           |  18 +--
 pymc3/tests/test_distributions_random.py    |  12 +-
 pymc3/tests/test_modelcontext.py            |   2 +-
 pymc3/tests/test_random.py                  |   4 +-
 pymc3/tests/test_variational_inference.py   |   4 +-
 pymc3/theanof.py                            |   2 +-
 pymc3/variational/inference.py              |   2 +-
 requirements-dev.txt                        |   1 +
 requirements.txt                            |   1 +
 scripts/check_all_tests_are_covered.py      | 102 +++++++++++++---
 scripts/generate_pip_deps_from_conda.py     |   1 +
 31 files changed, 371 insertions(+), 99 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ff7a69713a..740cc5442d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 exclude: ^(docs/logos|pymc3/tests/data)/
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v3.4.0
+  rev: v4.0.1
   hooks:
     -   id: check-merge-conflict
     -   id: check-toml
@@ -14,44 +14,43 @@ repos:
         exclude: ^requirements-dev\.txt$
     -   id: trailing-whitespace
 - repo: https://github.com/PyCQA/isort
-  rev: 5.7.0
+  rev: 5.10.1
   hooks:
     - id: isort
       name: isort
 - repo: https://github.com/asottile/pyupgrade
-  rev: v2.10.0
+  rev: v2.29.1
   hooks:
     - id: pyupgrade
       args: [--py37-plus]
 - repo: https://github.com/psf/black
-  rev: 20.8b1
+  rev: 21.12b0
   hooks:
     - id: black
 - repo: https://github.com/PyCQA/pylint
-  rev: pylint-2.6.0
+  rev: v2.12.2
   hooks:
     - id: pylint
       args: [--rcfile=.pylintrc]
-      files: ^pymc3/
+      files: ^pymc/
+- repo: https://github.com/MarcoGorelli/madforhooks
+  rev: 0.2.1
+  hooks:
+    - id: conda-env-sorter
+      files: ^conda-envs/environment-dev-py.+\.yml$
 - repo: local
   hooks:
     - id: check-no-tests-are-ignored
+      additional_dependencies: [pandas,pyyaml]
       entry: python scripts/check_all_tests_are_covered.py
       files: ^\.github/workflows/pytest\.yml$
       language: python
       name: Check no tests are ignored
       pass_filenames: false
-    - id: conda-env-sort
-      additional_dependencies: [pyyaml]
-      entry: python scripts/sort_conda_envs.py
-      files: ^conda-envs/environment-dev-py37\.yml$
-      language: python
-      name: Sort dependencies in conda envs
-      types: [yaml]
     - id: pip-from-conda
       additional_dependencies: [pyyaml]
       entry: python scripts/generate_pip_deps_from_conda.py
-      files: ^conda-envs/
+      files: ^conda-envs/environment-dev-py.+.yml$
       language: python
       name: Generate pip dependency from conda
     - id: no-relative-imports
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 98b349cc03..558bfe4cf7 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -3,6 +3,8 @@
 ## PyMC 3.11.5 (TBD)
 ### Backports
 + The `pm.logp(rv, x)` syntax is now available and recommended to make your model code `v4`-ready. Note that this backport is just an alias and much less capable than what's available with `pymc >=4` (see [#5083](https://github.com/pymc-devs/pymc/pulls/5083)).
++ The `pm.Distribution(testval=...)` kwarg was deprecated and will be replaced by `pm.Distribution(initval=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
++ The `pm.sample(start=...)` kwarg was deprecated and will be replaced by `pm.sample(initvals=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
 
 ## PyMC3 3.11.4 (20 August 2021)
 
diff --git a/conda-envs/environment-dev-py37.yml b/conda-envs/environment-dev-py37.yml
index c944732928..311e42d2c9 100644
--- a/conda-envs/environment-dev-py37.yml
+++ b/conda-envs/environment-dev-py37.yml
@@ -11,6 +11,7 @@ dependencies:
 - numpy=1.15
 - numpydoc>=0.9
 - pandas=0.24
+- pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
 - pytest>=3.0
@@ -21,3 +22,5 @@ dependencies:
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
+- pip:
+  - deprecat
diff --git a/conda-envs/environment-dev-py38.yml b/conda-envs/environment-dev-py38.yml
index 24e9040cb3..c204bb6d01 100644
--- a/conda-envs/environment-dev-py38.yml
+++ b/conda-envs/environment-dev-py38.yml
@@ -9,6 +9,7 @@ dependencies:
 - mkl-service
 - nbsphinx>=0.4
 - numpydoc>=0.9
+- pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
 - pytest>=3.0
@@ -18,3 +19,5 @@ dependencies:
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
+- pip:
+  - deprecat
diff --git a/conda-envs/environment-dev-py39.yml b/conda-envs/environment-dev-py39.yml
index 6f6db58e67..77606a297e 100644
--- a/conda-envs/environment-dev-py39.yml
+++ b/conda-envs/environment-dev-py39.yml
@@ -9,6 +9,7 @@ dependencies:
 - mkl-service
 - nbsphinx>=0.4
 - numpydoc>=0.9
+- pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
 - pytest>=3.0
@@ -18,3 +19,5 @@ dependencies:
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
+- pip:
+  - deprecat
diff --git a/conda-envs/windows-environment-dev-py38.yml b/conda-envs/windows-environment-dev-py38.yml
index ea8b0622c0..92aed0febb 100644
--- a/conda-envs/windows-environment-dev-py38.yml
+++ b/conda-envs/windows-environment-dev-py38.yml
@@ -24,3 +24,5 @@ dependencies:
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
+- pip:
+  - deprecat
diff --git a/docs/source/_static/main.css b/docs/source/_static/main.css
index 16fffc59dc..87c4f842ec 100644
--- a/docs/source/_static/main.css
+++ b/docs/source/_static/main.css
@@ -3,3 +3,130 @@
     margin: 0.5em;
     content: ":";
 }
+
+:root {
+  --pst-color-primary: 19, 6, 84;
+  --pst-color-success: 40, 167, 69;
+  --pst-color-info: 0, 123, 255; /*23, 162, 184;*/
+  --pst-color-warning: 255, 193, 7;
+  --pst-color-danger: 220, 53, 69;
+  --pst-color-text-base: 51, 51, 51;
+
+  --pst-color-admonition-default: var(--pst-color-info);
+  --pst-color-admonition-note: var(--pst-color-info);
+  --pst-color-admonition-attention: var(--pst-color-warning);
+  --pst-color-admonition-caution: var(--pst-color-warning);
+  --pst-color-admonition-warning: var(--pst-color-warning);
+  --pst-color-admonition-danger: var(--pst-color-danger);
+  --pst-color-admonition-error: var(--pst-color-danger);
+  --pst-color-admonition-hint: var(--pst-color-success);
+  --pst-color-admonition-tip: var(--pst-color-success);
+  --pst-color-admonition-important: var(--pst-color-success);
+}
+
+.admonition,
+div.admonition {
+ margin:1.5625em auto;
+ padding:0 .6rem .8rem;
+ overflow:hidden;
+ page-break-inside:avoid;
+ border-left:.2rem solid;
+ border-left-color:rgba(var(--pst-color-admonition-default),1);
+ border-bottom-color:rgba(var(--pst-color-admonition-default),1);
+ border-right-color:rgba(var(--pst-color-admonition-default),1);
+ border-top-color:rgba(var(--pst-color-admonition-default),1);
+ border-radius:.2rem;
+ box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);
+ transition:color .25s,background-color .25s,border-color .25s
+}
+.admonition :last-child,
+div.admonition :last-child {
+ margin-bottom:0
+}
+.admonition p.admonition-title~*,
+div.admonition p.admonition-title~* {
+ padding:0 1.4rem
+}
+.admonition>ol,
+.admonition>ul,
+div.admonition>ol,
+div.admonition>ul {
+ margin-left:1em
+}
+.admonition>.admonition-title,
+div.admonition>.admonition-title {
+ position:relative;
+ margin:0 -.6rem;
+ padding:.4rem .6rem .4rem 2rem;
+ font-weight:700;
+ background-color:rgba(var(--pst-color-admonition-default),.1)
+}
+.admonition>.admonition-title+*,
+div.admonition>.admonition-title+* {
+ margin-top:.4em
+}
+.admonition.attention,
+div.admonition.attention {
+ border-color:rgba(var(--pst-color-admonition-attention),1)
+}
+.admonition.attention>.admonition-title,
+div.admonition.attention>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-attention),.1)
+}
+.admonition.caution,
+div.admonition.caution {
+ border-color:rgba(var(--pst-color-admonition-caution),1)
+}
+.admonition.caution>.admonition-title,
+div.admonition.caution>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-caution),.1)
+}
+div.admonition.warning {
+ border-color:rgba(var(--pst-color-admonition-warning),1)
+}
+.admonition.warning>.admonition-title,
+div.admonition.warning>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-warning),.1)
+}
+div.admonition.danger {
+ border-color:rgba(var(--pst-color-admonition-danger),1)
+}
+.admonition.danger>.admonition-title,
+div.admonition.danger>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-danger),.1)
+}
+div.admonition.error {
+ border-color:rgba(var(--pst-color-admonition-error),1)
+}
+.admonition.error>.admonition-title,
+div.admonition.error>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-error),.1)
+}
+div.admonition.hint {
+ border-color:rgba(var(--pst-color-admonition-hint),1)
+}
+.admonition.hint>.admonition-title,
+div.admonition.hint>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-hint),.1)
+}
+div.admonition.tip {
+ border-color:rgba(var(--pst-color-admonition-tip),1)
+}
+.admonition.tip>.admonition-title,
+div.admonition.tip>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-tip),.1)
+}
+div.admonition.important {
+ border-color:rgba(var(--pst-color-admonition-important),1)
+}
+.admonition.important>.admonition-title,
+div.admonition.important>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-important),.1)
+}
+div.admonition.note {
+ border-color:rgba(var(--pst-color-admonition-note),1)
+}
+.admonition.note>.admonition-title,
+div.admonition.note>.admonition-title {
+ background-color:rgba(var(--pst-color-admonition-note),.1)
+}
diff --git a/docs/source/sphinxext/gallery_generator.py b/docs/source/sphinxext/gallery_generator.py
index 46273e04d6..a04bfce673 100644
--- a/docs/source/sphinxext/gallery_generator.py
+++ b/docs/source/sphinxext/gallery_generator.py
@@ -194,7 +194,7 @@ def build_gallery(srcdir, gallery):
     with open(table_of_contents_file) as toc:
         table_of_contents = toc.read()
 
-    js_contents = "Gallery.examples = {}\n{}".format(json.dumps(data), table_of_contents)
+    js_contents = f"Gallery.examples = {json.dumps(data)}\n{table_of_contents}"
 
     with open(js_file, "w") as js:
         js.write(js_contents)
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 131bd5b394..53ca2d3977 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -63,7 +63,7 @@ def __set_compiler_flags():
 
 
 def _hotfix_theano_printing():
-    """ This is a workaround for https://github.com/pymc-devs/aesara/issues/309 """
+    """This is a workaround for https://github.com/pymc-devs/aesara/issues/309"""
     try:
         import pydot
         import theano.printing
diff --git a/pymc3/backends/base.py b/pymc3/backends/base.py
index 8b52c3e09c..41801cc472 100644
--- a/pymc3/backends/base.py
+++ b/pymc3/backends/base.py
@@ -364,7 +364,7 @@ def __getattr__(self, name):
             return self.get_values(name)
         if name in self.stat_names:
             return self.get_sampler_stats(name)
-        raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))
+        raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
 
     def __len__(self):
         chain = self.chains[-1]
diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py
index 3ba2d0a040..8ec1a3ea7c 100644
--- a/pymc3/distributions/distribution.py
+++ b/pymc3/distributions/distribution.py
@@ -24,6 +24,8 @@
 
 import dill
 
+from deprecat.sphinx import deprecat
+
 if TYPE_CHECKING:
     from typing import Optional, Callable
 
@@ -67,6 +69,7 @@
 )  # type: contextvars.ContextVar[Optional[Callable]]
 
 PLATFORM = sys.platform
+UNSET = object()
 
 
 class _Unpickling:
@@ -95,7 +98,7 @@ def __new__(cls, name, *args, **kwargs):
         data = kwargs.pop("observed", None)
         cls.data = data
         if isinstance(data, ObservedRV) or isinstance(data, FreeRV):
-            raise TypeError("observed needs to be data but got: {}".format(type(data)))
+            raise TypeError(f"observed needs to be data but got: {type(data)}")
         total_size = kwargs.pop("total_size", None)
 
         dims = kwargs.pop("dims", None)
@@ -130,15 +133,54 @@ def dist(cls, *args, **kwargs):
         dist.__init__(*args, **kwargs)
         return dist
 
+    @deprecat(
+        deprecated_args={
+            "testval": dict(version="3.11.5", reason="replaced by `initval` in PyMC 4.0.0"),
+        }
+    )
     def __init__(
-        self, shape, dtype, testval=None, defaults=(), transform=None, broadcastable=None, dims=None
+        self,
+        shape,
+        dtype,
+        initval=None,
+        defaults=(),
+        transform=None,
+        broadcastable=None,
+        dims=None,
+        *,
+        testval=UNSET,
     ):
+        """Creates a PyMC distribution object.
+
+        Parameters
+        ----------
+        shape : tuple
+            Output shape of the RV.
+            Forwarded to the Theano TensorType of this RV.
+        dtype
+            Forwarded to the Theano TensorType of this RV.
+        initval : np.ndarray
+            Initial value for this RV.
+            In PyMC 4.0.0 this will no longer assign test values to the tensors.
+        defaults : tuple
+        transform : pm.Transform
+        broadcastable : tuple
+            Forwarded to the Theano TensorType of this RV.
+        dims : tuple
+            Ignored.
+        testval : np.ndarray
+            The old way of specifying initial values assigning test-values.
+        """
+        # Handle deprecated kwargs
+        if testval is not UNSET:
+            initval = testval
+
         self.shape = np.atleast_1d(shape)
         if False in (np.floor(self.shape) == self.shape):
             raise TypeError("Expected int elements in shape")
         self.dtype = dtype
         self.type = TensorType(self.dtype, self.shape, broadcastable)
-        self.testval = testval
+        self.testval = initval
         self.defaults = defaults
         self.transform = transform
 
@@ -288,7 +330,7 @@ def __init__(
         **kwargs,
     ):
         super().__init__(
-            shape=shape, dtype=dtype, testval=testval, defaults=defaults, *args, **kwargs
+            shape=shape, dtype=dtype, initval=testval, defaults=defaults, *args, **kwargs
         )
         self.parent_dist = parent_dist
 
@@ -353,16 +395,22 @@ class DensityDist(Distribution):
 
     """
 
+    @deprecat(
+        deprecated_args={
+            "testval": dict(version="3.11.5", reason="replaced by `initval` in PyMC 4.0.0"),
+        }
+    )
     def __init__(
         self,
         logp,
         shape=(),
         dtype=None,
-        testval=0,
+        initval=0,
         random=None,
         wrap_random_with_dist_shape=True,
         check_shape_in_random=True,
         *args,
+        testval=UNSET,
         **kwargs,
     ):
         """
@@ -379,8 +427,8 @@ def __init__(
             a value here.
         dtype: None, str (Optional)
             The dtype of the distribution.
-        testval: number or array (Optional)
-            The ``testval`` of the RV's tensor that follow the ``DensityDist``
+        initval: number or array (Optional)
+            The ``initval`` of the RV's tensor that follow the ``DensityDist``
             distribution.
         random: None or callable (Optional)
             If ``None``, no random method is attached to the ``DensityDist``
@@ -403,6 +451,8 @@ def __init__(
             If ``True``, the shape of the random samples generate in the
             ``random`` method is checked with the expected return shape. This
             test is only performed if ``wrap_random_with_dist_shape is False``.
+        testval : np.ndarray
+            The old way of specifying initial values assigning test-values.
         args, kwargs: (Optional)
             These are passed to the parent class' ``__init__``.
 
@@ -525,9 +575,13 @@ def __init__(
                 assert prior.shape == (10, 100, 3)
 
         """
+        # Handle deprecated kwargs
+        if testval is not UNSET:
+            initval = testval
+
         if dtype is None:
             dtype = theano.config.floatX
-        super().__init__(shape, dtype, testval, *args, **kwargs)
+        super().__init__(shape, dtype, initval, *args, **kwargs)
         self.logp = logp
         if type(self.logp) == types.MethodType:
             if PLATFORM != "linux":
diff --git a/pymc3/distributions/posterior_predictive.py b/pymc3/distributions/posterior_predictive.py
index 470f0f02a7..17ee6b1839 100644
--- a/pymc3/distributions/posterior_predictive.py
+++ b/pymc3/distributions/posterior_predictive.py
@@ -342,7 +342,7 @@ def __init__(self, vars, trace: _TraceDict, samples, model: Model | None, size=N
         self.size = size
         self.logger = logging.getLogger("posterior_predictive")
 
-    def __enter__(self) -> "_PosteriorPredictiveSampler":
+    def __enter__(self) -> _PosteriorPredictiveSampler:
         self._tok = vectorized_ppc.set(posterior_predictive_draw_values)
         return self
 
diff --git a/pymc3/gp/cov.py b/pymc3/gp/cov.py
index a696c30b8a..c0b676daba 100644
--- a/pymc3/gp/cov.py
+++ b/pymc3/gp/cov.py
@@ -155,7 +155,7 @@ def __array_wrap__(self, result):
 class Combination(Covariance):
     def __init__(self, factor_list):
         input_dim = max(
-            [factor.input_dim for factor in factor_list if isinstance(factor, Covariance)]
+            factor.input_dim for factor in factor_list if isinstance(factor, Covariance)
         )
         super().__init__(input_dim=input_dim)
         self.factor_list = []
diff --git a/pymc3/gp/util.py b/pymc3/gp/util.py
index b0875b905a..54cac03367 100644
--- a/pymc3/gp/util.py
+++ b/pymc3/gp/util.py
@@ -36,7 +36,7 @@ def infer_shape(X, n_points=None):
 
 
 def stabilize(K):
-    """ adds small diagonal to a covariance matrix """
+    """adds small diagonal to a covariance matrix"""
     return K + 1e-6 * tt.identity_like(K)
 
 
@@ -62,7 +62,7 @@ def kmeans_inducing_points(n_inducing, X):
 
 
 def conditioned_vars(varnames):
-    """ Decorator for validating attrs that are conditioned on. """
+    """Decorator for validating attrs that are conditioned on."""
 
     def gp_wrapper(cls):
         def make_getter(name):
diff --git a/pymc3/model.py b/pymc3/model.py
index dff1e3b78b..c469a2355c 100644
--- a/pymc3/model.py
+++ b/pymc3/model.py
@@ -723,7 +723,7 @@ def __call__(self, array, grad_out=None, extra_vars=None):
 
         if array.shape != (self.size,):
             raise ValueError(
-                "Invalid shape for array. Must be {} but is {}.".format((self.size,), array.shape)
+                f"Invalid shape for array. Must be {(self.size,)} but is {array.shape}."
             )
 
         if grad_out is None:
diff --git a/pymc3/model_graph.py b/pymc3/model_graph.py
index cd3feb3070..a67706f707 100644
--- a/pymc3/model_graph.py
+++ b/pymc3/model_graph.py
@@ -84,7 +84,7 @@ def _filter_parents(self, var, parents) -> Set[VarName]:
                 if self.transform_map[p] != var.name:
                     keep.add(self.transform_map[p])
             else:
-                raise AssertionError("Do not know what to do with {}".format(get_var_name(p)))
+                raise AssertionError(f"Do not know what to do with {get_var_name(p)}")
         return keep
 
     def get_parents(self, var: Tensor) -> Set[VarName]:
diff --git a/pymc3/ode/ode.py b/pymc3/ode/ode.py
index 2eba398404..6534d5c95f 100644
--- a/pymc3/ode/ode.py
+++ b/pymc3/ode/ode.py
@@ -136,7 +136,7 @@ def _simulate(self, y0, theta):
 
     def make_node(self, y0, theta):
         inputs = (y0, theta)
-        _log.debug("make_node for inputs {}".format(hash(inputs)))
+        _log.debug(f"make_node for inputs {hash(inputs)}")
         states = self._otypes[0]()
         sens = self._otypes[1]()
 
@@ -220,7 +220,7 @@ def infer_shape(self, fgraph, node, input_shapes):
         return output_shapes
 
     def grad(self, inputs, output_grads):
-        _log.debug("grad w.r.t. inputs {}".format(hash(tuple(inputs))))
+        _log.debug(f"grad w.r.t. inputs {hash(tuple(inputs))}")
 
         # fetch symbolic sensitivity output node from cache
         ihash = hash(tuple(inputs))
diff --git a/pymc3/sampling.py b/pymc3/sampling.py
index 22bf8ff53a..3e63412f37 100644
--- a/pymc3/sampling.py
+++ b/pymc3/sampling.py
@@ -23,7 +23,7 @@
 
 from collections import defaultdict
 from copy import copy, deepcopy
-from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
+from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Union, cast
 
 import arviz
 import numpy as np
@@ -32,6 +32,7 @@
 import xarray
 
 from arviz import InferenceData
+from deprecat.sphinx import deprecat
 from fastprogress.fastprogress import progress_bar
 
 import pymc3 as pm
@@ -232,12 +233,18 @@ def _print_step_hierarchy(s: Step, level=0) -> None:
         _log.info(">" * level + f"{s.__class__.__name__}: [{varnames}]")
 
 
+@deprecat(
+    deprecated_args={
+        "start": dict(version="3.11.5", reason="renamed to `initvals` in PyMC v4.0.0"),
+        "pickle_backend": dict(version="3.11.5", reason="removed in PyMC v4.0.0"),
+    }
+)
 def sample(
     draws=1000,
     step=None,
     init="auto",
     n_init=200000,
-    start=None,
+    initvals: Optional[Union[PointType, Sequence[Optional[PointType]]]] = None,
     trace=None,
     chain_idx=0,
     chains=None,
@@ -251,6 +258,7 @@ def sample(
     callback=None,
     jitter_max_retries=10,
     *,
+    start=None,
     return_inferencedata=None,
     idata_kwargs: dict = None,
     mp_ctx=None,
@@ -294,11 +302,10 @@ def sample(
         users.
     n_init : int
         Number of iterations of initializer. Only works for 'ADVI' init methods.
-    start : dict, or array of dict
-        Starting point in parameter space (or partial point)
-        Defaults to ``trace.point(-1))`` if there is a trace provided and model.test_point if not
-        (defaults to empty dict). Initialization methods for NUTS (see ``init`` keyword) can
-        overwrite the default.
+    initvals : optional, dict, array of dict
+        Dict or list of dicts with initial values to use instead of the defaults.
+        The keys should be names of transformed random variables.
+        Initialization methods for NUTS (see ``init`` keyword) can overwrite the default.
     trace : backend, list, or MultiTrace
         This should be a backend instance, a list of variables to track, or a MultiTrace object
         with past values. If a MultiTrace object is given, it must contain samples for the chain
@@ -339,6 +346,11 @@ def sample(
         Maximum number of repeated attempts (per chain) at creating an initial matrix with uniform jitter
         that yields a finite probability. This applies to ``jitter+adapt_diag`` and ``jitter+adapt_full``
         init methods.
+    start : dict, or array of dict
+        Starting point in parameter space (or partial point)
+        Defaults to ``trace.point(-1))`` if there is a trace provided and model.test_point if not
+        (defaults to empty dict). Initialization methods for NUTS (see ``init`` keyword) can
+        overwrite the default.
     return_inferencedata : bool, default=False
         Whether to return the trace as an :class:`arviz:arviz.InferenceData` (True) object or a `MultiTrace` (False)
         Defaults to `False`, but we'll switch to `True` in an upcoming release.
@@ -422,6 +434,10 @@ def sample(
             mean     sd  hdi_3%  hdi_97%
         p  0.609  0.047   0.528    0.699
     """
+    # Handle deprecated/forwards-compatible kwargs
+    if initvals is not None:
+        start = initvals
+
     model = modelcontext(model)
     start = deepcopy(start)
     if start is None:
diff --git a/pymc3/step_methods/hmc/quadpotential.py b/pymc3/step_methods/hmc/quadpotential.py
index 4c2e6acc7a..7fec886d96 100644
--- a/pymc3/step_methods/hmc/quadpotential.py
+++ b/pymc3/step_methods/hmc/quadpotential.py
@@ -161,13 +161,9 @@ def __init__(
         if initial_mean.ndim != 1:
             raise ValueError("Initial mean must be one-dimensional.")
         if initial_diag is not None and len(initial_diag) != n:
-            raise ValueError(
-                "Wrong shape for initial_diag: expected {} got {}".format(n, len(initial_diag))
-            )
+            raise ValueError(f"Wrong shape for initial_diag: expected {n} got {len(initial_diag)}")
         if len(initial_mean) != n:
-            raise ValueError(
-                "Wrong shape for initial_mean: expected {} got {}".format(n, len(initial_mean))
-            )
+            raise ValueError(f"Wrong shape for initial_mean: expected {n} got {len(initial_mean)}")
 
         if dtype is None:
             dtype = theano.config.floatX
@@ -520,9 +516,7 @@ def __init__(
         if initial_cov is not None and initial_cov.shape != (n, n):
             raise ValueError(f"Wrong shape for initial_cov: expected {n} got {initial_cov.shape}")
         if len(initial_mean) != n:
-            raise ValueError(
-                "Wrong shape for initial_mean: expected {} got {}".format(n, len(initial_mean))
-            )
+            raise ValueError(f"Wrong shape for initial_mean: expected {n} got {len(initial_mean)}")
 
         if dtype is None:
             dtype = theano.config.floatX
diff --git a/pymc3/step_methods/mlda.py b/pymc3/step_methods/mlda.py
index c000af1e34..a438b5fd45 100644
--- a/pymc3/step_methods/mlda.py
+++ b/pymc3/step_methods/mlda.py
@@ -883,24 +883,16 @@ def update_error_estimate(self, accepted, skipped_logp):
                 pm.set_data(
                     {
                         "mu_B": sum(
-                            [
-                                bias.get_mu()
-                                for bias in self.bias_all[
-                                    : len(self.bias_all) - self.num_levels + 2
-                                ]
-                            ]
+                            bias.get_mu()
+                            for bias in self.bias_all[: len(self.bias_all) - self.num_levels + 2]
                         )
                     }
                 )
                 pm.set_data(
                     {
                         "Sigma_B": sum(
-                            [
-                                bias.get_sigma()
-                                for bias in self.bias_all[
-                                    : len(self.bias_all) - self.num_levels + 2
-                                ]
-                            ]
+                            bias.get_sigma()
+                            for bias in self.bias_all[: len(self.bias_all) - self.num_levels + 2]
                         )
                     }
                 )
@@ -979,7 +971,7 @@ def extract_Q_estimate(trace, levels):
 
     Q_0_raw = trace.get_sampler_stats("Q_0")
     # total number of base level samples from all iterations
-    total_base_level_samples = sum([it.shape[0] for it in Q_0_raw])
+    total_base_level_samples = sum(it.shape[0] for it in Q_0_raw)
     Q_0 = np.concatenate(Q_0_raw).reshape((1, total_base_level_samples))
     ess_Q_0 = az.ess(np.array(Q_0, np.float64))
     Q_0_var = Q_0.var() / ess_Q_0
@@ -989,7 +981,7 @@ def extract_Q_estimate(trace, levels):
     for l in range(1, levels):
         Q_diff_raw = trace.get_sampler_stats(f"Q_{l}_{l-1}")
         # total number of samples from all iterations
-        total_level_samples = sum([it.shape[0] for it in Q_diff_raw])
+        total_level_samples = sum(it.shape[0] for it in Q_diff_raw)
         Q_diff = np.concatenate(Q_diff_raw).reshape((1, total_level_samples))
         ess_diff = az.ess(np.array(Q_diff, np.float64))
 
diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py
index 98baccb28f..351c65f7ab 100644
--- a/pymc3/tests/test_distributions.py
+++ b/pymc3/tests/test_distributions.py
@@ -280,7 +280,7 @@ def multinomial_logpdf(value, n, p):
 
 
 def dirichlet_multinomial_logpmf(value, n, a):
-    value, n, a = [np.asarray(x) for x in [value, n, a]]
+    value, n, a = (np.asarray(x) for x in [value, n, a])
     assert value.ndim == 1
     assert n.ndim == 0
     assert a.shape == value.shape
@@ -1875,7 +1875,7 @@ def test_multinomial_vec(self):
         )
 
         assert_almost_equal(
-            sum([model_single.fastlogp({"m": val}) for val in vals]),
+            sum(model_single.fastlogp({"m": val}) for val in vals),
             model_many.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -1889,7 +1889,7 @@ def test_multinomial_vec_1d_n(self):
             Multinomial("m", n=ns, p=p, shape=vals.shape)
 
         assert_almost_equal(
-            sum([multinomial_logpdf(val, n, p) for val, n in zip(vals, ns)]),
+            sum(multinomial_logpdf(val, n, p) for val, n in zip(vals, ns)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -1903,7 +1903,7 @@ def test_multinomial_vec_1d_n_2d_p(self):
             Multinomial("m", n=ns, p=ps, shape=vals.shape)
 
         assert_almost_equal(
-            sum([multinomial_logpdf(val, n, p) for val, n, p in zip(vals, ns, ps)]),
+            sum(multinomial_logpdf(val, n, p) for val, n, p in zip(vals, ns, ps)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -1917,7 +1917,7 @@ def test_multinomial_vec_2d_p(self):
             Multinomial("m", n=n, p=ps, shape=vals.shape)
 
         assert_almost_equal(
-            sum([multinomial_logpdf(val, n, p) for val, p in zip(vals, ps)]),
+            sum(multinomial_logpdf(val, n, p) for val, p in zip(vals, ps)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -2009,7 +2009,7 @@ def test_dirichlet_multinomial_vec(self):
         )
 
         assert_almost_equal(
-            sum([model_single.fastlogp({"m": val}) for val in vals]),
+            sum(model_single.fastlogp({"m": val}) for val in vals),
             model_many.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -2023,7 +2023,7 @@ def test_dirichlet_multinomial_vec_1d_n(self):
             DirichletMultinomial("m", n=ns, a=a, shape=vals.shape)
 
         assert_almost_equal(
-            sum([dirichlet_multinomial_logpmf(val, n, a) for val, n in zip(vals, ns)]),
+            sum(dirichlet_multinomial_logpmf(val, n, a) for val, n in zip(vals, ns)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -2037,7 +2037,7 @@ def test_dirichlet_multinomial_vec_1d_n_2d_a(self):
             DirichletMultinomial("m", n=ns, a=as_, shape=vals.shape)
 
         assert_almost_equal(
-            sum([dirichlet_multinomial_logpmf(val, n, a) for val, n, a in zip(vals, ns, as_)]),
+            sum(dirichlet_multinomial_logpmf(val, n, a) for val, n, a in zip(vals, ns, as_)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
@@ -2051,7 +2051,7 @@ def test_dirichlet_multinomial_vec_2d_a(self):
             DirichletMultinomial("m", n=n, a=as_, shape=vals.shape)
 
         assert_almost_equal(
-            sum([dirichlet_multinomial_logpmf(val, n, a) for val, a in zip(vals, as_)]),
+            sum(dirichlet_multinomial_logpmf(val, n, a) for val, a in zip(vals, as_)),
             model.fastlogp({"m": vals}),
             decimal=4,
         )
diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py
index fff795c1fb..a5e3c254f6 100644
--- a/pymc3/tests/test_distributions_random.py
+++ b/pymc3/tests/test_distributions_random.py
@@ -216,7 +216,7 @@ def setup_method(self, *args, **kwargs):
             self.model = pm.Model()
 
         def get_random_variable(self, shape, with_vector_params=False, name=None):
-            """ Creates a RandomVariable of the parametrized distribution. """
+            """Creates a RandomVariable of the parametrized distribution."""
             if with_vector_params:
                 params = {
                     key: value * np.ones(self.shape, dtype=np.dtype(type(value)))
@@ -240,7 +240,7 @@ def get_random_variable(self, shape, with_vector_params=False, name=None):
 
         @staticmethod
         def sample_random_variable(random_variable, size):
-            """ Draws samples from a RandomVariable using its .random() method. """
+            """Draws samples from a RandomVariable using its .random() method."""
             try:
                 if size is None:
                     return random_variable.random()
@@ -255,7 +255,7 @@ def sample_random_variable(random_variable, size):
         @pytest.mark.parametrize("size", [None, (), 1, (1,), 5, (4, 5)], ids=str)
         @pytest.mark.parametrize("shape", [None, ()], ids=str)
         def test_scalar_distribution_shape(self, shape, size):
-            """ Draws samples of different [size] from a scalar [shape] RV. """
+            """Draws samples of different [size] from a scalar [shape] RV."""
             rv = self.get_random_variable(shape)
             exp_shape = self.default_shape if shape is None else tuple(np.atleast_1d(shape))
             exp_size = self.default_size if size is None else tuple(np.atleast_1d(size))
@@ -275,7 +275,7 @@ def test_scalar_distribution_shape(self, shape, size):
             "shape", [None, (), (1,), (1, 1), (1, 2), (10, 11, 1), (9, 10, 2)], ids=str
         )
         def test_scalar_sample_shape(self, shape, size):
-            """ Draws samples of scalar [size] from a [shape] RV. """
+            """Draws samples of scalar [size] from a [shape] RV."""
             rv = self.get_random_variable(shape)
             exp_shape = self.default_shape if shape is None else tuple(np.atleast_1d(shape))
             exp_size = self.default_size if size is None else tuple(np.atleast_1d(size))
@@ -934,6 +934,10 @@ def ref_rand_uchol(size, mu, rowchol, colchol):
                     ref_rand=ref_rand_chol_transpose,
                 )
 
+    @pytest.mark.xfail(
+        condition=sys.platform.startswith("win"),
+        reason="Compilation problems. See https://github.com/pymc-devs/pymc/issues/5253",
+    )
     def test_kronecker_normal(self):
         def ref_rand(size, mu, covs, sigma):
             cov = pm.math.kronecker(covs[0], covs[1]).eval()
diff --git a/pymc3/tests/test_modelcontext.py b/pymc3/tests/test_modelcontext.py
index b7d44ca63c..1e11a9a411 100644
--- a/pymc3/tests/test_modelcontext.py
+++ b/pymc3/tests/test_modelcontext.py
@@ -34,7 +34,7 @@ def test_thread_safety(self):
         that thread A enters the context manager first, then B,
         then A attempts to declare a variable while B is still in the context manager.
         """
-        aInCtxt, bInCtxt, aDone = [threading.Event() for _ in range(3)]
+        aInCtxt, bInCtxt, aDone = (threading.Event() for _ in range(3))
         modelA = Model()
         modelB = Model()
 
diff --git a/pymc3/tests/test_random.py b/pymc3/tests/test_random.py
index 7a4ae42ce2..dfdfee4f63 100644
--- a/pymc3/tests/test_random.py
+++ b/pymc3/tests/test_random.py
@@ -44,8 +44,8 @@ def test_draw_value():
 
     assert _draw_value(5) == 5
     assert _draw_value(5.0) == 5
-    assert isinstance(_draw_value(5.0), type(5.0))
-    assert isinstance(_draw_value(5), type(5))
+    assert isinstance(_draw_value(5.0), float)
+    assert isinstance(_draw_value(5), int)
 
     with pm.Model():
         mu = 2 * tt.constant(np.array([5.0, 6.0])) + theano.shared(np.array(5))
diff --git a/pymc3/tests/test_variational_inference.py b/pymc3/tests/test_variational_inference.py
index 486db7d291..ac23171e51 100644
--- a/pymc3/tests/test_variational_inference.py
+++ b/pymc3/tests/test_variational_inference.py
@@ -145,7 +145,7 @@ def test_init_groups(three_var_model, raises, grouping):
         ({}, {MeanFieldGroup: (["one"], {}), FullRankGroup: (["two", "three"], {})}),
         ({}, {MeanFieldGroup: (["one"], {}), EmpiricalGroup: (["two", "three"], {"size": 100})}),
     ],
-    ids=lambda t: ", ".join("{}: {}".format(k.__name__, v[0]) for k, v in t[1].items()),
+    ids=lambda t: ", ".join(f"{k.__name__}: {v[0]}" for k, v in t[1].items()),
 )
 def three_var_groups(request, three_var_model):
     kw, grouping = request.param
@@ -199,7 +199,7 @@ def aevb_initial():
         (NormalizingFlowGroup, {"flow": "radial"}),
         (NormalizingFlowGroup, {"flow": "radial-loc"}),
     ],
-    ids=lambda t: "{c}: {d}".format(c=t[0].__name__, d=t[1]),
+    ids=lambda t: f"{t[0].__name__}: {t[1]}",
 )
 def parametric_grouped_approxes(request):
     return request.param
diff --git a/pymc3/theanof.py b/pymc3/theanof.py
index 7f22ce6f38..b2edccd80d 100644
--- a/pymc3/theanof.py
+++ b/pymc3/theanof.py
@@ -212,7 +212,7 @@ def grad(self, inp, grads):
         return grads
 
     def c_code(self, node, name, inp, out, sub):
-        return "{z} = {x};".format(x=inp[0], z=out[0])
+        return f"{out[0]} = {inp[0]};"
 
     def __eq__(self, other):
         return isinstance(self, type(other))
diff --git a/pymc3/variational/inference.py b/pymc3/variational/inference.py
index 85eb08e65c..2937dfa5ca 100644
--- a/pymc3/variational/inference.py
+++ b/pymc3/variational/inference.py
@@ -266,7 +266,7 @@ def _infmean(input_array):
                 )
         else:
             if n < 10:
-                logger.info("Finished [100%]: Loss = {:,.5g}".format(scores[-1]))
+                logger.info(f"Finished [100%]: Loss = {scores[-1]:,.5g}")
             else:
                 avg_loss = _infmean(scores[max(0, i - 1000) : i + 1])
                 logger.info(f"Finished [100%]: Average Loss = {avg_loss:,.5g}")
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ec92f04f0d..0f17952f5a 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,7 @@
 # This file is auto-generated by scripts/generate_pip_deps_from_conda.py, do not modify.
 # See that file for comments about the need/usage of each dependency.
 
+deprecat
 h5py>=2.7
 ipython>=7.16
 nbsphinx>=0.4
diff --git a/requirements.txt b/requirements.txt
index d0900a22d4..80f31124bc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 arviz>=0.11.0
 cachetools>=4.2.1
+deprecat
 dill
 fastprogress>=0.2.0
 numpy>=1.15.0
diff --git a/scripts/check_all_tests_are_covered.py b/scripts/check_all_tests_are_covered.py
index 058a867d02..90a6a99d7a 100644
--- a/scripts/check_all_tests_are_covered.py
+++ b/scripts/check_all_tests_are_covered.py
@@ -6,23 +6,93 @@
 This is intended to be used as a pre-commit hook, see `.pre-commit-config.yaml`.
 You can run it manually with `pre-commit run check-no-tests-are-ignored --all`.
 """
-
-import re
+import itertools
+import logging
+import os
 
 from pathlib import Path
 
+import pandas
+import yaml
+
+_log = logging.getLogger(__file__)
+logging.basicConfig(level=logging.DEBUG)
+
+
+def find_testfiles():
+    dp_repo = Path(__file__).parent.parent
+    all_tests = {
+        str(fp.relative_to(dp_repo)).replace(os.sep, "/")
+        for fp in (dp_repo / "pymc3" / "tests").glob("**/test_*.py")
+    }
+    _log.info("Found %i tests in total.", len(all_tests))
+    return all_tests
+
+
+def from_yaml():
+    """Determins how often each test file is run per platform and floatX setting.
+
+    An exception is raised if tests run multiple times with the same configuration.
+    """
+    # First collect the matrix definitions from testing workflows
+    matrices = {}
+    for wf in ["pytest.yml", "arviz_compat.yml"]:
+        wfname = wf.strip(".yml")
+        wfdef = yaml.safe_load(open(Path(".github", "workflows", wf)))
+        for jobname, jobdef in wfdef["jobs"].items():
+            matrix = jobdef.get("strategy", {}).get("matrix", {})
+            if matrix:
+                matrices[(wfname, jobname)] = matrix
+            else:
+                _log.warning("No matrix in %s/%s", wf, jobname)
+
+    # Now create an empty DataFrame to count based on OS/floatX/testfile
+    all_os = []
+    all_floatX = []
+    for matrix in matrices.values():
+        all_os += matrix["os"]
+        all_floatX += matrix["floatx"]
+    all_os = tuple(sorted(set(all_os)))
+    all_floatX = tuple(sorted(set(all_floatX)))
+    all_tests = find_testfiles()
+
+    df = pandas.DataFrame(
+        columns=pandas.MultiIndex.from_product(
+            [sorted(all_floatX), sorted(all_os)], names=["floatX", "os"]
+        ),
+        index=pandas.Index(sorted(all_tests), name="testfile"),
+    )
+    df.loc[:, :] = 0
+
+    # Count how often the testfiles are included in job definitions
+    for matrix in matrices.values():
+        for os_, floatX, subset in itertools.product(
+            matrix["os"], matrix["floatx"], matrix["test-subset"]
+        ):
+            testfiles = subset.split("\n")
+            ignored = {item.strip("--ignore=") for item in testfiles if item.startswith("--ignore")}
+            included = {item for item in testfiles if item and not item.startswith("--ignore")}
+            if ignored and not included:
+                # if no testfile is specified explicitly pytest runs all except the ignored ones
+                included = all_tests - ignored
+
+            for testfile in included:
+                df.loc[testfile, (floatX, os_)] += 1
+
+    ignored_by_all = set(df[df.eq(0).all(axis=1)].index)
+    run_multiple_times = set(df[df.gt(1).any(axis=1)].index)
+
+    # Print summary, warnings and raise errors on unwanted configurations
+    _log.info("Number of test runs (❌=0, ✅=once)\n%s", df.replace(0, "❌").replace(1, "✅"))
+
+    if ignored_by_all:
+        _log.warning("%i tests are completely ignored:\n%s", len(ignored_by_all), ignored_by_all)
+    if run_multiple_times:
+        raise Exception(
+            f"{len(run_multiple_times)} tests are run multiple times with the same OS and floatX setting:\n{run_multiple_times}"
+        )
+    return
+
+
 if __name__ == "__main__":
-    testing_workflows = ["pytest.yml"]
-    ignored = set()
-    non_ignored = set()
-    for wfyml in testing_workflows:
-        pytest_ci_job = Path(".github") / "workflows" / wfyml
-        txt = pytest_ci_job.read_text()
-        ignored = set(re.findall(r"(?<=--ignore=)(pymc3/tests.*\.py)", txt))
-        non_ignored = non_ignored.union(set(re.findall(r"(?<!--ignore=)(pymc3/tests.*\.py)", txt)))
-    assert (
-        ignored <= non_ignored
-    ), f"The following tests are ignored by the first job but not run by the others: {ignored.difference(non_ignored)}"
-    assert (
-        ignored >= non_ignored
-    ), f"The following tests are run by multiple jobs: {non_ignored.difference(ignored)}"
+    from_yaml()
diff --git a/scripts/generate_pip_deps_from_conda.py b/scripts/generate_pip_deps_from_conda.py
index 0413754af6..4831fc412a 100755
--- a/scripts/generate_pip_deps_from_conda.py
+++ b/scripts/generate_pip_deps_from_conda.py
@@ -45,6 +45,7 @@
     "pip",
     "python",
     "libblas",
+    "libblas=*=*mkl",
     "libpython",
     "m2w64-toolchain",
     "mkl-service",

From 1a24abf24dbec918f29f3e1f4ca922803bf98137 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ahmet=20Akko=C3=A7?= <themadprogramer@gmail.com>
Date: Wed, 29 Dec 2021 18:13:26 +0300
Subject: [PATCH 39/48] Updated PyMC3 and Theano Example (#5292)

* Updated Example

-Typo Corrections
-Missing imports
-Typecasting to int32 for the custom function, originally omitted

* Run pre-commit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 docs/source/PyMC3_and_Theano.rst | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/docs/source/PyMC3_and_Theano.rst b/docs/source/PyMC3_and_Theano.rst
index d2c521ad3f..97bf3f0efa 100644
--- a/docs/source/PyMC3_and_Theano.rst
+++ b/docs/source/PyMC3_and_Theano.rst
@@ -37,6 +37,8 @@ arbitrarily chosen) function
 First, we need to define symbolic variables for our inputs (this
 is similar to eg SymPy's `Symbol`)::
 
+    import pymc as pm
+    import numpy
     import theano
     import theano.tensor as tt
     # We don't specify the dtype of our input variables, so it
@@ -75,7 +77,7 @@ We can call this function with actual arrays as many times as we want::
 
     a_val = 1.2
     x_vals = np.random.randn(10)
-    y_vals = np.random.randn(10)
+    y_vals = np.int32(np.random.randn(10))
 
     out = func(a_val, x_vals, y_vals)
 
@@ -167,8 +169,8 @@ we generate a Theano variable. And for each variable (observed or otherwise)
 we add a term to the global logp. In the background something similar to
 this is happening::
 
-    # For illustration only, those functions don't actually exist
-    # in exactly this way!
+    # For illustration only, these functions don't exactly
+    # work this way!
     model = pm.Model()
 
     mu = tt.scalar('mu')

From 80dae6bd4892870e8ccdc7991309f7fbf5d597ff Mon Sep 17 00:00:00 2001
From: Laszlo <laszlo.treszkai@gmail.com>
Date: Thu, 6 Jan 2022 20:56:26 +0100
Subject: [PATCH 40/48] Workaround Theano issue with NumPy>=1.22.0 (#5310) (see
 #5316)

Co-authored-by: Laszlo Treszkai <laszlo.treszkai@finmatics.com>
---
 RELEASE-NOTES.md  |  3 +++
 pymc3/__init__.py | 22 ++++++++++++++++++++++
 2 files changed, 25 insertions(+)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 558bfe4cf7..0062d28c68 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -6,6 +6,9 @@
 + The `pm.Distribution(testval=...)` kwarg was deprecated and will be replaced by `pm.Distribution(initval=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
 + The `pm.sample(start=...)` kwarg was deprecated and will be replaced by `pm.sample(initvals=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
 
+### Bugfixes
++ A hotfix is applied on import to remain compatible with NumPy 1.22 (see [#5316](https://github.com/pymc-devs/pymc/pull/5316)).
+
 ## PyMC3 3.11.4 (20 August 2021)
 
 ### New Features
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 53ca2d3977..e8ea258fb4 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -18,8 +18,30 @@
 import logging
 import multiprocessing as mp
 import platform
+import warnings
 
+import numpy.distutils
 import semver
+
+# Workaround for Theano bug that tries to access blas_opt_info;
+#  must be done before importing theano.
+# https://github.com/pymc-devs/pymc/issues/5310
+# Copied from theano/link/c/cmodule.py: default_blas_ldflags()
+if (
+    hasattr(numpy.distutils, "__config__")
+    and numpy.distutils.__config__
+    and not hasattr(numpy.distutils.__config__, "blas_opt_info")
+):
+    import numpy.distutils.system_info  # noqa
+
+    # We need to catch warnings as in some cases NumPy print
+    # stuff that we don't want the user to see.
+    with warnings.catch_warnings(record=True):
+        numpy.distutils.system_info.system_info.verbosity = 0
+        blas_info = numpy.distutils.system_info.get_info("blas_opt")
+
+    numpy.distutils.__config__.blas_opt_info = blas_info
+
 import theano
 
 _log = logging.getLogger("pymc3")

From a585c725554c8914941a2aad4b45449a49f3cab7 Mon Sep 17 00:00:00 2001
From: Jed Ludlow <jed.ludlow@gmail.com>
Date: Sat, 22 Jan 2022 15:07:58 -0700
Subject: [PATCH 41/48] Add additional compiler flag to work around GCC bug.

Work around compiler bug in GCC < 8.4 related to structured exception
handling registers on Windows.
See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65782 for details.
---
 pymc3/__init__.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index e8ea258fb4..07850f73a7 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -81,7 +81,12 @@ def _check_backend_version():
 def __set_compiler_flags():
     # Workarounds for Theano compiler problems on various platforms
     current = theano.config.gcc__cxxflags
-    theano.config.gcc__cxxflags = f"{current} -Wno-c++11-narrowing"
+    augmented = f"{current} -Wno-c++11-narrowing"
+    # Work around compiler bug in GCC < 8.4 related to structured exception
+    # handling registers on Windows.
+    # See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65782 for details.
+    augmented = f"{augmented} -fno-asynchronous-unwind-tables"
+    theano.config.gcc__cxxflags = augmented
 
 
 def _hotfix_theano_printing():

From b5bb2e41b0be2e0b6d86fb0b77aa6683b9b6524d Mon Sep 17 00:00:00 2001
From: Jed Ludlow <jed.ludlow@gmail.com>
Date: Tue, 25 Jan 2022 20:49:26 -0700
Subject: [PATCH 42/48] Disable C++ exception handling

Update GCC compiler options to disable C++ exception handling altogether.
Also disable the create of stack unwinding tables since these will not be
required without exceptions enabled.
---
 pymc3/__init__.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 07850f73a7..e71f0cbee1 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -82,10 +82,16 @@ def __set_compiler_flags():
     # Workarounds for Theano compiler problems on various platforms
     current = theano.config.gcc__cxxflags
     augmented = f"{current} -Wno-c++11-narrowing"
+
     # Work around compiler bug in GCC < 8.4 related to structured exception
     # handling registers on Windows.
     # See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65782 for details.
-    augmented = f"{augmented} -fno-asynchronous-unwind-tables"
+    # First disable C++ exception handling altogether since it's not needed
+    # for the C extensions that we generate.
+    augmented = f"{augmented} -fno-exceptions"
+    # Now disable the generation of stack unwinding tables.
+    augmented = f"{augmented} -fno-unwind-tables -fno-asynchronous-unwind-tables"
+
     theano.config.gcc__cxxflags = augmented
 
 

From baec0c36d51c2d9f5803ac95b8f280092b567116 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sat, 5 Feb 2022 00:47:39 +0100
Subject: [PATCH 43/48] API backports and deprecation warnings (#5389)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Backwards-compatible rename `Lognormal`→`LogNormal`

* Warn about removal of trace-continuation support

* Fixup lognormal rename

* More fixup

* Fix docstring section underlines

* Temporarily pin upper numpydoc version

Tracking issue https://github.com/pymc-devs/pymc/issues/5401

* Temporarily pin upper numpydoc version

Tracking issue https://github.com/pymc-devs/pymc/issues/5401
---
 RELEASE-NOTES.md                            |  1 +
 conda-envs/environment-dev-py37.yml         |  2 +-
 conda-envs/environment-dev-py38.yml         |  2 +-
 conda-envs/environment-dev-py39.yml         |  2 +-
 conda-envs/windows-environment-dev-py38.yml |  2 +-
 pymc3/distributions/__init__.py             |  2 ++
 pymc3/distributions/continuous.py           | 27 ++++++++++++---------
 pymc3/distributions/timeseries.py           |  2 +-
 pymc3/distributions/transforms.py           |  4 +--
 pymc3/sampling.py                           |  3 +++
 requirements-dev.txt                        |  2 +-
 11 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 0062d28c68..6a76278874 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -5,6 +5,7 @@
 + The `pm.logp(rv, x)` syntax is now available and recommended to make your model code `v4`-ready. Note that this backport is just an alias and much less capable than what's available with `pymc >=4` (see [#5083](https://github.com/pymc-devs/pymc/pulls/5083)).
 + The `pm.Distribution(testval=...)` kwarg was deprecated and will be replaced by `pm.Distribution(initval=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
 + The `pm.sample(start=...)` kwarg was deprecated and will be replaced by `pm.sample(initvals=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
++ `pm.Lognormal` is now available as an alias for `pm.Lognormal` (see [#5389](https://github.com/pymc-devs/pymc/pull/5389)).
 
 ### Bugfixes
 + A hotfix is applied on import to remain compatible with NumPy 1.22 (see [#5316](https://github.com/pymc-devs/pymc/pull/5316)).
diff --git a/conda-envs/environment-dev-py37.yml b/conda-envs/environment-dev-py37.yml
index 311e42d2c9..5ca7f3a08a 100644
--- a/conda-envs/environment-dev-py37.yml
+++ b/conda-envs/environment-dev-py37.yml
@@ -9,7 +9,7 @@ dependencies:
 - mkl-service
 - nbsphinx>=0.4
 - numpy=1.15
-- numpydoc>=0.9
+- numpydoc<1.2
 - pandas=0.24
 - pip
 - pre-commit>=2.8.0
diff --git a/conda-envs/environment-dev-py38.yml b/conda-envs/environment-dev-py38.yml
index c204bb6d01..8d7ae55446 100644
--- a/conda-envs/environment-dev-py38.yml
+++ b/conda-envs/environment-dev-py38.yml
@@ -8,7 +8,7 @@ dependencies:
 - libblas=*=*mkl
 - mkl-service
 - nbsphinx>=0.4
-- numpydoc>=0.9
+- numpydoc<1.2
 - pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
diff --git a/conda-envs/environment-dev-py39.yml b/conda-envs/environment-dev-py39.yml
index 77606a297e..f2759028f2 100644
--- a/conda-envs/environment-dev-py39.yml
+++ b/conda-envs/environment-dev-py39.yml
@@ -8,7 +8,7 @@ dependencies:
 - libblas=*=*mkl
 - mkl-service
 - nbsphinx>=0.4
-- numpydoc>=0.9
+- numpydoc<1.2
 - pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
diff --git a/conda-envs/windows-environment-dev-py38.yml b/conda-envs/windows-environment-dev-py38.yml
index 92aed0febb..c058590e6e 100644
--- a/conda-envs/windows-environment-dev-py38.yml
+++ b/conda-envs/windows-environment-dev-py38.yml
@@ -16,7 +16,7 @@ dependencies:
 # Extra stuff for dev, testing and docs build
 - ipython>=7.16
 - nbsphinx>=0.4
-- numpydoc>=0.9
+- numpydoc<1.2
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
 - pytest>=3.0
diff --git a/pymc3/distributions/__init__.py b/pymc3/distributions/__init__.py
index 51958f541b..b46fd5675f 100644
--- a/pymc3/distributions/__init__.py
+++ b/pymc3/distributions/__init__.py
@@ -36,6 +36,7 @@
     Logistic,
     LogitNormal,
     Lognormal,
+    LogNormal,
     Moyal,
     Normal,
     Pareto,
@@ -120,6 +121,7 @@
     "Weibull",
     "Bound",
     "Lognormal",
+    "LogNormal",
     "HalfStudentT",
     "ChiSquared",
     "HalfNormal",
diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py
index 234ed935f2..0aaa95e8c6 100644
--- a/pymc3/distributions/continuous.py
+++ b/pymc3/distributions/continuous.py
@@ -62,7 +62,7 @@
     "Gamma",
     "Weibull",
     "HalfStudentT",
-    "Lognormal",
+    "LogNormal",
     "ChiSquared",
     "HalfNormal",
     "Wald",
@@ -318,7 +318,7 @@ def random(self, point=None, size=None):
         size: int, optional
 
         Raises
-        -------
+        ------
         ValueError
         """
         raise ValueError("Cannot sample from Flat distribution")
@@ -375,7 +375,7 @@ def random(self, point=None, size=None):
         size: int, optional
 
         Raises
-        -------
+        ------
         ValueError
         """
         raise ValueError("Cannot sample from HalfFlat distribution")
@@ -1710,7 +1710,7 @@ class AsymmetricLaplace(Continuous):
         Location parameter
 
     See Also:
-    --------
+    ---------
     `Reference <https://en.wikipedia.org/wiki/Asymmetric_Laplace_distribution>`_
     """
 
@@ -1778,7 +1778,7 @@ def logp(self, value):
         )
 
 
-class Lognormal(PositiveContinuous):
+class LogNormal(PositiveContinuous):
     r"""
     Log-normal log-likelihood.
 
@@ -1834,10 +1834,10 @@ class Lognormal(PositiveContinuous):
 
         # Example to show that we pass in only ``sigma`` or ``tau`` but not both.
         with pm.Model():
-            x = pm.Lognormal('x', mu=2, sigma=30)
+            x = pm.LogNormal('x', mu=2, sigma=30)
 
         with pm.Model():
-            x = pm.Lognormal('x', mu=2, tau=1/100)
+            x = pm.LogNormal('x', mu=2, tau=1/100)
     """
 
     def __init__(self, mu=0, sigma=None, tau=None, sd=None, *args, **kwargs):
@@ -1856,8 +1856,8 @@ def __init__(self, mu=0, sigma=None, tau=None, sd=None, *args, **kwargs):
         self.mode = tt.exp(self.mu - 1.0 / self.tau)
         self.variance = (tt.exp(1.0 / self.tau) - 1) * tt.exp(2 * self.mu + 1.0 / self.tau)
 
-        assert_negative_support(tau, "tau", "Lognormal")
-        assert_negative_support(sigma, "sigma", "Lognormal")
+        assert_negative_support(tau, "tau", "LogNormal")
+        assert_negative_support(sigma, "sigma", "LogNormal")
 
     def _random(self, mu, tau, size=None):
         samples = np.random.normal(size=size)
@@ -1865,7 +1865,7 @@ def _random(self, mu, tau, size=None):
 
     def random(self, point=None, size=None):
         """
-        Draw random values from Lognormal distribution.
+        Draw random values from LogNormal distribution.
 
         Parameters
         ----------
@@ -1885,7 +1885,7 @@ def random(self, point=None, size=None):
 
     def logp(self, value):
         """
-        Calculate log-probability of Lognormal distribution at specified value.
+        Calculate log-probability of LogNormal distribution at specified value.
 
         Parameters
         ----------
@@ -1911,7 +1911,7 @@ def _distr_parameters_for_repr(self):
 
     def logcdf(self, value):
         """
-        Compute the log of the cumulative distribution function for Lognormal distribution
+        Compute the log of the cumulative distribution function for LogNormal distribution
         at the specified value.
 
         Parameters
@@ -1935,6 +1935,9 @@ def logcdf(self, value):
         )
 
 
+Lognormal = LogNormal
+
+
 class StudentT(Continuous):
     r"""
     Student's T log-likelihood.
diff --git a/pymc3/distributions/timeseries.py b/pymc3/distributions/timeseries.py
index e3e1aa15bc..bfd8be3b4c 100644
--- a/pymc3/distributions/timeseries.py
+++ b/pymc3/distributions/timeseries.py
@@ -484,7 +484,7 @@ def random(self, point=None, size=None):
 
 
         Examples
-        -------
+        --------
         .. code-block:: python
 
             with pm.Model():
diff --git a/pymc3/distributions/transforms.py b/pymc3/distributions/transforms.py
index 880301182c..8cb587e3ce 100644
--- a/pymc3/distributions/transforms.py
+++ b/pymc3/distributions/transforms.py
@@ -64,7 +64,7 @@ def forward(self, x):
             Input tensor to be transformed.
 
         Returns
-        --------
+        -------
         tensor
             Transformed tensor.
         """
@@ -82,7 +82,7 @@ def forward_val(self, x, point):
             Test value used to draw (fix) bounds-like transformations
 
         Returns
-        --------
+        -------
         array_like
             Transformed array.
         """
diff --git a/pymc3/sampling.py b/pymc3/sampling.py
index 3e63412f37..eda8b250d5 100644
--- a/pymc3/sampling.py
+++ b/pymc3/sampling.py
@@ -1388,8 +1388,11 @@ def _choose_backend(trace, chain, **kwds) -> Backend:
         A trace object for the selected chain
     """
     if isinstance(trace, BaseTrace):
+        if len(trace) > 0:
+            warnings.warn("Trace-continuation is not supported in v4.", FutureWarning)
         return trace
     if isinstance(trace, MultiTrace):
+        warnings.warn("Starting from MultiTrace objects is not supported in v4.", FutureWarning)
         return trace._straces[chain]
     if trace is None:
         return NDArray(**kwds)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 0f17952f5a..5cdd6ed2d1 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,7 +5,7 @@ deprecat
 h5py>=2.7
 ipython>=7.16
 nbsphinx>=0.4
-numpydoc>=0.9
+numpydoc<1.2
 pre-commit>=2.8.0
 pytest-cov>=2.5
 pytest>=3.0

From 4691af7e8992a49c91ebd0933c196af4df51d4fb Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Sun, 6 Feb 2022 13:45:45 +0100
Subject: [PATCH 44/48] Pin ranges for NumPy and SciPy

Version ranges for NumPy and SciPy were aligned between all CI envs.
Since Theano-PyMC is incompatible with the new SciPy 1.8.0,
this upper limit will most likely stay for all `v3` versions.

See tracking issue #5448.
---
 RELEASE-NOTES.md                            | 1 +
 conda-envs/environment-dev-py37.yml         | 5 ++---
 conda-envs/environment-dev-py38.yml         | 2 ++
 conda-envs/environment-dev-py39.yml         | 2 ++
 conda-envs/windows-environment-dev-py38.yml | 2 ++
 requirements-dev.txt                        | 1 +
 requirements.txt                            | 4 ++--
 7 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 6a76278874..e42c18934c 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -8,6 +8,7 @@
 + `pm.Lognormal` is now available as an alias for `pm.Lognormal` (see [#5389](https://github.com/pymc-devs/pymc/pull/5389)).
 
 ### Bugfixes
++ The upper limit for the SciPy version is `<1.8.0` and will most probably remain for all future `3.x.x` releases. For compatibility with newer SciPy versions please update to `pymc>=4.0.0`. Also see [#5448](https://github.com/pymc-devs/pymc/pull/5448).
 + A hotfix is applied on import to remain compatible with NumPy 1.22 (see [#5316](https://github.com/pymc-devs/pymc/pull/5316)).
 
 ## PyMC3 3.11.4 (20 August 2021)
diff --git a/conda-envs/environment-dev-py37.yml b/conda-envs/environment-dev-py37.yml
index 5ca7f3a08a..4d6ee0804d 100644
--- a/conda-envs/environment-dev-py37.yml
+++ b/conda-envs/environment-dev-py37.yml
@@ -8,9 +8,8 @@ dependencies:
 - libblas=*=*mkl
 - mkl-service
 - nbsphinx>=0.4
-- numpy=1.15
+- numpy>=1.15,<1.22.2
 - numpydoc<1.2
-- pandas=0.24
 - pip
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
@@ -18,7 +17,7 @@ dependencies:
 - python-graphviz
 - python=3.7
 - recommonmark>=0.4
-- scipy=1.2
+- scipy>=1.4.1,<1.8.0
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
diff --git a/conda-envs/environment-dev-py38.yml b/conda-envs/environment-dev-py38.yml
index 8d7ae55446..3029a2199b 100644
--- a/conda-envs/environment-dev-py38.yml
+++ b/conda-envs/environment-dev-py38.yml
@@ -8,6 +8,7 @@ dependencies:
 - libblas=*=*mkl
 - mkl-service
 - nbsphinx>=0.4
+- numpy>=1.15,<1.22.2
 - numpydoc<1.2
 - pip
 - pre-commit>=2.8.0
@@ -16,6 +17,7 @@ dependencies:
 - python-graphviz
 - python=3.8
 - recommonmark>=0.4
+- scipy>=1.4.1,<1.8.0
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
diff --git a/conda-envs/environment-dev-py39.yml b/conda-envs/environment-dev-py39.yml
index f2759028f2..6b8bbca6f3 100644
--- a/conda-envs/environment-dev-py39.yml
+++ b/conda-envs/environment-dev-py39.yml
@@ -8,6 +8,7 @@ dependencies:
 - libblas=*=*mkl
 - mkl-service
 - nbsphinx>=0.4
+- numpy>=1.15,<1.22.2
 - numpydoc<1.2
 - pip
 - pre-commit>=2.8.0
@@ -16,6 +17,7 @@ dependencies:
 - python-graphviz
 - python=3.9
 - recommonmark>=0.4
+- scipy>=1.4.1,<1.8.0
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
diff --git a/conda-envs/windows-environment-dev-py38.yml b/conda-envs/windows-environment-dev-py38.yml
index c058590e6e..4fbd18e269 100644
--- a/conda-envs/windows-environment-dev-py38.yml
+++ b/conda-envs/windows-environment-dev-py38.yml
@@ -16,11 +16,13 @@ dependencies:
 # Extra stuff for dev, testing and docs build
 - ipython>=7.16
 - nbsphinx>=0.4
+- numpy>=1.15,<1.22.2
 - numpydoc<1.2
 - pre-commit>=2.8.0
 - pytest-cov>=2.5
 - pytest>=3.0
 - recommonmark>=0.4
+- scipy>=1.4.1,<1.8.0
 - sphinx-autobuild>=0.7
 - sphinx>=1.5
 - watermark
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 5cdd6ed2d1..a8b4bb4273 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,6 +5,7 @@ deprecat
 h5py>=2.7
 ipython>=7.16
 nbsphinx>=0.4
+numpy>=1.15,<1.22.2
 numpydoc<1.2
 pre-commit>=2.8.0
 pytest-cov>=2.5
diff --git a/requirements.txt b/requirements.txt
index 80f31124bc..acdfc619d5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,10 +3,10 @@ cachetools>=4.2.1
 deprecat
 dill
 fastprogress>=0.2.0
-numpy>=1.15.0
+numpy>=1.15.0,<1.22.2
 pandas>=0.24.0
 patsy>=0.5.1
-scipy>=1.2.0
+scipy>=1.7.3,<1.8.0
 semver>=2.13.0
 theano-pymc==1.1.2
 typing-extensions>=3.7.4

From d76fd4c4d3ccd59d1acc279a0ce77b5d51c01347 Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Tue, 8 Feb 2022 22:40:31 +0100
Subject: [PATCH 45/48] Add XFAIL on `test_negative_binomial` under float32

This is a regression, likely caused by an updated NumPy/SciPy dependency.
Also see https://github.com/pymc-devs/pymc/pull/5450
---
 pymc3/tests/test_distributions.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py
index 351c65f7ab..a0a0ca505d 100644
--- a/pymc3/tests/test_distributions.py
+++ b/pymc3/tests/test_distributions.py
@@ -1082,6 +1082,10 @@ def modified_scipy_hypergeom_logcdf(value, N, k, n):
             {"N": NatSmall, "k": NatSmall, "n": NatSmall},
         )
 
+    @pytest.mark.xfail(
+        condition=(theano.config.floatX == "float32"),
+        reason="Fails on float32 due to a float casting problem in the logcdf",
+    )
     def test_negative_binomial(self):
         def scipy_mu_alpha_logpmf(value, mu, alpha):
             return sp.nbinom.logpmf(value, alpha, 1 - mu / (mu + alpha))

From ed74406735b2faf721e7ebfa156cc6828a5ae16e Mon Sep 17 00:00:00 2001
From: Michael Osthege <michael.osthege@outlook.com>
Date: Tue, 15 Mar 2022 09:30:24 +0100
Subject: [PATCH 46/48] Final release preparations (#5597)

---
 .github/workflows/release.yml | 2 +-
 RELEASE-NOTES.md              | 2 +-
 pymc3/__init__.py             | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ee74a0d494..8e28f2ce3b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,7 +9,7 @@ jobs:
   release-job:
     runs-on: ubuntu-latest
     env:
-      PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
+      PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_PYMC3 }}
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index e42c18934c..9eb5d46875 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,6 +1,6 @@
 # Release Notes
 
-## PyMC 3.11.5 (TBD)
+## PyMC 3.11.5 (14 March 2022)
 ### Backports
 + The `pm.logp(rv, x)` syntax is now available and recommended to make your model code `v4`-ready. Note that this backport is just an alias and much less capable than what's available with `pymc >=4` (see [#5083](https://github.com/pymc-devs/pymc/pulls/5083)).
 + The `pm.Distribution(testval=...)` kwarg was deprecated and will be replaced by `pm.Distribution(initval=...)`in `pymc >=4` (see [#5226](https://github.com/pymc-devs/pymc/pulls/5226)).
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index e71f0cbee1..80083f88ee 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -13,7 +13,7 @@
 #   limitations under the License.
 
 # pylint: disable=wildcard-import
-__version__ = "3.11.4"
+__version__ = "3.11.5"
 
 import logging
 import multiprocessing as mp

From 7d50de8e13422019808210d5c4222fa425c47eae Mon Sep 17 00:00:00 2001
From: Osvaldo A Martin <aloctavodia@gmail.com>
Date: Thu, 30 May 2024 15:46:58 -0300
Subject: [PATCH 47/48] add legacy messages (#7338)

---
 README.rst        | 6 ++++++
 pymc3/__init__.py | 4 ++++
 pymc3/sampling.py | 6 ++++++
 3 files changed, 16 insertions(+)

diff --git a/README.rst b/README.rst
index b09e641106..629a84d8cf 100644
--- a/README.rst
+++ b/README.rst
@@ -5,6 +5,12 @@
 
 |Build Status| |Coverage| |NumFOCUS_badge| |Binder| |Dockerhub|
 
+.. warning::
+   This is the legacy version of PyMC3, now renamed to PyMC.
+   If you are looking for the latest version of PyMC, please visit
+   `PyMC's documentation <https://www.pymc.io/welcome.html>`__
+   
+
 PyMC3 is a Python package for Bayesian statistical modeling and Probabilistic Machine Learning
 focusing on advanced Markov chain Monte Carlo (MCMC) and variational inference (VI)
 algorithms. Its flexibility and extensibility make it applicable to a
diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index 80083f88ee..d59e9ce3d2 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -52,6 +52,10 @@
         handler = logging.StreamHandler()
         _log.addHandler(handler)
 
+_log.warning("The version of PyMC you are using is very outdated.\n\nPlease upgrade to the latest "
+             "version of PyMC https://www.pymc.io/projects/docs/en/stable/installation.html\n\n"
+             "Also notice that PyMC3 has been renamed to PyMC."
+             )
 
 def _check_backend_version():
     backend_paths = theano.__spec__.submodule_search_locations
diff --git a/pymc3/sampling.py b/pymc3/sampling.py
index eda8b250d5..4b5ebf779a 100644
--- a/pymc3/sampling.py
+++ b/pymc3/sampling.py
@@ -434,10 +434,16 @@ def sample(
             mean     sd  hdi_3%  hdi_97%
         p  0.609  0.047   0.528    0.699
     """
+    _log.info("The version of PyMC you are using is very outdated.\n\nPlease upgrade to the latest "
+                "version of PyMC https://www.pymc.io/projects/docs/en/stable/installation.html\n\n"
+                "Also notice that PyMC3 has been renamed to PyMC."
+                )
+
     # Handle deprecated/forwards-compatible kwargs
     if initvals is not None:
         start = initvals
 
+
     model = modelcontext(model)
     start = deepcopy(start)
     if start is None:

From 1d00f3eb81723523968f3610e81a0c42fd96326f Mon Sep 17 00:00:00 2001
From: Osvaldo A Martin <aloctavodia@gmail.com>
Date: Thu, 30 May 2024 19:14:31 -0300
Subject: [PATCH 48/48] Update __init__.py

---
 pymc3/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pymc3/__init__.py b/pymc3/__init__.py
index d59e9ce3d2..0a5f7caad5 100644
--- a/pymc3/__init__.py
+++ b/pymc3/__init__.py
@@ -13,7 +13,7 @@
 #   limitations under the License.
 
 # pylint: disable=wildcard-import
-__version__ = "3.11.5"
+__version__ = "3.11.6"
 
 import logging
 import multiprocessing as mp
