Commit 4fe96fe8 authored by Philipp Arras's avatar Philipp Arras
Browse files

Restructure operator tests

parent fb0a4dc3
Pipeline #76912 passed with stages
in 12 minutes and 1 second
Changes since NIFTy 6
=====================
*None.*
Naming of operator tests
------------------------
The implementation tests for nonlinear operators are now available in
`ift.extra.check_operator()` and for linear operators
`ift.extra.check_linear_operator()`.
Changes since NIFTy 5
......
......@@ -97,7 +97,7 @@ def main():
p_space = ift.UnstructuredDomain(N_params)
params = ift.full(p_space, 0.)
R = PolynomialResponse(p_space, x)
ift.extra.consistency_check(R)
ift.extra.check_linear_operator(R)
d_space = R.target
d = ift.makeField(d_space, y)
......
......@@ -15,6 +15,8 @@
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
from itertools import combinations
import numpy as np
from numpy.testing import assert_
......@@ -23,13 +25,103 @@ from .field import Field
from .linearization import Linearization
from .multi_domain import MultiDomain
from .multi_field import MultiField
from .operators.energy_operators import EnergyOperator
from .operators.linear_operator import LinearOperator
from .sugar import from_random
from .operators.operator import Operator
from .sugar import from_random, full, makeDomain
__all__ = ["consistency_check", "check_jacobian_consistency",
__all__ = ["check_linear_operator", "check_operator",
"assert_allclose"]
def check_linear_operator(op, domain_dtype=np.float64, target_dtype=np.float64,
atol=0, rtol=1e-7, only_r_linear=False):
"""
Checks an operator for algebraic consistency of its capabilities.
Checks whether times(), adjoint_times(), inverse_times() and
adjoint_inverse_times() (if in capability list) is implemented
consistently. Additionally, it checks whether the operator is linear.
Parameters
----------
op : LinearOperator
Operator which shall be checked.
domain_dtype : dtype
The data type of the random vectors in the operator's domain. Default
is `np.float64`.
target_dtype : dtype
The data type of the random vectors in the operator's target. Default
is `np.float64`.
atol : float
Absolute tolerance for the check. If rtol is specified,
then satisfying any tolerance will let the check pass.
Default: 0.
rtol : float
Relative tolerance for the check. If atol is specified,
then satisfying any tolerance will let the check pass.
Default: 0.
only_r_linear: bool
set to True if the operator is only R-linear, not C-linear.
This will relax the adjointness test accordingly.
"""
if not isinstance(op, LinearOperator):
raise TypeError('This test tests only linear operators.')
_domain_check_linear(op, domain_dtype)
_domain_check_linear(op.adjoint, target_dtype)
_domain_check_linear(op.inverse, target_dtype)
_domain_check_linear(op.adjoint.inverse, domain_dtype)
_check_linearity(op, domain_dtype, atol, rtol)
_check_linearity(op.adjoint, target_dtype, atol, rtol)
_check_linearity(op.inverse, target_dtype, atol, rtol)
_check_linearity(op.adjoint.inverse, domain_dtype, atol, rtol)
_full_implementation(op, domain_dtype, target_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.adjoint, target_dtype, domain_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.inverse, target_dtype, domain_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.adjoint.inverse, domain_dtype, target_dtype, atol,
rtol, only_r_linear)
def check_operator(op, loc, tol=1e-8, ntries=100, perf_check=True,
only_r_differentiable=True, metric_sampling=True):
"""
Performs various checks of the implementation of linear and nonlinear
operators.
Computes the Jacobian with finite differences and compares it to the
implemented Jacobian.
Parameters
----------
op : Operator
Operator which shall be checked.
loc : Field or MultiField
An Field or MultiField instance which has the same domain
as op. The location at which the gradient is checked
tol : float
Tolerance for the check.
perf_check : Boolean
Do performance check. May be disabled for very unimportant operators.
only_r_differentiable : Boolean
Jacobians of C-differentiable operators need to be C-linear.
Default: True
metric_sampling: Boolean
If op is an EnergyOperator, metric_sampling determines whether the
test shall try to sample from the metric or not.
"""
if not isinstance(op, Operator):
raise TypeError('This test tests only linear operators.')
_domain_check_nonlinear(op, loc)
_performance_check(op, loc, bool(perf_check))
_linearization_value_consistency(op, loc)
_jac_vs_finite_differences(op, loc, tol, ntries, only_r_differentiable)
_check_nontrivial_constant(op, loc, tol, ntries, only_r_differentiable,
metric_sampling)
def assert_allclose(f1, f2, atol, rtol):
if isinstance(f1, Field):
return np.testing.assert_allclose(f1.val, f2.val, atol=atol, rtol=rtol)
......@@ -44,6 +136,20 @@ def assert_equal(f1, f2):
assert_equal(val, f2[key])
def _nozero(fld):
if isinstance(fld, Field):
return np.testing.assert_((fld != 0).s_all())
for val in fld.values():
_nozero(val)
def _allzero(fld):
if isinstance(fld, Field):
return np.testing.assert_((fld == 0.).s_all())
for val in fld.values():
_allzero(val)
def _adjoint_implementation(op, domain_dtype, target_dtype, atol, rtol,
only_r_linear):
needed_cap = op.TIMES | op.ADJOINT_TIMES
......@@ -90,7 +196,8 @@ def _check_linearity(op, domain_dtype, atol, rtol):
assert_allclose(val1, val2, atol=atol, rtol=rtol)
def _actual_domain_check_linear(op, domain_dtype=None, inp=None):
def _domain_check_linear(op, domain_dtype=None, inp=None):
_domain_check(op)
needed_cap = op.TIMES
if (op.capability & needed_cap) != needed_cap:
return
......@@ -102,8 +209,9 @@ def _actual_domain_check_linear(op, domain_dtype=None, inp=None):
assert_(op(inp).domain is op.target)
def _actual_domain_check_nonlinear(op, loc):
assert isinstance(loc, (Field, MultiField))
def _domain_check_nonlinear(op, loc):
_domain_check(op)
assert_(isinstance(loc, (Field, MultiField)))
assert_(loc.domain is op.domain)
for wm in [False, True]:
lin = Linearization.make_var(loc, wm)
......@@ -118,8 +226,8 @@ def _actual_domain_check_nonlinear(op, loc):
assert_(reslin.jac.domain is reslin.domain)
assert_(reslin.jac.target is reslin.target)
assert_(lin.want_metric == reslin.want_metric)
_actual_domain_check_linear(reslin.jac, inp=loc)
_actual_domain_check_linear(reslin.jac.adjoint, inp=reslin.jac(loc))
_domain_check_linear(reslin.jac, inp=loc)
_domain_check_linear(reslin.jac.adjoint, inp=reslin.jac(loc))
if reslin.metric is not None:
assert_(reslin.metric.domain is reslin.metric.target)
assert_(reslin.metric.domain is op.domain)
......@@ -171,58 +279,6 @@ def _performance_check(op, pos, raise_on_fail):
raise RuntimeError(s)
def consistency_check(op, domain_dtype=np.float64, target_dtype=np.float64,
atol=0, rtol=1e-7, only_r_linear=False):
"""
Checks an operator for algebraic consistency of its capabilities.
Checks whether times(), adjoint_times(), inverse_times() and
adjoint_inverse_times() (if in capability list) is implemented
consistently. Additionally, it checks whether the operator is linear.
Parameters
----------
op : LinearOperator
Operator which shall be checked.
domain_dtype : dtype
The data type of the random vectors in the operator's domain. Default
is `np.float64`.
target_dtype : dtype
The data type of the random vectors in the operator's target. Default
is `np.float64`.
atol : float
Absolute tolerance for the check. If rtol is specified,
then satisfying any tolerance will let the check pass.
Default: 0.
rtol : float
Relative tolerance for the check. If atol is specified,
then satisfying any tolerance will let the check pass.
Default: 0.
only_r_linear: bool
set to True if the operator is only R-linear, not C-linear.
This will relax the adjointness test accordingly.
"""
if not isinstance(op, LinearOperator):
raise TypeError('This test tests only linear operators.')
_domain_check(op)
_actual_domain_check_linear(op, domain_dtype)
_actual_domain_check_linear(op.adjoint, target_dtype)
_actual_domain_check_linear(op.inverse, target_dtype)
_actual_domain_check_linear(op.adjoint.inverse, domain_dtype)
_check_linearity(op, domain_dtype, atol, rtol)
_check_linearity(op.adjoint, target_dtype, atol, rtol)
_check_linearity(op.inverse, target_dtype, atol, rtol)
_check_linearity(op.adjoint.inverse, domain_dtype, atol, rtol)
_full_implementation(op, domain_dtype, target_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.adjoint, target_dtype, domain_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.inverse, target_dtype, domain_dtype, atol, rtol,
only_r_linear)
_full_implementation(op.adjoint.inverse, domain_dtype, target_dtype, atol,
rtol, only_r_linear)
def _get_acceptable_location(op, loc, lin):
if not np.isfinite(lin.val.s_sum()):
raise ValueError('Initial value must be finite')
......@@ -255,34 +311,46 @@ def _linearization_value_consistency(op, loc):
assert_allclose(fld0, fld1, 0, 1e-7)
def check_jacobian_consistency(op, loc, tol=1e-8, ntries=100, perf_check=True,
only_r_differentiable=True):
"""
Checks the Jacobian of an operator against its finite difference
approximation.
Computes the Jacobian with finite differences and compares it to the
implemented Jacobian.
Parameters
----------
op : Operator
Operator which shall be checked.
loc : Field or MultiField
An Field or MultiField instance which has the same domain
as op. The location at which the gradient is checked
tol : float
Tolerance for the check.
perf_check : Boolean
Do performance check. May be disabled for very unimportant operators.
only_r_differentiable : Boolean
Jacobians of C-differentiable operators need to be C-linear.
Default: True
"""
_domain_check(op)
_actual_domain_check_nonlinear(op, loc)
_performance_check(op, loc, bool(perf_check))
_linearization_value_consistency(op, loc)
def _check_nontrivial_constant(op, loc, tol, ntries, only_r_differentiable,
metric_sampling):
return # FIXME
# Assumes that the operator is not constant
if isinstance(op.domain, DomainTuple):
return
keys = op.domain.keys()
for ll in range(0, len(keys)):
for cstkeys in combinations(keys, ll):
cstdom, vardom = {}, {}
for kk, dd in op.domain.items():
if kk in cstkeys:
cstdom[kk] = dd
else:
vardom[kk] = dd
cstdom, vardom = makeDomain(cstdom), makeDomain(vardom)
cstloc = loc.extract(cstdom)
val0 = op(loc)
_, op0 = op.simplify_for_constant_input(cstloc)
val1 = op0(loc)
val2 = op0(loc.unite(cstloc))
assert_equal(val1, val2)
assert_equal(val0, val1)
lin = Linearization.make_var(loc, want_metric=True)
oplin = op0(lin)
if isinstance(op, EnergyOperator):
_allzero(oplin.gradient.extract(cstdom))
_allzero(oplin.jac(from_random(cstdom).unite(full(vardom, 0))))
if isinstance(op, EnergyOperator) and metric_sampling:
samp0 = oplin.metric.draw_sample()
_allzero(samp0.extract(cstdom))
_nozero(samp0.extract(vardom))
_jac_vs_finite_differences(op0, loc, tol, ntries, only_r_differentiable)
def _jac_vs_finite_differences(op, loc, tol, ntries, only_r_differentiable):
for _ in range(ntries):
lin = op(Linearization.make_var(loc))
loc2, lin2 = _get_acceptable_location(op, loc, lin)
......@@ -307,8 +375,6 @@ def check_jacobian_consistency(op, loc, tol=1e-8, ntries=100, perf_check=True,
print(hist)
raise ValueError("gradient and value seem inconsistent")
loc = locnext
ddtype = loc.values()[0].dtype if isinstance(loc, MultiField) else loc.dtype
tdtype = dirder.values()[0].dtype if isinstance(dirder, MultiField) else dirder.dtype
consistency_check(linmid.jac, domain_dtype=ddtype, target_dtype=tdtype,
only_r_linear=only_r_differentiable)
check_linear_operator(linmid.jac, domain_dtype=loc.dtype,
target_dtype=dirder.dtype,
only_r_linear=only_r_differentiable)
......@@ -32,13 +32,13 @@ ntries = 10
def test_gaussian(field):
energy = ift.GaussianEnergy(domain=field.domain)
ift.extra.check_jacobian_consistency(energy, field)
ift.extra.check_operator(energy, field)
def test_ScaledEnergy(field):
icov = ift.ScalingOperator(field.domain, 1.2)
energy = ift.GaussianEnergy(inverse_covariance=icov, sampling_dtype=np.float64)
ift.extra.check_jacobian_consistency(energy.scale(0.3), field)
ift.extra.check_operator(energy.scale(0.3), field)
lin = ift.Linearization.make_var(field, want_metric=True)
met1 = energy(lin).metric
......@@ -54,17 +54,17 @@ def test_QuadraticFormOperator(field):
op = ift.ScalingOperator(field.domain, 1.2)
endo = ift.makeOp(op.draw_sample_with_dtype(dtype=np.float64))
energy = ift.QuadraticFormOperator(endo)
ift.extra.check_jacobian_consistency(energy, field)
ift.extra.check_operator(energy, field)
def test_studentt(field):
if isinstance(field.domain, ift.MultiDomain):
return
energy = ift.StudentTEnergy(domain=field.domain, theta=.5)
ift.extra.check_jacobian_consistency(energy, field, tol=1e-6)
ift.extra.check_operator(energy, field, tol=1e-6)
theta = ift.from_random(field.domain, 'normal').exp()
energy = ift.StudentTEnergy(domain=field.domain, theta=theta)
ift.extra.check_jacobian_consistency(energy, field, tol=1e-6, ntries=ntries)
ift.extra.check_operator(energy, field, tol=1e-6, ntries=ntries)
def test_hamiltonian_and_KL(field):
......@@ -72,10 +72,10 @@ def test_hamiltonian_and_KL(field):
space = field.domain
lh = ift.GaussianEnergy(domain=space)
hamiltonian = ift.StandardHamiltonian(lh)
ift.extra.check_jacobian_consistency(hamiltonian, field, ntries=ntries)
ift.extra.check_operator(hamiltonian, field, ntries=ntries)
samps = [ift.from_random(space, 'normal') for i in range(2)]
kl = ift.AveragedEnergy(hamiltonian, samps)
ift.extra.check_jacobian_consistency(kl, field, ntries=ntries)
ift.extra.check_operator(kl, field, ntries=ntries)
def test_variablecovariancegaussian(field):
......@@ -84,7 +84,7 @@ def test_variablecovariancegaussian(field):
dc = {'a': field, 'b': field.ptw("exp")}
mf = ift.MultiField.from_dict(dc)
energy = ift.VariableCovarianceGaussianEnergy(field.domain, 'a', 'b', np.float64)
ift.extra.check_jacobian_consistency(energy, mf, tol=1e-6, ntries=ntries)
ift.extra.check_operator(energy, mf, tol=1e-6, ntries=ntries)
energy(ift.Linearization.make_var(mf, want_metric=True)).metric.draw_sample()
......@@ -93,7 +93,7 @@ def test_specialgamma(field):
return
energy = ift.operators.energy_operators._SpecialGammaEnergy(field)
loc = ift.from_random(energy.domain).exp()
ift.extra.check_jacobian_consistency(energy, loc, tol=1e-6, ntries=ntries)
ift.extra.check_operator(energy, loc, tol=1e-6, ntries=ntries)
energy(ift.Linearization.make_var(loc, want_metric=True)).metric.draw_sample()
......@@ -105,7 +105,7 @@ def test_inverse_gamma(field):
d = ift.random.current_rng().normal(10, size=space.shape)**2
d = ift.Field(space, d)
energy = ift.InverseGammaLikelihood(d)
ift.extra.check_jacobian_consistency(energy, field, tol=1e-5)
ift.extra.check_operator(energy, field, tol=1e-5)
def testPoissonian(field):
......@@ -116,7 +116,7 @@ def testPoissonian(field):
d = ift.random.current_rng().poisson(120, size=space.shape)
d = ift.Field(space, d)
energy = ift.PoissonianEnergy(d)
ift.extra.check_jacobian_consistency(energy, field, tol=1e-6)
ift.extra.check_operator(energy, field, tol=1e-6)
def test_bernoulli(field):
......@@ -127,4 +127,4 @@ def test_bernoulli(field):
d = ift.random.current_rng().binomial(1, 0.1, size=space.shape)
d = ift.Field(space, d)
energy = ift.BernoulliEnergy(d)
ift.extra.check_jacobian_consistency(energy, field, tol=1e-5)
ift.extra.check_operator(energy, field, tol=1e-5)
......@@ -70,7 +70,7 @@ def test_gaussian_energy(space, nonlinearity, noise, seed):
N = None
energy = ift.GaussianEnergy(d, N) @ d_model()
ift.extra.check_jacobian_consistency(
ift.extra.check_operator(
energy, xi0, ntries=ntries, tol=1e-6)
......@@ -99,9 +99,9 @@ def testgaussianenergy_compatibility(cplx):
np.testing.assert_equal(val0, val1)
np.testing.assert_equal(val1, val2)
ift.extra.check_jacobian_consistency(e, loc, ntries=ntries)
ift.extra.check_jacobian_consistency(e0, loc, ntries=ntries)
ift.extra.check_jacobian_consistency(e1, loc, ntries=ntries)
ift.extra.check_operator(e, loc, ntries=ntries)
ift.extra.check_operator(e0, loc, ntries=ntries, tol=1e-7)
ift.extra.check_operator(e1, loc, ntries=ntries)
# Test jacobian is zero
lin = ift.Linearization.make_var(loc, want_metric=True)
......
......@@ -61,7 +61,7 @@ def test_blockdiagonal():
op = ift.BlockDiagonalOperator(
dom, {"d1": ift.ScalingOperator(dom["d1"], 20.)})
op2 = op(op)
ift.extra.consistency_check(op2)
ift.extra.check_linear_operator(op2)
assert_equal(type(op2), ift.BlockDiagonalOperator)
f1 = op2(ift.full(dom, 1))
for val in f1.values():
......
......@@ -42,7 +42,7 @@ def testLOSResponse(sp, dtype):
sigma_low = 1e-4*ift.random.current_rng().standard_normal(10)
sigma_ups = 1e-5*ift.random.current_rng().standard_normal(10)
op = ift.LOSResponse(sp, starts, ends, sigma_low, sigma_ups)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
......@@ -50,13 +50,13 @@ def testOperatorCombinations(sp, dtype):
a = ift.DiagonalOperator(ift.Field.from_random(sp, "normal", dtype=dtype))
b = ift.DiagonalOperator(ift.Field.from_random(sp, "normal", dtype=dtype))
op = ift.SandwichOperator.make(a, b)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = a(b)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = a + b
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = a - b
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
def testLinearInterpolator():
......@@ -65,60 +65,60 @@ def testLinearInterpolator():
pos[0, :] *= 0.9
pos[1, :] *= 7*3.5
op = ift.LinearInterpolator(sp, pos)
ift.extra.consistency_check(op)
ift.extra.check_linear_operator(op)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
def testRealizer(sp):
op = ift.Realizer(sp)
ift.extra.consistency_check(op, np.complex128, np.float64,
ift.extra.check_linear_operator(op, np.complex128, np.float64,
only_r_linear=True)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
def testImaginizer(sp):
op = ift.Imaginizer(sp)
ift.extra.consistency_check(op, np.complex128, np.float64,
ift.extra.check_linear_operator(op, np.complex128, np.float64,
only_r_linear=True)
loc = ift.from_random(op.domain, dtype=np.complex128)
ift.extra.check_jacobian_consistency(op, loc)
ift.extra.check_operator(op, loc)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
def testConjugationOperator(sp):
op = ift.ConjugationOperator(sp)
ift.extra.consistency_check(op, np.complex128, np.complex128,
ift.extra.check_linear_operator(op, np.complex128, np.complex128,
only_r_linear=True)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
def testOperatorAdaptor(sp, dtype):
op = ift.DiagonalOperator(ift.Field.from_random(sp, "normal", dtype=dtype))
ift.extra.consistency_check(op.adjoint, dtype, dtype)
ift.extra.consistency_check(op.inverse, dtype, dtype)
ift.extra.consistency_check(op.inverse.adjoint, dtype, dtype)
ift.extra.consistency_check(op.adjoint.inverse, dtype, dtype)
ift.extra.check_linear_operator(op.adjoint, dtype, dtype)
ift.extra.check_linear_operator(op.inverse, dtype, dtype)
ift.extra.check_linear_operator(op.inverse.adjoint, dtype, dtype)
ift.extra.check_linear_operator(op.adjoint.inverse, dtype, dtype)
@pmp('sp1', _h_spaces + _p_spaces + _pow_spaces)
@pmp('sp2', _h_spaces + _p_spaces + _pow_spaces)
def testNullOperator(sp1, sp2, dtype):
op = ift.NullOperator(sp1, sp2)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
mdom1 = ift.MultiDomain.make({'a': sp1})
mdom2 = ift.MultiDomain.make({'b': sp2})
op = ift.NullOperator(mdom1, mdom2)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = ift.NullOperator(sp1, mdom2)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = ift.NullOperator(mdom1, sp2)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _p_RG_spaces)
def testHarmonicSmoothingOperator(sp, dtype):
op = ift.HarmonicSmoothingOperator(sp, 0.1)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _h_spaces + _p_spaces + _pow_spaces)
......@@ -129,43 +129,43 @@ def testDOFDistributor(sp, dtype):
dofdex = np.arange(sp.size).reshape(sp.shape) % 3
dofdex = ift.Field.from_raw(sp, dofdex)
op = ift.DOFDistributor(dofdex)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _h_spaces)
def testPPO(sp, dtype):
op = ift.PowerDistributor(target=sp)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
ps = ift.PowerSpace(
sp, ift.PowerSpace.useful_binbounds(sp, logarithmic=False, nbin=3))
op = ift.PowerDistributor(target=sp, power_space=ps)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
ps = ift.PowerSpace(
sp, ift.PowerSpace.useful_binbounds(sp, logarithmic=True, nbin=3))
op = ift.PowerDistributor(target=sp, power_space=ps)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _h_RG_spaces + _p_RG_spaces)
def testFFT(sp, dtype):
op = ift.FFTOperator(sp)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = ift.FFTOperator(sp.get_default_codomain())
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
@pmp('sp', _h_RG_spaces + _p_RG_spaces)
def testHartley(sp, dtype):
op = ift.HartleyOperator(sp)
ift.extra.consistency_check(op, dtype, dtype)
ift.extra.check_linear_operator(op, dtype, dtype)
op = ift.HartleyOperator(sp.get_default_codomain())