From 0bc6ad64b1c6dbeffcf65a11fe00fa28e9faee9d Mon Sep 17 00:00:00 2001 From: Afshawn Lotfi Date: Fri, 12 Dec 2025 18:51:44 +0000 Subject: [PATCH] Add SU2 mesh format support and comprehensive unit tests - Introduced shared types and mappings for SU2 mesh format in `su2fmt/types.py`. - Implemented a new test suite for SU2 mesh functionality in `tests/su2mesh_test.py`, covering: - Basic mesh creation and properties for 2D and 3D meshes. - Mesh creation with boundary markers and different element types. - Parsing of simple and whitespace-affected mesh files. - Round-trip parsing and exporting of mesh files. - Handling of empty meshes and multizone meshes with varying dimensions. - Removed outdated test file `tests/test_su2mesh.py` to streamline testing structure. --- .devcontainer/Dockerfile | 35 -- .devcontainer/devcontainer.json | 25 +- .github/workflows/pypi-publish.yml | 27 ++ .github/workflows/python-test.yml | 28 ++ .gitignore | 2 + Makefile | 29 ++ README.md | 4 +- examples/onera_wing.ipynb | 287 ++++++---------- pyproject.toml | 4 +- su2fmt/__init__.py | 2 +- su2fmt/exporter.py | 101 +++--- su2fmt/mesh.py | 45 --- su2fmt/parser.py | 188 ++++++++--- su2fmt/types.py | 28 ++ tests/su2mesh_test.py | 507 +++++++++++++++++++++++++++++ tests/test_su2mesh.py | 353 -------------------- 16 files changed, 922 insertions(+), 743 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 .github/workflows/pypi-publish.yml create mode 100644 .github/workflows/python-test.yml create mode 100644 Makefile delete mode 100644 su2fmt/mesh.py create mode 100644 su2fmt/types.py create mode 100644 tests/su2mesh_test.py delete mode 100644 tests/test_su2mesh.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index fea9559..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/base:jammy - -RUN apt-get update -y && \ - apt install -y libgl1-mesa-glx curl software-properties-common wget libgl1-mesa-glx xvfb libasound2 libgtk-3-0 libx11-xcb1 && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -USER vscode -WORKDIR /home/vscode -RUN ARCH=$(uname -m) && \ - if [ "$ARCH" = "x86_64" ]; then \ - MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-py312_24.7.1-0-Linux-x86_64.sh"; \ - elif [ "$ARCH" = "aarch64" ]; then \ - MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-py312_24.7.1-0-Linux-aarch64.sh"; \ - else \ - echo "Unsupported architecture: $ARCH"; exit 1; \ - fi && \ - wget $MINICONDA_URL && \ - chmod +x $(basename $MINICONDA_URL) && \ - ./$(basename $MINICONDA_URL) -b && \ - rm $(basename $MINICONDA_URL) - - - -# Install Python dependencies from requirements_dev.txt if it exists -# Initialize conda and run package installs -RUN CONDA_PATH=/home/vscode/miniconda3/bin/conda && \ - $CONDA_PATH init && \ - $CONDA_PATH install -n base ipykernel --update-deps --force-reinstall -y && \ - $CONDA_PATH install -n base -c conda-forge cadquery - -RUN . /home/vscode/miniconda3/etc/profile.d/conda.sh && \ - conda activate base && \ - pip install ezdxf --upgrade - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e5002e8..afed12c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,22 +1,15 @@ { "name": "Python 3", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - "args": { - // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local on arm64/Apple Silicon. - "VARIANT": "3.10-bullseye", - // Options - "NODE_VERSION": "lts/*" + "image": "mcr.microsoft.com/devcontainers/python:3.10", + // Add the IDs of extensions you want installed when the container is created. + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "eamodio.gitlens" + ] } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "eamodio.gitlens" - ], "remoteUser": "vscode" } \ No newline at end of file diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..4df963d --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,27 @@ +name: Publish Python Package + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install . + + - name: Build package + run: cd python && python -m build --sdist + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: dist/ \ No newline at end of file diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..1c57f26 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,28 @@ +name: Python Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + - name: Test with unittest + run: | + python -m unittest discover tests \ No newline at end of file diff --git a/.gitignore b/.gitignore index 19c24be..41113c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.tar.gz +*.zip *.cgns *.su2 *.geo_unrolled diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e83589 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# Hypersim Build Pipeline Makefile + +# Build Python package only +build-package: + @echo "๐Ÿ“ฆ Building Python package..." + python -m build + +# Clean build artifacts +clean: + @echo "๐Ÿงน Cleaning build artifacts..." + rm -rf dist/ build/ *.egg-info/ + @echo "โœ… Clean completed" + +# Run tests +test: + @echo "๐Ÿงช Running tests..." + python -m unittest discover -s tests -v + +# Install package in development mode +install: + @echo "๐Ÿ“ฆ Installing package in development mode..." + pip install -e . + pip uninstall -y su2fmt + +# Quick build without push (for testing) +build: + @echo "๐Ÿ—๏ธ Building locally (no push)..." + python -m build + @echo "โœ… Local build completed" diff --git a/README.md b/README.md index a03d27e..3194e92 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,10 @@ mesh = parse_mesh("example.su2") # export mesh file export_mesh(mesh, "example_generated.su2") ``` -#### Note: `visualize_mesh` has been moved to [ezmesh](https://github.com/OpenOrion/ezmesh) - # Devlopement Setup ``` git clone https://github.com/OpenOrion/su2fmt.git cd su2fmt -pip install -r requirements_dev.txt +make install ``` diff --git a/examples/onera_wing.ipynb b/examples/onera_wing.ipynb index 45f59e0..8aae179 100644 --- a/examples/onera_wing.ipynb +++ b/examples/onera_wing.ipynb @@ -10,8 +10,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Downloading SU2 file to examples/mesh_ONERAM6_inv_ffd.su2...\n", - "Download complete.\n" + "SU2 file already exists at examples/mesh_ONERAM6_inv_ffd.su2\n" ] } ], @@ -38,217 +37,115 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "469dfd40", "metadata": {}, + "outputs": [], + "source": [ + "from su2fmt import parse_mesh, export_mesh\n", + "\n", + "# parses mesh file\n", + "mesh = parse_mesh(\"examples/mesh_ONERAM6_inv_ffd.su2\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "344fadeb", + "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "SU2Mesh(vertices=array([[ 1.0478000e+01, 1.0958086e+01, -9.6719999e+00],\n", - " [ 1.0478000e+01, 0.0000000e+00, -9.6719999e+00],\n", - " [ 1.0478000e+01, 1.0958086e+01, 9.6719999e+00],\n", - " ...,\n", - " [ 1.1782739e+00, 6.7358527e+00, 8.1744852e+00],\n", - " [ 1.2735977e-06, 5.4790425e+00, 7.2858000e+00],\n", - " [-3.9809862e-01, 5.5136032e+00, -3.0066946e+00]],\n", - " shape=(108396, 3), dtype=float32), indices=array([99733, 99768, 99766, ..., 1911, 55863, 55],\n", - " shape=(1748256,), dtype=uint32), element_types=array([10, 10, 10, ..., 10, 10, 10], shape=(582752,), dtype=int32), markers={'LOWER_SIDE': array([13342, 13341, 12920, ..., 16404, 16437, 16438], shape=(51852,)), 'TIP': array([ 9886, 9896, 9894, ..., 10223, 10227, 10224], shape=(7791,)), 'UPPER_SIDE': array([ 7712, 8165, 8168, ..., 4333, 4342, 13912], shape=(49719,)), 'XNORMAL_FACES': array([ 72, 4713, 75, 5, 1924, 80, 1, 62, 1918, 66, 67,\n", - " 4685, 3, 75, 4715, 57, 59, 4660, 85, 4742, 4751, 4756,\n", - " 4759, 4758, 4736, 4748, 4751, 4729, 4730, 4767, 54, 4658, 4656,\n", - " 4728, 4753, 4731, 4652, 4711, 4709, 4657, 4658, 4715, 4650, 4668,\n", - " 4656, 4650, 4656, 4654, 4770, 4776, 96, 89, 91, 4759, 53,\n", - " 4647, 71, 86, 4744, 4743, 75, 4713, 4715, 4770, 4773, 4776,\n", - " 4758, 4774, 4773, 68, 4690, 4689, 4761, 4760, 4758, 4728, 4729,\n", - " 4774, 4748, 4750, 4751, 4760, 4761, 4753, 4738, 4739, 4758, 4688,\n", - " 4689, 4690, 4687, 4690, 4685, 4660, 4662, 4661, 4654, 4658, 4657,\n", - " 4650, 4654, 4651, 65, 68, 4689, 4731, 4753, 1923, 4661, 4685,\n", - " 1917, 69, 4683, 4648, 4747, 4749, 4748, 4737, 4746, 4748, 4728,\n", - " 4731, 4730, 4713, 4711, 4652, 4672, 4674, 4688, 4672, 4673, 4675,\n", - " 4653, 4654, 4657, 7, 1923, 92, 4, 4744, 86, 65, 4683,\n", - " 69, 4776, 4775, 94, 73, 4709, 4711, 4773, 4774, 4776, 65,\n", - " 4689, 4683, 4738, 4773, 4770, 4736, 4738, 4737, 4657, 4715, 4713,\n", - " 4670, 4673, 4672, 4651, 4653, 4683, 60, 4675, 63, 53, 69,\n", - " 4648, 4731, 1923, 84, 82, 4730, 4731, 61, 4673, 4670, 4743,\n", - " 4744, 4747, 53, 4648, 4647, 4654, 4656, 4658, 88, 92, 4753,\n", - " 0, 1917, 67, 61, 4670, 62, 2, 71, 4709, 66, 4685,\n", - " 4690, 71, 4647, 4709, 4729, 4767, 4775, 4672, 4688, 4687, 4663,\n", - " 4670, 4672, 54, 76, 4658, 56, 57, 4661, 54, 4656, 64,\n", - " 89, 4759, 4756, 4761, 4758, 4759, 4735, 4743, 4746, 4651, 4654,\n", - " 4653, 4776, 94, 96, 85, 4751, 87, 82, 4731, 83, 5,\n", - " 87, 1924, 61, 63, 4673, 4770, 96, 95, 86, 4743, 4735,\n", - " 1918, 4670, 4660, 60, 4668, 4675, 4748, 4749, 4750, 1918, 4660,\n", - " 59, 4737, 4748, 4736, 4687, 4688, 4690, 4687, 4685, 4662, 78,\n", - " 4747, 79, 6, 97, 1922, 60, 64, 4668, 94, 4775, 4767,\n", - " 92, 1923, 4753, 4750, 80, 1924, 4713, 72, 4711, 4756, 4758,\n", - " 4739, 4735, 4737, 4770, 4760, 4753, 4728, 4683, 4689, 4688, 4652,\n", - " 4653, 4657, 4648, 4653, 4652, 1, 1918, 59, 76, 4715, 4658,\n", - " 4751, 1924, 87, 4739, 4742, 4756, 4651, 4688, 4674, 4674, 4675,\n", - " 4668, 4660, 4663, 4662, 86, 4735, 93, 66, 4690, 68, 4,\n", - " 79, 4744, 0, 56, 1917, 97, 4767, 1922, 93, 4735, 4770,\n", - " 79, 4747, 4744, 4661, 1917, 56, 64, 4656, 4668, 4735, 4746,\n", - " 4737, 4728, 4730, 4729, 4663, 4672, 4687, 4652, 4657, 4713, 4651,\n", - " 4683, 4688, 4661, 4662, 4685, 4648, 4709, 4647, 4648, 4652, 4709,\n", - " 85, 90, 4742, 6, 1922, 82, 3, 4715, 76, 1922, 4767,\n", - " 4730, 4750, 4749, 80, 67, 1917, 4685, 4774, 4775, 4776, 4738,\n", - " 4758, 4773, 63, 4675, 4673, 4746, 4747, 4748, 4736, 4751, 4742,\n", - " 4736, 4739, 4738, 4650, 4651, 4674, 88, 4761, 91, 83, 4731,\n", - " 84, 72, 73, 4711, 4751, 4750, 1924, 88, 4753, 4761, 90,\n", - " 4756, 4742, 78, 4749, 4747, 62, 4670, 1918, 4758, 4760, 4774,\n", - " 4729, 4775, 4774, 4743, 4747, 4746, 57, 4660, 4661, 4662, 4663,\n", - " 4687, 4648, 4683, 4653, 89, 4756, 90, 78, 80, 4749, 7,\n", - " 84, 1923, 2, 4709, 73, 1922, 4730, 82, 91, 4761, 4759,\n", - " 4737, 4738, 4770, 4736, 4742, 4739, 4728, 4774, 4760, 4672, 4675,\n", - " 4674, 4660, 4670, 4663, 4674, 4668, 4650, 94, 4767, 97, 93,\n", - " 4770, 95]), 'ZNORMAL_FACES': array([ 77, 4724, 79, 1913, 4720, 4724, 4646, 19337, 19331,\n", - " 4787, 4782, 4788, 4710, 4712, 73, 4719, 4726, 4723,\n", - " 4667, 4664, 4645, 1906, 4643, 19334, 4645, 4646, 19334,\n", - " 19375, 19376, 4707, 101, 4706, 104, 1903, 4787, 4788,\n", - " 4726, 100, 81, 80, 4726, 4725, 4666, 4643, 58,\n", - " 4787, 19333, 4784, 4720, 19377, 4718, 19375, 4718, 19377,\n", - " 1906, 19334, 19331, 5, 4725, 81, 2, 73, 4708,\n", - " 4710, 72, 75, 4666, 4667, 4643, 56, 4665, 4664,\n", - " 1915, 4705, 1916, 1908, 19340, 4644, 19333, 19331, 19337,\n", - " 1902, 19331, 19333, 3, 4714, 75, 99, 4788, 4732,\n", - " 1913, 4724, 77, 4782, 4783, 4786, 73, 4712, 4708,\n", - " 1907, 4785, 1909, 4705, 4710, 4704, 4707, 4794, 19375,\n", - " 4665, 4644, 4645, 4646, 19331, 19334, 6, 82, 4727,\n", - " 4712, 4705, 1915, 4724, 4720, 4721, 4704, 4707, 19376,\n", - " 19333, 19337, 4784, 4710, 75, 4716, 7, 99, 4732,\n", - " 5, 80, 4725, 4782, 4786, 82, 82, 4786, 4727,\n", - " 1, 59, 4666, 79, 4724, 4723, 4704, 4706, 4707,\n", - " 4794, 4795, 19375, 1912, 19374, 1914, 1908, 1910, 19340,\n", - " 4644, 4646, 4645, 103, 4794, 4796, 19340, 19338, 19337,\n", - " 1903, 4788, 99, 77, 79, 4, 3, 74, 4714,\n", - " 84, 4732, 4788, 19337, 19338, 19339, 56, 4659, 4665,\n", - " 4704, 4716, 4706, 4787, 4784, 4783, 1902, 19333, 1904,\n", - " 1902, 1906, 19331, 1908, 4644, 1911, 1910, 19338, 19340,\n", - " 4782, 82, 83, 79, 4723, 78, 4786, 4785, 1907,\n", - " 57, 4667, 59, 4712, 1915, 70, 1914, 19377, 4720,\n", - " 4665, 4645, 4664, 102, 4795, 4794, 7, 4732, 84,\n", - " 56, 4664, 57, 1, 4666, 58, 4783, 4785, 4786,\n", - " 104, 4706, 4716, 4718, 4719, 4721, 4704, 4710, 4716,\n", - " 1911, 4665, 55, 19339, 4783, 4784, 1909, 19338, 1910,\n", - " 1916, 4705, 19376, 4784, 19337, 19339, 83, 84, 4788,\n", - " 4710, 73, 72, 19375, 19374, 19376, 101, 4796, 4706,\n", - " 75, 4714, 4716, 55, 4665, 4659, 4706, 4796, 4707,\n", - " 4724, 4721, 4723, 58, 4643, 1905, 4718, 19375, 4795,\n", - " 4720, 4718, 4721, 4643, 4645, 19334, 19375, 19377, 19374,\n", - " 0, 4659, 56, 1904, 4787, 1903, 57, 4664, 4667,\n", - " 4718, 4795, 4719, 1905, 4643, 1906, 4667, 4645, 4643,\n", - " 4704, 19376, 4705, 19376, 19374, 1912, 19340, 19337, 4646,\n", - " 103, 4796, 101, 2, 4708, 70, 4707, 4796, 4794,\n", - " 19339, 4785, 4783, 1904, 19333, 4787, 1914, 4720, 1913,\n", - " 78, 4726, 80, 104, 4716, 74, 6, 4727, 98,\n", - " 4786, 1907, 98, 100, 4719, 4795, 81, 4725, 4726,\n", - " 74, 4716, 4714, 70, 4708, 4712, 59, 4667, 4666,\n", - " 4719, 4723, 4721, 1909, 4785, 19339, 1909, 19339, 19338,\n", - " 100, 4795, 102, 4726, 4719, 100, 83, 4788, 4782,\n", - " 98, 4727, 4786, 0, 55, 4659, 78, 4723, 4726,\n", - " 4787, 4783, 4782, 1911, 4644, 4665, 4705, 4712, 4710,\n", - " 19376, 1912, 1916, 4644, 19340, 4646, 1914, 19374, 19377,\n", - " 102, 4794, 103]), 'YNORMAL_FACE': array([ 4, 86, 4745, 19370, 19371, 19373, 0, 67, 4684,\n", - " 19353, 19361, 19358, 19341, 19356, 19347, 19342, 19346, 19356,\n", - " 19346, 19351, 19347, 4734, 4745, 86, 4769, 4778, 19358,\n", - " 1908, 19352, 19344, 4696, 19369, 19379, 66, 68, 4693,\n", - " 19365, 19378, 19366, 53, 4649, 69, 19362, 19370, 19363,\n", - " 19341, 19348, 19364, 19344, 19349, 19346, 4771, 4769, 4772,\n", - " 4778, 19359, 19358, 95, 96, 4771, 19369, 19370, 19379,\n", - " 4771, 4772, 95, 19361, 19363, 19366, 4780, 4778, 4771,\n", - " 1919, 4700, 4699, 4649, 4696, 4697, 4699, 4698, 4649,\n", - " 4684, 4686, 4692, 4733, 19378, 19365, 4696, 19379, 19350,\n", - " 65, 69, 4697, 4780, 4771, 96, 19370, 19372, 19371,\n", - " 19365, 19366, 19368, 19343, 19345, 19355, 4693, 4692, 4686,\n", - " 4691, 65, 4697, 67, 4686, 4684, 4684, 19352, 1911,\n", - " 4649, 4698, 4696, 19360, 4777, 4781, 1916, 19371, 19372,\n", - " 98, 4781, 97, 19341, 19364, 19361, 19353, 19359, 19354,\n", - " 19354, 19357, 19355, 19342, 19356, 19354, 1919, 4699, 71,\n", - " 4691, 4694, 4695, 19344, 19345, 1910, 4717, 4733, 19365,\n", - " 19360, 19357, 4777, 1907, 1909, 19343, 19345, 19343, 1909,\n", - " 94, 4777, 4780, 19362, 19363, 19364, 77, 4745, 4717,\n", - " 70, 1915, 4700, 4695, 4694, 4693, 4717, 19368, 1913,\n", - " 4733, 4769, 19378, 94, 4780, 96, 97, 4781, 4768,\n", - " 19353, 19358, 19359, 19354, 19359, 19357, 19342, 19355, 19345,\n", - " 4777, 4779, 4780, 4699, 4649, 53, 4691, 4697, 4696,\n", - " 4696, 4698, 19369, 19351, 19349, 4692, 4691, 19350, 4694,\n", - " 98, 97, 6, 4772, 4734, 93, 19342, 19354, 19355,\n", - " 70, 4700, 1919, 4772, 4769, 4734, 4692, 4693, 4694,\n", - " 1907, 19343, 19360, 19345, 1909, 1910, 4777, 19357, 19359,\n", - " 19344, 1910, 1908, 93, 95, 4772, 4694, 19350, 19351,\n", - " 19363, 19373, 19366, 19361, 19364, 19363, 19348, 19362, 19364,\n", - " 0, 4684, 55, 19343, 19357, 19360, 19348, 19350, 19362,\n", - " 19346, 19349, 19351, 19347, 19351, 19348, 19342, 19344, 19346,\n", - " 4768, 4781, 4777, 4684, 1911, 55, 1912, 19371, 1916,\n", - " 1908, 1911, 19352, 19372, 19369, 4698, 4692, 19349, 19352,\n", - " 19368, 19367, 1914, 86, 93, 4734, 2, 1919, 71,\n", - " 19362, 19379, 19370, 19369, 19372, 19370, 19358, 19361, 19378,\n", - " 19341, 19361, 19353, 19343, 19355, 19357, 4771, 4778, 4769,\n", - " 4733, 4717, 4745, 4717, 1913, 77, 4698, 4699, 4700,\n", - " 4778, 4779, 19359, 1907, 19360, 4781, 19368, 1914, 1913,\n", - " 19372, 4698, 4700, 4692, 19352, 4684, 98, 1907, 4781,\n", - " 19363, 19370, 19373, 19366, 19373, 19367, 19341, 19353, 19356,\n", - " 19341, 19347, 19348, 4780, 4779, 4778, 4693, 4686, 66,\n", - " 1915, 1916, 19372, 1915, 19372, 4700, 1914, 19367, 19373,\n", - " 4717, 19365, 19368, 94, 97, 4768, 19351, 4692, 4694,\n", - " 66, 4686, 67, 19366, 19367, 19368, 19344, 19352, 19349,\n", - " 4733, 4745, 4734, 19342, 19345, 19344, 4691, 4695, 65,\n", - " 4696, 19350, 4691, 2, 70, 1919, 65, 4695, 68,\n", - " 4649, 4697, 69, 4733, 4734, 4769, 4777, 19359, 4779,\n", - " 1914, 19373, 1912, 1912, 19373, 19371, 53, 71, 4699,\n", - " 4, 4745, 77, 94, 4768, 4777, 19361, 19366, 19378,\n", - " 19350, 19379, 19362, 19353, 19354, 19356, 19346, 19347, 19356,\n", - " 19348, 19351, 19350, 4695, 4693, 68, 4769, 19358, 19378]), 'SYMMETRY_FACE': array([ 3622, 9206, 2870, ..., 4678, 19336, 19323], shape=(5502,))}, marker_types={'LOWER_SIDE': array([5, 5, 5, ..., 5, 5, 5], shape=(17284,), dtype=int32), 'TIP': array([5, 5, 5, ..., 5, 5, 5], shape=(2597,), dtype=int32), 'UPPER_SIDE': array([5, 5, 5, ..., 5, 5, 5], shape=(16573,), dtype=int32), 'XNORMAL_FACES': array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5],\n", - " dtype=int32), 'ZNORMAL_FACES': array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], dtype=int32), 'YNORMAL_FACE': array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n", - " 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5],\n", - " dtype=int32), 'SYMMETRY_FACE': array([5, 5, 5, ..., 5, 5, 5], shape=(1834,), dtype=int32)}, izone=1, ndime=3, nelem=582752, npoin=108396, nmark=7)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Mesh dimensions: 3D\n", + "Number of vertices: 108396\n", + "Number of elements: 582752\n", + "Number of markers: 7\n", + "Marker names: ['LOWER_SIDE', 'TIP', 'UPPER_SIDE', 'XNORMAL_FACES', 'ZNORMAL_FACES', 'YNORMAL_FACE', 'SYMMETRY_FACE']\n" + ] } ], "source": [ - "from su2fmt import parse_mesh, export_mesh\n", + "# Print mesh information\n", + "print(f\"Mesh dimensions: {mesh.dim}D\")\n", + "print(f\"Number of vertices: {mesh.vertex_count}\")\n", + "print(f\"Number of elements: {mesh.polygon_count}\")\n", + "print(f\"Number of markers: {len(mesh.markers)}\")\n", + "print(f\"Marker names: {list(mesh.markers.keys())}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8c725515", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exported mesh to examples/mesh_ONERAM6_output.su2\n" + ] + } + ], + "source": [ + "# The mesh is now a meshly.Mesh object with full meshly capabilities\n", + "# You can use all meshly features like optimization, simplification, encoding, etc.\n", "\n", - "# parses mesh file\n", - "mesh = parse_mesh(\"examples/mesh_ONERAM6_inv_ffd.su2\")\n", - "mesh" + "# Export the mesh back to SU2 format\n", + "export_mesh(mesh, \"examples/mesh_ONERAM6_output.su2\")\n", + "print(\"Exported mesh to examples/mesh_ONERAM6_output.su2\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9905b7c", + "metadata": {}, + "source": [ + "## Using meshly features\n", + "\n", + "Now that the mesh is a `meshly.Mesh` object, you can use all meshly features such as:\n", + "- Mesh optimization (vertex cache, overdraw, vertex fetch)\n", + "- Mesh simplification\n", + "- Encoding/decoding for compression\n", + "- Saving/loading to ZIP files\n", + "- Marker management and manipulation\n", + "- And more!" ] }, { "cell_type": "code", - "execution_count": null, - "id": "8dcad536", + "execution_count": 5, + "id": "9e0ee32c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoded mesh size: vertices=955818 bytes\n", + "Saved compressed mesh to examples/mesh_ONERAM6.zip\n" + ] + } + ], "source": [ - "import meshio\n", - "mesh = meshio.read(\"examples/mesh_ONERAM6_inv_ffd.su2\")" + "# Example: Use meshly utilities\n", + "from meshly import MeshUtils\n", + "\n", + "# Encode the mesh for compression\n", + "encoded = MeshUtils.encode(mesh)\n", + "print(f\"Encoded mesh size: vertices={len(encoded.vertices)} bytes\")\n", + "\n", + "# You can also save to a zip file\n", + "MeshUtils.save_to_zip(mesh, \"examples/mesh_ONERAM6.zip\")\n", + "print(\"Saved compressed mesh to examples/mesh_ONERAM6.zip\")" ] } ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -262,7 +159,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.19" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index 7d29c75..f0d6804 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "su2fmt" -version = "3.0.0" +version = "4.0.0" description = "the open source SU2 mesh format parser and exporter" authors = [ {name = "Open Orion"} ] dependencies = [ "numpy", - "meshly" + "meshly>=1.3.0a0" ] requires-python = ">=3.7" diff --git a/su2fmt/__init__.py b/su2fmt/__init__.py index 047839a..06653ba 100644 --- a/su2fmt/__init__.py +++ b/su2fmt/__init__.py @@ -1,3 +1,3 @@ -from su2fmt.mesh import SU2Mesh from su2fmt.parser import parse_mesh, combine_meshes from su2fmt.exporter import export_mesh +from su2fmt.types import SU2ElementType diff --git a/su2fmt/exporter.py b/su2fmt/exporter.py index 137f348..d9dbfdd 100644 --- a/su2fmt/exporter.py +++ b/su2fmt/exporter.py @@ -1,25 +1,26 @@ import numpy.typing as npt from typing import List import numpy as np -from su2fmt.mesh import ElementType, SU2Mesh +from meshly import Mesh +from su2fmt.types import SU2ElementType, VTK_TO_SU2_MAPPING ELEMENT_INDENT = " " * 2 def get_element_vertex_count(element_type: int) -> int: """Get the number of vertices for a given element type.""" - if element_type == ElementType.LINE.value: + if element_type == SU2ElementType.LINE.value: return 2 - elif element_type == ElementType.TRIANGLE.value: + elif element_type == SU2ElementType.TRIANGLE.value: return 3 - elif element_type == ElementType.QUADRILATERAL.value: + elif element_type == SU2ElementType.QUADRILATERAL.value: return 4 - elif element_type == ElementType.TETRAHEDRON.value: + elif element_type == SU2ElementType.TETRAHEDRON.value: return 4 - elif element_type == ElementType.HEXAHEDRON.value: + elif element_type == SU2ElementType.HEXAHEDRON.value: return 8 - elif element_type == ElementType.PRISM.value: + elif element_type == SU2ElementType.PRISM.value: return 6 - elif element_type == ElementType.PYRAMID.value: + elif element_type == SU2ElementType.PYRAMID.value: return 5 else: raise ValueError(f"Unknown element type: {element_type}") @@ -32,67 +33,81 @@ def get_unused_point_indexes(points: npt.NDArray[np.float64], indices: npt.NDArr used_point_indexes.add(point_index) return point_indexes.difference(used_point_indexes) -def export_mesh(mesh: SU2Mesh, file_path: str): - """Export a single SU2Mesh to a file.""" +def export_mesh(mesh: Mesh, file_path: str): + """Export a meshly.Mesh to SU2 format file.""" with open(file_path, 'w+') as file: spaces = " " * 8 - # Write zone header if needed - file.write(f"NDIME= {mesh.ndime}\n") + # Get dimension from mesh + ndime = mesh.dim if hasattr(mesh, 'dim') else 3 + + # Write zone header + file.write(f"NDIME= {ndime}\n") # Write points - unused_point_indexes = get_unused_point_indexes(mesh.vertices, mesh.indices) if mesh.indices is not None else set() - npoin = mesh.npoin if mesh.npoin is not None else len(mesh.vertices) + unused_point_indexes = get_unused_point_indexes(mesh.vertices, np.asarray(mesh.indices)) if mesh.indices is not None and len(mesh.indices) > 0 else set() + npoin = len(mesh.vertices) file.write(f"NPOIN= {npoin - len(unused_point_indexes)}\n") for index, point in enumerate(mesh.vertices): if index in unused_point_indexes: continue - point_row = [*(point[:-1] if mesh.ndime == 2 else point), index] + point_row = [*(point[:-1] if ndime == 2 else point), index] file.write(f"{spaces}{spaces.join(map(str, point_row))}\n") # Write elements - file.write(f"NELEM= {mesh.nelem}\n") - if mesh.indices is not None: - # Reconstruct elements from flattened indices and element types + nelem = mesh.polygon_count if mesh.indices is not None and len(mesh.indices) > 0 else 0 + file.write(f"NELEM= {nelem}\n") + + if mesh.indices is not None and len(mesh.indices) > 0 and mesh.cell_types is not None: + # Reconstruct elements from flattened indices using index_sizes idx = 0 element_index = 0 - for elem_type in mesh.element_types: - num_vertices = get_element_vertex_count(elem_type) + + for i, vtk_type in enumerate(mesh.cell_types): + if mesh.index_sizes is not None: + num_vertices = mesh.index_sizes[i] + else: + num_vertices = get_element_vertex_count(int(vtk_type)) + + # Convert VTK cell type to SU2 element type + su2_type = VTK_TO_SU2_MAPPING.get(int(vtk_type), int(vtk_type)) + element_vertices = mesh.indices[idx:idx + num_vertices] - element_row = [elem_type, *element_vertices, element_index] + element_row = [su2_type, *element_vertices, element_index] file.write(f"{ELEMENT_INDENT}{spaces.join(map(str, element_row))}\n") idx += num_vertices element_index += 1 # Write markers - file.write(f"NMARK= {mesh.nmark}\n") - for marker_tag, marker_indices in mesh.markers.items(): + nmark = len(mesh.markers) if hasattr(mesh, 'markers') and mesh.markers else 0 + file.write(f"NMARK= {nmark}\n") + + # Get reconstructed markers (list-of-lists format) + reconstructed_markers = mesh.get_reconstructed_markers() if hasattr(mesh, 'get_reconstructed_markers') else mesh.markers + + for marker_tag, marker_elements in reconstructed_markers.items(): file.write(f"MARKER_TAG= {marker_tag}\n") - # Count number of marker elements - marker_types_for_tag = mesh.marker_types.get(marker_tag, []) - if len(marker_types_for_tag) > 0: - num_marker_elems = len(marker_types_for_tag) - else: - # Assume all are lines if no types specified - num_marker_elems = len(marker_indices) // 2 - + # Number of marker elements + num_marker_elems = len(marker_elements) file.write(f"MARKER_ELEMS= {num_marker_elems}\n") + # Get marker cell types if available + marker_cell_types = mesh.marker_cell_types.get(marker_tag) if hasattr(mesh, 'marker_cell_types') else None + # Write marker elements - idx = 0 - for i in range(num_marker_elems): - if len(marker_types_for_tag) > 0: - marker_type = marker_types_for_tag[i] - num_vertices = get_element_vertex_count(marker_type) - else: - marker_type = ElementType.LINE.value - num_vertices = get_element_vertex_count(marker_type) + for i, element_vertices in enumerate(marker_elements): + if marker_cell_types is None or i >= len(marker_cell_types): + raise ValueError( + f"Missing cell type information for marker '{marker_tag}' element {i}. " + f"Marker has {len(marker_elements)} elements but cell_types has " + f"{len(marker_cell_types) if marker_cell_types else 0} entries." + ) - element_vertices = marker_indices[idx:idx + num_vertices] - element_row = [marker_type, *element_vertices] - file.write(f"{ELEMENT_INDENT}{spaces.join(map(str, element_row))}\n") + # Convert VTK cell type to SU2 element type + su2_marker_type = VTK_TO_SU2_MAPPING.get(int(marker_cell_types[i]), int(marker_cell_types[i])) - idx += num_vertices + element_row = [su2_marker_type, *element_vertices] + file.write(f"{ELEMENT_INDENT}{spaces.join(map(str, element_row))}\n") diff --git a/su2fmt/mesh.py b/su2fmt/mesh.py deleted file mode 100644 index 791113e..0000000 --- a/su2fmt/mesh.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Optional, Dict -import numpy as np -from enum import Enum -from meshly import Mesh -from pydantic import Field - -class ElementType(Enum): - LINE = 3 - TRIANGLE = 5 - QUADRILATERAL = 9 - TETRAHEDRON = 10 - HEXAHEDRON = 12 - PRISM = 13 - PYRAMID = 14 - -class SU2Mesh(Mesh): - """ - A mesh class for SU2 format files, inheriting from meshly.Mesh. - - This extends the base Mesh class with SU2-specific attributes like - element types, markers, and zone information. - """ - # SU2-specific attributes - element_types: np.ndarray = Field(..., description="Element types for each element") - markers: Dict[str, np.ndarray] = Field(default_factory=dict, description="Boundary markers") - marker_types: Dict[str, np.ndarray] = Field(default_factory=dict, description="Marker element types") - - # Zone information - izone: int = Field(1, description="Zone index") - ndime: int = Field(3, description="Number of dimensions from file") - - @property - def nelem(self) -> int: - """Number of elements (derived from element_types).""" - return len(self.element_types) if len(self.element_types) > 0 else 0 - - @property - def npoin(self) -> int: - """Number of points (derived from vertices).""" - return len(self.vertices) if len(self.vertices) > 0 else 0 - - @property - def nmark(self) -> int: - """Number of markers (derived from markers dict).""" - return len(self.markers) diff --git a/su2fmt/parser.py b/su2fmt/parser.py index 4048bc7..ea9e6b5 100644 --- a/su2fmt/parser.py +++ b/su2fmt/parser.py @@ -1,19 +1,25 @@ -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import numpy as np import numpy.typing as npt -from su2fmt.mesh import SU2Mesh +from meshly import Mesh +from su2fmt.types import SU2ElementType, SU2_TO_VTK_MAPPING -def parse_mesh(file_path: str) -> SU2Mesh: +def parse_mesh(file_path: str) -> Union[Mesh, List[Mesh]]: with open(file_path, 'r') as file: + meshes = [] + nzone: int = 1 + + izone_len = 0 + izone: int = 0 ndime: Optional[int] = None nelem: Optional[int] = None npoin: Optional[int] = None nmark: int = 0 points: List[npt.NDArray[np.float32]] = [] elements: List[npt.NDArray[np.int64]] = [] - element_types: List[int] = [] + element_types: List[SU2ElementType] = [] markers: Dict[str, List[npt.NDArray[np.int64]]] = {} - marker_types: Dict[str, List[int]] = {} + marker_types: Dict[str, List[SU2ElementType]] = {} element_index: Optional[int] = None point_index: Optional[int] = None @@ -25,28 +31,116 @@ def parse_mesh(file_path: str) -> SU2Mesh: for line in file: line = line.strip() - if line.startswith('NMARK='): + if line.startswith('NZONE='): + nzone = int(line.split('=')[1].strip().split()[0]) + elif line.startswith('NMARK='): nmark = int(line.split('=')[1].strip().split()[0]) + elif line.startswith('IZONE='): + izone_len += 1 + izone = int(line.split('=')[1].strip().split()[0]) + elif line.startswith('NDIME='): + if izone_len > 1: + assert ndime is not None, "NDIME must be defined for zone" + assert nelem is not None, "NELEM must be defined for zone" + assert npoin is not None, "NPOIN must be defined for zone" + + # Convert to meshly format + indices = [] + index_sizes = [] + cell_types = [] + + for elem, elem_type in zip(elements, element_types): + indices.extend(elem) + index_sizes.append(len(elem)) + cell_types.append(SU2_TO_VTK_MAPPING.get(elem_type.value, elem_type.value)) + + # Convert markers to list-of-lists format for meshly + mesh_markers = {} + + for marker_tag_key, marker_elements in markers.items(): + # Keep markers as list of lists (meshly format) + mesh_markers[marker_tag_key] = [elem.tolist() for elem in marker_elements] + + mesh = Mesh( + vertices=np.array(points), + indices=np.array(indices, dtype=np.uint32) if indices else np.array([], dtype=np.uint32), + index_sizes=np.array(index_sizes, dtype=np.uint32) if index_sizes else None, + cell_types=np.array(cell_types, dtype=np.uint32) if cell_types else None, + markers=mesh_markers if mesh_markers else {}, + dim=ndime + ) + meshes.append(mesh) + ndime = int(line.split('=')[1].strip().split()[0]) + nelem = None + npoin = None + elements = [] + element_types = [] + points = [] + markers = {} + marker_types = {} + element_index = None + point_index = None + marker_tag = None + nmark_elems = None + marker_index = None + elif line.startswith('NELEM='): nelem = int(line.split('=')[1].strip().split()[0]) element_index = 0 + elif line.startswith('NPOIN='): npoin = int(line.split('=')[1].strip().split()[0]) point_index = 0 + elif line.startswith('MARKER_TAG='): marker_tag = line.split('=')[1].strip() + elif line.startswith('MARKER_ELEMS='): nmark_elems = int(line.split('=')[1].strip().split()[0]) marker_index = 0 elif element_index is not None and nelem: assert nelem is not None, "NELEM must be defined for zone before reading elements" - element = np.array(line.split()[1:-1], dtype=np.int64) - element_type = int(line.split()[0]) + parts = line.split() + elem_type = SU2ElementType(int(parts[0])) + + # Determine expected number of vertices for this element type + type_vertex_counts = { + SU2ElementType.LINE: 2, + SU2ElementType.TRIANGLE: 3, + SU2ElementType.QUADRILATERAL: 4, + SU2ElementType.TETRAHEDRON: 4, + SU2ElementType.PYRAMID: 5, + SU2ElementType.PRISM: 6, + SU2ElementType.HEXAHEDRON: 8, + } + expected_vertices = type_vertex_counts.get(elem_type) + + if expected_vertices is None: + raise ValueError(f"Unknown element type: {elem_type} (value: {elem_type.value})") + + # Check if last element is an index or a vertex + # If we have exactly expected_vertices + 1 elements after type, last is index + # If we have exactly expected_vertices elements after type, no index + num_values = len(parts) - 1 # Excluding element type + + if num_values == expected_vertices + 1: + # Has element index at end + element = np.array(parts[1:-1], dtype=np.int64) + elif num_values == expected_vertices: + # No element index + element = np.array(parts[1:], dtype=np.int64) + else: + raise ValueError( + f"Element type {elem_type.name} expects {expected_vertices} vertices, " + f"but found {num_values} values (with or without element index). " + f"Line: {line}" + ) + elements.append(element) - element_types.append(element_type) + element_types.append(elem_type) if element_index == nelem-1: element_index = None else: @@ -58,75 +152,69 @@ def parse_mesh(file_path: str) -> SU2Mesh: point = np.array(line.split()[:2]+[0], dtype=np.float32) else: point = np.array(line.split()[:3], dtype=np.float32) - assert len(point) == 3, "Point must have 3 coordinates" points.append(point) if point_index == npoin-1: point_index = None else: point_index += 1 + elif marker_index is not None: assert nmark_elems is not None, "MARKER_ELEMS must be defined for zone before reading markers" - assert marker_tag is not None, "MARKER_TAG must be defined for marker before reading marker elements" + assert marker_tag is not None, "MARKER_TAG must be defined for marker before reading marker elements" if marker_tag not in markers: markers[marker_tag] = [] marker_types[marker_tag] = [] - marker_components = line.split() markers[marker_tag].append( np.array(marker_components[1:], dtype=np.int64) ) - marker_types[marker_tag].append(int(marker_components[0])) + marker_types[marker_tag].append(SU2ElementType(int(marker_components[0]))) if marker_index == nmark_elems - 1: marker_index = None else: marker_index += 1 - # Convert to the format expected by SU2Mesh - assert ndime is not None, "NDIME must be defined" - assert nelem is not None, "NELEM must be defined" - assert npoin is not None, "NPOIN must be defined" + assert ndime is not None, "NDIME must be defined for zone" + assert nelem is not None, "NELEM must be defined for zone" + assert npoin is not None, "NPOIN must be defined for zone" - # Flatten indices for meshly format + # Convert final zone to meshly format indices = [] - for elem in elements: + index_sizes = [] + cell_types = [] + + for elem, elem_type in zip(elements, element_types): indices.extend(elem) + index_sizes.append(len(elem)) + cell_types.append(SU2_TO_VTK_MAPPING.get(elem_type.value, elem_type.value)) - # Convert markers to numpy arrays + # Convert markers to list-of-lists format for meshly mesh_markers = {} - mesh_marker_types = {} - for marker_tag, marker_elements in markers.items(): - marker_indices = [] - marker_type_values = [] - - if marker_tag in marker_types: - for elem, elem_type in zip(marker_elements, marker_types[marker_tag]): - marker_indices.extend(elem) - marker_type_values.append(elem_type) - - if marker_indices: - mesh_markers[marker_tag] = np.array(marker_indices, dtype=np.int64) - mesh_marker_types[marker_tag] = np.array(marker_type_values, dtype=np.int32) - - return SU2Mesh( + for marker_tag_key, marker_elements in markers.items(): + # Keep markers as list of lists (meshly format) + mesh_markers[marker_tag_key] = [elem.tolist() for elem in marker_elements] + + mesh = Mesh( vertices=np.array(points), - indices=np.array(indices, dtype=np.uint32) if indices else None, - element_types=np.array(element_types, dtype=np.int32), - markers=mesh_markers, - marker_types=mesh_marker_types, - izone=1, - ndime=ndime + indices=np.array(indices, dtype=np.uint32) if indices else np.array([], dtype=np.uint32), + index_sizes=np.array(index_sizes, dtype=np.uint32) if index_sizes else None, + cell_types=np.array(cell_types, dtype=np.uint32) if cell_types else None, + markers=mesh_markers if mesh_markers else {}, + dim=ndime ) + meshes.append(mesh) + + # Return single mesh if only one zone, otherwise return list + if len(meshes) == 1 and nzone == 1: + return meshes[0] + else: + return meshes -def combine_meshes(meshes: List[SU2Mesh]) -> List[SU2Mesh]: - """Return list of meshes with updated zone indices.""" - result = [] - for i, mesh in enumerate(meshes, 1): - # Create a copy with updated izone - mesh_copy = mesh.model_copy() - mesh_copy.izone = i - result.append(mesh_copy) - return result \ No newline at end of file +def combine_meshes(meshes: List[Mesh]) -> List[Mesh]: + """Combine meshes - for multi-zone support.""" + # Simply return the list as meshly.Mesh.combine() can be used if needed + return meshes \ No newline at end of file diff --git a/su2fmt/types.py b/su2fmt/types.py new file mode 100644 index 0000000..32eac93 --- /dev/null +++ b/su2fmt/types.py @@ -0,0 +1,28 @@ +"""Shared types and mappings for SU2 mesh format.""" +from enum import Enum +from meshly import VTKCellType + + +class SU2ElementType(Enum): + LINE = 3 + TRIANGLE = 5 + QUADRILATERAL = 9 + TETRAHEDRON = 10 + HEXAHEDRON = 12 + PRISM = 13 + PYRAMID = 14 + + +# SU2 to VTK cell type mapping (single source of truth) +SU2_TO_VTK_MAPPING = { + SU2ElementType.LINE.value: VTKCellType.VTK_LINE, + SU2ElementType.TRIANGLE.value: VTKCellType.VTK_TRIANGLE, + SU2ElementType.QUADRILATERAL.value: VTKCellType.VTK_QUAD, + SU2ElementType.TETRAHEDRON.value: VTKCellType.VTK_TETRA, + SU2ElementType.HEXAHEDRON.value: VTKCellType.VTK_HEXAHEDRON, + SU2ElementType.PRISM.value: VTKCellType.VTK_WEDGE, + SU2ElementType.PYRAMID.value: VTKCellType.VTK_PYRAMID, +} + +# Reverse mapping derived from the forward mapping +VTK_TO_SU2_MAPPING = {v: k for k, v in SU2_TO_VTK_MAPPING.items()} diff --git a/tests/su2mesh_test.py b/tests/su2mesh_test.py new file mode 100644 index 0000000..e2e383c --- /dev/null +++ b/tests/su2mesh_test.py @@ -0,0 +1,507 @@ +import unittest +import numpy as np +import tempfile +import os +from typing import List +from su2fmt import parse_mesh, export_mesh, SU2ElementType +from meshly import Mesh + + +class TestSU2Mesh(unittest.TestCase): + """Test cases for SU2Mesh functionality.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + """Clean up test fixtures.""" + import shutil + shutil.rmtree(self.temp_dir) + + def test_basic_mesh_creation(self): + """Test basic Mesh creation and properties.""" + vertices = np.array([ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0] + ], dtype=np.float32) + + indices = np.array([0, 1, 2], dtype=np.uint32) + cell_types = np.array([SU2ElementType.TRIANGLE.value], dtype=np.uint32) + index_sizes = np.array([3], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + dim=3 + ) + + # Test computed properties + self.assertEqual(mesh.dim, 3) + self.assertEqual(mesh.polygon_count, 1) + self.assertEqual(mesh.vertex_count, 3) + self.assertEqual(mesh.index_count, 3) + + def test_2d_mesh_creation(self): + """Test 2D mesh creation.""" + vertices = np.array([ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0] + ], dtype=np.float32) + + indices = np.array([0, 1, 2], dtype=np.uint32) + cell_types = np.array([SU2ElementType.TRIANGLE.value], dtype=np.uint32) + index_sizes = np.array([3], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + dim=2 + ) + + self.assertEqual(mesh.dim, 2) + self.assertEqual(mesh.vertex_count, 3) + + def test_mesh_with_markers(self): + """Test mesh creation with boundary markers.""" + vertices = np.array([ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 1.0, 0.0] + ], dtype=np.float32) + + indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32) + cell_types = np.array([ + SU2ElementType.TRIANGLE.value, + SU2ElementType.TRIANGLE.value + ], dtype=np.uint32) + index_sizes = np.array([3, 3], dtype=np.uint32) + + markers = { + "wall": [[0, 1], [1, 3]], + "inlet": [[0, 2]] + } + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + markers=markers, + dim=3 + ) + + self.assertEqual(len(mesh.markers), 2) + self.assertIn("wall", mesh.markers) + self.assertIn("inlet", mesh.markers) + reconstructed = mesh.get_reconstructed_markers() + self.assertEqual(len(reconstructed["wall"]), 2) + self.assertEqual(len(reconstructed["inlet"]), 1) + + def test_different_element_types(self): + """Test mesh with different element types.""" + vertices = np.array([ + [0.0, 0.0, 0.0], # 0 + [1.0, 0.0, 0.0], # 1 + [0.0, 1.0, 0.0], # 2 + [1.0, 1.0, 0.0], # 3 + [0.5, 0.5, 1.0] # 4 + ], dtype=np.float32) + + # Triangle + Tetrahedron + indices = np.array([0, 1, 2, 0, 1, 2, 4], dtype=np.uint32) + cell_types = np.array([ + SU2ElementType.TRIANGLE.value, + SU2ElementType.TETRAHEDRON.value + ], dtype=np.uint32) + index_sizes = np.array([3, 4], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + dim=3 + ) + + self.assertEqual(mesh.polygon_count, 2) + self.assertEqual(mesh.vertex_count, 5) + + def test_simple_mesh_parsing(self): + """Test parsing a simple mesh file.""" + mesh_content = """NDIME= 2 +NPOIN= 4 + 0.0000000000 0.0000000000 0 + 1.0000000000 0.0000000000 1 + 1.0000000000 1.0000000000 2 + 0.0000000000 1.0000000000 3 +NELEM= 2 +5 0 1 2 0 +5 0 2 3 1 +NMARK= 2 +MARKER_TAG= wall +MARKER_ELEMS= 2 +3 0 1 +3 2 3 +MARKER_TAG= inlet +MARKER_ELEMS= 1 +3 1 2 +""" + + # Write to temporary file + mesh_file = os.path.join(self.temp_dir, "test_mesh.su2") + with open(mesh_file, 'w') as f: + f.write(mesh_content) + + # Parse the mesh + mesh = parse_mesh(mesh_file) + assert isinstance(mesh, Mesh), "Parsed mesh should be an instance of Mesh" + + # Verify parsing results + self.assertEqual(mesh.dim, 2) + self.assertEqual(mesh.polygon_count, 2) + self.assertEqual(mesh.vertex_count, 4) + self.assertEqual(len(mesh.markers), 2) + + # Check vertices + self.assertEqual(mesh.vertices.shape, (4, 3)) # Always 3D internally + + # Check elements + self.assertEqual(len(mesh.cell_types), 2) + self.assertTrue(all(et == SU2ElementType.TRIANGLE.value for et in mesh.cell_types)) + + # Check markers + self.assertIn("wall", mesh.markers) + self.assertIn("inlet", mesh.markers) + reconstructed = mesh.get_reconstructed_markers() + self.assertEqual(len(reconstructed["wall"]), 2) # 2 lines + self.assertEqual(len(reconstructed["inlet"]), 1) # 1 line + + def test_mesh_with_whitespace_issues(self): + """Test parsing mesh with whitespace issues (like the original problem).""" + mesh_content = """NDIME= 3 +NPOIN= 4 4 + 0.0000000000 0.0000000000 0.0000000000 0 + 1.0000000000 0.0000000000 0.0000000000 1 + 0.0000000000 1.0000000000 0.0000000000 2 + 0.0000000000 0.0000000000 1.0000000000 3 +NELEM= 1 +10 0 1 2 3 0 +NMARK= 1 +MARKER_TAG= boundary_face +MARKER_ELEMS= 1 +5 0 1 2 +""" + + mesh_file = os.path.join(self.temp_dir, "whitespace_mesh.su2") + with open(mesh_file, 'w') as f: + f.write(mesh_content) + + # This should parse without errors despite the whitespace + mesh = parse_mesh(mesh_file) + assert isinstance(mesh, Mesh), "Parsed mesh should be an instance of Mesh" + self.assertEqual(mesh.dim, 3) + self.assertEqual(mesh.vertex_count, 4) + self.assertEqual(mesh.polygon_count, 1) + self.assertEqual(len(mesh.markers), 1) + self.assertIn("boundary_face", mesh.markers) + + def test_round_trip_parse_export(self): + """Test round-trip: parse -> export -> parse.""" + original_content = """NDIME= 3 +NPOIN= 4 + 0.0000000000 0.0000000000 0.0000000000 0 + 1.0000000000 0.0000000000 0.0000000000 1 + 0.0000000000 1.0000000000 0.0000000000 2 + 0.0000000000 0.0000000000 1.0000000000 3 +NELEM= 1 +10 0 1 2 3 0 +NMARK= 1 +MARKER_TAG= boundary +MARKER_ELEMS= 1 +5 0 1 2 +""" + + # Write original file + original_file = os.path.join(self.temp_dir, "original.su2") + with open(original_file, 'w') as f: + f.write(original_content) + + # Parse + mesh1 = parse_mesh(original_file) + assert isinstance(mesh1, Mesh), "Parsed mesh should be an instance of Mesh" + # Export + exported_file = os.path.join(self.temp_dir, "exported.su2") + export_mesh(mesh1, exported_file) + + # Parse exported file + mesh2 = parse_mesh(exported_file) + assert isinstance(mesh2, Mesh), "Parsed mesh should be an instance of Mesh" + # Compare meshes + self.assertEqual(mesh1.dim, mesh2.dim) + self.assertEqual(mesh1.polygon_count, mesh2.polygon_count) + self.assertEqual(mesh1.vertex_count, mesh2.vertex_count) + self.assertEqual(len(mesh1.markers), len(mesh2.markers)) + + # Compare vertices (allowing for small floating point differences) + np.testing.assert_allclose(mesh1.vertices, mesh2.vertices, rtol=1e-6) + + # Compare cell types + np.testing.assert_array_equal(mesh1.cell_types, mesh2.cell_types) + + # Compare markers + markers1 = mesh1.get_reconstructed_markers() + markers2 = mesh2.get_reconstructed_markers() + for marker_name in markers1: + self.assertIn(marker_name, markers2) + self.assertEqual(len(markers1[marker_name]), len(markers2[marker_name])) + + def test_meshly_features(self): + """Test that meshly features work correctly.""" + vertices = np.array([ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0] + ], dtype=np.float32) + + indices = np.array([0, 1, 2], dtype=np.uint32) + cell_types = np.array([SU2ElementType.TRIANGLE.value], dtype=np.uint32) + index_sizes = np.array([3], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + dim=3 + ) + + # Test basic meshly properties + self.assertEqual(mesh.vertex_count, 3) + self.assertEqual(mesh.index_count, 3) + self.assertEqual(mesh.polygon_count, 1) + + # Test model serialization + mesh_dict = mesh.model_dump() + self.assertIn("vertices", mesh_dict) + self.assertIn("indices", mesh_dict) + self.assertIn("cell_types", mesh_dict) + self.assertIn("dim", mesh_dict) + + def test_hexahedron_mesh(self): + """Test mesh with hexahedral elements.""" + # Create a simple cube with one hexahedron + vertices = np.array([ + [0.0, 0.0, 0.0], # 0 + [1.0, 0.0, 0.0], # 1 + [1.0, 1.0, 0.0], # 2 + [0.0, 1.0, 0.0], # 3 + [0.0, 0.0, 1.0], # 4 + [1.0, 0.0, 1.0], # 5 + [1.0, 1.0, 1.0], # 6 + [0.0, 1.0, 1.0] # 7 + ], dtype=np.float32) + + # Hexahedron indices (8 vertices) + indices = np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.uint32) + cell_types = np.array([SU2ElementType.HEXAHEDRON.value], dtype=np.uint32) + index_sizes = np.array([8], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + cell_types=cell_types, + index_sizes=index_sizes, + dim=3 + ) + + self.assertEqual(mesh.polygon_count, 1) + self.assertEqual(mesh.vertex_count, 8) + self.assertEqual(len(mesh.cell_types), 1) + self.assertEqual(mesh.cell_types[0], SU2ElementType.HEXAHEDRON.value) + + def test_empty_mesh(self): + """Test handling of empty mesh.""" + vertices = np.array([], dtype=np.float32).reshape(0, 3) + indices = np.array([], dtype=np.uint32) + + mesh = Mesh( + vertices=vertices, + indices=indices, + dim=3 + ) + + self.assertEqual(mesh.vertex_count, 0) + self.assertEqual(mesh.index_count, 0) + + def test_multizone_parsing(self): + """Test parsing of multizone mesh files.""" + multizone_content = """NZONE= 2 + +IZONE= 1 +NDIME= 2 +NELEM= 2 +5 0 1 2 0 +5 0 2 3 1 +NPOIN= 4 +0.0 0.0 0 +1.0 0.0 1 +1.0 1.0 2 +0.0 1.0 3 +NMARK= 1 +MARKER_TAG= wall1 +MARKER_ELEMS= 2 +3 0 1 +3 2 3 + +IZONE= 2 +NDIME= 2 +NELEM= 1 +5 0 1 2 0 +NPOIN= 3 +2.0 0.0 0 +3.0 0.0 1 +2.5 1.0 2 +NMARK= 1 +MARKER_TAG= wall2 +MARKER_ELEMS= 1 +3 0 1 +""" + + # Write to temporary file + mesh_file = os.path.join(self.temp_dir, "multizone_mesh.su2") + with open(mesh_file, 'w') as f: + f.write(multizone_content) + + # Parse the mesh + result = parse_mesh(mesh_file) + assert isinstance(result, List), "Parsed result should be a list of Mesh" + + # Should return a list of meshes + self.assertIsInstance(result, list, "Multizone mesh should return a list") + self.assertEqual(len(result), 2, "Should have 2 zones") + + # Check first zone + zone1 = result[0] + self.assertIsInstance(zone1, Mesh, "Zone 1 should be Mesh") + self.assertEqual(zone1.dim, 2, "Zone 1 dim should be 2") + self.assertEqual(zone1.polygon_count, 2, "Zone 1 should have 2 elements") + self.assertEqual(zone1.vertex_count, 4, "Zone 1 should have 4 points") + self.assertEqual(len(zone1.markers), 1, "Zone 1 should have 1 marker") + self.assertIn('wall1', zone1.markers, "Zone 1 should have wall1 marker") + + # Check second zone + zone2 = result[1] + self.assertIsInstance(zone2, Mesh, "Zone 2 should be Mesh") + self.assertEqual(zone2.dim, 2, "Zone 2 dim should be 2") + self.assertEqual(zone2.polygon_count, 1, "Zone 2 should have 1 element") + self.assertEqual(zone2.vertex_count, 3, "Zone 2 should have 3 points") + self.assertEqual(len(zone2.markers), 1, "Zone 2 should have 1 marker") + self.assertIn('wall2', zone2.markers, "Zone 2 should have wall2 marker") + + def test_single_zone_backward_compatibility(self): + """Test that single zone files still work (backward compatibility).""" + # This test uses the existing simple mesh content from test_simple_mesh_parsing + mesh_content = """NDIME= 2 +NPOIN= 4 + 0.0000000000 0.0000000000 0 + 1.0000000000 0.0000000000 1 + 1.0000000000 1.0000000000 2 + 0.0000000000 1.0000000000 3 +NELEM= 2 +5 0 1 2 0 +5 0 2 3 1 +NMARK= 2 +MARKER_TAG= wall +MARKER_ELEMS= 2 +3 0 1 +3 2 3 +MARKER_TAG= inlet +MARKER_ELEMS= 1 +3 1 2 +""" + + # Write to temporary file + mesh_file = os.path.join(self.temp_dir, "single_zone_mesh.su2") + with open(mesh_file, 'w') as f: + f.write(mesh_content) + + # Parse the mesh + result = parse_mesh(mesh_file) + assert isinstance(result, Mesh), "Parsed mesh should be an instance of Mesh" + # Should return a single mesh, not a list + self.assertIsInstance(result, Mesh, "Single zone mesh should return Mesh directly") + self.assertEqual(result.dim, 2, "Single zone dim should be 2") + self.assertEqual(result.polygon_count, 2, "Single zone should have 2 elements") + self.assertEqual(result.vertex_count, 4, "Single zone should have 4 points") + self.assertEqual(len(result.markers), 2, "Single zone should have 2 markers") + self.assertIn('wall', result.markers, "Single zone should have wall marker") + self.assertIn('inlet', result.markers, "Single zone should have inlet marker") + + def test_multizone_with_different_dimensions(self): + """Test multizone mesh with zones having different dimensions.""" + multizone_mixed_content = """NZONE= 2 + +IZONE= 1 +NDIME= 2 +NELEM= 1 +5 0 1 2 0 +NPOIN= 3 +0.0 0.0 0 +1.0 0.0 1 +0.5 1.0 2 +NMARK= 1 +MARKER_TAG= boundary2d +MARKER_ELEMS= 1 +3 0 1 + +IZONE= 2 +NDIME= 3 +NELEM= 1 +10 0 1 2 3 0 +NPOIN= 4 +0.0 0.0 0.0 0 +1.0 0.0 0.0 1 +0.5 1.0 0.0 2 +0.5 0.5 1.0 3 +NMARK= 1 +MARKER_TAG= boundary3d +MARKER_ELEMS= 1 +5 0 1 2 +""" + + # Write to temporary file + mesh_file = os.path.join(self.temp_dir, "mixed_dimension_mesh.su2") + with open(mesh_file, 'w') as f: + f.write(multizone_mixed_content) + + # Parse the mesh + result = parse_mesh(mesh_file) + assert isinstance(result, list), "Parsed result should be a list of Mesh" + + # Should return a list of meshes + self.assertIsInstance(result, list, "Multizone mesh should return a list") + self.assertEqual(len(result), 2, "Should have 2 zones") + + # Check 2D zone + zone1 = result[0] + self.assertEqual(zone1.dim, 2, "Zone 1 should be 2D") + self.assertEqual(zone1.polygon_count, 1, "Zone 1 should have 1 element") + self.assertIn('boundary2d', zone1.markers, "Zone 1 should have boundary2d marker") + + # Check 3D zone + zone2 = result[1] + self.assertEqual(zone2.dim, 3, "Zone 2 should be 3D") + self.assertEqual(zone2.polygon_count, 1, "Zone 2 should have 1 element") + self.assertIn('boundary3d', zone2.markers, "Zone 2 should have boundary3d marker") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_su2mesh.py b/tests/test_su2mesh.py deleted file mode 100644 index 32535b4..0000000 --- a/tests/test_su2mesh.py +++ /dev/null @@ -1,353 +0,0 @@ -import unittest -import numpy as np -import tempfile -import os -from su2fmt import SU2Mesh, parse_mesh, export_mesh -from su2fmt.mesh import ElementType - - -class TestSU2Mesh(unittest.TestCase): - """Test cases for SU2Mesh functionality.""" - - def setUp(self): - """Set up test fixtures.""" - self.temp_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up test fixtures.""" - import shutil - shutil.rmtree(self.temp_dir) - - def test_basic_mesh_creation(self): - """Test basic SU2Mesh creation and properties.""" - vertices = np.array([ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0] - ], dtype=np.float32) - - indices = np.array([0, 1, 2], dtype=np.uint32) - element_types = np.array([ElementType.TRIANGLE.value], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=3 - ) - - # Test computed properties - self.assertEqual(mesh.ndime, 3) - self.assertEqual(mesh.nelem, 1) - self.assertEqual(mesh.npoin, 3) - self.assertEqual(mesh.nmark, 0) - self.assertEqual(mesh.vertex_count, 3) - self.assertEqual(mesh.index_count, 3) - - def test_2d_mesh_creation(self): - """Test 2D mesh creation.""" - vertices = np.array([ - [0.0, 0.0], - [1.0, 0.0], - [0.0, 1.0] - ], dtype=np.float32) - - indices = np.array([0, 1, 2], dtype=np.uint32) - element_types = np.array([ElementType.TRIANGLE.value], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=2 - ) - - self.assertEqual(mesh.ndime, 2) - self.assertEqual(mesh.npoin, 3) - - def test_mesh_with_markers(self): - """Test mesh creation with boundary markers.""" - vertices = np.array([ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [1.0, 1.0, 0.0] - ], dtype=np.float32) - - indices = np.array([0, 1, 2, 1, 2, 3], dtype=np.uint32) - element_types = np.array([ - ElementType.TRIANGLE.value, - ElementType.TRIANGLE.value - ], dtype=np.int32) - - markers = { - "wall": np.array([0, 1, 1, 3], dtype=np.int64), - "inlet": np.array([0, 2], dtype=np.int64) - } - - marker_types = { - "wall": np.array([ElementType.LINE.value, ElementType.LINE.value], dtype=np.int32), - "inlet": np.array([ElementType.LINE.value], dtype=np.int32) - } - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - markers=markers, - marker_types=marker_types, - izone=1, - ndime=3 - ) - - self.assertEqual(mesh.nmark, 2) - self.assertIn("wall", mesh.markers) - self.assertIn("inlet", mesh.markers) - self.assertEqual(len(mesh.markers["wall"]), 4) - self.assertEqual(len(mesh.markers["inlet"]), 2) - - def test_different_element_types(self): - """Test mesh with different element types.""" - vertices = np.array([ - [0.0, 0.0, 0.0], # 0 - [1.0, 0.0, 0.0], # 1 - [0.0, 1.0, 0.0], # 2 - [1.0, 1.0, 0.0], # 3 - [0.5, 0.5, 1.0] # 4 - ], dtype=np.float32) - - # Triangle + Tetrahedron - indices = np.array([0, 1, 2, 0, 1, 2, 4], dtype=np.uint32) - element_types = np.array([ - ElementType.TRIANGLE.value, - ElementType.TETRAHEDRON.value - ], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=3 - ) - - self.assertEqual(mesh.nelem, 2) - self.assertEqual(mesh.npoin, 5) - - def test_simple_mesh_parsing(self): - """Test parsing a simple mesh file.""" - mesh_content = """NDIME= 2 -NPOIN= 4 - 0.0000000000 0.0000000000 0 - 1.0000000000 0.0000000000 1 - 1.0000000000 1.0000000000 2 - 0.0000000000 1.0000000000 3 -NELEM= 2 -5 0 1 2 0 -5 0 2 3 1 -NMARK= 2 -MARKER_TAG= wall -MARKER_ELEMS= 2 -3 0 1 -3 2 3 -MARKER_TAG= inlet -MARKER_ELEMS= 1 -3 1 2 -""" - - # Write to temporary file - mesh_file = os.path.join(self.temp_dir, "test_mesh.su2") - with open(mesh_file, 'w') as f: - f.write(mesh_content) - - # Parse the mesh - mesh = parse_mesh(mesh_file) - - # Verify parsing results - self.assertEqual(mesh.ndime, 2) - self.assertEqual(mesh.nelem, 2) - self.assertEqual(mesh.npoin, 4) - self.assertEqual(mesh.nmark, 2) - - # Check vertices - self.assertEqual(mesh.vertices.shape, (4, 3)) # Always 3D internally - - # Check elements - self.assertEqual(len(mesh.element_types), 2) - self.assertTrue(all(et == ElementType.TRIANGLE.value for et in mesh.element_types)) - - # Check markers - self.assertIn("wall", mesh.markers) - self.assertIn("inlet", mesh.markers) - self.assertEqual(len(mesh.markers["wall"]), 4) # 2 lines * 2 vertices each - self.assertEqual(len(mesh.markers["inlet"]), 2) # 1 line * 2 vertices - - def test_mesh_with_whitespace_issues(self): - """Test parsing mesh with whitespace issues (like the original problem).""" - mesh_content = """NDIME= 3 -NPOIN= 4 4 - 0.0000000000 0.0000000000 0.0000000000 0 - 1.0000000000 0.0000000000 0.0000000000 1 - 0.0000000000 1.0000000000 0.0000000000 2 - 0.0000000000 0.0000000000 1.0000000000 3 -NELEM= 1 -10 0 1 2 3 0 -NMARK= 1 -MARKER_TAG= boundary_face -MARKER_ELEMS= 1 -5 0 1 2 -""" - - mesh_file = os.path.join(self.temp_dir, "whitespace_mesh.su2") - with open(mesh_file, 'w') as f: - f.write(mesh_content) - - # This should parse without errors despite the whitespace - mesh = parse_mesh(mesh_file) - - self.assertEqual(mesh.ndime, 3) - self.assertEqual(mesh.npoin, 4) - self.assertEqual(mesh.nelem, 1) - self.assertEqual(mesh.nmark, 1) - self.assertIn("boundary_face", mesh.markers) - - def test_round_trip_parse_export(self): - """Test round-trip: parse -> export -> parse.""" - original_content = """NDIME= 3 -NPOIN= 4 - 0.0000000000 0.0000000000 0.0000000000 0 - 1.0000000000 0.0000000000 0.0000000000 1 - 0.0000000000 1.0000000000 0.0000000000 2 - 0.0000000000 0.0000000000 1.0000000000 3 -NELEM= 1 -10 0 1 2 3 0 -NMARK= 1 -MARKER_TAG= boundary -MARKER_ELEMS= 1 -5 0 1 2 -""" - - # Write original file - original_file = os.path.join(self.temp_dir, "original.su2") - with open(original_file, 'w') as f: - f.write(original_content) - - # Parse - mesh1 = parse_mesh(original_file) - - # Export - exported_file = os.path.join(self.temp_dir, "exported.su2") - export_mesh(mesh1, exported_file) - - # Parse exported file - mesh2 = parse_mesh(exported_file) - - # Compare meshes - self.assertEqual(mesh1.ndime, mesh2.ndime) - self.assertEqual(mesh1.nelem, mesh2.nelem) - self.assertEqual(mesh1.npoin, mesh2.npoin) - self.assertEqual(mesh1.nmark, mesh2.nmark) - - # Compare vertices (allowing for small floating point differences) - np.testing.assert_allclose(mesh1.vertices, mesh2.vertices, rtol=1e-6) - - # Compare element types - np.testing.assert_array_equal(mesh1.element_types, mesh2.element_types) - - # Compare markers - for marker_name in mesh1.markers: - self.assertIn(marker_name, mesh2.markers) - np.testing.assert_array_equal( - mesh1.markers[marker_name], - mesh2.markers[marker_name] - ) - - def test_meshly_features(self): - """Test that meshly features work correctly.""" - vertices = np.array([ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0] - ], dtype=np.float32) - - indices = np.array([0, 1, 2], dtype=np.uint32) - element_types = np.array([ElementType.TRIANGLE.value], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=3 - ) - - # Test basic meshly properties - self.assertEqual(mesh.vertex_count, 3) - self.assertEqual(mesh.index_count, 3) - - # Test model serialization - mesh_dict = mesh.model_dump() - self.assertIn("vertices", mesh_dict) - self.assertIn("indices", mesh_dict) - self.assertIn("element_types", mesh_dict) - - # Test that computed properties are not in the dump (ndime is stored) - self.assertIn("ndime", mesh_dict) # ndime is stored field - self.assertNotIn("nelem", mesh_dict) - self.assertNotIn("npoin", mesh_dict) - self.assertNotIn("nmark", mesh_dict) - - def test_hexahedron_mesh(self): - """Test mesh with hexahedral elements.""" - # Create a simple cube with one hexahedron - vertices = np.array([ - [0.0, 0.0, 0.0], # 0 - [1.0, 0.0, 0.0], # 1 - [1.0, 1.0, 0.0], # 2 - [0.0, 1.0, 0.0], # 3 - [0.0, 0.0, 1.0], # 4 - [1.0, 0.0, 1.0], # 5 - [1.0, 1.0, 1.0], # 6 - [0.0, 1.0, 1.0] # 7 - ], dtype=np.float32) - - # Hexahedron indices (8 vertices) - indices = np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.uint32) - element_types = np.array([ElementType.HEXAHEDRON.value], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=3 - ) - - self.assertEqual(mesh.nelem, 1) - self.assertEqual(mesh.npoin, 8) - self.assertEqual(len(mesh.element_types), 1) - self.assertEqual(mesh.element_types[0], ElementType.HEXAHEDRON.value) - - def test_empty_mesh(self): - """Test handling of empty mesh.""" - vertices = np.array([], dtype=np.float32).reshape(0, 3) - indices = np.array([], dtype=np.uint32) - element_types = np.array([], dtype=np.int32) - - mesh = SU2Mesh( - vertices=vertices, - indices=indices, - element_types=element_types, - izone=1, - ndime=3 - ) - - self.assertEqual(mesh.nelem, 0) - self.assertEqual(mesh.npoin, 0) - self.assertEqual(mesh.nmark, 0) - - -if __name__ == '__main__': - unittest.main()