Skip to content

Commit

Permalink
Merge pull request #360 from jcapriot/random_generator
Browse files Browse the repository at this point in the history
Update use of `numpy`'s random number generators.
  • Loading branch information
jcapriot authored Sep 8, 2024
2 parents 98caca7 + 186130e commit b0285e4
Show file tree
Hide file tree
Showing 50 changed files with 518 additions and 375 deletions.
4 changes: 3 additions & 1 deletion .ci/environment_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ channels:
dependencies:
- numpy>=1.22.4
- scipy>=1.8

# optionals
- vtk>=6
- pyvista
- omf
- matplotlib

# documentation
- sphinx
- pydata-sphinx-theme==0.13.3
- sphinx-gallery>=0.1.13
- numpydoc>=1.5
- jupyter
- graphviz
- pymatsolver>=0.1.2
- pillow
- pooch

# testing
- sympy
- pytest
Expand Down
24 changes: 12 additions & 12 deletions discretize/base/base_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -2419,15 +2419,15 @@ def get_face_inner_product_deriv(
>>> import numpy as np
>>> import matplotlib as mpl
>>> mpl.rcParams.update({'font.size': 14})
>>> np.random.seed(45)
>>> rng = np.random.default_rng(45)
>>> mesh = TensorMesh([[(1, 4)], [(1, 4)]])
Define a model, and a random vector to multiply the derivative with,
then we grab the respective derivative function and calculate the
sparse matrix,
>>> m = np.random.rand(mesh.nC) # physical property parameters
>>> u = np.random.rand(mesh.nF) # vector of shape (n_faces)
>>> m = rng.random(mesh.nC) # physical property parameters
>>> u = rng.random(mesh.nF) # vector of shape (n_faces)
>>> Mf = mesh.get_face_inner_product(m)
>>> F = mesh.get_face_inner_product_deriv(m) # Function handle
>>> dFdm_u = F(u)
Expand Down Expand Up @@ -2458,8 +2458,8 @@ def get_face_inner_product_deriv(
function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation
of this function on a spy plot.
>>> m = np.random.rand(mesh.nC, 3) # anisotropic physical property parameters
>>> u = np.random.rand(mesh.nF) # vector of shape (n_faces)
>>> m = rng.random((mesh.nC, 3)) # anisotropic physical property parameters
>>> u = rng.random(mesh.nF) # vector of shape (n_faces)
>>> Mf = mesh.get_face_inner_product(m)
>>> F = mesh.get_face_inner_product_deriv(m) # Function handle
>>> dFdm_u = F(u)
Expand Down Expand Up @@ -2602,14 +2602,14 @@ def get_edge_inner_product_deriv(
>>> import numpy as np
>>> import matplotlib as mpl
>>> mpl.rcParams.update({'font.size': 14})
>>> np.random.seed(45)
>>> rng = np.random.default_rng(45)
>>> mesh = TensorMesh([[(1, 4)], [(1, 4)]])
Next we create a random isotropic model vector, and a random vector to multiply
the derivative with (for illustration purposes).
>>> m = np.random.rand(mesh.nC) # physical property parameters
>>> u = np.random.rand(mesh.nF) # vector of shape (n_edges)
>>> m = rng.random(mesh.nC) # physical property parameters
>>> u = rng.random(mesh.nF) # vector of shape (n_edges)
>>> Me = mesh.get_edge_inner_product(m)
>>> F = mesh.get_edge_inner_product_deriv(m) # Function handle
>>> dFdm_u = F(u)
Expand Down Expand Up @@ -2640,8 +2640,8 @@ def get_edge_inner_product_deriv(
function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation
of this function on a spy plot.
>>> m = np.random.rand(mesh.nC, 3) # physical property parameters
>>> u = np.random.rand(mesh.nF) # vector of shape (n_edges)
>>> m = rng.random((mesh.nC, 3)) # physical property parameters
>>> u = rng.random(mesh.nF) # vector of shape (n_edges)
>>> Me = mesh.get_edge_inner_product(m)
>>> F = mesh.get_edge_inner_product_deriv(m) # Function handle
>>> dFdm_u = F(u)
Expand Down Expand Up @@ -4128,9 +4128,9 @@ def get_interpolation_matrix(
>>> from discretize import TensorMesh
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> np.random.seed(14)
>>> rng = np.random.default_rng(14)
>>> locs = np.random.rand(50)*0.8+0.1
>>> locs = rng.random(50)*0.8+0.1
>>> dense = np.linspace(0, 1, 200)
>>> fun = lambda x: np.cos(2*np.pi*x)
Expand Down
4 changes: 2 additions & 2 deletions discretize/mixins/mpl_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def plot_slice(
>>> from matplotlib import pyplot as plt
>>> import discretize
>>> from pymatsolver import Solver
>>> from scipy.sparse.linalg import spsolve
>>> hx = [(5, 2, -1.3), (2, 4), (5, 2, 1.3)]
>>> hy = [(2, 2, -1.3), (2, 6), (2, 2, 1.3)]
>>> hz = [(2, 2, -1.3), (2, 6), (2, 2, 1.3)]
Expand All @@ -482,7 +482,7 @@ def plot_slice(
>>> q[[4, 4], [4, 4], [2, 6]]=[-1, 1]
>>> q = discretize.utils.mkvc(q)
>>> A = M.face_divergence * M.cell_gradient
>>> b = Solver(A) * (q)
>>> b = spsolve(A, q)
and finaly, plot the vector values of the result, which are defined on faces
Expand Down
88 changes: 60 additions & 28 deletions discretize/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@
"You break it, you fix it.",
]

# Initiate random number generator
rng = np.random.default_rng()
_happiness_rng = np.random.default_rng()


def setup_mesh(mesh_type, nC, nDim):
def setup_mesh(mesh_type, nC, nDim, random_seed=None):
"""Generate arbitrary mesh for testing.
For the mesh type, number of cells along each axis and dimension specified,
Expand All @@ -100,19 +99,25 @@ def setup_mesh(mesh_type, nC, nDim):
number of base mesh cells and must be a power of 2.
nDim : int
The dimension of the mesh. Must be 1, 2 or 3.
random_seed : numpy.random.Generator, int, optional
If ``random`` is in `mesh_type`, this is the random number generator to use for
creating that random mesh. If an integer or None it is used to seed a new
`numpy.random.default_rng`.
Returns
-------
discretize.base.BaseMesh
A discretize mesh of class specified by the input argument *mesh_type*
"""
if "random" in mesh_type:
rng = np.random.default_rng(random_seed)
if "TensorMesh" in mesh_type:
if "uniform" in mesh_type:
h = [nC, nC, nC]
elif "random" in mesh_type:
h1 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h3 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h1 = rng.random(nC) * nC * 0.5 + nC * 0.5
h2 = rng.random(nC) * nC * 0.5 + nC * 0.5
h3 = rng.random(nC) * nC * 0.5 + nC * 0.5
h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize
else:
raise Exception("Unexpected mesh_type")
Expand All @@ -127,14 +132,14 @@ def setup_mesh(mesh_type, nC, nDim):
else:
h = [nC, nC, nC]
elif "random" in mesh_type:
h1 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h1 = rng.random(nC) * nC * 0.5 + nC * 0.5
if "symmetric" in mesh_type:
h2 = [
2 * np.pi,
]
else:
h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h3 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h2 = rng.random(nC) * nC * 0.5 + nC * 0.5
h3 = rng.random(nC) * nC * 0.5 + nC * 0.5
h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize
h[1] = h[1] * 2 * np.pi
else:
Expand Down Expand Up @@ -179,9 +184,9 @@ def setup_mesh(mesh_type, nC, nDim):
if "uniform" in mesh_type or "notatree" in mesh_type:
h = [nC, nC, nC]
elif "random" in mesh_type:
h1 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h3 = np.random.rand(nC) * nC * 0.5 + nC * 0.5
h1 = rng.random(nC) * nC * 0.5 + nC * 0.5
h2 = rng.random(nC) * nC * 0.5 + nC * 0.5
h3 = rng.random(nC) * nC * 0.5 + nC * 0.5
h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize
else:
raise Exception("Unexpected mesh_type")
Expand Down Expand Up @@ -216,13 +221,13 @@ class for the given operator. Within the test class, the user sets the parameter
OrderTest inherits from :class:`unittest.TestCase`.
Parameters
Attributes
----------
name : str
Name the convergence test
meshTypes : list of str
List denoting the mesh types on which the convergence will be tested.
List entries must of the list {'uniformTensorMesh', 'randomTensorMesh',
List entries must be of the list {'uniformTensorMesh', 'randomTensorMesh',
'uniformCylindricalMesh', 'randomCylindricalMesh', 'uniformTree', 'randomTree',
'uniformCurv', 'rotateCurv', 'sphereCurv'}
expectedOrders : float or list of float (default = 2.0)
Expand All @@ -236,6 +241,10 @@ class for the given operator. Within the test class, the user sets the parameter
for the meshes used in the convergence test; e.g. [4, 8, 16, 32]
meshDimension : int
Mesh dimension. Must be 1, 2 or 3
random_seed : numpy.random.Generator, int, optional
If ``random`` is in `mesh_type`, this is the random number generator
used generate the random meshes, if an ``int`` or ``None``, it used to seed
a new `numpy.random.default_rng`.
Notes
-----
Expand Down Expand Up @@ -302,6 +311,7 @@ class for the given operator. Within the test class, the user sets the parameter
meshTypes = ["uniformTensorMesh"]
_meshType = meshTypes[0]
meshDimension = 3
random_seed = None

def setupMesh(self, nC):
"""Generate mesh and set as current mesh for testing.
Expand All @@ -316,7 +326,9 @@ def setupMesh(self, nC):
Float
Maximum cell width for the mesh
"""
mesh, max_h = setup_mesh(self._meshType, nC, self.meshDimension)
mesh, max_h = setup_mesh(
self._meshType, nC, self.meshDimension, random_seed=self.random_seed
)
self.M = mesh
return max_h

Expand All @@ -335,7 +347,7 @@ def getError(self):
"""
return 1.0

def orderTest(self):
def orderTest(self, random_seed=None):
"""Perform an order test.
For number of cells specified in meshSizes setup mesh, call getError
Expand All @@ -360,6 +372,9 @@ def orderTest(self):
"expectedOrders must have the same length as the meshTypes"
)

if random_seed is not None:
self.random_seed = random_seed

def test_func(n_cells):
max_h = self.setupMesh(n_cells)
err = self.getError()
Expand Down Expand Up @@ -496,9 +511,9 @@ class in older versions of `discretize`.
np.testing.assert_allclose(orders[-1], expected_order, rtol=rtol)
elif test_type == "all":
np.testing.assert_allclose(orders, expected_order, rtol=rtol)
print(np.random.choice(happiness))
print(_happiness_rng.choice(happiness))
except AssertionError as err:
print(np.random.choice(sadness))
print(_happiness_rng.choice(sadness))
raise err

return orders
Expand Down Expand Up @@ -552,6 +567,7 @@ def check_derivative(
tolerance=0.85,
eps=1e-10,
ax=None,
random_seed=None,
):
"""Perform a basic derivative check.
Expand All @@ -560,8 +576,8 @@ def check_derivative(
Parameters
----------
fctn : function
Function handle
fctn : callable
The function to test.
x0 : numpy.ndarray
Point at which to check derivative
num : int, optional
Expand All @@ -570,7 +586,7 @@ def check_derivative(
If *True*, plot the convergence of the approximation of the derivative
dx : numpy.ndarray, optional
Step direction. By default, this parameter is set to *None* and a random
step direction is chosen.
step direction is chosen using `rng`.
expectedOrder : int, optional
The expected order of convergence for the numerical derivative
tolerance : float, optional
Expand All @@ -580,6 +596,10 @@ def check_derivative(
ax : matplotlib.pyplot.Axes, optional
An axis object for the convergence plot if *plotIt = True*.
Otherwise, the function will create a new axis.
random_seed : numpy.random.Generator, int, optional
If `dx` is ``None``, this is the random number generator to use for
generating a step direction. If an integer or None, it is used to seed
a new `numpy.random.default_rng`.
Returns
-------
Expand All @@ -591,10 +611,11 @@ def check_derivative(
>>> from discretize import tests, utils
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> rng = np.random.default_rng(786412)
>>> def simplePass(x):
... return np.sin(x), utils.sdiag(np.cos(x))
>>> passed = tests.check_derivative(simplePass, np.random.randn(5))
>>> passed = tests.check_derivative(simplePass, rng.standard_normal(5), random_seed=rng)
==================== check_derivative ====================
iter h |ft-f0| |ft-f0-h*J0*dx| Order
---------------------------------------------------------
Expand All @@ -611,6 +632,7 @@ def check_derivative(
__tracebackhide__ = True
# matplotlib is a soft dependencies for discretize,
# lazy-loaded to decrease load time of discretize.

try:
import matplotlib
import matplotlib.pyplot as plt
Expand All @@ -627,7 +649,8 @@ def check_derivative(
x0 = mkvc(x0)

if dx is None:
dx = np.random.randn(len(x0))
rng = np.random.default_rng(random_seed)
dx = rng.standard_normal(len(x0))

h = np.logspace(-1, -num, num)
E0 = np.ones(h.shape)
Expand Down Expand Up @@ -693,14 +716,17 @@ def _plot_it(axes, passed):
# Thus it has no higher order derivatives.
pass
else:
test = np.mean(order1) > tolerance * expectedOrder
order_mean = np.mean(order1)
expected = tolerance * expectedOrder
test = order_mean > expected
if not test:
raise AssertionError(
f"\n Order mean {np.mean(order1)} is not greater than"
f" {tolerance} of the expected order {expectedOrder}."
f"\n Order mean {order_mean} is not greater than"
f" {expected} = tolerance: {tolerance} "
f"* expected order: {expectedOrder}."
)
print("{0!s} PASS! {1!s}".format("=" * 25, "=" * 25))
print(np.random.choice(happiness) + "\n")
print(_happiness_rng.choice(happiness) + "\n")
if plotIt:
_plot_it(ax, True)
except AssertionError as err:
Expand All @@ -709,7 +735,7 @@ def _plot_it(axes, passed):
"*" * 57, "<" * 25, ">" * 25, "*" * 57
)
)
print(np.random.choice(sadness) + "\n")
print(_happiness_rng.choice(sadness) + "\n")
if plotIt:
_plot_it(ax, False)
raise err
Expand Down Expand Up @@ -773,6 +799,7 @@ def assert_isadjoint(
rtol=1e-6,
atol=0.0,
assert_error=True,
random_seed=None,
):
r"""Do a dot product test for the forward operator and its adjoint operator.
Expand Down Expand Up @@ -823,6 +850,9 @@ def assert_isadjoint(
assertion error if failed). If set to False, the result of the test is
returned as boolean and a message is printed.
random_seed : numpy.random.Generator, int, optional
The random number generator to use for the adjoint test. If an integer or None
it is used to seed a new `numpy.random.default_rng`.
Returns
-------
Expand All @@ -837,6 +867,8 @@ def assert_isadjoint(
"""
__tracebackhide__ = True

rng = np.random.default_rng(random_seed)

def random(size, iscomplex):
"""Create random data of size and dtype of <size>."""
out = rng.standard_normal(size)
Expand Down
Loading

0 comments on commit b0285e4

Please sign in to comment.