diff --git a/.github/workflows/build_and_upload_wheels.yml b/.github/workflows/deploy.yml similarity index 100% rename from .github/workflows/build_and_upload_wheels.yml rename to .github/workflows/deploy.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e632d0546..69b5d5b64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,8 +10,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] - os: [ubuntu-20.04, macos-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-20.04, macos-13, macos-14, windows-latest] steps: - uses: actions/checkout@v3 diff --git a/gustaf/_version.py b/gustaf/_version.py index 2a1e88ffd..4d3475fbb 100644 --- a/gustaf/_version.py +++ b/gustaf/_version.py @@ -3,4 +3,4 @@ Current version. """ -version = "0.0.24" +version = "0.0.25" diff --git a/gustaf/helpers/data.py b/gustaf/helpers/data.py index 1d01f55b8..2b879ddf9 100644 --- a/gustaf/helpers/data.py +++ b/gustaf/helpers/data.py @@ -251,6 +251,20 @@ def __contains__(self, key): """ return key in self._saved + def __len__(self): + """ + Returns number of items. + + Parameters + ---------- + None + + Returns + ------- + len: int + """ + return len(self._saved) + def pop(self, key, default=None): """ Applied pop() to saved data @@ -318,6 +332,20 @@ def items(self): """ return self._saved.items() + def update(self, **kwargs): + """ + Updates given kwargs using __setitem__. + + Parameters + ---------- + **kwargs: kwargs + + Returns + ------- + None + """ + self._saved.update(**kwargs) + class ComputedData(DataHolder): _depends = None diff --git a/gustaf/io/meshio.py b/gustaf/io/meshio.py index ad66e6ebc..c2706ae48 100644 --- a/gustaf/io/meshio.py +++ b/gustaf/io/meshio.py @@ -84,7 +84,7 @@ def load(fname): return meshes[0] if len(meshes) == 1 else meshes -def export(mesh, fname, submeshes=None, **kwargs): +def export(fname, mesh, submeshes=None, **kwargs): """Export mesh elements and vertex data into meshio and use its write function. The definition of submeshes with identical vertex coordinates is possible. In that case vertex numbering and data from the main mesh @@ -132,10 +132,10 @@ def export(mesh, fname, submeshes=None, **kwargs): Parameters ------------ - mesh: Edges, Faces or Volumes - Input mesh fname: Union[str, pathlib.Path] File to save the mesh in. + mesh: Edges, Faces or Volumes + Input mesh submeshes: Iterable Submeshes where the vertices are identical to the main mesh. The element type can be identical to mesh.elements or lower-dimensional (e.g. @@ -164,7 +164,8 @@ def export(mesh, fname, submeshes=None, **kwargs): cells = [] # Merge main mesh and submeshes in one list - meshes = [mesh] + meshes = mesh if isinstance(mesh, list) else [mesh] + if submeshes is not None: meshes.extend(submeshes) diff --git a/gustaf/io/mfem.py b/gustaf/io/mfem.py index 1b38ac711..c4ca9e890 100644 --- a/gustaf/io/mfem.py +++ b/gustaf/io/mfem.py @@ -107,15 +107,15 @@ def extract_values(fname, start_index, n_lines, total_lines, dtype): return mesh -def export(mesh, fname): +def export(fname, mesh): """Export mesh in MFEM format. Supports 2D triangle and quadrilateral meshes. Does not support different element attributes or difference in vertex dimension and mesh dimension. Parameters ------------ - mesh: Faces fname: str + mesh: Faces Returns ------------ diff --git a/gustaf/io/mixd.py b/gustaf/io/mixd.py index 60025d8dc..e873e54ce 100644 --- a/gustaf/io/mixd.py +++ b/gustaf/io/mixd.py @@ -116,8 +116,8 @@ def load( def export( - mesh, fname, + mesh, space_time=False, dual=False, ): diff --git a/gustaf/io/nutils.py b/gustaf/io/nutils.py index b5915c462..f12d359e2 100644 --- a/gustaf/io/nutils.py +++ b/gustaf/io/nutils.py @@ -61,14 +61,14 @@ def load(fname): return mesh -def export(mesh, fname): +def export(fname, mesh): """Export in Nutils format. Files are saved as np.savez(). Supports triangle,and tetrahedron Meshes. Parameters ----------- - mesh: Faces or Volumes fname: str + mesh: Faces or Volumes Returns -------- diff --git a/pyproject.toml b/pyproject.toml index 466cffad5..6dda0e807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,12 @@ keywords = [ "visualization", "mesh", ] -requires-python = ">=3.7" +requires-python = ">=3.8" license = {file = "LICENSE.txt"} classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -81,7 +80,7 @@ version = {attr = "gustaf._version.version"} [tool.ruff] line-length = 79 -target-version = "py37" +target-version = "py38" [tool.ruff.lint] select = [ @@ -107,7 +106,6 @@ ignore = [ "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "B904", # Within an `except` clause, raise exceptions with ... - # "PLR0911", # Too many return statements ] [tool.ruff.lint.per-file-ignores] diff --git a/tests/test_helpers/test_data.py b/tests/test_helpers/test_data.py new file mode 100644 index 000000000..c1bdb6e5e --- /dev/null +++ b/tests/test_helpers/test_data.py @@ -0,0 +1,302 @@ +import sys + +import numpy as np +import pytest + +import gustaf + + +def new_tracked_array(dtype=float): + """ + create new tracked array and checks if default flags are set correctly. + Then sets modified to False, to give an easy start for testing + """ + ta = gustaf.helpers.data.make_tracked_array( + [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + ], + dtype=dtype, + ) + + assert ta.modified + assert ta._super_arr + + ta.modified = False + + return ta + + +def test_TrackedArray(): + """test if modified flag is well set""" + # 1. set item + ta = new_tracked_array() + ta[0] = 1 + assert ta.modified + + ta = new_tracked_array() + ta[1, 1] = 2 + assert ta.modified + + # in place + ta = new_tracked_array() + ta += 5 + assert ta.modified + + ta = new_tracked_array() + ta -= 3 + assert ta.modified + + ta = new_tracked_array() + ta *= 1 + assert ta.modified + + ta = new_tracked_array() + ta /= 1.5 + assert ta.modified + + # old distributions of numpy does not have this feature + if sys.version_info > (3, 9): + ta = new_tracked_array() + ta @= ta + assert ta.modified + + ta = new_tracked_array() + ta **= 2 + assert ta.modified + + ta = new_tracked_array() + ta %= 3 + assert ta.modified + + ta = new_tracked_array() + ta //= 2 + assert ta.modified + + ta = new_tracked_array(int) + ta <<= 3 + assert ta.modified + + ta = new_tracked_array(int) + ta >>= 1 + assert ta.modified + + ta = new_tracked_array(int) + ta |= 3 + assert ta.modified + + ta = new_tracked_array(int) + ta &= 3 + assert ta.modified + + ta = new_tracked_array(int) + ta ^= 3 + assert ta.modified + + # child array modification + ta = new_tracked_array() + ta_child = ta[0] + assert ta_child.base is ta + ta_child += 5 + assert ta.modified + assert ta_child.modified + + # copy returns normal np.ndarray + assert isinstance(new_tracked_array().copy(), np.ndarray) + + +def test_DataHolder(): + """Base class of dataholder types""" + + class Helpee: + pass + + helpee = Helpee() + + dataholder = gustaf.helpers.data.DataHolder(helpee) + + # setitem is pure abstract + with pytest.raises(NotImplementedError): + dataholder["somedata"] = [] + + # test other functions by injecting some keys and values directly to the + # member + dataholder._saved.update(a=1, b=2, c=3) + + # getitem + assert dataholder["a"] == 1 + assert dataholder["b"] == 2 + assert dataholder["c"] == 3 + with pytest.raises(KeyError): + dataholder["d"] + + # contains + assert "a" in dataholder + assert "b" in dataholder + assert "c" in dataholder + assert "d" not in dataholder + assert "e" not in dataholder + + # len + assert len(dataholder) == 3 + + # pop + assert dataholder.pop("c") == 3 + assert "c" not in dataholder + + # get + # 1. key + assert dataholder.get("a") == 1 + # 2. key and default + assert dataholder.get("b", 2) == 2 + # 3. key and wrong default + assert dataholder.get("b", 3) == 2 + # 4. empty key - always None + assert dataholder.get("c") is None + # 5. empty key and default + assert dataholder.get("c", "123") == "123" + + # keys + assert len(set(dataholder.keys()).difference({"a", "b"})) == 0 + + # values + assert len(set(dataholder.values()).difference({1, 2})) == 0 + + # items + for k, v in dataholder.items(): + assert k in dataholder.keys() # noqa SIM118 + assert v in dataholder.values() + + # update + dataholder.update(b=22, c=33, d=44) + assert dataholder["a"] == 1 + assert dataholder["b"] == 22 + assert dataholder["c"] == 33 + assert dataholder["d"] == 44 + + # clear + dataholder.clear() + assert "a" not in dataholder + assert "b" not in dataholder + assert "c" not in dataholder + assert "d" not in dataholder + assert len(dataholder) == 0 + + +@pytest.mark.parametrize( + "grid", ("edges", "faces_tri", "faces_quad", "volumes_tet", "volumes_hexa") +) +def test_ComputedData(grid, request): + grid = request.getfixturevalue(grid) + + # vertex related data + v_data = ( + "unique_vertices", + "bounds", + "bounds_diagonal", + "bounds_diagonal_norm", + ) + + # element related data + e_data = ( + "sorted_edges", + "unique_edges", + "single_edges", + "edges", + "sorted_faces", + "unique_faces", + "single_faces", + "faces", + "sorted_volumes", + "unique_volumes", + ) + + # for both + both_data = ("centers", "referenced_vertices") + + # entities before modification + data_dependency = {"vertex": v_data, "element": e_data, "both": both_data} + before = {} + for dependency, attributes in data_dependency.items(): + # init + before[dependency] = {} + for attr in attributes: + func = getattr(grid, attr, None) + if attr is not None and callable(func): + before[dependency][attr] = func() + + # ensure that func is called at least once + assert len(before[dependency]) != 0 + + # loop to check if you get the saved data + for attributes in before.values(): + for attr, value in attributes.items(): + func = getattr(grid, attr, None) + assert value is func() + + # change vertices - assign new vertices + grid.vertices = grid.vertices.copy() + for dependency, attributes in before.items(): + if dependency == "element": + continue + for attr, value in attributes.items(): + func = getattr(grid, attr, None) + assert value is not func() # should be different object + + # change elements - assign new elements + grid.elements = grid.elements.copy() + for dependency, attributes in before.items(): + if dependency == "vertex": + continue + for attr, value in attributes.items(): + func = getattr(grid, attr, None) + assert value is not func() + + +@pytest.mark.parametrize( + "grid", ("edges", "faces_tri", "faces_quad", "volumes_tet", "volumes_hexa") +) +def test_VertexData(grid, request): + grid = request.getfixturevalue(grid) + + key = "vertices" + + # set data + grid.vertex_data[key] = grid.vertices + + # get_data - data is viewed as TrackedArray, so check against base + assert grid.vertices is grid.vertex_data[key].base + + # scalar extraction should return a norm + assert np.allclose( + grid.vertex_data.as_scalar(key).ravel(), + np.linalg.norm(grid.vertex_data.get(key), axis=1), + ) + + # norms should be saved, as long as data array isn't changed + assert grid.vertex_data.as_scalar(key) is grid.vertex_data.as_scalar(key) + + before = grid.vertex_data.as_scalar(key) + # trigger modified flag on data - either reset or inplace change + # reset first - with copy, just so that we can try to make inplace changes + # later + grid.vertex_data[key] = grid.vertex_data[key].copy() + assert before is not grid.vertex_data.as_scalar(key) + assert grid.vertex_data.as_scalar(key) is grid.vertex_data.as_scalar(key) + + grid.vertex_data[key][0] = grid.vertex_data[key][0] + assert before is not grid.vertex_data.as_scalar(key) + assert grid.vertex_data.as_scalar(key) is grid.vertex_data.as_scalar(key) + + # check arrow data + assert grid.vertex_data[key] is grid.vertex_data.as_arrow(key) + + # check wrong length assignment + with pytest.raises(ValueError): + grid.vertex_data["bad"] = np.vstack((grid.vertices, grid.vertices)) + + # check wrong arrow data request + with pytest.raises(ValueError): + grid.vertex_data["norm"] = grid.vertex_data.as_scalar(key) + grid.vertex_data.as_arrow("norm")