├── .dockerignore ├── .github └── workflows │ ├── ci.yml │ ├── coverage.yml │ ├── docker-publish-nightly.yml │ ├── docker-publish.yml │ └── rustdoc.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── rcb_cartesian.rs ├── coverage.sh ├── docs ├── build.sh ├── index.html └── man.css ├── examples └── simple.rs ├── ffi ├── Cargo.toml ├── Doxyfile ├── Makefile ├── README.md ├── examples │ ├── kk_fm.c │ └── rcb.c ├── include │ └── coupe.h └── src │ ├── data.rs │ └── lib.rs ├── rustfmt.toml ├── src ├── algorithms.rs ├── algorithms │ ├── arc_swap.rs │ ├── ckk.rs │ ├── fiduccia_mattheyses.rs │ ├── graph_growth.rs │ ├── greedy.rs │ ├── hilbert_curve.rs │ ├── k_means.rs │ ├── kernighan_lin.rs │ ├── kk.rs │ ├── multi_jagged.rs │ ├── recursive_bisection.rs │ ├── vn │ │ ├── best.rs │ │ ├── first.rs │ │ └── mod.rs │ └── z_curve.rs ├── average.rs ├── cartesian │ ├── mod.rs │ └── rcb.rs ├── defer.rs ├── geometry.rs ├── imbalance.rs ├── lib.rs ├── main.rs ├── nextafter.rs ├── real.rs ├── topology │ ├── mod.rs │ └── sprs.rs └── work_share.rs └── tools ├── .gitignore ├── Cargo.toml ├── Makefile ├── README.md ├── build.rs ├── doc ├── apply-part.1.scd ├── apply-weight.1.scd ├── mesh-dup.1.scd ├── mesh-part.1.scd ├── mesh-points.1.scd ├── mesh-refine.1.scd ├── mesh-reorder.1.scd ├── mesh-svg.1.scd ├── part-bench.1.scd ├── part-info.1.scd └── weight-gen.1.scd ├── mesh-io ├── Cargo.toml ├── ffi │ ├── Cargo.toml │ ├── meshio.h │ └── src │ │ └── lib.rs └── src │ ├── lib.rs │ ├── medit │ ├── mod.rs │ ├── parser.rs │ └── serializer.rs │ ├── partition.rs │ ├── vtk.rs │ └── weight.rs ├── num-part ├── Cargo.toml ├── README.md └── src │ ├── database │ ├── migration-000-init.sql │ └── mod.rs │ ├── help_after.txt │ └── main.rs ├── report ├── common.sh ├── efficiency ├── imbedgecut.sh ├── perf ├── quality └── weak-scaling └── src ├── bin ├── apply-part.rs ├── apply-weight.rs ├── mesh-dup.rs ├── mesh-part.rs ├── mesh-points.rs ├── mesh-refine.rs ├── mesh-reorder.rs ├── mesh-svg.rs ├── part-bench.rs ├── part-info.rs └── weight-gen.rs ├── ittapi.rs ├── ittapi_stub.rs ├── lib.rs ├── metis.rs ├── scotch.rs └── zoom_in.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | /benches 3 | /example 4 | /target 5 | /build 6 | 7 | # files 8 | LICENCE* 9 | *.md 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: 1 12 | BINDGEN_EXTRA_CLANG_ARGS: "-I/usr/include/scotch" # ubuntu package use 13 | 14 | jobs: 15 | test: 16 | name: Test Suite 17 | runs-on: ubuntu-22.04 18 | strategy: 19 | matrix: 20 | rust: 21 | - stable 22 | steps: 23 | - run: sudo apt-get -y install libclang-dev libmetis-dev libscotch-dev 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: ${{ matrix.rust }} 29 | override: true 30 | - name: Build tests and benchmarks 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: test 34 | args: --all --all-targets --no-run --locked 35 | - name: Run tests 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: test 39 | args: --all --locked 40 | 41 | fmt: 42 | name: Rustfmt 43 | runs-on: ubuntu-22.04 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | override: true 51 | components: rustfmt 52 | - uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: --all --check 56 | 57 | clippy: 58 | name: Clippy 59 | runs-on: ubuntu-22.04 60 | steps: 61 | - run: sudo apt-get -y install libclang-dev libmetis-dev libscotch-dev 62 | - uses: actions/checkout@v2 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | profile: minimal 66 | toolchain: stable 67 | override: true 68 | components: clippy 69 | - uses: actions-rs/cargo@v1 70 | with: 71 | command: clippy 72 | args: > 73 | --all --all-targets --locked -- 74 | -D warnings 75 | -D clippy::cargo_common_metadata 76 | -D clippy::negative_feature_names 77 | -D clippy::redundant_feature_names 78 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | coverage: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Install Rust 15 | run: rustup toolchain install nightly --component llvm-tools-preview 16 | # nightly for doc tests 17 | - name: Install cargo-llvm-cov 18 | uses: taiki-e/install-action@cargo-llvm-cov 19 | - name: Generate code coverage 20 | run: ./coverage.sh 21 | # no default because we lack scotch and metis 22 | - name: Upload coverage to Codecov 23 | uses: codecov/codecov-action@v3 24 | with: 25 | files: lcov.info 26 | fail_ci_if_error: true 27 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | build: 22 | 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v2 31 | 32 | - name: Lint Dockerfile 33 | uses: hadolint/hadolint-action@master 34 | with: 35 | dockerfile: "Dockerfile" 36 | ignore: 'DL3008' 37 | #ignore: 'DL3006,DL3008' # DL3006: false positive, rust image has a tag 38 | 39 | # Login against a Docker registry except on PR 40 | # https://github.com/docker/login-action 41 | - name: Log into registry ${{ env.REGISTRY }} 42 | if: github.event_name != 'pull_request' 43 | uses: docker/login-action@v1 44 | with: 45 | registry: ${{ env.REGISTRY }} 46 | username: ${{ github.actor }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | # Extract metadata (tags, labels) for Docker 50 | # https://github.com/docker/metadata-action 51 | - name: Extract Docker metadata 52 | id: meta 53 | uses: docker/metadata-action@v5 54 | with: 55 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 56 | labels: | 57 | org.opencontainers.image.title=Coupe 58 | org.opencontainers.image.description=Coupe Image 59 | org.opencontainers.image.vendor=CEA 60 | tags: | 61 | type=raw,value=nightly 62 | 63 | # Build and push Docker image with Buildx 64 | - name: Build and push nightly Docker image 65 | uses: docker/build-push-action@v5 66 | with: 67 | push: ${{ github.event_name != 'pull_request' }} 68 | tags: ${{ steps.meta.outputs.tags }} 69 | labels: ${{ steps.meta.outputs.labels }} 70 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | workflow_dispatch: 10 | inputs: 11 | tag_name: 12 | description: 'Tag name for image' 13 | required: true 14 | default: 'master' 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | build: 22 | 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v2 31 | 32 | # Login against a Docker registry except on PR 33 | # https://github.com/docker/login-action 34 | - name: Log into registry ${{ env.REGISTRY }} 35 | if: github.event_name != 'pull_request' 36 | uses: docker/login-action@v1 37 | with: 38 | registry: ${{ env.REGISTRY }} 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | # Extract metadata (tags, labels) for Docker 43 | # https://github.com/docker/metadata-action 44 | - name: Extract Docker metadata 45 | id: meta 46 | uses: docker/metadata-action@v5 47 | with: 48 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 49 | labels: | 50 | org.opencontainers.image.title=Coupe 51 | org.opencontainers.image.description=Coupe Image 52 | org.opencontainers.image.vendor=CEA 53 | tags: | 54 | type=raw, ${{ github.event.inputs.tag_name }} 55 | type=raw, ${{ github.event.inputs.tag_name }}-{{date 'YYYYMMDD'}} 56 | 57 | # Build and push Docker image with Buildx (don't push on PR) 58 | # https://github.com/docker/build-push-action 59 | - name: Build and push Docker image 60 | uses: docker/build-push-action@v5 61 | with: 62 | push: ${{ github.event_name != 'pull_request' }} 63 | tags: ${{ steps.meta.outputs.tags }} 64 | labels: ${{ steps.meta.outputs.labels }} 65 | -------------------------------------------------------------------------------- /.github/workflows/rustdoc.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | CARGO_INCREMENTAL: 0 10 | CARGO_NET_RETRY: 10 11 | RUSTFLAGS: "-D warnings" 12 | RUSTUP_MAX_RETRIES: 10 13 | RUST_BACKTRACE: 1 14 | BINDGEN_EXTRA_CLANG_ARGS: "-I/usr/include/scotch" # ubuntu package use 15 | 16 | jobs: 17 | rustdoc: 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - run: sudo apt-get -y install libclang-dev libmetis-dev libscotch-dev 22 | - uses: actions/checkout@v2 23 | - uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | profile: minimal 27 | override: true 28 | components: rustfmt, rust-src 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: doc 32 | args: --all --no-deps 33 | - run: | 34 | sudo apt-get -y install scdoc mandoc 35 | ./docs/build.sh 36 | - name: Deploy Docs 37 | uses: peaceiris/actions-gh-pages@364c31d33bb99327c77b3a5438a83a357a6729ad # v3.4.0 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_branch: gh-pages 41 | publish_dir: ./target/doc 42 | force_orphan: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | /target 3 | /examples/target 4 | **/*.rs.bk 5 | 6 | # ide 7 | .vscode/* 8 | *.iml 9 | .idea/* 10 | 11 | # mesh files 12 | *.vtk 13 | *.vtu 14 | *.mesh 15 | *.meshb 16 | *.part 17 | *.weight 18 | *.weights 19 | 20 | # trace files 21 | *.json 22 | 23 | # flamegraphs and efficiency graphs 24 | perf.data 25 | perf.data.old 26 | callgrind.out.* 27 | *.svg 28 | *.png 29 | *.csv 30 | 31 | # coverage 32 | *.profraw 33 | *.profdata 34 | 35 | # generated man pages 36 | tools/doc/*.1 37 | 38 | # generated documentation 39 | /build 40 | /ffi/build 41 | 42 | # num-part's SQLite databases 43 | *.db 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "ffi", 5 | "tools", 6 | "tools/mesh-io", 7 | "tools/mesh-io/ffi", 8 | "tools/num-part", 9 | ] 10 | 11 | [package] 12 | name = "coupe" 13 | version = "0.1.0" 14 | authors = [ 15 | "Armand Touminet ", 16 | "Hubert Hirtz ", 17 | "Cédric Chevalier", 18 | "Sébastien Morais" 19 | ] 20 | edition = "2021" 21 | license = "MIT OR Apache-2.0" 22 | description = """ 23 | Coupe is a mesh partitioning library. It implements composable 24 | geometric and topologic algorithms. 25 | """ 26 | keywords = ["mesh", "partitioning"] 27 | categories = ["algorithms", "mathematics"] 28 | readme = "README.md" 29 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 30 | autobenches = false 31 | 32 | [features] 33 | default = ["sprs"] 34 | 35 | # Enable the nightly `stdsimd` feature and AVX512-accelerated algorithms. 36 | # Requires rust nightly. 37 | avx512 = [] 38 | 39 | [dependencies] 40 | approx = "0.5" 41 | itertools = "0.12" 42 | nalgebra = { version = "0.32", default-features = false, features = ["rand", "std"] } 43 | num-traits = "0.2" 44 | rayon = "1" 45 | tracing = { version = "0.1", default-features = false, features = ["std"] } 46 | rand = "0.8" 47 | sprs = { version = "0.11", optional = true, default-features = false, features = ["multi_thread"] } 48 | ittapi = "0.4" 49 | 50 | [dev-dependencies] 51 | criterion = "0.5" 52 | proptest = { version = "1", default-features = false, features = ["std", "timeout"] } 53 | tracing-subscriber = "0.3" 54 | tracing-chrome = "0.7" 55 | tracing-tree = "0.3" 56 | num_cpus = "1" 57 | core_affinity = "0.8" 58 | 59 | 60 | [[bench]] 61 | name = "rcb_cartesian" 62 | harness = false 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILDER PATTERN 2 | 3 | FROM rust:1 AS builder 4 | 5 | WORKDIR /builder 6 | 7 | COPY . . 8 | 9 | RUN apt-get update -y && apt-get install -y --no-install-recommends libclang-dev libmetis-dev libscotch-dev 10 | 11 | ARG BINDGEN_EXTRA_CLANG_ARGS="-I/usr/include/scotch" 12 | 13 | RUN cargo install --path tools --root /builder/install --features metis,intel-perf 14 | 15 | # FINAL IMAGE 16 | 17 | FROM debian:stable-slim 18 | 19 | WORKDIR /coupe 20 | 21 | COPY --from=builder /usr/lib/x86_64-linux-gnu/libscotch* /usr/lib/x86_64-linux-gnu/ 22 | COPY --from=builder /usr/lib/x86_64-linux-gnu/libmetis* /usr/lib/x86_64-linux-gnu/ 23 | COPY --from=builder /builder/install/bin /usr/bin 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2018-2023 CEA 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [coupe] 2 | 3 | A modular, multi-threaded partitioning library. 4 | 5 | Coupe implements a variety of algorithms that can be used to partition meshes, 6 | graphs and numbers. See [the API docs][coupe] for a list. These algorithms can 7 | be composed together to build relevant partitions of your data. 8 | 9 | ## Usage 10 | 11 | ### From the command-line 12 | 13 | A list of tools is provided to work with coupe from the command-line, you may 14 | find them, along with their documentation in the `tools/` directory. 15 | 16 | ### From Rust 17 | 18 | See the API documentation on [docs.rs][coupe], and the `examples/` directory for 19 | example usages of the library. 20 | 21 | ### From other languages 22 | 23 | Coupe offers a C interface which can be found in the `ffi/` directory. 24 | 25 | Bindings for other languages have not been made yet. If you end up developing 26 | such bindings, please send us a note so they can be shown here! 27 | 28 | ## Contributing 29 | 30 | Contributions are welcome and accepted as pull requests on [GitHub][pulls]. 31 | 32 | You may also ask questions on the [discussion forum][discussions] and file bug 33 | reports on the [issue tracker][issues]. 34 | 35 | ## License 36 | 37 | Licensed under either of 38 | 39 | * Apache License, Version 2.0 40 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 41 | * MIT license 42 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 43 | 44 | at your option. 45 | 46 | The SPDX license identifier for this project is MIT OR Apache-2.0. 47 | 48 | Unless you explicitly state otherwise, any contribution intentionally submitted 49 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 50 | dual licensed as above, without any additional terms or conditions. 51 | 52 | 53 | [67]: https://github.com/LIHPC-Computational-Geometry/coupe/pull/67 54 | [coupe]: https://lihpc-computational-geometry.github.io/coupe 55 | [discussions]: https://github.com/LIHPC-Computational-Geometry/coupe/discussions 56 | [issues]: https://github.com/LIHPC-Computational-Geometry/coupe/issues 57 | [pulls]: https://github.com/LIHPC-Computational-Geometry/coupe/pulls 58 | -------------------------------------------------------------------------------- /benches/rcb_cartesian.rs: -------------------------------------------------------------------------------- 1 | use criterion::black_box; 2 | use criterion::criterion_group; 3 | use criterion::criterion_main; 4 | use criterion::Criterion; 5 | use std::num::NonZeroUsize; 6 | 7 | pub fn bench(c: &mut Criterion) { 8 | let width = NonZeroUsize::new(10000).unwrap(); 9 | let height = NonZeroUsize::new(10000).unwrap(); 10 | 11 | let count = usize::from(width) * usize::from(height); 12 | 13 | let grid = coupe::Grid::new_2d(width, height); 14 | let weights: Vec = (0..count).map(|i| i as f64).collect(); 15 | let mut partition = vec![0; count]; 16 | 17 | let core_count = num_cpus::get(); 18 | let mut group = c.benchmark_group("rcb_cartesian"); 19 | 20 | for thread_count in [1, 2, 4, 8, 16, 24, 32, 40] { 21 | let pool = rayon::ThreadPoolBuilder::new() 22 | .num_threads(thread_count) 23 | .spawn_handler(|thread| { 24 | let mut b = std::thread::Builder::new(); 25 | if let Some(name) = thread.name() { 26 | b = b.name(name.to_owned()); 27 | } 28 | if let Some(stack_size) = thread.stack_size() { 29 | b = b.stack_size(stack_size); 30 | } 31 | b.spawn(move || { 32 | let core_idx = thread.index() % core_count; 33 | core_affinity::set_for_current(core_affinity::CoreId { id: core_idx }); 34 | thread.run(); 35 | })?; 36 | Ok(()) 37 | }) 38 | .build() 39 | .unwrap(); 40 | group.bench_function(&thread_count.to_string(), |b| { 41 | pool.install(|| { 42 | b.iter(|| grid.rcb(black_box(&mut partition), black_box(&weights), 12)) 43 | }); 44 | }); 45 | } 46 | } 47 | 48 | criterion_group!(benches, bench); 49 | criterion_main!(benches); 50 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Cargo target directory 5 | TARGET="${TARGET:-target}" 6 | 7 | # Generate coverage for coupe and tools 8 | # We need to call this before coverage for ffi, because cargo-llvm-cov removes 9 | # all previous profraw files before runs. 10 | # No default features to avoid linking to SCOTCH and METIS. 11 | cargo +nightly llvm-cov \ 12 | --doctests \ 13 | --no-report \ 14 | --no-default-features \ 15 | --workspace 16 | 17 | # Building on nightly since we used nightly for cargo-llvm-cov. 18 | RUSTFLAGS="-Cinstrument-coverage $RUSTFLAGS" cargo +nightly build -p coupe-ffi 19 | 20 | mkdir -p "$TARGET/ffi-examples" 21 | for f in ffi/examples/*.c 22 | do 23 | example="$(basename "$f" .c)" 24 | 25 | clang "$f" \ 26 | -o "$TARGET/ffi-examples/$example" \ 27 | -L"$TARGET/debug" \ 28 | -lcoupe \ 29 | -g -Wall -Wextra -Werror 30 | 31 | LLVM_PROFILE_FILE="$TARGET/llvm-cov-target/coupe-$example.profraw" \ 32 | LD_LIBRARY_PATH="$TARGET/debug" \ 33 | "$TARGET/ffi-examples/$example" 34 | done 35 | 36 | # Feed ffi objects to cargo-llvm-cov 37 | mv "$TARGET/debug/libcoupe.so" "$TARGET/llvm-cov-target/debug/" 38 | mv "$TARGET/debug/libcoupe.a" "$TARGET/llvm-cov-target/debug/" 39 | 40 | cargo +nightly llvm-cov \ 41 | report \ 42 | --lcov \ 43 | --output-path lcov.info 44 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p target/doc/man 5 | for f in tools/doc/*.scd 6 | do 7 | html=$(basename "$f" .scd).html 8 | scdoc <"$f" | 9 | mandoc -Thtml -O style=man.css | 10 | sed 's|\([a-z-]\+\)(1)|\1(1)|g' >target/doc/man/"$html" 11 | done 12 | 13 | cp docs/index.html target/doc/ 14 | cp docs/man.css target/doc/man/ 15 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | coupe 3 | 4 | 5 | 6 | 20 | 21 |

coupe

22 | 23 | 28 | 29 |

