diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml deleted file mode 100644 index 5a51cd4..0000000 --- a/.github/workflows/pypi.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: PyPI -on: - pull_request: - branches: main - release: - types: published -jobs: - wheels: - strategy: - fail-fast: false - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.79.0 - default: true - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: build wheels - run: | - python -m pip install cffi maturin - maturin build --release - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }}-${{ matrix.python-version }}-wheel - path: target/wheels - twine: - needs: wheels - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@v4 - - name: check - run: | - mkdir -p wheelhouse/ - mv ./*-wheel*/*.whl wheelhouse/ - pip install twine - twine check wheelhouse/*.whl - - name: upload - if: github.event_name == 'release' - run: twine upload -u __token__ -p ${{ secrets.PYPI_TOKEN }} wheelhouse/*.whl diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..499541f --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,73 @@ +name: Python +on: + pull_request: + branches: main + release: + types: published +jobs: + test: + if: github.event_name != 'release' + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + runs-on: ${{ matrix.os }} + steps: + - name: checkout + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.79.0 + default: true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: maturin + run: pip install maturin + - name: build + run: maturin build --release --features python + - name: install + run: pip install --find-links=target/wheels/ automesh[develop] + - name: pycodestyle + run: pycodestyle --verbose . + - name: pytest + run: pytest --verbose . + wheels: + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.79.0 + default: true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: maturin + run: pip install maturin + - name: build + run: maturin build --release --features python + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }}-${{ matrix.python-version }}-wheel + path: target/wheels + twine: + needs: wheels + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + - name: check + run: | + mkdir -p wheelhouse/ + mv ./*-wheel*/*.whl wheelhouse/ + pip install twine + twine check wheelhouse/*.whl + - name: upload + if: github.event_name == 'release' + run: twine upload -u __token__ -p ${{ secrets.PYPI_TOKEN }} wheelhouse/*.whl diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..5788401 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,47 @@ +name: Rust +on: + pull_request: + branches: main + release: + types: published +env: + CARGO_TERM_COLOR: always +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + runs-on: ${{ matrix.os }} + steps: + - name: checkout + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + components: clippy, rustfmt + default: true + toolchain: 1.79.0 + - name: build + run: cargo build --release + - name: clippy + run: cargo clippy --release -- -D warnings + - name: doc + run: cargo doc --release + - name: fmt + run: cargo fmt --all -- --check + - name: test + run: cargo test --release + package: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + - name: package + run: cargo package + - name: login + if: github.event_name == 'release' + run: cargo login ${{ secrets.CRATES_IO_TOKEN }} + - name: publish + if: github.event_name == 'release' + run: cargo publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 904ac3c..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Test -on: - pull_request: - branches: main -env: - CARGO_TERM_COLOR: always -jobs: - Cargo: - strategy: - fail-fast: false - matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] - runs-on: ${{ matrix.os }} - steps: - - name: checkout - uses: actions/checkout@v4 - - name: clippy - run: cargo clippy --release -- -D warnings diff --git a/.gitignore b/.gitignore index ec84396..e851353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# ignore virtual environment +.venv/ + # Generated by Cargo # will have compiled files and executables debug/ @@ -15,3 +18,5 @@ Cargo.lock **.whl *.vscode +**__pycache__ +**.pytest_cache diff --git a/Cargo.toml b/Cargo.toml index 29250a7..f57dd50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Chad Brian Hovey ", "Michael Robert Buche "] categories = ["mathematics", "science"] description = "automesh" -documentation = "https://github.com/autotwin/automesh" +documentation = "https://docs.rs/automesh" edition = "2021" homepage = "https://github.com/autotwin/automesh" keywords = ["mesh"] @@ -12,10 +12,14 @@ repository = "https://github.com/autotwin/automesh" version = "0.1.2" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -pyo3 = {version = "=0.22", features = ["extension-module"]} +numpy = {version = "=0.21", optional = true} +pyo3 = {version = "=0.21", features = ["extension-module"], optional = true} + +[features] +python = ["dep:numpy", "dep:pyo3"] [profile.release] codegen-units = 1 diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..fb58abc --- /dev/null +++ b/doc/README.md @@ -0,0 +1,22 @@ +# README + +* [Exodus II file format](exodus.md) + + +## Discussions + +### 2024-07-03 + +* weekly interval pair programming Wed 1100-1300 EST (0900-1100 MST) +* repo updates +* pre-commit, prevent a local from commiting prior to push +* maturin, PyO3 is the rust package for Python binding in Rust, and muturin is the packager +* iterators are great, https://doc.rust-lang.org/std/iter/trait.Iterator.html + +```bash +python -m pip install --upgrade pip +pip install maturin +maturin develop --release --extras dev +# pip install pre-commit # already installed with maturin +pre-commit install +``` diff --git a/doc/exodus.md b/doc/exodus.md new file mode 100644 index 0000000..c403d4b --- /dev/null +++ b/doc/exodus.md @@ -0,0 +1,128 @@ +# Exodus II + +## Excerpts from Schoof-1994[^Schoof-1994]: + +"EXODUS II is a model developed to store and retrieve data for finite element analyses. It is used for preprocesing (problem definition), postprocessing (results visualization), as well as code to code data transfer. An EXODUS II data file is a random access, machine independent binary file..." + +EXODUS II depends on the Network Common Data Form ([NetCDF](https://www.unidata.ucar.edu/software/netcdf/)) library. + +NetCDF is a public domain database library that provides low-level data storage. The NetCDF library stores data in eXternal Data Representation (XDR) format, which provides machine independency. + +EXODUS II library functions provide a map between finite element data objects and NetCDF dimensions, attributes, and variables. + +EXODUS II data objects: + +* Initialization Data + * Number of nodes + * Number of elements + * *optional* informational text + * et cetera +* Model - static objects (i.e., objects that do not change over time) + * Nodal coordinates + * Element connectivity + * Node sets + * Side sets +* *optional* Results + * Nodal results + * Element results + * Global results + +Note: automesh will use Initialization Data and Model sections; it will not use the Results section. + +Quadrilateral | Hexahedral +:---: | :---: +![exodus_quad_node_numbering](fig/exodus_quad_node_numbering.png) | ![exodus_hex_node_numbering](fig/exodus_hex_node_numbering.png) + +> Figure 1: EXODUS II node numbering scheme for quadrilateral and hexahedral finite elements. + +Quadrilateral | Hexahedral +:---: | :---: +![exodus_quad_sideset_numbering](fig/exodus_quad_sideset_numbering.png) | ![exodus_hex_sideset_numbering](fig/exodus_hex_sideset_numbering.png) + +> Figure 2: EXODUS II sideset numbering scheme for quadrilateral and hexahedral finite elements. + +## Pattern + +```bash +------ +Test 1 +------ +nsd = 2 +nelx = nely = 1 + y + ^ + 3| 4 + *-----* + |4 3| + | (1) | + |1 2| + *-----* --> x + 1 2 +connectivity +1 2 4 3 + +------ +Test 2 +------ +nelx = 2 +nely = 1 + y + ^ + 4| 5 6 + *-----*-----* + |4 3|4 3| + | (1) | (2) | + |1 2|1 2| + *-----*-----* --> x + 1 2 3 +connectivity +1 2 5 4 +2 3 6 5 + +------ +Test 3 +------ +nelx = 3 +nely = 1 + y + ^ + 5| 6 7 8 + *-----*-----*-----* + |4 3|4 3|4 3| + | (1) | (2) | (3) | + |1 2|1 2|1 2| + *-----*-----*-----* --> x + 1 2 3 4 +connectivity +1 2 6 5 +2 3 7 6 +3 4 8 7 + +------ +Test 4 +------ +nelx = 2 +nely = 2 + y + ^ + 7| 8 9 + *-----*-----* + |4 3|4 3| + | (3) | (4) | + |1 2|1 2| + 4*-----*-----*6 + |4 3|4 3| + | (1) | (2) | + |1 2|1 2| + *-----*-----* --> x + 1 2 3 +connectivity +1 2 5 4 +2 3 6 4 +4 6 8 7 +5 6 9 8 +``` + +## References + +[^Schoof-1994]: Schoof LA, Yarberry VR. EXODUS II: a finite element data model. Sandia National Lab.(SNL-NM), Albuquerque, NM (United States); 1994 Sep 1. [link](https://www.osti.gov/biblio/10102115) diff --git a/doc/fig/exodus_hex_node_numbering.png b/doc/fig/exodus_hex_node_numbering.png new file mode 100644 index 0000000..d10b624 Binary files /dev/null and b/doc/fig/exodus_hex_node_numbering.png differ diff --git a/doc/fig/exodus_hex_sideset_numbering.png b/doc/fig/exodus_hex_sideset_numbering.png new file mode 100644 index 0000000..5282d1e Binary files /dev/null and b/doc/fig/exodus_hex_sideset_numbering.png differ diff --git a/doc/fig/exodus_quad_node_numbering.png b/doc/fig/exodus_quad_node_numbering.png new file mode 100644 index 0000000..2f49a81 Binary files /dev/null and b/doc/fig/exodus_quad_node_numbering.png differ diff --git a/doc/fig/exodus_quad_sideset_numbering.png b/doc/fig/exodus_quad_sideset_numbering.png new file mode 100644 index 0000000..31560ea Binary files /dev/null and b/doc/fig/exodus_quad_sideset_numbering.png differ diff --git a/pyproject.toml b/pyproject.toml index be4a995..0d97d84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,31 +1,51 @@ [build-system] -requires = ["maturin==1.6.0"] -build-backend = "maturin" +build-backend = 'maturin' +requires = [ + 'cffi==1.16.0', + 'maturin==1.6.0' +] [project] -name = "automesh" -description = "automesh" authors = [ - {email = "chovey@sandia.gov"}, - {name = "Chad Brian Hovey"}, - {email = "mrbuche@sandia.gov"}, - {name = "Michael R. Buche"}, + {email = 'chovey@sandia.gov'}, + {name = 'Chad Brian Hovey'}, + {email = 'mrbuche@sandia.gov'}, + {name = 'Michael R. Buche'}, +] +classifiers = [ + 'License :: OSI Approved :: BSD License', + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Science/Research', + 'Topic :: Scientific/Engineering', + 'Programming Language :: Python', + 'Programming Language :: Rust', ] -requires-python = ">=3.8,<3.13" +description = 'automesh' dependencies = [ - "cffi==1.16.0", - "maturin==1.6.0" + 'numpy' ] -classifiers = [ - 'License :: OSI Approved :: BSD License', - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering', - 'Programming Language :: Python', - 'Programming Language :: Rust', +name = 'automesh' +requires-python = '>=3.8,<3.13' + +[project.optional-dependencies] +develop = [ + 'pre-commit', + 'pycodestyle', + 'pytest' ] [project.urls] -Documentation = "https://github.com/autotwin/automesh" -Homepage = "https://github.com/autotwin/automesh" -Repository = "https://github.com/autotwin/automesh" +Documentation = 'https://github.com/autotwin/automesh' +Homepage = 'https://github.com/autotwin/automesh' +Repository = 'https://github.com/autotwin/automesh' + +[tool.pytest.ini_options] +python_files = [ + '*.py' +] +python_functions = [ + '*' +] +testpaths = [ + 'tests/' +] diff --git a/src/exodus/mod.rs b/src/exodus/mod.rs index ac7e4f2..311c403 100644 --- a/src/exodus/mod.rs +++ b/src/exodus/mod.rs @@ -1,16 +1,9 @@ -use pyo3::prelude::*; +#[cfg(feature = "python")] +pub mod py; -pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { - parent_module.add_class::()?; - Ok(()) -} - -#[pyclass] pub struct Exodus {} -#[pymethods] impl Exodus { - #[new] pub fn init() -> Self { Self {} } diff --git a/src/exodus/py.rs b/src/exodus/py.rs new file mode 100644 index 0000000..1029303 --- /dev/null +++ b/src/exodus/py.rs @@ -0,0 +1,17 @@ +use pyo3::prelude::*; + +pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + parent_module.add_class::()?; + Ok(()) +} + +#[pyclass] +pub struct Exodus {} + +#[pymethods] +impl Exodus { + #[new] + pub fn init() -> Self { + Self {} + } +} diff --git a/src/lib.rs b/src/lib.rs index 2423a97..3ea6993 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,8 @@ -use pyo3::prelude::*; - -mod exodus; -mod spn; - pub use exodus::Exodus; pub use spn::Spn; -#[pymodule] -fn automesh(m: &Bound<'_, PyModule>) -> PyResult<()> { - exodus::register_module(m)?; - spn::register_module(m)?; - Ok(()) -} +#[cfg(feature = "python")] +mod py; + +mod exodus; +mod spn; diff --git a/src/py.rs b/src/py.rs new file mode 100644 index 0000000..3aa7a76 --- /dev/null +++ b/src/py.rs @@ -0,0 +1,10 @@ +use pyo3::prelude::*; + +pub use super::{exodus::py::Exodus, spn::py::Spn}; + +#[pymodule] +fn automesh(m: &Bound<'_, PyModule>) -> PyResult<()> { + super::exodus::py::register_module(m)?; + super::spn::py::register_module(m)?; + Ok(()) +} diff --git a/src/spn/mod.rs b/src/spn/mod.rs index deac867..0da2731 100644 --- a/src/spn/mod.rs +++ b/src/spn/mod.rs @@ -1,40 +1,42 @@ use super::Exodus; -use pyo3::prelude::*; use std::{ fs::File, io::{BufRead, BufReader}, }; -pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { - parent_module.add_class::()?; - Ok(()) -} +#[cfg(feature = "python")] +pub mod py; type Data = Vec>>; -#[pyclass] pub struct Spn { data: Data, } -#[pymethods] impl Spn { pub fn exodus(&self) -> Exodus { let _ = self.data; Exodus {} } - #[new] + pub fn get_data(&self) -> &Data { + &self.data + } pub fn init(file_path: &str, nelx: usize, nely: usize, nelz: usize) -> Self { - let flat = BufReader::new(File::open(file_path).expect("File was not found.")) - .lines() - .map(|line| line.unwrap().parse().unwrap()) - .collect::>(); - let mut data = vec![vec![vec![0; nelx]; nely]; nelz]; - data.iter_mut() - .flatten() - .flatten() - .zip(flat.iter()) - .for_each(|(data_entry, flat_entry)| *data_entry = *flat_entry); + let data = init_data(file_path, nelx, nely, nelz); Self { data } } } + +fn init_data(file_path: &str, nelx: usize, nely: usize, nelz: usize) -> Data { + let flat = BufReader::new(File::open(file_path).expect("File was not found.")) + .lines() + .map(|line| line.unwrap().parse().unwrap()) + .collect::>(); + let mut data = vec![vec![vec![0; nelx]; nely]; nelz]; + data.iter_mut() + .flatten() + .flatten() + .zip(flat.iter()) + .for_each(|(data_entry, flat_entry)| *data_entry = *flat_entry); + data +} diff --git a/src/spn/py.rs b/src/spn/py.rs new file mode 100644 index 0000000..3e0617f --- /dev/null +++ b/src/spn/py.rs @@ -0,0 +1,29 @@ +use super::super::py::Exodus; +use numpy::PyArray3; +use pyo3::prelude::*; + +pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + parent_module.add_class::()?; + Ok(()) +} + +#[pyclass] +pub struct Spn { + data: super::Data, +} + +#[pymethods] +impl Spn { + pub fn get_data<'py>(&self, python: Python<'py>) -> Bound<'py, PyArray3> { + PyArray3::from_vec3_bound(python, &self.data).unwrap() + } + pub fn exodus(&self) -> Exodus { + let _ = self.data; + Exodus {} + } + #[new] + pub fn init(file_path: &str, nelx: usize, nely: usize, nelz: usize) -> Self { + let data = super::init_data(file_path, nelx, nely, nelz); + Self { data } + } +} diff --git a/tests/spn.py b/tests/spn.py new file mode 100644 index 0000000..843dec1 --- /dev/null +++ b/tests/spn.py @@ -0,0 +1,14 @@ +import numpy as np +from automesh import Spn + +gold = np.array([ + [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], +]) + + +def read(): + spn = Spn('tests/spn/f.spn', 3, 5, 4) + assert (spn.get_data() == gold).all() diff --git a/tests/spn.rs b/tests/spn.rs new file mode 100644 index 0000000..7c91a82 --- /dev/null +++ b/tests/spn.rs @@ -0,0 +1,30 @@ +use automesh::Spn; + +const NELX: usize = 3; +const NELY: usize = 5; +const NELZ: usize = 4; + +const GOLD: [[[u8; NELX]; NELY]; NELZ] = [ + [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 0]], +]; + +#[test] +fn read() { + let spn = Spn::init("tests/spn/f.spn", NELX, NELY, NELZ); + GOLD.iter() + .zip(spn.get_data().iter()) + .for_each(|(gold_i, spn_i)| { + gold_i + .iter() + .zip(spn_i.iter()) + .for_each(|(gold_ij, spn_ij)| { + gold_ij + .iter() + .zip(spn_ij.iter()) + .for_each(|(gold_ijk, spn_ijk)| assert_eq!(gold_ijk, spn_ijk)) + }) + }) +} diff --git a/tests/spn/f.spn b/tests/spn/f.spn new file mode 100644 index 0000000..50391b0 --- /dev/null +++ b/tests/spn/f.spn @@ -0,0 +1,60 @@ +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +0 +0 +1 +1 +0 +1 +0 +0 +1 +0 +0 +1 +1 +1 +1 +0 +0 +1 +1 +0 +1 +0 +0 +1 +0 +0 +1 +1 +1 +1 +0 +0 +1 +1 +0 +1 +0 +0 +1 +0 +0