Skip to content

Fixing sym method #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/196.test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixing `sym` method
85 changes: 55 additions & 30 deletions src/ansys/math/core/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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
-------
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.")
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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}"
)


Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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})"
Expand All @@ -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.
Expand Down
30 changes: 26 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import os

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the patch looks good. A few suggestions and comments will be provided below.

from pathlib import Path

import numpy as np
import pytest
from scipy import sparse

# import time

Expand All @@ -33,15 +35,15 @@
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


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
Expand Down Expand Up @@ -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

"""

Expand Down Expand Up @@ -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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider setting a random seed for reproducibility before generating the sparse random matrix.



@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)
48 changes: 40 additions & 8 deletions tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,35 @@ def test_shape(mm):
assert m1.shape == shape


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the current assert message with a more informative one:

Suggested change
assert sparse_mat.data.nbytes // 1024**2 > 4, f"Matrix size is not over gRPC message limit (4 MB): {sparse_mat.data.nbytes // 1024**2} MB"

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):
Expand Down Expand Up @@ -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)):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition of tests for symmetric dense and sparse matrices. It helps check if the server version meets the requirements.

Please make the suggested changes to the assert messages for better readability and submit the patch for review again.

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)
Expand Down
Loading