From 4db8dcafb8eca1c0014ae20bd163fa94b8f38ee4 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Fri, 21 Jun 2024 14:29:42 +0200 Subject: [PATCH 01/11] simple implementation GeneralLinearOperator --- psydac/linalg/basic.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 07ff42670..c8fe3e1fd 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -1111,3 +1111,46 @@ def solve(self, rhs, out=None): @property def T(self): return self.transpose() + +#=============================================================================== +class GeneralLinearOperator(LinearOperator): + """ + General operator acting between two vector spaces V and W. It only requires a dot method. + + """ + + def __init__(self, domain, codomain, dot): + + assert isinstance(domain, VectorSpace) + assert isinstance(codomain, VectorSpace) + from types import LambdaType + assert isinstance(dot, LambdaType) + + self._domain = domain + self._codomain = codomain + self._dot = dot + + @property + def domain(self): + return self._domain + + @property + def codomain(self): + return self._codomain + + @property + def dtype(self): + return None + + def dot(self, v, out=None): + assert isinstance(v, Vector) + assert v.space == self.domain + + if out is not None: + assert isinstance(out, Vector) + assert out.space == self.codomain + + out = self._dot(v) + return out + else: + return self._dot(v) From 2adf6ad7f83716e84743a5b1f1100945307f8e91 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Sun, 23 Jun 2024 11:38:57 +0200 Subject: [PATCH 02/11] general linear operator needs toarray, tosparse,transpose method --- psydac/linalg/basic.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index c8fe3e1fd..d3ad57e37 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -1150,7 +1150,14 @@ def dot(self, v, out=None): assert isinstance(out, Vector) assert out.space == self.codomain - out = self._dot(v) - return out - else: - return self._dot(v) + return self._dot(v, out=out) + + def toarray(self): + raise NotImplementedError('toarray() is not defined for GeneralLinearOperators.') + + def tosparse(self): + raise NotImplementedError('tosparse() is not defined for GeneralLinearOperators.') + + def transpose(self): + raise NotImplementedError('transpose() is not defined for GeneralLinearOperators.') + From 41f3373266f6e70f4385e404656b4929f01d1221 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Thu, 4 Jul 2024 16:23:31 +0200 Subject: [PATCH 03/11] improve docstrings --- psydac/linalg/basic.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index d3ad57e37..96125daed 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -25,7 +25,8 @@ 'ComposedLinearOperator', 'PowerLinearOperator', 'InverseLinearOperator', - 'LinearSolver' + 'LinearSolver', + 'MatrixFreeLinearOperator' ) #=============================================================================== @@ -1113,9 +1114,9 @@ def T(self): return self.transpose() #=============================================================================== -class GeneralLinearOperator(LinearOperator): +class MatrixFreeLinearOperator(LinearOperator): """ - General operator acting between two vector spaces V and W. It only requires a dot method. + General operator acting between two vector spaces V and W. It only requires a callable dot method. """ @@ -1149,7 +1150,7 @@ def dot(self, v, out=None): if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain - + return self._dot(v, out=out) def toarray(self): From 2067c4c42b8a8d97c16e38a046cebd528004c600 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Thu, 4 Jul 2024 16:38:16 +0200 Subject: [PATCH 04/11] fixed message notimplementederror --- psydac/linalg/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 96050426d..88e3d757a 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -1154,11 +1154,11 @@ def dot(self, v, out=None): return self._dot(v, out=out) def toarray(self): - raise NotImplementedError('toarray() is not defined for GeneralLinearOperators.') + raise NotImplementedError('toarray() is not defined for MatrixFreeLinearOperator.') def tosparse(self): - raise NotImplementedError('tosparse() is not defined for GeneralLinearOperators.') + raise NotImplementedError('tosparse() is not defined for MatrixFreeLinearOperator.') def transpose(self): - raise NotImplementedError('transpose() is not defined for GeneralLinearOperators.') + raise NotImplementedError('transpose() is not defined for MatrixFreeLinearOperator.') From d32bd988057ffa1d409d51dc7eeb6d477beff7a5 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Thu, 4 Jul 2024 16:39:45 +0200 Subject: [PATCH 05/11] put import outside class --- psydac/linalg/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 88e3d757a..8210a13a9 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -11,6 +11,7 @@ import numpy as np from scipy.sparse import coo_matrix +from types import LambdaType from psydac.utilities.utils import is_real @@ -1123,8 +1124,7 @@ class MatrixFreeLinearOperator(LinearOperator): def __init__(self, domain, codomain, dot): assert isinstance(domain, VectorSpace) - assert isinstance(codomain, VectorSpace) - from types import LambdaType + assert isinstance(codomain, VectorSpace) assert isinstance(dot, LambdaType) self._domain = domain From b85ec764bffd5241977918781e4324129265a938 Mon Sep 17 00:00:00 2001 From: e-moral-sanchez Date: Mon, 8 Jul 2024 12:53:36 +0200 Subject: [PATCH 06/11] set tol to rtol in MINRES --- psydac/api/tests/test_api_2d_compatible_spaces.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psydac/api/tests/test_api_2d_compatible_spaces.py b/psydac/api/tests/test_api_2d_compatible_spaces.py index feb7cd439..45c04a943 100644 --- a/psydac/api/tests/test_api_2d_compatible_spaces.py +++ b/psydac/api/tests/test_api_2d_compatible_spaces.py @@ -130,7 +130,7 @@ def run_stokes_2d_dir(domain, f, ue, pe, *, homogeneous, ncells, degree, scipy=F # ... solve linear system using scipy.sparse.linalg or psydac if scipy: - tol = 1e-11 + rtol = 1e-11 equation_h.assemble() A0 = equation_h.linear_system.lhs.tosparse() b0 = equation_h.linear_system.rhs.toarray() @@ -145,17 +145,17 @@ def run_stokes_2d_dir(domain, f, ue, pe, *, homogeneous, ncells, degree, scipy=F A1 = a1_h.assemble().tosparse() b1 = l1_h.assemble().toarray() - x1, info = sp_minres(A1, b1, tol=tol) + x1, info = sp_minres(A1, b1, rtol=rtol) print('Boundary solution with scipy.sparse: success = {}'.format(info == 0)) - x0, info = sp_minres(A0, b0 - A0.dot(x1), tol=tol) + x0, info = sp_minres(A0, b0 - A0.dot(x1), rtol=rtol) print('Interior solution with scipy.sparse: success = {}'.format(info == 0)) # Solution is sum of boundary and interior contributions x = x0 + x1 else: - x, info = sp_minres(A0, b0, tol=tol) + x, info = sp_minres(A0, b0, rtol=rtol) print('Solution with scipy.sparse: success = {}'.format(info == 0)) # Convert to stencil format From 273ec8fa801b390abd4f0024be3ca6052d8bea8d Mon Sep 17 00:00:00 2001 From: Martin Campos Pinto Date: Fri, 12 Jul 2024 19:16:08 +0200 Subject: [PATCH 07/11] add features and tests to MatrixFree linear ops --- psydac/linalg/basic.py | 69 +++++++++++++++++++++++++++--- psydac/linalg/solvers.py | 2 +- psydac/linalg/tests/test_linalg.py | 18 ++++---- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 8210a13a9..33cc77d63 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -12,6 +12,7 @@ import numpy as np from scipy.sparse import coo_matrix from types import LambdaType +from inspect import signature from psydac.utilities.utils import is_real @@ -1117,11 +1118,37 @@ def T(self): #=============================================================================== class MatrixFreeLinearOperator(LinearOperator): """ - General operator acting between two vector spaces V and W. It only requires a callable dot method. + General linear operator represented by a callable dot method. + + Parameters + ---------- + domain : VectorSpace + The domain of the linear operator. + + codomain : VectorSpace + The codomain of the linear operator. + + dot : Callable + The method of the linear operator, assumed to map from domain to codomain. + This method can take out as an optional argument but this is not mandatory. + + dot_transpose: Callable + The method of the transpose of the linear operator, assumed to map from codomain to domain. + This method can take out as an optional argument but this is not mandatory. + + Examples + -------- + # example 1: a matrix encapsulated as a (fake) matrix-free linear operator + A_SM = StencilMatrix(V, W) + AT_SM = A_SM.transpose() + A = MatrixFreeLinearOperator(domain=V, codomain=W, dot=lambda v: A_SM @ v, dot_transpose=lambda v: AT_SM @ v) + + # example 2: a truly matrix-free linear operator + A = MatrixFreeLinearOperator(domain=V, codomain=V, dot=lambda v: 2*v, dot_transpose=lambda v: 2*v) """ - def __init__(self, domain, codomain, dot): + def __init__(self, domain, codomain, dot, dot_transpose=None): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) @@ -1131,6 +1158,18 @@ def __init__(self, domain, codomain, dot): self._codomain = codomain self._dot = dot + sig = signature(dot) + self._dot_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY]) + + if dot_transpose is not None: + assert isinstance(dot_transpose, LambdaType) + self._dot_transpose = dot_transpose + sig = signature(dot_transpose) + self._dot_transpose_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY]) + else: + self._dot_transpose = None + self._dot_transpose_takes_out_arg = False + @property def domain(self): return self._domain @@ -1150,8 +1189,16 @@ def dot(self, v, out=None): if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain + else: + out = self.codomain.zeros() - return self._dot(v, out=out) + if self._dot_takes_out_arg: + self._dot(v, out=out) + else: + # provided dot product does not take an out argument: we simply copy the result into out + self._dot(v).copy(out=out) + + return out def toarray(self): raise NotImplementedError('toarray() is not defined for MatrixFreeLinearOperator.') @@ -1159,6 +1206,18 @@ def toarray(self): def tosparse(self): raise NotImplementedError('tosparse() is not defined for MatrixFreeLinearOperator.') - def transpose(self): - raise NotImplementedError('transpose() is not defined for MatrixFreeLinearOperator.') + def transpose(self, conjugate=False): + if self._dot_transpose is None: + raise NotImplementedError('no transpose dot method was given -- cannot create the transpose operator') + + if conjugate: + if self._dot_transpose_takes_out_arg: + new_dot = lambda v, out=None: self._dot_transpose(v, out=out).conjugate() + else: + new_dot = lambda v: self._dot_transpose(v).conjugate() + else: + new_dot = self._dot_transpose + + return MatrixFreeLinearOperator(domain=self.codomain, codomain=self.domain, dot=new_dot, dot_transpose=self._dot) + \ No newline at end of file diff --git a/psydac/linalg/solvers.py b/psydac/linalg/solvers.py index 297720ab7..ca1079be7 100644 --- a/psydac/linalg/solvers.py +++ b/psydac/linalg/solvers.py @@ -39,7 +39,7 @@ def inverse(A, solver, **kwargs): A : psydac.linalg.basic.LinearOperator Left-hand-side matrix A of linear system; individual entries A[i,j] can't be accessed, but A has 'shape' attribute and provides 'dot(p)' - function (i.e. matrix-vector product A*p). + function (e.g. a matrix-vector product A*p). solver : str Preferred iterative solver. Options are: 'cg', 'pcg', 'bicg', diff --git a/psydac/linalg/tests/test_linalg.py b/psydac/linalg/tests/test_linalg.py index 8b30802b8..15d5ad312 100644 --- a/psydac/linalg/tests/test_linalg.py +++ b/psydac/linalg/tests/test_linalg.py @@ -20,7 +20,7 @@ def array_equal(a, b): def sparse_equal(a, b): return (a.tosparse() != b.tosparse()).nnz == 0 -def is_pos_def(A): +def assert_pos_def(A): assert isinstance(A, LinearOperator) A_array = A.toarray() assert np.all(np.linalg.eigvals(A_array) > 0) @@ -50,7 +50,7 @@ def get_StencilVectorSpace(n1, n2, p1, p2, P1, P2): V = StencilVectorSpace(C) return V -def get_positive_definite_stencilmatrix(V): +def get_positive_definite_StencilMatrix(V): np.random.seed(2) assert isinstance(V, StencilVectorSpace) @@ -700,9 +700,9 @@ def test_positive_definite_matrix(n1, n2, p1, p2): P1 = False P2 = False V = get_StencilVectorSpace(n1, n2, p1, p2, P1, P2) - S = get_positive_definite_stencilmatrix(V) + S = get_positive_definite_StencilMatrix(V) - is_pos_def(S) + assert_pos_def(S) #=============================================================================== @pytest.mark.parametrize('n1', [3, 5]) @@ -745,7 +745,7 @@ def test_operator_evaluation(n1, n2, p1, p2): V = get_StencilVectorSpace(n1, n2, p1, p2, P1, P2) # Initiate positive definite StencilMatrices for which the cg inverse works (necessary for certain tests) - S = get_positive_definite_stencilmatrix(V) + S = get_positive_definite_StencilMatrix(V) # Initiate StencilVectors v = StencilVector(V) @@ -769,7 +769,7 @@ def test_operator_evaluation(n1, n2, p1, p2): ### 2.1 PowerLO test Bmat = B.toarray() - is_pos_def(B) + assert_pos_def(B) uarr = u.toarray() b0 = ( B**0 @ u ).toarray() b1 = ( B**1 @ u ).toarray() @@ -799,7 +799,7 @@ def test_operator_evaluation(n1, n2, p1, p2): assert np.array_equal(zeros, z2) Smat = S.toarray() - is_pos_def(S) + assert_pos_def(S) varr = v.toarray() s0 = ( S**0 @ v ).toarray() s1 = ( S**1 @ v ).toarray() @@ -960,8 +960,8 @@ def test_x0update(solver): P1 = False P2 = False V = get_StencilVectorSpace(n1, n2, p1, p2, P1, P2) - A = get_positive_definite_stencilmatrix(V) - is_pos_def(A) + A = get_positive_definite_StencilMatrix(V) + assert_pos_def(A) b = StencilVector(V) for n in range(n1): b[n, :] = 1. From 9163b3fa0c400faf15ed47a11dd806924c10bbfa Mon Sep 17 00:00:00 2001 From: Martin Campos Pinto Date: Fri, 12 Jul 2024 19:47:13 +0200 Subject: [PATCH 08/11] tests for matrix free linear ops --- psydac/linalg/tests/test_matrix_free.py | 127 ++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 psydac/linalg/tests/test_matrix_free.py diff --git a/psydac/linalg/tests/test_matrix_free.py b/psydac/linalg/tests/test_matrix_free.py new file mode 100644 index 000000000..e475fb15f --- /dev/null +++ b/psydac/linalg/tests/test_matrix_free.py @@ -0,0 +1,127 @@ +import pytest +import numpy as np + +from psydac.linalg.block import BlockLinearOperator, BlockVector, BlockVectorSpace +from psydac.linalg.basic import LinearOperator, ZeroOperator, IdentityOperator, ComposedLinearOperator, SumLinearOperator, PowerLinearOperator, ScaledLinearOperator +from psydac.linalg.basic import MatrixFreeLinearOperator +from psydac.linalg.stencil import StencilVectorSpace, StencilVector, StencilMatrix +from psydac.linalg.solvers import ConjugateGradient, inverse +from psydac.ddm.cart import DomainDecomposition, CartDecomposition + +from psydac.linalg.tests.test_linalg import get_StencilVectorSpace, get_positive_definite_StencilMatrix, assert_pos_def + +def get_random_StencilMatrix(domain, codomain): + + np.random.seed(2) + V = domain + W = codomain + assert isinstance(V, StencilVectorSpace) + assert isinstance(W, StencilVectorSpace) + [n1, n2] = V._npts + [p1, p2] = V._pads + [P1, P2] = V._periods + assert (P1 == False) and (P2 == False) + + [m1, m2] = W._npts + [q1, q2] = W._pads + [Q1, Q2] = W._periods + assert (Q1 == False) and (Q2 == False) + + S = StencilMatrix(V, W) + + for i in range(0, q1+1): + if i != 0: + for j in range(-q2, q2+1): + S[:, :, i, j] = 2*np.random.random()-1 + else: + for j in range(1, q2+1): + S[:, :, i, j] = 2*np.random.random()-1 + S.remove_spurious_entries() + + return S + +def get_random_StencilVector(V): + np.random.seed(3) + assert isinstance(V, StencilVectorSpace) + [n1, n2] = V._npts + v = StencilVector(V) + for i in range(n1): + for j in range(n2): + v[i,j] = np.random.random() + return v + +#=============================================================================== +@pytest.mark.parametrize('n1', [3, 5]) +@pytest.mark.parametrize('n2', [4, 7]) +@pytest.mark.parametrize('p1', [2, 6]) +@pytest.mark.parametrize('p2', [3, 9]) + +def test_fake_matrix_free(n1, n2, p1, p2): + P1 = False + P2 = False + m1 = (n2+n1)//2 + m2 = n1+1 + q1 = p1 # using same degrees because both spaces must have same padding for now + q2 = p2 + V1 = get_StencilVectorSpace(n1, n2, p1, p2, P1, P2) + V2 = get_StencilVectorSpace(m1, m2, q1, q2, P1, P2) + S = get_random_StencilMatrix(codomain=V2, domain=V1) + O = MatrixFreeLinearOperator(codomain=V2, domain=V1, dot=lambda v: S @ v) + + print(f'O.domain = {O.domain}') + print(f'S.domain = {S.domain}') + print(f'V1: = {V1}') + v = get_random_StencilVector(V1) + tol = 1e-10 + y = S.dot(v) + x = O.dot(v) + print(f'error = {np.linalg.norm( (x - y).toarray() )}') + assert np.linalg.norm( (x - y).toarray() ) < tol + O.dot(v, out=x) + print(f'error = {np.linalg.norm( (x - y).toarray() )}') + assert np.linalg.norm( (x - y).toarray() ) < tol + +@pytest.mark.parametrize('solver', ['cg', 'pcg', 'bicg', 'minres', 'lsmr']) + +def test_solvers_matrix_free(solver): + print(f'solver = {solver}') + n1 = 4 + n2 = 3 + p1 = 5 + p2 = 2 + P1 = False + P2 = False + V = get_StencilVectorSpace(n1, n2, p1, p2, P1, P2) + A_SM = get_positive_definite_StencilMatrix(V) + assert_pos_def(A_SM) + AT_SM = A_SM.transpose() + A = MatrixFreeLinearOperator(domain=V, codomain=V, dot=lambda v: A_SM @ v, dot_transpose=lambda v: AT_SM @ v) + + # get rhs and solution + b = get_random_StencilVector(V) + x = A.dot(b) + + # Create Inverse with A + tol = 1e-6 + if solver == 'pcg': + inv_diagonal = A_SM.diagonal(inverse=True) + A_inv = inverse(A, solver, pc=inv_diagonal, tol=tol) + else: + A_inv = inverse(A, solver, tol=tol) + + AA = A_inv._A + xx = AA.dot(b) + print(f'norm(xx) = {np.linalg.norm( xx.toarray() )}') + print(f'norm(x) = {np.linalg.norm( x.toarray() )}') + + # Apply inverse and check + y = A_inv @ x + error = np.linalg.norm( (b - y).toarray()) + assert np.linalg.norm( (b - y).toarray() ) < tol + +#=============================================================================== +# SCRIPT FUNCTIONALITY +#=============================================================================== +if __name__ == "__main__": + import sys + pytest.main( sys.argv ) From 1e53d49f8f0cf9863ed3545ef735205e5cbe86f0 Mon Sep 17 00:00:00 2001 From: Martin Campos Pinto Date: Tue, 16 Jul 2024 03:32:32 +0200 Subject: [PATCH 09/11] require scipy >= 1.14 in requirements.txt Calls to scipy `minres` now use `rtol` instead of `tol` to support version 1.14.0, but this causes previous versions of scipy to fail --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6874bc8fd..a2f753062 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ wheel setuptools >= 61, != 67.2.0 numpy >= 1.16 +scipy >= 1.14 Cython >= 0.25, < 3.0 mpi4py >= 3.0.0 From 2e898b832e8780db564b1815a062751ee54430b3 Mon Sep 17 00:00:00 2001 From: Martin Campos Pinto Date: Tue, 16 Jul 2024 03:59:08 +0200 Subject: [PATCH 10/11] discard tests with python3.8 calls to scipy's minres now only use rtol which prevents from using scipy < 1.14. In turns this prevent from using python3.8 which is close to being unsupported anyway, see https://devguide.python.org/versions/ --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 826027fa1..83dd52cd1 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - python-version: [ 3.8, 3.9, '3.10', '3.11' ] + python-version: [ 3.9, '3.10', '3.11' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: From baf0231cec9552f5b8c546f9b01ffb507ff1f7a4 Mon Sep 17 00:00:00 2001 From: Martin Campos Pinto Date: Tue, 16 Jul 2024 05:43:31 +0200 Subject: [PATCH 11/11] avoid minres iteration if converged --- psydac/linalg/solvers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/psydac/linalg/solvers.py b/psydac/linalg/solvers.py index ca1079be7..862868ea2 100644 --- a/psydac/linalg/solvers.py +++ b/psydac/linalg/solvers.py @@ -1165,7 +1165,7 @@ def solve(self, b, out=None): A.dot(x, out=y) y -= b y *= -1.0 - y.copy(out=res_old) + y.copy(out=res_old) # res = b - A*x beta = sqrt(res_old.dot(res_old)) @@ -1193,8 +1193,15 @@ def solve(self, b, out=None): print( "+---------+---------------------+") template = "| {:7d} | {:19.2e} |" + # check whether solution is already converged: + if beta < tol: + istop = 1 + rnorm = beta + if verbose: + print( template.format(itn, rnorm )) - for itn in range(1, maxiter + 1 ): + while istop == 0 and itn < maxiter: + itn += 1 s = 1.0/beta y.copy(out=v)