diff --git a/doc/changelog.d/196.test.md b/doc/changelog.d/196.test.md new file mode 100644 index 00000000..9704c778 --- /dev/null +++ b/doc/changelog.d/196.test.md @@ -0,0 +1 @@ +Fixing `sym` method \ No newline at end of file diff --git a/src/ansys/math/core/math.py b/src/ansys/math/core/math.py index 59deada4..c7fd92a8 100644 --- a/src/ansys/math/core/math.py +++ b/src/ansys/math/core/math.py @@ -46,6 +46,7 @@ from ansys.tools.versioning import requires_version from ansys.tools.versioning.utils import server_meets_version import numpy as np +from scipy import sparse MYCTYPE = { np.int32: "I", @@ -437,7 +438,7 @@ def rand(self, nrow, ncol=None, dtype=np.double, name=None, asarray=False): return self.vec(nrow, dtype, init="rand", name=name, asarray=asarray) return self.mat(nrow, ncol, dtype, init="rand", name=name, asarray=asarray) - def matrix(self, matrix, name=None, triu=False): + def matrix(self, matrix, name=None, sym=None): """Send a SciPy matrix or NumPy array to MAPDL. Parameters @@ -447,9 +448,9 @@ def matrix(self, matrix, name=None, triu=False): name : str, optional AnsMath matrix name. The default is ``None``, in which case a name is automatically generated. - triu : bool, optional - Whether the matrix is the upper triangular. The default is ``False``, - which means that the matrix is unsymmetric. + sym : bool, optional + Whether the matrix is symmetric rather than dense. + The default is ``None`` which means that the matrix will be tested whether it is symmetric or not. Returns ------- @@ -479,9 +480,7 @@ def matrix(self, matrix, name=None, triu=False): elif not isinstance(name, str): raise TypeError("``name`` parameter must be a string") - from scipy import sparse - - self._set_mat(name, matrix, triu) + self._set_mat(name, matrix, sym) if sparse.issparse(matrix): ans_mat = AnsSparseMat(name, self._mapdl) else: @@ -1193,7 +1192,7 @@ def _set_vec(self, vname, arr, dtype=None, chunk_size=DEFAULT_CHUNKSIZE): self._mapdl._stub.SetVecData(chunks_generator) @protect_grpc - def _set_mat(self, mname, arr, sym=False, dtype=None, chunk_size=DEFAULT_CHUNKSIZE): + def _set_mat(self, mname, arr, sym=None, dtype=None, chunk_size=DEFAULT_CHUNKSIZE): """Transfer a 2D dense or sparse SciPy array to MAPDL as an AnsMath matrix. Parameters @@ -1204,7 +1203,7 @@ def _set_mat(self, mname, arr, sym=False, dtype=None, chunk_size=DEFAULT_CHUNKSI Matrix to upload. sym : bool Whether the matrix is symmetric rather than dense. - The default is ``False`` which means that the matrix is dense. + The default is ``None`` which means that the matrix will be tested whether it is symmetric or not. dtype : np.dtype, optional NumPy data type to upload the array as. The options are ``np.double``, ``np.int32``, and ``np.int64``. The default is the current array @@ -1213,7 +1212,6 @@ def _set_mat(self, mname, arr, sym=False, dtype=None, chunk_size=DEFAULT_CHUNKSI Chunk size in bytes. The value must be less than 4MB. """ - from scipy import sparse if ":" in mname: raise ValueError("The character ':' is not permitted in the name of an AnsMath matrix.") @@ -1227,6 +1225,12 @@ def _set_mat(self, mname, arr, sym=False, dtype=None, chunk_size=DEFAULT_CHUNKSI raise ValueError("Arrays must be 2-dimensional.") if sparse.issparse(arr): + if sym is None: + arrT = arr.T + sym = ( + bool(np.allclose(arr.data, arrT.data)) + and bool(np.allclose(arr.indices, arrT.indices)) + ) and bool(np.allclose(arr.indptr, arrT.indptr)) self._send_sparse(mname, arr, sym, dtype, chunk_size) else: # must be dense matrix self._send_dense(mname, arr, dtype, chunk_size) @@ -1250,9 +1254,6 @@ def _send_dense(self, mname, arr, dtype, chunk_size): def _send_sparse(self, mname, arr, sym, dtype, chunk_size): """Send a SciPy sparse sparse matrix to MAPDL.""" - if sym is None: - raise ValueError("The symmetric flag ``sym`` must be set for a sparse matrix.") - from scipy import sparse arr = sparse.csr_matrix(arr) @@ -1272,9 +1273,9 @@ def _send_sparse(self, mname, arr, sym, dtype, chunk_size): # data vector dataname = f"{mname}_DATA" - ans_vec = self.set_vec(arr.data, dataname) + self.set_vec(arr.data, dataname) if dtype is None: - info = self._mapdl._data_info(ans_vec.id) + info = self._mapdl._data_info(dataname) dtype = ANSYS_VALUE_TYPE[info.stype] # indptr vector @@ -1289,8 +1290,7 @@ def _send_sparse(self, mname, arr, sym, dtype, chunk_size): flagsym = "TRUE" if sym else "FALSE" self._mapdl.run( - f"*SMAT,{mname},{MYCTYPE[dtype]},ALLOC,CSR,{indptrname},{indxname}," - f"{dataname},{flagsym}" + f"*SMAT,{mname},{MYCTYPE[dtype]},ALLOC,CSR,{indptrname},{indxname},{dataname},{flagsym}" ) @@ -1701,22 +1701,46 @@ def sym(self) -> bool: bool ``True`` when this matrix is symmetric. + Notes + ----- + ``sym`` requires MAPDL version 2022R2 or later. If a previous version is used, the default value is False. + """ info = self._mapdl._data_info(self.id) if server_meets_version(self._mapdl._server_version, (0, 5, 0)): # pragma: no cover - return info.mattype in [ - 0, - 1, - 2, - ] # [UPPER, LOWER, DIAG] respectively - - warn( - "Call to ``sym`` method cannot evaluate if this matrix is symmetric " - "with this version of MAPDL." - ) - return True + sym = True + type_mat = info.mattype + if type_mat == 2: # [UPPER=0, LOWER=1, DIAG=2, FULL=3] + pass # A diagonal matrix is symmetric. + + else: + if info.objtype == 2: # DMAT + n = info.size1 + i = 2 + j = 1 + t = 1e-16 + while i < n and sym is True: + while j < i and sym is True: + if abs(self[i][j] - self[j][i]) > t: + sym = False + j += 1 + i += 1 + + elif info.objtype == 3: # SMAT + mat = self.asarray() + matT = mat.T + sym = ( + bool(np.allclose(mat.data, matT.data)) + and bool(np.allclose(mat.indices, matT.indices)) + ) and bool(np.allclose(mat.indptr, matT.indptr)) + + else: + warn("``sym`` requires MAPDL version 2022R2 or later. The default value is False.") + sym = False + + return sym def asarray(self, dtype=None) -> np.ndarray: """Return the matrix as a NumPy array. @@ -1854,9 +1878,10 @@ def copy(self): class AnsSparseMat(AnsMat): """Provides the AnsMath sparse matrix objects.""" - def __init__(self, uid, mapdl): + def __init__(self, uid, mapdl, sym=True): """Initiate an AnsMath sparse matrix object.""" AnsMat.__init__(self, uid, mapdl, ObjType.SMAT) + self.sym = sym def __repr__(self): return f"AnsMath sparse matrix ({self.nrow}, {self.ncol})" @@ -1876,7 +1901,7 @@ def copy(self): AnsMath sparse matrix (126, 126) """ - return AnsSparseMat(AnsMathObj.copy(self), self._mapdl) + return AnsSparseMat(AnsMathObj.copy(self), self._mapdl, self._sym) def todense(self) -> np.ndarray: """Return the array as a NumPy dense array. diff --git a/tests/conftest.py b/tests/conftest.py index ac113597..a1da044e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,9 @@ import os from pathlib import Path +import numpy as np import pytest +from scipy import sparse # import time @@ -33,7 +35,7 @@ from ansys.mapdl.core._version import SUPPORTED_ANSYS_VERSIONS from ansys.mapdl.core.errors import MapdlExitedError from ansys.mapdl.core.launcher import MAPDL_DEFAULT_PORT, get_start_instance -from ansys.tools.path import find_ansys +from ansys.tools.path import find_mapdl # Check if MAPDL is installed # NOTE: checks in this order to get the newest installed version @@ -41,7 +43,7 @@ valid_rver = SUPPORTED_ANSYS_VERSIONS.keys() -EXEC_FILE, rver = find_ansys() +EXEC_FILE, rver = find_mapdl() if rver: rver = int(rver * 10) HAS_GRPC = int(rver) >= 211 or ON_CI @@ -79,8 +81,8 @@ {os_msg} If you do have Ansys installed, you may have to patch pymapdl to -automatically find your Ansys installation. Email the developer at: -alexander.kaszynski@ansys.com +automatically find your Ansys installation. Email the developers at: +pyansys.core@ansys.com """ @@ -174,3 +176,23 @@ def cube_solve(cleared, mapdl): # solve first 10 non-trivial modes out = mapdl.modal_analysis(nmode=10, freqb=1) + + +@pytest.fixture(scope="function") +def sparse_asym_mat(): + return sparse.random(5000, 5000, density=0.05, format="csr") + + +@pytest.fixture(scope="function") +def sparse_sym_mat(sparse_asym_mat): + return sparse_asym_mat + (sparse_asym_mat.T) + + +@pytest.fixture(scope="function") +def dense_asym_mat(): + return np.random.rand(1000, 1000) + + +@pytest.fixture(scope="function") +def dense_sym_mat(dense_asym_mat): + return dense_asym_mat + (dense_asym_mat.T) diff --git a/tests/test_math.py b/tests/test_math.py index b1c17744..d2afbbc5 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -204,19 +204,35 @@ def test_shape(mm): assert m1.shape == shape -def test_matrix(mm): - sz = 5000 - mat = sparse.random(sz, sz, density=0.05, format="csr") - assert mat.data.nbytes // 1024**2 > 4, "Must test over gRPC message limit" +@pytest.mark.parametrize("sparse_mat", ["sparse_asym_mat", "sparse_sym_mat"]) +def test_sparse_matrix(mm, sparse_mat, request): + sparse_mat = request.getfixturevalue(sparse_mat) + assert sparse_mat.data.nbytes // 1024**2 > 4, "Must test over gRPC message limit" name = "TMP_MATRIX" - ans_mat = mm.matrix(mat, name) + ans_mat = mm.matrix(sparse_mat, name) assert ans_mat.id == name mat_back = ans_mat.asarray() - assert np.allclose(mat.data, mat_back.data) - assert np.allclose(mat.indices, mat_back.indices) - assert np.allclose(mat.indptr, mat_back.indptr) + assert np.allclose(sparse_mat.data, mat_back.data) + assert np.allclose(sparse_mat.indices, mat_back.indices) + assert np.allclose(sparse_mat.indptr, mat_back.indptr) + + +@pytest.mark.parametrize("dense_mat", ["dense_asym_mat", "dense_sym_mat"]) +def test_dense_matrix(mm, dense_mat, request): + mapdl_version = mm._mapdl.version + if mapdl_version < 21.2: + pytest.skip("Requires MAPDL 2021 R2 or later.") + + dense_mat = request.getfixturevalue(dense_mat) + + name = "TMP_MATRIX" + ans_mat = mm.matrix(dense_mat, name) + assert ans_mat.id == name + + mat_back = ans_mat.asarray() + assert np.allclose(dense_mat, mat_back) def test_matrix_fail(mm): @@ -742,6 +758,22 @@ def test_invalid_sparse_name(mm): mm.matrix(mat, name=1) +def test_sym_dmat(mm, dense_sym_mat): + dmat = mm.matrix(dense_sym_mat) + if not server_meets_version(mm._server_version, (0, 5, 0)): + assert dmat.sym() is False + else: + assert dmat.sym() is True + + +def test_sym_smat(mm, sparse_sym_mat): + smat = mm.matrix(sparse_sym_mat) + if not server_meets_version(mm._server_version, (0, 5, 0)): + assert smat.sym() is False + else: + assert smat.sym() is True + + def test_free_all(mm): my_mat1 = mm.ones(10) my_mat2 = mm.ones(10)