30 | Coupe is a mesh partitioning platform. It provides solutions to solve different 31 | variants of the mesh partitioning problem, mainly in the context of 32 | load-balancing parallel mesh-based applications. From partitioning weights 33 | ensuring balance to topological partitioning that minimizes communication 34 | metrics through geometric methods, Coupe offers a large panel of algorithms to 35 | fit user-specific problems. Coupe exploits shared memory parallelism, is written 36 | in Rust, and consists of a library and command line tools. Experimenting with 37 | different algorithms and parameters is easy. 38 | -------------------------------------------------------------------------------- /docs/man.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 80ch; 3 | font-family: monospace; 4 | font-size: 14px; 5 | } 6 | 7 | .manual-text { 8 | padding: 0 9ex 1ex 4ex; 9 | } 10 | 11 | .head, .foot { 12 | width: 100%; 13 | color: #999; 14 | } 15 | .head-vol { 16 | text-align: center; 17 | } 18 | .head-rtitle { 19 | text-align: right; 20 | } 21 | 22 | h1 { 23 | font-size: 16px; 24 | } 25 | 26 | .Bd-indent { 27 | padding-left: 4ex; 28 | } 29 | 30 | pre { 31 | color: #434241; 32 | } 33 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use coupe::Partition as _; 4 | use coupe::Point2D; 5 | 6 | fn main() -> Result<(), Box> { 7 | // Let's define a graph: 8 | // 9 | // Node IDs: Weights: 10 | // 11 | // 2 5 8 3 4 5 12 | // +---+---+ +---+---+ 13 | // | | | | | | 14 | // 1+---4---+7 2+---3---+4 15 | // | | | | | | 16 | // +---+---+ +---+---+ 17 | // 0 3 6 1 2 3 18 | // 19 | let coordinates: &[Point2D] = &[ 20 | Point2D::new(0.0, 0.0), 21 | Point2D::new(0.0, 1.0), 22 | Point2D::new(0.0, 2.0), 23 | Point2D::new(1.0, 0.0), 24 | Point2D::new(1.0, 1.0), 25 | Point2D::new(1.0, 2.0), 26 | Point2D::new(2.0, 0.0), 27 | Point2D::new(2.0, 1.0), 28 | Point2D::new(2.0, 2.0), 29 | ]; 30 | 31 | let weights: [f64; 9] = [1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0]; 32 | 33 | let graph: sprs::CsMat = { 34 | let mut g = sprs::CsMat::empty(sprs::CSR, 9); 35 | g.insert(0, 1, 1); 36 | g.insert(0, 3, 1); 37 | 38 | g.insert(1, 0, 1); 39 | g.insert(1, 2, 1); 40 | g.insert(1, 4, 1); 41 | 42 | g.insert(2, 1, 1); 43 | g.insert(2, 5, 1); 44 | 45 | g.insert(3, 0, 1); 46 | g.insert(3, 4, 1); 47 | g.insert(3, 6, 1); 48 | 49 | g.insert(4, 1, 1); 50 | g.insert(4, 3, 1); 51 | g.insert(4, 5, 1); 52 | g.insert(4, 7, 1); 53 | 54 | g.insert(5, 2, 1); 55 | g.insert(5, 4, 1); 56 | g.insert(5, 8, 1); 57 | 58 | g.insert(6, 3, 1); 59 | g.insert(6, 7, 1); 60 | 61 | g.insert(7, 4, 1); 62 | g.insert(7, 6, 1); 63 | g.insert(7, 8, 1); 64 | 65 | g.insert(8, 5, 1); 66 | g.insert(8, 7, 1); 67 | 68 | g 69 | }; 70 | 71 | let mut partition = [0; 9]; 72 | 73 | fn print_partition(partition: &[usize]) { 74 | println!(); 75 | println!(" {}---{}---{}", partition[2], partition[5], partition[8]); 76 | println!(" | | |"); 77 | println!(" {}---{}---{}", partition[1], partition[4], partition[7]); 78 | println!(" | | |"); 79 | println!(" {}---{}---{}", partition[0], partition[3], partition[6]); 80 | println!(); 81 | } 82 | 83 | coupe::HilbertCurve { 84 | part_count: 2, 85 | ..Default::default() 86 | } 87 | .partition(&mut partition, (coordinates, weights))?; 88 | 89 | println!("Initial partitioning with a Hilbert curve:"); 90 | print_partition(&partition); 91 | 92 | coupe::FiducciaMattheyses::default().partition(&mut partition, (graph.view(), &weights))?; 93 | 94 | println!("Partition improving with Fiduccia-Mattheyses:"); 95 | print_partition(&partition); 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coupe-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "C bindings to coupe" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["mesh", "partitioning"] 8 | categories = ["algorithms", "mathematics"] 9 | readme = "README.md" 10 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 11 | 12 | 13 | [lib] 14 | name = "coupe" 15 | doc = false # name conflicts with the rust lib 16 | crate-type = [ 17 | "cdylib", 18 | "staticlib", 19 | ] 20 | 21 | 22 | [dependencies] 23 | coupe = { version = "0.1", path = ".." } 24 | -------------------------------------------------------------------------------- /ffi/Doxyfile: -------------------------------------------------------------------------------- 1 | # Doxyfile 1.9.3 2 | 3 | PROJECT_NAME = coupe 4 | PROJECT_BRIEF = the concurrent partitioner 5 | OUTPUT_DIRECTORY = build 6 | OPTIMIZE_OUTPUT_FOR_C = YES 7 | EXTRACT_ALL = YES 8 | QUIET = YES 9 | INPUT = README.md include/ 10 | USE_MDFILE_AS_MAINPAGE = README.md 11 | -------------------------------------------------------------------------------- /ffi/Makefile: -------------------------------------------------------------------------------- 1 | RUSTTARGET ?= ./target 2 | PREFIX ?= /usr/local 3 | 4 | CARGO ?= cargo 5 | SCDOC ?= scdoc 6 | DOXYGEN ?= doxygen 7 | LIBDIR ?= lib 8 | INCDIR ?= include 9 | 10 | all: coupe 11 | 12 | coupe: 13 | $(CARGO) build --locked --release 14 | 15 | doc: 16 | $(DOXYGEN) Doxyfile 17 | @printf "HTML docs generated at \e]8;;file://$(PWD)/build/html/index.html\e\\\\build/html\e]8;;\e\\\\\n" 18 | 19 | clean: 20 | $(CARGO) clean 21 | 22 | install: 23 | mkdir -p $(DESTDIR)$(PREFIX)/$(LIBDIR) 24 | mkdir -p $(DESTDIR)$(PREFIX)/$(INCDIR) 25 | cp -f include/coupe.h $(DESTDIR)$(PREFIX)/$(INCDIR)/ 26 | cp -f $(RUSTTARGET)/release/libcoupe.a $(DESTDIR)$(PREFIX)/$(LIBDIR)/ 27 | cp -f $(RUSTTARGET)/release/libcoupe.so $(DESTDIR)$(PREFIX)/$(LIBDIR)/ 28 | 29 | .PHONY: coupe clean doc install 30 | -------------------------------------------------------------------------------- /ffi/README.md: -------------------------------------------------------------------------------- 1 | # libcoupe.so 2 | 3 | C interface to coupe. 4 | 5 | ## Building 6 | 7 | You can build this project with cargo, or use the provided Makefile: 8 | 9 | ``` 10 | make 11 | sudo make install 12 | ``` 13 | 14 | Both a static (`libcoupe.a`) and a dynamic (`libcoupe.so`) library are produced. 15 | 16 | **Note:** while we try our best to limit panics, coupe is not free of them. We 17 | recommend packagers to keep the default `panic = "unwind"` or 18 | `RUSTFLAGS="-Cpanic=unwind"` flag, so that they can be caught and do not abort 19 | the whole process. 20 | 21 | ## Usage 22 | 23 | These bindings target C99, but should work with later versions of the language. 24 | 25 | A couple example usages can be found in the `examples/` directory. 26 | 27 | ## Documentation 28 | 29 | The full documentation is written as doc comments in `include/coupe.h` and 30 | should be seamlessly understood by IDEs and language servers. 31 | 32 | A simple `Doxyfile` and make target are also provided to generate HTML and LaTeX 33 | documents: 34 | 35 | ``` 36 | make doc 37 | ``` 38 | 39 | Generated documentation is placed inside a newly created `build/` directory. 40 | -------------------------------------------------------------------------------- /ffi/examples/kk_fm.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/coupe.h" 3 | 4 | #define POINT_COUNT 9 5 | #define DIMENSION 2 6 | 7 | static void 8 | print_partition(uintptr_t partition[POINT_COUNT]) 9 | { 10 | printf("\n" 11 | " %ld---%ld---%ld\n" 12 | " | | |\n" 13 | " %ld---%ld---%ld\n" 14 | " | | |\n" 15 | " %ld---%ld---%ld\n" 16 | "\n", 17 | partition[2], partition[5], partition[8], 18 | partition[1], partition[4], partition[7], 19 | partition[0], partition[3], partition[6]); 20 | } 21 | 22 | int 23 | main() 24 | { 25 | /* Let's define a graph: 26 | * 27 | * Node IDs: Weights: 28 | * 29 | * 2 5 8 3 4 5 30 | * +---+---+ +---+---+ 31 | * | | | | | | 32 | * 1+---4---+7 2+---3---+4 33 | * | | | | | | 34 | * +---+---+ +---+---+ 35 | * 0 3 6 1 2 3 36 | */ 37 | int weight_array[POINT_COUNT] = { 1, 2, 3, 2, 3, 4, 3, 4, 5 }; 38 | coupe_data *weights = coupe_data_array(POINT_COUNT, COUPE_INT, weight_array); 39 | if (weights == NULL) { 40 | fprintf(stderr, "Out of memory\n"); 41 | return 1; 42 | } 43 | 44 | uintptr_t xadj[POINT_COUNT+1] = {0, 2, 5, 7, 10, 14, 17, 19, 22, 24}; 45 | uintptr_t adjncy[24] = 46 | {1, 3, 0, 2, 4, 1, 5, 0, 4, 6, 1, 3, 5, 7, 2, 4, 8, 3, 7, 4, 6, 8, 5, 7}; 47 | int64_t edge_weights[24] = 48 | {1, 1, 1, 2, 2, 2, 3, 1, 2, 2, 2, 2, 3, 3, 3, 2, 4, 2, 3, 3, 3, 4, 4, 4}; 49 | struct coupe_adjncy *adjacency = 50 | coupe_adjncy_csr(POINT_COUNT, xadj, adjncy, COUPE_INT64, edge_weights); 51 | if (adjacency == NULL) { 52 | fprintf(stderr, "Either out of memory, or invalid adjacency structure\n"); 53 | coupe_data_free(weights); 54 | return 1; 55 | } 56 | 57 | uintptr_t partition[POINT_COUNT]; 58 | 59 | /* Let's run the Karmarkar-Karp algorithm on this graph: */ 60 | uintptr_t part_count = 2; 61 | enum coupe_err err = coupe_karmarkar_karp(partition, weights, part_count); 62 | if (err != COUPE_ERR_OK) { 63 | fprintf(stderr, "Error: %s\n", coupe_strerror(err)); 64 | coupe_adjncy_free(adjacency); 65 | coupe_data_free(weights); 66 | return 1; 67 | } 68 | 69 | printf("Initial partitioning with Karmarkar-Karp:\n"); 70 | print_partition(partition); 71 | 72 | /* Let's refine the partition with Fiduccia-Mattheyses: */ 73 | err = coupe_fiduccia_mattheyses(partition, adjacency, weights, 0, 0, 0.3, 0); 74 | if (err != COUPE_ERR_OK) { 75 | fprintf(stderr, "Error: %s\n", coupe_strerror(err)); 76 | coupe_adjncy_free(adjacency); 77 | coupe_data_free(weights); 78 | return 1; 79 | } 80 | 81 | printf("Partition refined by Fiduccia-Mattheyses:\n"); 82 | print_partition(partition); 83 | 84 | coupe_adjncy_free(adjacency); 85 | coupe_data_free(weights); 86 | return 0; 87 | } 88 | -------------------------------------------------------------------------------- /ffi/examples/rcb.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/coupe.h" 3 | 4 | #define POINT_COUNT 4 5 | #define DIMENSION 2 6 | 7 | int 8 | main() 9 | { 10 | uintptr_t partition[POINT_COUNT]; 11 | 12 | double point_array[POINT_COUNT][DIMENSION] = { 13 | {0.0, 0.0}, 14 | {0.0, 1.0}, 15 | {1.0, 0.0}, 16 | {1.0, 1.0}, 17 | }; 18 | struct coupe_data *points = coupe_data_array(POINT_COUNT, COUPE_DOUBLE, point_array); 19 | 20 | int one = 1; 21 | struct coupe_data *weights = coupe_data_constant(POINT_COUNT, COUPE_INT, &one); 22 | 23 | uintptr_t iter_count = 1; 24 | double tolerance = 0.05; 25 | enum coupe_err err = 26 | coupe_rcb(partition, DIMENSION, points, weights, iter_count, tolerance); 27 | if (err != COUPE_ERR_OK) { 28 | fprintf(stderr, "Error: %s\n", coupe_strerror(err)); 29 | coupe_data_free(points); 30 | coupe_data_free(weights); 31 | return 1; 32 | } 33 | 34 | printf("With 1 iteration (2 parts), RCB returned: %s\n", coupe_strerror(err)); 35 | printf("partition:\n"); 36 | printf("%ld %ld\n%ld %ld\n", partition[0], partition[1], partition[2], partition[3]); 37 | 38 | iter_count = 2; 39 | err = coupe_rcb(partition, DIMENSION, points, weights, iter_count, tolerance); 40 | if (err != COUPE_ERR_OK) { 41 | fprintf(stderr, "Error: %s\n", coupe_strerror(err)); 42 | coupe_data_free(points); 43 | coupe_data_free(weights); 44 | return 1; 45 | } 46 | 47 | printf("With 2 iterations (4 parts), RCB returned: %s\n", coupe_strerror(err)); 48 | printf("partition:\n"); 49 | printf("%ld %ld\n%ld %ld\n", partition[0], partition[1], partition[2], partition[3]); 50 | 51 | coupe_data_free(points); 52 | coupe_data_free(weights); 53 | return 0; 54 | } 55 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /src/algorithms.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | mod arc_swap; 4 | mod ckk; 5 | mod fiduccia_mattheyses; 6 | mod graph_growth; 7 | mod greedy; 8 | mod hilbert_curve; 9 | mod k_means; 10 | mod kernighan_lin; 11 | mod kk; 12 | mod multi_jagged; 13 | mod recursive_bisection; 14 | mod vn; 15 | mod z_curve; 16 | 17 | pub use arc_swap::ArcSwap; 18 | pub use arc_swap::AsWeight; 19 | pub use arc_swap::Metadata as AsMetadata; 20 | pub use ckk::CkkWeight; 21 | pub use ckk::CompleteKarmarkarKarp; 22 | pub use fiduccia_mattheyses::FiducciaMattheyses; 23 | pub use fiduccia_mattheyses::FmWeight; 24 | pub use fiduccia_mattheyses::Metadata as FmMetadata; 25 | pub use graph_growth::GraphGrowth; 26 | pub use greedy::Greedy; 27 | pub use greedy::GreedyWeight; 28 | pub use hilbert_curve::Error as HilbertCurveError; 29 | pub use hilbert_curve::HilbertCurve; 30 | pub use k_means::KMeans; 31 | pub use kernighan_lin::KernighanLin; 32 | pub use kk::KarmarkarKarp; 33 | pub use kk::KkWeight; 34 | pub use multi_jagged::MultiJagged; 35 | pub use recursive_bisection::Rcb; 36 | pub use recursive_bisection::RcbWeight; 37 | pub use recursive_bisection::Rib; 38 | pub use vn::VnBest; 39 | pub use vn::VnBestWeight; 40 | pub use vn::VnFirst; 41 | pub use vn::VnFirstWeight; 42 | pub use z_curve::ZCurve; 43 | 44 | /// Common errors thrown by algorithms. 45 | #[derive(Clone, Copy, Debug)] 46 | #[non_exhaustive] 47 | pub enum Error { 48 | /// No partition that matches the given criteria could been found. 49 | NotFound, 50 | 51 | /// Input sets don't have matching lengths. 52 | InputLenMismatch { expected: usize, actual: usize }, 53 | 54 | /// Input contains negative values and such values are not supported. 55 | NegativeValues, 56 | 57 | /// When a partition improving algorithm is given more than 2 parts. 58 | BiPartitioningOnly, 59 | } 60 | 61 | impl fmt::Display for Error { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | match self { 64 | Error::NotFound => write!(f, "no partition found"), 65 | Error::InputLenMismatch { expected, actual } => write!( 66 | f, 67 | "input sets don't have the same length (expected {expected} items, got {actual})", 68 | ), 69 | Error::NegativeValues => write!(f, "input contains negative values"), 70 | Error::BiPartitioningOnly => write!(f, "expected no more than two parts"), 71 | } 72 | } 73 | } 74 | 75 | impl std::error::Error for Error {} 76 | 77 | /// Map elements to parts randomly. 78 | /// 79 | /// # Example 80 | /// 81 | /// ```rust 82 | /// # fn main() -> Result<(), std::convert::Infallible> { 83 | /// use coupe::Partition as _; 84 | /// use rand; 85 | /// 86 | /// let mut partition = [0; 12]; 87 | /// 88 | /// coupe::Random { rng: rand::thread_rng(), part_count: 3 } 89 | /// .partition(&mut partition, ())?; 90 | /// # Ok(()) 91 | /// # } 92 | /// ``` 93 | #[derive(Debug)] 94 | pub struct Random { 95 | pub rng: R, 96 | pub part_count: usize, 97 | } 98 | 99 | impl crate::Partition<()> for Random 100 | where 101 | R: rand::Rng, 102 | { 103 | type Metadata = (); 104 | type Error = std::convert::Infallible; 105 | 106 | fn partition(&mut self, part_ids: &mut [usize], _: ()) -> Result { 107 | for part_id in part_ids { 108 | *part_id = self.rng.gen_range(0..self.part_count); 109 | } 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/algorithms/ckk.rs: -------------------------------------------------------------------------------- 1 | use num_traits::FromPrimitive; 2 | use num_traits::ToPrimitive; 3 | 4 | use super::Error; 5 | use std::iter::Sum; 6 | use std::ops::Add; 7 | use std::ops::Sub; 8 | 9 | /// Adds an element `e` to a vector `v` and maintain order. 10 | fn add(v: &mut Vec, e0: T) -> usize { 11 | match v.binary_search_by(|e| crate::partial_cmp(e, &e0)) { 12 | Ok(index) | Err(index) => { 13 | v.insert(index, e0); 14 | index 15 | } 16 | } 17 | } 18 | 19 | /// Type stored in each iteration of the algorithm to allow backtracking. 20 | struct Step { 21 | /// The highest value picked by the algorithm at this step. 22 | a: usize, 23 | 24 | /// The second highest value picked by the algorithm at this step. 25 | b: usize, 26 | 27 | /// Whether `a` and `b` must end up in separate parts or not. 28 | separate: bool, 29 | } 30 | 31 | fn ckk_bipart_build(partition: &mut [usize], last_weight: usize, steps: &[Step]) { 32 | partition[last_weight] = 0; 33 | for Step { a, b, separate } in steps.iter().rev() { 34 | if *separate { 35 | partition[*b] = 1 - partition[*a]; 36 | } else { 37 | partition[*b] = partition[*a]; 38 | } 39 | } 40 | } 41 | 42 | fn ckk_bipart_rec( 43 | partition: &mut [usize], 44 | weights: &mut Vec<(T, usize)>, 45 | tolerance: T, 46 | steps: &mut Vec, 47 | ) -> bool 48 | where 49 | T: CkkWeight, 50 | { 51 | debug_assert_ne!(weights.len(), 0); 52 | 53 | if weights.len() == 1 { 54 | let (last_weight, last_id) = weights[0]; 55 | if last_weight <= tolerance { 56 | ckk_bipart_build(partition, last_id, steps); 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | let (a_weight, a_id) = weights.pop().unwrap(); 63 | let (b_weight, b_id) = weights.pop().unwrap(); 64 | 65 | let a_minus_b = (a_weight - b_weight, a_id); 66 | let a_minus_b_idx = add(weights, a_minus_b); 67 | steps.push(Step { 68 | a: a_id, 69 | b: b_id, 70 | separate: true, 71 | }); 72 | if ckk_bipart_rec(partition, weights, tolerance, steps) { 73 | return true; 74 | } 75 | 76 | weights.remove(a_minus_b_idx); 77 | steps.pop(); 78 | let a_plus_b = (a_weight + b_weight, a_id); 79 | let a_plus_b_idx = add(weights, a_plus_b); 80 | steps.push(Step { 81 | a: a_id, 82 | b: b_id, 83 | separate: true, 84 | }); 85 | if ckk_bipart_rec(partition, weights, tolerance, steps) { 86 | return true; 87 | } 88 | 89 | weights.remove(a_plus_b_idx); 90 | steps.pop(); 91 | weights.push((b_weight, b_id)); 92 | weights.push((a_weight, a_id)); 93 | 94 | false 95 | } 96 | 97 | fn ckk_bipart(partition: &mut [usize], weights: I, tolerance: f64) -> Result<(), Error> 98 | where 99 | I: IntoIterator, 100 | T: CkkWeight, 101 | { 102 | let mut weights: Vec<(T, usize)> = weights.into_iter().zip(0..).collect(); 103 | if weights.len() != partition.len() { 104 | return Err(Error::InputLenMismatch { 105 | expected: partition.len(), 106 | actual: weights.len(), 107 | }); 108 | } 109 | if weights.is_empty() { 110 | return Ok(()); 111 | } 112 | weights.sort_unstable_by(crate::partial_cmp); 113 | 114 | let sum: T = weights.iter().map(|(weight, _idx)| *weight).sum(); 115 | let tolerance = T::from_f64(sum.to_f64().unwrap() * tolerance).unwrap(); 116 | 117 | let mut steps = Vec::new(); 118 | 119 | if ckk_bipart_rec(partition, &mut weights, tolerance, &mut steps) { 120 | Ok(()) 121 | } else { 122 | Err(Error::NotFound) 123 | } 124 | } 125 | 126 | /// # Complete Karmarkar-Karp algorithm 127 | /// 128 | /// Extension of the 129 | /// [Karmarkar-Karp number partitioning algorithm][crate::KarmarkarKarp] that 130 | /// explores all possible solutions until the `tolerance` constraint is 131 | /// respected. 132 | /// 133 | /// This algorithm is currently implemented in the bi-partitioning case only. 134 | /// 135 | /// # Example 136 | /// 137 | /// ```rust 138 | /// # fn main() -> Result<(), coupe::Error> { 139 | /// use coupe::Partition as _; 140 | /// 141 | /// let weights: [i32; 4] = [3, 5, 3, 9]; 142 | /// let mut partition = [0; 4]; 143 | /// 144 | /// coupe::CompleteKarmarkarKarp { tolerance: 0.1 } 145 | /// .partition(&mut partition, weights)?; 146 | /// # Ok(()) 147 | /// # } 148 | /// ``` 149 | /// 150 | /// # Reference 151 | /// 152 | /// Korf, Richard E., 1998. A complete anytime algorithm for number 153 | /// partitioning. *Artificial Intelligence*, 106(2):181 – 203. 154 | /// . 155 | #[derive(Clone, Copy, Debug)] 156 | pub struct CompleteKarmarkarKarp { 157 | /// Constraint on the normalized imbalance between the two parts. 158 | pub tolerance: f64, 159 | } 160 | 161 | /// Trait alias for values accepted as weights by [CompleteKarmarkarKarp]. 162 | pub trait CkkWeight 163 | where 164 | Self: Copy + Sum + PartialOrd + FromPrimitive + ToPrimitive, 165 | Self: Add + Sub, 166 | { 167 | } 168 | 169 | impl CkkWeight for T 170 | where 171 | Self: Copy + Sum + PartialOrd + FromPrimitive + ToPrimitive, 172 | Self: Add + Sub, 173 | { 174 | } 175 | 176 | impl crate::Partition for CompleteKarmarkarKarp 177 | where 178 | W: IntoIterator, 179 | W::Item: CkkWeight, 180 | { 181 | type Metadata = (); 182 | type Error = Error; 183 | 184 | fn partition( 185 | &mut self, 186 | part_ids: &mut [usize], 187 | weights: W, 188 | ) -> Result { 189 | ckk_bipart(part_ids, weights, self.tolerance) 190 | } 191 | } 192 | 193 | // TODO tests 194 | -------------------------------------------------------------------------------- /src/algorithms/graph_growth.rs: -------------------------------------------------------------------------------- 1 | use rand::seq::SliceRandom; 2 | use sprs::CsMatView; 3 | 4 | fn graph_growth( 5 | initial_ids: &mut [usize], 6 | weights: &[f64], 7 | adjacency: CsMatView<'_, f64>, 8 | num_parts: usize, 9 | ) { 10 | let (shape_x, shape_y) = adjacency.shape(); 11 | assert_eq!(shape_x, shape_y); 12 | assert_eq!(weights.len(), shape_x); 13 | 14 | // let total_weight = weights.iter().sum::(); 15 | // let weight_per_part = total_weight / num_parts as f64; 16 | let max_expansion_per_pass = 20; 17 | 18 | let mut rng = rand::thread_rng(); 19 | 20 | // select two random nodes to grow from 21 | let indices = (0..weights.len()).collect::>(); 22 | let indices = indices 23 | .as_slice() 24 | .choose_multiple(&mut rng, num_parts) 25 | .cloned() 26 | .collect::>(); 27 | 28 | // tracks if each node has already been assigned to a partition or not 29 | let mut assigned = vec![false; weights.len()]; 30 | let unique_ids = indices.clone(); 31 | 32 | // assign initial nodes 33 | for (idx, id) in indices.iter().zip(unique_ids.iter()) { 34 | initial_ids[*idx] = *id; 35 | assigned[*idx] = true; 36 | } 37 | 38 | let mut remaining_nodes = weights.len() - num_parts; 39 | 40 | while remaining_nodes > 0 { 41 | let mut num_expansion = vec![0; unique_ids.len()]; 42 | 43 | for (i, row) in adjacency.outer_iterator().enumerate() { 44 | let id = initial_ids[i]; 45 | if assigned[i] { 46 | for (j, _w) in row.iter() { 47 | if !assigned[j] && initial_ids[j] != id { 48 | let idx = unique_ids.iter().position(|el| *el == id).unwrap(); 49 | if num_expansion[idx] > max_expansion_per_pass { 50 | break; 51 | } 52 | num_expansion[idx] += 1; 53 | initial_ids[j] = id; 54 | assigned[j] = true; 55 | remaining_nodes -= 1; 56 | break; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Graph Growth algorithm 65 | /// 66 | /// A topologic algorithm that generates a partition from a topologic mesh. 67 | /// Given a number k of parts, the algorithm selects k nodes randomly and assigns them to a different part. 68 | /// Then, at each iteration, each part is expanded to neighbor nodes that are not yet assigned to a part 69 | /// 70 | /// # Example 71 | /// 72 | /// ```rust 73 | /// # fn main() -> Result<(), std::convert::Infallible> { 74 | /// use coupe::Partition as _; 75 | /// use coupe::Point2D; 76 | /// use sprs::CsMat; 77 | /// 78 | /// // +--+--+--+ 79 | /// // | | | | 80 | /// // +--+--+--+ 81 | /// 82 | /// let weights = [1.0; 8]; 83 | /// let mut partition = [0; 8]; 84 | /// 85 | /// let mut adjacency = CsMat::empty(sprs::CSR, 8); 86 | /// adjacency.reserve_outer_dim(8); 87 | /// eprintln!("shape: {:?}", adjacency.shape()); 88 | /// adjacency.insert(0, 1, 1.); 89 | /// adjacency.insert(1, 2, 1.); 90 | /// adjacency.insert(2, 3, 1.); 91 | /// adjacency.insert(4, 5, 1.); 92 | /// adjacency.insert(5, 6, 1.); 93 | /// adjacency.insert(6, 7, 1.); 94 | /// adjacency.insert(0, 4, 1.); 95 | /// adjacency.insert(1, 5, 1.); 96 | /// adjacency.insert(2, 6, 1.); 97 | /// adjacency.insert(3, 7, 1.); 98 | /// 99 | /// // symmetry 100 | /// adjacency.insert(1, 0, 1.); 101 | /// adjacency.insert(2, 1, 1.); 102 | /// adjacency.insert(3, 2, 1.); 103 | /// adjacency.insert(5, 4, 1.); 104 | /// adjacency.insert(6, 5, 1.); 105 | /// adjacency.insert(7, 6, 1.); 106 | /// adjacency.insert(4, 0, 1.); 107 | /// adjacency.insert(5, 1, 1.); 108 | /// adjacency.insert(6, 2, 1.); 109 | /// adjacency.insert(7, 3, 1.); 110 | /// 111 | /// coupe::GraphGrowth { part_count: 2 } 112 | /// .partition(&mut partition, (adjacency.view(), &weights))?; 113 | /// # Ok(()) 114 | /// # } 115 | /// ``` 116 | #[derive(Debug, Clone, Copy)] 117 | pub struct GraphGrowth { 118 | pub part_count: usize, 119 | } 120 | 121 | impl<'a, W> crate::Partition<(CsMatView<'a, f64>, W)> for GraphGrowth 122 | where 123 | W: AsRef<[f64]>, 124 | { 125 | type Metadata = (); 126 | type Error = std::convert::Infallible; 127 | 128 | fn partition( 129 | &mut self, 130 | part_ids: &mut [usize], 131 | (adjacency, weights): (CsMatView<'_, f64>, W), 132 | ) -> Result { 133 | graph_growth( 134 | part_ids, 135 | weights.as_ref(), 136 | adjacency.view(), 137 | self.part_count, 138 | ); 139 | Ok(()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/algorithms/greedy.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use num_traits::Zero; 3 | use std::ops::AddAssign; 4 | 5 | /// Implementation of the greedy algorithm. 6 | fn greedy( 7 | partition: &mut [usize], 8 | weights: impl IntoIterator, 9 | part_count: usize, 10 | ) -> Result<(), Error> 11 | where 12 | T: GreedyWeight, 13 | { 14 | if part_count < 2 { 15 | partition.fill(0); 16 | return Ok(()); 17 | } 18 | 19 | // Initialization: make the partition and record the weight of each part in another vector. 20 | let mut weights: Vec<_> = weights 21 | .into_iter() 22 | .zip(0..) // Keep track of the weights' indicies 23 | .collect(); 24 | 25 | if weights.len() != partition.len() { 26 | return Err(Error::InputLenMismatch { 27 | expected: partition.len(), 28 | actual: weights.len(), 29 | }); 30 | } 31 | 32 | weights.sort_unstable_by(crate::partial_cmp); 33 | let mut part_weights = vec![T::zero(); part_count]; 34 | 35 | // Put each weight in the lightweightest part. 36 | for (weight, weight_id) in weights.into_iter().rev() { 37 | let (min_part_weight_idx, _min_part_weight) = part_weights 38 | .iter() 39 | .enumerate() 40 | .min_by(|(_, part_weight0), (_, part_weight1)| { 41 | crate::partial_cmp(part_weight0, part_weight1) 42 | }) 43 | .unwrap(); // Will not panic because !part_weights.is_empty() 44 | partition[weight_id] = min_part_weight_idx; 45 | part_weights[min_part_weight_idx] += weight; 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | /// Trait alias for values accepted as weights by [Greedy]. 52 | pub trait GreedyWeight 53 | where 54 | Self: PartialOrd + Zero + Clone + AddAssign, 55 | { 56 | } 57 | 58 | impl GreedyWeight for T where Self: PartialOrd + Zero + Clone + AddAssign {} 59 | 60 | /// # Greedy number partitioning algorithm 61 | /// 62 | /// Greedily assign weights to each part. 63 | /// 64 | /// # Example 65 | /// 66 | /// ```rust 67 | /// # fn main() -> Result<(), coupe::Error> { 68 | /// use coupe::Partition as _; 69 | /// 70 | /// let weights = [3.2, 6.8, 10.0, 7.5]; 71 | /// let mut partition = [0; 4]; 72 | /// 73 | /// coupe::Greedy { part_count: 2 } 74 | /// .partition(&mut partition, weights)?; 75 | /// # Ok(()) 76 | /// # } 77 | /// ``` 78 | /// 79 | /// # Reference 80 | /// 81 | /// Horowitz, Ellis and Sahni, Sartaj, 1974. Computing partitions with 82 | /// applications to the knapsack problem. *J. ACM*, 21(2):277–292. 83 | /// . 84 | #[derive(Clone, Copy, Debug)] 85 | pub struct Greedy { 86 | pub part_count: usize, 87 | } 88 | 89 | impl crate::Partition for Greedy 90 | where 91 | W: IntoIterator, 92 | W::Item: GreedyWeight, 93 | { 94 | type Metadata = (); 95 | type Error = Error; 96 | 97 | fn partition( 98 | &mut self, 99 | part_ids: &mut [usize], 100 | weights: W, 101 | ) -> Result { 102 | greedy(part_ids, weights, self.part_count) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/algorithms/kk.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use num_traits::Zero; 3 | use std::collections::BinaryHeap; 4 | use std::ops::Sub; 5 | use std::ops::SubAssign; 6 | 7 | /// Implementation of the Karmarkar-Karp algorithm (bi-partitioning case). 8 | /// 9 | /// # Differences with the k-partitioning implementation 10 | /// 11 | /// This function has better performance than [kk] called with `num_parts == 2`. 12 | fn kk_bipart(partition: &mut [usize], weights: impl Iterator) 13 | where 14 | T: Ord + Sub, 15 | { 16 | let mut weights: BinaryHeap<(T, usize)> = weights 17 | .into_iter() 18 | .zip(0..) // Keep track of the weights' indicies 19 | .collect(); 20 | 21 | // Core algorithm: find the imbalance of the partition. 22 | // "opposites" is built in this loop to backtrack the solution. It tracks weights that must end 23 | // up in opposite parts. 24 | 25 | let mut opposites = Vec::with_capacity(weights.len()); 26 | while 2 <= weights.len() { 27 | let (a_weight, a_id) = weights.pop().unwrap(); 28 | let (b_weight, b_id) = weights.pop().unwrap(); 29 | 30 | opposites.push((a_id, b_id)); 31 | 32 | // put "a-b" in the same part as "a". 33 | weights.push((a_weight - b_weight, a_id)); 34 | } 35 | 36 | // Backtracking. 37 | // We use an array that maps weight IDs to their part (true or false) and their weight value. 38 | // It is initialized with the last element of "weights" (which is the imbalance of the 39 | // partition). 40 | 41 | let (_imbalance, last_diff) = weights.pop().unwrap(); 42 | partition[last_diff] = 0; 43 | for (a, b) in opposites.into_iter().rev() { 44 | // put "b" in the opposite part of "a" (which is were "a-b" was put). 45 | partition[b] = 1 - partition[a]; 46 | } 47 | } 48 | 49 | /// Implementation of the Karmarkar-Karp algorithm (general case). 50 | fn kk(partition: &mut [usize], weights: I, num_parts: usize) 51 | where 52 | T: KkWeight, 53 | I: Iterator + ExactSizeIterator, 54 | { 55 | // Initialize "m", a "k*num_weights" matrix whose first column is "weights". 56 | let weight_count = weights.len(); 57 | let mut m: BinaryHeap> = weights 58 | .zip(0..) 59 | .map(|(w, id)| { 60 | let mut v: Vec<(T, usize)> = (0..num_parts) 61 | .map(|p| (T::zero(), weight_count * p + id)) 62 | .collect(); 63 | v[0].0 = w; 64 | v 65 | }) 66 | .collect(); 67 | 68 | // Core algorithm: same as the bi-partitioning case. However, instead of putting the two 69 | // largest weights in two different parts, the largest weight of each row is put into the same 70 | // part as the smallest one, and so on. 71 | 72 | let mut opposites = Vec::with_capacity(weight_count); 73 | while 2 <= m.len() { 74 | let a = m.pop().unwrap(); 75 | let b = m.pop().unwrap(); 76 | 77 | // tuples = [ (a0, bn), (a1, bn-1), ... ] 78 | let tuples: Vec<_> = a 79 | .iter() 80 | .zip(b.iter().rev()) 81 | .map(|((_, a_id), (_, b_id))| (*a_id, *b_id)) 82 | .collect(); 83 | 84 | // e = [ a0 + bn, a1 + bn-1, ... ] 85 | let mut e: Vec<_> = a 86 | .iter() 87 | .zip(b.iter().rev()) 88 | .map(|(a, b)| (a.0 + b.0, a.1)) 89 | .collect(); 90 | e.sort_unstable_by(|ei, ej| T::cmp(&ej.0, &ei.0)); 91 | 92 | let emin = e[e.len() - 1].0; 93 | for ei in &mut e { 94 | ei.0 -= emin; 95 | } 96 | opposites.push(tuples); 97 | m.push(e); 98 | } 99 | 100 | // Backtracking. Same as the bi-partitioning case. 101 | 102 | // parts = [ [m0i] for m0i in m[0] ] 103 | let mut parts: Vec = vec![0; num_parts * weight_count]; 104 | let imbalance = m.pop().unwrap(); // first and last element of "m". 105 | for (i, w) in imbalance.into_iter().enumerate() { 106 | // Put each remaining element in a different part. 107 | parts[w.1] = i; 108 | } 109 | for tuples in opposites.into_iter().rev() { 110 | for (a, b) in tuples { 111 | parts[b] = parts[a]; 112 | } 113 | } 114 | 115 | parts.truncate(partition.len()); 116 | partition.copy_from_slice(&parts); 117 | } 118 | 119 | /// Trait alias for values accepted as weights by [KarmarkarKarp]. 120 | pub trait KkWeight 121 | where 122 | Self: Zero + Ord + Sub + SubAssign + Copy, 123 | { 124 | } 125 | 126 | impl KkWeight for T where Self: Zero + Ord + Sub + SubAssign + Copy {} 127 | 128 | /// # Karmarkar-Karp algorithm 129 | /// 130 | /// Also called the Largest Differencing Method. 131 | /// 132 | /// Similar to the [greedy number partitioning algorithm][crate::Greedy], but 133 | /// instead of puting the highest weight into the lowest part, it puts the two 134 | /// highest weights in two different parts and keeps their difference. 135 | /// 136 | /// # Example 137 | /// 138 | /// ```rust 139 | /// # fn main() -> Result<(), coupe::Error> { 140 | /// use coupe::Partition as _; 141 | /// 142 | /// let weights = [3, 5, 3, 9]; 143 | /// let mut partition = [0; 4]; 144 | /// 145 | /// coupe::KarmarkarKarp { part_count: 3 } 146 | /// .partition(&mut partition, weights)?; 147 | /// # Ok(()) 148 | /// # } 149 | /// ``` 150 | /// 151 | /// # Reference 152 | /// 153 | /// Karmarkar, Narenda and Karp, Richard M., 1983. The differencing method of 154 | /// set partitioning. Technical report, Berkeley, CA, USA. 155 | #[derive(Clone, Copy, Debug)] 156 | pub struct KarmarkarKarp { 157 | pub part_count: usize, 158 | } 159 | 160 | impl crate::Partition for KarmarkarKarp 161 | where 162 | W: IntoIterator, 163 | W::IntoIter: ExactSizeIterator, 164 | W::Item: KkWeight, 165 | { 166 | type Metadata = (); 167 | type Error = Error; 168 | 169 | fn partition( 170 | &mut self, 171 | part_ids: &mut [usize], 172 | weights: W, 173 | ) -> Result { 174 | if self.part_count < 2 || part_ids.len() < 2 { 175 | return Ok(()); 176 | } 177 | let weights = weights.into_iter(); 178 | if weights.len() != part_ids.len() { 179 | return Err(Error::InputLenMismatch { 180 | expected: part_ids.len(), 181 | actual: weights.len(), 182 | }); 183 | } 184 | if self.part_count == 2 { 185 | // The bi-partitioning is a special case that can be handled faster 186 | // than the general case. 187 | kk_bipart(part_ids, weights); 188 | } else { 189 | kk(part_ids, weights, self.part_count); 190 | } 191 | Ok(()) 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | use super::*; 198 | use proptest::prelude::*; 199 | 200 | proptest!( 201 | #[test] 202 | fn test_kk_bipart( 203 | weights in (2..200_usize).prop_flat_map(|weight_count| { 204 | prop::collection::vec(0..1_000_000_u64, weight_count) 205 | }) 206 | ) { 207 | let mut partition = vec![0; weights.len()]; 208 | let mut partition_bipart = vec![0; weights.len()]; 209 | kk(&mut partition, weights.iter().cloned(), 2); 210 | kk_bipart(&mut partition_bipart, weights.iter().cloned()); 211 | prop_assert_eq!(partition, partition_bipart); 212 | } 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /src/algorithms/vn/first.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use itertools::Itertools as _; 3 | use rayon::iter::IntoParallelRefIterator as _; 4 | use rayon::iter::ParallelIterator as _; 5 | use std::iter::Sum; 6 | use std::ops::AddAssign; 7 | use std::ops::Sub; 8 | 9 | fn vn_first(partition: &mut [usize], weights: &[T], num_parts: usize) -> Result 10 | where 11 | T: VnFirstWeight, 12 | { 13 | if weights.len() != partition.len() { 14 | return Err(Error::InputLenMismatch { 15 | expected: partition.len(), 16 | actual: weights.len(), 17 | }); 18 | } 19 | debug_assert_ne!(num_parts, 0); 20 | if weights.is_empty() || num_parts < 2 { 21 | return Ok(0); 22 | } 23 | 24 | let mut part_loads = 25 | crate::imbalance::compute_parts_load(partition, num_parts, weights.par_iter().cloned()); 26 | let total_weight: T = part_loads.iter().cloned().sum(); 27 | if total_weight.is_zero() { 28 | return Ok(0); 29 | } 30 | 31 | let (min_load, mut max_load) = part_loads.iter().cloned().minmax().into_option().unwrap(); 32 | let mut imbalance = max_load - min_load; 33 | 34 | let mut i = weights.len(); 35 | let mut i_last = 0; 36 | let mut algo_iterations = 0; 37 | while i != i_last { 38 | i = (i + 1) % weights.len(); 39 | 40 | // loop through the weights. 41 | let p = partition[i]; 42 | 43 | if part_loads[p] < max_load { 44 | // weight #i is not in the heaviest partition, and thus the move 45 | // will not reduce the max imbalance. 46 | continue; 47 | } 48 | 49 | for q in 0..num_parts { 50 | // loop through the parts. 51 | if p == q { 52 | // weight #i is already in partition #q. 53 | continue; 54 | } 55 | 56 | part_loads[p] = part_loads[p] - weights[i]; 57 | part_loads[q] += weights[i]; 58 | let (new_min_load, new_max_load) = 59 | part_loads.iter().cloned().minmax().into_option().unwrap(); 60 | let new_imbalance = new_max_load - new_min_load; 61 | if imbalance < new_imbalance { 62 | // The move does not decrease the partition imbalance. 63 | part_loads[p] += weights[i]; 64 | part_loads[q] = part_loads[q] - weights[i]; 65 | continue; 66 | } 67 | imbalance = new_imbalance; 68 | max_load = new_max_load; 69 | partition[i] = q; 70 | i_last = i; 71 | } 72 | 73 | algo_iterations += 1; 74 | } 75 | 76 | Ok(algo_iterations) 77 | } 78 | 79 | /// Trait alias for values accepted as weights by [VnFirst]. 80 | pub trait VnFirstWeight 81 | where 82 | Self: Copy + Send + Sync, 83 | Self: Sum + PartialOrd + num_traits::Zero + num_traits::One, 84 | Self: Sub + AddAssign, 85 | { 86 | } 87 | 88 | impl VnFirstWeight for T 89 | where 90 | Self: Copy + Send + Sync, 91 | Self: Sum + PartialOrd + num_traits::Zero + num_traits::One, 92 | Self: Sub + AddAssign, 93 | { 94 | } 95 | 96 | /// # Descent Vector-of-Numbers algorithm 97 | /// 98 | /// This algorithm moves weights from parts to parts whenever it decreases the 99 | /// imbalance. See also its [greedy version][crate::VnBest]. 100 | /// 101 | /// # Example 102 | /// 103 | /// ```rust 104 | /// # fn main() -> Result<(), Box> { 105 | /// use coupe::Partition as _; 106 | /// use rand; 107 | /// 108 | /// let part_count = 2; 109 | /// let mut partition = [0; 4]; 110 | /// let weights = [4, 6, 2, 9]; 111 | /// 112 | /// coupe::Random { rng: rand::thread_rng(), part_count } 113 | /// .partition(&mut partition, ())?; 114 | /// coupe::VnFirst 115 | /// .partition(&mut partition, &weights)?; 116 | /// # Ok(()) 117 | /// # } 118 | /// ``` 119 | /// 120 | /// # Reference 121 | /// 122 | /// Remi Barat. Load Balancing of Multi-physics Simulation by Multi-criteria 123 | /// Graph Partitioning. Other [cs.OH]. Université de Bordeaux, 2017. English. 124 | /// NNT : 2017BORD0961. tel-01713977 125 | #[derive(Clone, Copy, Debug)] 126 | pub struct VnFirst; 127 | 128 | impl<'a, W> crate::Partition<&'a [W]> for VnFirst 129 | where 130 | W: VnFirstWeight, 131 | { 132 | type Metadata = usize; 133 | type Error = Error; 134 | 135 | fn partition( 136 | &mut self, 137 | part_ids: &mut [usize], 138 | weights: &'a [W], 139 | ) -> Result { 140 | let part_count = 1 + *part_ids.par_iter().max().unwrap_or(&0); 141 | if part_count < 2 { 142 | return Ok(0); 143 | } 144 | vn_first(part_ids, weights, part_count) 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use proptest::prelude::*; 151 | 152 | use super::*; 153 | use crate::*; 154 | 155 | #[test] 156 | fn small_mono() { 157 | const W: [i32; 6] = [1, 2, 3, 4, 5, 6]; 158 | let mut part = [0; W.len()]; 159 | 160 | vn_first(&mut part, &W, 1).unwrap(); 161 | let imb_ini = imbalance::imbalance(2, &part, W); 162 | vn_first(&mut part, &W, 2).unwrap(); 163 | let imb_end = imbalance::imbalance(2, &part, W); 164 | assert!(imb_end <= imb_ini); 165 | println!("imbalance : {} < {}", imb_end, imb_ini); 166 | } 167 | 168 | proptest!( 169 | /// vn_first should always improve balance ! 170 | #[test] 171 | fn improve_mono( 172 | (weights, mut partition) in 173 | (2..200_usize).prop_flat_map(|num_weights| { 174 | (prop::collection::vec(1..1000_u64, num_weights), 175 | prop::collection::vec(0..2_usize, num_weights)) 176 | }) 177 | ) { 178 | let imb_ini = imbalance::max_imbalance(2, &partition, weights.par_iter().cloned()); 179 | vn_first(&mut partition, &weights, 2).unwrap(); 180 | let imb_end = imbalance::max_imbalance(2, &partition, weights.par_iter().cloned()); 181 | // Not sure if it is true for max_imbalance (i.e. weighter - lighter) 182 | proptest::prop_assert!(imb_end <= imb_ini); 183 | } 184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /src/algorithms/vn/mod.rs: -------------------------------------------------------------------------------- 1 | mod best; 2 | mod first; 3 | 4 | pub use best::VnBest; 5 | pub use best::VnBestWeight; 6 | pub use first::VnFirst; 7 | pub use first::VnFirstWeight; 8 | -------------------------------------------------------------------------------- /src/average.rs: -------------------------------------------------------------------------------- 1 | /// Compute the average of two values without overflow. 2 | pub trait Average { 3 | fn avg(a: Self, b: Self) -> Self; 4 | } 5 | 6 | impl Average for f64 { 7 | fn avg(a: Self, b: Self) -> Self { 8 | (a + b) / 2.0 9 | } 10 | } 11 | 12 | macro_rules! impl_int { 13 | ( $t:ty ) => { 14 | impl Average for $t { 15 | /// Ref: 16 | fn avg(a: Self, b: Self) -> Self { 17 | (a & b) + (a ^ b) / 2 18 | } 19 | } 20 | }; 21 | } 22 | 23 | impl_int!(i64); 24 | impl_int!(u64); 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | 30 | #[test] 31 | fn test_int() { 32 | assert_eq!(i64::avg(i64::MAX, i64::MAX - 2), i64::MAX - 1); 33 | assert_eq!(u64::avg(u64::MAX, u64::MAX - 2), u64::MAX - 1); 34 | } 35 | 36 | #[test] 37 | fn test_float() { 38 | assert_eq!(f64::avg(1e308, 1e307), 5.5e307); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/defer.rs: -------------------------------------------------------------------------------- 1 | struct Defer(Option) 2 | where 3 | F: FnOnce(); 4 | 5 | impl Drop for Defer 6 | where 7 | F: FnOnce(), 8 | { 9 | fn drop(&mut self) { 10 | if let Some(f) = self.0.take() { 11 | f(); 12 | } 13 | } 14 | } 15 | 16 | /// Call `f` on drop. 17 | pub fn defer(f: impl FnOnce()) -> impl Drop { 18 | Defer(Some(f)) 19 | } 20 | -------------------------------------------------------------------------------- /src/imbalance.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use num_traits::FromPrimitive; 3 | use num_traits::ToPrimitive; 4 | use num_traits::Zero; 5 | use rayon::iter::IndexedParallelIterator; 6 | use rayon::iter::IntoParallelIterator; 7 | use rayon::iter::IntoParallelRefIterator as _; 8 | use rayon::iter::ParallelIterator as _; 9 | use std::iter::Sum; 10 | use std::ops::AddAssign; 11 | use std::ops::Div; 12 | use std::ops::Sub; 13 | 14 | pub fn compute_parts_load(partition: &[usize], num_parts: usize, weights: W) -> Vec 15 | where 16 | W: IntoParallelIterator, 17 | W::Iter: IndexedParallelIterator, 18 | W::Item: Zero + Clone + AddAssign, 19 | { 20 | debug_assert!(*partition.par_iter().max().unwrap_or(&0) < num_parts); 21 | 22 | partition 23 | .par_iter() 24 | .zip(weights) 25 | .fold( 26 | || vec![W::Item::zero(); num_parts], 27 | |mut acc, (&part, w)| { 28 | acc[part] += w; 29 | acc 30 | }, 31 | ) 32 | .reduce_with(|mut weights0, weights1| { 33 | for (w0, w1) in weights0.iter_mut().zip(weights1) { 34 | *w0 += w1; 35 | } 36 | weights0 37 | }) 38 | .unwrap_or_else(|| vec![W::Item::zero(); num_parts]) 39 | } 40 | 41 | /// Compute the imbalance of the given partition. 42 | pub fn imbalance(num_parts: usize, partition: &[usize], weights: W) -> f64 43 | where 44 | W: IntoParallelIterator, 45 | W::Iter: IndexedParallelIterator, 46 | W::Item: Clone + PartialOrd + PartialEq, 47 | W::Item: Zero + FromPrimitive + ToPrimitive, 48 | W::Item: AddAssign + Div + Sub + Sum, 49 | { 50 | let weights = weights.into_par_iter(); 51 | debug_assert_eq!(partition.len(), weights.len()); 52 | 53 | if num_parts == 0 { 54 | // Avoid a division by zero. 55 | return 0.0; 56 | } 57 | 58 | let part_loads = compute_parts_load(partition, num_parts, weights); 59 | let total_weight: W::Item = part_loads.iter().cloned().sum(); 60 | 61 | let ideal_part_weight = total_weight.to_f64().unwrap() / num_parts.to_f64().unwrap(); 62 | if ideal_part_weight == 0.0 { 63 | // Avoid divisions by zero. 64 | return 0.0; 65 | } 66 | 67 | part_loads 68 | .into_iter() 69 | .map(|part_weight| { 70 | let part_weight: f64 = part_weight.to_f64().unwrap(); 71 | (part_weight - ideal_part_weight) / ideal_part_weight 72 | }) 73 | .minmax() 74 | .into_option() 75 | .unwrap_or((0.0, 0.0)) 76 | .1 77 | } 78 | 79 | pub fn imbalance_target(targets: &[W::Item], partition: &[usize], weights: W) -> W::Item 80 | where 81 | W: IntoParallelIterator, 82 | W::Iter: IndexedParallelIterator, 83 | W::Item: Zero + Sum + Copy + AddAssign + Sub + PartialOrd, 84 | { 85 | let num_parts = targets.len(); 86 | compute_parts_load(partition, num_parts, weights) 87 | .iter() 88 | .zip(targets) 89 | .map(|(x, t)| *x - *t) 90 | .max_by(|imb0, imb1| W::Item::partial_cmp(imb0, imb1).unwrap()) 91 | .unwrap_or_else(W::Item::zero) 92 | } 93 | 94 | pub fn max_imbalance(num_parts: usize, partition: &[usize], weights: W) -> W::Item 95 | where 96 | W: IntoParallelIterator, 97 | W::Iter: IndexedParallelIterator, 98 | W::Item: Zero + Sum + Copy + AddAssign + Sub + PartialOrd, 99 | { 100 | compute_parts_load(partition, num_parts, weights) 101 | .iter() 102 | .minmax() 103 | .into_option() 104 | .map_or_else(W::Item::zero, |m| *m.1 - *m.0) 105 | } 106 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A mesh partitioning library that implements multithreaded, composable geometric algorithms. 2 | //! 3 | //! # Crate Layout 4 | //! 5 | //! Coupe exposes a [`Partition`] trait, which is in turn implemented by 6 | //! algorithms. See its documentation for more details. The trait is generic around its input, which means algorithms 7 | //! can partition different type of collections (e.g. 2D and 3D meshes). 8 | //! 9 | //! # Available algorithms 10 | //! 11 | //! ## Partitioner algorithms 12 | //! 13 | //! - Space filling curves: 14 | //! + [Z-curve][ZCurve] 15 | //! + [Hilbert curve][HilbertCurve] 16 | //! - [Recursive Coordinate Bisection][Rcb] 17 | //! - [Recursive Inertial Bisection][Rib] 18 | //! - [Multi jagged][MultiJagged] 19 | //! - Number partitioning: 20 | //! + [Greedy][Greedy] 21 | //! + [Karmarkar-Karp][KarmarkarKarp] and its [complete][CompleteKarmarkarKarp] version 22 | //! 23 | //! ## Partition improving algorithms 24 | //! 25 | //! - [K-means][KMeans] 26 | //! - Number partitioning: 27 | //! + [VN-Best][VnBest] 28 | //! + [VN-First][VnFirst] 29 | //! - [Fiduccia-Mattheyses][FiducciaMattheyses] 30 | //! - [Kernighan-Lin][KernighanLin] 31 | 32 | #![cfg_attr(feature = "avx512", feature(stdsimd))] 33 | #![warn( 34 | missing_copy_implementations, 35 | missing_debug_implementations, 36 | rust_2018_idioms 37 | )] 38 | 39 | mod algorithms; 40 | mod average; 41 | mod cartesian; 42 | mod defer; 43 | mod geometry; 44 | pub mod imbalance; 45 | mod nextafter; 46 | mod real; 47 | mod topology; 48 | mod work_share; 49 | 50 | pub use crate::algorithms::*; 51 | pub use crate::average::Average; 52 | pub use crate::cartesian::*; 53 | pub use crate::geometry::BoundingBox; 54 | pub use crate::geometry::{Point2D, Point3D, PointND}; 55 | pub use crate::nextafter::nextafter; 56 | pub use crate::real::Real; 57 | pub use crate::topology::Topology; 58 | 59 | pub use nalgebra; 60 | pub use num_traits; 61 | pub use rayon; 62 | pub use sprs; 63 | 64 | use std::cmp::Ordering; 65 | use std::mem; 66 | use std::sync::atomic::AtomicUsize; 67 | 68 | /// The `Partition` trait allows for partitioning data. 69 | /// 70 | /// Partitioning algorithms implement this trait. 71 | /// 72 | /// The generic argument `M` defines the input of the algorithms (e.g. an 73 | /// adjacency matrix or a 2D set of points). 74 | /// 75 | /// The input partition must be of the correct size and its contents may or may 76 | /// not be used by the algorithms. 77 | pub trait Partition { 78 | /// Diagnostic data returned for a specific run of the algorithm. 79 | type Metadata; 80 | 81 | /// Error details, should the algorithm fail to run. 82 | type Error; 83 | 84 | /// Partition the given data and output the part ID of each element in 85 | /// `part_ids`. 86 | /// 87 | /// Part IDs must be contiguous and start from zero, meaning the number of 88 | /// parts is one plus the maximum of `part_ids`. If a lower ID does not 89 | /// appear in the array, the part is assumed to be empty. 90 | fn partition(&mut self, part_ids: &mut [usize], data: M) 91 | -> Result; 92 | } 93 | 94 | fn partial_cmp(a: &W, b: &W) -> Ordering 95 | where 96 | W: PartialOrd, 97 | { 98 | if a < b { 99 | Ordering::Less 100 | } else { 101 | Ordering::Greater 102 | } 103 | } 104 | 105 | /// Transmute a mutable slice of [`usize`] into an immutable slice of 106 | /// [`AtomicUsize`]. 107 | /// 108 | /// # Panics 109 | /// 110 | /// Panics on platforms wher `usize` and `AtomicUsize` do not have the same 111 | /// byte representation (size and alignment). 112 | fn as_atomic(p: &mut [usize]) -> &[AtomicUsize] { 113 | assert_eq!(mem::size_of::(), mem::size_of::()); 114 | assert_eq!(mem::align_of::(), mem::align_of::()); 115 | 116 | unsafe { 117 | // While we could use [slice::align_to], their doc says: 118 | // 119 | // > The method may make the middle slice the greatest length possible 120 | // > for a given type and input slice, but only your algorithm’s 121 | // > performance should depend on that, not its correctness. 122 | // 123 | // So we have to use [mem::transmute] to ensure all the slice is 124 | // converted. 125 | mem::transmute::<&mut [usize], &[AtomicUsize]>(p) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let x = std::env::args().nth(1).unwrap().parse().unwrap(); 3 | let y = std::env::args().nth(2).unwrap().parse().unwrap(); 4 | let iter = std::env::args() 5 | .nth(3) 6 | .unwrap_or_else(|| String::from("12")) 7 | .parse() 8 | .unwrap(); 9 | eprintln!("grid size: ({x},{y}); rcb iters: {iter}"); 10 | let grid = coupe::Grid::new_2d(x, y); 11 | let n = usize::from(x) * usize::from(y); 12 | let weights: Vec = (0..n).map(|i| i as f64).collect(); 13 | let mut partition = vec![0; n]; 14 | 15 | let domain = ittapi::Domain::new("MyIncredibleDomain"); 16 | let before = std::time::Instant::now(); 17 | let task = ittapi::Task::begin(&domain, "MyIncredibleTask"); 18 | grid.rcb(&mut partition, &weights, iter); 19 | std::mem::drop(task); 20 | eprintln!("time: {:?}", before.elapsed()); 21 | 22 | let i = usize::from(x); 23 | eprint!("partition[{}] = {}\r", i, partition[i]); 24 | } 25 | -------------------------------------------------------------------------------- /src/nextafter.rs: -------------------------------------------------------------------------------- 1 | // Adapted from the float_next_after crate, under MIT. 2 | // See 3 | 4 | pub fn nextafter(from: f64, to: f64) -> f64 { 5 | if from == to { 6 | to 7 | } else if from.is_nan() || to.is_nan() { 8 | f64::NAN 9 | } else if from >= f64::INFINITY { 10 | f64::INFINITY 11 | } else if from <= f64::NEG_INFINITY { 12 | f64::NEG_INFINITY 13 | } else if from == 0_f64 { 14 | f64::copysign(f64::from_bits(1), to) 15 | } else { 16 | let ret = if (from < to) == (0_f64 < from) { 17 | f64::from_bits(from.to_bits() + 1) 18 | } else { 19 | f64::from_bits(from.to_bits() - 1) 20 | }; 21 | if ret == 0_f64 { 22 | f64::copysign(ret, from) 23 | } else { 24 | ret 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | 33 | const POS_INF: f64 = std::f64::INFINITY; 34 | const NEG_INF: f64 = std::f64::NEG_INFINITY; 35 | const POS_ZERO: f64 = 0.0; 36 | const NEG_ZERO: f64 = -0.0; 37 | 38 | // Note: Not the same as f64::MIN_POSITIVE, because that is only the min *normal* number. 39 | const SMALLEST_POS: f64 = 5e-324; 40 | const SMALLEST_NEG: f64 = -5e-324; 41 | const LARGEST_POS: f64 = std::f64::MAX; 42 | const LARGEST_NEG: f64 = std::f64::MIN; 43 | 44 | const POS_ONE: f64 = 1.0; 45 | const NEG_ONE: f64 = -1.0; 46 | const NEXT_LARGER_THAN_ONE: f64 = 1.0 + std::f64::EPSILON; 47 | const NEXT_SMALLER_THAN_ONE: f64 = 0.999_999_999_999_999_9; 48 | 49 | const SEQUENCE_BIG_NUM: (f64, f64) = (16_237_485_966.000_004, 16_237_485_966.000_006); 50 | 51 | const NAN: f64 = std::f64::NAN; 52 | 53 | fn is_pos_zero(x: f64) -> bool { 54 | x.to_bits() == POS_ZERO.to_bits() 55 | } 56 | 57 | fn is_neg_zero(x: f64) -> bool { 58 | x.to_bits() == NEG_ZERO.to_bits() 59 | } 60 | 61 | #[test] 62 | fn next_larger_than_0() { 63 | assert_eq!(nextafter(POS_ZERO, POS_INF), SMALLEST_POS); 64 | assert_eq!(nextafter(NEG_ZERO, POS_INF), SMALLEST_POS); 65 | } 66 | 67 | #[test] 68 | fn next_smaller_than_0() { 69 | assert_eq!(nextafter(POS_ZERO, NEG_INF), SMALLEST_NEG); 70 | assert_eq!(nextafter(NEG_ZERO, NEG_INF), SMALLEST_NEG); 71 | } 72 | 73 | #[test] 74 | fn step_towards_zero() { 75 | // For steps towards zero, the sign of the zero reflects the direction 76 | // from where zero was approached. 77 | assert!(is_pos_zero(nextafter(SMALLEST_POS, POS_ZERO))); 78 | assert!(is_pos_zero(nextafter(SMALLEST_POS, NEG_ZERO))); 79 | assert!(is_pos_zero(nextafter(SMALLEST_POS, NEG_INF))); 80 | assert!(is_neg_zero(nextafter(SMALLEST_NEG, NEG_ZERO))); 81 | assert!(is_neg_zero(nextafter(SMALLEST_NEG, POS_ZERO))); 82 | assert!(is_neg_zero(nextafter(SMALLEST_NEG, POS_INF))); 83 | } 84 | 85 | #[test] 86 | fn special_case_signed_zeros() { 87 | // For a non-zero dest, stepping away from either POS_ZERO or NEG_ZERO 88 | // has a non-zero result. Only if the destination itself points to the 89 | // "other zero", the next_after call performs a zero sign switch. 90 | assert!(is_neg_zero(nextafter(POS_ZERO, NEG_ZERO))); 91 | assert!(is_pos_zero(nextafter(NEG_ZERO, POS_ZERO))); 92 | } 93 | 94 | #[test] 95 | fn nextafter_around_one() { 96 | assert_eq!(nextafter(POS_ONE, POS_INF), NEXT_LARGER_THAN_ONE); 97 | assert_eq!(nextafter(POS_ONE, NEG_INF), NEXT_SMALLER_THAN_ONE); 98 | assert_eq!(nextafter(NEG_ONE, NEG_INF), -NEXT_LARGER_THAN_ONE); 99 | assert_eq!(nextafter(NEG_ONE, POS_INF), -NEXT_SMALLER_THAN_ONE); 100 | } 101 | 102 | #[test] 103 | fn nextafter_for_big_pos_number() { 104 | let (lo, hi) = SEQUENCE_BIG_NUM; 105 | assert_eq!(nextafter(lo, POS_INF), hi); 106 | assert_eq!(nextafter(hi, NEG_INF), lo); 107 | assert_eq!(nextafter(lo, hi), hi); 108 | assert_eq!(nextafter(hi, lo), lo); 109 | } 110 | 111 | #[test] 112 | fn nextafter_for_big_neg_number() { 113 | let (lo, hi) = SEQUENCE_BIG_NUM; 114 | let (lo, hi) = (-lo, -hi); 115 | assert_eq!(nextafter(lo, NEG_INF), hi); 116 | assert_eq!(nextafter(hi, POS_INF), lo); 117 | assert_eq!(nextafter(lo, hi), hi); 118 | assert_eq!(nextafter(hi, lo), lo); 119 | } 120 | 121 | #[test] 122 | fn step_to_largest_is_possible() { 123 | let smaller = nextafter(LARGEST_POS, NEG_INF); 124 | assert_eq!(nextafter(smaller, POS_INF), LARGEST_POS); 125 | let smaller = nextafter(LARGEST_NEG, POS_INF); 126 | assert_eq!(nextafter(smaller, NEG_INF), LARGEST_NEG); 127 | } 128 | 129 | #[test] 130 | fn jump_to_infinity() { 131 | // Incrementing the max representable number has to go to infinity. 132 | assert_eq!(nextafter(LARGEST_POS, POS_INF), POS_INF); 133 | assert_eq!(nextafter(LARGEST_NEG, NEG_INF), NEG_INF); 134 | } 135 | 136 | #[test] 137 | fn stays_at_infinity() { 138 | // Once infinity is reached, there is not going back to normal numbers 139 | assert_eq!(nextafter(POS_INF, NEG_INF), POS_INF); 140 | assert_eq!(nextafter(NEG_INF, POS_INF), NEG_INF); 141 | } 142 | 143 | #[test] 144 | fn returns_nan_for_any_nan_involved() { 145 | assert!(nextafter(NAN, POS_ONE).is_nan()); 146 | assert!(nextafter(POS_ONE, NAN).is_nan()); 147 | assert!(nextafter(NAN, NAN).is_nan()); 148 | } 149 | 150 | #[test] 151 | fn returns_identity_for_equal_dest() { 152 | let values = [ 153 | POS_ZERO, 154 | NEG_ZERO, 155 | POS_ONE, 156 | NEG_ONE, 157 | SEQUENCE_BIG_NUM.0, 158 | SEQUENCE_BIG_NUM.1, 159 | POS_INF, 160 | NEG_INF, 161 | SMALLEST_POS, 162 | SMALLEST_NEG, 163 | LARGEST_POS, 164 | LARGEST_NEG, 165 | ]; 166 | for x in values { 167 | assert_eq!(nextafter(x, x), x); 168 | } 169 | } 170 | 171 | #[test] 172 | fn roundtrip() { 173 | let values = [ 174 | POS_ONE, 175 | NEG_ONE, 176 | SEQUENCE_BIG_NUM.0, 177 | SEQUENCE_BIG_NUM.1, 178 | SMALLEST_POS, 179 | SMALLEST_NEG, 180 | ]; 181 | for orig in values { 182 | assert_eq!(nextafter(nextafter(orig, POS_INF), NEG_INF), orig); 183 | assert_eq!(nextafter(nextafter(orig, NEG_INF), POS_INF), orig); 184 | 185 | let upper = nextafter(orig, POS_INF); 186 | let lower = nextafter(orig, NEG_INF); 187 | 188 | assert_eq!(nextafter(nextafter(orig, upper), lower), orig); 189 | assert_eq!(nextafter(nextafter(orig, lower), upper), orig); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/topology/mod.rs: -------------------------------------------------------------------------------- 1 | use num_traits::FromPrimitive; 2 | use rayon::iter::IndexedParallelIterator; 3 | use rayon::iter::IntoParallelIterator; 4 | use rayon::iter::ParallelIterator as _; 5 | use std::collections::HashSet; 6 | use std::iter::Sum; 7 | use std::ops::Mul; 8 | 9 | #[cfg(feature = "sprs")] 10 | mod sprs; 11 | 12 | /// `Topology` is implemented for types that represent mesh topology. 13 | pub trait Topology { 14 | /// Return type for [`Topology::neighbors`]. 15 | /// 16 | /// This is an implementation detail and will be removed when Rust allows us 17 | /// to do so (at most when async fns are allowed in traits). 18 | type Neighbors<'n>: Iterator 19 | where 20 | Self: 'n; 21 | 22 | /// The number of elements in the mesh. 23 | fn len(&self) -> usize; 24 | 25 | /// Whether the topology has no elements. 26 | fn is_empty(&self) -> bool { 27 | self.len() == 0 28 | } 29 | 30 | /// An iterator over the neighbors of the given vertex. 31 | fn neighbors(&self, vertex: usize) -> Self::Neighbors<'_>; 32 | 33 | /// The edge cut of a partition. 34 | /// 35 | /// Given a partition and a weighted graph associated to a mesh, the edge 36 | /// cut of a partition is defined as the total weight of the edges that link 37 | /// graph nodes of different parts. 38 | /// 39 | /// # Example 40 | /// 41 | /// A partition with two parts (0 and 1) 42 | /// ```text,ignore 43 | /// 0 44 | /// 1*──┆─*────* 0 45 | /// ╱ ╲ ┆╱ ╱ 46 | /// 1* 1*┆ <┈┈╱┈┈┈ Dotted line passes through edged that contribute to edge cut. 47 | /// ╲ ╱ ┆ ╱ If all edges have a weight of 1 then edge_cut = 3 48 | /// 1* ┆╲ ╱ 49 | /// * 0 50 | /// ``` 51 | fn edge_cut(&self, partition: &[usize]) -> E 52 | where 53 | Self: Sync, 54 | E: Sum + Send, 55 | { 56 | (0..self.len()) 57 | .into_par_iter() 58 | .map(|vertex| { 59 | let vertex_part = partition[vertex]; 60 | self.neighbors(vertex) 61 | .filter(|(neighbor, _edge_weight)| { 62 | vertex_part != partition[*neighbor] && *neighbor < vertex 63 | }) 64 | .map(|(_neighbor, edge_weight)| edge_weight) 65 | .sum() 66 | }) 67 | .sum() 68 | } 69 | 70 | /// The λ-1 cut (lambda-1 cut) of a partition. 71 | /// 72 | /// The λ-1 cut is the sum, for each vertex, of the number of different 73 | /// parts in its neighborhood times its communication weight. 74 | /// 75 | /// This metric better represents the actual communication cost of a 76 | /// partition, albeit more expensive to compute. 77 | fn lambda_cut(&self, partition: &[usize], weights: W) -> W::Item 78 | where 79 | Self: Sync, 80 | W: IntoParallelIterator, 81 | W::Iter: IndexedParallelIterator, 82 | W::Item: Sum + Mul + FromPrimitive, 83 | { 84 | (0..self.len()) 85 | .into_par_iter() 86 | .zip(weights) 87 | .map_with(HashSet::new(), |neighbor_parts, (vertex, vertex_weight)| { 88 | neighbor_parts.clear(); 89 | neighbor_parts.insert(partition[vertex]); 90 | neighbor_parts.extend(self.neighbors(vertex).map(|(v, _)| partition[v])); 91 | W::Item::from_usize(neighbor_parts.len() - 1).unwrap() * vertex_weight 92 | }) 93 | .sum() 94 | } 95 | } 96 | 97 | impl<'a, T, E> Topology for &'a T 98 | where 99 | E: Copy, 100 | T: Topology, 101 | { 102 | type Neighbors<'n> = T::Neighbors<'n> 103 | where Self: 'n; 104 | 105 | fn len(&self) -> usize { 106 | T::len(self) 107 | } 108 | 109 | fn neighbors(&self, vertex: usize) -> Self::Neighbors<'_> { 110 | T::neighbors(self, vertex) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/topology/sprs.rs: -------------------------------------------------------------------------------- 1 | use super::Topology; 2 | use num_traits::FromPrimitive; 3 | use rayon::iter::IndexedParallelIterator; 4 | use rayon::iter::IntoParallelIterator; 5 | use rayon::iter::IntoParallelRefIterator; 6 | use rayon::iter::ParallelIterator; 7 | use std::collections::HashSet; 8 | use std::iter::Cloned; 9 | use std::iter::Sum; 10 | use std::iter::Zip; 11 | use std::ops::Mul; 12 | 13 | impl<'a, E> Topology for sprs::CsMatView<'a, E> 14 | where 15 | E: Copy + Sync, 16 | { 17 | type Neighbors<'n> = Zip>, Cloned>> 18 | where Self: 'n; 19 | 20 | fn len(&self) -> usize { 21 | debug_assert_eq!(self.rows(), self.cols()); 22 | self.rows() 23 | } 24 | 25 | fn neighbors(&self, vertex: usize) -> Self::Neighbors<'_> { 26 | // `CsVecView` does not implement `IntoIterator`, so we have to 27 | // implement it ourselves. It's needed to pass through the `&'_ self` 28 | // lifetime and not end up with a local one. 29 | let (indices, data) = self.outer_view(vertex).unwrap().into_raw_storage(); 30 | indices.iter().cloned().zip(data.iter().cloned()) 31 | } 32 | 33 | fn edge_cut(&self, partition: &[usize]) -> E 34 | where 35 | E: Sum + Send, 36 | { 37 | let indptr = self.indptr().into_raw_storage(); 38 | let indices = self.indices(); 39 | let data = self.data(); 40 | indptr 41 | .par_iter() 42 | .zip(&indptr[1..]) 43 | .enumerate() 44 | .map(|(vertex, (start, end))| { 45 | let neighbors = &indices[*start..*end]; 46 | let edge_weights = &data[*start..*end]; 47 | let vertex_part = partition[vertex]; 48 | neighbors 49 | .iter() 50 | .zip(edge_weights) 51 | .take_while(|(neighbor, _edge_weight)| **neighbor < vertex) 52 | .filter(|(neighbor, _edge_weight)| vertex_part != partition[**neighbor]) 53 | .map(|(_neighbor, edge_weight)| *edge_weight) 54 | .sum() 55 | }) 56 | .sum() 57 | } 58 | 59 | fn lambda_cut(&self, partition: &[usize], weights: W) -> W::Item 60 | where 61 | W: IntoParallelIterator, 62 | W::Iter: IndexedParallelIterator, 63 | W::Item: Sum + Mul + FromPrimitive, 64 | { 65 | let indptr = self.indptr().into_raw_storage(); 66 | let indices = self.indices(); 67 | indptr 68 | .par_iter() 69 | .zip(&indptr[1..]) 70 | .zip(weights) 71 | .enumerate() 72 | .map_with( 73 | HashSet::new(), 74 | |neighbor_parts, (vertex, ((start, end), vertex_weight))| { 75 | let neighbors = &indices[*start..*end]; 76 | neighbor_parts.clear(); 77 | neighbor_parts.insert(partition[vertex]); 78 | neighbor_parts.extend(neighbors.iter().map(|v| partition[*v])); 79 | W::Item::from_usize(neighbor_parts.len() - 1).unwrap() * vertex_weight 80 | }, 81 | ) 82 | .sum() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/work_share.rs: -------------------------------------------------------------------------------- 1 | /// Split `total_work` among a given number of threads. 2 | /// 3 | /// Returns `(thread_count, work_per_thread)`, where `thread_count` is the 4 | /// amount of threads that have actual work, and `work_per_thread` is the 5 | /// maximum amount of work these thread have (RCB here splits by chunks, thus 6 | /// the last thread will not have this much work). 7 | /// 8 | /// # Panics 9 | /// 10 | /// Panics if either argument is zero. 11 | pub fn work_share(total_work: usize, max_threads: usize) -> (usize, usize) { 12 | let max_threads = usize::min(total_work, max_threads); 13 | 14 | // ceil(total_work / max_threads) 15 | let work_per_thread = (total_work + max_threads - 1) / max_threads; 16 | 17 | // ceil(total_work / work_per_thread) 18 | let thread_count = (total_work + work_per_thread - 1) / work_per_thread; 19 | 20 | (work_per_thread, thread_count) 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | 27 | #[test] 28 | fn test_work_share() { 29 | assert_eq!(work_share(100, 4), (25, 4)); 30 | assert_eq!(work_share(101, 4), (26, 4)); 31 | 32 | assert_eq!(work_share(100, 20), (5, 20)); 33 | assert_eq!(work_share(101, 20), (6, 17)); 34 | 35 | assert_eq!(work_share(100, 100), (1, 100)); 36 | assert_eq!(work_share(100, 101), (1, 100)); 37 | 38 | assert_eq!(work_share(100, 1), (100, 1)); 39 | assert_eq!(work_share(100, 2), (50, 2)); 40 | assert_eq!(work_share(100, 3), (34, 3)); 41 | assert_eq!(work_share(1, 100), (1, 1)); 42 | assert_eq!(work_share(2, 100), (1, 2)); 43 | assert_eq!(work_share(3, 100), (1, 3)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coupe-tools" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Hubert Hirtz "] 6 | description = "Tools to work with coupe from the command line" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["cli", "mesh", "partitioning"] 9 | categories = ["algorithms", "command-line-utilities", "mathematics"] 10 | readme = "README.md" 11 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 12 | 13 | 14 | [features] 15 | default = [] 16 | 17 | # Integrate with Intel performance tools 18 | # See tools/README.md for details. 19 | intel-perf = ["dep:ittapi"] 20 | 21 | # Add METIS partitioning procedures as algorithms 22 | metis = ["dep:metis"] 23 | 24 | # Add SCOTCH partitioning procedures as algorithms 25 | scotch = ["dep:scotch"] 26 | 27 | 28 | [dependencies] 29 | 30 | # Partitioners 31 | coupe = { version = "0.1", path = ".." } 32 | scotch = { version = "0.2", optional = true } 33 | metis = { version = "0.1", optional = true } 34 | 35 | # Better tracing and profiling in Intel tools 36 | ittapi = { version = "0.4", optional = true } 37 | 38 | # Random number generation 39 | rand = { version = "0.8", default-features = false, features = ["std"] } 40 | rand_pcg = { version = "0.3", default-features = false } 41 | 42 | # Mesh formats 43 | mesh-io = { path = "mesh-io", version = "0.1" } 44 | 45 | # Command-line interface 46 | getopts = { version = "0.2", default-features = false } 47 | anyhow = { version = "1", default-features = false, features = ["std"] } 48 | 49 | # Debug output 50 | tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "std"] } 51 | tracing-chrome = { version = "0.7", default-features = false } 52 | tracing-tree = { version = "0.3", default-features = false } 53 | 54 | # Benchmark framework 55 | criterion = { version = "0.5", default-features = false } 56 | 57 | # Other utilities 58 | itertools = { version = "0.12", default-features = false } 59 | once_cell = "1" 60 | rayon = "1" 61 | num_cpus = "1" 62 | core_affinity = "0.8" 63 | 64 | [dev-dependencies] 65 | proptest = { version = "1", default-features = false, features = ["std"] } 66 | -------------------------------------------------------------------------------- /tools/Makefile: -------------------------------------------------------------------------------- 1 | RUSTTARGET ?= ./target 2 | PREFIX ?= /usr/local 3 | 4 | CARGO ?= cargo 5 | RM ?= rm 6 | SCDOC ?= scdoc 7 | RUSTFLAGS ?= 8 | BINDIR ?= bin 9 | LIBDIR ?= lib 10 | MANDIR ?= share/man 11 | 12 | tools = apply-part apply-weight mesh-refine mesh-part part-bench part-info weight-gen 13 | toolsdoc = $(foreach tool,$(tools),doc/$(tool).1) 14 | toolsbin = $(foreach tool,$(tools),$(RUSTTARGET)/release/$(tool)) 15 | 16 | all: tools mesh-io $(toolsdoc) 17 | 18 | tools: 19 | $(CARGO) build --locked --release --bins 20 | 21 | mesh-io: 22 | $(CARGO) build --locked --release -p mesh-io-ffi 23 | 24 | doc/%.1: doc/%.1.scd 25 | $(SCDOC) <$^ >$@ 26 | 27 | clean: 28 | $(CARGO) clean 29 | 30 | install: 31 | mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR) 32 | mkdir -p $(DESTDIR)$(PREFIX)/$(LIBDIR) 33 | mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR)/man1 34 | cp -f $(toolsbin) $(DESTDIR)$(PREFIX)/$(BINDIR)/ 35 | cp -f $(toolsdoc) $(DESTDIR)$(PREFIX)/$(MANDIR)/man1/ 36 | cp -f $(RUSTTARGET)/release/libmeshio.a $(DESTDIR)$(PREFIX)/$(LIBDIR)/ 37 | cp -f $(RUSTTARGET)/release/libmeshio.so $(DESTDIR)$(PREFIX)/$(LIBDIR)/ 38 | 39 | .PHONY: tools mesh-io clean install 40 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # The coupe toolkit 2 | 3 | This directory contains tools to work around coupe, and other partitioners. It 4 | includes the following tools: 5 | 6 | - mesh-io, a library used to encode and decode meshes in different formats, 7 | - num-part, a framework to evaluate the quality of number partitioning 8 | algorithms specifically, 9 | - in the `src/bin` directory, a collection of tools to partition meshes and 10 | evaluate mesh partitions: 11 | - weight-gen generates a distribution of cell weights for a mesh, 12 | - mesh-part runs a partitioner on a given mesh and weight distribution, then 13 | outputs a partition file, 14 | - part-bench runs criterion on given partitioners, meshes and weights, 15 | - part-info displays information about a partition, for a given mesh and 16 | weight distribution, 17 | - apply-part encodes a partition in a mesh file for visualization. 18 | - apply-weight encodes a weight distribution in a mesh file for 19 | visualization, 20 | - mesh-svg outputs an SVG given a mesh file, for use with the two above 21 | tools, 22 | - mesh-dup and mesh-refine increase the size of a mesh by either duplicating 23 | the vertices or splitting its elements into smaller ones, respectively, 24 | - mesh-reorder changes the order of mesh elements, 25 | - mesh-points extracts the cell centers of a given mesh. 26 | - in the `report` directory, a collection of shell scripts that aggregate 27 | results into visual reports: 28 | - `imbedgecut` generates an SVG graph comparing the imbalance/edge-cut of 29 | various algorithms, 30 | - `quality` generates an HTML report of partitioning results for a given 31 | mesh directory, 32 | - `efficiency` generates a CSV file and a SVG graph showing the efficiency 33 | (strong scaling) of algorithms, 34 | - `efficiency-weak` is the weak-scaling equivalent of the above tool. It 35 | will also run the algorithm itself to overcome a limitation in part-bench. 36 | 37 | ## Building 38 | 39 | [scdoc] is required to build the man pages. 40 | 41 | For end users, a simple Makefile with basic options is provided: 42 | 43 | ``` 44 | make 45 | sudo make install 46 | ``` 47 | 48 | Otherwise, these tools can be built with cargo: 49 | 50 | ``` 51 | cargo build --bins 52 | ``` 53 | 54 | C bindings to mesh-io can be built with the following command: 55 | 56 | ``` 57 | cargo build -p mesh-io-ffi 58 | ``` 59 | 60 | ### Integration with other partitioners 61 | 62 | The `mesh-part` and `part-bench` tools have optional support for [METIS] and 63 | [SCOTCH] that is disabled by default. To enable these features, use the 64 | `--features` command-line flag as shown below. Note that these features require 65 | a working clang install (version 5.0 or higher). 66 | 67 | ```sh 68 | # Enable SCOTCH and METIS algorithms 69 | cargo build --bins --features metis,scotch 70 | ``` 71 | 72 | ### Integration with Intel performance tools 73 | 74 | The `mesh-part` and `part-bench` tools can better integrate with Intel VTune and 75 | Advisor through the use of *Instrumentation and Tracing Technology APIs*. 76 | 77 | To enable this integration, do so through the `intel-perf` cargo feature: 78 | 79 | ```sh 80 | cargo build --bins --feature intel-perf 81 | ``` 82 | 83 | When enabled, `mesh-part` and `part-bench` will wrap algorithm calls into 84 | *tasks*. See [Intel's documentation][intel] on how to analyze them. 85 | 86 | ## Usage 87 | 88 | See the man pages in the `doc/` directory. 89 | 90 | For example, here is a quick walk-through: 91 | 92 | ```shell 93 | # Cell weights increase linearly according to its position on the X axis. 94 | weight-gen --distribution linear,x,0,100 heart.linear.weights 95 | 96 | # Apply coupe's hilbert curve followed by the Fidducia-Mattheyses algorithm. 97 | mesh-part --algorithm hilbert,3 \ 98 | --algorithm fm \ 99 | --mesh heart.mesh \ 100 | --weights heart.linear.weights \ 101 | >heart.linear.hilbert-fm.part 102 | 103 | # Apply METIS' recursive bisection on the same mesh and weights. 104 | mesh-part --algorithm metis:recursive,3 105 | --mesh heart.mesh \ 106 | --weights heart.linear.weights \ 107 | >heart.linear.metis.part 108 | 109 | # Compare both partitions. 110 | part-info --mesh heart.mesh --weights heart.linear.weights \ 111 | --partition heart.linear.hilbert-fm.part 112 | part-info --mesh heart.mesh --weights heart.linear.weights \ 113 | --partition heart.linear.metis.part 114 | ``` 115 | 116 | ### Usage with Docker 117 | 118 | Here is a quick walk-through allowing you to launch a container with Coupe bins 119 | and to interact with it from your host. 120 | 121 | ```shell 122 | # Pull docker image. 123 | docker pull ghcr.io/lihpc-computational-geometry/coupe:main 124 | 125 | # Add "coupe" tag to the docker image. 126 | docker image tag ghcr.io/lihpc-computational-geometry/coupe:main coupe 127 | 128 | # Run the container while binding the current directory and the container. 129 | # This will allow you to access inner generated data from your host. 130 | # Warning: removing a binded file from within the container also removes 131 | # it from the host. 132 | docker container run -dit \ 133 | --name coupe_c \ 134 | --mount type=bind,source="$(pwd)",target=/coupe/shared \ 135 | coupe 136 | 137 | # Generate a linear weight distribution from an embedded mesh file. 138 | # Note: option `--integers` can be used to generate integers instead of 139 | # floating-point numbers. 140 | docker exec coupe_c sh -c 'weight-gen --distribution linear,x,0,100 \ 141 | shared/sample.linear.weights' 143 | 144 | # Partition the previous mesh and the generated weight distribution into 2 parts. 145 | # using the Recursive Coordinate Bisection (RCB) algorithm coupled with the 146 | # Fidducia-Mattheyses algorithm. 147 | docker exec coupe_c sh -c 'mesh-part \ 148 | --algorithm rcb,1 \ 149 | --algorithm fm \ 150 | --mesh shared/sample.mesh \ 151 | --weights shared/sample.linear.weights \ 152 | >shared/sample.linear.rcb-fm.part' 153 | 154 | # Merge partition file into MEDIT mesh file and converts it to .svg file. 155 | docker exec coupe_c sh -c 'apply-part \ 156 | --mesh shared/sample.mesh \ 157 | --partition shared/sample.linear.rcb-fm.part \ 158 | | mesh-svg >shared/sample.rcb-fm.svg' 159 | 160 | # Open the svg file in firefox. 161 | firefox sample.rcb-fm.svg & 162 | ``` 163 | 164 | [intel]: https://www.intel.com/content/www/us/en/develop/documentation/vtune-help/top/analyze-performance/code-profiling-scenarios/task-analysis.html#task-analysis_TOP_TASKS 165 | [METIS]: https://github.com/LIHPC-Computational-Geometry/metis-rs 166 | [SCOTCH]: https://github.com/LIHPC-Computational-Geometry/scotch-rs 167 | [scdoc]: https://sr.ht/~sircmpwn/scdoc/ 168 | -------------------------------------------------------------------------------- /tools/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | fn main() { 5 | let git_commit = { 6 | let hash = Command::new("git") 7 | .args(["rev-parse", "HEAD"]) 8 | .output() 9 | .unwrap() 10 | .stdout; 11 | let mut hash = String::from_utf8(hash).unwrap(); 12 | hash.truncate(7); 13 | hash 14 | }; 15 | 16 | let git_clean_tree = Command::new("git") 17 | .args(["status", "-s"]) 18 | .output() 19 | .unwrap() 20 | .stdout 21 | .is_empty(); 22 | 23 | let crate_version = env::var("CARGO_PKG_VERSION").unwrap(); 24 | 25 | let coupe_version = if git_clean_tree { 26 | format!("{crate_version}-{git_commit}") 27 | } else { 28 | format!("{crate_version}-{git_commit}+dirty") 29 | }; 30 | 31 | println!("cargo:rustc-env=COUPE_VERSION={coupe_version}"); 32 | } 33 | -------------------------------------------------------------------------------- /tools/doc/apply-part.1.scd: -------------------------------------------------------------------------------- 1 | apply-part(1) 2 | 3 | # NAME 4 | 5 | apply-part - Colorize a mesh with a partition file for visualization 6 | 7 | apply-weight - Colorize a mesh with a weight file for visualization 8 | 9 | # SYNOPSIS 10 | 11 | *apply-part* --mesh --partition [output.mesh] 12 | 13 | *apply-weight* --mesh --weights [output.mesh] 14 | 15 | # DESCRIPTION 16 | 17 | apply-part (apply-weight) merges a partition file (resp. weight file) into 18 | the free-form fields of a MEDIT mesh file, so that the partition (resp. weight 19 | distribution) can be visualized with medit tools. 20 | 21 | If output.mesh is omitted or is -, it is written to standard output. 22 | 23 | Only some specific mesh formats are supported. See *INPUT FORMAT* for details. 24 | 25 | # OPTIONS 26 | 27 | *-h, --help* 28 | Show a help message and exit. 29 | 30 | *--version* 31 | Show version information and exit. 32 | 33 | *-f, --format* 34 | Override the output format. By default, the file format is inferred from 35 | the file extension. See *OUTPUT FORMAT* for more info. 36 | 37 | *-m, --mesh* 38 | Use the given mesh file as template. 39 | 40 | *-p, --partition* 41 | Use the given partition file. This file is expected to come from 42 | *mesh-part*(1) with the same mesh. 43 | 44 | *-w, --weights* 45 | Use the given weight file. This file is expected to come from 46 | *weight-gen*(1) with the same mesh. 47 | 48 | # INPUT FORMAT 49 | 50 | These programs can accept multiple mesh formats, and will detect automatically 51 | the correct version to use, regardless of the file extension. There is 52 | currently no way to force a particular format for input. 53 | 54 | The supported mesh formats are: 55 | 56 | - MEDIT, both ASCII and binary variants, 57 | - VTK, legacy ASCII and binary variants. The binary variant assumes big endian 58 | numbers. 59 | 60 | # OUTPUT FORMAT 61 | 62 | The *-f, --format* option changes the output format of the program. These 63 | values are currently accepted: 64 | 65 | - _meshb_ (default): output MEDIT binary, 66 | - _mesh_: output MEDIT ASCII, 67 | - _vtk-ascii_: output VTK ASCII, 68 | - _vtk-binary_: output VTK binary (big endian). 69 | 70 | In general, you'll want the default _meshb_ format. It is faster to read from and 71 | write to. Use the MEDIT ASCII format for debugging or for compatibility. 72 | 73 | # SEE ALSO 74 | 75 | *mesh-part*(1) *part-info*(1) *weight-gen*(1) 76 | 77 | # AUTHORS 78 | 79 | This executable is part of coupe, which is maintained by Hubert Hirtz 80 | under the direction of Franck Ledoux 81 | and the supervision of Cédric Chevalier and Sébastien 82 | Morais . 83 | 84 | For more information on coupe development, see 85 | . 86 | -------------------------------------------------------------------------------- /tools/doc/apply-weight.1.scd: -------------------------------------------------------------------------------- 1 | apply-part.1.scd -------------------------------------------------------------------------------- /tools/doc/mesh-dup.1.scd: -------------------------------------------------------------------------------- 1 | mesh-dup(1) 2 | 3 | # NAME 4 | 5 | mesh-dup - Duplicate the elements of a mesh 6 | 7 | # SYNOPSIS 8 | 9 | *mesh-dup* [--times ] [input.mesh [output.mesh]] 10 | 11 | # DESCRIPTION 12 | 13 | mesh-dup copies the elements of a mesh a number of times so that the output mesh 14 | forms a (hyper)grid of input meshs, mainly for performance tests on algorithms. 15 | 16 | If input.mesh is omitted or is -, it is read from standard input. 17 | If output.mesh is omitted or is -, it is written to standard output. 18 | 19 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 20 | FORMAT* for details. 21 | 22 | # OPTIONS 23 | 24 | *-h, --help* 25 | Show a help message and exit. 26 | 27 | *--version* 28 | Show version information and exit. 29 | 30 | *-f, --format* 31 | Override the output format. By default, the file format is inferred from 32 | the file extension. See *apply-part*(1)'s *OUTPUT FORMAT* for more info. 33 | 34 | *-n, --times* 35 | Iterate a given number of times. By default, only once. 36 | 37 | # SEE ALSO 38 | 39 | *mesh-refine*(1) *mesh-part*(1) 40 | 41 | # AUTHORS 42 | 43 | This executable is part of coupe, which is maintained by Hubert Hirtz 44 | under the direction of Franck Ledoux 45 | and the supervision of Cédric Chevalier and Sébastien 46 | Morais . 47 | 48 | For more information on coupe development, see 49 | . 50 | -------------------------------------------------------------------------------- /tools/doc/mesh-part.1.scd: -------------------------------------------------------------------------------- 1 | mesh-part(1) 2 | 3 | # NAME 4 | 5 | mesh-part - Partition a mesh 6 | 7 | part-bench - Benchmark an algorithm 8 | 9 | # SYNOPSIS 10 | 11 | *mesh-part* [options...] [output.part] 12 | 13 | *part-bench* [options...] 14 | 15 | # DESCRIPTION 16 | 17 | mesh-part applies partitioning algorithms onto a given mesh in order, and 18 | outputs the resulting partition. 19 | 20 | If output.part is omitted or is -, it is written to standard output. 21 | 22 | part-bench benchmarks the speed of the given algorithms and prints the results. 23 | 24 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 25 | FORMAT* for details. 26 | 27 | See *ALGORITHMS* for a list of supported algorithms. 28 | 29 | See *PARTITION FILE* for a description of the output format of mesh-part. 30 | 31 | # OPTIONS 32 | 33 | *-h, --help* 34 | Show a help message and exit. 35 | 36 | *--version* 37 | Show version information and exit. 38 | 39 | *-a, --algorithm* 40 | Apply the given algorithm on the mesh. This option can be specified 41 | multiple times, so that algorithms can be chained together. See 42 | *ALGORITHMS* for information on the _spec_ argument and a list of supported 43 | algorithms. 44 | 45 | *-E, --edge-weights* 46 | Change how edge weights are set. Possible values are: 47 | 48 | - _uniform_ (default): all edges have the same weight, 49 | - _linear_: edge weights are the sum of the vertex weights, 50 | - _sqrt_: edge weights are the sum of the vertex weights' square roots. 51 | 52 | *-m, --mesh* 53 | Required. Partition the given mesh file. 54 | 55 | *-w, --weights* 56 | Use the given weight file. This file is expected to come from 57 | *weight-gen*(1) with the same mesh. 58 | 59 | Options specific to *mesh-part*: 60 | 61 | *-t, --trace* 62 | Emits a trace file viewable in or any compatible viewer. 63 | The LOG environment variable must be specified. 64 | 65 | Options specific to *part-bench*: 66 | 67 | *-e, --efficiency* [threads] 68 | Measure strong scaling by running the algorithm with different amounts of 69 | threads. 70 | 71 | By default, part-bench starts at 1 thread, then doubles the thread count 72 | until it exceeds the number of available hardware threads. 73 | 74 | You can specify arbitrary thread counts in the following manner: 75 | 76 | ``` 77 | threads = range *( , range ) 78 | range = VALUE / ( FROM : TO ) / ( FROM : TO : STEP ) 79 | ``` 80 | 81 | For example, the following invocation will run the algorithms for 1 thread, 82 | then 2, 6, 10, ... to 64, then 72, 80, 88, ... to 256. 83 | 84 | part-bench -e 1,2:64:4,64:256:8,256 85 | 86 | Ranges are exclusive. 87 | 88 | *-b, --baseline* ++ 89 | *-s, --save-baseline* 90 | Compare against a named baseline. If *--save-baseline* is specified, the 91 | results of the benchmark will be saved and overwrite the previous ones. 92 | 93 | # ENVIRONMENT VARIABLES 94 | 95 | Users can use environment variables to alter the execution of these programs: 96 | 97 | - _LOG=coupe_, for mesh-part, enable debug algorithm logging of coupe's 98 | algorithms, 99 | - _RAYON_NUM_THREADS=n_ restricts the maximum number of threads to _n_. 100 | 101 | # ALGORITHMS 102 | 103 | The option *-a, --algorithm* defines an algorithm used to partition the input 104 | mesh. It can be specified multiple times to chain algorithms. 105 | 106 | If a partition improving algorithm is in the begining of the chain, it will 107 | be fed a default partition where all weights are in the same part. 108 | 109 | For example, 110 | 111 | mesh-part --algorithm hilbert,4 --algorithm fm,0.05 112 | 113 | Miscellaneous partitioning algorithms: 114 | 115 | *random*,PART_COUNT,[SEED=0] 116 | Creates a random partition 117 | 118 | Number partitioning algorithms:++ 119 | These algorithms create partitions by only taking weights into account. 120 | 121 | *greedy*,PART_COUNT 122 | Greedy number partitioning algorithm 123 | 124 | *kk*,PART_COUNT 125 | Karmarkar-Karp, Least Difference Method 126 | 127 | *ckk*,TOLERANCE 128 | Complete Karmarkar-Karp 129 | 130 | Number partition improving algorithms:++ 131 | These algorithms improve partitions by only taking weights into account. 132 | 133 | *vn-best* 134 | Steepest descent Vector of Numbers algorithm 135 | 136 | *vn-first* 137 | Descent Vector of Numbers algorithm 138 | 139 | Geometric partitioning algorithms:++ 140 | These algorithms create partitions using cell coordinates. 141 | 142 | *rcb*,PART_COUNT,[TOLERANCE=0.05] 143 | Recursive Coordinate Biscection 144 | 145 | *hilbert*,PART_COUNT,[ORDER=12] 146 | Hilbert Curve 147 | 148 | Geometric partition improving algorithms:++ 149 | These algorithms improve partitions using cell coordinates. 150 | 151 | *kmeans* 152 | Balanced k-means. 153 | 154 | Graph partition improving algorithms:++ 155 | These algorithms improve partitions using the topology of the mesh. 156 | 157 | *fm*,[args...] 158 | Fidducia-Mattheyses algorithm. Arguments (in this order): 159 | - MAX_IMBALANCE (default: initial imbalance) 160 | - MAX_BAD_MOVES_IN_A_ROW (default: 0) 161 | - MAX_PASSES (default: 0, i.e. UINTPTR_MAX) 162 | - MAX_MOVES_PER_PASS (default: 0, i.e. UINTPTR_MAX) 163 | 164 | *arcswap*,[MAX_IMBALANCE] 165 | Multi-threaded, FM-like algorithm. By default this will not worsen the 166 | imbalance. 167 | 168 | *kl*,[MAX_BAD_MOVES_IN_A_ROW=1] 169 | Kernighan-Lin algorithm 170 | 171 | METIS partitioning algorithms:++ 172 | These algorithms require *mesh-part* to be built with METIS support. 173 | 174 | *metis:recursive*,PART_COUNT 175 | METIS's recursive graph partitioning. 176 | 177 | *metis:kway*,PART_COUNT 178 | METIS's kway graph partitioning. 179 | 180 | SCOTCH partitioning algorithms:++ 181 | These algorithms require *mesh-part* to be built with SCOTCH support. 182 | 183 | *scotch:std*,PART_COUNT 184 | The default partitioning algorithm from SCOTCH. 185 | 186 | # PARTITION FILE 187 | 188 | A partition file is a binary file that consists of a header followed by a list of 189 | part IDs. Below is the ABNF representation of the file: 190 | 191 | ``` 192 | file = magic id-count ids 193 | magic = %x4d %x65 %x50 %x65 ; "MePe" 194 | id-count = U64 ; Number of weights 195 | ids = *U64 ; id-count weights 196 | ``` 197 | 198 | where _U64_ is a little-endian 8-byte unsigned integer. 199 | 200 | # SEE ALSO 201 | 202 | *apply-part*(1) *part-info*(1) *weight-gen*(1) 203 | 204 | # AUTHORS 205 | 206 | This executable is part of coupe, which is maintained by Hubert Hirtz 207 | under the direction of Franck Ledoux 208 | and the supervision of Cédric Chevalier and Sébastien 209 | Morais . 210 | 211 | For more information on coupe development, see 212 | . 213 | -------------------------------------------------------------------------------- /tools/doc/mesh-points.1.scd: -------------------------------------------------------------------------------- 1 | mesh-points(1) 2 | 3 | # NAME 4 | 5 | mesh-points - Extract the cell centers of the given mesh 6 | 7 | # SYNOPSIS 8 | 9 | *mesh-points* [input.mesh [output.dat]] 10 | 11 | # DESCRIPTION 12 | 13 | mesh-points extracts the cell centers of the given mesh. 14 | 15 | If input.mesh is omitted or is -, it is read from standard input. 16 | If output.dat is omitted or is -, it is written to standard output. 17 | 18 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 19 | FORMAT* for details. 20 | 21 | # OPTIONS 22 | 23 | *-h, --help* 24 | Show a help message and exit. 25 | 26 | *--with-ids* 27 | Add a column with the cell IDs (refs). 28 | 29 | # USAGE EXAMPLE WITH GNUPLOT 30 | 31 | Using *--with-ids* on a 3D mesh, one ends up with a file with 4 columns. The 32 | first three are the cell center coordinates, and the fourth is the cell ID: 33 | 34 | ``` 35 | 1.2 4 6.6 0 36 | 3 12.5 9.01 4 37 | etc. 38 | ``` 39 | 40 | Then the gnuplot commands can be used to generate a 3D, colored plot of those 41 | points: 42 | 43 | ``` 44 | splot 'mesh.points' using 1:2:3:4 with points palette 45 | ``` 46 | 47 | # SEE ALSO 48 | 49 | *apply-part*(1) *apply-weight*(1) 50 | 51 | # AUTHORS 52 | 53 | This executable is part of coupe, which is maintained by Hubert Hirtz 54 | under the direction of Franck Ledoux 55 | and the supervision of Cédric Chevalier and Sébastien 56 | Morais . 57 | 58 | For more information on coupe development, see 59 | . 60 | -------------------------------------------------------------------------------- /tools/doc/mesh-refine.1.scd: -------------------------------------------------------------------------------- 1 | mesh-refine(1) 2 | 3 | # NAME 4 | 5 | mesh-refine - Refine a mesh 6 | 7 | # SYNOPSIS 8 | 9 | *mesh-refine* [--times ] [input.mesh [output.mesh]] 10 | 11 | # DESCRIPTION 12 | 13 | mesh-refine splits the cell of a mesh into smaller cells, mainly for performance 14 | tests on algorithms. 15 | 16 | If input.mesh is omitted or is -, it is read from standard input. 17 | If output.mesh is omitted or is -, it is written to standard output. 18 | 19 | mesh-refine only works on 2D meshes composed exclusively of triangles and 20 | quadrilaterals. It splits either of them into four equal parts at each 21 | iteration. 22 | 23 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 24 | FORMAT* for details. 25 | 26 | # OPTIONS 27 | 28 | *-h, --help* 29 | Show a help message and exit. 30 | 31 | *--version* 32 | Show version information and exit. 33 | 34 | *-f, --format* 35 | Override the output format. By default, the file format is inferred from 36 | the file extension. See *apply-part*(1)'s *OUTPUT FORMAT* for more info. 37 | 38 | *-n, --times* 39 | Iterate a given number of times. By default, only once. 40 | 41 | # SEE ALSO 42 | 43 | *mesh-dup*(1) *mesh-part*(1) 44 | 45 | # AUTHORS 46 | 47 | This executable is part of coupe, which is maintained by Hubert Hirtz 48 | under the direction of Franck Ledoux 49 | and the supervision of Cédric Chevalier and Sébastien 50 | Morais . 51 | 52 | For more information on coupe development, see 53 | . 54 | -------------------------------------------------------------------------------- /tools/doc/mesh-reorder.1.scd: -------------------------------------------------------------------------------- 1 | mesh-reorder(1) 2 | 3 | # NAME 4 | 5 | mesh-reorder - Change the order of the vertices and the elements of a mesh 6 | 7 | # SYNOPSIS 8 | 9 | *mesh-reorder* [input.mesh [output.mesh]] 10 | 11 | # DESCRIPTION 12 | 13 | mesh-reorder shuffles the vertices and the elements of a mesh, without altering 14 | its mathematical structure: only the order in which they appear in the file is 15 | changed. 16 | 17 | If input.mesh is omitted or is -, it is read from standard input. 18 | If output.mesh is omitted or is -, it is written to standard output. 19 | 20 | The tool can be used in conjunction of *mesh-reorder* or *mesh-dup*, which both 21 | produce very typical element orderings and may disrupt algorithms. 22 | 23 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 24 | FORMAT* for details. 25 | 26 | # OPTIONS 27 | 28 | *-h, --help* 29 | Show a help message and exit. 30 | 31 | *--version* 32 | Show version information and exit. 33 | 34 | *-f, --format* 35 | Override the output format. By default, the file format is inferred from 36 | the file extension. See *apply-part*(1)'s *OUTPUT FORMAT* for more info. 37 | 38 | # SEE ALSO 39 | 40 | *mesh-dup*(1) *mesh-refine*(1) 41 | 42 | # AUTHORS 43 | 44 | This executable is part of coupe, which is maintained by Hubert Hirtz 45 | under the direction of Franck Ledoux 46 | and the supervision of Cédric Chevalier and Sébastien 47 | Morais . 48 | 49 | For more information on coupe development, see 50 | . 51 | -------------------------------------------------------------------------------- /tools/doc/mesh-svg.1.scd: -------------------------------------------------------------------------------- 1 | mesh-svg(1) 2 | 3 | # NAME 4 | 5 | mesh-svg - Render a 2D mesh into a SVG image 6 | 7 | # SYNOPSIS 8 | 9 | *mesh-svg* [input.mesh [output.svg]] 10 | 11 | # DESCRIPTION 12 | 13 | mesh-svg renders a 2D mesh as a SVG, optimizing for output size. 14 | 15 | If input.mesh is omitted or is -, it is read from standard input. 16 | If output.svg is omitted or is -, it is written to standard output. 17 | 18 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 19 | FORMAT* for details. 20 | 21 | # OPTIONS 22 | 23 | *-h, --help* 24 | Show a help message and exit. 25 | 26 | *--version* 27 | Show version information and exit. 28 | 29 | *-o, --no-optimize* 30 | Do not merge elements of the same ref/color together. 31 | 32 | By default, mesh-svg merges elements of the same color together in the SVG, 33 | so that the visuals stay the same but file size is drastically reduced. This 34 | argument disables this behavior. 35 | 36 | # SEE ALSO 37 | 38 | *apply-part*(1) *apply-weight*(1) 39 | 40 | # AUTHORS 41 | 42 | This executable is part of coupe, which is maintained by Hubert Hirtz 43 | under the direction of Franck Ledoux 44 | and the supervision of Cédric Chevalier and Sébastien 45 | Morais . 46 | 47 | For more information on coupe development, see 48 | . 49 | -------------------------------------------------------------------------------- /tools/doc/part-bench.1.scd: -------------------------------------------------------------------------------- 1 | mesh-part.1.scd -------------------------------------------------------------------------------- /tools/doc/part-info.1.scd: -------------------------------------------------------------------------------- 1 | part-info(1) 2 | 3 | # NAME 4 | 5 | part-info - Print information about the quality a partition 6 | 7 | # SYNOPSIS 8 | 9 | *part-info* [options...] 10 | 11 | # DESCRIPTION 12 | 13 | part-info prints the following information: 14 | 15 | - the imbalance for each criterion, 16 | - the edge cut, 17 | - the lambda-1 cut. 18 | 19 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 20 | FORMAT* for details. 21 | 22 | # OPTIONS 23 | 24 | *-h, --help* 25 | Show a help message and exit. 26 | 27 | *--version* 28 | Show version information and exit. 29 | 30 | *-E, --edge-weights* 31 | Change how edge weights are set. Possible values are listed in 32 | *mesh-part*(1). 33 | 34 | *-m, --mesh* 35 | Required. A path to the mesh that has been partitionned. 36 | 37 | *-n, --parts* 38 | The expected number of parts. By default, part-info will take the actual 39 | number of parts (which can be lower). 40 | 41 | *-p, --partition* 42 | Required. A path to the partition file. 43 | 44 | *-w, --weights* 45 | Required. A path to the weight file used to partition the mesh. 46 | 47 | # SEE ALSO 48 | 49 | *mesh-part*(1) *apply-part*(1) *weight-gen*(1) 50 | 51 | # AUTHORS 52 | 53 | This executable is part of coupe, which is maintained by Hubert Hirtz 54 | under the direction of Franck Ledoux 55 | and the supervision of Cédric Chevalier and Sébastien 56 | Morais . 57 | 58 | For more information on coupe development, see 59 | . 60 | -------------------------------------------------------------------------------- /tools/doc/weight-gen.1.scd: -------------------------------------------------------------------------------- 1 | weight-gen(1) 2 | 3 | # NAME 4 | 5 | weight-gen - Generate a distribution of weights for a given mesh 6 | 7 | # SYNOPSIS 8 | 9 | *weight-gen* [options...] [input.mesh [output.weights]] 10 | 11 | # DESCRIPTION 12 | 13 | weight-gen generates weights for a given mesh. 14 | 15 | If input.mesh is omitted or is -, it is read from standard input. 16 | If output.weights is omitted or is -, it is written to standard output. 17 | 18 | Unless *-i, --integers* is specified, the generated weights are floating-point 19 | numbers. 20 | 21 | Only some specific mesh formats are supported. See *apply-part*(1)'s *INPUT 22 | FORMAT* for details. 23 | 24 | See *WEIGHT FILE* for a description of the output format. 25 | 26 | # OPTIONS 27 | 28 | *-h, --help* 29 | Show a help message and exit. 30 | 31 | *--version* 32 | Show version information and exit. 33 | 34 | *-d, --distribution* 35 | Required. The definition of a weight distribution. This option must be 36 | specified once per criterion. See *DISTRIBUTION FORMAT* and *SUPPORTED 37 | DISTRIBUTIONS* for more information. 38 | 39 | *-i, --integers* 40 | Truncate weights and generate an integer weight distribution instead. 41 | 42 | # DISTRIBUTION FORMAT 43 | 44 | The option *-d, --distribution* defines how input weights are laid out. It 45 | can be specified multiple times, one for each criterion. Its value must 46 | follow the following syntax: 47 | 48 | ``` 49 | distribution := name *( "," param) 50 | param := value / axis 51 | name := STRING 52 | value := FLOAT 53 | axis := "x" / "y" / "z" / "0" / "1" / "2" 54 | ``` 55 | 56 | For example, the following will lay out weights in ascending order on the 57 | horizontal (X, abscissa) axis, from 0 to 100: 58 | 59 | weight-gen --distribution linear,x,0,100 60 | 61 | And the following example shows how to form a spike of height 4.2 at the origin: 62 | 63 | weight-gen --distribution spike,4.2,0,0 64 | 65 | # SUPPORTED DISTRIBUTIONS 66 | 67 | The following distributions are supported: 68 | 69 | *constant*,VALUE 70 | Set all weights to be equal to the given VALUE. 71 | 72 | *linear*,AXIS,FROM,TO 73 | Weights follow a linear slope on the given axis, where the points at the 74 | lowest coordinate on that axis are assigned the value FROM, and the one 75 | at the highest coordinate are assigned the value TO. 76 | 77 | *spike*,HEIGHT,POSITION,... 78 | Weights form a spike of the given height at the given position. The 79 | spike has an exponential shape: weights are of the order of _exp(-d)_ where 80 | _d_ is the distance between the element and the center of the mesh. Multiple 81 | spikes can be specified. 82 | 83 | # WEIGHT FILE 84 | 85 | A weight file is a binary file that consists of a header followed by a list of 86 | weights, which can either be integers or floating points. Below is the ABNF 87 | representation of the file: 88 | 89 | ``` 90 | file = magic version flags criterion-count weight-count weights 91 | magic = %x4d %x65 %x57 %x65 ; "MeWe" 92 | version = %x01 ; Version 1 93 | flags = %x00-FF ; see the Flags paragraph 94 | criterion-count = U16 ; Number of criteria 95 | weight-count = U64 ; Number of weights per criterion 96 | weights = *I64 / *F64 ; criterion-count times weight-count weights 97 | ``` 98 | 99 | Some notes: 100 | 101 | - Weights are laid out as weight-count arrays of criterion-count items. 102 | - _U16_ is a little-endian 2-byte unsigned integer. 103 | - _U64_ is a little-endian 8-byte unsigned integer. 104 | - _I64_ is a little-endian 8-byte signed integer. 105 | - _F64_ is a little-endian-encoded binary64 IEEE 754-2008 floating point. 106 | 107 | The _flags_ byte has the following meaning, from the least significant bit to 108 | the most: 109 | 110 | - The first bit is 1 if weights are integers (_I64_), or 0 if weights are floats 111 | (_F64_), 112 | - The other bits are left unspecified. 113 | 114 | # SEE ALSO 115 | 116 | *mesh-part*(1) 117 | 118 | # AUTHORS 119 | 120 | This executable is part of coupe, which is maintained by Hubert Hirtz 121 | under the direction of Franck Ledoux 122 | and the supervision of Cédric Chevalier and Sébastien 123 | Morais . 124 | 125 | For more information on coupe development, see 126 | . 127 | -------------------------------------------------------------------------------- /tools/mesh-io/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mesh-io" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Hubert Hirtz "] 6 | description = "(de)serializing library for various mesh formats" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["mesh", "parser"] 9 | categories = ["mathematics", "parsing"] 10 | readme = "../README.md" 11 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 12 | 13 | 14 | [features] 15 | default = ["vtk-legacy"] 16 | 17 | # File format: VTK (legacy) 18 | vtk-legacy = ["vtkio"] 19 | 20 | # Read from and write to compressed mesh files 21 | compression = ["vtkio?/compression"] 22 | 23 | 24 | [dependencies] 25 | itertools = "0.12" 26 | 27 | # VTK file formats 28 | vtkio = { version = "0.6", default-features = false, optional = true } 29 | -------------------------------------------------------------------------------- /tools/mesh-io/ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mesh-io-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Hubert Hirtz "] 6 | description = "C bindings to mesh-io" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["mesh", "parser"] 9 | categories = ["mathematics", "parsing"] 10 | readme = "../../README.md" 11 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 12 | 13 | 14 | [lib] 15 | name = "meshio" 16 | crate-type = ["cdylib", "staticlib"] 17 | 18 | 19 | [dependencies] 20 | mesh-io = { version = "0.1", path = ".." } 21 | -------------------------------------------------------------------------------- /tools/mesh-io/ffi/meshio.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBMESHIO_H 2 | #define LIBMESHIO_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | #define ERROR_GENERIC -1 11 | #define ERROR_BAD_HEADER -2 12 | #define ERROR_UNSUPPORTED_VERSION -3 13 | 14 | /** Read a partition file from a file descriptor. 15 | * 16 | * Returns a negative number on error. 17 | * The partition must be freed using partition_free. 18 | * The file descriptor is closed. */ 19 | int mio_partition_read(uint64_t *size, uint64_t **partition, int fd); 20 | /** Write a partition file to a file descriptor. 21 | * 22 | * Returns a negative number on error. 23 | * The file descriptor is left open. */ 24 | int mio_partition_write(int fd, uint64_t size, const uint64_t *partition); 25 | /** Free a partition. 26 | * 27 | * Both arguments must match the result of mio_partition_read. */ 28 | void mio_partition_free(uint64_t size, uint64_t *partition); 29 | 30 | /** An array of weights, which can either be floats or integers. */ 31 | struct mio_weights; 32 | /** Read weights from the given file descriptor. 33 | * 34 | * Returns NULL on error. 35 | * The result must be freed using mio_weights_free. 36 | * The file descriptor is closed. */ 37 | struct mio_weights *mio_weights_read(int fd); 38 | /** The number of weights in the given weight array. */ 39 | uint64_t mio_weights_count(struct mio_weights *weights); 40 | /** Copy and convert the first criterion of the array into the prealocated 41 | * `criterion` array. 42 | * 43 | * The given pointer must be allocated with enough memory to contain all weights 44 | * (must point to at least mio_weights_count(weights) doubles). 45 | * Integer weights are converted to double. */ 46 | void mio_weights_first_criterion(double *criterion, struct mio_weights *weights); 47 | /** Free a weight array. */ 48 | void mio_weights_free(struct mio_weights *weights); 49 | 50 | /** A mesh. */ 51 | struct mio_mesh; 52 | /** Read a mesh from the given file descriptor. 53 | * 54 | * Returns NULL on error. 55 | * The return value must be freed with mio_mesh_free. 56 | * The file descriptor is closed. */ 57 | struct mio_mesh *mio_mesh_read(int fd); 58 | /** Free the given mesh. */ 59 | void mio_mesh_free(struct mio_mesh *mesh); 60 | /** The dimension of the mesh. */ 61 | int mio_mesh_dimension(struct mio_mesh *mesh); 62 | /** The number of nodes/vertices in the mesh. */ 63 | uint64_t mio_mesh_node_count(struct mio_mesh *mesh); 64 | /** The coordinates of the given node/vertex. */ 65 | const double *mio_mesh_coordinates(struct mio_mesh *mesh, uintptr_t node_idx); 66 | 67 | /** The number of elements/cells in the mesh. */ 68 | uint64_t mio_mesh_element_count(struct mio_mesh *mesh); 69 | /** Information about an element. */ 70 | struct mio_element { 71 | int dimension; 72 | int node_count; 73 | /** node_count indices that can be passed to mio_mesh_coordinates. */ 74 | const uintptr_t *nodes; 75 | }; 76 | /** Retrieve information about an element. */ 77 | void mio_mesh_element(struct mio_element *element, struct mio_mesh *mesh, uintptr_t element_idx); 78 | 79 | #ifdef __cplusplus 80 | } 81 | #endif 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /tools/mesh-io/ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_safety_doc)] // See `meshio.h`. 2 | 3 | use mesh_io::Mesh; 4 | use std::ffi::c_int; 5 | use std::fs; 6 | use std::io; 7 | use std::os::unix::io::FromRawFd as _; 8 | use std::os::unix::io::IntoRawFd as _; 9 | use std::ptr; 10 | 11 | const ERROR_OTHER: c_int = -1; 12 | const ERROR_BAD_HEADER: c_int = -2; 13 | const ERROR_UNSUPPORTED_VERSION: c_int = -3; 14 | 15 | fn err_partition_code(err: mesh_io::partition::Error) -> c_int { 16 | match err { 17 | mesh_io::partition::Error::BadHeader => ERROR_BAD_HEADER, 18 | mesh_io::partition::Error::UnsupportedVersion => ERROR_UNSUPPORTED_VERSION, 19 | mesh_io::partition::Error::Io(_) => ERROR_OTHER, 20 | } 21 | } 22 | 23 | #[no_mangle] 24 | pub unsafe extern "C" fn mio_partition_read( 25 | size: *mut u64, 26 | partition: *mut *mut u64, 27 | fd: c_int, 28 | ) -> c_int { 29 | let f = fs::File::from_raw_fd(fd); 30 | let r = io::BufReader::new(f); 31 | match mesh_io::partition::read(r) { 32 | Ok(p) => { 33 | let mut p = p.into_boxed_slice(); 34 | *size = p.len() as u64; 35 | *partition = p.as_mut_ptr() as *mut u64; 36 | 0 37 | } 38 | Err(err) => err_partition_code(err), 39 | } 40 | } 41 | 42 | #[no_mangle] 43 | pub unsafe extern "C" fn mio_partition_write(fd: c_int, size: u64, partition: *const u64) -> c_int { 44 | let f = fs::File::from_raw_fd(fd); 45 | let mut w = io::BufWriter::new(f); 46 | let p = std::slice::from_raw_parts(partition, size as usize) 47 | .iter() 48 | .map(|id| *id as usize); 49 | if mesh_io::partition::write(&mut w, p).is_err() { 50 | return ERROR_OTHER; 51 | } 52 | match w.into_inner() { 53 | Ok(f) => { 54 | f.into_raw_fd(); 55 | 0 56 | } 57 | Err(_) => ERROR_OTHER, 58 | } 59 | } 60 | 61 | #[no_mangle] 62 | pub unsafe extern "C" fn mio_partition_free(size: u64, partition: *mut u64) { 63 | let size = size as usize; 64 | drop(Vec::from_raw_parts(partition, size, size)); 65 | } 66 | 67 | #[no_mangle] 68 | pub unsafe extern "C" fn mio_weights_read(fd: c_int) -> *mut mesh_io::weight::Array { 69 | let f = fs::File::from_raw_fd(fd); 70 | let mut r = io::BufReader::new(f); 71 | let w = match mesh_io::weight::read(&mut r) { 72 | Ok(w) => w, 73 | Err(_) => return ptr::null_mut(), 74 | }; 75 | Box::into_raw(Box::new(w)) 76 | } 77 | 78 | #[no_mangle] 79 | pub unsafe extern "C" fn mio_weights_count(weights: *const mesh_io::weight::Array) -> u64 { 80 | assert!(!weights.is_null()); 81 | 82 | let len = match &*weights { 83 | mesh_io::weight::Array::Integers(is) => is.len(), 84 | mesh_io::weight::Array::Floats(fs) => fs.len(), 85 | }; 86 | 87 | len.try_into().unwrap() 88 | } 89 | 90 | #[no_mangle] 91 | pub unsafe extern "C" fn mio_weights_first_criterion( 92 | criterion: *mut f64, 93 | weights: *const mesh_io::weight::Array, 94 | ) { 95 | assert!(!weights.is_null()); 96 | 97 | let weights = &*weights; 98 | let len = match weights { 99 | mesh_io::weight::Array::Integers(is) => is.len(), 100 | mesh_io::weight::Array::Floats(fs) => fs.len(), 101 | }; 102 | let criterion = std::slice::from_raw_parts_mut(criterion, len); 103 | 104 | match weights { 105 | mesh_io::weight::Array::Integers(is) => { 106 | for (c, w) in criterion.iter_mut().zip(is) { 107 | *c = w[0] as f64; 108 | } 109 | } 110 | mesh_io::weight::Array::Floats(fs) => { 111 | for (c, w) in criterion.iter_mut().zip(fs) { 112 | *c = w[0]; 113 | } 114 | } 115 | } 116 | } 117 | 118 | #[no_mangle] 119 | pub unsafe extern "C" fn mio_weights_free(weights: *mut mesh_io::weight::Array) { 120 | if !weights.is_null() { 121 | drop(Box::from_raw(weights)); 122 | } 123 | } 124 | 125 | #[no_mangle] 126 | pub unsafe extern "C" fn mio_mesh_read(fd: c_int) -> *mut Mesh { 127 | let f = fs::File::from_raw_fd(fd); 128 | let mut r = io::BufReader::new(f); 129 | let m = match Mesh::from_reader(&mut r) { 130 | Ok(m) => m, 131 | Err(_) => return ptr::null_mut(), 132 | }; 133 | Box::into_raw(Box::new(m)) 134 | } 135 | 136 | #[no_mangle] 137 | pub unsafe extern "C" fn mio_mesh_free(medit: *mut Mesh) { 138 | if !medit.is_null() { 139 | drop(Box::from_raw(medit)); 140 | } 141 | } 142 | 143 | #[no_mangle] 144 | pub unsafe extern "C" fn mio_mesh_dimension(medit: *const Mesh) -> c_int { 145 | assert!(!medit.is_null()); 146 | let dimension = (*medit).dimension(); 147 | dimension.try_into().unwrap() 148 | } 149 | 150 | #[no_mangle] 151 | pub unsafe extern "C" fn mio_mesh_node_count(medit: *const Mesh) -> u64 { 152 | assert!(!medit.is_null()); 153 | let count = (*medit).node_count(); 154 | count.try_into().unwrap() 155 | } 156 | 157 | #[no_mangle] 158 | pub unsafe extern "C" fn mio_mesh_coordinates(medit: *const Mesh, node_idx: usize) -> *const f64 { 159 | assert!(!medit.is_null()); 160 | let node = (*medit).node(node_idx).as_ptr(); 161 | node 162 | } 163 | 164 | #[no_mangle] 165 | pub unsafe extern "C" fn mio_mesh_element_count(medit: *const Mesh) -> u64 { 166 | assert!(!medit.is_null()); 167 | let count = (*medit).element_count(); 168 | count.try_into().unwrap() 169 | } 170 | 171 | #[repr(C)] 172 | pub struct MeditElement { 173 | dimension: c_int, 174 | node_count: c_int, 175 | nodes: *const usize, 176 | } 177 | 178 | #[no_mangle] 179 | pub unsafe extern "C" fn mio_mesh_element( 180 | element: *mut MeditElement, 181 | medit: *const Mesh, 182 | element_idx: usize, 183 | ) { 184 | assert!(!medit.is_null()); 185 | if let Some((el_type, el_nodes, _el_ref)) = (*medit).elements().nth(element_idx) { 186 | *element = MeditElement { 187 | dimension: el_type.dimension().try_into().unwrap(), 188 | node_count: el_nodes.len().try_into().unwrap(), 189 | nodes: el_nodes.as_ptr(), 190 | }; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /tools/mesh-io/src/medit/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module allows to load Medit mesh files, as 2 | //! described by Frey in 3 | //! [MEDIT : An interactive Mesh visualization Software](https://hal.inria.fr/inria-00069921). 4 | 5 | pub use parser::parse_ascii; 6 | pub use parser::parse_binary; 7 | pub use parser::test_format_ascii; 8 | pub use parser::test_format_binary; 9 | pub use parser::Error as ParseError; 10 | pub use serializer::DisplayAscii; 11 | 12 | mod parser; 13 | mod serializer; 14 | mod code { 15 | pub const DIMENSION: i64 = 3; 16 | pub const VERTEX: i64 = 4; 17 | pub const EDGE: i64 = 5; 18 | pub const TRIANGLE: i64 = 6; 19 | pub const QUAD: i64 = 7; 20 | pub const TETRAHEDRON: i64 = 8; 21 | pub const HEXAHEDRON: i64 = 9; 22 | pub const END: i64 = 54; 23 | } 24 | -------------------------------------------------------------------------------- /tools/mesh-io/src/medit/serializer.rs: -------------------------------------------------------------------------------- 1 | use super::code; 2 | use crate::ElementType; 3 | use crate::Mesh; 4 | use std::fmt; 5 | use std::io; 6 | 7 | /// Deserialize a mesh into the ASCII MEDIT format. 8 | /// 9 | /// This type implements [`Display`](fmt::Display). 10 | #[derive(Debug)] 11 | pub struct DisplayAscii<'a> { 12 | mesh: &'a Mesh, 13 | } 14 | 15 | impl Mesh { 16 | pub fn display_medit_ascii(&self) -> DisplayAscii<'_> { 17 | DisplayAscii { mesh: self } 18 | } 19 | } 20 | 21 | struct AsciiElementType(ElementType); 22 | 23 | impl fmt::Display for AsciiElementType { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | match self.0 { 26 | ElementType::Vertex => write!(f, "Vertices"), 27 | ElementType::Edge => write!(f, "Edges"), 28 | ElementType::Triangle => write!(f, "Triangles"), 29 | ElementType::Quadrangle => write!(f, "Quadrangles"), 30 | ElementType::Quadrilateral => write!(f, "Quadrilaterals"), 31 | ElementType::Tetrahedron => write!(f, "Tetrahedra"), 32 | ElementType::Hexahedron => write!(f, "Hexahedra"), 33 | } 34 | } 35 | } 36 | 37 | impl ElementType { 38 | fn code(self) -> i64 { 39 | match self { 40 | ElementType::Vertex => code::VERTEX, 41 | ElementType::Edge => code::EDGE, 42 | ElementType::Triangle => code::TRIANGLE, 43 | ElementType::Quadrangle | ElementType::Quadrilateral => code::QUAD, 44 | ElementType::Tetrahedron => code::TETRAHEDRON, 45 | ElementType::Hexahedron => code::HEXAHEDRON, 46 | } 47 | } 48 | } 49 | 50 | impl fmt::Display for DisplayAscii<'_> { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | write!( 53 | f, 54 | "MeshVersionFormatted 2\nDimension {}\n\nVertices\n\t{}\n", 55 | self.mesh.dimension, 56 | self.mesh.node_count(), 57 | )?; 58 | for (coordinates, node_ref) in self.mesh.nodes() { 59 | for coordinate in coordinates { 60 | write!(f, " {}", coordinate)?; 61 | } 62 | writeln!(f, " {}", node_ref)?; 63 | } 64 | for (element_type, nodes, refs) in &self.mesh.topology { 65 | if *element_type == ElementType::Vertex { 66 | // Breaks MEDIT and meshio-py. 67 | continue; 68 | } 69 | let element_count = refs.len(); 70 | write!( 71 | f, 72 | "\n{}\n\t{}\n", 73 | AsciiElementType(*element_type), 74 | element_count, 75 | )?; 76 | for (element, element_ref) in nodes.chunks(element_type.node_count()).zip(refs) { 77 | for node in element { 78 | write!(f, " {}", node + 1)?; 79 | } 80 | writeln!(f, " {}", element_ref)?; 81 | } 82 | } 83 | write!(f, "\nEnd") 84 | } 85 | } 86 | 87 | impl Mesh { 88 | pub fn serialize_medit_binary(&self, mut w: W) -> io::Result<()> { 89 | // Header 90 | w.write_all(&i32::to_le_bytes(1))?; // magic code 91 | w.write_all(&i32::to_le_bytes(4))?; // version 92 | w.write_all(&i32::to_le_bytes(code::DIMENSION as i32))?; 93 | let mut bitpos = 4 + 4 + 4 + 8 + 4; // byte position of the vertices 94 | w.write_all(&i64::to_le_bytes(bitpos as i64))?; 95 | w.write_all(&i32::to_le_bytes(self.dimension as i32))?; 96 | 97 | // Nodes 98 | w.write_all(&i32::to_le_bytes(code::VERTEX as i32))?; 99 | let node_count = self.node_refs.len(); 100 | bitpos += 8 * node_count * (self.dimension + 1); // +1 for the int ref 101 | bitpos += 4 + 8 + 8; // take into account the code, bitpos and node_count 102 | w.write_all(&i64::to_le_bytes(bitpos as i64))?; 103 | w.write_all(&i64::to_le_bytes(node_count as i64))?; 104 | for (coordinates, node_ref) in self.nodes() { 105 | for coordinate in coordinates { 106 | w.write_all(&f64::to_le_bytes(*coordinate))?; 107 | } 108 | w.write_all(&i64::to_le_bytes(node_ref as i64))?; 109 | } 110 | 111 | // Elements 112 | for (element_type, nodes, refs) in &self.topology { 113 | if *element_type == ElementType::Vertex { 114 | // Breaks MEDIT and meshio-py. 115 | continue; 116 | } 117 | w.write_all(&i32::to_le_bytes(element_type.code() as i32))?; 118 | let element_count = refs.len(); 119 | let nodes_per_element = element_type.node_count(); 120 | bitpos += 8 * element_count * (nodes_per_element + 1); // +1 for the int ref 121 | bitpos += 4 + 8 + 8; // take into account the code, bitpos and element_count 122 | w.write_all(&i64::to_le_bytes(bitpos as i64))?; 123 | w.write_all(&i64::to_le_bytes(element_count as i64))?; 124 | for (element, element_ref) in nodes.chunks(nodes_per_element).zip(refs) { 125 | for node in element { 126 | w.write_all(&i64::to_le_bytes(*node as i64 + 1))?; 127 | } 128 | w.write_all(&i64::to_le_bytes(*element_ref as i64))?; 129 | } 130 | } 131 | 132 | w.write_all(&i32::to_le_bytes(54))?; // End 133 | 134 | Ok(()) 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::super::parse_ascii; 141 | 142 | #[test] 143 | fn test_serialize() { 144 | let input = "MeshVersionFormatted 2 145 | Dimension 3 146 | 147 | Vertices 148 | \t4 149 | 2.3 0 1 0 150 | 1231 2 3.14 0 151 | -21.2 21 0.0001 0 152 | -0.2 -0.2 -0.2 0 153 | 154 | Triangles 155 | \t2 156 | 1 2 3 0 157 | 2 3 4 0 158 | 159 | End"; 160 | let mesh = parse_ascii(input.as_bytes()).unwrap(); 161 | let output = mesh.display_medit_ascii().to_string(); 162 | assert_eq!(input, output); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tools/mesh-io/src/partition.rs: -------------------------------------------------------------------------------- 1 | //! Partition file format encoder/decoder. 2 | //! 3 | //! See `mesh-part(1)` for a specification. 4 | 5 | use std::fmt; 6 | use std::io; 7 | 8 | // TODO compile_error when sizeof(usize) < sizeof(u64) 9 | 10 | #[derive(Debug)] 11 | pub enum Error { 12 | BadHeader, 13 | UnsupportedVersion, 14 | Io(io::Error), 15 | } 16 | 17 | impl From for Error { 18 | fn from(err: io::Error) -> Error { 19 | Error::Io(err) 20 | } 21 | } 22 | 23 | impl fmt::Display for Error { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | match self { 26 | Error::BadHeader => write!(f, "bad file header"), 27 | Error::UnsupportedVersion => write!(f, "unsupported file version"), 28 | Error::Io(_) => write!(f, "read/write error"), 29 | } 30 | } 31 | } 32 | 33 | impl std::error::Error for Error { 34 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 35 | match self { 36 | Error::Io(err) => Some(err), 37 | _ => None, 38 | } 39 | } 40 | } 41 | 42 | pub type Result = std::result::Result; 43 | 44 | /// Wrapping `r` in a [`std::io::BufReader`] is recommended. 45 | pub fn read(mut r: R) -> Result> 46 | where 47 | R: io::Read, 48 | { 49 | let mut header = [0x00; 4]; 50 | r.read_exact(&mut header)?; 51 | if &header != b"MePe" { 52 | return Err(Error::BadHeader); 53 | } 54 | 55 | let mut count_buf = [0x00; 8]; 56 | r.read_exact(&mut count_buf)?; 57 | let count = u64::from_le_bytes(count_buf) as usize; 58 | 59 | let mut partition = Vec::with_capacity(count); 60 | for _ in 0..count { 61 | let mut weight_buf = [0x00; 8]; 62 | r.read_exact(&mut weight_buf)?; 63 | partition.push(u64::from_le_bytes(weight_buf) as usize); 64 | } 65 | 66 | Ok(partition) 67 | } 68 | 69 | /// Wrapping `w` in a [`std::io::BufWriter`] is recommended. 70 | pub fn write(mut w: W, array: I) -> io::Result<()> 71 | where 72 | I: IntoIterator, 73 | I::IntoIter: ExactSizeIterator, 74 | W: io::Write, 75 | { 76 | let array = array.into_iter(); 77 | 78 | write!(w, "MePe")?; 79 | w.write_all(&u64::to_le_bytes(array.len() as u64))?; 80 | 81 | for id in array { 82 | w.write_all(&u64::to_le_bytes(id as u64))?; 83 | } 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /tools/mesh-io/src/weight.rs: -------------------------------------------------------------------------------- 1 | //! Weight file format encoder/decoder. 2 | //! 3 | //! See `weight-gen(1)` for a specification. 4 | 5 | use std::any::TypeId; 6 | use std::fmt; 7 | use std::io; 8 | 9 | // TODO compile_error when sizeof(usize) < sizeof(u64) 10 | 11 | const VERSION: u8 = 1; 12 | const FLAG_INTEGER: u8 = 1 << 0; 13 | 14 | #[derive(Debug)] 15 | pub enum Array { 16 | Integers(Vec>), 17 | Floats(Vec>), 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum Error { 22 | BadHeader, 23 | UnsupportedVersion, 24 | Io(io::Error), 25 | } 26 | 27 | impl From for Error { 28 | fn from(err: io::Error) -> Error { 29 | Error::Io(err) 30 | } 31 | } 32 | 33 | impl fmt::Display for Error { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | Error::BadHeader => write!(f, "bad file header"), 37 | Error::UnsupportedVersion => write!(f, "unsupported file version"), 38 | Error::Io(_) => write!(f, "read/write error"), 39 | } 40 | } 41 | } 42 | 43 | impl std::error::Error for Error { 44 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 45 | match self { 46 | Error::Io(err) => Some(err), 47 | _ => None, 48 | } 49 | } 50 | } 51 | 52 | pub type Result = std::result::Result; 53 | 54 | fn read_inner(mut r: R, criterion_count: usize, from_bytes: F) -> Result>> 55 | where 56 | F: Fn(&[u8]) -> T + Copy, 57 | R: io::Read, 58 | { 59 | let mut count_buf = [0x00; 8]; 60 | r.read_exact(&mut count_buf)?; 61 | let weight_count = u64::from_le_bytes(count_buf) as usize; 62 | 63 | let mut weights = Vec::with_capacity(weight_count); 64 | 65 | for _ in 0..weight_count { 66 | let mut weight_buf = vec![0x00; criterion_count * 8]; 67 | r.read_exact(&mut weight_buf)?; 68 | let weight = weight_buf.chunks_exact(8).map(from_bytes).collect(); 69 | weights.push(weight); 70 | } 71 | 72 | Ok(weights) 73 | } 74 | 75 | /// Wrapping `r` in a [`std::io::BufReader`] is recommended. 76 | /// 77 | /// TODO checksum input data 78 | pub fn read(mut r: R) -> Result 79 | where 80 | R: io::Read, 81 | { 82 | let mut header = [0x00; 4]; 83 | r.read_exact(&mut header)?; 84 | if &header != b"MeWe" { 85 | return Err(Error::BadHeader); 86 | } 87 | 88 | let mut flags = [0x00; 4]; 89 | r.read_exact(&mut flags)?; 90 | let version = flags[0]; 91 | if version != VERSION { 92 | return Err(Error::UnsupportedVersion); 93 | } 94 | let is_integer = (flags[1] & FLAG_INTEGER) != 0; 95 | let criterion_count = u16::from_le_bytes([flags[2], flags[3]]) as usize; 96 | if criterion_count == 0 { 97 | return Ok(Array::Integers(Vec::new())); 98 | } 99 | 100 | Ok(if is_integer { 101 | Array::Integers(read_inner(r, criterion_count, |bytes| { 102 | let bytes = <[u8; 8]>::try_from(bytes).unwrap(); 103 | i64::from_le_bytes(bytes) 104 | })?) 105 | } else { 106 | Array::Floats(read_inner(r, criterion_count, |bytes| { 107 | let bytes = <[u8; 8]>::try_from(bytes).unwrap(); 108 | f64::from_le_bytes(bytes) 109 | })?) 110 | }) 111 | } 112 | 113 | fn write_inner(mut w: W, array: I, to_bytes: F) -> Result<()> 114 | where 115 | I: IntoIterator, 116 | I::IntoIter: ExactSizeIterator, 117 | I::Item: IntoIterator, 118 | ::IntoIter: ExactSizeIterator, 119 | F: Fn(T) -> [u8; 8], 120 | T: 'static, 121 | W: io::Write, 122 | { 123 | let flags: u8 = if TypeId::of::() == TypeId::of::() { 124 | FLAG_INTEGER 125 | } else { 126 | 0 127 | }; 128 | 129 | let mut array = array.into_iter(); 130 | let len = array.len(); 131 | let first = match array.next() { 132 | Some(v) => v.into_iter(), 133 | None => { 134 | let buf = [ 135 | b'M', b'e', b'W', b'e', VERSION, flags, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136 | ]; 137 | w.write_all(&buf)?; 138 | return Ok(()); 139 | } 140 | }; 141 | let criterion_count = first.len(); 142 | assert!( 143 | criterion_count < std::mem::size_of::(), 144 | "Too many criterions", 145 | ); 146 | 147 | write!(w, "MeWe")?; 148 | w.write_all(&[VERSION, flags])?; 149 | w.write_all(&u16::to_le_bytes(criterion_count as u16))?; 150 | w.write_all(&u64::to_le_bytes(len as u64))?; 151 | 152 | let mut write_weight = move |weight: ::IntoIter| -> Result<()> { 153 | for criterion in weight { 154 | w.write_all(&to_bytes(criterion))?; 155 | } 156 | Ok(()) 157 | }; 158 | 159 | write_weight(first)?; 160 | array 161 | .map(IntoIterator::into_iter) 162 | .try_for_each(write_weight) 163 | } 164 | 165 | /// Wrapping `r` in a [`std::io::BufWriter`] is recommended. 166 | /// 167 | /// TODO checksum input data 168 | pub fn write_integers(w: W, array: I) -> Result<()> 169 | where 170 | I: IntoIterator, 171 | I::IntoIter: ExactSizeIterator, 172 | I::Item: IntoIterator, 173 | ::IntoIter: ExactSizeIterator, 174 | W: io::Write, 175 | { 176 | write_inner(w, array, i64::to_le_bytes) 177 | } 178 | 179 | /// Wrapping `w` in a [`std::io::BufWriter`] is recommended. 180 | /// 181 | /// TODO checksum input data 182 | pub fn write_floats(w: W, array: I) -> Result<()> 183 | where 184 | I: IntoIterator, 185 | I::IntoIter: ExactSizeIterator, 186 | I::Item: IntoIterator, 187 | ::IntoIter: ExactSizeIterator, 188 | W: io::Write, 189 | { 190 | write_inner(w, array, f64::to_le_bytes) 191 | } 192 | -------------------------------------------------------------------------------- /tools/num-part/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "num-part" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Hubert Hirtz "] 6 | description = "Command-line framework to measure quality of number-partitioning algorithms" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["cli", "partitioning"] 9 | categories = ["algorithms", "command-line-utilities", "mathematics"] 10 | readme = "../README.md" 11 | repository = "https://github.com/LIHPC-Computational-Geometry/coupe" 12 | 13 | 14 | [dependencies] 15 | 16 | # Algorithm parsing 17 | coupe-tools = { path = "..", version = "0.1", default-features = false } 18 | mesh-io = { path = "../mesh-io", version = "0.1" } 19 | 20 | # Random number generation 21 | rand = { version = "0.8", default-features = false, features = ["std"] } 22 | rand_distr = { version = "0.4", default-features = false, features = ["std"] } 23 | rand_pcg = { version = "0.3", default-features = false } 24 | itertools = { version = "0.12", default-features = false } 25 | 26 | # Partitioners 27 | coupe = { version = "0.1", path = "../.." } 28 | 29 | # SQLite interface to save experiments 30 | rusqlite = { version = "0.30", default-features = false } 31 | 32 | # Command-line interface 33 | getopts = { version = "0.2", default-features = false } 34 | anyhow = { version = "1", default-features = false, features = ["std"] } 35 | 36 | # HTML reports TODO 37 | -------------------------------------------------------------------------------- /tools/num-part/README.md: -------------------------------------------------------------------------------- 1 | # num-part 2 | 3 | A quality evaluation framework for number-partitioning algorithms. 4 | 5 | This program generates sets of random numbers that follow a given distribution, 6 | runs algorithms on these sets, then saves the results in a SQLite database. 7 | 8 | ## Usage 9 | 10 | See `num-part --help` for usage instructions. 11 | 12 | The syntax for algorithms is the same as `mesh-part(1)`. However, only number 13 | partitioning algorithms will work. 14 | 15 | After some runs, you can query the SQLite database for results. For example, 16 | 17 | ``` 18 | sqlite3 num_part.db < Result<()> { 12 | let tx = database 13 | .transaction() 14 | .context("failed to begin transaction")?; 15 | 16 | let my_version = MIGRATIONS.len(); 17 | let db_version = tx 18 | .query_row("PRAGMA user_version", [], |row| row.get(0)) 19 | .context("failed to query database version")?; 20 | 21 | if my_version == db_version { 22 | // Already at the latest version. 23 | return Ok(()); 24 | } 25 | if my_version < db_version { 26 | anyhow::bail!( 27 | "Your version ({}) is older than the database's ({})", 28 | my_version, 29 | db_version 30 | ); 31 | } 32 | 33 | eprintln!( 34 | "Upgrading database from version {} to version {}", 35 | db_version, my_version 36 | ); 37 | for (v, migration) in MIGRATIONS[db_version..my_version].iter().enumerate() { 38 | tx.execute_batch(migration) 39 | .with_context(|| format!("failed to execute migration #{}", v))?; 40 | } 41 | 42 | // PRAGMA user_version does not seem to support prepared statements. 43 | tx.execute_batch(&format!("PRAGMA user_version = {}", my_version)) 44 | .context("failed to update database version")?; 45 | tx.commit().context("failed to commit transaction")?; 46 | 47 | Ok(()) 48 | } 49 | 50 | fn default_path() -> Result { 51 | let cargo_target_dir = env::var("CARGO_TARGET_DIR").context("quoi")?; 52 | let mut path = PathBuf::from(cargo_target_dir); 53 | path.push("part"); 54 | Ok(path) 55 | } 56 | 57 | pub struct Experiment<'a> { 58 | pub algorithm: &'a str, 59 | pub seed_id: i64, 60 | pub distribution_id: i64, 61 | pub weight_count: usize, 62 | pub iteration: usize, 63 | pub case_type: bool, 64 | pub imbalance: f64, 65 | pub algo_iterations: Option, 66 | } 67 | 68 | pub struct Database { 69 | conn: rusqlite::Connection, 70 | } 71 | 72 | pub fn open(filename: Option) -> Result { 73 | let filename = match filename { 74 | Some(s) => PathBuf::from(s), 75 | None => default_path()?, 76 | }; 77 | let mut conn = rusqlite::Connection::open(filename)?; 78 | upgrade_schema(&mut conn).context("failed to upgrade the database schema")?; 79 | Ok(Database { conn }) 80 | } 81 | 82 | impl Database { 83 | pub fn insert_distribution(&mut self, name: &str, params: [f64; 3]) -> Result { 84 | let tx = self 85 | .conn 86 | .transaction() 87 | .context("failed to begin transaction")?; 88 | 89 | let id = tx 90 | .query_row( 91 | "SELECT id FROM distribution 92 | WHERE name = ? AND param1 = ? AND param2 = ? AND param3 = ?", 93 | rusqlite::params![name, params[0], params[1], params[2]], 94 | |row| row.get(0), 95 | ) 96 | .optional() 97 | .context("failed to query database")?; 98 | 99 | if let Some(id) = id { 100 | return Ok(id); 101 | } 102 | 103 | tx.execute( 104 | "INSERT INTO 105 | distribution (name, param1, param2, param3) 106 | VALUES (? , ? , ? , ? )", 107 | rusqlite::params![name, params[0], params[1], params[2]], 108 | ) 109 | .context("failed to insert row")?; 110 | let id = tx.last_insert_rowid(); 111 | tx.commit().context("failed to commit transaction")?; 112 | 113 | Ok(id) 114 | } 115 | 116 | pub fn insert_seed(&mut self, seed: &[u8]) -> Result { 117 | let tx = self 118 | .conn 119 | .transaction() 120 | .context("failed to begin transaction")?; 121 | 122 | let id = tx 123 | .query_row("SELECT id FROM seed WHERE bytes = ?", [seed], |row| { 124 | row.get(0) 125 | }) 126 | .optional() 127 | .context("failed to query database")?; 128 | 129 | if let Some(id) = id { 130 | return Ok(id); 131 | } 132 | 133 | tx.execute("INSERT INTO seed(bytes) VALUES(?)", [seed]) 134 | .context("failed to insert row")?; 135 | let id = tx.last_insert_rowid(); 136 | tx.commit().context("failed to commit transaction")?; 137 | 138 | Ok(id) 139 | } 140 | 141 | pub fn insert_experiment(&mut self, e: Experiment<'_>) -> Result<()> { 142 | self.conn.execute( 143 | "INSERT OR IGNORE INTO 144 | experiment (algorithm, seed, distribution, sample_size, iteration, case_type, imbalance, algo_iterations) 145 | VALUES (? , ? , ? , ? , ? , ? , ? , ? )", 146 | rusqlite::params![ 147 | e.algorithm, 148 | e.seed_id, 149 | e.distribution_id, 150 | e.weight_count, 151 | e.iteration, 152 | e.case_type, 153 | e.imbalance, 154 | e.algo_iterations, 155 | ], 156 | )?; 157 | Ok(()) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tools/num-part/src/help_after.txt: -------------------------------------------------------------------------------- 1 | DISTRIBUTION 2 | 3 | The option "-d, --distribution" defines how input weights are generated. It 4 | can be specified multiple times, one for each criterion. Its value must 5 | follow the following syntax: 6 | 7 | distribution := distribution-name *( "," distribution-param ) 8 | distribution-name := one of the distributions in the following list 9 | distribution-param := any floating-point number (eg. "42.01") 10 | 11 | For example, the following will generate a one-criterion number set 12 | distributed uniformily between 0 and 100: 13 | 14 | --distribution uniform,0,100 15 | 16 | And the following example shows how to generate a two-criteria vector-of- 17 | number set, which first criterion follows a normal distribution and the 18 | second a beta distribution: 19 | 20 | --distribution normal,100,2 --distribution beta,0.2,0.2 21 | 22 | 23 | SUPPORTED DISTRIBUTIONS 24 | 25 | uniform LOWER_BOUND UPPER_BOUND 26 | The uniform distribution. Takes two parameters: one for each bound. 27 | 28 | normal MEAN STD_DEV 29 | The normal/gaussian distribution. Takes two parameters: the first is 30 | for the mean/average and the other for the standard deviation. 31 | 32 | exp LAMBDA 33 | The exponential distribution, whose density is 34 | LAMBDA * exp(-LAMBDA * x) 35 | 36 | pareto SCALE SHAPE 37 | The pareto distribution, whose density is 38 | SHAPE * SCALE^SHAPE / x^(SHAPE + 1) 39 | 40 | beta ALPHA BETA [SCALE] 41 | The beta distribution. Both first parameters must be strictly greater 42 | than zero. The third "SCALE" parameter may be used to change the upper 43 | bound, so that this distribution may be used with integers. Defaults to 44 | 1. 45 | -------------------------------------------------------------------------------- /tools/report/common.sh: -------------------------------------------------------------------------------- 1 | ALGORITHMS=" 2 | -a kk,2 3 | -a kk,2 -a arcswap,0.05 4 | -a kk,2 -a fm,0.05 5 | -a kk,2 -a fm,0.05,16 6 | -a rcb,1 7 | -a rcb,1 -a arcswap,0.05 8 | -a rcb,1 -a fm,0.05 9 | -a rcb,1 -a fm,0.05,16 10 | -a rcb,1 -a vn-best 11 | -a rcb,1 -a vn-best -a arcswap,0.05 12 | -a rcb,1 -a vn-first 13 | -a rcb,1 -a vn-first -a arcswap,0.05 14 | -a hilbert,2 15 | -a hilbert,2 -a arcswap,0.05 16 | -a hilbert,2 -a fm,0.05 17 | -a hilbert,2 -a fm,0.05,16 18 | -a hilbert,2 -a vn-best 19 | -a hilbert,2 -a vn-best -a arcswap,0.05 20 | -a hilbert,2 -a vn-first 21 | -a hilbert,2 -a vn-first -a arcswap,0.05 22 | 23 | -a kk,3 24 | -a kk,3 -a arcswap,0.05 25 | -a hilbert,3 26 | -a hilbert,3 -a arcswap,0.05 27 | -a hilbert,3 -a vn-best 28 | -a hilbert,3 -a vn-best -a arcswap,0.05 29 | -a hilbert,3 -a vn-first 30 | -a hilbert,3 -a vn-first -a arcswap,0.05 31 | 32 | -a kk,4 33 | -a kk,4 -a arcswap,0.05 34 | -a rcb,2 35 | -a rcb,2 -a arcswap,0.05 36 | -a rcb,2 -a vn-best 37 | -a rcb,2 -a vn-best -a arcswap,0.05 38 | -a rcb,2 -a vn-first 39 | -a rcb,2 -a vn-first -a arcswap,0.05 40 | -a hilbert,4 41 | -a hilbert,4 -a arcswap,0.05 42 | -a hilbert,4 -a vn-best 43 | -a hilbert,4 -a vn-best -a arcswap,0.05 44 | -a hilbert,4 -a vn-first 45 | -a hilbert,4 -a vn-first -a arcswap,0.05 46 | 47 | -a kk,128 48 | -a kk,128 -a arcswap,0.05 49 | -a rcb,7 50 | -a rcb,7 -a arcswap,0.05 51 | -a rcb,7 -a vn-best 52 | -a rcb,7 -a vn-best -a arcswap,0.05 53 | -a rcb,7 -a vn-first 54 | -a rcb,7 -a vn-first -a arcswap,0.05 55 | -a hilbert,128 56 | -a hilbert,128 -a arcswap,0.05 57 | -a hilbert,128 -a vn-best 58 | -a hilbert,128 -a vn-best -a arcswap,0.05 59 | -a hilbert,128 -a vn-first 60 | -a hilbert,128 -a vn-first -a arcswap,0.05 61 | " 62 | 63 | WEIGHT_DISTRIBUTIONS=" 64 | constant,1 65 | linear,x,0,100 66 | spike,1000 67 | " 68 | 69 | say() { 70 | verb=$1 71 | shift 72 | 73 | YELLOW="\e[33;1m" 74 | RESET="\e[0m" 75 | if ! [ -t 2 ] 76 | then 77 | # Not a tty, dont use escape codes 78 | YELLOW="" 79 | RESET="" 80 | fi 81 | 82 | MAX_VERB_LEN=12 83 | verblen=${#verb} 84 | indent=$((MAX_VERB_LEN - verblen)) 85 | 86 | printf "%${indent}s$YELLOW%s$RESET %s\n" "" "$verb" "$@" >&2 87 | } 88 | 89 | sayfile() { 90 | verb=$1 91 | file=$2 92 | 93 | say "$verb" "$(basename "$file")" 94 | } 95 | 96 | command -v jq >/dev/null || { 97 | echo "command 'jq' not found" >&2 98 | exit 1 99 | } 100 | 101 | TARGET_DIR=$(cargo metadata --format-version=1 | jq -r '.target_directory') 102 | -------------------------------------------------------------------------------- /tools/report/efficiency: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . "$(dirname "$0")"/common.sh 5 | 6 | command -v gnuplot >/dev/null || { 7 | echo "command 'gnuplot' not found" >&2 8 | exit 1 9 | } 10 | 11 | [ $# -lt 2 ] && { 12 | echo "usage: $0 BASELINE BENCHMARK_GROUPS..." >&2 13 | exit 1 14 | } 15 | 16 | BASELINE=$1 17 | shift 18 | 19 | csv="$BASELINE.strong.csv" 20 | svg="$BASELINE.strong.svg" 21 | csv_wt="$BASELINE.wt.csv" 22 | svg_wt="$BASELINE.wt.svg" 23 | 24 | bg_path() { 25 | benchmark_group=$1 26 | 27 | benchmark_dir=$(echo "$benchmark_group" | tr ':' '_' | cut -b-64) 28 | echo "$TARGET_DIR/criterion/$benchmark_dir" 29 | } 30 | 31 | time_ns() { 32 | benchmark_group=$1 33 | threads=$2 34 | 35 | path="$(bg_path "$benchmark_group")/threads=$threads/$BASELINE/estimates.json" 36 | if [ -f "$path" ] 37 | then jq -r '.mean.point_estimate' <"$path" 38 | else return 1 39 | fi 40 | } 41 | 42 | stddev_ns() { 43 | threads=$1 44 | 45 | path="$(bg_path "$benchmark_group")/threads=$threads/$BASELINE/estimates.json" 46 | if [ -f "$path" ] 47 | then jq -r '.mean.standard_error' <"$path" 48 | else return 1 49 | fi 50 | } 51 | 52 | sayfile Aggregating "$csv_wt" 53 | 54 | thread_counts=$( 55 | for benchmark_group in "$@" 56 | do 57 | for bench in "$(bg_path "$benchmark_group")/threads="* 58 | do 59 | thread_count=$(echo "$bench" | sed 's/.*threads=\(.*\)/\1/') 60 | echo "$thread_count" 61 | done 62 | done | sort -n | uniq 63 | ) 64 | 65 | ( 66 | printf "thread count" 67 | for benchmark_group in "$@" 68 | do 69 | group=$(echo "$benchmark_group" | tr ';' '_') 70 | printf ";%s" "$group" 71 | done 72 | printf "\n" 73 | ) >"$csv_wt" 74 | 75 | echo "$thread_counts" | while read -r thread_count 76 | do 77 | printf "%d" "$thread_count" 78 | for benchmark_group in "$@" 79 | do 80 | if t=$(time_ns "$benchmark_group" "$thread_count") 81 | then 82 | printf ";%f" "$t" 83 | else 84 | printf ";" 85 | fi 86 | done 87 | printf "\n" 88 | done >>"$csv_wt" 89 | 90 | sayfile Aggregating "$csv" 91 | 92 | awk ' 93 | BEGIN { FS = ";"; OFS = ";" } 94 | (NR == 1) { print } 95 | (NR == 2) { 96 | for (i = 2; i <= NF; i++) { t_seq[i] = $i; $i = 1 } 97 | print 98 | } 99 | (NR > 2) { 100 | for (i = 2; i <= NF; i++) { 101 | if ($i != "") { $i = t_seq[i] / $i } 102 | } 103 | print 104 | } 105 | ' "$csv_wt" >"$csv" 106 | 107 | speedup_bound=$(awk ' 108 | BEGIN { max_speedup = 0; FS = ";" } 109 | (NR > 1) { 110 | for (i = 2; i <= NF; i++) { 111 | if (max_speedup < $i) { max_speedup = $i } 112 | } 113 | } 114 | END { print max_speedup * 1.05 } 115 | ' "$csv") 116 | 117 | max_thread_count=$(echo "$thread_counts" | tail -n1) 118 | plot_cmds() { 119 | csv="$1" 120 | shift 121 | 122 | i=2 123 | for p in "$@" 124 | do 125 | p=$(echo "$p" | sed 's/_/\\_/g') 126 | printf "'%s' using 1:%d title '%s' with linespoints" "$csv" "$i" "$p" 127 | if [ $i -le $# ] 128 | then printf ", " 129 | fi 130 | i=$((i+1)) 131 | done 132 | } 133 | 134 | sayfile Rendering "$svg" 135 | 136 | gnuplot <&2 39 | 40 | for _ in $(seq $iter_count) 41 | do 42 | info=$( 43 | RAYON_NUM_THREADS=4 mesh-part $ARGS $algorithm | 44 | part-info $ARGS -p /dev/stdin 45 | ) 46 | edge_cut=$(echo "$info" | grep edge | cut -f2 -d:) 47 | imbalance=$(echo "$info" | grep imbalances | sed 's/.*\[\(.*\)\]/\1/') 48 | echo "$edge_cut;$imbalance" 49 | done >"$tmpdir/$algorithm" 50 | done 51 | 52 | say Drawing histo.svg 53 | gnuplot <"$weight_file" 2>/dev/null 47 | 48 | run_algorithm "$mesh_file" "$weight_file" "$ALGORITHM" 49 | 50 | j=$((j+1)) 51 | done 52 | 53 | i=$((i+1)) 54 | done 55 | 56 | say Report "$HTML_REPORT" 57 | -------------------------------------------------------------------------------- /tools/report/quality: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . "$(dirname "$0")"/common.sh 5 | cargo build -p coupe-tools --bins --release 6 | PATH="$TARGET_DIR/release:$PATH" 7 | 8 | OUTPUT_DIR="$TARGET_DIR/coupe-report" 9 | say mkdir "$OUTPUT_DIR" 10 | mkdir -p "$OUTPUT_DIR" 11 | 12 | run_algorithm() { 13 | mesh_file=$1 14 | weight_file=$2 15 | algorithm=$3 16 | 17 | algorithm_id=$(echo "$algorithm" | cut -b4- | sed 's/ -a /-/g') 18 | partition_file="${weight_file%.*}.$algorithm_id.part" 19 | 20 | log_file="${partition_file%.*}.log" 21 | out_mesh="${partition_file%.*}.mesh" 22 | out_svg="${out_mesh%.*}.svg" 23 | echo "$out_svg" 24 | 25 | sayfile Computing "$partition_file" 26 | mesh-part --mesh "$mesh_file" --weights "$weight_file" --edge-weights linear $algorithm --verbose \ 27 | >"$partition_file" \ 28 | 2>"$log_file" \ 29 | || { 30 | rm "$partition_file" 31 | return 1 32 | } 33 | 34 | sayfile Rendering "$out_mesh" 35 | apply-part --mesh "$mesh_file" --partition "$partition_file" >"$out_mesh" 36 | 37 | sayfile Rendering "$out_svg" 38 | medit2svg <"$out_mesh" >"$out_svg" 2>/dev/null 39 | } 40 | 41 | HTML_REPORT="$OUTPUT_DIR/quality.html" 42 | 43 | if [ $# -eq 0 ] 44 | then 45 | echo "usage: $0 MESHDIR" 46 | exit 1 47 | fi 48 | INPUT_DIR=$1 49 | 50 | cat >"$HTML_REPORT" < 52 | 53 | 54 | 118 |

119 | EOF 120 | 121 | i=0 122 | for mesh_file in "$INPUT_DIR"/*.mesh 123 | do 124 | [ -z "$mesh_file" ] && continue 125 | 126 | attrs="" 127 | if [ $i -eq 0 ] 128 | then attrs="checked" 129 | fi 130 | 131 | cat >>"$HTML_REPORT" <<-EOF 132 |
133 | 134 | 135 |
136 | EOF 137 | 138 | j=0 139 | echo "$WEIGHT_DISTRIBUTIONS" | while read -r weight_distribution 140 | do 141 | [ -z "$weight_distribution" ] && continue 142 | 143 | attrs="" 144 | if [ $j -eq 0 ] 145 | then attrs="checked" 146 | fi 147 | 148 | cat >>"$HTML_REPORT" <<-EOF 149 |
150 | 151 | 152 |
153 | EOF 154 | 155 | weight_file="$OUTPUT_DIR/$(basename "${mesh_file%.*}").$weight_distribution.weights" 156 | sayfile Generating "$weight_file" 157 | weight-gen -d "$weight_distribution" <"$mesh_file" >"$weight_file" 2>/dev/null 158 | 159 | echo "$ALGORITHMS" | while read -r algorithm 160 | do 161 | algorithm=$(echo "$algorithm" | sed 's/#.*//') 162 | [ -z "$algorithm" ] && continue 163 | 164 | if svg_file=$(run_algorithm "$mesh_file" "$weight_file" "$algorithm") 165 | then 166 | cat >>"$HTML_REPORT" <<-EOF 167 |
168 | Algorithm: $algorithm 169 |
170 | 				$(cat "${svg_file%.*}.log")
171 | 				$(part-info --mesh "$mesh_file" --weights "$weight_file" --partition "${svg_file%.*}.part" --edge-weights linear)
172 | 				mesh file: ${svg_file%.*}.mesh
173 | 				
174 | $mesh_file; $weight_distribution; $algorithm 175 |
176 | EOF 177 | else 178 | cat >>"$HTML_REPORT" <<-EOF 179 |
180 | Algorithm: $algorithm 181 |
182 | 				$(cat "${svg_file%.*}.log")
183 | 				
184 |
185 | EOF 186 | fi 187 | done 188 | 189 | cat >>"$HTML_REPORT" <<-EOF 190 |
191 |
192 | EOF 193 | j=$((j+1)) 194 | done 195 | 196 | cat >>"$HTML_REPORT" <<-EOF 197 |
198 |
199 | EOF 200 | i=$((i+1)) 201 | done 202 | 203 | cat >>"$HTML_REPORT" < 205 | EOF 206 | 207 | say Report "$HTML_REPORT" 208 | -------------------------------------------------------------------------------- /tools/report/weak-scaling: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Usage example: 4 | # 5 | # echo "1 2 $(seq 4 4 63) $(seq 64 8 127) $(seq 128 16 256)" | 6 | # ./tools/report/weak-scaling monkey.meshb spike,1000 -a random,2 -a fm,0.04 7 | 8 | set -e 9 | 10 | . "$(dirname "$0")"/common.sh 11 | cargo build -p coupe-tools --bins --release 12 | PATH="$TARGET_DIR/release:$PATH" 13 | 14 | MESH_FILE=$1 15 | shift 16 | 17 | WEIGHT_DIST=$1 18 | shift 19 | 20 | tmp_mesh=$(echo 'maketemp(/dev/shm/weak-scaling.XXXXXX)' | m4) 21 | tmp_weights="$tmp_mesh".weights 22 | trap 'rm -f $tmp_mesh $tmp_weights' 0 2 3 15 23 | 24 | bench_name=$(basename "$tmp_mesh" | tr '.' ';') 25 | for arg 26 | do 27 | if [ "$arg" != "${arg#-}" ] 28 | then continue 29 | fi 30 | bench_name="$bench_name;$arg" 31 | done 32 | 33 | mesh_dup() { 34 | n=$1 35 | 36 | say mesh-dup "$MESH_FILE ($n times)" 37 | rm "$tmp_mesh" 38 | mesh-dup -n "$n" <"$MESH_FILE" >"$tmp_mesh" 39 | } 40 | 41 | weight_gen() { 42 | say weight-gen "$tmp_weights" 43 | weight-gen --distribution "$WEIGHT_DIST" <"$tmp_mesh" >"$tmp_weights" 44 | } 45 | 46 | part_bench() { 47 | thread_count=$1 48 | shift 49 | 50 | say part-bench "$thread_count threads" 51 | part-bench --mesh "$tmp_mesh" \ 52 | --weights "$tmp_weights" \ 53 | --sample-size 10 \ 54 | --edge-weights linear \ 55 | --efficiency="$thread_count" \ 56 | "$@" 57 | } 58 | 59 | [ -t 0 ] && echo "Reading thread counts from stdin..." 2>&1 60 | awk '{ for (i = 1; i <= NF; i++) { print $i } }' | while read -r thread_count 61 | do 62 | mesh_dup "$thread_count" 63 | weight_gen 64 | 65 | say sleep "Cooling CPUs down for 4s..." 66 | sleep 4 67 | 68 | part_bench "$thread_count" "$@" 69 | done 70 | -------------------------------------------------------------------------------- /tools/src/bin/apply-part.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | use mesh_io::ElementType; 4 | use std::fs; 5 | use std::io; 6 | 7 | const USAGE: &str = "Usage: apply-part [options] [out-mesh] >out.mesh"; 8 | 9 | fn main() -> Result<()> { 10 | let mut options = getopts::Options::new(); 11 | options.optopt("f", "format", "output format", "EXT"); 12 | options.optopt("m", "mesh", "mesh file", "FILE"); 13 | options.optopt("p", "partition", "partition file", "FILE"); 14 | 15 | let matches = coupe_tools::parse_args(options, USAGE, 1)?; 16 | 17 | let format = matches 18 | .opt_get("f") 19 | .context("invalid value for option 'format'")?; 20 | 21 | let mesh_file = matches 22 | .opt_str("m") 23 | .context("missing required option 'mesh'")?; 24 | let mut mesh = mesh_io::Mesh::from_file(mesh_file).context("failed to read mesh file")?; 25 | 26 | let partition_file = matches 27 | .opt_str("p") 28 | .context("missing required option 'partition'")?; 29 | let partition_file = fs::File::open(partition_file).context("failed to open partition file")?; 30 | let partition_file = io::BufReader::new(partition_file); 31 | let parts = 32 | mesh_io::partition::read(partition_file).context("failed to read partition file")?; 33 | 34 | if let Some(element_dim) = mesh 35 | .topology() 36 | .iter() 37 | .map(|(el_type, _, _)| el_type.dimension()) 38 | .max() 39 | { 40 | mesh.elements_mut() 41 | .filter(|(element_type, _, _)| { 42 | element_type.dimension() == element_dim && *element_type != ElementType::Edge 43 | }) 44 | .zip(parts) 45 | .for_each(|((_, _, element_ref), part)| *element_ref = part as isize); 46 | } 47 | 48 | coupe_tools::write_mesh(&mesh, format, matches.free.first())?; 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /tools/src/bin/apply-weight.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | use mesh_io::ElementType; 4 | use mesh_io::Mesh; 5 | use std::fs; 6 | use std::io; 7 | 8 | const USAGE: &str = "Usage: apply-weight [options] [out-mesh] >out.mesh"; 9 | 10 | fn apply(mesh: &mut Mesh, weights: impl Iterator) { 11 | if let Some(element_dim) = mesh 12 | .topology() 13 | .iter() 14 | .map(|(el_type, _, _)| el_type.dimension()) 15 | .max() 16 | { 17 | mesh.elements_mut() 18 | .filter(|(element_type, _, _)| { 19 | element_type.dimension() == element_dim && *element_type != ElementType::Edge 20 | }) 21 | .zip(weights) 22 | .for_each(|((_, _, element_ref), weight)| *element_ref = weight); 23 | } 24 | } 25 | 26 | fn main() -> Result<()> { 27 | let mut options = getopts::Options::new(); 28 | options.optopt("f", "format", "output format", "EXT"); 29 | options.optopt("m", "mesh", "mesh file", "FILE"); 30 | options.optopt("w", "weights", "weight file", "FILE"); 31 | 32 | let matches = coupe_tools::parse_args(options, USAGE, 1)?; 33 | 34 | let format = matches 35 | .opt_get("f") 36 | .context("invalid value for option 'format'")?; 37 | 38 | let mesh_file = matches 39 | .opt_str("m") 40 | .context("missing required option 'mesh'")?; 41 | let mut mesh = Mesh::from_file(mesh_file).context("failed to read mesh file")?; 42 | 43 | let weight_file = matches 44 | .opt_str("w") 45 | .context("missing required option 'weights'")?; 46 | let weight_file = fs::File::open(weight_file).context("failed to open weight file")?; 47 | let weight_file = io::BufReader::new(weight_file); 48 | let weights = mesh_io::weight::read(weight_file).context("failed to read weight file")?; 49 | 50 | match weights { 51 | mesh_io::weight::Array::Integers(is) => { 52 | apply(&mut mesh, is.into_iter().map(|i| i[0] as isize)); 53 | } 54 | mesh_io::weight::Array::Floats(fs) => { 55 | apply(&mut mesh, fs.into_iter().map(|f| f[0] as isize)); 56 | } 57 | } 58 | 59 | coupe_tools::write_mesh(&mesh, format, matches.free.first())?; 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /tools/src/bin/mesh-dup.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | 4 | const USAGE: &str = "Usage: mesh-dup [options] [in-mesh [out-mesh]] out.mesh"; 5 | 6 | fn main() -> Result<()> { 7 | let mut options = getopts::Options::new(); 8 | options.optopt("f", "format", "output format", "EXT"); 9 | options.optopt("n", "times", "numbers of duplicates (default: 2)", "NUMBER"); 10 | 11 | let matches = coupe_tools::parse_args(options, USAGE, 2)?; 12 | 13 | let format = matches 14 | .opt_get("f") 15 | .context("invalid value for option 'format'")?; 16 | 17 | let n: usize = matches 18 | .opt_get("n") 19 | .context("invalid value for option 'times'")? 20 | .unwrap_or(2); 21 | 22 | let mesh = coupe_tools::read_mesh(matches.free.first())?; 23 | let mesh = mesh.duplicate(n); 24 | coupe_tools::write_mesh(&mesh, format, matches.free.get(1))?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /tools/src/bin/mesh-part.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | use coupe::nalgebra::allocator::Allocator; 4 | use coupe::nalgebra::ArrayStorage; 5 | use coupe::nalgebra::Const; 6 | use coupe::nalgebra::DefaultAllocator; 7 | use coupe::nalgebra::DimDiff; 8 | use coupe::nalgebra::DimSub; 9 | use coupe::nalgebra::ToTypenum; 10 | use mesh_io::weight; 11 | use mesh_io::Mesh; 12 | use std::fs; 13 | use std::io; 14 | use tracing_subscriber::filter::EnvFilter; 15 | use tracing_subscriber::layer::SubscriberExt as _; 16 | use tracing_subscriber::util::SubscriberInitExt as _; 17 | use tracing_subscriber::Registry; 18 | use tracing_tree::HierarchicalLayer; 19 | 20 | const USAGE: &str = "Usage: mesh-part [options] [out-part] >out.part"; 21 | 22 | fn main_d( 23 | matches: &getopts::Matches, 24 | edge_weights: coupe_tools::EdgeWeightDistribution, 25 | mesh: Mesh, 26 | weights: weight::Array, 27 | ) -> Result> 28 | where 29 | Const: DimSub> + ToTypenum, 30 | DefaultAllocator: Allocator, Const, Buffer = ArrayStorage> 31 | + Allocator, Const<1>>>, 32 | { 33 | let algorithm_specs = matches.opt_strs("a"); 34 | let algorithms: Vec<_> = algorithm_specs 35 | .iter() 36 | .map(|algorithm_spec| { 37 | coupe_tools::parse_algorithm::(algorithm_spec) 38 | .with_context(|| format!("invalid algorithm {:?}", algorithm_spec)) 39 | }) 40 | .collect::>()?; 41 | 42 | let mut partition = vec![0; coupe_tools::used_element_count(&mesh)]; 43 | let problem = coupe_tools::Problem::new(mesh, weights, edge_weights); 44 | 45 | let show_metadata = matches.opt_present("v"); 46 | 47 | let intel_domain = coupe_tools::ittapi::domain("algorithm-chain"); 48 | 49 | for (algorithm_spec, mut algorithm) in algorithm_specs.iter().zip(algorithms) { 50 | let name = format!("{algorithm_spec}.to_runner"); 51 | let task = coupe_tools::ittapi::begin(&intel_domain, &name); 52 | 53 | let mut algorithm = algorithm.to_runner(&problem); 54 | 55 | drop(task); 56 | let task = coupe_tools::ittapi::begin(&intel_domain, algorithm_spec); 57 | 58 | let metadata = algorithm(&mut partition) 59 | .with_context(|| format!("failed to apply algorithm {:?}", algorithm_spec))?; 60 | 61 | drop(task); 62 | 63 | if !show_metadata { 64 | continue; 65 | } 66 | if let Some(metadata) = metadata { 67 | eprintln!("{algorithm_spec}: {metadata:?}"); 68 | } else { 69 | eprintln!("{algorithm_spec}:"); 70 | } 71 | } 72 | 73 | Ok(partition) 74 | } 75 | 76 | fn main() -> Result<()> { 77 | let mut options = getopts::Options::new(); 78 | options.optmulti( 79 | "a", 80 | "algorithm", 81 | "name of the algorithm to run, see ALGORITHMS", 82 | "NAME", 83 | ); 84 | options.optopt( 85 | "E", 86 | "edge-weights", 87 | "Change how edge weights are set", 88 | "VARIANT", 89 | ); 90 | options.optopt("m", "mesh", "mesh file", "FILE"); 91 | options.optopt("t", "trace", "emit a chrome trace", "FILE"); 92 | options.optflag("v", "verbose", "print diagnostic data"); 93 | options.optopt("w", "weights", "weight file", "FILE"); 94 | 95 | let matches = coupe_tools::parse_args(options, USAGE, 1)?; 96 | 97 | let registry = Registry::default().with(EnvFilter::from_env("LOG")).with( 98 | HierarchicalLayer::new(4) 99 | .with_thread_ids(true) 100 | .with_targets(true) 101 | .with_bracketed_fields(true), 102 | ); 103 | let _chrome_trace_guard = match matches.opt_str("t") { 104 | Some(filename) => { 105 | let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() 106 | .file(filename) 107 | .build(); 108 | registry.with(chrome_layer).init(); 109 | Some(guard) 110 | } 111 | None => { 112 | registry.init(); 113 | None 114 | } 115 | }; 116 | 117 | let edge_weights = matches 118 | .opt_get("E") 119 | .context("invalid value for -E, --edge-weights")? 120 | .unwrap_or(coupe_tools::EdgeWeightDistribution::Uniform); 121 | 122 | let mesh_file = matches 123 | .opt_str("m") 124 | .context("missing required option 'mesh'")?; 125 | let mesh_file = fs::File::open(mesh_file).context("failed to open mesh file")?; 126 | let mesh_file = io::BufReader::new(mesh_file); 127 | 128 | let weight_file = matches 129 | .opt_str("w") 130 | .context("missing required option 'weights'")?; 131 | let weights = fs::File::open(weight_file).context("failed to open weight file")?; 132 | let weights = io::BufReader::new(weights); 133 | 134 | let (mesh, weights) = rayon::join( 135 | || Mesh::from_reader(mesh_file).context("failed to read mesh file"), 136 | || weight::read(weights).context("failed to read weight file"), 137 | ); 138 | let mesh = mesh?; 139 | let weights = weights?; 140 | 141 | let partition = match mesh.dimension() { 142 | 2 => main_d::<2>(&matches, edge_weights, mesh, weights)?, 143 | 3 => main_d::<3>(&matches, edge_weights, mesh, weights)?, 144 | n => anyhow::bail!("expected 2D or 3D mesh, got a {n}D mesh"), 145 | }; 146 | 147 | let output = coupe_tools::writer(matches.free.first())?; 148 | mesh_io::partition::write(output, partition).context("failed to write partition")?; 149 | 150 | Ok(()) 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use coupe::sprs::CsMat; 156 | use coupe::sprs::CSR; 157 | 158 | #[test] 159 | fn test_adjacency_convert() { 160 | let mut adjacency = CsMat::empty(CSR, 15); 161 | adjacency.reserve_outer_dim(15); 162 | adjacency.insert(0, 1, 1.0); 163 | adjacency.insert(0, 5, 1.0); 164 | 165 | adjacency.insert(1, 0, 1.0); 166 | adjacency.insert(1, 2, 1.0); 167 | adjacency.insert(1, 6, 1.0); 168 | 169 | adjacency.insert(2, 1, 1.0); 170 | adjacency.insert(2, 3, 1.0); 171 | adjacency.insert(2, 7, 1.0); 172 | 173 | adjacency.insert(3, 2, 1.0); 174 | adjacency.insert(3, 4, 1.0); 175 | adjacency.insert(3, 8, 1.0); 176 | 177 | adjacency.insert(4, 3, 1.0); 178 | adjacency.insert(4, 9, 1.0); 179 | 180 | adjacency.insert(5, 0, 1.0); 181 | adjacency.insert(5, 6, 1.0); 182 | adjacency.insert(5, 10, 1.0); 183 | 184 | adjacency.insert(6, 1, 1.0); 185 | adjacency.insert(6, 5, 1.0); 186 | adjacency.insert(6, 7, 1.0); 187 | adjacency.insert(6, 11, 1.0); 188 | 189 | adjacency.insert(7, 2, 1.0); 190 | adjacency.insert(7, 6, 1.0); 191 | adjacency.insert(7, 8, 1.0); 192 | adjacency.insert(7, 12, 1.0); 193 | 194 | adjacency.insert(8, 3, 1.0); 195 | adjacency.insert(8, 7, 1.0); 196 | adjacency.insert(8, 9, 1.0); 197 | adjacency.insert(8, 13, 1.0); 198 | 199 | adjacency.insert(9, 4, 1.0); 200 | adjacency.insert(9, 8, 1.0); 201 | adjacency.insert(9, 14, 1.0); 202 | 203 | adjacency.insert(10, 5, 1.0); 204 | adjacency.insert(10, 11, 1.0); 205 | 206 | adjacency.insert(11, 6, 1.0); 207 | adjacency.insert(11, 10, 1.0); 208 | adjacency.insert(11, 12, 1.0); 209 | 210 | adjacency.insert(12, 7, 1.0); 211 | adjacency.insert(12, 11, 1.0); 212 | adjacency.insert(12, 13, 1.0); 213 | 214 | adjacency.insert(13, 8, 1.0); 215 | adjacency.insert(13, 12, 1.0); 216 | adjacency.insert(13, 14, 1.0); 217 | 218 | adjacency.insert(14, 9, 1.0); 219 | adjacency.insert(14, 13, 1.0); 220 | 221 | let (xadj, adjncy, _) = adjacency.into_raw_storage(); 222 | 223 | assert_eq!( 224 | xadj.as_slice(), 225 | &[0, 2, 5, 8, 11, 13, 16, 20, 24, 28, 31, 33, 36, 39, 42, 44] 226 | ); 227 | assert_eq!( 228 | adjncy.as_slice(), 229 | &[ 230 | 1, 5, 0, 2, 6, 1, 3, 7, 2, 4, 8, 3, 9, 0, 6, 10, 1, 5, 7, 11, 2, 6, 8, 12, 3, 7, 9, 231 | 13, 4, 8, 14, 5, 11, 6, 10, 12, 7, 11, 13, 8, 12, 14, 9, 13 232 | ] 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tools/src/bin/mesh-points.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use mesh_io::ElementType; 3 | use mesh_io::Mesh; 4 | use std::io; 5 | 6 | const USAGE: &str = "Usage: mesh-points [options] [in-mesh [out-plot]] out.plot"; 7 | 8 | fn write_points(mut w: impl io::Write, mesh: &Mesh, with_ids: bool) -> Result<()> { 9 | let points = coupe_tools::barycentres::(mesh); 10 | let ids: Box> = if with_ids { 11 | Box::new( 12 | match mesh 13 | .topology() 14 | .iter() 15 | .map(|(el_type, _, _)| el_type.dimension()) 16 | .max() 17 | { 18 | Some(element_dim) => mesh 19 | .elements() 20 | .filter_map(|(element_type, _nodes, element_ref)| { 21 | if element_type.dimension() != element_dim 22 | || element_type == ElementType::Edge 23 | { 24 | return None; 25 | } 26 | Some(element_ref) 27 | }) 28 | .collect(), 29 | None => Vec::new(), 30 | } 31 | .into_iter(), 32 | ) 33 | } else { 34 | Box::new(std::iter::repeat(0)) 35 | }; 36 | 37 | for (id, point) in ids.zip(points) { 38 | for coord in point.into_iter() { 39 | write!(w, "{coord} ")?; 40 | } 41 | if with_ids { 42 | write!(w, "{id}")?; 43 | } 44 | writeln!(w)?; 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | fn main() -> Result<()> { 51 | let mut options = getopts::Options::new(); 52 | options.optflag("", "with-ids", "add a column with the cell id"); 53 | 54 | let matches = coupe_tools::parse_args(options, USAGE, 2)?; 55 | 56 | let mesh = coupe_tools::read_mesh(matches.free.first())?; 57 | let output = coupe_tools::writer(matches.free.get(1))?; 58 | let with_ids = matches.opt_present("with-ids"); 59 | match mesh.dimension() { 60 | 2 => write_points::<2>(output, &mesh, with_ids)?, 61 | 3 => write_points::<3>(output, &mesh, with_ids)?, 62 | n => anyhow::bail!("expected 2D or 3D mesh, got a {n}D mesh"), 63 | }; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /tools/src/bin/mesh-refine.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | 4 | const USAGE: &str = "Usage: mesh-refine [options] [in-mesh [out-mesh]] out.mesh"; 5 | 6 | fn main() -> Result<()> { 7 | let mut options = getopts::Options::new(); 8 | options.optopt("f", "format", "output format", "EXT"); 9 | options.optopt( 10 | "n", 11 | "times", 12 | "numbers of times to refine (default: 1)", 13 | "NUMBER", 14 | ); 15 | 16 | let matches = coupe_tools::parse_args(options, USAGE, 2)?; 17 | 18 | let format = matches 19 | .opt_get("f") 20 | .context("invalid value for option 'format'")?; 21 | 22 | let n: usize = matches 23 | .opt_get("n") 24 | .context("invalid value for option 'times'")? 25 | .unwrap_or(1); 26 | 27 | eprintln!("Reading mesh..."); 28 | let mut mesh = coupe_tools::read_mesh(matches.free.first())?; 29 | eprintln!(" -> Dimension: {}", mesh.dimension()); 30 | eprintln!(" -> Nodes: {}", mesh.node_count()); 31 | eprintln!(" -> Elements: {}", mesh.element_count()); 32 | 33 | for i in 0..n { 34 | eprint!("\rRefining mesh... {i:2}/{n}"); 35 | mesh = mesh.refine(); 36 | } 37 | eprintln!("\rRefining mesh... done "); 38 | 39 | eprintln!(" -> Nodes: {}", mesh.node_count()); 40 | eprintln!(" -> Elements: {}", mesh.element_count()); 41 | 42 | eprintln!("Writing mesh..."); 43 | coupe_tools::write_mesh(&mesh, format, matches.free.get(1))?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /tools/src/bin/mesh-reorder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | use mesh_io::Mesh; 4 | 5 | const USAGE: &str = "Usage: mesh-reorder [options] [in-mesh [out-mesh]] out.mesh"; 6 | 7 | fn shuffle_couple(mut rng: R, data: &[T], refs: &[isize]) -> (Vec, Vec, Vec) 8 | where 9 | R: rand::RngCore, 10 | T: Clone, 11 | { 12 | use rand::seq::SliceRandom as _; 13 | 14 | let dim = data.len() / refs.len(); 15 | assert_eq!(data.len() % refs.len(), 0); 16 | 17 | let mut permutation: Vec<_> = (0..refs.len()).collect(); 18 | permutation.shuffle(&mut rng); 19 | 20 | let data = permutation 21 | .iter() 22 | .flat_map(|i| &data[dim * i..dim * (i + 1)]) 23 | .cloned() 24 | .collect(); 25 | let refs = permutation.iter().map(|i| refs[*i]).collect(); 26 | 27 | (data, refs, permutation) 28 | } 29 | 30 | fn shuffle(mut rng: R, mesh: Mesh) -> Mesh 31 | where 32 | R: rand::RngCore, 33 | { 34 | let dimension = mesh.dimension(); 35 | 36 | let (coordinates, node_refs, node_permutation) = 37 | shuffle_couple(&mut rng, mesh.coordinates(), mesh.node_refs()); 38 | 39 | let node_permutation = { 40 | let mut p = vec![0; node_permutation.len()]; 41 | for (i, e) in node_permutation.into_iter().enumerate() { 42 | p[e] = i; 43 | } 44 | p 45 | }; 46 | 47 | let topology = mesh 48 | .topology() 49 | .iter() 50 | .map(|(el_type, el_nodes, el_refs)| { 51 | let (mut el_nodes, el_refs, _) = shuffle_couple(&mut rng, el_nodes, el_refs); 52 | for node in &mut el_nodes { 53 | *node = node_permutation[*node]; 54 | } 55 | (*el_type, el_nodes, el_refs) 56 | }) 57 | .collect(); 58 | 59 | Mesh::from_raw_parts(dimension, coordinates, node_refs, topology) 60 | } 61 | 62 | fn main() -> Result<()> { 63 | let mut options = getopts::Options::new(); 64 | options.optopt("f", "format", "output format", "EXT"); 65 | 66 | let matches = coupe_tools::parse_args(options, USAGE, 2)?; 67 | 68 | let format = matches 69 | .opt_get("f") 70 | .context("invalid value for option 'format'")?; 71 | 72 | eprintln!("Reading mesh..."); 73 | let mut mesh = coupe_tools::read_mesh(matches.free.first())?; 74 | 75 | eprintln!("Shuffling mesh..."); 76 | mesh = shuffle(rand::thread_rng(), mesh); 77 | 78 | eprintln!("Writing mesh..."); 79 | coupe_tools::write_mesh(&mesh, format, matches.free.get(1))?; 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /tools/src/bin/weight-gen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use anyhow::Result; 3 | use coupe::PointND; 4 | use mesh_io::Mesh; 5 | use rayon::iter::IntoParallelRefIterator as _; 6 | use rayon::iter::ParallelIterator as _; 7 | use std::cmp; 8 | 9 | const USAGE: &str = "Usage: weight-gen [options] [in-mesh [out-weights]] out.weights"; 10 | 11 | fn partial_cmp(a: &f64, b: &f64) -> cmp::Ordering { 12 | if a < b { 13 | cmp::Ordering::Less 14 | } else { 15 | cmp::Ordering::Greater 16 | } 17 | } 18 | 19 | #[derive(Clone, Copy)] 20 | #[repr(usize)] 21 | enum Axis { 22 | X, 23 | Y, 24 | Z, 25 | } 26 | 27 | impl std::str::FromStr for Axis { 28 | type Err = anyhow::Error; 29 | 30 | fn from_str(s: &str) -> Result { 31 | Ok(match s { 32 | "0" | "x" | "X" => Self::X, 33 | "1" | "y" | "Y" => Self::Y, 34 | "2" | "z" | "Z" => Self::Z, 35 | _ => anyhow::bail!("expected 0/1/2/x/y/z"), 36 | }) 37 | } 38 | } 39 | 40 | #[derive(Copy, Clone)] 41 | struct Spike { 42 | height: f64, 43 | position: PointND, 44 | } 45 | 46 | #[derive(Clone)] 47 | enum Distribution { 48 | Constant(f64), 49 | Linear(Axis, f64, f64), 50 | Spike(Vec>), 51 | } 52 | 53 | fn parse_distribution(definition: &str) -> Result> { 54 | let mut args = definition.split(','); 55 | let name = args.next().context("empty definition")?; 56 | 57 | fn required(maybe_arg: Option>) -> Result { 58 | maybe_arg.context("not enough arguments")? 59 | } 60 | 61 | fn f64_arg(arg: Option<&str>) -> Option> { 62 | arg.map(|arg| { 63 | let f = arg 64 | .parse::() 65 | .with_context(|| format!("arg {:?} is not a valid float", arg))?; 66 | if !f.is_finite() { 67 | anyhow::bail!("arg {:?} is not finite", arg); 68 | } 69 | Ok(f) 70 | }) 71 | } 72 | 73 | fn axis_arg(arg: Option<&str>) -> Option> { 74 | let arg = arg?; 75 | Some( 76 | arg.parse::() 77 | .with_context(|| format!("arg {:?} is not a valid axis", arg)), 78 | ) 79 | } 80 | 81 | Ok(match name { 82 | "constant" => { 83 | let value = required(f64_arg(args.next()))?; 84 | Distribution::Constant(value) 85 | } 86 | "linear" => { 87 | let axis = required(axis_arg(args.next()))?; 88 | let from = required(f64_arg(args.next()))?; 89 | let to = required(f64_arg(args.next()))?; 90 | Distribution::Linear(axis, from, to) 91 | } 92 | "spike" => { 93 | let mut spikes = Vec::new(); 94 | while let Some(height) = f64_arg(args.next()) { 95 | let height = height?; 96 | if height <= 0.0 { 97 | anyhow::bail!( 98 | "expected 'spike' height to be strictly positive, found {height}" 99 | ); 100 | } 101 | let mut position = [0.0; D]; 102 | for position in &mut position { 103 | *position = required(f64_arg(args.next()))?; 104 | } 105 | spikes.push(Spike { 106 | height, 107 | position: PointND::from(position), 108 | }); 109 | } 110 | Distribution::Spike(spikes) 111 | } 112 | _ => anyhow::bail!("unknown distribution {:?}", name), 113 | }) 114 | } 115 | 116 | fn apply_distribution( 117 | d: Distribution, 118 | points: &[PointND], 119 | ) -> Box) -> f64> { 120 | match d { 121 | Distribution::Constant(value) => Box::new(move |_coordinates| value), 122 | Distribution::Linear(axis, from, to) => { 123 | let axis_iter = points.par_iter().map(|point| point[axis as usize]); 124 | let (min, max) = rayon::join( 125 | || axis_iter.clone().min_by(partial_cmp).unwrap(), 126 | || axis_iter.clone().max_by(partial_cmp).unwrap(), 127 | ); 128 | let mut alpha = if max == min { 129 | 0.0 130 | } else { 131 | (to - from) / (max - min) 132 | }; 133 | while to - from < alpha * (max - min) { 134 | alpha = coupe::nextafter(alpha, f64::NEG_INFINITY); 135 | } 136 | Box::new(move |coordinates| f64::mul_add(coordinates[axis as usize] - min, alpha, from)) 137 | } 138 | Distribution::Spike(mut spikes) => { 139 | for spike in &mut spikes { 140 | spike.height = f64::ln(spike.height); 141 | } 142 | Box::new(move |point| { 143 | spikes 144 | .iter() 145 | .map(|spike| { 146 | let distance = (spike.position - point).norm(); 147 | f64::exp(spike.height - distance) 148 | }) 149 | .sum() 150 | }) 151 | } 152 | } 153 | } 154 | 155 | fn weight_gen( 156 | mesh: Mesh, 157 | distributions: Vec, 158 | matches: getopts::Matches, 159 | ) -> Result<()> { 160 | let distributions: Vec> = distributions 161 | .into_iter() 162 | .map(|spec| parse_distribution(&spec)) 163 | .collect::>()?; 164 | 165 | let points = coupe_tools::barycentres::(&mesh); 166 | 167 | let distributions: Vec<_> = distributions 168 | .into_iter() 169 | .map(|distribution| apply_distribution(distribution, &points)) 170 | .collect(); 171 | 172 | let weights = points.iter().map(|point| { 173 | distributions 174 | .iter() 175 | .map(|distribution| distribution(*point)) 176 | }); 177 | 178 | let output = coupe_tools::writer(matches.free.get(1))?; 179 | if matches.opt_present("i") { 180 | let weights = weights.map(|weight| weight.map(|criterion| criterion as i64)); 181 | mesh_io::weight::write_integers(output, weights) 182 | } else { 183 | mesh_io::weight::write_floats(output, weights) 184 | } 185 | .context("failed to write weight array") 186 | } 187 | 188 | fn main() -> Result<()> { 189 | let mut options = getopts::Options::new(); 190 | options.optmulti( 191 | "d", 192 | "distribution", 193 | "definition of the weight distribution, see DISTRIBUTION", 194 | "DEFINITION", 195 | ); 196 | options.optflag( 197 | "i", 198 | "integers", 199 | "generate integers instead of floating-point numbers", 200 | ); 201 | 202 | let matches = coupe_tools::parse_args(options, USAGE, 2)?; 203 | 204 | let distributions = matches.opt_strs("d"); 205 | if distributions.is_empty() { 206 | anyhow::bail!("missing required option 'distribution'"); 207 | } 208 | 209 | let mesh = coupe_tools::read_mesh(matches.free.first())?; 210 | 211 | match mesh.dimension() { 212 | 2 => weight_gen::<2>(mesh, distributions, matches), 213 | 3 => weight_gen::<3>(mesh, distributions, matches), 214 | n => anyhow::bail!("expected 2D or 3D mesh, got a {n}D mesh"), 215 | } 216 | } 217 | 218 | #[cfg(test)] 219 | mod tests { 220 | use super::*; 221 | use coupe::Point2D; 222 | use proptest::collection::vec; 223 | use proptest::strategy::Strategy; 224 | 225 | /// Strategy for finite, non-NaN floats that are within reasonable bounds. 226 | fn float() -> impl Strategy { 227 | -1e150..1e150 228 | } 229 | 230 | proptest::proptest!( 231 | #[test] 232 | fn linear_within_bounds( 233 | points in vec( 234 | float().prop_map(|a| Point2D::new(a, a)), 235 | 2..200 236 | ), 237 | ) { 238 | const LOW: f64 = 0.0; 239 | const HIGH: f64 = 100.0; 240 | let dist = Distribution::Linear(Axis::X, LOW, HIGH); 241 | let dist = apply_distribution(dist, &points); 242 | for p in points { 243 | let weight = dist(p); 244 | proptest::prop_assert!( 245 | (LOW..=HIGH).contains(&weight), 246 | "point {p:?} has weight {weight} which is not in [{LOW}, {HIGH}]", 247 | ); 248 | } 249 | } 250 | ); 251 | } 252 | -------------------------------------------------------------------------------- /tools/src/ittapi.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around ittapi to avoid spreading #[cfg] flags in the codebase. 2 | //! 3 | //! Version of the module where ittapi is enabled. See `ittapi_stub.rs`. 4 | 5 | pub fn domain(s: &str) -> ittapi::Domain { 6 | ittapi::Domain::new(s) 7 | } 8 | 9 | pub fn begin<'a>(domain: &'a ittapi::Domain, name: &'a str) -> ittapi::Task<'a> { 10 | ittapi::Task::begin(domain, name) 11 | } 12 | -------------------------------------------------------------------------------- /tools/src/ittapi_stub.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around ittapi to avoid spreading #[cfg] flags in the codebase. 2 | //! 3 | //! Version of the module where ittapi is disabled. See `ittapi.rs`. 4 | 5 | pub struct Domain; 6 | pub struct Task; 7 | 8 | // This Drop impl is needed to silence `clippy::drop_non_drop`. 9 | impl Drop for Task { 10 | fn drop(&mut self) {} 11 | } 12 | 13 | pub fn domain(_: &str) -> Domain { 14 | Domain 15 | } 16 | 17 | pub fn begin(_: &Domain, _: &str) -> Task { 18 | Task 19 | } 20 | -------------------------------------------------------------------------------- /tools/src/metis.rs: -------------------------------------------------------------------------------- 1 | use super::Problem; 2 | use super::Runner; 3 | use super::ToRunner; 4 | use crate::zoom_in::zoom_in; 5 | use anyhow::Context; 6 | use mesh_io::weight; 7 | use metis::Idx; 8 | 9 | pub struct Recursive { 10 | pub part_count: Idx, 11 | pub tolerance: Option, 12 | } 13 | 14 | impl ToRunner for Recursive { 15 | fn to_runner<'a>(&'a mut self, problem: &'a Problem) -> Runner<'a> { 16 | let (ncon, mut weights) = match &problem.weights { 17 | weight::Array::Integers(is) => { 18 | let ncon = is.first().map_or(1, Vec::len) as Idx; 19 | let weights = zoom_in(is.iter().map(|v| v.iter().cloned())); 20 | (ncon, weights) 21 | } 22 | weight::Array::Floats(fs) => { 23 | let ncon = fs.first().map_or(1, Vec::len) as Idx; 24 | let weights = zoom_in(fs.iter().map(|v| v.iter().cloned())); 25 | (ncon, weights) 26 | } 27 | }; 28 | 29 | let (xadj, adjncy, adjwgt) = problem.adjacency().into_raw_storage(); 30 | let mut xadj: Vec<_> = xadj.iter().map(|i| *i as Idx).collect(); 31 | let mut adjncy: Vec<_> = adjncy.iter().map(|i| *i as Idx).collect(); 32 | let mut adjwgt = zoom_in(adjwgt.iter().map(|v| Some(*v))); 33 | 34 | let tolerance = self.tolerance.map(|f| (f * 1000.0) as Idx); 35 | 36 | let mut metis_partition = vec![0; problem.adjacency().rows()]; 37 | Box::new(move |partition| { 38 | let mut graph = metis::Graph::new(ncon, self.part_count, &mut xadj, &mut adjncy); 39 | if let Some(weights) = &mut weights { 40 | graph = graph.set_vwgt(weights); 41 | } 42 | if let Some(adjwgt) = &mut adjwgt { 43 | graph = graph.set_adjwgt(adjwgt); 44 | } 45 | if let Some(tolerance) = tolerance { 46 | graph = graph.set_option(metis::option::UFactor(tolerance)); 47 | } 48 | graph 49 | .part_recursive(&mut metis_partition) 50 | .context("METIS partitioning failed")?; 51 | for (dst, src) in partition.iter_mut().zip(&metis_partition) { 52 | *dst = *src as usize; 53 | } 54 | Ok(None) 55 | }) 56 | } 57 | } 58 | 59 | pub struct KWay { 60 | pub part_count: Idx, 61 | pub tolerance: Option, 62 | } 63 | 64 | impl ToRunner for KWay { 65 | fn to_runner<'a>(&'a mut self, problem: &'a Problem) -> Runner<'a> { 66 | let (ncon, mut weights) = match &problem.weights { 67 | weight::Array::Integers(is) => { 68 | let ncon = is.first().map_or(1, Vec::len) as Idx; 69 | let weights = zoom_in(is.iter().map(|v| v.iter().cloned())); 70 | (ncon, weights) 71 | } 72 | weight::Array::Floats(fs) => { 73 | let ncon = fs.first().map_or(1, Vec::len) as Idx; 74 | let weights = zoom_in(fs.iter().map(|v| v.iter().cloned())); 75 | (ncon, weights) 76 | } 77 | }; 78 | 79 | let (xadj, adjncy, adjwgt) = problem.adjacency().into_raw_storage(); 80 | let mut xadj: Vec<_> = xadj.iter().map(|i| *i as Idx).collect(); 81 | let mut adjncy: Vec<_> = adjncy.iter().map(|i| *i as Idx).collect(); 82 | let mut adjwgt = zoom_in(adjwgt.iter().map(|v| Some(*v))); 83 | 84 | let tolerance = self.tolerance.map(|f| (f * 1000.0) as Idx); 85 | 86 | let mut metis_partition = vec![0; problem.adjacency().rows()]; 87 | Box::new(move |partition| { 88 | let mut graph = metis::Graph::new(ncon, self.part_count, &mut xadj, &mut adjncy); 89 | if let Some(weights) = &mut weights { 90 | graph = graph.set_vwgt(weights); 91 | } 92 | if let Some(adjwgt) = &mut adjwgt { 93 | graph = graph.set_adjwgt(adjwgt); 94 | } 95 | if let Some(tolerance) = tolerance { 96 | graph = graph.set_option(metis::option::UFactor(tolerance)); 97 | } 98 | graph 99 | .part_kway(&mut metis_partition) 100 | .context("METIS partitioning failed")?; 101 | for (dst, src) in partition.iter_mut().zip(&metis_partition) { 102 | *dst = *src as usize; 103 | } 104 | Ok(None) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tools/src/scotch.rs: -------------------------------------------------------------------------------- 1 | use super::runner_error; 2 | use super::Problem; 3 | use super::ToRunner; 4 | use crate::zoom_in::zoom_in; 5 | use anyhow::Context as _; 6 | use mesh_io::weight; 7 | use scotch::graph::Data; 8 | use scotch::Graph; 9 | use scotch::Num; 10 | 11 | pub struct Standard { 12 | pub part_count: Num, 13 | } 14 | 15 | impl ToRunner for Standard { 16 | fn to_runner<'a>(&'a mut self, problem: &'a Problem) -> super::Runner<'a> { 17 | let weights = match &problem.weights { 18 | weight::Array::Integers(is) => { 19 | if is.first().map_or(1, Vec::len) != 1 { 20 | return runner_error("SCOTCH cannot do multi-criteria partitioning"); 21 | } 22 | zoom_in(is.iter().map(|v| Some(v[0]))) 23 | } 24 | weight::Array::Floats(fs) => { 25 | if fs.first().map_or(1, Vec::len) != 1 { 26 | return runner_error("SCOTCH cannot do multi-criteria partitioning"); 27 | } 28 | zoom_in(fs.iter().map(|v| Some(v[0]))) 29 | } 30 | }; 31 | 32 | let (xadj, adjncy, adjwgt) = problem.adjacency().into_raw_storage(); 33 | let xadj: Vec<_> = xadj.iter().map(|i| *i as Num).collect(); 34 | let adjncy: Vec<_> = adjncy.iter().map(|i| *i as Num).collect(); 35 | let adjwgt = zoom_in(adjwgt.iter().map(|v| Some(*v))); 36 | 37 | let mut strat = scotch::Strategy::new(); 38 | let arch = scotch::Architecture::complete(self.part_count as Num); 39 | 40 | let mut scotch_partition = vec![0; problem.adjacency().rows()]; 41 | Box::new(move |partition| { 42 | let graph_data = Data::new( 43 | 0, 44 | &xadj, 45 | &[], 46 | weights.as_deref().unwrap_or(&[]), 47 | &[], 48 | &adjncy, 49 | adjwgt.as_deref().unwrap_or(&[]), 50 | ); 51 | let mut graph = Graph::build(&graph_data).context("failed to build SCOTCH graph")?; 52 | graph.check().context("failed to build SCOTCH graph")?; 53 | graph 54 | .mapping(&arch, &mut scotch_partition) 55 | .compute(&mut strat) 56 | .context("SCOTCH partitioning failed")?; 57 | for (dst, src) in partition.iter_mut().zip(&scotch_partition) { 58 | *dst = *src as usize; 59 | } 60 | Ok(None) 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tools/src/zoom_in.rs: -------------------------------------------------------------------------------- 1 | use coupe::num_traits::NumAssign; 2 | use coupe::num_traits::PrimInt; 3 | use coupe::num_traits::ToPrimitive; 4 | use itertools::Itertools; 5 | use std::iter::Sum; 6 | 7 | /// Scales and offsets the input weights into the bandwidth of type `O`. 8 | /// 9 | /// This function ensures two things: 10 | /// 11 | /// - output weights are strictly greater than zero, 12 | /// - the sum of each criterion is in `O`. 13 | /// 14 | /// If all weights are the same, returns `None`. 15 | pub fn zoom_in(inputs: I) -> Option> 16 | where 17 | I: IntoIterator, 18 | I::IntoIter: Clone + ExactSizeIterator, 19 | I::Item: IntoIterator, 20 | W: Sum + Clone + PartialOrd + ToPrimitive + NumAssign, 21 | O: PrimInt, 22 | { 23 | let inputs = inputs.into_iter(); 24 | let criterion_count = match inputs.clone().next() { 25 | Some(v) => v.into_iter().count(), 26 | None => return Some(Vec::new()), 27 | }; 28 | let count = inputs.clone().count(); 29 | 30 | // Compensated summation of each criterion. 31 | let (mut sums, compensations) = inputs.clone().fold( 32 | (vec![0.0; criterion_count], vec![0.0; criterion_count]), 33 | |(mut sum, mut compensation), w| { 34 | for ((s, w), c) in sum.iter_mut().zip(w).zip(&mut compensation) { 35 | let w = w.to_f64().unwrap(); 36 | let t = *s + w; 37 | if f64::abs(w) <= f64::abs(*s) { 38 | *c += (*s - t) + w; 39 | } else { 40 | *c += (w - t) + *s; 41 | } 42 | *s = t; 43 | } 44 | (sum, compensation) 45 | }, 46 | ); 47 | for (s, c) in sums.iter_mut().zip(compensations) { 48 | *s += c; 49 | } 50 | 51 | if !sums.iter().all(|s| s.is_finite()) { 52 | // At least one of the sum is either infinite or NaN. In this case, just 53 | // abort. This can happen when some weights aren't finite, or when their 54 | // sum is infinite. 55 | // TODO handle the case where all weights are finite but their sum is infinite 56 | // TODO return an error somehow, so that the user knows the data is corrupt 57 | return None; 58 | } 59 | 60 | let sum = sums 61 | .into_iter() 62 | // won't panic because sums are ensured to be finite 63 | .max_by(|a, b| f64::partial_cmp(a, b).unwrap()) 64 | .unwrap(); 65 | 66 | let (min_input, max_input) = inputs.clone().flatten().minmax().into_option().unwrap(); 67 | let min_input = min_input.to_f64().unwrap(); 68 | let max_input = max_input.to_f64().unwrap(); 69 | 70 | if !f64::is_normal(max_input - min_input) { 71 | // Input weight are all (about) the same, don't scale them. 72 | // Otherwise 1/(max_input-min_input) is not finite. 73 | return None; 74 | } 75 | 76 | // Actually scale from [0,max] to [1,INTMAX], because it makes 77 | // more sense in the case of load balancing. 78 | let min_input = f64::min(min_input, 0.0); 79 | 80 | let max_output = ((max_input - min_input) * O::max_value().to_f64().unwrap() + sum 81 | - max_input * count as f64) 82 | / (sum - min_input * count as f64); 83 | 84 | let scale = (max_output - 1.0) / (max_input - min_input); 85 | let offset = (max_input - max_output * min_input) / (max_input - min_input); 86 | 87 | let scaled_inputs: Vec = inputs 88 | .flat_map(move |v| { 89 | v.into_iter().map(move |v| { 90 | let v = v.to_f64().unwrap(); 91 | O::from(f64::mul_add(v, scale, offset)).unwrap() 92 | }) 93 | }) 94 | .collect(); 95 | 96 | let Some(first_input) = scaled_inputs.first() else { 97 | return None; 98 | }; 99 | if scaled_inputs.iter().all(|i| i == first_input) { 100 | None 101 | } else { 102 | Some(scaled_inputs) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | 110 | #[test] 111 | fn test_zoom_in() { 112 | let z = zoom_in::<_, i32, _>(vec![[1.0 - f64::EPSILON], [1.0], [1.0 + f64::EPSILON]]); 113 | assert!(z.is_none()); 114 | 115 | let z: Vec = zoom_in(vec![[0.0], [1.0]]).unwrap(); 116 | assert_eq!(z, vec![1, i32::MAX - 1]); 117 | 118 | let z: Vec = zoom_in(vec![[0.0], [1.0], [1.0]]).unwrap(); 119 | assert_eq!(z, vec![1, i32::MAX / 2, i32::MAX / 2]); 120 | 121 | let z = zoom_in::<_, i32, _>(vec![[i32::MAX - 2], [i32::MAX - 1], [i32::MAX]]); 122 | assert!(z.is_none()); 123 | 124 | let z: Vec = zoom_in(vec![[i32::MAX - 6], [i32::MAX - 3], [i32::MAX]]).unwrap(); 125 | assert_eq!(z, vec![i32::MAX / 3 - 1, i32::MAX / 3, i32::MAX / 3 + 1]); 126 | } 127 | } 128 | --------------------------------------------------------------------------------