Commit fd98cba2 authored by Theo Steininger's avatar Theo Steininger

Integrated real harmonic RGSpace representation deeply into NIFTy.

parent 39b6628e
Pipeline #16544 failed with stage
in 6 minutes and 17 seconds
......@@ -2,13 +2,14 @@ import numpy as np
from nifty import RGSpace, PowerSpace, Field, FFTOperator, ComposedOperator,\
DiagonalOperator, ResponseOperator, plotting,\
create_power_operator
create_power_operator, nifty_configuration
from nifty.library import WienerFilterCurvature
if __name__ == "__main__":
distribution_strategy = 'not'
nifty_configuration['default_distribution_strategy'] = 'fftw'
nifty_configuration['harmonic_rg_base'] = 'real'
# Setting up variable parameters
......@@ -36,19 +37,17 @@ if __name__ == "__main__":
signal_space = RGSpace([N_pixels, N_pixels], distances=L/N_pixels)
harmonic_space = FFTOperator.get_default_codomain(signal_space)
fft = FFTOperator(harmonic_space, target=signal_space,
domain_dtype=np.complex, target_dtype=np.float)
power_space = PowerSpace(harmonic_space,
distribution_strategy=distribution_strategy)
fft = FFTOperator(harmonic_space, target=signal_space)
power_space = PowerSpace(harmonic_space)
# Creating the mock data
S = create_power_operator(harmonic_space, power_spectrum=power_spectrum,
distribution_strategy=distribution_strategy)
S = create_power_operator(harmonic_space, power_spectrum=power_spectrum)
mock_power = Field(power_space, val=power_spectrum,
distribution_strategy=distribution_strategy)
mock_power = Field(power_space, val=power_spectrum)
np.random.seed(43)
mock_harmonic = mock_power.power_synthesize(real_signal=True)
if nifty_configuration['harmonic_rg_base'] == 'real':
mock_harmonic = mock_harmonic.real
mock_signal = fft(mock_harmonic)
R = ResponseOperator(signal_space, sigma=(response_sigma,))
......@@ -74,9 +73,10 @@ if __name__ == "__main__":
plotter = plotting.RG2DPlotter()
plotter.path = 'mock_signal.html'
plotter(mock_signal)
plotter(mock_signal.real)
plotter.path = 'data.html'
plotter(Field(signal_space,
val=data.val.get_full_data().reshape(signal_space.shape)))
plotter(Field(
signal_space,
val=data.val.get_full_data().real.reshape(signal_space.shape)))
plotter.path = 'map.html'
plotter(m_s)
plotter(m_s.real)
import numpy as np
from nifty import RGSpace, PowerSpace, Field, RealFFTOperator,\
ComposedOperator, DiagonalOperator, ResponseOperator,\
plotting, create_power_operator
from nifty.library import WienerFilterCurvature
if __name__ == "__main__":
distribution_strategy = 'not'
# Setting up variable parameters
# Typical distance over which the field is correlated
correlation_length = 0.05
# Variance of field in position space sqrt(<|s_x|^2>)
field_variance = 2.
# smoothing length of response (in same unit as L)
response_sigma = 0.01
# The signal to noise ratio
signal_to_noise = 0.7
# note that field_variance**2 = a*k_0/4. for this analytic form of power
# spectrum
def power_spectrum(k):
a = 4 * correlation_length * field_variance**2
return a / (1 + k * correlation_length) ** 4
# Setting up the geometry
# Total side-length of the domain
L = 2.
# Grid resolution (pixels per axis)
N_pixels = 512
signal_space = RGSpace([N_pixels, N_pixels], distances=L/N_pixels)
harmonic_space = RealFFTOperator.get_default_codomain(signal_space)
fft = RealFFTOperator(harmonic_space, target=signal_space)
power_space = PowerSpace(harmonic_space,
distribution_strategy=distribution_strategy)
# Creating the mock data
S = create_power_operator(harmonic_space, power_spectrum=power_spectrum,
distribution_strategy=distribution_strategy)
mock_power = Field(power_space, val=power_spectrum,
distribution_strategy=distribution_strategy)
np.random.seed(43)
mock_harmonic = mock_power.power_synthesize(real_signal=True)
mock_harmonic = mock_harmonic.real + mock_harmonic.imag
mock_signal = fft(mock_harmonic)
R = ResponseOperator(signal_space, sigma=(response_sigma,))
data_domain = R.target[0]
R_harmonic = ComposedOperator([fft, R], default_spaces=[0, 0])
N = DiagonalOperator(data_domain,
diagonal=mock_signal.var()/signal_to_noise,
bare=True)
noise = Field.from_random(domain=data_domain,
random_type='normal',
std=mock_signal.std()/np.sqrt(signal_to_noise),
mean=0)
data = R(mock_signal) + noise
# Wiener filter
j = R_harmonic.adjoint_times(N.inverse_times(data))
wiener_curvature = WienerFilterCurvature(S=S, N=N, R=R_harmonic)
m = wiener_curvature.inverse_times(j)
m_s = fft(m)
plotter = plotting.RG2DPlotter()
plotter.path = 'mock_signal.html'
plotter(mock_signal)
plotter.path = 'data.html'
plotter(Field(signal_space,
val=data.val.get_full_data().reshape(signal_space.shape)))
plotter.path = 'map.html'
plotter(m_s)
......@@ -70,11 +70,18 @@ variable_default_distribution_strategy = keepers.Variable(
if z == 'fftw' else True),
genus='str')
variable_harmonic_rg_base = keepers.Variable(
'harmonic_rg_base',
['real', 'complex'],
lambda z: z in ['real', 'complex'],
genus='str')
nifty_configuration = keepers.get_Configuration(
name='NIFTy',
variables=[variable_fft_module,
variable_default_field_dtype,
variable_default_distribution_strategy],
variable_default_distribution_strategy,
variable_harmonic_rg_base],
file_name='NIFTy.conf',
search_paths=[os.path.expanduser('~') + "/.config/nifty/",
os.path.expanduser('~') + "/.config/",
......
......@@ -19,7 +19,6 @@
from __future__ import division
import ast
import itertools
import numpy as np
from keepers import Versionable,\
......
......@@ -18,4 +18,3 @@
from transformations import *
from .fft_operator import FFTOperator
from .real_fft_operator import RealFFTOperator
......@@ -142,17 +142,11 @@ class FFTOperator(LinearOperator):
backward_class, self.target[0], self.domain[0], module=module)
# Store the dtype information
if domain_dtype is None:
self.logger.info("Setting domain_dtype to np.complex.")
self.domain_dtype = np.complex
else:
self.domain_dtype = np.dtype(domain_dtype)
self.domain_dtype = \
None if domain_dtype is None else np.dtype(domain_dtype)
if target_dtype is None:
self.logger.info("Setting target_dtype to np.complex.")
self.target_dtype = np.complex
else:
self.target_dtype = np.dtype(target_dtype)
self.target_dtype = \
None if target_dtype is None else np.dtype(target_dtype)
def _times(self, x, spaces):
spaces = utilities.cast_axis_to_tuple(spaces, len(x.domain))
......@@ -172,8 +166,10 @@ class FFTOperator(LinearOperator):
result_domain = list(x.domain)
result_domain[spaces[0]] = self.target[0]
result_dtype = \
new_val.dtype if self.target_dtype is None else self.target_dtype
result_field = x.copy_empty(domain=result_domain,
dtype=self.target_dtype)
dtype=result_dtype)
result_field.set_val(new_val=new_val, copy=True)
return result_field
......@@ -196,8 +192,11 @@ class FFTOperator(LinearOperator):
result_domain = list(x.domain)
result_domain[spaces[0]] = self.domain[0]
result_dtype = \
new_val.dtype if self.domain_dtype is None else self.domain_dtype
result_field = x.copy_empty(domain=result_domain,
dtype=self.domain_dtype)
dtype=result_dtype)
result_field.set_val(new_val=new_val, copy=True)
return result_field
......
This diff is collapsed.
......@@ -260,7 +260,7 @@ class MPIFFT(Transform):
p()
if p.has_output:
result = p.output_array
result = p.output_array.copy()
if result.shape != val.shape:
raise ValueError("Output shape is different than input shape. "
"Maybe fftw tries to optimize the "
......
......@@ -43,6 +43,8 @@ class RGRGTransformation(Transformation):
else:
raise ValueError('Unsupported FFT module:' + module)
self.harmonic_base = nifty_configuration['harmonic_rg_base']
# ---Mandatory properties and methods---
@property
......@@ -144,7 +146,31 @@ class RGRGTransformation(Transformation):
val = self._transform.domain.weight(val, power=1, axes=axes)
# Perform the transformation
if self.harmonic_base == 'complex':
Tval = self._transform.transform(val, axes, **kwargs)
else:
if issubclass(val.dtype.type, np.complexfloating):
Tval_real = self._transform.transform(val.real, axes,
**kwargs)
Tval_imag = self._transform.transform(val.imag, axes,
**kwargs)
if self.codomain.harmonic:
Tval_real.data.real += Tval_real.data.imag
Tval_real.data.imag = \
Tval_imag.data.real + Tval_imag.data.imag
else:
Tval_real.data.real -= Tval_real.data.imag
Tval_real.data.imag = \
Tval_imag.data.real - Tval_imag.data.imag
Tval = Tval_real
else:
Tval = self._transform.transform(val, axes, **kwargs)
if self.codomain.harmonic:
Tval.data.real += Tval.data.imag
else:
Tval.data.real -= Tval.data.imag
Tval = Tval.real
if not self._transform.codomain.harmonic:
# correct for inverse fft.
......
......@@ -36,7 +36,7 @@ from d2o import distributed_data_object,\
STRATEGIES as DISTRIBUTION_STRATEGIES
from nifty.spaces.space import Space
from nifty.config import nifty_configuration
class RGSpace(Space):
"""
......@@ -122,6 +122,9 @@ class RGSpace(Space):
# return fixed_points
def hermitianize_inverter(self, x, axes):
if nifty_configuration['harmonic_rg_base'] == 'real':
return x
else:
# calculate the number of dimensions the input array has
dimensions = len(x.shape)
# prepare the slicing object which will be used for mirroring
......
......@@ -16,6 +16,8 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
import numpy as np
from nifty import Space,\
PowerSpace,\
Field,\
......@@ -71,9 +73,10 @@ def create_power_operator(domain, power_spectrum, dtype=None,
distribution_strategy='not')
f = fp.power_synthesize(mean=1, std=0, real_signal=False,
distribution_strategy=distribution_strategy)
# MR FIXME: we need the real part here. Could this also be achieved
# by setting real_signal=True in the call above?
if not issubclass(fp.dtype.type, np.complexfloating):
f = f.real
f **= 2
return DiagonalOperator(domain, diagonal=f, bare=True)
......
......@@ -31,14 +31,7 @@ from itertools import product
from test.common import expand
from nose.plugins.skip import SkipTest
def _harmonic_type(itp):
otp = itp
if otp == np.float64:
otp = np.complex128
elif otp == np.float32:
otp = np.complex64
return otp
from d2o import STRATEGIES
def _get_rtol(tp):
......@@ -49,100 +42,119 @@ def _get_rtol(tp):
class FFTOperatorTests(unittest.TestCase):
@expand(product([10, 11], [False, True], [0.1, 1, 3.7]))
def test_RG_distance_1D(self, dim1, zc1, d):
foo = RGSpace([dim1], zerocenter=zc1, distances=d)
res = foo.get_distance_array('not')
assert_equal(res[zc1 * (dim1 // 2)], 0.)
@expand(product([10, 11], [9, 28], [False, True], [False, True],
[0.1, 1, 3.7]))
def test_RG_distance_2D(self, dim1, dim2, zc1, zc2, d):
foo = RGSpace([dim1, dim2], zerocenter=[zc1, zc2], distances=d)
res = foo.get_distance_array('not')
assert_equal(res[zc1 * (dim1 // 2), zc2 * (dim2 // 2)], 0.)
@expand(product(["numpy", "fftw", "fftw_mpi"],
[16, ], [False, True], [False, True],
[0.1, 1, 3.7],
[np.float64, np.complex128, np.float32, np.complex64]))
def test_fft1D(self, module, dim1, zc1, zc2, d, itp):
[16, ], [0.1, 1, 3.7], STRATEGIES['global'],
[np.float64, np.float32, np.complex64, np.complex128],
['real', 'complex']))
def test_fft1D(self, module, dim1, d, distribution_strategy, itp, base):
if module == "fftw_mpi":
if not hasattr(gdi.get('fftw'), 'FFTW_MPI'):
raise SkipTest
if module == "fftw" and "fftw" not in gdi:
raise SkipTest
tol = _get_rtol(itp)
a = RGSpace(dim1, zerocenter=zc1, distances=d)
b = RGSpace(dim1, zerocenter=zc2, distances=1./(dim1*d), harmonic=True)
fft = FFTOperator(domain=a, target=b, domain_dtype=itp,
target_dtype=_harmonic_type(itp), module=module)
a = RGSpace(dim1, distances=d)
b = RGSpace(dim1, distances=1./(dim1*d), harmonic=True)
fft = FFTOperator(domain=a, target=b, module=module)
fft._forward_transformation.harmonic_base = base
fft._backward_transformation.harmonic_base = base
np.random.seed(16)
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=itp)
dtype=itp,
distribution_strategy=distribution_strategy)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val.get_full_data(),
out.val.get_full_data(),
rtol=tol, atol=tol)
@expand(product(["numpy", "fftw", "fftw_mpi"],
[12, 15], [9, 12], [False, True],
[False, True], [False, True], [False, True], [0.1, 1, 3.7],
[0.4, 1, 2.7],
[np.float64, np.complex128, np.float32, np.complex64]))
def test_fft2D(self, module, dim1, dim2, zc1, zc2, zc3, zc4, d1, d2, itp):
[12, 15], [9, 12], [0.1, 1, 3.7],
[0.4, 1, 2.7], STRATEGIES['global'],
[np.float64, np.float32, np.complex64, np.complex128],
['real', 'complex']))
def test_fft2D(self, module, dim1, dim2, d1, d2, distribution_strategy,
itp, base):
if module == "fftw_mpi":
if not hasattr(gdi.get('fftw'), 'FFTW_MPI'):
raise SkipTest
if module == "fftw" and "fftw" not in gdi:
raise SkipTest
tol = _get_rtol(itp)
a = RGSpace([dim1, dim2], zerocenter=[zc1, zc2], distances=[d1, d2])
b = RGSpace([dim1, dim2], zerocenter=[zc3, zc4],
a = RGSpace([dim1, dim2], distances=[d1, d2])
b = RGSpace([dim1, dim2],
distances=[1./(dim1*d1), 1./(dim2*d2)], harmonic=True)
fft = FFTOperator(domain=a, target=b, domain_dtype=itp,
target_dtype=_harmonic_type(itp), module=module)
fft = FFTOperator(domain=a, target=b, module=module)
fft._forward_transformation.harmonic_base = base
fft._backward_transformation.harmonic_base = base
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=itp)
dtype=itp,
distribution_strategy=distribution_strategy)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=tol, atol=tol)
@expand(product(["numpy", "fftw", "fftw_mpi"],
[0, 1, 2],
STRATEGIES['global'],
[np.float64, np.float32, np.complex64, np.complex128],
['real', 'complex']))
def test_composed_fft(self, module, index, distribution_strategy, dtype,
base):
if module == "fftw_mpi":
if not hasattr(gdi.get('fftw'), 'FFTW_MPI'):
raise SkipTest
if module == "fftw" and "fftw" not in gdi:
raise SkipTest
tol = _get_rtol(dtype)
a = [a1, a2, a3] = [RGSpace((8,)), RGSpace((4, 4)), RGSpace((5, 6))]
fft = FFTOperator(domain=a[index], module=module,
default_spaces=(index,))
fft._forward_transformation.harmonic_base = base
fft._backward_transformation.harmonic_base = base
inp = Field.from_random(domain=(a1, a2, a3), random_type='normal',
std=7, mean=3, dtype=dtype,
distribution_strategy=distribution_strategy)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=tol, atol=tol)
@expand(product([0, 3, 6, 11, 30],
[np.float64, np.complex128, np.float32, np.complex64]))
[np.float64, np.float32, np.complex64, np.complex128]))
def test_sht(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = GLSpace(nlat=lm+1)
fft = FFTOperator(domain=a, target=b, domain_dtype=tp, target_dtype=tp)
fft = FFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=tp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=tol, atol=tol)
@expand(product([128, 256],
[np.float64, np.complex128, np.float32, np.complex64]))
[np.float64, np.float32, np.complex64, np.complex128]))
def test_sht2(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
a = LMSpace(lmax=lm)
b = HPSpace(nside=lm//2)
fft = FFTOperator(domain=a, target=b, domain_dtype=tp, target_dtype=tp)
fft = FFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=1e-3, atol=1e-1)
@expand(product([128, 256],
[np.float64, np.complex128, np.float32, np.complex64]))
[np.float64, np.float32, np.complex64, np.complex128]))
def test_dotsht(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = GLSpace(nlat=lm+1)
fft = FFTOperator(domain=a, target=b, domain_dtype=tp, target_dtype=tp)
fft = FFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.times(inp)
......@@ -151,14 +163,14 @@ class FFTOperatorTests(unittest.TestCase):
assert_allclose(v1, v2, rtol=tol, atol=tol)
@expand(product([128, 256],
[np.float64, np.complex128, np.float32, np.complex64]))
[np.float64, np.float32, np.complex64, np.complex128]))
def test_dotsht2(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = HPSpace(nside=lm//2)
fft = FFTOperator(domain=a, target=b, domain_dtype=tp, target_dtype=tp)
fft = FFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.times(inp)
......
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2013-2017 Max-Planck-Society
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
import unittest
import numpy as np
from numpy.testing import assert_equal,\
assert_allclose
from nifty.config import dependency_injector as gdi
from nifty import Field,\
RGSpace,\
LMSpace,\
HPSpace,\
GLSpace,\
RealFFTOperator
from itertools import product
from test.common import expand
from nose.plugins.skip import SkipTest
def _get_rtol(tp):
if (tp == np.float64) or (tp == np.complex128):
return 1e-10
else:
return 1e-5
class RealFFTOperatorTests(unittest.TestCase):
@expand(product(["numpy", "fftw", "fftw_mpi"],
[16, ], [0.1, 1, 3.7],
[np.float64, np.float32]))
def test_fft1D(self, module, dim1, d, itp):
if module == "fftw_mpi":
if not hasattr(gdi.get('fftw'), 'FFTW_MPI'):
raise SkipTest
if module == "fftw" and "fftw" not in gdi:
raise SkipTest
tol = _get_rtol(itp)
a = RGSpace(dim1, distances=d)
b = RGSpace(dim1, distances=1./(dim1*d), harmonic=True)
fft = RealFFTOperator(domain=a, target=b, module=module)
np.random.seed(16)
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=itp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val.get_full_data(),
out.val.get_full_data(),
rtol=tol, atol=tol)
@expand(product(["numpy", "fftw", "fftw_mpi"],
[12, 15], [9, 12], [0.1, 1, 3.7],
[0.4, 1, 2.7],
[np.float64, np.float32]))
def test_fft2D(self, module, dim1, dim2, d1, d2, itp):
if module == "fftw_mpi":
if not hasattr(gdi.get('fftw'), 'FFTW_MPI'):
raise SkipTest
if module == "fftw" and "fftw" not in gdi:
raise SkipTest
tol = _get_rtol(itp)
a = RGSpace([dim1, dim2], distances=[d1, d2])
b = RGSpace([dim1, dim2],
distances=[1./(dim1*d1), 1./(dim2*d2)], harmonic=True)
fft = RealFFTOperator(domain=a, target=b, module=module)
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=itp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=tol, atol=tol)
@expand(product([0, 3, 6, 11, 30], [np.float64, np.float32]))
def test_sht(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = GLSpace(nlat=lm+1)
fft = RealFFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3,
dtype=tp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=tol, atol=tol)
@expand(product([128, 256], [np.float64, np.float32]))
def test_sht2(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
a = LMSpace(lmax=lm)
b = HPSpace(nside=lm//2)
fft = RealFFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.adjoint_times(fft.times(inp))
assert_allclose(inp.val, out.val, rtol=1e-3, atol=1e-1)
@expand(product([128, 256], [np.float64, np.float32]))
def test_dotsht(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = GLSpace(nlat=lm+1)
fft = RealFFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.times(inp)
v1 = np.sqrt(out.vdot(out))
v2 = np.sqrt(inp.vdot(fft.adjoint_times(out)))
assert_allclose(v1, v2, rtol=tol, atol=tol)
@expand(product([128, 256], [np.float64, np.float32]))
def test_dotsht2(self, lm, tp):
if 'pyHealpix' not in gdi:
raise SkipTest
tol = _get_rtol(tp)
a = LMSpace(lmax=lm)
b = HPSpace(nside=lm//2)
fft = RealFFTOperator(domain=a, target=b)
inp = Field.from_random(domain=a, random_type='normal', std=1, mean=0,
dtype=tp)
out = fft.times(inp)
v1 = np.sqrt(out.vdot(out))
v2 = np.sqrt(inp.vdot(fft.adjoint_times(out)))
assert_allclose(v1, v2, rtol=tol, atol=tol)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment