diff --git a/.gitignore b/.gitignore index 859a5a8e..94ab9e70 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ refs/ # Keep the min test suite to ship with the repo !data/min/ !data/min/**/*.csv +!data/cubes/ +!data/cubes/**/*.csv !gold/min/ !gold/min/**/*.fimg !gold/min/**/*.bmp @@ -97,8 +99,8 @@ uv.lock # Cython *.html -src/riley/cyth/riley.c -src/riley/cyth/riley.html +src/riley/cython/riley.c +src/riley/cython/riley.html # Windows :( *.dll diff --git a/MANIFEST.in b/MANIFEST.in index 228305e0..be588697 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ include scripts/python_package_data.toml recursive-include src/riley *.py *.pxd *.h *.zig prune src/riley/__pycache__ -prune src/riley/cyth/__pycache__ +prune src/riley/cython/__pycache__ prune src/riley/zig/__pycache__ prune src/riley_raster.egg-info prune src/riley.egg-info diff --git a/README.md b/README.md index 0cd4e48f..9bffa63e 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,13 @@ or with the build system: zig build demo- -Doptimize=ReleaseFast ``` -where `CASE` is one of `sphere200`, `rabbits`, `dicuq`, or `stereocal`. Zig demo output is written to `./out/demo-CASE/`. +where `CASE` is one of `sphere200`, `psf`, `rabbits`, `dicuq`, or `stereocal`. Zig demo output is written to `./out/demo-CASE/`. +The `psf` demo writes separate `global_subpx_full` and `global_subpx_stripe` subdirectories. Zig demo source on GitHub: - [`demo_sphere200.zig`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/demo_sphere200.zig) +- [`demo_psf.zig`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/demo_psf.zig) - [`demo_rabbits.zig`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/demo_rabbits.zig) - [`demo_dicuq.zig`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/demo_dicuq.zig) - [`demo_stereocal.zig`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/demo_stereocal.zig) @@ -115,6 +117,7 @@ Python demo output is written to `Path.cwd() / "out-riley-py" / "demo-CASE"`. Python demo source on GitHub: - [`demo_sphere200.py`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/riley/pydemos/demo_sphere200.py) +- [`demo_psf.py`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/riley/pydemos/demo_psf.py) - [`demo_rabbits.py`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/riley/pydemos/demo_rabbits.py) - [`demo_dicuq.py`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/riley/pydemos/demo_dicuq.py) - [`demo_dic_from_exodus.py`](https://github.com/Computer-Aided-Validation-Laboratory/riley-raster/blob/main/src/riley/pydemos/demo_dic_from_exodus.py) diff --git a/build.zig b/build.zig index 75c91edd..2d0d65c4 100644 --- a/build.zig +++ b/build.zig @@ -2,8 +2,8 @@ const std = @import("std"); const riley_version = std.SemanticVersion{ .major = 2026, - .minor = 7, - .patch = 1, + .minor = 9, + .patch = 0, }; const RunEntry = struct { @@ -52,7 +52,7 @@ pub fn build(b: *std.Build) void { build_options_module, ); shared_lib.installHeader( - b.path("src/riley/cyth/riley.h"), + b.path("src/riley/cython/riley.h"), "riley.h", ); b.installArtifact(shared_lib); @@ -95,6 +95,11 @@ pub fn build(b: *std.Build) void { .description = "Run the sphere200 demo", .source_path = "src/demo_sphere200.zig", }, + .{ + .step_name = "demo-psf", + .description = "Run the Gaussian PSF demo", + .source_path = "src/demo_psf.zig", + }, .{ .step_name = "demo-rabbits", .description = "Run the rabbits demo", diff --git a/build.zig.zon b/build.zig.zon index 579e04b2..bc956d4e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .riley, - .version = "2026.7.1", + .version = "2026.9.0", .fingerprint = 0x7380980579fcd8b7, // Changing this has security and trust implications. .minimum_zig_version = "0.16.0", .dependencies = .{ diff --git a/data/FE/save_sim_to_csv.py b/data/FE/save_sim_to_csv.py index aef6fa8a..192d82a1 100644 --- a/data/FE/save_sim_to_csv.py +++ b/data/FE/save_sim_to_csv.py @@ -37,7 +37,7 @@ def main() -> None: mesh_world.coords, (2464, 2056), uv_span_max=0.8, - projection_plane=( + proj_plane=( np.array((0.0, 0.0, -1.0), dtype=np.float64), np.array((0.0, 0.0, 0.0), dtype=np.float64), ), diff --git a/data/audit_mesh_convention.py b/data/audit_mesh_convention.py new file mode 100644 index 00000000..cbd730af --- /dev/null +++ b/data/audit_mesh_convention.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Report whether every CSV mesh below ``data`` follows Riley's convention.""" + +from __future__ import annotations + +from pathlib import Path +import sys +import importlib.util + +import numpy as np + + +DATA_DIR = Path(__file__).resolve().parent +CONNECT_FILENAMES = ("connect.csv", "connectivity.csv") + + +def _load_meshconv(): + """Load Riley's dependency-free convention module without its Cython API.""" + + module_path = DATA_DIR.parent / "src" / "riley" / "python" / "meshconv.py" + spec = importlib.util.spec_from_file_location("riley_meshconv", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load mesh-convention module: {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +meshconv = _load_meshconv() + + +def main() -> int: + results = [audit_mesh(mesh_dir) for mesh_dir in find_mesh_dirs()] + width = max(len(mesh_name) for status, mesh_name, _ in results) + + print("Riley mesh convention audit") + print() + for status, mesh_name, detail in results: + print(f"{status:<11} {mesh_name:<{width}} {detail}") + + conforms = sum(status == "CONFORMS" for status, _, _ in results) + errors = len(results) - conforms + print() + print(f"Summary: {conforms} conform, {errors} do not ({len(results)} meshes total).") + return 1 if errors else 0 + + +def find_mesh_dirs() -> list[Path]: + return sorted( + { + path.parent + for filename in CONNECT_FILENAMES + for path in DATA_DIR.rglob(filename) + if path.parent.joinpath("coords.csv").is_file() + }, + ) + + +def audit_mesh(mesh_dir: Path) -> tuple[str, str, str]: + mesh_name = mesh_dir.relative_to(DATA_DIR).as_posix() + try: + coords = np.loadtxt(mesh_dir / "coords.csv", delimiter=",", dtype=np.float64) + connect_paths = [mesh_dir / name for name in CONNECT_FILENAMES] + connect_paths = [path for path in connect_paths if path.is_file()] + connect_tables = [np.loadtxt(path, delimiter=",", dtype=np.int64) for path in connect_paths] + if len(connect_tables) == 2 and not np.array_equal(*connect_tables): + raise ValueError("connect.csv and connectivity.csv differ") + + mesh = meshconv.SimData( + coords=np.atleast_2d(coords), + connect={"connect1": np.atleast_2d(connect_tables[0])}, + mesh_type=_mesh_type_hint(mesh_dir), + ) + report = meshconv.check_mesh_convention(mesh) + except (OSError, ValueError, NotImplementedError) as err: + return "DOES NOT", mesh_name, str(err) + + if report.is_valid: + return "CONFORMS", mesh_name, "" + return "DOES NOT", mesh_name, ", ".join(report.failed_checks) + + +def _mesh_type_hint(mesh_dir: Path) -> meshconv.EMeshType | None: + if ( + mesh_dir.parent.name in {"FE", "cubes"} + or mesh_dir.name.endswith("calplate3d") + ): + return meshconv.EMeshType.VOL + if mesh_dir.name.startswith(("tri", "quad")): + return meshconv.EMeshType.SURF + return None + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/data/bench/gen_bench_data.py b/data/bench/gen_bench_data.py index f345b912..096a4803 100644 --- a/data/bench/gen_bench_data.py +++ b/data/bench/gen_bench_data.py @@ -1,10 +1,23 @@ import numpy as np import os +from riley.python import meshconv + def save_csv(path, data): os.makedirs(os.path.dirname(path), exist_ok=True) np.savetxt(path, data, delimiter=',', fmt='%.10f' if data.dtype == np.float64 else '%d') +def save_surface_mesh(out_dir, coords, connect): + connect = meshconv.enforce_mesh_convention( + meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + ).connect["connect1"] + save_csv(f"{out_dir}/coords.csv", coords) + save_csv(f"{out_dir}/connect.csv", connect) + def get_nodes_for_elem(etype): return { "tri3": 3, @@ -68,8 +81,7 @@ def generate_fullscreen(etype, out_dir): else: connect = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8]]) else: connect = np.array([[0, 1, 2, 3]]) - save_csv(f"{out_dir}/coords.csv", coords) - save_csv(f"{out_dir}/connect.csv", connect) + save_surface_mesh(out_dir, coords, connect) save_csv(f"{out_dir}/field.csv", compute_rgb_fields(coords)) save_csv(f"{out_dir}/uvs.csv", compute_uvs(coords)) @@ -102,8 +114,7 @@ def generate_grid(etype, out_dir, N=320): q8 = [i0, i1, i2, i3, m01, m12, m23, m30] if etype == "quad9": q8.append(i0+(xn+1)+1) conn.append(q8) - save_csv(f"{out_dir}/coords.csv", coords) - save_csv(f"{out_dir}/connect.csv", np.array(conn)) + save_surface_mesh(out_dir, coords, np.array(conn)) save_csv(f"{out_dir}/field.csv", compute_rgb_fields(coords)) save_csv(f"{out_dir}/uvs.csv", compute_uvs(coords)) @@ -199,8 +210,7 @@ def generate_sphere(etype, out_dir, N_target): q.append((r + 1) * cols + (c + 1)) conn.append(q) - save_csv(f"{out_dir}/coords.csv", coords) - save_csv(f"{out_dir}/connect.csv", np.array(conn)) + save_surface_mesh(out_dir, coords, np.array(conn)) save_csv(f"{out_dir}/uvs.csv", uvs) save_csv(f"{out_dir}/field.csv", fields) diff --git a/data/calplate/main_gen_calplate.py b/data/calplate/main_gen_calplate.py index ce63ce31..39d08dfc 100644 --- a/data/calplate/main_gen_calplate.py +++ b/data/calplate/main_gen_calplate.py @@ -8,6 +8,8 @@ from scipy.spatial.transform import Rotation from scipy.stats import qmc +from riley.python import meshconv + BASE_DIR = Path(__file__).resolve().parent @@ -445,11 +447,20 @@ def write_case( connect: np.ndarray, uvs: np.ndarray, states: list[MotionState], + enforce_convention: bool = False, ) -> None: out_dir = BASE_DIR / case_name out_dir.mkdir(parents=True, exist_ok=True) disp_x, disp_y, disp_z = displacement_fields(coords, states) + if enforce_convention: + connect = meshconv.enforce_mesh_convention( + meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + ).connect["connect1"] save_csv_matrix(out_dir / "coords.csv", coords, "%.10f") save_csv_matrix(out_dir / "connect.csv", connect, "%d") @@ -478,7 +489,7 @@ def main() -> None: cases = mesh_cases() for case_name, (coords, connect, uvs) in cases.items(): - write_case(case_name, coords, connect, uvs, states) + write_case(case_name, coords, connect, uvs, states, enforce_convention=True) print(f"Generated {len(cases)} calplate mesh cases in {BASE_DIR}") print(f"Mode: {CAL_MODE}") diff --git a/data/cubes/README.md b/data/cubes/README.md new file mode 100644 index 00000000..091ce277 --- /dev/null +++ b/data/cubes/README.md @@ -0,0 +1,9 @@ +# Mesh-convention cube fixtures + +These files are small CSV exports of PyVale's 10 mm element-test cubes. +`connectivity.csv` deliberately retains the source Exodus convention: it is +one-based and node-major. Riley mesh-convention tests use these fixtures to +verify normalization to zero-based, row-major connectivity. + +The supported fixtures are `tet4`, `tet10`, `hex8`, `hex20`, and `hex27`. +`tet14` is retained as the explicit unsupported-topology fixture. diff --git a/data/cubes/hex20/connectivity.csv b/data/cubes/hex20/connectivity.csv new file mode 100644 index 00000000..59fc5254 --- /dev/null +++ b/data/cubes/hex20/connectivity.csv @@ -0,0 +1,8 @@ +0,1,2,3,4,5,6,7,8,9,10,11,16,17,18,19,12,13,14,15 +1,20,21,2,5,22,23,6,24,25,26,9,29,30,31,17,13,27,28,14 +3,2,32,33,7,6,34,35,10,36,37,38,18,41,42,43,15,14,39,40 +2,21,44,32,6,23,45,34,26,46,47,36,31,49,50,41,14,28,48,39 +4,5,6,7,51,52,53,54,16,17,18,19,59,60,61,62,55,56,57,58 +5,22,23,6,52,63,64,53,29,30,31,17,67,68,69,60,56,65,66,57 +7,6,34,35,54,53,70,71,18,41,42,43,61,74,75,76,58,57,72,73 +6,23,45,34,53,64,77,70,31,49,50,41,69,79,80,74,57,66,78,72 diff --git a/data/cubes/hex20/coords.csv b/data/cubes/hex20/coords.csv new file mode 100644 index 00000000..a8f61b9f --- /dev/null +++ b/data/cubes/hex20/coords.csv @@ -0,0 +1,81 @@ +0,0,0 +0.0050000000000000001,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.0050000000000000001,0 +0,0,0.0050000000000000001 +0.0050000000000000001,0,0.0050000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0050000000000000001,0.0050000000000000001 +0.0025000000000000001,0,0 +0.0050000000000000001,0.0025000000000000001,0 +0.0025000000000000001,0.0050000000000000001,0 +0,0.0025000000000000001,0 +0,0,0.0025000000000000001 +0.0050000000000000001,0,0.0025000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0025000000000000001 +0,0.0050000000000000001,0.0025000000000000001 +0.0025000000000000001,0,0.0050000000000000001 +0.0050000000000000001,0.0025000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0025000000000000001,0.0050000000000000001 +0.01,0,0 +0.01,0.0050000000000000001,0 +0.01,0,0.0050000000000000001 +0.01,0.0050000000000000001,0.0050000000000000001 +0.0074999999999999997,0,0 +0.01,0.0025000000000000001,0 +0.0074999999999999997,0.0050000000000000001,0 +0.01,0,0.0025000000000000001 +0.01,0.0050000000000000001,0.0025000000000000001 +0.0074999999999999997,0,0.0050000000000000001 +0.01,0.0025000000000000001,0.0050000000000000001 +0.0074999999999999997,0.0050000000000000001,0.0050000000000000001 +0.0050000000000000001,0.01,0 +0,0.01,0 +0.0050000000000000001,0.01,0.0050000000000000001 +0,0.01,0.0050000000000000001 +0.0050000000000000001,0.0074999999999999997,0 +0.0025000000000000001,0.01,0 +0,0.0074999999999999997,0 +0.0050000000000000001,0.01,0.0025000000000000001 +0,0.01,0.0025000000000000001 +0.0050000000000000001,0.0074999999999999997,0.0050000000000000001 +0.0025000000000000001,0.01,0.0050000000000000001 +0,0.0074999999999999997,0.0050000000000000001 +0.01,0.01,0 +0.01,0.01,0.0050000000000000001 +0.01,0.0074999999999999997,0 +0.0074999999999999997,0.01,0 +0.01,0.01,0.0025000000000000001 +0.01,0.0074999999999999997,0.0050000000000000001 +0.0074999999999999997,0.01,0.0050000000000000001 +0,0,0.01 +0.0050000000000000001,0,0.01 +0.0050000000000000001,0.0050000000000000001,0.01 +0,0.0050000000000000001,0.01 +0,0,0.0074999999999999997 +0.0050000000000000001,0,0.0074999999999999997 +0.0050000000000000001,0.0050000000000000001,0.0074999999999999997 +0,0.0050000000000000001,0.0074999999999999997 +0.0025000000000000001,0,0.01 +0.0050000000000000001,0.0025000000000000001,0.01 +0.0025000000000000001,0.0050000000000000001,0.01 +0,0.0025000000000000001,0.01 +0.01,0,0.01 +0.01,0.0050000000000000001,0.01 +0.01,0,0.0074999999999999997 +0.01,0.0050000000000000001,0.0074999999999999997 +0.0074999999999999997,0,0.01 +0.01,0.0025000000000000001,0.01 +0.0074999999999999997,0.0050000000000000001,0.01 +0.0050000000000000001,0.01,0.01 +0,0.01,0.01 +0.0050000000000000001,0.01,0.0074999999999999997 +0,0.01,0.0074999999999999997 +0.0050000000000000001,0.0074999999999999997,0.01 +0.0025000000000000001,0.01,0.01 +0,0.0074999999999999997,0.01 +0.01,0.01,0.01 +0.01,0.01,0.0074999999999999997 +0.01,0.0074999999999999997,0.01 +0.0074999999999999997,0.01,0.01 diff --git a/data/cubes/hex27/connectivity.csv b/data/cubes/hex27/connectivity.csv new file mode 100644 index 00000000..cc8a8ea3 --- /dev/null +++ b/data/cubes/hex27/connectivity.csv @@ -0,0 +1,8 @@ +0,1,2,3,4,5,6,7,8,9,10,11,16,17,18,19,12,13,14,15,21,22,23,24,20,25,26 +1,27,28,2,5,29,30,6,31,32,33,9,36,37,38,17,13,34,35,14,40,41,42,22,39,43,44 +3,2,45,46,7,6,47,48,10,49,50,51,18,54,55,56,15,14,52,53,23,58,59,60,57,61,62 +2,28,63,45,6,30,64,47,33,65,66,49,38,68,69,54,14,35,67,52,42,71,72,58,70,73,74 +4,5,6,7,75,76,77,78,16,17,18,19,83,84,85,86,79,80,81,82,87,88,89,90,25,91,92 +5,29,30,6,76,93,94,77,36,37,38,17,97,98,99,84,80,95,96,81,100,101,102,88,43,103,104 +7,6,47,48,78,77,105,106,18,54,55,56,85,109,110,111,82,81,107,108,89,112,113,114,61,115,116 +6,30,64,47,77,94,117,105,38,68,69,54,99,119,120,109,81,96,118,107,102,121,122,112,73,123,124 diff --git a/data/cubes/hex27/coords.csv b/data/cubes/hex27/coords.csv new file mode 100644 index 00000000..0fcb825d --- /dev/null +++ b/data/cubes/hex27/coords.csv @@ -0,0 +1,125 @@ +0,0,0 +0.0050000000000000001,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.0050000000000000001,0 +0,0,0.0050000000000000001 +0.0050000000000000001,0,0.0050000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0050000000000000001,0.0050000000000000001 +0.0025000000000000001,0,0 +0.0050000000000000001,0.0025000000000000001,0 +0.0025000000000000001,0.0050000000000000001,0 +0,0.0025000000000000001,0 +0,0,0.0025000000000000001 +0.0050000000000000001,0,0.0025000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0025000000000000001 +0,0.0050000000000000001,0.0025000000000000001 +0.0025000000000000001,0,0.0050000000000000001 +0.0050000000000000001,0.0025000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0025000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0025000000000000001,0 +0.0025000000000000001,0,0.0025000000000000001 +0.0050000000000000001,0.0025000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0050000000000000001,0.0025000000000000001 +0,0.0025000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0025000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0025000000000000001,0.0025000000000000001 +0.01,0,0 +0.01,0.0050000000000000001,0 +0.01,0,0.0050000000000000001 +0.01,0.0050000000000000001,0.0050000000000000001 +0.0074999999999999997,0,0 +0.01,0.0025000000000000001,0 +0.0074999999999999997,0.0050000000000000001,0 +0.01,0,0.0025000000000000001 +0.01,0.0050000000000000001,0.0025000000000000001 +0.0074999999999999997,0,0.0050000000000000001 +0.01,0.0025000000000000001,0.0050000000000000001 +0.0074999999999999997,0.0050000000000000001,0.0050000000000000001 +0.0074999999999999997,0.0025000000000000001,0 +0.0074999999999999997,0,0.0025000000000000001 +0.01,0.0025000000000000001,0.0025000000000000001 +0.0074999999999999997,0.0050000000000000001,0.0025000000000000001 +0.0074999999999999997,0.0025000000000000001,0.0050000000000000001 +0.0074999999999999997,0.0025000000000000001,0.0025000000000000001 +0.0050000000000000001,0.01,0 +0,0.01,0 +0.0050000000000000001,0.01,0.0050000000000000001 +0,0.01,0.0050000000000000001 +0.0050000000000000001,0.0074999999999999997,0 +0.0025000000000000001,0.01,0 +0,0.0074999999999999997,0 +0.0050000000000000001,0.01,0.0025000000000000001 +0,0.01,0.0025000000000000001 +0.0050000000000000001,0.0074999999999999997,0.0050000000000000001 +0.0025000000000000001,0.01,0.0050000000000000001 +0,0.0074999999999999997,0.0050000000000000001 +0.0025000000000000001,0.0074999999999999997,0 +0.0050000000000000001,0.0074999999999999997,0.0025000000000000001 +0.0025000000000000001,0.01,0.0025000000000000001 +0,0.0074999999999999997,0.0025000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0050000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0025000000000000001 +0.01,0.01,0 +0.01,0.01,0.0050000000000000001 +0.01,0.0074999999999999997,0 +0.0074999999999999997,0.01,0 +0.01,0.01,0.0025000000000000001 +0.01,0.0074999999999999997,0.0050000000000000001 +0.0074999999999999997,0.01,0.0050000000000000001 +0.0074999999999999997,0.0074999999999999997,0 +0.01,0.0074999999999999997,0.0025000000000000001 +0.0074999999999999997,0.01,0.0025000000000000001 +0.0074999999999999997,0.0074999999999999997,0.0050000000000000001 +0.0074999999999999997,0.0074999999999999997,0.0025000000000000001 +0,0,0.01 +0.0050000000000000001,0,0.01 +0.0050000000000000001,0.0050000000000000001,0.01 +0,0.0050000000000000001,0.01 +0,0,0.0074999999999999997 +0.0050000000000000001,0,0.0074999999999999997 +0.0050000000000000001,0.0050000000000000001,0.0074999999999999997 +0,0.0050000000000000001,0.0074999999999999997 +0.0025000000000000001,0,0.01 +0.0050000000000000001,0.0025000000000000001,0.01 +0.0025000000000000001,0.0050000000000000001,0.01 +0,0.0025000000000000001,0.01 +0.0025000000000000001,0,0.0074999999999999997 +0.0050000000000000001,0.0025000000000000001,0.0074999999999999997 +0.0025000000000000001,0.0050000000000000001,0.0074999999999999997 +0,0.0025000000000000001,0.0074999999999999997 +0.0025000000000000001,0.0025000000000000001,0.01 +0.0025000000000000001,0.0025000000000000001,0.0074999999999999997 +0.01,0,0.01 +0.01,0.0050000000000000001,0.01 +0.01,0,0.0074999999999999997 +0.01,0.0050000000000000001,0.0074999999999999997 +0.0074999999999999997,0,0.01 +0.01,0.0025000000000000001,0.01 +0.0074999999999999997,0.0050000000000000001,0.01 +0.0074999999999999997,0,0.0074999999999999997 +0.01,0.0025000000000000001,0.0074999999999999997 +0.0074999999999999997,0.0050000000000000001,0.0074999999999999997 +0.0074999999999999997,0.0025000000000000001,0.01 +0.0074999999999999997,0.0025000000000000001,0.0074999999999999997 +0.0050000000000000001,0.01,0.01 +0,0.01,0.01 +0.0050000000000000001,0.01,0.0074999999999999997 +0,0.01,0.0074999999999999997 +0.0050000000000000001,0.0074999999999999997,0.01 +0.0025000000000000001,0.01,0.01 +0,0.0074999999999999997,0.01 +0.0050000000000000001,0.0074999999999999997,0.0074999999999999997 +0.0025000000000000001,0.01,0.0074999999999999997 +0,0.0074999999999999997,0.0074999999999999997 +0.0025000000000000001,0.0074999999999999997,0.01 +0.0025000000000000001,0.0074999999999999997,0.0074999999999999997 +0.01,0.01,0.01 +0.01,0.01,0.0074999999999999997 +0.01,0.0074999999999999997,0.01 +0.0074999999999999997,0.01,0.01 +0.01,0.0074999999999999997,0.0074999999999999997 +0.0074999999999999997,0.01,0.0074999999999999997 +0.0074999999999999997,0.0074999999999999997,0.01 +0.0074999999999999997,0.0074999999999999997,0.0074999999999999997 diff --git a/data/cubes/hex8/connectivity.csv b/data/cubes/hex8/connectivity.csv new file mode 100644 index 00000000..85fe4732 --- /dev/null +++ b/data/cubes/hex8/connectivity.csv @@ -0,0 +1,8 @@ +0,1,2,3,4,5,6,7 +1,8,9,2,5,10,11,6 +3,2,12,13,7,6,14,15 +2,9,16,12,6,11,17,14 +4,5,6,7,18,19,20,21 +5,10,11,6,19,22,23,20 +7,6,14,15,21,20,24,25 +6,11,17,14,20,23,26,24 diff --git a/data/cubes/hex8/coords.csv b/data/cubes/hex8/coords.csv new file mode 100644 index 00000000..0317ada8 --- /dev/null +++ b/data/cubes/hex8/coords.csv @@ -0,0 +1,27 @@ +0,0,0 +0.0050000000000000001,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.0050000000000000001,0 +0,0,0.0050000000000000001 +0.0050000000000000001,0,0.0050000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0050000000000000001,0.0050000000000000001 +0.01,0,0 +0.01,0.0050000000000000001,0 +0.01,0,0.0050000000000000001 +0.01,0.0050000000000000001,0.0050000000000000001 +0.0050000000000000001,0.01,0 +0,0.01,0 +0.0050000000000000001,0.01,0.0050000000000000001 +0,0.01,0.0050000000000000001 +0.01,0.01,0 +0.01,0.01,0.0050000000000000001 +0,0,0.01 +0.0050000000000000001,0,0.01 +0.0050000000000000001,0.0050000000000000001,0.01 +0,0.0050000000000000001,0.01 +0.01,0,0.01 +0.01,0.0050000000000000001,0.01 +0.0050000000000000001,0.01,0.01 +0,0.01,0.01 +0.01,0.01,0.01 diff --git a/data/cubes/tet10/connectivity.csv b/data/cubes/tet10/connectivity.csv new file mode 100644 index 00000000..50ed5378 --- /dev/null +++ b/data/cubes/tet10/connectivity.csv @@ -0,0 +1,24 @@ +0,1,2,3,4,5,6,7,8,9 +2,1,10,3,5,11,12,9,8,13 +10,1,14,3,11,15,16,13,8,17 +14,1,0,3,15,4,18,17,8,7 +0,19,14,3,20,21,18,7,22,17 +14,19,23,3,21,24,25,17,22,26 +23,19,27,3,24,28,29,26,22,30 +27,19,0,3,28,20,31,30,22,7 +14,32,10,3,33,34,16,17,35,13 +10,32,36,3,34,37,38,13,35,39 +36,32,23,3,37,40,41,39,35,26 +23,32,14,3,40,33,25,26,35,17 +10,42,2,3,43,44,12,13,45,9 +2,42,46,3,44,47,48,9,45,49 +46,42,36,3,47,50,51,49,45,39 +36,42,10,3,50,43,38,39,45,13 +2,52,0,3,53,54,6,9,55,7 +0,52,27,3,54,56,31,7,55,30 +27,52,46,3,56,57,58,30,55,49 +46,52,2,3,57,53,48,49,55,9 +27,59,23,3,60,61,29,30,62,26 +23,59,36,3,61,63,41,26,62,39 +36,59,46,3,63,64,51,39,62,49 +46,59,27,3,64,60,58,49,62,30 diff --git a/data/cubes/tet10/coords.csv b/data/cubes/tet10/coords.csv new file mode 100644 index 00000000..c09e8dfc --- /dev/null +++ b/data/cubes/tet10/coords.csv @@ -0,0 +1,65 @@ +0,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.01,0 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0025000000000000001,0 +0.0025000000000000001,0.0074999999999999997,0 +0,0.0050000000000000001,0 +0.0025000000000000001,0.0025000000000000001,0.0025000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0025000000000000001 +0.01,0.01,0 +0.0074999999999999997,0.0074999999999999997,0 +0.0050000000000000001,0.01,0 +0.0074999999999999997,0.0074999999999999997,0.0025000000000000001 +0.01,0,0 +0.0074999999999999997,0.0025000000000000001,0 +0.01,0.0050000000000000001,0 +0.0074999999999999997,0.0025000000000000001,0.0025000000000000001 +0.0050000000000000001,0,0 +0.0050000000000000001,0,0.0050000000000000001 +0.0025000000000000001,0,0.0025000000000000001 +0.0074999999999999997,0,0.0025000000000000001 +0.0050000000000000001,0.0025000000000000001,0.0050000000000000001 +0.01,0,0.01 +0.0074999999999999997,0,0.0074999999999999997 +0.01,0,0.0050000000000000001 +0.0074999999999999997,0.0025000000000000001,0.0074999999999999997 +0,0,0.01 +0.0025000000000000001,0,0.0074999999999999997 +0.0050000000000000001,0,0.01 +0.0025000000000000001,0.0025000000000000001,0.0074999999999999997 +0,0,0.0050000000000000001 +0.01,0.0050000000000000001,0.0050000000000000001 +0.01,0.0025000000000000001,0.0025000000000000001 +0.01,0.0074999999999999997,0.0025000000000000001 +0.0074999999999999997,0.0050000000000000001,0.0050000000000000001 +0.01,0.01,0.01 +0.01,0.0074999999999999997,0.0074999999999999997 +0.01,0.01,0.0050000000000000001 +0.0074999999999999997,0.0074999999999999997,0.0074999999999999997 +0.01,0.0025000000000000001,0.0074999999999999997 +0.01,0.0050000000000000001,0.01 +0.0050000000000000001,0.01,0.0050000000000000001 +0.0074999999999999997,0.01,0.0025000000000000001 +0.0025000000000000001,0.01,0.0025000000000000001 +0.0050000000000000001,0.0074999999999999997,0.0050000000000000001 +0,0.01,0.01 +0.0025000000000000001,0.01,0.0074999999999999997 +0,0.01,0.0050000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0074999999999999997 +0.0074999999999999997,0.01,0.0074999999999999997 +0.0050000000000000001,0.01,0.01 +0,0.0050000000000000001,0.0050000000000000001 +0,0.0074999999999999997,0.0025000000000000001 +0,0.0025000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0025000000000000001,0.0074999999999999997 +0,0.0074999999999999997,0.0074999999999999997 +0,0.0050000000000000001,0.01 +0.0050000000000000001,0.0050000000000000001,0.01 +0.0025000000000000001,0.0025000000000000001,0.01 +0.0074999999999999997,0.0025000000000000001,0.01 +0.0050000000000000001,0.0050000000000000001,0.0074999999999999997 +0.0074999999999999997,0.0074999999999999997,0.01 +0.0025000000000000001,0.0074999999999999997,0.01 diff --git a/data/cubes/tet14/connectivity.csv b/data/cubes/tet14/connectivity.csv new file mode 100644 index 00000000..af12bcd3 --- /dev/null +++ b/data/cubes/tet14/connectivity.csv @@ -0,0 +1,14 @@ +1,3,15,22,1,22,39,46,22,15,63,39,15,3,83,63,3,1,46,83,46,39,63,83 +2,2,2,2,32,32,32,32,56,56,56,56,76,76,76,76,96,96,96,96,112,112,112,112 +3,15,22,1,22,39,46,1,15,63,39,22,3,83,63,15,1,46,83,3,39,63,83,46 +4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4 +5,6,16,23,33,34,40,47,57,58,64,70,77,78,84,90,97,98,103,106,113,114,119,122 +6,16,23,5,34,40,47,33,58,64,70,57,78,84,90,77,98,103,106,97,114,119,122,113 +7,17,24,29,29,41,48,53,24,65,71,41,17,85,91,65,7,53,107,85,48,71,91,107 +8,10,18,25,8,25,42,49,25,18,66,42,18,10,86,66,10,8,49,86,49,42,66,86 +9,9,9,9,35,35,35,35,59,59,59,59,79,79,79,79,99,99,99,99,115,115,115,115 +10,18,25,8,25,42,49,8,18,66,42,25,10,86,66,18,8,49,86,10,42,66,86,49 +11,19,26,30,36,43,50,54,60,67,72,75,80,87,92,95,100,104,108,111,116,120,123,125 +13,20,27,12,38,44,51,37,62,68,73,61,82,88,93,81,102,105,109,101,118,121,124,117 +14,21,28,31,31,45,52,55,28,69,74,45,21,89,94,69,14,55,110,89,52,74,94,110 +12,13,20,27,37,38,44,51,61,62,68,73,81,82,88,93,101,102,105,109,117,118,121,124 diff --git a/data/cubes/tet14/coords.csv b/data/cubes/tet14/coords.csv new file mode 100644 index 00000000..830629d3 --- /dev/null +++ b/data/cubes/tet14/coords.csv @@ -0,0 +1,125 @@ +0,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.01,0 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0.0025000000000000001,0.0025000000000000001,0 +0.0025000000000000001,0.0074999999999999997,0 +0,0.0050000000000000001,0 +0.0025000000000000001,0.0025000000000000001,0.0025000000000000001 +0.0050000000000000001,0.0050000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0025000000000000001 +0.0016666666666666668,0.0050000000000000001,0 +0.0033333333333333335,0.0033333333333333335,0.0016666666666666668 +0.0033333333333333335,0.0066666666666666671,0.0016666666666666668 +0.0016666666666666668,0.0050000000000000001,0.0016666666666666668 +0.01,0.01,0 +0.0074999999999999997,0.0074999999999999997,0 +0.0050000000000000001,0.01,0 +0.0074999999999999997,0.0074999999999999997,0.0025000000000000001 +0.0050000000000000001,0.0083333333333333332,0 +0.0066666666666666671,0.0066666666666666671,0.0016666666666666668 +0.0050000000000000001,0.0083333333333333332,0.0016666666666666668 +0.01,0,0 +0.0074999999999999997,0.0025000000000000001,0 +0.01,0.0050000000000000001,0 +0.0074999999999999997,0.0025000000000000001,0.0025000000000000001 +0.0083333333333333332,0.0050000000000000001,0 +0.0066666666666666671,0.0033333333333333335,0.0016666666666666668 +0.0083333333333333332,0.0050000000000000001,0.0016666666666666668 +0.0050000000000000001,0,0 +0.0050000000000000001,0.0016666666666666668,0 +0.0050000000000000001,0.0016666666666666668,0.0016666666666666668 +0.0050000000000000001,0,0.0050000000000000001 +0.0025000000000000001,0,0.0025000000000000001 +0.0074999999999999997,0,0.0025000000000000001 +0.0050000000000000001,0.0025000000000000001,0.0050000000000000001 +0.0050000000000000001,0,0.0016666666666666668 +0.0033333333333333335,0.0016666666666666668,0.0033333333333333335 +0.0066666666666666671,0.0016666666666666668,0.0033333333333333335 +0.01,0,0.01 +0.0074999999999999997,0,0.0074999999999999997 +0.01,0,0.0050000000000000001 +0.0074999999999999997,0.0025000000000000001,0.0074999999999999997 +0.0083333333333333332,0,0.0050000000000000001 +0.0066666666666666671,0.0016666666666666668,0.0066666666666666671 +0.0083333333333333332,0.0016666666666666668,0.0050000000000000001 +0,0,0.01 +0.0025000000000000001,0,0.0074999999999999997 +0.0050000000000000001,0,0.01 +0.0025000000000000001,0.0025000000000000001,0.0074999999999999997 +0.0050000000000000001,0,0.0083333333333333332 +0.0033333333333333335,0.0016666666666666668,0.0066666666666666671 +0.0050000000000000001,0.0016666666666666668,0.0083333333333333332 +0,0,0.0050000000000000001 +0.0016666666666666668,0,0.0050000000000000001 +0.0016666666666666668,0.0016666666666666668,0.0050000000000000001 +0.01,0.0050000000000000001,0.0050000000000000001 +0.01,0.0025000000000000001,0.0025000000000000001 +0.01,0.0074999999999999997,0.0025000000000000001 +0.0074999999999999997,0.0050000000000000001,0.0050000000000000001 +0.01,0.0050000000000000001,0.0016666666666666668 +0.0083333333333333332,0.0033333333333333335,0.0033333333333333335 +0.0083333333333333332,0.0066666666666666671,0.0033333333333333335 +0.01,0.01,0.01 +0.01,0.0074999999999999997,0.0074999999999999997 +0.01,0.01,0.0050000000000000001 +0.0074999999999999997,0.0074999999999999997,0.0074999999999999997 +0.01,0.0083333333333333332,0.0050000000000000001 +0.0083333333333333332,0.0066666666666666671,0.0066666666666666671 +0.0083333333333333332,0.0083333333333333332,0.0050000000000000001 +0.01,0.0025000000000000001,0.0074999999999999997 +0.01,0.0050000000000000001,0.01 +0.01,0.0050000000000000001,0.0083333333333333332 +0.0083333333333333332,0.0033333333333333335,0.0066666666666666671 +0.0083333333333333332,0.0050000000000000001,0.0083333333333333332 +0.01,0.0016666666666666668,0.0050000000000000001 +0.0050000000000000001,0.01,0.0050000000000000001 +0.0074999999999999997,0.01,0.0025000000000000001 +0.0025000000000000001,0.01,0.0025000000000000001 +0.0050000000000000001,0.0074999999999999997,0.0050000000000000001 +0.0050000000000000001,0.01,0.0016666666666666668 +0.0066666666666666671,0.0083333333333333332,0.0033333333333333335 +0.0033333333333333335,0.0083333333333333332,0.0033333333333333335 +0,0.01,0.01 +0.0025000000000000001,0.01,0.0074999999999999997 +0,0.01,0.0050000000000000001 +0.0025000000000000001,0.0074999999999999997,0.0074999999999999997 +0.0016666666666666668,0.01,0.0050000000000000001 +0.0033333333333333335,0.0083333333333333332,0.0066666666666666671 +0.0016666666666666668,0.0083333333333333332,0.0050000000000000001 +0.0074999999999999997,0.01,0.0074999999999999997 +0.0050000000000000001,0.01,0.01 +0.0050000000000000001,0.01,0.0083333333333333332 +0.0066666666666666671,0.0083333333333333332,0.0066666666666666671 +0.0050000000000000001,0.0083333333333333332,0.0083333333333333332 +0.0083333333333333332,0.01,0.0050000000000000001 +0,0.0050000000000000001,0.0050000000000000001 +0,0.0074999999999999997,0.0025000000000000001 +0,0.0025000000000000001,0.0025000000000000001 +0.0025000000000000001,0.0050000000000000001,0.0050000000000000001 +0,0.0050000000000000001,0.0016666666666666668 +0.0016666666666666668,0.0066666666666666671,0.0033333333333333335 +0.0016666666666666668,0.0033333333333333335,0.0033333333333333335 +0,0.0025000000000000001,0.0074999999999999997 +0,0.0016666666666666668,0.0050000000000000001 +0.0016666666666666668,0.0033333333333333335,0.0066666666666666671 +0,0.0074999999999999997,0.0074999999999999997 +0,0.0050000000000000001,0.01 +0,0.0050000000000000001,0.0083333333333333332 +0.0016666666666666668,0.0066666666666666671,0.0066666666666666671 +0.0016666666666666668,0.0050000000000000001,0.0083333333333333332 +0,0.0083333333333333332,0.0050000000000000001 +0.0050000000000000001,0.0050000000000000001,0.01 +0.0025000000000000001,0.0025000000000000001,0.01 +0.0074999999999999997,0.0025000000000000001,0.01 +0.0050000000000000001,0.0050000000000000001,0.0074999999999999997 +0.0050000000000000001,0.0016666666666666668,0.01 +0.0033333333333333335,0.0033333333333333335,0.0083333333333333332 +0.0066666666666666671,0.0033333333333333335,0.0083333333333333332 +0.0074999999999999997,0.0074999999999999997,0.01 +0.0083333333333333332,0.0050000000000000001,0.01 +0.0066666666666666671,0.0066666666666666671,0.0083333333333333332 +0.0025000000000000001,0.0074999999999999997,0.01 +0.0050000000000000001,0.0083333333333333332,0.01 +0.0033333333333333335,0.0066666666666666671,0.0083333333333333332 +0.0016666666666666668,0.0050000000000000001,0.01 diff --git a/data/cubes/tet4/connectivity.csv b/data/cubes/tet4/connectivity.csv new file mode 100644 index 00000000..13ac6ec1 --- /dev/null +++ b/data/cubes/tet4/connectivity.csv @@ -0,0 +1,24 @@ +0,1,2,3 +2,1,4,3 +4,1,5,3 +5,1,0,3 +0,6,5,3 +5,6,7,3 +7,6,8,3 +8,6,0,3 +5,9,4,3 +4,9,10,3 +10,9,7,3 +7,9,5,3 +4,11,2,3 +2,11,12,3 +12,11,10,3 +10,11,4,3 +2,13,0,3 +0,13,8,3 +8,13,12,3 +12,13,2,3 +8,14,7,3 +7,14,10,3 +10,14,12,3 +12,14,8,3 diff --git a/data/cubes/tet4/coords.csv b/data/cubes/tet4/coords.csv new file mode 100644 index 00000000..670f2b7d --- /dev/null +++ b/data/cubes/tet4/coords.csv @@ -0,0 +1,15 @@ +0,0,0 +0.0050000000000000001,0.0050000000000000001,0 +0,0.01,0 +0.0050000000000000001,0.0050000000000000001,0.0050000000000000001 +0.01,0.01,0 +0.01,0,0 +0.0050000000000000001,0,0.0050000000000000001 +0.01,0,0.01 +0,0,0.01 +0.01,0.0050000000000000001,0.0050000000000000001 +0.01,0.01,0.01 +0.0050000000000000001,0.01,0.0050000000000000001 +0,0.01,0.01 +0,0.0050000000000000001,0.0050000000000000001 +0.0050000000000000001,0.0050000000000000001,0.01 diff --git a/data/edge/gen_data_rot.py b/data/edge/gen_data_rot.py index 59d8832d..f8fa72e5 100644 --- a/data/edge/gen_data_rot.py +++ b/data/edge/gen_data_rot.py @@ -2,10 +2,18 @@ import os from pathlib import Path +from riley.python import meshconv + # Coordinate System: Right-handed Cartesian (X right, Y up, Z towards viewer). # Vertex Winding: All elements MUST follow Counter-Clockwise (CCW) winding. def save_case(base_dir, name, coords, connect, disp_x, disp_y, disp_z): + mesh = meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + connect = meshconv.enforce_mesh_convention(mesh).connect["connect1"] out_dir = Path(base_dir) / name out_dir.mkdir(parents=True, exist_ok=True) np.savetxt(out_dir / "coords.csv", coords, delimiter=",") diff --git a/data/edge/gen_distort_data.py b/data/edge/gen_distort_data.py index ad664be2..79cb484d 100644 --- a/data/edge/gen_distort_data.py +++ b/data/edge/gen_distort_data.py @@ -2,6 +2,8 @@ import numpy as np +from riley.python import meshconv + EDGE_LENG = 10.0 ELEM_ROT = 20.0 @@ -37,6 +39,12 @@ def save_case( disp_z, ELEM_ROT, ) + mesh = meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + connect = meshconv.enforce_mesh_convention(mesh).connect["connect1"] np.savetxt(out_dir / "coords.csv", coords, delimiter=",") np.savetxt( out_dir / "connect.csv", diff --git a/data/edge/gen_stress_data.py b/data/edge/gen_stress_data.py index 5853e5de..588fee0c 100644 --- a/data/edge/gen_stress_data.py +++ b/data/edge/gen_stress_data.py @@ -1,6 +1,8 @@ import os import numpy as np +from riley.python import meshconv + def generate_stress_data(): path = "data/edge/tri6_stress" os.makedirs(path, exist_ok=True) @@ -43,6 +45,14 @@ def move_away(p, target, dist): m30_new = move_away(m30, c2, 1.0) nodes = [v0, v1, v2, v3, m01_new, m12_new, m20_new, m13_new, m30_new] + coords = np.asarray(nodes, dtype=np.float64) + connect = meshconv.enforce_mesh_convention( + meshconv.MeshData( + coords=coords, + connect={"connect1": np.array([[0, 1, 2, 4, 5, 6], [1, 0, 3, 4, 8, 7]], dtype=np.int64)}, + mesh_type="surface", + ) + ).connect["connect1"] # coords.csv with open(f"{path}/coords.csv", "w") as f: @@ -52,8 +62,7 @@ def move_away(p, target, dist): # connectivity.csv # Tri6: 0,1,2 corners, 3,4,5 midsides (0-1, 1-2, 2-0) with open(f"{path}/connectivity.csv", "w") as f: - f.write("0,1,2,4,5,6\n") # Tri 1 - f.write("1,0,3,4,8,7\n") # Tri 2 + np.savetxt(f, connect, delimiter=",", fmt="%d") # uvs.csv (rescale to be between 0.3 and 0.7) with open(f"{path}/uvs.csv", "w") as f: diff --git a/data/edge/gen_vertbulge_data.py b/data/edge/gen_vertbulge_data.py index 8131b1e8..45f02ddd 100644 --- a/data/edge/gen_vertbulge_data.py +++ b/data/edge/gen_vertbulge_data.py @@ -1,6 +1,8 @@ import os import numpy as np +from riley.python import meshconv + def generate_vertbulge_data(): path = "data/edge/tri6_vertbulge" os.makedirs(path, exist_ok=True) @@ -19,6 +21,14 @@ def generate_vertbulge_data(): m20 = np.array([0.0, 5.0, 0.0]) nodes = [v0, v1, v2, m01, m12, m20] + coords = np.asarray(nodes, dtype=np.float64) + connect = meshconv.enforce_mesh_convention( + meshconv.MeshData( + coords=coords, + connect={"connect1": np.array([[0, 1, 2, 3, 4, 5]], dtype=np.int64)}, + mesh_type="surface", + ) + ).connect["connect1"] # coords.csv with open(f"{path}/coords.csv", "w") as f: @@ -27,7 +37,7 @@ def generate_vertbulge_data(): # connectivity.csv with open(f"{path}/connectivity.csv", "w") as f: - f.write("0,1,2,3,4,5\n") + np.savetxt(f, connect, delimiter=",", fmt="%d") # uvs.csv (center them) with open(f"{path}/uvs.csv", "w") as f: diff --git a/data/min/quad4ibi_sphere200/connect.csv b/data/min/quad4ibi_sphere200/connect.csv index 6e916021..983d7e8d 100644 --- a/data/min/quad4ibi_sphere200/connect.csv +++ b/data/min/quad4ibi_sphere200/connect.csv @@ -1,225 +1,225 @@ -0,1,17,16 -1,2,18,17 -2,3,19,18 -3,4,20,19 -4,5,21,20 -5,6,22,21 -6,7,23,22 -7,8,24,23 -8,9,25,24 -9,10,26,25 -10,11,27,26 -11,12,28,27 -12,13,29,28 -13,14,30,29 -14,15,31,30 -16,17,33,32 -17,18,34,33 -18,19,35,34 -19,20,36,35 -20,21,37,36 -21,22,38,37 -22,23,39,38 -23,24,40,39 -24,25,41,40 -25,26,42,41 -26,27,43,42 -27,28,44,43 -28,29,45,44 -29,30,46,45 -30,31,47,46 -32,33,49,48 -33,34,50,49 -34,35,51,50 -35,36,52,51 -36,37,53,52 -37,38,54,53 -38,39,55,54 -39,40,56,55 -40,41,57,56 -41,42,58,57 -42,43,59,58 -43,44,60,59 -44,45,61,60 -45,46,62,61 -46,47,63,62 -48,49,65,64 -49,50,66,65 -50,51,67,66 -51,52,68,67 -52,53,69,68 -53,54,70,69 -54,55,71,70 -55,56,72,71 -56,57,73,72 -57,58,74,73 -58,59,75,74 -59,60,76,75 -60,61,77,76 -61,62,78,77 -62,63,79,78 -64,65,81,80 -65,66,82,81 -66,67,83,82 -67,68,84,83 -68,69,85,84 -69,70,86,85 -70,71,87,86 -71,72,88,87 -72,73,89,88 -73,74,90,89 -74,75,91,90 -75,76,92,91 -76,77,93,92 -77,78,94,93 -78,79,95,94 -80,81,97,96 -81,82,98,97 -82,83,99,98 -83,84,100,99 -84,85,101,100 -85,86,102,101 -86,87,103,102 -87,88,104,103 -88,89,105,104 -89,90,106,105 -90,91,107,106 -91,92,108,107 -92,93,109,108 -93,94,110,109 -94,95,111,110 -96,97,113,112 -97,98,114,113 -98,99,115,114 -99,100,116,115 -100,101,117,116 -101,102,118,117 -102,103,119,118 -103,104,120,119 -104,105,121,120 -105,106,122,121 -106,107,123,122 -107,108,124,123 -108,109,125,124 -109,110,126,125 -110,111,127,126 -112,113,129,128 -113,114,130,129 -114,115,131,130 -115,116,132,131 -116,117,133,132 -117,118,134,133 -118,119,135,134 -119,120,136,135 -120,121,137,136 -121,122,138,137 -122,123,139,138 -123,124,140,139 -124,125,141,140 -125,126,142,141 -126,127,143,142 -128,129,145,144 -129,130,146,145 -130,131,147,146 -131,132,148,147 -132,133,149,148 -133,134,150,149 -134,135,151,150 -135,136,152,151 -136,137,153,152 -137,138,154,153 -138,139,155,154 -139,140,156,155 -140,141,157,156 -141,142,158,157 -142,143,159,158 -144,145,161,160 -145,146,162,161 -146,147,163,162 -147,148,164,163 -148,149,165,164 -149,150,166,165 -150,151,167,166 -151,152,168,167 -152,153,169,168 -153,154,170,169 -154,155,171,170 -155,156,172,171 -156,157,173,172 -157,158,174,173 -158,159,175,174 -160,161,177,176 -161,162,178,177 -162,163,179,178 -163,164,180,179 -164,165,181,180 -165,166,182,181 -166,167,183,182 -167,168,184,183 -168,169,185,184 -169,170,186,185 -170,171,187,186 -171,172,188,187 -172,173,189,188 -173,174,190,189 -174,175,191,190 -176,177,193,192 -177,178,194,193 -178,179,195,194 -179,180,196,195 -180,181,197,196 -181,182,198,197 -182,183,199,198 -183,184,200,199 -184,185,201,200 -185,186,202,201 -186,187,203,202 -187,188,204,203 -188,189,205,204 -189,190,206,205 -190,191,207,206 -192,193,209,208 -193,194,210,209 -194,195,211,210 -195,196,212,211 -196,197,213,212 -197,198,214,213 -198,199,215,214 -199,200,216,215 -200,201,217,216 -201,202,218,217 -202,203,219,218 -203,204,220,219 -204,205,221,220 -205,206,222,221 -206,207,223,222 -208,209,225,224 -209,210,226,225 -210,211,227,226 -211,212,228,227 -212,213,229,228 -213,214,230,229 -214,215,231,230 -215,216,232,231 -216,217,233,232 -217,218,234,233 -218,219,235,234 -219,220,236,235 -220,221,237,236 -221,222,238,237 -222,223,239,238 -224,225,241,240 -225,226,242,241 -226,227,243,242 -227,228,244,243 -228,229,245,244 -229,230,246,245 -230,231,247,246 -231,232,248,247 -232,233,249,248 -233,234,250,249 -234,235,251,250 -235,236,252,251 -236,237,253,252 -237,238,254,253 -238,239,255,254 +0,16,17,1 +1,17,18,2 +2,18,19,3 +3,19,20,4 +4,20,21,5 +5,21,22,6 +6,22,23,7 +7,23,24,8 +8,24,25,9 +9,25,26,10 +10,26,27,11 +11,27,28,12 +12,28,29,13 +13,29,30,14 +14,30,31,15 +16,32,33,17 +17,33,34,18 +18,34,35,19 +19,35,36,20 +20,36,37,21 +21,37,38,22 +22,38,39,23 +23,39,40,24 +24,40,41,25 +25,41,42,26 +26,42,43,27 +27,43,44,28 +28,44,45,29 +29,45,46,30 +30,46,47,31 +32,48,49,33 +33,49,50,34 +34,50,51,35 +35,51,52,36 +36,52,53,37 +37,53,54,38 +38,54,55,39 +39,55,56,40 +40,56,57,41 +41,57,58,42 +42,58,59,43 +43,59,60,44 +44,60,61,45 +45,61,62,46 +46,62,63,47 +48,64,65,49 +49,65,66,50 +50,66,67,51 +51,67,68,52 +52,68,69,53 +53,69,70,54 +54,70,71,55 +55,71,72,56 +56,72,73,57 +57,73,74,58 +58,74,75,59 +59,75,76,60 +60,76,77,61 +61,77,78,62 +62,78,79,63 +64,80,81,65 +65,81,82,66 +66,82,83,67 +67,83,84,68 +68,84,85,69 +69,85,86,70 +70,86,87,71 +71,87,88,72 +72,88,89,73 +73,89,90,74 +74,90,91,75 +75,91,92,76 +76,92,93,77 +77,93,94,78 +78,94,95,79 +80,96,97,81 +81,97,98,82 +82,98,99,83 +83,99,100,84 +84,100,101,85 +85,101,102,86 +86,102,103,87 +87,103,104,88 +88,104,105,89 +89,105,106,90 +90,106,107,91 +91,107,108,92 +92,108,109,93 +93,109,110,94 +94,110,111,95 +96,112,113,97 +97,113,114,98 +98,114,115,99 +99,115,116,100 +100,116,117,101 +101,117,118,102 +102,118,119,103 +103,119,120,104 +104,120,121,105 +105,121,122,106 +106,122,123,107 +107,123,124,108 +108,124,125,109 +109,125,126,110 +110,126,127,111 +112,128,129,113 +113,129,130,114 +114,130,131,115 +115,131,132,116 +116,132,133,117 +117,133,134,118 +118,134,135,119 +119,135,136,120 +120,136,137,121 +121,137,138,122 +122,138,139,123 +123,139,140,124 +124,140,141,125 +125,141,142,126 +126,142,143,127 +128,144,145,129 +129,145,146,130 +130,146,147,131 +131,147,148,132 +132,148,149,133 +133,149,150,134 +134,150,151,135 +135,151,152,136 +136,152,153,137 +137,153,154,138 +138,154,155,139 +139,155,156,140 +140,156,157,141 +141,157,158,142 +142,158,159,143 +144,160,161,145 +145,161,162,146 +146,162,163,147 +147,163,164,148 +148,164,165,149 +149,165,166,150 +150,166,167,151 +151,167,168,152 +152,168,169,153 +153,169,170,154 +154,170,171,155 +155,171,172,156 +156,172,173,157 +157,173,174,158 +158,174,175,159 +160,176,177,161 +161,177,178,162 +162,178,179,163 +163,179,180,164 +164,180,181,165 +165,181,182,166 +166,182,183,167 +167,183,184,168 +168,184,185,169 +169,185,186,170 +170,186,187,171 +171,187,188,172 +172,188,189,173 +173,189,190,174 +174,190,191,175 +176,192,193,177 +177,193,194,178 +178,194,195,179 +179,195,196,180 +180,196,197,181 +181,197,198,182 +182,198,199,183 +183,199,200,184 +184,200,201,185 +185,201,202,186 +186,202,203,187 +187,203,204,188 +188,204,205,189 +189,205,206,190 +190,206,207,191 +192,208,209,193 +193,209,210,194 +194,210,211,195 +195,211,212,196 +196,212,213,197 +197,213,214,198 +198,214,215,199 +199,215,216,200 +200,216,217,201 +201,217,218,202 +202,218,219,203 +203,219,220,204 +204,220,221,205 +205,221,222,206 +206,222,223,207 +208,224,225,209 +209,225,226,210 +210,226,227,211 +211,227,228,212 +212,228,229,213 +213,229,230,214 +214,230,231,215 +215,231,232,216 +216,232,233,217 +217,233,234,218 +218,234,235,219 +219,235,236,220 +220,236,237,221 +221,237,238,222 +222,238,239,223 +224,240,241,225 +225,241,242,226 +226,242,243,227 +227,243,244,228 +228,244,245,229 +229,245,246,230 +230,246,247,231 +231,247,248,232 +232,248,249,233 +233,249,250,234 +234,250,251,235 +235,251,252,236 +236,252,253,237 +237,253,254,238 +238,254,255,239 diff --git a/data/min/quad4newton_sphere200/connect.csv b/data/min/quad4newton_sphere200/connect.csv index 6e916021..983d7e8d 100644 --- a/data/min/quad4newton_sphere200/connect.csv +++ b/data/min/quad4newton_sphere200/connect.csv @@ -1,225 +1,225 @@ -0,1,17,16 -1,2,18,17 -2,3,19,18 -3,4,20,19 -4,5,21,20 -5,6,22,21 -6,7,23,22 -7,8,24,23 -8,9,25,24 -9,10,26,25 -10,11,27,26 -11,12,28,27 -12,13,29,28 -13,14,30,29 -14,15,31,30 -16,17,33,32 -17,18,34,33 -18,19,35,34 -19,20,36,35 -20,21,37,36 -21,22,38,37 -22,23,39,38 -23,24,40,39 -24,25,41,40 -25,26,42,41 -26,27,43,42 -27,28,44,43 -28,29,45,44 -29,30,46,45 -30,31,47,46 -32,33,49,48 -33,34,50,49 -34,35,51,50 -35,36,52,51 -36,37,53,52 -37,38,54,53 -38,39,55,54 -39,40,56,55 -40,41,57,56 -41,42,58,57 -42,43,59,58 -43,44,60,59 -44,45,61,60 -45,46,62,61 -46,47,63,62 -48,49,65,64 -49,50,66,65 -50,51,67,66 -51,52,68,67 -52,53,69,68 -53,54,70,69 -54,55,71,70 -55,56,72,71 -56,57,73,72 -57,58,74,73 -58,59,75,74 -59,60,76,75 -60,61,77,76 -61,62,78,77 -62,63,79,78 -64,65,81,80 -65,66,82,81 -66,67,83,82 -67,68,84,83 -68,69,85,84 -69,70,86,85 -70,71,87,86 -71,72,88,87 -72,73,89,88 -73,74,90,89 -74,75,91,90 -75,76,92,91 -76,77,93,92 -77,78,94,93 -78,79,95,94 -80,81,97,96 -81,82,98,97 -82,83,99,98 -83,84,100,99 -84,85,101,100 -85,86,102,101 -86,87,103,102 -87,88,104,103 -88,89,105,104 -89,90,106,105 -90,91,107,106 -91,92,108,107 -92,93,109,108 -93,94,110,109 -94,95,111,110 -96,97,113,112 -97,98,114,113 -98,99,115,114 -99,100,116,115 -100,101,117,116 -101,102,118,117 -102,103,119,118 -103,104,120,119 -104,105,121,120 -105,106,122,121 -106,107,123,122 -107,108,124,123 -108,109,125,124 -109,110,126,125 -110,111,127,126 -112,113,129,128 -113,114,130,129 -114,115,131,130 -115,116,132,131 -116,117,133,132 -117,118,134,133 -118,119,135,134 -119,120,136,135 -120,121,137,136 -121,122,138,137 -122,123,139,138 -123,124,140,139 -124,125,141,140 -125,126,142,141 -126,127,143,142 -128,129,145,144 -129,130,146,145 -130,131,147,146 -131,132,148,147 -132,133,149,148 -133,134,150,149 -134,135,151,150 -135,136,152,151 -136,137,153,152 -137,138,154,153 -138,139,155,154 -139,140,156,155 -140,141,157,156 -141,142,158,157 -142,143,159,158 -144,145,161,160 -145,146,162,161 -146,147,163,162 -147,148,164,163 -148,149,165,164 -149,150,166,165 -150,151,167,166 -151,152,168,167 -152,153,169,168 -153,154,170,169 -154,155,171,170 -155,156,172,171 -156,157,173,172 -157,158,174,173 -158,159,175,174 -160,161,177,176 -161,162,178,177 -162,163,179,178 -163,164,180,179 -164,165,181,180 -165,166,182,181 -166,167,183,182 -167,168,184,183 -168,169,185,184 -169,170,186,185 -170,171,187,186 -171,172,188,187 -172,173,189,188 -173,174,190,189 -174,175,191,190 -176,177,193,192 -177,178,194,193 -178,179,195,194 -179,180,196,195 -180,181,197,196 -181,182,198,197 -182,183,199,198 -183,184,200,199 -184,185,201,200 -185,186,202,201 -186,187,203,202 -187,188,204,203 -188,189,205,204 -189,190,206,205 -190,191,207,206 -192,193,209,208 -193,194,210,209 -194,195,211,210 -195,196,212,211 -196,197,213,212 -197,198,214,213 -198,199,215,214 -199,200,216,215 -200,201,217,216 -201,202,218,217 -202,203,219,218 -203,204,220,219 -204,205,221,220 -205,206,222,221 -206,207,223,222 -208,209,225,224 -209,210,226,225 -210,211,227,226 -211,212,228,227 -212,213,229,228 -213,214,230,229 -214,215,231,230 -215,216,232,231 -216,217,233,232 -217,218,234,233 -218,219,235,234 -219,220,236,235 -220,221,237,236 -221,222,238,237 -222,223,239,238 -224,225,241,240 -225,226,242,241 -226,227,243,242 -227,228,244,243 -228,229,245,244 -229,230,246,245 -230,231,247,246 -231,232,248,247 -232,233,249,248 -233,234,250,249 -234,235,251,250 -235,236,252,251 -236,237,253,252 -237,238,254,253 -238,239,255,254 +0,16,17,1 +1,17,18,2 +2,18,19,3 +3,19,20,4 +4,20,21,5 +5,21,22,6 +6,22,23,7 +7,23,24,8 +8,24,25,9 +9,25,26,10 +10,26,27,11 +11,27,28,12 +12,28,29,13 +13,29,30,14 +14,30,31,15 +16,32,33,17 +17,33,34,18 +18,34,35,19 +19,35,36,20 +20,36,37,21 +21,37,38,22 +22,38,39,23 +23,39,40,24 +24,40,41,25 +25,41,42,26 +26,42,43,27 +27,43,44,28 +28,44,45,29 +29,45,46,30 +30,46,47,31 +32,48,49,33 +33,49,50,34 +34,50,51,35 +35,51,52,36 +36,52,53,37 +37,53,54,38 +38,54,55,39 +39,55,56,40 +40,56,57,41 +41,57,58,42 +42,58,59,43 +43,59,60,44 +44,60,61,45 +45,61,62,46 +46,62,63,47 +48,64,65,49 +49,65,66,50 +50,66,67,51 +51,67,68,52 +52,68,69,53 +53,69,70,54 +54,70,71,55 +55,71,72,56 +56,72,73,57 +57,73,74,58 +58,74,75,59 +59,75,76,60 +60,76,77,61 +61,77,78,62 +62,78,79,63 +64,80,81,65 +65,81,82,66 +66,82,83,67 +67,83,84,68 +68,84,85,69 +69,85,86,70 +70,86,87,71 +71,87,88,72 +72,88,89,73 +73,89,90,74 +74,90,91,75 +75,91,92,76 +76,92,93,77 +77,93,94,78 +78,94,95,79 +80,96,97,81 +81,97,98,82 +82,98,99,83 +83,99,100,84 +84,100,101,85 +85,101,102,86 +86,102,103,87 +87,103,104,88 +88,104,105,89 +89,105,106,90 +90,106,107,91 +91,107,108,92 +92,108,109,93 +93,109,110,94 +94,110,111,95 +96,112,113,97 +97,113,114,98 +98,114,115,99 +99,115,116,100 +100,116,117,101 +101,117,118,102 +102,118,119,103 +103,119,120,104 +104,120,121,105 +105,121,122,106 +106,122,123,107 +107,123,124,108 +108,124,125,109 +109,125,126,110 +110,126,127,111 +112,128,129,113 +113,129,130,114 +114,130,131,115 +115,131,132,116 +116,132,133,117 +117,133,134,118 +118,134,135,119 +119,135,136,120 +120,136,137,121 +121,137,138,122 +122,138,139,123 +123,139,140,124 +124,140,141,125 +125,141,142,126 +126,142,143,127 +128,144,145,129 +129,145,146,130 +130,146,147,131 +131,147,148,132 +132,148,149,133 +133,149,150,134 +134,150,151,135 +135,151,152,136 +136,152,153,137 +137,153,154,138 +138,154,155,139 +139,155,156,140 +140,156,157,141 +141,157,158,142 +142,158,159,143 +144,160,161,145 +145,161,162,146 +146,162,163,147 +147,163,164,148 +148,164,165,149 +149,165,166,150 +150,166,167,151 +151,167,168,152 +152,168,169,153 +153,169,170,154 +154,170,171,155 +155,171,172,156 +156,172,173,157 +157,173,174,158 +158,174,175,159 +160,176,177,161 +161,177,178,162 +162,178,179,163 +163,179,180,164 +164,180,181,165 +165,181,182,166 +166,182,183,167 +167,183,184,168 +168,184,185,169 +169,185,186,170 +170,186,187,171 +171,187,188,172 +172,188,189,173 +173,189,190,174 +174,190,191,175 +176,192,193,177 +177,193,194,178 +178,194,195,179 +179,195,196,180 +180,196,197,181 +181,197,198,182 +182,198,199,183 +183,199,200,184 +184,200,201,185 +185,201,202,186 +186,202,203,187 +187,203,204,188 +188,204,205,189 +189,205,206,190 +190,206,207,191 +192,208,209,193 +193,209,210,194 +194,210,211,195 +195,211,212,196 +196,212,213,197 +197,213,214,198 +198,214,215,199 +199,215,216,200 +200,216,217,201 +201,217,218,202 +202,218,219,203 +203,219,220,204 +204,220,221,205 +205,221,222,206 +206,222,223,207 +208,224,225,209 +209,225,226,210 +210,226,227,211 +211,227,228,212 +212,228,229,213 +213,229,230,214 +214,230,231,215 +215,231,232,216 +216,232,233,217 +217,233,234,218 +218,234,235,219 +219,235,236,220 +220,236,237,221 +221,237,238,222 +222,238,239,223 +224,240,241,225 +225,241,242,226 +226,242,243,227 +227,243,244,228 +228,244,245,229 +229,245,246,230 +230,246,247,231 +231,247,248,232 +232,248,249,233 +233,249,250,234 +234,250,251,235 +235,251,252,236 +236,252,253,237 +237,253,254,238 +238,254,255,239 diff --git a/data/min/quad8_sphere200/connect.csv b/data/min/quad8_sphere200/connect.csv index 6ce07ff1..7718f4c5 100644 --- a/data/min/quad8_sphere200/connect.csv +++ b/data/min/quad8_sphere200/connect.csv @@ -1,225 +1,225 @@ -0,2,64,62,1,33,63,31 -2,4,66,64,3,35,65,33 -4,6,68,66,5,37,67,35 -6,8,70,68,7,39,69,37 -8,10,72,70,9,41,71,39 -10,12,74,72,11,43,73,41 -12,14,76,74,13,45,75,43 -14,16,78,76,15,47,77,45 -16,18,80,78,17,49,79,47 -18,20,82,80,19,51,81,49 -20,22,84,82,21,53,83,51 -22,24,86,84,23,55,85,53 -24,26,88,86,25,57,87,55 -26,28,90,88,27,59,89,57 -28,30,92,90,29,61,91,59 -62,64,126,124,63,95,125,93 -64,66,128,126,65,97,127,95 -66,68,130,128,67,99,129,97 -68,70,132,130,69,101,131,99 -70,72,134,132,71,103,133,101 -72,74,136,134,73,105,135,103 -74,76,138,136,75,107,137,105 -76,78,140,138,77,109,139,107 -78,80,142,140,79,111,141,109 -80,82,144,142,81,113,143,111 -82,84,146,144,83,115,145,113 -84,86,148,146,85,117,147,115 -86,88,150,148,87,119,149,117 -88,90,152,150,89,121,151,119 -90,92,154,152,91,123,153,121 -124,126,188,186,125,157,187,155 -126,128,190,188,127,159,189,157 -128,130,192,190,129,161,191,159 -130,132,194,192,131,163,193,161 -132,134,196,194,133,165,195,163 -134,136,198,196,135,167,197,165 -136,138,200,198,137,169,199,167 -138,140,202,200,139,171,201,169 -140,142,204,202,141,173,203,171 -142,144,206,204,143,175,205,173 -144,146,208,206,145,177,207,175 -146,148,210,208,147,179,209,177 -148,150,212,210,149,181,211,179 -150,152,214,212,151,183,213,181 -152,154,216,214,153,185,215,183 -186,188,250,248,187,219,249,217 -188,190,252,250,189,221,251,219 -190,192,254,252,191,223,253,221 -192,194,256,254,193,225,255,223 -194,196,258,256,195,227,257,225 -196,198,260,258,197,229,259,227 -198,200,262,260,199,231,261,229 -200,202,264,262,201,233,263,231 -202,204,266,264,203,235,265,233 -204,206,268,266,205,237,267,235 -206,208,270,268,207,239,269,237 -208,210,272,270,209,241,271,239 -210,212,274,272,211,243,273,241 -212,214,276,274,213,245,275,243 -214,216,278,276,215,247,277,245 -248,250,312,310,249,281,311,279 -250,252,314,312,251,283,313,281 -252,254,316,314,253,285,315,283 -254,256,318,316,255,287,317,285 -256,258,320,318,257,289,319,287 -258,260,322,320,259,291,321,289 -260,262,324,322,261,293,323,291 -262,264,326,324,263,295,325,293 -264,266,328,326,265,297,327,295 -266,268,330,328,267,299,329,297 -268,270,332,330,269,301,331,299 -270,272,334,332,271,303,333,301 -272,274,336,334,273,305,335,303 -274,276,338,336,275,307,337,305 -276,278,340,338,277,309,339,307 -310,312,374,372,311,343,373,341 -312,314,376,374,313,345,375,343 -314,316,378,376,315,347,377,345 -316,318,380,378,317,349,379,347 -318,320,382,380,319,351,381,349 -320,322,384,382,321,353,383,351 -322,324,386,384,323,355,385,353 -324,326,388,386,325,357,387,355 -326,328,390,388,327,359,389,357 -328,330,392,390,329,361,391,359 -330,332,394,392,331,363,393,361 -332,334,396,394,333,365,395,363 -334,336,398,396,335,367,397,365 -336,338,400,398,337,369,399,367 -338,340,402,400,339,371,401,369 -372,374,436,434,373,405,435,403 -374,376,438,436,375,407,437,405 -376,378,440,438,377,409,439,407 -378,380,442,440,379,411,441,409 -380,382,444,442,381,413,443,411 -382,384,446,444,383,415,445,413 -384,386,448,446,385,417,447,415 -386,388,450,448,387,419,449,417 -388,390,452,450,389,421,451,419 -390,392,454,452,391,423,453,421 -392,394,456,454,393,425,455,423 -394,396,458,456,395,427,457,425 -396,398,460,458,397,429,459,427 -398,400,462,460,399,431,461,429 -400,402,464,462,401,433,463,431 -434,436,498,496,435,467,497,465 -436,438,500,498,437,469,499,467 -438,440,502,500,439,471,501,469 -440,442,504,502,441,473,503,471 -442,444,506,504,443,475,505,473 -444,446,508,506,445,477,507,475 -446,448,510,508,447,479,509,477 -448,450,512,510,449,481,511,479 -450,452,514,512,451,483,513,481 -452,454,516,514,453,485,515,483 -454,456,518,516,455,487,517,485 -456,458,520,518,457,489,519,487 -458,460,522,520,459,491,521,489 -460,462,524,522,461,493,523,491 -462,464,526,524,463,495,525,493 -496,498,560,558,497,529,559,527 -498,500,562,560,499,531,561,529 -500,502,564,562,501,533,563,531 -502,504,566,564,503,535,565,533 -504,506,568,566,505,537,567,535 -506,508,570,568,507,539,569,537 -508,510,572,570,509,541,571,539 -510,512,574,572,511,543,573,541 -512,514,576,574,513,545,575,543 -514,516,578,576,515,547,577,545 -516,518,580,578,517,549,579,547 -518,520,582,580,519,551,581,549 -520,522,584,582,521,553,583,551 -522,524,586,584,523,555,585,553 -524,526,588,586,525,557,587,555 -558,560,622,620,559,591,621,589 -560,562,624,622,561,593,623,591 -562,564,626,624,563,595,625,593 -564,566,628,626,565,597,627,595 -566,568,630,628,567,599,629,597 -568,570,632,630,569,601,631,599 -570,572,634,632,571,603,633,601 -572,574,636,634,573,605,635,603 -574,576,638,636,575,607,637,605 -576,578,640,638,577,609,639,607 -578,580,642,640,579,611,641,609 -580,582,644,642,581,613,643,611 -582,584,646,644,583,615,645,613 -584,586,648,646,585,617,647,615 -586,588,650,648,587,619,649,617 -620,622,684,682,621,653,683,651 -622,624,686,684,623,655,685,653 -624,626,688,686,625,657,687,655 -626,628,690,688,627,659,689,657 -628,630,692,690,629,661,691,659 -630,632,694,692,631,663,693,661 -632,634,696,694,633,665,695,663 -634,636,698,696,635,667,697,665 -636,638,700,698,637,669,699,667 -638,640,702,700,639,671,701,669 -640,642,704,702,641,673,703,671 -642,644,706,704,643,675,705,673 -644,646,708,706,645,677,707,675 -646,648,710,708,647,679,709,677 -648,650,712,710,649,681,711,679 -682,684,746,744,683,715,745,713 -684,686,748,746,685,717,747,715 -686,688,750,748,687,719,749,717 -688,690,752,750,689,721,751,719 -690,692,754,752,691,723,753,721 -692,694,756,754,693,725,755,723 -694,696,758,756,695,727,757,725 -696,698,760,758,697,729,759,727 -698,700,762,760,699,731,761,729 -700,702,764,762,701,733,763,731 -702,704,766,764,703,735,765,733 -704,706,768,766,705,737,767,735 -706,708,770,768,707,739,769,737 -708,710,772,770,709,741,771,739 -710,712,774,772,711,743,773,741 -744,746,808,806,745,777,807,775 -746,748,810,808,747,779,809,777 -748,750,812,810,749,781,811,779 -750,752,814,812,751,783,813,781 -752,754,816,814,753,785,815,783 -754,756,818,816,755,787,817,785 -756,758,820,818,757,789,819,787 -758,760,822,820,759,791,821,789 -760,762,824,822,761,793,823,791 -762,764,826,824,763,795,825,793 -764,766,828,826,765,797,827,795 -766,768,830,828,767,799,829,797 -768,770,832,830,769,801,831,799 -770,772,834,832,771,803,833,801 -772,774,836,834,773,805,835,803 -806,808,870,868,807,839,869,837 -808,810,872,870,809,841,871,839 -810,812,874,872,811,843,873,841 -812,814,876,874,813,845,875,843 -814,816,878,876,815,847,877,845 -816,818,880,878,817,849,879,847 -818,820,882,880,819,851,881,849 -820,822,884,882,821,853,883,851 -822,824,886,884,823,855,885,853 -824,826,888,886,825,857,887,855 -826,828,890,888,827,859,889,857 -828,830,892,890,829,861,891,859 -830,832,894,892,831,863,893,861 -832,834,896,894,833,865,895,863 -834,836,898,896,835,867,897,865 -868,870,932,930,869,901,931,899 -870,872,934,932,871,903,933,901 -872,874,936,934,873,905,935,903 -874,876,938,936,875,907,937,905 -876,878,940,938,877,909,939,907 -878,880,942,940,879,911,941,909 -880,882,944,942,881,913,943,911 -882,884,946,944,883,915,945,913 -884,886,948,946,885,917,947,915 -886,888,950,948,887,919,949,917 -888,890,952,950,889,921,951,919 -890,892,954,952,891,923,953,921 -892,894,956,954,893,925,955,923 -894,896,958,956,895,927,957,925 -896,898,960,958,897,929,959,927 +0,62,64,2,31,63,33,1 +2,64,66,4,33,65,35,3 +4,66,68,6,35,67,37,5 +6,68,70,8,37,69,39,7 +8,70,72,10,39,71,41,9 +10,72,74,12,41,73,43,11 +12,74,76,14,43,75,45,13 +14,76,78,16,45,77,47,15 +16,78,80,18,47,79,49,17 +18,80,82,20,49,81,51,19 +20,82,84,22,51,83,53,21 +22,84,86,24,53,85,55,23 +24,86,88,26,55,87,57,25 +26,88,90,28,57,89,59,27 +28,90,92,30,59,91,61,29 +62,124,126,64,93,125,95,63 +64,126,128,66,95,127,97,65 +66,128,130,68,97,129,99,67 +68,130,132,70,99,131,101,69 +70,132,134,72,101,133,103,71 +72,134,136,74,103,135,105,73 +74,136,138,76,105,137,107,75 +76,138,140,78,107,139,109,77 +78,140,142,80,109,141,111,79 +80,142,144,82,111,143,113,81 +82,144,146,84,113,145,115,83 +84,146,148,86,115,147,117,85 +86,148,150,88,117,149,119,87 +88,150,152,90,119,151,121,89 +90,152,154,92,121,153,123,91 +124,186,188,126,155,187,157,125 +126,188,190,128,157,189,159,127 +128,190,192,130,159,191,161,129 +130,192,194,132,161,193,163,131 +132,194,196,134,163,195,165,133 +134,196,198,136,165,197,167,135 +136,198,200,138,167,199,169,137 +138,200,202,140,169,201,171,139 +140,202,204,142,171,203,173,141 +142,204,206,144,173,205,175,143 +144,206,208,146,175,207,177,145 +146,208,210,148,177,209,179,147 +148,210,212,150,179,211,181,149 +150,212,214,152,181,213,183,151 +152,214,216,154,183,215,185,153 +186,248,250,188,217,249,219,187 +188,250,252,190,219,251,221,189 +190,252,254,192,221,253,223,191 +192,254,256,194,223,255,225,193 +194,256,258,196,225,257,227,195 +196,258,260,198,227,259,229,197 +198,260,262,200,229,261,231,199 +200,262,264,202,231,263,233,201 +202,264,266,204,233,265,235,203 +204,266,268,206,235,267,237,205 +206,268,270,208,237,269,239,207 +208,270,272,210,239,271,241,209 +210,272,274,212,241,273,243,211 +212,274,276,214,243,275,245,213 +214,276,278,216,245,277,247,215 +248,310,312,250,279,311,281,249 +250,312,314,252,281,313,283,251 +252,314,316,254,283,315,285,253 +254,316,318,256,285,317,287,255 +256,318,320,258,287,319,289,257 +258,320,322,260,289,321,291,259 +260,322,324,262,291,323,293,261 +262,324,326,264,293,325,295,263 +264,326,328,266,295,327,297,265 +266,328,330,268,297,329,299,267 +268,330,332,270,299,331,301,269 +270,332,334,272,301,333,303,271 +272,334,336,274,303,335,305,273 +274,336,338,276,305,337,307,275 +276,338,340,278,307,339,309,277 +310,372,374,312,341,373,343,311 +312,374,376,314,343,375,345,313 +314,376,378,316,345,377,347,315 +316,378,380,318,347,379,349,317 +318,380,382,320,349,381,351,319 +320,382,384,322,351,383,353,321 +322,384,386,324,353,385,355,323 +324,386,388,326,355,387,357,325 +326,388,390,328,357,389,359,327 +328,390,392,330,359,391,361,329 +330,392,394,332,361,393,363,331 +332,394,396,334,363,395,365,333 +334,396,398,336,365,397,367,335 +336,398,400,338,367,399,369,337 +338,400,402,340,369,401,371,339 +372,434,436,374,403,435,405,373 +374,436,438,376,405,437,407,375 +376,438,440,378,407,439,409,377 +378,440,442,380,409,441,411,379 +380,442,444,382,411,443,413,381 +382,444,446,384,413,445,415,383 +384,446,448,386,415,447,417,385 +386,448,450,388,417,449,419,387 +388,450,452,390,419,451,421,389 +390,452,454,392,421,453,423,391 +392,454,456,394,423,455,425,393 +394,456,458,396,425,457,427,395 +396,458,460,398,427,459,429,397 +398,460,462,400,429,461,431,399 +400,462,464,402,431,463,433,401 +434,496,498,436,465,497,467,435 +436,498,500,438,467,499,469,437 +438,500,502,440,469,501,471,439 +440,502,504,442,471,503,473,441 +442,504,506,444,473,505,475,443 +444,506,508,446,475,507,477,445 +446,508,510,448,477,509,479,447 +448,510,512,450,479,511,481,449 +450,512,514,452,481,513,483,451 +452,514,516,454,483,515,485,453 +454,516,518,456,485,517,487,455 +456,518,520,458,487,519,489,457 +458,520,522,460,489,521,491,459 +460,522,524,462,491,523,493,461 +462,524,526,464,493,525,495,463 +496,558,560,498,527,559,529,497 +498,560,562,500,529,561,531,499 +500,562,564,502,531,563,533,501 +502,564,566,504,533,565,535,503 +504,566,568,506,535,567,537,505 +506,568,570,508,537,569,539,507 +508,570,572,510,539,571,541,509 +510,572,574,512,541,573,543,511 +512,574,576,514,543,575,545,513 +514,576,578,516,545,577,547,515 +516,578,580,518,547,579,549,517 +518,580,582,520,549,581,551,519 +520,582,584,522,551,583,553,521 +522,584,586,524,553,585,555,523 +524,586,588,526,555,587,557,525 +558,620,622,560,589,621,591,559 +560,622,624,562,591,623,593,561 +562,624,626,564,593,625,595,563 +564,626,628,566,595,627,597,565 +566,628,630,568,597,629,599,567 +568,630,632,570,599,631,601,569 +570,632,634,572,601,633,603,571 +572,634,636,574,603,635,605,573 +574,636,638,576,605,637,607,575 +576,638,640,578,607,639,609,577 +578,640,642,580,609,641,611,579 +580,642,644,582,611,643,613,581 +582,644,646,584,613,645,615,583 +584,646,648,586,615,647,617,585 +586,648,650,588,617,649,619,587 +620,682,684,622,651,683,653,621 +622,684,686,624,653,685,655,623 +624,686,688,626,655,687,657,625 +626,688,690,628,657,689,659,627 +628,690,692,630,659,691,661,629 +630,692,694,632,661,693,663,631 +632,694,696,634,663,695,665,633 +634,696,698,636,665,697,667,635 +636,698,700,638,667,699,669,637 +638,700,702,640,669,701,671,639 +640,702,704,642,671,703,673,641 +642,704,706,644,673,705,675,643 +644,706,708,646,675,707,677,645 +646,708,710,648,677,709,679,647 +648,710,712,650,679,711,681,649 +682,744,746,684,713,745,715,683 +684,746,748,686,715,747,717,685 +686,748,750,688,717,749,719,687 +688,750,752,690,719,751,721,689 +690,752,754,692,721,753,723,691 +692,754,756,694,723,755,725,693 +694,756,758,696,725,757,727,695 +696,758,760,698,727,759,729,697 +698,760,762,700,729,761,731,699 +700,762,764,702,731,763,733,701 +702,764,766,704,733,765,735,703 +704,766,768,706,735,767,737,705 +706,768,770,708,737,769,739,707 +708,770,772,710,739,771,741,709 +710,772,774,712,741,773,743,711 +744,806,808,746,775,807,777,745 +746,808,810,748,777,809,779,747 +748,810,812,750,779,811,781,749 +750,812,814,752,781,813,783,751 +752,814,816,754,783,815,785,753 +754,816,818,756,785,817,787,755 +756,818,820,758,787,819,789,757 +758,820,822,760,789,821,791,759 +760,822,824,762,791,823,793,761 +762,824,826,764,793,825,795,763 +764,826,828,766,795,827,797,765 +766,828,830,768,797,829,799,767 +768,830,832,770,799,831,801,769 +770,832,834,772,801,833,803,771 +772,834,836,774,803,835,805,773 +806,868,870,808,837,869,839,807 +808,870,872,810,839,871,841,809 +810,872,874,812,841,873,843,811 +812,874,876,814,843,875,845,813 +814,876,878,816,845,877,847,815 +816,878,880,818,847,879,849,817 +818,880,882,820,849,881,851,819 +820,882,884,822,851,883,853,821 +822,884,886,824,853,885,855,823 +824,886,888,826,855,887,857,825 +826,888,890,828,857,889,859,827 +828,890,892,830,859,891,861,829 +830,892,894,832,861,893,863,831 +832,894,896,834,863,895,865,833 +834,896,898,836,865,897,867,835 +868,930,932,870,899,931,901,869 +870,932,934,872,901,933,903,871 +872,934,936,874,903,935,905,873 +874,936,938,876,905,937,907,875 +876,938,940,878,907,939,909,877 +878,940,942,880,909,941,911,879 +880,942,944,882,911,943,913,881 +882,944,946,884,913,945,915,883 +884,946,948,886,915,947,917,885 +886,948,950,888,917,949,919,887 +888,950,952,890,919,951,921,889 +890,952,954,892,921,953,923,891 +892,954,956,894,923,955,925,893 +894,956,958,896,925,957,927,895 +896,958,960,898,927,959,929,897 diff --git a/data/min/quad9_sphere200/connect.csv b/data/min/quad9_sphere200/connect.csv index 8770e9af..86563b91 100644 --- a/data/min/quad9_sphere200/connect.csv +++ b/data/min/quad9_sphere200/connect.csv @@ -1,225 +1,225 @@ -0,2,64,62,1,33,63,31,32 -2,4,66,64,3,35,65,33,34 -4,6,68,66,5,37,67,35,36 -6,8,70,68,7,39,69,37,38 -8,10,72,70,9,41,71,39,40 -10,12,74,72,11,43,73,41,42 -12,14,76,74,13,45,75,43,44 -14,16,78,76,15,47,77,45,46 -16,18,80,78,17,49,79,47,48 -18,20,82,80,19,51,81,49,50 -20,22,84,82,21,53,83,51,52 -22,24,86,84,23,55,85,53,54 -24,26,88,86,25,57,87,55,56 -26,28,90,88,27,59,89,57,58 -28,30,92,90,29,61,91,59,60 -62,64,126,124,63,95,125,93,94 -64,66,128,126,65,97,127,95,96 -66,68,130,128,67,99,129,97,98 -68,70,132,130,69,101,131,99,100 -70,72,134,132,71,103,133,101,102 -72,74,136,134,73,105,135,103,104 -74,76,138,136,75,107,137,105,106 -76,78,140,138,77,109,139,107,108 -78,80,142,140,79,111,141,109,110 -80,82,144,142,81,113,143,111,112 -82,84,146,144,83,115,145,113,114 -84,86,148,146,85,117,147,115,116 -86,88,150,148,87,119,149,117,118 -88,90,152,150,89,121,151,119,120 -90,92,154,152,91,123,153,121,122 -124,126,188,186,125,157,187,155,156 -126,128,190,188,127,159,189,157,158 -128,130,192,190,129,161,191,159,160 -130,132,194,192,131,163,193,161,162 -132,134,196,194,133,165,195,163,164 -134,136,198,196,135,167,197,165,166 -136,138,200,198,137,169,199,167,168 -138,140,202,200,139,171,201,169,170 -140,142,204,202,141,173,203,171,172 -142,144,206,204,143,175,205,173,174 -144,146,208,206,145,177,207,175,176 -146,148,210,208,147,179,209,177,178 -148,150,212,210,149,181,211,179,180 -150,152,214,212,151,183,213,181,182 -152,154,216,214,153,185,215,183,184 -186,188,250,248,187,219,249,217,218 -188,190,252,250,189,221,251,219,220 -190,192,254,252,191,223,253,221,222 -192,194,256,254,193,225,255,223,224 -194,196,258,256,195,227,257,225,226 -196,198,260,258,197,229,259,227,228 -198,200,262,260,199,231,261,229,230 -200,202,264,262,201,233,263,231,232 -202,204,266,264,203,235,265,233,234 -204,206,268,266,205,237,267,235,236 -206,208,270,268,207,239,269,237,238 -208,210,272,270,209,241,271,239,240 -210,212,274,272,211,243,273,241,242 -212,214,276,274,213,245,275,243,244 -214,216,278,276,215,247,277,245,246 -248,250,312,310,249,281,311,279,280 -250,252,314,312,251,283,313,281,282 -252,254,316,314,253,285,315,283,284 -254,256,318,316,255,287,317,285,286 -256,258,320,318,257,289,319,287,288 -258,260,322,320,259,291,321,289,290 -260,262,324,322,261,293,323,291,292 -262,264,326,324,263,295,325,293,294 -264,266,328,326,265,297,327,295,296 -266,268,330,328,267,299,329,297,298 -268,270,332,330,269,301,331,299,300 -270,272,334,332,271,303,333,301,302 -272,274,336,334,273,305,335,303,304 -274,276,338,336,275,307,337,305,306 -276,278,340,338,277,309,339,307,308 -310,312,374,372,311,343,373,341,342 -312,314,376,374,313,345,375,343,344 -314,316,378,376,315,347,377,345,346 -316,318,380,378,317,349,379,347,348 -318,320,382,380,319,351,381,349,350 -320,322,384,382,321,353,383,351,352 -322,324,386,384,323,355,385,353,354 -324,326,388,386,325,357,387,355,356 -326,328,390,388,327,359,389,357,358 -328,330,392,390,329,361,391,359,360 -330,332,394,392,331,363,393,361,362 -332,334,396,394,333,365,395,363,364 -334,336,398,396,335,367,397,365,366 -336,338,400,398,337,369,399,367,368 -338,340,402,400,339,371,401,369,370 -372,374,436,434,373,405,435,403,404 -374,376,438,436,375,407,437,405,406 -376,378,440,438,377,409,439,407,408 -378,380,442,440,379,411,441,409,410 -380,382,444,442,381,413,443,411,412 -382,384,446,444,383,415,445,413,414 -384,386,448,446,385,417,447,415,416 -386,388,450,448,387,419,449,417,418 -388,390,452,450,389,421,451,419,420 -390,392,454,452,391,423,453,421,422 -392,394,456,454,393,425,455,423,424 -394,396,458,456,395,427,457,425,426 -396,398,460,458,397,429,459,427,428 -398,400,462,460,399,431,461,429,430 -400,402,464,462,401,433,463,431,432 -434,436,498,496,435,467,497,465,466 -436,438,500,498,437,469,499,467,468 -438,440,502,500,439,471,501,469,470 -440,442,504,502,441,473,503,471,472 -442,444,506,504,443,475,505,473,474 -444,446,508,506,445,477,507,475,476 -446,448,510,508,447,479,509,477,478 -448,450,512,510,449,481,511,479,480 -450,452,514,512,451,483,513,481,482 -452,454,516,514,453,485,515,483,484 -454,456,518,516,455,487,517,485,486 -456,458,520,518,457,489,519,487,488 -458,460,522,520,459,491,521,489,490 -460,462,524,522,461,493,523,491,492 -462,464,526,524,463,495,525,493,494 -496,498,560,558,497,529,559,527,528 -498,500,562,560,499,531,561,529,530 -500,502,564,562,501,533,563,531,532 -502,504,566,564,503,535,565,533,534 -504,506,568,566,505,537,567,535,536 -506,508,570,568,507,539,569,537,538 -508,510,572,570,509,541,571,539,540 -510,512,574,572,511,543,573,541,542 -512,514,576,574,513,545,575,543,544 -514,516,578,576,515,547,577,545,546 -516,518,580,578,517,549,579,547,548 -518,520,582,580,519,551,581,549,550 -520,522,584,582,521,553,583,551,552 -522,524,586,584,523,555,585,553,554 -524,526,588,586,525,557,587,555,556 -558,560,622,620,559,591,621,589,590 -560,562,624,622,561,593,623,591,592 -562,564,626,624,563,595,625,593,594 -564,566,628,626,565,597,627,595,596 -566,568,630,628,567,599,629,597,598 -568,570,632,630,569,601,631,599,600 -570,572,634,632,571,603,633,601,602 -572,574,636,634,573,605,635,603,604 -574,576,638,636,575,607,637,605,606 -576,578,640,638,577,609,639,607,608 -578,580,642,640,579,611,641,609,610 -580,582,644,642,581,613,643,611,612 -582,584,646,644,583,615,645,613,614 -584,586,648,646,585,617,647,615,616 -586,588,650,648,587,619,649,617,618 -620,622,684,682,621,653,683,651,652 -622,624,686,684,623,655,685,653,654 -624,626,688,686,625,657,687,655,656 -626,628,690,688,627,659,689,657,658 -628,630,692,690,629,661,691,659,660 -630,632,694,692,631,663,693,661,662 -632,634,696,694,633,665,695,663,664 -634,636,698,696,635,667,697,665,666 -636,638,700,698,637,669,699,667,668 -638,640,702,700,639,671,701,669,670 -640,642,704,702,641,673,703,671,672 -642,644,706,704,643,675,705,673,674 -644,646,708,706,645,677,707,675,676 -646,648,710,708,647,679,709,677,678 -648,650,712,710,649,681,711,679,680 -682,684,746,744,683,715,745,713,714 -684,686,748,746,685,717,747,715,716 -686,688,750,748,687,719,749,717,718 -688,690,752,750,689,721,751,719,720 -690,692,754,752,691,723,753,721,722 -692,694,756,754,693,725,755,723,724 -694,696,758,756,695,727,757,725,726 -696,698,760,758,697,729,759,727,728 -698,700,762,760,699,731,761,729,730 -700,702,764,762,701,733,763,731,732 -702,704,766,764,703,735,765,733,734 -704,706,768,766,705,737,767,735,736 -706,708,770,768,707,739,769,737,738 -708,710,772,770,709,741,771,739,740 -710,712,774,772,711,743,773,741,742 -744,746,808,806,745,777,807,775,776 -746,748,810,808,747,779,809,777,778 -748,750,812,810,749,781,811,779,780 -750,752,814,812,751,783,813,781,782 -752,754,816,814,753,785,815,783,784 -754,756,818,816,755,787,817,785,786 -756,758,820,818,757,789,819,787,788 -758,760,822,820,759,791,821,789,790 -760,762,824,822,761,793,823,791,792 -762,764,826,824,763,795,825,793,794 -764,766,828,826,765,797,827,795,796 -766,768,830,828,767,799,829,797,798 -768,770,832,830,769,801,831,799,800 -770,772,834,832,771,803,833,801,802 -772,774,836,834,773,805,835,803,804 -806,808,870,868,807,839,869,837,838 -808,810,872,870,809,841,871,839,840 -810,812,874,872,811,843,873,841,842 -812,814,876,874,813,845,875,843,844 -814,816,878,876,815,847,877,845,846 -816,818,880,878,817,849,879,847,848 -818,820,882,880,819,851,881,849,850 -820,822,884,882,821,853,883,851,852 -822,824,886,884,823,855,885,853,854 -824,826,888,886,825,857,887,855,856 -826,828,890,888,827,859,889,857,858 -828,830,892,890,829,861,891,859,860 -830,832,894,892,831,863,893,861,862 -832,834,896,894,833,865,895,863,864 -834,836,898,896,835,867,897,865,866 -868,870,932,930,869,901,931,899,900 -870,872,934,932,871,903,933,901,902 -872,874,936,934,873,905,935,903,904 -874,876,938,936,875,907,937,905,906 -876,878,940,938,877,909,939,907,908 -878,880,942,940,879,911,941,909,910 -880,882,944,942,881,913,943,911,912 -882,884,946,944,883,915,945,913,914 -884,886,948,946,885,917,947,915,916 -886,888,950,948,887,919,949,917,918 -888,890,952,950,889,921,951,919,920 -890,892,954,952,891,923,953,921,922 -892,894,956,954,893,925,955,923,924 -894,896,958,956,895,927,957,925,926 -896,898,960,958,897,929,959,927,928 +0,62,64,2,31,63,33,1,32 +2,64,66,4,33,65,35,3,34 +4,66,68,6,35,67,37,5,36 +6,68,70,8,37,69,39,7,38 +8,70,72,10,39,71,41,9,40 +10,72,74,12,41,73,43,11,42 +12,74,76,14,43,75,45,13,44 +14,76,78,16,45,77,47,15,46 +16,78,80,18,47,79,49,17,48 +18,80,82,20,49,81,51,19,50 +20,82,84,22,51,83,53,21,52 +22,84,86,24,53,85,55,23,54 +24,86,88,26,55,87,57,25,56 +26,88,90,28,57,89,59,27,58 +28,90,92,30,59,91,61,29,60 +62,124,126,64,93,125,95,63,94 +64,126,128,66,95,127,97,65,96 +66,128,130,68,97,129,99,67,98 +68,130,132,70,99,131,101,69,100 +70,132,134,72,101,133,103,71,102 +72,134,136,74,103,135,105,73,104 +74,136,138,76,105,137,107,75,106 +76,138,140,78,107,139,109,77,108 +78,140,142,80,109,141,111,79,110 +80,142,144,82,111,143,113,81,112 +82,144,146,84,113,145,115,83,114 +84,146,148,86,115,147,117,85,116 +86,148,150,88,117,149,119,87,118 +88,150,152,90,119,151,121,89,120 +90,152,154,92,121,153,123,91,122 +124,186,188,126,155,187,157,125,156 +126,188,190,128,157,189,159,127,158 +128,190,192,130,159,191,161,129,160 +130,192,194,132,161,193,163,131,162 +132,194,196,134,163,195,165,133,164 +134,196,198,136,165,197,167,135,166 +136,198,200,138,167,199,169,137,168 +138,200,202,140,169,201,171,139,170 +140,202,204,142,171,203,173,141,172 +142,204,206,144,173,205,175,143,174 +144,206,208,146,175,207,177,145,176 +146,208,210,148,177,209,179,147,178 +148,210,212,150,179,211,181,149,180 +150,212,214,152,181,213,183,151,182 +152,214,216,154,183,215,185,153,184 +186,248,250,188,217,249,219,187,218 +188,250,252,190,219,251,221,189,220 +190,252,254,192,221,253,223,191,222 +192,254,256,194,223,255,225,193,224 +194,256,258,196,225,257,227,195,226 +196,258,260,198,227,259,229,197,228 +198,260,262,200,229,261,231,199,230 +200,262,264,202,231,263,233,201,232 +202,264,266,204,233,265,235,203,234 +204,266,268,206,235,267,237,205,236 +206,268,270,208,237,269,239,207,238 +208,270,272,210,239,271,241,209,240 +210,272,274,212,241,273,243,211,242 +212,274,276,214,243,275,245,213,244 +214,276,278,216,245,277,247,215,246 +248,310,312,250,279,311,281,249,280 +250,312,314,252,281,313,283,251,282 +252,314,316,254,283,315,285,253,284 +254,316,318,256,285,317,287,255,286 +256,318,320,258,287,319,289,257,288 +258,320,322,260,289,321,291,259,290 +260,322,324,262,291,323,293,261,292 +262,324,326,264,293,325,295,263,294 +264,326,328,266,295,327,297,265,296 +266,328,330,268,297,329,299,267,298 +268,330,332,270,299,331,301,269,300 +270,332,334,272,301,333,303,271,302 +272,334,336,274,303,335,305,273,304 +274,336,338,276,305,337,307,275,306 +276,338,340,278,307,339,309,277,308 +310,372,374,312,341,373,343,311,342 +312,374,376,314,343,375,345,313,344 +314,376,378,316,345,377,347,315,346 +316,378,380,318,347,379,349,317,348 +318,380,382,320,349,381,351,319,350 +320,382,384,322,351,383,353,321,352 +322,384,386,324,353,385,355,323,354 +324,386,388,326,355,387,357,325,356 +326,388,390,328,357,389,359,327,358 +328,390,392,330,359,391,361,329,360 +330,392,394,332,361,393,363,331,362 +332,394,396,334,363,395,365,333,364 +334,396,398,336,365,397,367,335,366 +336,398,400,338,367,399,369,337,368 +338,400,402,340,369,401,371,339,370 +372,434,436,374,403,435,405,373,404 +374,436,438,376,405,437,407,375,406 +376,438,440,378,407,439,409,377,408 +378,440,442,380,409,441,411,379,410 +380,442,444,382,411,443,413,381,412 +382,444,446,384,413,445,415,383,414 +384,446,448,386,415,447,417,385,416 +386,448,450,388,417,449,419,387,418 +388,450,452,390,419,451,421,389,420 +390,452,454,392,421,453,423,391,422 +392,454,456,394,423,455,425,393,424 +394,456,458,396,425,457,427,395,426 +396,458,460,398,427,459,429,397,428 +398,460,462,400,429,461,431,399,430 +400,462,464,402,431,463,433,401,432 +434,496,498,436,465,497,467,435,466 +436,498,500,438,467,499,469,437,468 +438,500,502,440,469,501,471,439,470 +440,502,504,442,471,503,473,441,472 +442,504,506,444,473,505,475,443,474 +444,506,508,446,475,507,477,445,476 +446,508,510,448,477,509,479,447,478 +448,510,512,450,479,511,481,449,480 +450,512,514,452,481,513,483,451,482 +452,514,516,454,483,515,485,453,484 +454,516,518,456,485,517,487,455,486 +456,518,520,458,487,519,489,457,488 +458,520,522,460,489,521,491,459,490 +460,522,524,462,491,523,493,461,492 +462,524,526,464,493,525,495,463,494 +496,558,560,498,527,559,529,497,528 +498,560,562,500,529,561,531,499,530 +500,562,564,502,531,563,533,501,532 +502,564,566,504,533,565,535,503,534 +504,566,568,506,535,567,537,505,536 +506,568,570,508,537,569,539,507,538 +508,570,572,510,539,571,541,509,540 +510,572,574,512,541,573,543,511,542 +512,574,576,514,543,575,545,513,544 +514,576,578,516,545,577,547,515,546 +516,578,580,518,547,579,549,517,548 +518,580,582,520,549,581,551,519,550 +520,582,584,522,551,583,553,521,552 +522,584,586,524,553,585,555,523,554 +524,586,588,526,555,587,557,525,556 +558,620,622,560,589,621,591,559,590 +560,622,624,562,591,623,593,561,592 +562,624,626,564,593,625,595,563,594 +564,626,628,566,595,627,597,565,596 +566,628,630,568,597,629,599,567,598 +568,630,632,570,599,631,601,569,600 +570,632,634,572,601,633,603,571,602 +572,634,636,574,603,635,605,573,604 +574,636,638,576,605,637,607,575,606 +576,638,640,578,607,639,609,577,608 +578,640,642,580,609,641,611,579,610 +580,642,644,582,611,643,613,581,612 +582,644,646,584,613,645,615,583,614 +584,646,648,586,615,647,617,585,616 +586,648,650,588,617,649,619,587,618 +620,682,684,622,651,683,653,621,652 +622,684,686,624,653,685,655,623,654 +624,686,688,626,655,687,657,625,656 +626,688,690,628,657,689,659,627,658 +628,690,692,630,659,691,661,629,660 +630,692,694,632,661,693,663,631,662 +632,694,696,634,663,695,665,633,664 +634,696,698,636,665,697,667,635,666 +636,698,700,638,667,699,669,637,668 +638,700,702,640,669,701,671,639,670 +640,702,704,642,671,703,673,641,672 +642,704,706,644,673,705,675,643,674 +644,706,708,646,675,707,677,645,676 +646,708,710,648,677,709,679,647,678 +648,710,712,650,679,711,681,649,680 +682,744,746,684,713,745,715,683,714 +684,746,748,686,715,747,717,685,716 +686,748,750,688,717,749,719,687,718 +688,750,752,690,719,751,721,689,720 +690,752,754,692,721,753,723,691,722 +692,754,756,694,723,755,725,693,724 +694,756,758,696,725,757,727,695,726 +696,758,760,698,727,759,729,697,728 +698,760,762,700,729,761,731,699,730 +700,762,764,702,731,763,733,701,732 +702,764,766,704,733,765,735,703,734 +704,766,768,706,735,767,737,705,736 +706,768,770,708,737,769,739,707,738 +708,770,772,710,739,771,741,709,740 +710,772,774,712,741,773,743,711,742 +744,806,808,746,775,807,777,745,776 +746,808,810,748,777,809,779,747,778 +748,810,812,750,779,811,781,749,780 +750,812,814,752,781,813,783,751,782 +752,814,816,754,783,815,785,753,784 +754,816,818,756,785,817,787,755,786 +756,818,820,758,787,819,789,757,788 +758,820,822,760,789,821,791,759,790 +760,822,824,762,791,823,793,761,792 +762,824,826,764,793,825,795,763,794 +764,826,828,766,795,827,797,765,796 +766,828,830,768,797,829,799,767,798 +768,830,832,770,799,831,801,769,800 +770,832,834,772,801,833,803,771,802 +772,834,836,774,803,835,805,773,804 +806,868,870,808,837,869,839,807,838 +808,870,872,810,839,871,841,809,840 +810,872,874,812,841,873,843,811,842 +812,874,876,814,843,875,845,813,844 +814,876,878,816,845,877,847,815,846 +816,878,880,818,847,879,849,817,848 +818,880,882,820,849,881,851,819,850 +820,882,884,822,851,883,853,821,852 +822,884,886,824,853,885,855,823,854 +824,886,888,826,855,887,857,825,856 +826,888,890,828,857,889,859,827,858 +828,890,892,830,859,891,861,829,860 +830,892,894,832,861,893,863,831,862 +832,894,896,834,863,895,865,833,864 +834,896,898,836,865,897,867,835,866 +868,930,932,870,899,931,901,869,900 +870,932,934,872,901,933,903,871,902 +872,934,936,874,903,935,905,873,904 +874,936,938,876,905,937,907,875,906 +876,938,940,878,907,939,909,877,908 +878,940,942,880,909,941,911,879,910 +880,942,944,882,911,943,913,881,912 +882,944,946,884,913,945,915,883,914 +884,946,948,886,915,947,917,885,916 +886,948,950,888,917,949,919,887,918 +888,950,952,890,919,951,921,889,920 +890,952,954,892,921,953,923,891,922 +892,954,956,894,923,955,925,893,924 +894,956,958,896,925,957,927,895,926 +896,958,960,898,927,959,929,897,928 diff --git a/data/rabbits/feebs_quad4/connectivity.csv b/data/rabbits/feebs_quad4/connectivity.csv index 1ea722bc..8cbba4f2 100644 --- a/data/rabbits/feebs_quad4/connectivity.csv +++ b/data/rabbits/feebs_quad4/connectivity.csv @@ -1,208 +1,208 @@ -197,232,216,223 -141,181,214,205 -145,196,233,195 -229,217,137,194 -232,219,210,209 -163,108,107,98 -115,36,37,38 -195,233,236,212 -156,126,172,174 -234,181,105,148 -46,124,171,45 -194,222,237,229 -172,67,68,69 -217,221,208,137 -231,244,191,220 -73,112,71,72 -187,210,219,193 -45,171,190,178 -224,180,144,149 -231,220,192,190 -71,112,174,70 -51,52,129,93 -233,246,238,236 -238,242,249,236 -191,89,151,220 -94,3,4,102 -100,111,108,163 -117,150,97,99 -235,154,140,186 -247,189,162,133 -114,153,222,194 -164,158,111,100 -162,0,1,133 -196,152,130,144 -155,109,6,7 -225,147,207,201 -33,169,107,32 -190,192,150,178 -175,207,147,102 -42,163,98,41 -215,196,144,180 -152,101,77,130 -149,125,104,203 -235,171,124,110 -186,230,237,244 -44,164,100,43 -66,126,116,65 -247,133,1,2 -227,238,246,224 -0,162,104,83 -40,115,38,39 -154,134,93,140 -97,120,26,27 -246,233,196,215 -77,101,173,156 -194,137,85,114 -244,237,222,191 -218,243,228,182 -64,145,195,84 -124,48,49,110 -155,96,201,207 -108,31,32,107 -220,151,120,192 -157,202,201,96 -89,24,25,151 -99,97,27,28 -206,229,237,230 -239,213,226,250 -214,245,241,243 -96,8,9,157 -213,136,200,226 -136,213,240,202 -106,76,77,156 -46,47,48,124 -155,7,8,96 -76,106,112,75 -250,226,241,245 -211,187,193,221 -226,200,198,241 -228,86,160,182 -150,117,45,178 -157,9,10,87 -122,87,10,11 -225,201,202,240 -84,195,212,184 -161,138,193,219 -120,151,25,26 -87,136,202,157 -82,125,80,81 -165,12,13,86 -182,160,91,199 -165,86,228,198 -88,118,17,18 -161,197,118,88 -83,104,125,82 -67,172,126,66 -235,110,134,154 -122,11,12,165 -121,19,20,166 -102,147,183,94 -86,13,14,160 -110,49,50,134 -160,14,15,91 -199,91,159,223 -158,99,28,29 -158,29,30,111 -111,30,31,108 -169,119,36,167 -138,161,88,121 -91,15,16,159 -223,159,118,197 -159,16,17,118 -232,197,161,219 -114,85,21,22 -134,50,51,93 -21,85,166,20 -89,191,222,153 -153,114,22,23 -221,193,138,208 -24,89,153,23 -136,87,122,200 -163,42,43,100 -115,40,41,98 -19,121,88,18 -166,85,137,208 -218,182,199,248 -95,130,77,78 -150,192,120,97 -247,2,3,94 -218,248,204,205 -75,112,73,74 -204,176,141,205 -148,63,64,84 -149,144,130,95 -147,225,242,183 -239,234,184,212 -148,105,62,63 -235,186,244,231 -183,242,238,227 -221,217,188,211 -240,249,242,225 -205,214,243,218 -179,143,123,92 -216,248,199,223 -188,177,143,211 -105,128,61,62 -141,176,90,128 -246,215,180,224 -56,57,135,92 -211,143,179,187 -198,200,122,165 -125,131,79,80 -84,184,234,148 -214,181,234,245 -143,177,132,123 -208,138,121,166 -247,94,183,227 -142,179,92,135 -92,123,55,56 -156,173,116,126 -152,116,173,101 -149,203,189,224 -131,95,78,79 -189,203,104,162 -206,146,177,188 -128,90,60,61 -245,234,239,250 -6,109,4,5 -177,146,103,132 -217,229,206,188 -123,132,54,55 -187,179,142,210 -109,155,207,175 -57,58,113,135 -103,129,52,53 -145,64,65,116 -34,119,169,33 -146,168,129,103 -36,119,34,35 -164,44,45,117 -176,139,127,90 -113,127,139,170 -248,216,185,204 -170,142,135,113 -176,204,185,139 -90,127,59,60 -127,113,58,59 -170,209,210,142 -209,185,216,232 -139,185,209,170 -132,103,53,54 -230,168,146,206 -181,141,128,105 -241,198,228,243 -69,70,174,172 -102,4,109,175 -168,230,186,140 -95,131,125,149 -171,235,231,190 -156,174,112,106 -169,167,98,107 -36,115,98,167 -158,164,117,99 -196,145,116,152 -93,129,168,140 -247,227,224,189 -240,213,239,249 -212,236,249,239 +197,223,216,232 +141,205,214,181 +145,195,233,196 +229,194,137,217 +232,209,210,219 +163,98,107,108 +115,38,37,36 +195,212,236,233 +156,174,172,126 +234,148,105,181 +46,45,171,124 +194,229,237,222 +172,69,68,67 +217,137,208,221 +231,220,191,244 +73,72,71,112 +187,193,219,210 +45,178,190,171 +224,149,144,180 +231,190,192,220 +71,70,174,112 +51,93,129,52 +233,236,238,246 +238,236,249,242 +191,220,151,89 +94,102,4,3 +100,163,108,111 +117,99,97,150 +235,186,140,154 +247,133,162,189 +114,194,222,153 +164,100,111,158 +162,133,1,0 +196,144,130,152 +155,7,6,109 +225,201,207,147 +33,32,107,169 +190,178,150,192 +175,102,147,207 +42,41,98,163 +215,180,144,196 +152,130,77,101 +149,203,104,125 +235,110,124,171 +186,244,237,230 +44,43,100,164 +66,65,116,126 +247,2,1,133 +227,224,246,238 +0,83,104,162 +40,39,38,115 +154,140,93,134 +97,27,26,120 +246,215,196,233 +77,156,173,101 +194,114,85,137 +244,191,222,237 +218,182,228,243 +64,84,195,145 +124,110,49,48 +155,207,201,96 +108,107,32,31 +220,192,120,151 +157,96,201,202 +89,151,25,24 +99,28,27,97 +206,230,237,229 +239,250,226,213 +214,243,241,245 +96,157,9,8 +213,226,200,136 +136,202,240,213 +106,156,77,76 +46,124,48,47 +155,96,8,7 +76,75,112,106 +250,245,241,226 +211,221,193,187 +226,241,198,200 +228,182,160,86 +150,178,45,117 +157,87,10,9 +122,11,10,87 +225,240,202,201 +84,184,212,195 +161,219,193,138 +120,26,25,151 +87,157,202,136 +82,81,80,125 +165,86,13,12 +182,199,91,160 +165,198,228,86 +88,18,17,118 +161,88,118,197 +83,82,125,104 +67,66,126,172 +235,154,134,110 +122,165,12,11 +121,166,20,19 +102,94,183,147 +86,160,14,13 +110,134,50,49 +160,91,15,14 +199,223,159,91 +158,29,28,99 +158,111,30,29 +111,108,31,30 +169,167,36,119 +138,121,88,161 +91,159,16,15 +223,197,118,159 +159,118,17,16 +232,219,161,197 +114,22,21,85 +134,93,51,50 +21,20,166,85 +89,153,222,191 +153,23,22,114 +221,208,138,193 +24,23,153,89 +136,200,122,87 +163,100,43,42 +115,98,41,40 +19,18,88,121 +166,208,137,85 +218,248,199,182 +95,78,77,130 +150,97,120,192 +247,94,3,2 +218,205,204,248 +75,74,73,112 +204,205,141,176 +148,84,64,63 +149,95,130,144 +147,183,242,225 +239,212,184,234 +148,63,62,105 +235,231,244,186 +183,227,238,242 +221,211,188,217 +240,225,242,249 +205,218,243,214 +179,92,123,143 +216,223,199,248 +188,211,143,177 +105,62,61,128 +141,128,90,176 +246,224,180,215 +56,92,135,57 +211,187,179,143 +198,165,122,200 +125,80,79,131 +84,148,234,184 +214,245,234,181 +143,123,132,177 +208,166,121,138 +247,227,183,94 +142,135,92,179 +92,56,55,123 +156,126,116,173 +152,101,173,116 +149,224,189,203 +131,79,78,95 +189,162,104,203 +206,188,177,146 +128,61,60,90 +245,250,239,234 +6,5,4,109 +177,132,103,146 +217,188,206,229 +123,55,54,132 +187,210,142,179 +109,175,207,155 +57,135,113,58 +103,53,52,129 +145,116,65,64 +34,33,169,119 +146,103,129,168 +36,35,34,119 +164,117,45,44 +176,90,127,139 +113,170,139,127 +248,204,185,216 +170,113,135,142 +176,139,185,204 +90,60,59,127 +127,59,58,113 +170,142,210,209 +209,232,216,185 +139,170,209,185 +132,54,53,103 +230,206,146,168 +181,105,128,141 +241,243,228,198 +69,172,174,70 +102,175,109,4 +168,140,186,230 +95,149,125,131 +171,190,231,235 +156,106,112,174 +169,107,98,167 +36,167,98,115 +158,99,117,164 +196,152,116,145 +93,140,168,129 +247,189,224,227 +240,249,239,213 +212,239,249,236 diff --git a/data/rabbits/feebs_quad8/connectivity.csv b/data/rabbits/feebs_quad8/connectivity.csv index 08900cfa..66841b7b 100644 --- a/data/rabbits/feebs_quad8/connectivity.csv +++ b/data/rabbits/feebs_quad8/connectivity.csv @@ -1,208 +1,208 @@ -281,316,300,307,335,336,337,338 -225,265,298,289,339,340,341,342 -229,280,317,279,343,344,345,346 -313,301,221,278,347,348,349,350 -316,303,294,293,351,352,353,354 -247,192,191,182,355,356,357,358 -199,36,37,38,359,120,121,360 -279,317,320,296,345,361,362,363 -240,210,256,258,364,365,366,367 -318,265,189,232,368,369,370,371 -46,208,255,45,372,373,374,129 -278,306,321,313,375,376,377,350 -256,67,68,69,378,151,152,379 -301,305,292,221,380,381,382,348 -315,328,275,304,383,384,385,386 -73,196,71,72,387,388,155,156 -271,294,303,277,389,352,390,391 -45,255,274,262,374,392,393,394 -308,264,228,233,395,396,397,398 -315,304,276,274,386,399,400,401 -71,196,258,70,388,402,403,154 -51,52,213,177,135,404,405,406 -317,330,322,320,407,408,409,361 -322,326,333,320,410,411,412,409 -275,173,235,304,413,414,415,385 -178,3,4,186,416,87,417,418 -184,195,192,247,419,420,355,421 -201,234,181,183,422,423,424,425 -319,238,224,270,426,427,428,429 -331,273,246,217,430,431,432,433 -198,237,306,278,434,435,375,436 -248,242,195,184,437,438,419,439 -246,0,1,217,440,84,441,432 -280,236,214,228,442,443,444,445 -239,193,6,7,446,447,90,448 -309,231,291,285,449,450,451,452 -33,253,191,32,453,454,455,116 -274,276,234,262,400,456,457,393 -259,291,231,186,458,450,459,460 -42,247,182,41,461,358,462,125 -299,280,228,264,463,445,396,464 -236,185,77,214,465,466,467,443 -233,209,188,287,468,469,470,471 -319,255,208,194,472,373,473,474 -270,314,321,328,475,476,477,478 -44,248,184,43,479,439,480,127 -66,210,200,65,481,482,483,149 -331,217,1,2,433,441,85,484 -311,322,330,308,485,408,486,487 -0,246,188,83,440,488,489,167 -40,199,38,39,490,360,122,123 -238,218,177,224,491,492,493,427 -181,204,26,27,494,495,110,496 -330,317,280,299,407,344,463,497 -77,185,257,240,466,498,499,500 -278,221,169,198,349,501,502,436 -328,321,306,275,477,376,503,384 -302,327,312,266,504,505,506,507 -64,229,279,168,508,346,509,510 -208,48,49,194,511,132,512,473 -239,180,285,291,513,514,451,515 -192,31,32,191,516,115,455,356 -304,235,204,276,415,517,518,399 -241,286,285,180,519,520,514,521 -173,24,25,235,522,108,523,414 -183,181,27,28,424,496,111,524 -290,313,321,314,525,377,476,526 -323,297,310,334,527,528,529,530 -298,329,325,327,531,532,533,534 -180,8,9,241,535,92,536,521 -297,220,284,310,537,538,539,528 -220,297,324,286,537,540,541,542 -190,76,77,240,543,160,500,544 -46,47,48,208,130,131,511,372 -239,7,8,180,448,91,535,513 -76,190,196,75,543,545,546,159 -334,310,325,329,529,547,532,548 -295,271,277,305,549,391,550,551 -310,284,282,325,539,552,553,547 -312,170,244,266,554,555,556,506 -234,201,45,262,422,557,394,457 -241,9,10,171,536,93,558,559 -206,171,10,11,560,558,94,561 -309,285,286,324,452,520,541,562 -168,279,296,268,509,363,563,564 -245,222,277,303,565,566,390,567 -204,235,25,26,517,523,109,495 -171,220,286,241,568,542,519,559 -82,209,80,81,569,570,164,165 -249,12,13,170,571,96,572,573 -266,244,175,283,556,574,575,576 -249,170,312,282,573,554,577,578 -172,202,17,18,579,580,101,581 -245,281,202,172,582,583,579,584 -83,188,209,82,489,469,569,166 -67,256,210,66,378,365,481,150 -319,194,218,238,474,585,491,426 -206,11,12,249,561,95,571,586 -205,19,20,250,587,103,588,589 -186,231,267,178,459,590,591,418 -170,13,14,244,572,97,592,555 -194,49,50,218,512,133,593,585 -244,14,15,175,592,98,594,574 -283,175,243,307,575,595,596,597 -242,183,28,29,598,524,112,599 -242,29,30,195,599,113,600,438 -195,30,31,192,600,114,516,420 -253,203,36,251,601,602,603,604 -222,245,172,205,565,584,605,606 -175,15,16,243,594,99,607,595 -307,243,202,281,596,608,583,338 -243,16,17,202,607,100,580,608 -316,281,245,303,335,582,567,351 -198,169,21,22,502,609,105,610 -218,50,51,177,593,134,406,492 -21,169,250,20,609,611,588,104 -173,275,306,237,413,503,435,612 -237,198,22,23,434,610,106,613 -305,277,222,292,550,566,614,381 -24,173,237,23,522,612,613,107 -220,171,206,284,568,560,615,538 -247,42,43,184,461,126,480,421 -199,40,41,182,490,124,462,616 -19,205,172,18,587,605,581,102 -250,169,221,292,611,501,382,617 -302,266,283,332,507,576,618,619 -179,214,77,78,620,467,161,621 -234,276,204,181,456,518,494,423 -331,2,3,178,484,86,416,622 -302,332,288,289,619,623,624,625 -75,196,73,74,546,387,157,158 -288,260,225,289,626,627,342,624 -232,63,64,168,628,147,510,629 -233,228,214,179,397,444,620,630 -231,309,326,267,449,631,632,590 -323,318,268,296,633,634,563,635 -232,189,62,63,370,636,146,628 -319,270,328,315,429,478,383,637 -267,326,322,311,632,410,485,638 -305,301,272,295,380,639,640,551 -324,333,326,309,641,411,631,562 -289,298,327,302,341,534,504,625 -263,227,207,176,642,643,644,645 -300,332,283,307,646,618,597,337 -272,261,227,295,647,648,649,640 -189,212,61,62,650,651,145,636 -225,260,174,212,627,652,653,654 -330,299,264,308,497,464,395,486 -56,57,219,176,140,655,656,657 -295,227,263,271,649,642,658,549 -282,284,206,249,552,615,586,578 -209,215,79,80,659,660,163,570 -168,268,318,232,564,634,371,629 -298,265,318,329,340,368,661,531 -227,261,216,207,648,662,663,643 -292,222,205,250,614,606,589,617 -331,178,267,311,622,591,638,664 -226,263,176,219,665,645,656,666 -176,207,55,56,644,667,139,657 -240,257,200,210,499,668,482,364 -236,200,257,185,669,668,498,465 -233,287,273,308,471,670,671,398 -215,179,78,79,672,621,162,660 -273,287,188,246,670,470,488,431 -290,230,261,272,673,674,647,675 -212,174,60,61,653,676,144,651 -329,318,323,334,661,633,530,548 -6,193,4,5,447,677,88,89 -261,230,187,216,674,678,679,662 -301,313,290,272,347,525,675,639 -207,216,54,55,663,680,138,667 -271,263,226,294,658,665,681,389 -193,239,291,259,446,515,458,682 -57,58,197,219,141,683,684,655 -187,213,52,53,685,404,136,686 -229,64,65,200,508,148,483,687 -34,203,253,33,688,601,453,117 -230,252,213,187,689,690,685,678 -36,203,34,35,602,688,118,119 -248,44,45,201,479,128,557,691 -260,223,211,174,692,693,694,652 -197,211,223,254,695,693,696,697 -332,300,269,288,646,698,699,623 -254,226,219,197,700,666,684,697 -260,288,269,223,626,699,701,692 -174,211,59,60,694,702,143,676 -211,197,58,59,695,683,142,702 -254,293,294,226,703,353,681,700 -293,269,300,316,704,698,336,354 -223,269,293,254,701,704,703,696 -216,187,53,54,679,686,137,680 -314,252,230,290,705,689,673,526 -265,225,212,189,339,654,650,369 -325,282,312,327,553,577,505,533 -69,70,258,256,153,403,366,379 -186,4,193,259,417,677,682,460 -252,314,270,224,705,475,428,706 -179,215,209,233,672,659,468,630 -255,319,315,274,472,637,401,392 -240,258,196,190,367,402,545,544 -253,251,182,191,604,707,357,454 -36,199,182,251,359,616,707,603 -242,248,201,183,437,691,425,598 -280,229,200,236,343,687,669,442 -177,213,252,224,405,690,706,493 -331,311,308,273,664,487,671,430 -324,297,323,333,540,527,708,641 -296,320,333,323,362,412,708,635 +281,307,300,316,338,337,336,335 +225,289,298,265,342,341,340,339 +229,279,317,280,346,345,344,343 +313,278,221,301,350,349,348,347 +316,293,294,303,354,353,352,351 +247,182,191,192,358,357,356,355 +199,38,37,36,360,121,120,359 +279,296,320,317,363,362,361,345 +240,258,256,210,367,366,365,364 +318,232,189,265,371,370,369,368 +46,45,255,208,129,374,373,372 +278,313,321,306,350,377,376,375 +256,69,68,67,379,152,151,378 +301,221,292,305,348,382,381,380 +315,304,275,328,386,385,384,383 +73,72,71,196,156,155,388,387 +271,277,303,294,391,390,352,389 +45,262,274,255,394,393,392,374 +308,233,228,264,398,397,396,395 +315,274,276,304,401,400,399,386 +71,70,258,196,154,403,402,388 +51,177,213,52,406,405,404,135 +317,320,322,330,361,409,408,407 +322,320,333,326,409,412,411,410 +275,304,235,173,385,415,414,413 +178,186,4,3,418,417,87,416 +184,247,192,195,421,355,420,419 +201,183,181,234,425,424,423,422 +319,270,224,238,429,428,427,426 +331,217,246,273,433,432,431,430 +198,278,306,237,436,375,435,434 +248,184,195,242,439,419,438,437 +246,217,1,0,432,441,84,440 +280,228,214,236,445,444,443,442 +239,7,6,193,448,90,447,446 +309,285,291,231,452,451,450,449 +33,32,191,253,116,455,454,453 +274,262,234,276,393,457,456,400 +259,186,231,291,460,459,450,458 +42,41,182,247,125,462,358,461 +299,264,228,280,464,396,445,463 +236,214,77,185,443,467,466,465 +233,287,188,209,471,470,469,468 +319,194,208,255,474,473,373,472 +270,328,321,314,478,477,476,475 +44,43,184,248,127,480,439,479 +66,65,200,210,149,483,482,481 +331,2,1,217,484,85,441,433 +311,308,330,322,487,486,408,485 +0,83,188,246,167,489,488,440 +40,39,38,199,123,122,360,490 +238,224,177,218,427,493,492,491 +181,27,26,204,496,110,495,494 +330,299,280,317,497,463,344,407 +77,240,257,185,500,499,498,466 +278,198,169,221,436,502,501,349 +328,275,306,321,384,503,376,477 +302,266,312,327,507,506,505,504 +64,168,279,229,510,509,346,508 +208,194,49,48,473,512,132,511 +239,291,285,180,515,451,514,513 +192,191,32,31,356,455,115,516 +304,276,204,235,399,518,517,415 +241,180,285,286,521,514,520,519 +173,235,25,24,414,523,108,522 +183,28,27,181,524,111,496,424 +290,314,321,313,526,476,377,525 +323,334,310,297,530,529,528,527 +298,327,325,329,534,533,532,531 +180,241,9,8,521,536,92,535 +297,310,284,220,528,539,538,537 +220,286,324,297,542,541,540,537 +190,240,77,76,544,500,160,543 +46,208,48,47,372,511,131,130 +239,180,8,7,513,535,91,448 +76,75,196,190,159,546,545,543 +334,329,325,310,548,532,547,529 +295,305,277,271,551,550,391,549 +310,325,282,284,547,553,552,539 +312,266,244,170,506,556,555,554 +234,262,45,201,457,394,557,422 +241,171,10,9,559,558,93,536 +206,11,10,171,561,94,558,560 +309,324,286,285,562,541,520,452 +168,268,296,279,564,563,363,509 +245,303,277,222,567,390,566,565 +204,26,25,235,495,109,523,517 +171,241,286,220,559,519,542,568 +82,81,80,209,165,164,570,569 +249,170,13,12,573,572,96,571 +266,283,175,244,576,575,574,556 +249,282,312,170,578,577,554,573 +172,18,17,202,581,101,580,579 +245,172,202,281,584,579,583,582 +83,82,209,188,166,569,469,489 +67,66,210,256,150,481,365,378 +319,238,218,194,426,491,585,474 +206,249,12,11,586,571,95,561 +205,250,20,19,589,588,103,587 +186,178,267,231,418,591,590,459 +170,244,14,13,555,592,97,572 +194,218,50,49,585,593,133,512 +244,175,15,14,574,594,98,592 +283,307,243,175,597,596,595,575 +242,29,28,183,599,112,524,598 +242,195,30,29,438,600,113,599 +195,192,31,30,420,516,114,600 +253,251,36,203,604,603,602,601 +222,205,172,245,606,605,584,565 +175,243,16,15,595,607,99,594 +307,281,202,243,338,583,608,596 +243,202,17,16,608,580,100,607 +316,303,245,281,351,567,582,335 +198,22,21,169,610,105,609,502 +218,177,51,50,492,406,134,593 +21,20,250,169,104,588,611,609 +173,237,306,275,612,435,503,413 +237,23,22,198,613,106,610,434 +305,292,222,277,381,614,566,550 +24,23,237,173,107,613,612,522 +220,284,206,171,538,615,560,568 +247,184,43,42,421,480,126,461 +199,182,41,40,616,462,124,490 +19,18,172,205,102,581,605,587 +250,292,221,169,617,382,501,611 +302,332,283,266,619,618,576,507 +179,78,77,214,621,161,467,620 +234,181,204,276,423,494,518,456 +331,178,3,2,622,416,86,484 +302,289,288,332,625,624,623,619 +75,74,73,196,158,157,387,546 +288,289,225,260,624,342,627,626 +232,168,64,63,629,510,147,628 +233,179,214,228,630,620,444,397 +231,267,326,309,590,632,631,449 +323,296,268,318,635,563,634,633 +232,63,62,189,628,146,636,370 +319,315,328,270,637,383,478,429 +267,311,322,326,638,485,410,632 +305,295,272,301,551,640,639,380 +324,309,326,333,562,631,411,641 +289,302,327,298,625,504,534,341 +263,176,207,227,645,644,643,642 +300,307,283,332,337,597,618,646 +272,295,227,261,640,649,648,647 +189,62,61,212,636,145,651,650 +225,212,174,260,654,653,652,627 +330,308,264,299,486,395,464,497 +56,176,219,57,657,656,655,140 +295,271,263,227,549,658,642,649 +282,249,206,284,578,586,615,552 +209,80,79,215,570,163,660,659 +168,232,318,268,629,371,634,564 +298,329,318,265,531,661,368,340 +227,207,216,261,643,663,662,648 +292,250,205,222,617,589,606,614 +331,311,267,178,664,638,591,622 +226,219,176,263,666,656,645,665 +176,56,55,207,657,139,667,644 +240,210,200,257,364,482,668,499 +236,185,257,200,465,498,668,669 +233,308,273,287,398,671,670,471 +215,79,78,179,660,162,621,672 +273,246,188,287,431,488,470,670 +290,272,261,230,675,647,674,673 +212,61,60,174,651,144,676,653 +329,334,323,318,548,530,633,661 +6,5,4,193,89,88,677,447 +261,216,187,230,662,679,678,674 +301,272,290,313,639,675,525,347 +207,55,54,216,667,138,680,663 +271,294,226,263,389,681,665,658 +193,259,291,239,682,458,515,446 +57,219,197,58,655,684,683,141 +187,53,52,213,686,136,404,685 +229,200,65,64,687,483,148,508 +34,33,253,203,117,453,601,688 +230,187,213,252,678,685,690,689 +36,35,34,203,119,118,688,602 +248,201,45,44,691,557,128,479 +260,174,211,223,652,694,693,692 +197,254,223,211,697,696,693,695 +332,288,269,300,623,699,698,646 +254,197,219,226,697,684,666,700 +260,223,269,288,692,701,699,626 +174,60,59,211,676,143,702,694 +211,59,58,197,702,142,683,695 +254,226,294,293,700,681,353,703 +293,316,300,269,354,336,698,704 +223,254,293,269,696,703,704,701 +216,54,53,187,680,137,686,679 +314,290,230,252,526,673,689,705 +265,189,212,225,369,650,654,339 +325,327,312,282,533,505,577,553 +69,256,258,70,379,366,403,153 +186,259,193,4,460,682,677,417 +252,224,270,314,706,428,475,705 +179,233,209,215,630,468,659,672 +255,274,315,319,392,401,637,472 +240,190,196,258,544,545,402,367 +253,191,182,251,454,357,707,604 +36,251,182,199,603,707,616,359 +242,183,201,248,598,425,691,437 +280,236,200,229,442,669,687,343 +177,224,252,213,493,706,690,405 +331,273,308,311,430,671,487,664 +324,333,323,297,641,708,527,540 +296,323,333,320,635,708,412,362 diff --git a/data/rabbits/feebs_quad9/connectivity.csv b/data/rabbits/feebs_quad9/connectivity.csv index c905aa23..33e0b1ea 100644 --- a/data/rabbits/feebs_quad9/connectivity.csv +++ b/data/rabbits/feebs_quad9/connectivity.csv @@ -1,208 +1,208 @@ -281,316,300,307,335,336,337,338,339 -225,265,298,289,340,341,342,343,344 -229,280,317,279,345,346,347,348,349 -313,301,221,278,350,351,352,353,354 -316,303,294,293,355,356,357,358,359 -247,192,191,182,360,361,362,363,364 -199,36,37,38,365,120,121,366,367 -279,317,320,296,347,368,369,370,371 -240,210,256,258,372,373,374,375,376 -318,265,189,232,377,378,379,380,381 -46,208,255,45,382,383,384,129,385 -278,306,321,313,386,387,388,353,389 -256,67,68,69,390,151,152,391,392 -301,305,292,221,393,394,395,351,396 -315,328,275,304,397,398,399,400,401 -73,196,71,72,402,403,155,156,404 -271,294,303,277,405,356,406,407,408 -45,255,274,262,384,409,410,411,412 -308,264,228,233,413,414,415,416,417 -315,304,276,274,400,418,419,420,421 -71,196,258,70,403,422,423,154,424 -51,52,213,177,135,425,426,427,428 -317,330,322,320,429,430,431,368,432 -322,326,333,320,433,434,435,431,436 -275,173,235,304,437,438,439,399,440 -178,3,4,186,441,87,442,443,444 -184,195,192,247,445,446,360,447,448 -201,234,181,183,449,450,451,452,453 -319,238,224,270,454,455,456,457,458 -331,273,246,217,459,460,461,462,463 -198,237,306,278,464,465,386,466,467 -248,242,195,184,468,469,445,470,471 -246,0,1,217,472,84,473,461,474 -280,236,214,228,475,476,477,478,479 -239,193,6,7,480,481,90,482,483 -309,231,291,285,484,485,486,487,488 -33,253,191,32,489,490,491,116,492 -274,276,234,262,419,493,494,410,495 -259,291,231,186,496,485,497,498,499 -42,247,182,41,500,363,501,125,502 -299,280,228,264,503,478,414,504,505 -236,185,77,214,506,507,508,476,509 -233,209,188,287,510,511,512,513,514 -319,255,208,194,515,383,516,517,518 -270,314,321,328,519,520,521,522,523 -44,248,184,43,524,470,525,127,526 -66,210,200,65,527,528,529,149,530 -331,217,1,2,462,473,85,531,532 -311,322,330,308,533,430,534,535,536 -0,246,188,83,472,537,538,167,539 -40,199,38,39,540,366,122,123,541 -238,218,177,224,542,543,544,455,545 -181,204,26,27,546,547,110,548,549 -330,317,280,299,429,346,503,550,551 -77,185,257,240,507,552,553,554,555 -278,221,169,198,352,556,557,466,558 -328,321,306,275,521,387,559,398,560 -302,327,312,266,561,562,563,564,565 -64,229,279,168,566,348,567,568,569 -208,48,49,194,570,132,571,516,572 -239,180,285,291,573,574,486,575,576 -192,31,32,191,577,115,491,361,578 -304,235,204,276,439,579,580,418,581 -241,286,285,180,582,583,574,584,585 -173,24,25,235,586,108,587,438,588 -183,181,27,28,451,548,111,589,590 -290,313,321,314,591,388,520,592,593 -323,297,310,334,594,595,596,597,598 -298,329,325,327,599,600,601,602,603 -180,8,9,241,604,92,605,584,606 -297,220,284,310,607,608,609,595,610 -220,297,324,286,607,611,612,613,614 -190,76,77,240,615,160,554,616,617 -46,47,48,208,130,131,570,382,618 -239,7,8,180,482,91,604,573,619 -76,190,196,75,615,620,621,159,622 -334,310,325,329,596,623,600,624,625 -295,271,277,305,626,407,627,628,629 -310,284,282,325,609,630,631,623,632 -312,170,244,266,633,634,635,563,636 -234,201,45,262,449,637,411,494,638 -241,9,10,171,605,93,639,640,641 -206,171,10,11,642,639,94,643,644 -309,285,286,324,487,583,612,645,646 -168,279,296,268,567,370,647,648,649 -245,222,277,303,650,651,406,652,653 -204,235,25,26,579,587,109,547,654 -171,220,286,241,655,613,582,640,656 -82,209,80,81,657,658,164,165,659 -249,12,13,170,660,96,661,662,663 -266,244,175,283,635,664,665,666,667 -249,170,312,282,662,633,668,669,670 -172,202,17,18,671,672,101,673,674 -245,281,202,172,675,676,671,677,678 -83,188,209,82,538,511,657,166,679 -67,256,210,66,390,373,527,150,680 -319,194,218,238,517,681,542,454,682 -206,11,12,249,643,95,660,683,684 -205,19,20,250,685,103,686,687,688 -186,231,267,178,497,689,690,443,691 -170,13,14,244,661,97,692,634,693 -194,49,50,218,571,133,694,681,695 -244,14,15,175,692,98,696,664,697 -283,175,243,307,665,698,699,700,701 -242,183,28,29,702,589,112,703,704 -242,29,30,195,703,113,705,469,706 -195,30,31,192,705,114,577,446,707 -253,203,36,251,708,709,710,711,712 -222,245,172,205,650,677,713,714,715 -175,15,16,243,696,99,716,698,717 -307,243,202,281,699,718,676,338,719 -243,16,17,202,716,100,672,718,720 -316,281,245,303,335,675,652,355,721 -198,169,21,22,557,722,105,723,724 -218,50,51,177,694,134,427,543,725 -21,169,250,20,722,726,686,104,727 -173,275,306,237,437,559,465,728,729 -237,198,22,23,464,723,106,730,731 -305,277,222,292,627,651,732,394,733 -24,173,237,23,586,728,730,107,734 -220,171,206,284,655,642,735,608,736 -247,42,43,184,500,126,525,447,737 -199,40,41,182,540,124,501,738,739 -19,205,172,18,685,713,673,102,740 -250,169,221,292,726,556,395,741,742 -302,266,283,332,564,666,743,744,745 -179,214,77,78,746,508,161,747,748 -234,276,204,181,493,580,546,450,749 -331,2,3,178,531,86,441,750,751 -302,332,288,289,744,752,753,754,755 -75,196,73,74,621,402,157,158,756 -288,260,225,289,757,758,343,753,759 -232,63,64,168,760,147,568,761,762 -233,228,214,179,415,477,746,763,764 -231,309,326,267,484,765,766,689,767 -323,318,268,296,768,769,647,770,771 -232,189,62,63,379,772,146,760,773 -319,270,328,315,457,522,397,774,775 -267,326,322,311,766,433,533,776,777 -305,301,272,295,393,778,779,628,780 -324,333,326,309,781,434,765,645,782 -289,298,327,302,342,602,561,754,783 -263,227,207,176,784,785,786,787,788 -300,332,283,307,789,743,700,337,790 -272,261,227,295,791,792,793,779,794 -189,212,61,62,795,796,145,772,797 -225,260,174,212,758,798,799,800,801 -330,299,264,308,550,504,413,534,802 -56,57,219,176,140,803,804,805,806 -295,227,263,271,793,784,807,626,808 -282,284,206,249,630,735,683,669,809 -209,215,79,80,810,811,163,658,812 -168,268,318,232,648,769,380,761,813 -298,265,318,329,341,377,814,599,815 -227,261,216,207,792,816,817,785,818 -292,222,205,250,732,714,687,741,819 -331,178,267,311,750,690,776,820,821 -226,263,176,219,822,787,804,823,824 -176,207,55,56,786,825,139,805,826 -240,257,200,210,553,827,528,372,828 -236,200,257,185,829,827,552,506,830 -233,287,273,308,513,831,832,416,833 -215,179,78,79,834,747,162,811,835 -273,287,188,246,831,512,537,460,836 -290,230,261,272,837,838,791,839,840 -212,174,60,61,799,841,144,796,842 -329,318,323,334,814,768,597,624,843 -6,193,4,5,481,844,88,89,845 -261,230,187,216,838,846,847,816,848 -301,313,290,272,350,591,839,778,849 -207,216,54,55,817,850,138,825,851 -271,263,226,294,807,822,852,405,853 -193,239,291,259,480,575,496,854,855 -57,58,197,219,141,856,857,803,858 -187,213,52,53,859,425,136,860,861 -229,64,65,200,566,148,529,862,863 -34,203,253,33,864,708,489,117,865 -230,252,213,187,866,867,859,846,868 -36,203,34,35,709,864,118,119,869 -248,44,45,201,524,128,637,870,871 -260,223,211,174,872,873,874,798,875 -197,211,223,254,876,873,877,878,879 -332,300,269,288,789,880,881,752,882 -254,226,219,197,883,823,857,878,884 -260,288,269,223,757,881,885,872,886 -174,211,59,60,874,887,143,841,888 -211,197,58,59,876,856,142,887,889 -254,293,294,226,890,357,852,883,891 -293,269,300,316,892,880,336,358,893 -223,269,293,254,885,892,890,877,894 -216,187,53,54,847,860,137,850,895 -314,252,230,290,896,866,837,592,897 -265,225,212,189,340,800,795,378,898 -325,282,312,327,631,668,562,601,899 -69,70,258,256,153,423,374,391,900 -186,4,193,259,442,844,854,498,901 -252,314,270,224,896,519,456,902,903 -179,215,209,233,834,810,510,763,904 -255,319,315,274,515,774,420,409,905 -240,258,196,190,375,422,620,616,906 -253,251,182,191,711,907,362,490,908 -36,199,182,251,365,738,907,710,909 -242,248,201,183,468,870,452,702,910 -280,229,200,236,345,862,829,475,911 -177,213,252,224,426,867,902,544,912 -331,311,308,273,820,535,832,459,913 -324,297,323,333,611,594,914,781,915 -296,320,333,323,369,435,914,770,916 +281,307,300,316,338,337,336,335,339 +225,289,298,265,343,342,341,340,344 +229,279,317,280,348,347,346,345,349 +313,278,221,301,353,352,351,350,354 +316,293,294,303,358,357,356,355,359 +247,182,191,192,363,362,361,360,364 +199,38,37,36,366,121,120,365,367 +279,296,320,317,370,369,368,347,371 +240,258,256,210,375,374,373,372,376 +318,232,189,265,380,379,378,377,381 +46,45,255,208,129,384,383,382,385 +278,313,321,306,353,388,387,386,389 +256,69,68,67,391,152,151,390,392 +301,221,292,305,351,395,394,393,396 +315,304,275,328,400,399,398,397,401 +73,72,71,196,156,155,403,402,404 +271,277,303,294,407,406,356,405,408 +45,262,274,255,411,410,409,384,412 +308,233,228,264,416,415,414,413,417 +315,274,276,304,420,419,418,400,421 +71,70,258,196,154,423,422,403,424 +51,177,213,52,427,426,425,135,428 +317,320,322,330,368,431,430,429,432 +322,320,333,326,431,435,434,433,436 +275,304,235,173,399,439,438,437,440 +178,186,4,3,443,442,87,441,444 +184,247,192,195,447,360,446,445,448 +201,183,181,234,452,451,450,449,453 +319,270,224,238,457,456,455,454,458 +331,217,246,273,462,461,460,459,463 +198,278,306,237,466,386,465,464,467 +248,184,195,242,470,445,469,468,471 +246,217,1,0,461,473,84,472,474 +280,228,214,236,478,477,476,475,479 +239,7,6,193,482,90,481,480,483 +309,285,291,231,487,486,485,484,488 +33,32,191,253,116,491,490,489,492 +274,262,234,276,410,494,493,419,495 +259,186,231,291,498,497,485,496,499 +42,41,182,247,125,501,363,500,502 +299,264,228,280,504,414,478,503,505 +236,214,77,185,476,508,507,506,509 +233,287,188,209,513,512,511,510,514 +319,194,208,255,517,516,383,515,518 +270,328,321,314,522,521,520,519,523 +44,43,184,248,127,525,470,524,526 +66,65,200,210,149,529,528,527,530 +331,2,1,217,531,85,473,462,532 +311,308,330,322,535,534,430,533,536 +0,83,188,246,167,538,537,472,539 +40,39,38,199,123,122,366,540,541 +238,224,177,218,455,544,543,542,545 +181,27,26,204,548,110,547,546,549 +330,299,280,317,550,503,346,429,551 +77,240,257,185,554,553,552,507,555 +278,198,169,221,466,557,556,352,558 +328,275,306,321,398,559,387,521,560 +302,266,312,327,564,563,562,561,565 +64,168,279,229,568,567,348,566,569 +208,194,49,48,516,571,132,570,572 +239,291,285,180,575,486,574,573,576 +192,191,32,31,361,491,115,577,578 +304,276,204,235,418,580,579,439,581 +241,180,285,286,584,574,583,582,585 +173,235,25,24,438,587,108,586,588 +183,28,27,181,589,111,548,451,590 +290,314,321,313,592,520,388,591,593 +323,334,310,297,597,596,595,594,598 +298,327,325,329,602,601,600,599,603 +180,241,9,8,584,605,92,604,606 +297,310,284,220,595,609,608,607,610 +220,286,324,297,613,612,611,607,614 +190,240,77,76,616,554,160,615,617 +46,208,48,47,382,570,131,130,618 +239,180,8,7,573,604,91,482,619 +76,75,196,190,159,621,620,615,622 +334,329,325,310,624,600,623,596,625 +295,305,277,271,628,627,407,626,629 +310,325,282,284,623,631,630,609,632 +312,266,244,170,563,635,634,633,636 +234,262,45,201,494,411,637,449,638 +241,171,10,9,640,639,93,605,641 +206,11,10,171,643,94,639,642,644 +309,324,286,285,645,612,583,487,646 +168,268,296,279,648,647,370,567,649 +245,303,277,222,652,406,651,650,653 +204,26,25,235,547,109,587,579,654 +171,241,286,220,640,582,613,655,656 +82,81,80,209,165,164,658,657,659 +249,170,13,12,662,661,96,660,663 +266,283,175,244,666,665,664,635,667 +249,282,312,170,669,668,633,662,670 +172,18,17,202,673,101,672,671,674 +245,172,202,281,677,671,676,675,678 +83,82,209,188,166,657,511,538,679 +67,66,210,256,150,527,373,390,680 +319,238,218,194,454,542,681,517,682 +206,249,12,11,683,660,95,643,684 +205,250,20,19,687,686,103,685,688 +186,178,267,231,443,690,689,497,691 +170,244,14,13,634,692,97,661,693 +194,218,50,49,681,694,133,571,695 +244,175,15,14,664,696,98,692,697 +283,307,243,175,700,699,698,665,701 +242,29,28,183,703,112,589,702,704 +242,195,30,29,469,705,113,703,706 +195,192,31,30,446,577,114,705,707 +253,251,36,203,711,710,709,708,712 +222,205,172,245,714,713,677,650,715 +175,243,16,15,698,716,99,696,717 +307,281,202,243,338,676,718,699,719 +243,202,17,16,718,672,100,716,720 +316,303,245,281,355,652,675,335,721 +198,22,21,169,723,105,722,557,724 +218,177,51,50,543,427,134,694,725 +21,20,250,169,104,686,726,722,727 +173,237,306,275,728,465,559,437,729 +237,23,22,198,730,106,723,464,731 +305,292,222,277,394,732,651,627,733 +24,23,237,173,107,730,728,586,734 +220,284,206,171,608,735,642,655,736 +247,184,43,42,447,525,126,500,737 +199,182,41,40,738,501,124,540,739 +19,18,172,205,102,673,713,685,740 +250,292,221,169,741,395,556,726,742 +302,332,283,266,744,743,666,564,745 +179,78,77,214,747,161,508,746,748 +234,181,204,276,450,546,580,493,749 +331,178,3,2,750,441,86,531,751 +302,289,288,332,754,753,752,744,755 +75,74,73,196,158,157,402,621,756 +288,289,225,260,753,343,758,757,759 +232,168,64,63,761,568,147,760,762 +233,179,214,228,763,746,477,415,764 +231,267,326,309,689,766,765,484,767 +323,296,268,318,770,647,769,768,771 +232,63,62,189,760,146,772,379,773 +319,315,328,270,774,397,522,457,775 +267,311,322,326,776,533,433,766,777 +305,295,272,301,628,779,778,393,780 +324,309,326,333,645,765,434,781,782 +289,302,327,298,754,561,602,342,783 +263,176,207,227,787,786,785,784,788 +300,307,283,332,337,700,743,789,790 +272,295,227,261,779,793,792,791,794 +189,62,61,212,772,145,796,795,797 +225,212,174,260,800,799,798,758,801 +330,308,264,299,534,413,504,550,802 +56,176,219,57,805,804,803,140,806 +295,271,263,227,626,807,784,793,808 +282,249,206,284,669,683,735,630,809 +209,80,79,215,658,163,811,810,812 +168,232,318,268,761,380,769,648,813 +298,329,318,265,599,814,377,341,815 +227,207,216,261,785,817,816,792,818 +292,250,205,222,741,687,714,732,819 +331,311,267,178,820,776,690,750,821 +226,219,176,263,823,804,787,822,824 +176,56,55,207,805,139,825,786,826 +240,210,200,257,372,528,827,553,828 +236,185,257,200,506,552,827,829,830 +233,308,273,287,416,832,831,513,833 +215,79,78,179,811,162,747,834,835 +273,246,188,287,460,537,512,831,836 +290,272,261,230,839,791,838,837,840 +212,61,60,174,796,144,841,799,842 +329,334,323,318,624,597,768,814,843 +6,5,4,193,89,88,844,481,845 +261,216,187,230,816,847,846,838,848 +301,272,290,313,778,839,591,350,849 +207,55,54,216,825,138,850,817,851 +271,294,226,263,405,852,822,807,853 +193,259,291,239,854,496,575,480,855 +57,219,197,58,803,857,856,141,858 +187,53,52,213,860,136,425,859,861 +229,200,65,64,862,529,148,566,863 +34,33,253,203,117,489,708,864,865 +230,187,213,252,846,859,867,866,868 +36,35,34,203,119,118,864,709,869 +248,201,45,44,870,637,128,524,871 +260,174,211,223,798,874,873,872,875 +197,254,223,211,878,877,873,876,879 +332,288,269,300,752,881,880,789,882 +254,197,219,226,878,857,823,883,884 +260,223,269,288,872,885,881,757,886 +174,60,59,211,841,143,887,874,888 +211,59,58,197,887,142,856,876,889 +254,226,294,293,883,852,357,890,891 +293,316,300,269,358,336,880,892,893 +223,254,293,269,877,890,892,885,894 +216,54,53,187,850,137,860,847,895 +314,290,230,252,592,837,866,896,897 +265,189,212,225,378,795,800,340,898 +325,327,312,282,601,562,668,631,899 +69,256,258,70,391,374,423,153,900 +186,259,193,4,498,854,844,442,901 +252,224,270,314,902,456,519,896,903 +179,233,209,215,763,510,810,834,904 +255,274,315,319,409,420,774,515,905 +240,190,196,258,616,620,422,375,906 +253,191,182,251,490,362,907,711,908 +36,251,182,199,710,907,738,365,909 +242,183,201,248,702,452,870,468,910 +280,236,200,229,475,829,862,345,911 +177,224,252,213,544,902,867,426,912 +331,273,308,311,459,832,535,820,913 +324,333,323,297,781,914,594,611,915 +296,323,333,320,770,914,435,369,916 diff --git a/data/rabbits/feebs_tri3/connectivity.csv b/data/rabbits/feebs_tri3/connectivity.csv index 8940b18c..79e8a0c9 100644 --- a/data/rabbits/feebs_tri3/connectivity.csv +++ b/data/rabbits/feebs_tri3/connectivity.csv @@ -1,335 +1,335 @@ -69,71,70 -70,71,72 -73,75,74 -56,57,55 -73,74,76 -55,57,54 -77,79,78 -80,71,69 -81,83,82 -81,82,21 -80,69,43 -84,86,85 -87,82,83 -85,86,88 -89,91,90 -90,91,92 -6,94,93 -94,6,95 -96,97,86 -86,97,98 -49,100,99 -101,103,102 -104,40,41 -101,102,19 -105,70,104 -104,70,106 -69,70,105 -69,105,42 -74,85,88 -49,99,48 -107,103,108 -107,108,72 -6,7,95 -109,73,110 -111,113,112 -112,113,110 -107,114,103 -115,116,106 -75,117,74 -74,88,76 -118,120,119 -119,120,2 -115,87,79 -121,91,98 -122,114,107 -123,124,44 -74,117,85 -76,88,125 -122,107,126 -114,102,103 -122,128,127 -122,127,129 -116,115,79 -108,101,82 -73,130,75 -75,130,13 -131,132,121 -131,121,133 -101,20,82 -134,109,135 -136,137,97 -97,137,133 -80,124,71 -71,124,126 -103,101,108 -136,97,96 -113,139,138 -113,138,135 -136,96,16 -140,128,123 -140,123,45 -17,136,16 -66,67,141 -142,141,67 -140,131,128 -128,131,127 -71,126,72 -136,143,137 -144,111,112 -132,145,121 -146,102,114 -146,114,129 -147,138,139 -147,139,51 -67,68,142 -84,85,117 -139,113,111 -139,111,50 -148,150,149 -148,149,151 -131,140,132 -132,140,46 -137,143,146 -146,143,18 -152,154,153 -152,153,155 -84,117,14 -156,158,157 -156,157,155 -95,7,159 -159,7,8 -160,162,161 -133,137,127 -160,161,61 -27,164,163 -164,30,163 -163,26,27 -165,162,160 -129,114,122 -162,165,166 -57,162,166 -84,14,15 -109,130,73 -149,150,152 -152,150,9 -0,1,167 -151,149,157 -167,1,120 -155,153,156 -95,148,168 -168,148,151 -147,158,138 -138,158,156 -169,158,147 -2,120,1 -169,157,158 -83,81,170 -171,169,172 -170,173,83 -174,176,175 -174,175,177 -141,176,178 -141,178,65 -168,179,119 -168,119,94 -118,179,180 -118,180,177 -179,118,119 -78,83,173 -78,79,83 -98,91,89 -98,89,88 -86,98,88 -0,167,68 -165,181,166 -166,181,53 -182,183,77 -182,77,37 -116,183,184 -185,186,24 -187,185,188 -188,185,25 -183,116,77 -116,79,77 -112,100,144 -125,100,112 -125,112,76 -189,190,34 -191,186,185 -163,188,26 -191,192,186 -191,36,192 -92,91,145 -186,173,23 -189,187,188 -163,193,190 -190,193,33 -145,91,121 -92,145,47 -163,189,188 -194,181,63 -163,190,189 -138,156,135 -178,196,195 -178,195,64 -31,32,193 -31,193,30 -29,30,164 -29,164,28 -135,156,134 -144,100,49 -184,106,116 -172,169,197 -198,130,109 -167,142,68 -12,130,198 -96,86,84 -96,84,15 -16,96,15 -191,185,187 -25,185,24 -198,109,134 -134,199,198 -11,198,199 -66,141,65 -97,133,98 -133,121,98 -192,173,186 -173,192,78 -24,186,23 -192,37,78 -26,188,25 -2,200,119 -34,190,33 -173,170,23 -200,93,94 -170,22,23 -200,2,3 -82,87,108 -115,108,87 -64,195,63 -194,195,196 -153,154,199 -199,134,153 -195,194,63 -199,154,10 -110,73,76 -112,110,76 -172,197,201 -197,52,201 -189,34,35 -142,202,175 -94,119,200 -196,201,194 -201,196,172 -197,169,147 -180,151,171 -177,180,171 -151,180,179 -81,21,22 -81,22,170 -5,93,4 -93,200,3 -37,192,36 -78,37,77 -93,3,4 -191,35,36 -196,178,176 -65,178,64 -123,128,124 -177,175,118 -142,175,176 -126,124,128 -128,122,126 -187,35,191 -35,187,189 -194,201,181 -53,181,201 -201,52,53 -184,40,104 -120,202,167 -141,142,176 -104,106,184 -40,183,39 -40,184,183 -72,108,115 -83,79,87 -165,62,63 -72,115,106 -52,197,147 -160,62,165 -62,160,61 -174,177,172 -70,72,106 -63,181,165 -174,196,176 -196,174,172 -162,57,58 -109,110,135 -113,135,110 -118,175,202 -60,161,59 -161,162,58 -161,58,59 -51,139,50 -82,20,21 -127,131,133 -6,93,5 -202,142,167 -127,137,129 -150,8,9 -48,92,47 -101,19,20 -104,41,105 -80,43,44 -61,161,60 -12,198,11 -166,53,54 -136,17,143 -57,166,54 -42,105,41 -11,199,10 -148,159,150 -10,154,9 -132,46,145 -182,37,38 -182,39,183 -39,182,38 -118,202,120 -140,45,46 -157,149,155 -33,193,32 -30,193,163 -159,148,95 -159,8,150 -143,17,18 -146,18,102 -146,129,137 -50,144,49 -145,46,47 -125,90,100 -164,27,28 -45,123,44 -90,125,89 -88,89,125 -48,99,92 -117,13,14 -19,102,18 -130,12,13 -90,99,100 -43,69,42 -172,177,171 -50,111,144 -75,13,117 -99,90,92 -171,151,157 -156,153,134 -44,124,80 -157,169,171 -126,107,72 -52,147,51 -168,94,95 -168,151,179 -152,155,149 -152,9,154 +69,70,71 +70,72,71 +73,74,75 +56,55,57 +73,76,74 +55,54,57 +77,78,79 +80,69,71 +81,82,83 +81,21,82 +80,43,69 +84,85,86 +87,83,82 +85,88,86 +89,90,91 +90,92,91 +6,93,94 +94,95,6 +96,86,97 +86,98,97 +49,99,100 +101,102,103 +104,41,40 +101,19,102 +105,104,70 +104,106,70 +69,105,70 +69,42,105 +74,88,85 +49,48,99 +107,108,103 +107,72,108 +6,95,7 +109,110,73 +111,112,113 +112,110,113 +107,103,114 +115,106,116 +75,74,117 +74,76,88 +118,119,120 +119,2,120 +115,79,87 +121,98,91 +122,107,114 +123,44,124 +74,85,117 +76,125,88 +122,126,107 +114,103,102 +122,127,128 +122,129,127 +116,79,115 +108,82,101 +73,75,130 +75,13,130 +131,121,132 +131,133,121 +101,82,20 +134,135,109 +136,97,137 +97,133,137 +80,71,124 +71,126,124 +103,108,101 +136,96,97 +113,138,139 +113,135,138 +136,16,96 +140,123,128 +140,45,123 +17,16,136 +66,141,67 +142,67,141 +140,128,131 +128,127,131 +71,72,126 +136,137,143 +144,112,111 +132,121,145 +146,114,102 +146,129,114 +147,139,138 +147,51,139 +67,142,68 +84,117,85 +139,111,113 +139,50,111 +148,149,150 +148,151,149 +131,132,140 +132,46,140 +137,146,143 +146,18,143 +152,153,154 +152,155,153 +84,14,117 +156,157,158 +156,155,157 +95,159,7 +159,8,7 +160,161,162 +133,127,137 +160,61,161 +27,163,164 +164,163,30 +163,27,26 +165,160,162 +129,122,114 +162,166,165 +57,166,162 +84,15,14 +109,73,130 +149,152,150 +152,9,150 +0,167,1 +151,157,149 +167,120,1 +155,156,153 +95,168,148 +168,151,148 +147,138,158 +138,156,158 +169,147,158 +2,1,120 +169,158,157 +83,170,81 +171,172,169 +170,83,173 +174,175,176 +174,177,175 +141,178,176 +141,65,178 +168,119,179 +168,94,119 +118,180,179 +118,177,180 +179,119,118 +78,173,83 +78,83,79 +98,89,91 +98,88,89 +86,88,98 +0,68,167 +165,166,181 +166,53,181 +182,77,183 +182,37,77 +116,184,183 +185,24,186 +187,188,185 +188,25,185 +183,77,116 +116,77,79 +112,144,100 +125,112,100 +125,76,112 +189,34,190 +191,185,186 +163,26,188 +191,186,192 +191,192,36 +92,145,91 +186,23,173 +189,188,187 +163,190,193 +190,33,193 +145,121,91 +92,47,145 +163,188,189 +194,63,181 +163,189,190 +138,135,156 +178,195,196 +178,64,195 +31,193,32 +31,30,193 +29,164,30 +29,28,164 +135,134,156 +144,49,100 +184,116,106 +172,197,169 +198,109,130 +167,68,142 +12,198,130 +96,84,86 +96,15,84 +16,15,96 +191,187,185 +25,24,185 +198,134,109 +134,198,199 +11,199,198 +66,65,141 +97,98,133 +133,98,121 +192,186,173 +173,78,192 +24,23,186 +192,78,37 +26,25,188 +2,119,200 +34,33,190 +173,23,170 +200,94,93 +170,23,22 +200,3,2 +82,108,87 +115,87,108 +64,63,195 +194,196,195 +153,199,154 +199,153,134 +195,63,194 +199,10,154 +110,76,73 +112,76,110 +172,201,197 +197,201,52 +189,35,34 +142,175,202 +94,200,119 +196,194,201 +201,172,196 +197,147,169 +180,171,151 +177,171,180 +151,179,180 +81,22,21 +81,170,22 +5,4,93 +93,3,200 +37,36,192 +78,77,37 +93,4,3 +191,36,35 +196,176,178 +65,64,178 +123,124,128 +177,118,175 +142,176,175 +126,128,124 +128,126,122 +187,191,35 +35,189,187 +194,181,201 +53,201,181 +201,53,52 +184,104,40 +120,167,202 +141,176,142 +104,184,106 +40,39,183 +40,183,184 +72,115,108 +83,87,79 +165,63,62 +72,106,115 +52,147,197 +160,165,62 +62,61,160 +174,172,177 +70,106,72 +63,165,181 +174,176,196 +196,172,174 +162,58,57 +109,135,110 +113,110,135 +118,202,175 +60,59,161 +161,58,162 +161,59,58 +51,50,139 +82,21,20 +127,133,131 +6,5,93 +202,167,142 +127,129,137 +150,9,8 +48,47,92 +101,20,19 +104,105,41 +80,44,43 +61,60,161 +12,11,198 +166,54,53 +136,143,17 +57,54,166 +42,41,105 +11,10,199 +148,150,159 +10,9,154 +132,145,46 +182,38,37 +182,183,39 +39,38,182 +118,120,202 +140,46,45 +157,155,149 +33,32,193 +30,163,193 +159,95,148 +159,150,8 +143,18,17 +146,102,18 +146,137,129 +50,49,144 +145,47,46 +125,100,90 +164,28,27 +45,44,123 +90,89,125 +88,125,89 +48,92,99 +117,14,13 +19,18,102 +130,13,12 +90,100,99 +43,42,69 +172,171,177 +50,144,111 +75,117,13 +99,92,90 +171,157,151 +156,134,153 +44,80,124 +157,171,169 +126,72,107 +52,51,147 +168,95,94 +168,179,151 +152,149,155 +152,154,9 diff --git a/data/rabbits/feebs_tri6/connectivity.csv b/data/rabbits/feebs_tri6/connectivity.csv index d58c4f9e..a523c94e 100644 --- a/data/rabbits/feebs_tri6/connectivity.csv +++ b/data/rabbits/feebs_tri6/connectivity.csv @@ -1,335 +1,335 @@ -138,140,139,272,273,274 -139,140,141,273,275,276 -142,144,143,277,278,279 -56,57,55,125,280,124 -142,143,145,279,281,282 -55,57,54,280,283,123 -146,148,147,284,285,286 -149,140,138,287,272,288 -150,152,151,289,290,291 -150,151,21,291,292,293 -149,138,43,288,294,295 -153,155,154,296,297,298 -156,151,152,299,290,300 -154,155,157,297,301,302 -158,160,159,303,304,305 -159,160,161,304,306,307 -6,163,162,308,309,310 -163,6,164,308,311,312 -165,166,155,313,314,315 -155,166,167,314,316,317 -49,169,168,318,319,320 -170,172,171,321,322,323 -173,40,41,324,109,325 -170,171,19,323,326,327 -174,139,173,328,329,330 -173,139,175,329,331,332 -138,139,174,274,328,333 -138,174,42,333,334,335 -143,154,157,336,302,337 -49,168,48,320,338,117 -176,172,177,339,340,341 -176,177,141,341,342,343 -6,7,164,75,344,311 -178,142,179,345,346,347 -180,182,181,348,349,350 -181,182,179,349,351,352 -176,183,172,353,354,339 -184,185,175,355,356,357 -144,186,143,358,359,278 -143,157,145,337,360,281 -187,189,188,361,362,363 -188,189,2,362,364,365 -184,156,148,366,367,368 -190,160,167,369,370,371 -191,183,176,372,353,373 -192,193,44,374,375,376 -143,186,154,359,377,336 -145,157,194,360,378,379 -191,176,195,373,380,381 -183,171,172,382,322,354 -191,197,196,383,384,385 -191,196,198,385,386,387 -185,184,148,355,368,388 -177,170,151,389,390,391 -142,199,144,392,393,277 -144,199,13,393,394,395 -200,201,190,396,397,398 -200,190,202,398,399,400 -170,20,151,401,402,390 -203,178,204,403,404,405 -205,206,166,406,407,408 -166,206,202,407,409,410 -149,193,140,411,412,287 -140,193,195,412,413,414 -172,170,177,321,389,340 -205,166,165,408,313,415 -182,208,207,416,417,418 -182,207,204,418,419,420 -205,165,16,415,421,422 -209,197,192,423,424,425 -209,192,45,425,426,427 -17,205,16,428,422,85 -66,67,210,135,429,430 -211,210,67,431,429,432 -209,200,197,433,434,423 -197,200,196,434,435,384 -140,195,141,414,436,275 -205,212,206,437,438,406 -213,180,181,439,350,440 -201,214,190,441,442,397 -215,171,183,443,382,444 -215,183,198,444,445,446 -216,207,208,447,417,448 -216,208,51,448,449,450 -67,68,211,136,451,432 -153,154,186,298,377,452 -208,182,180,416,348,453 -208,180,50,453,454,455 -217,219,218,456,457,458 -217,218,220,458,459,460 -200,209,201,433,461,396 -201,209,46,461,462,463 -206,212,215,438,464,465 -215,212,18,464,466,467 -221,223,222,468,469,470 -221,222,224,470,471,472 -153,186,14,452,473,474 -225,227,226,475,476,477 -225,226,224,477,478,479 -164,7,228,344,480,481 -228,7,8,480,76,482 -229,231,230,483,484,485 -202,206,196,409,486,487 -229,230,61,485,488,489 -27,233,232,490,491,492 -233,30,232,493,494,491 -232,26,27,495,95,492 -234,231,229,496,483,497 -198,183,191,445,372,387 -231,234,235,496,498,499 -57,231,235,500,499,501 -153,14,15,474,83,502 -178,199,142,503,392,345 -218,219,221,457,504,505 -221,219,9,504,506,507 -0,1,236,69,508,509 -220,218,226,459,510,511 -236,1,189,508,512,513 -224,222,225,471,514,479 -164,217,237,515,516,517 -237,217,220,516,460,518 -216,227,207,519,520,447 -207,227,225,520,475,521 -238,227,216,522,519,523 -2,189,1,364,512,70 -238,226,227,524,476,522 -152,150,239,289,525,526 -240,238,241,527,528,529 -239,242,152,530,531,526 -243,245,244,532,533,534 -243,244,246,534,535,536 -210,245,247,537,538,539 -210,247,65,539,540,541 -237,248,188,542,543,544 -237,188,163,544,545,546 -187,248,249,547,548,549 -187,249,246,549,550,551 -248,187,188,547,363,543 -147,152,242,552,531,553 -147,148,152,285,554,552 -167,160,158,370,303,555 -167,158,157,555,556,557 -155,167,157,317,557,301 -0,236,68,509,558,137 -234,250,235,559,560,498 -235,250,53,560,561,562 -251,252,146,563,564,565 -251,146,37,565,566,567 -185,252,253,568,569,570 -254,255,24,571,572,573 -256,254,257,574,575,576 -257,254,25,575,577,578 -252,185,146,568,579,564 -185,148,146,388,284,579 -181,169,213,580,581,440 -194,169,181,582,580,583 -194,181,145,583,584,379 -258,259,34,585,586,587 -260,255,254,588,571,589 -232,257,26,590,591,495 -260,261,255,592,593,588 -260,36,261,594,595,592 -161,160,214,306,596,597 -255,242,23,598,599,600 -258,256,257,601,576,602 -232,262,259,603,604,605 -259,262,33,604,606,607 -214,160,190,596,369,442 -161,214,47,597,608,609 -232,258,257,610,602,590 -263,250,63,611,612,613 -232,259,258,605,585,610 -207,225,204,521,614,419 -247,265,264,615,616,617 -247,264,64,617,618,619 -31,32,262,100,620,621 -31,262,30,621,622,99 -29,30,233,98,493,623 -29,233,28,623,624,97 -204,225,203,614,625,405 -213,169,49,581,318,626 -253,175,185,627,356,570 -241,238,266,528,628,629 -267,199,178,630,503,631 -236,211,68,632,451,558 -12,199,267,633,630,634 -165,155,153,315,296,635 -165,153,15,635,502,636 -16,165,15,421,636,84 -260,254,256,589,574,637 -25,254,24,577,573,93 -267,178,203,631,403,638 -203,268,267,639,640,638 -11,267,268,641,640,642 -66,210,65,430,541,134 -166,202,167,410,643,316 -202,190,167,399,371,643 -261,242,255,644,598,593 -242,261,147,644,645,553 -24,255,23,572,600,92 -261,37,147,646,647,645 -26,257,25,591,578,94 -2,269,188,648,649,365 -34,259,33,586,607,102 -242,239,23,530,650,599 -269,162,163,651,309,652 -239,22,23,653,91,650 -269,2,3,648,71,654 -151,156,177,299,655,391 -184,177,156,656,655,366 -64,264,63,618,657,132 -263,264,265,658,616,659 -222,223,268,469,660,661 -268,203,222,639,662,661 -264,263,63,658,613,657 -268,223,10,660,663,664 -179,142,145,346,282,665 -181,179,145,352,665,584 -241,266,270,629,666,667 -266,52,270,668,669,666 -258,34,35,587,103,670 -211,271,244,671,672,673 -163,188,269,545,649,652 -265,270,263,674,675,659 -270,265,241,674,676,667 -266,238,216,628,523,677 -249,220,240,678,679,680 -246,249,240,550,680,681 -220,249,248,678,548,682 -150,21,22,293,90,683 -150,22,239,683,653,525 -5,162,4,684,685,73 -162,269,3,651,654,686 -37,261,36,646,595,105 -147,37,146,647,566,286 -162,3,4,686,72,685 -260,35,36,687,104,594 -265,247,245,615,538,688 -65,247,64,540,619,133 -192,197,193,424,689,374 -246,244,187,535,690,551 -211,244,245,673,533,691 -195,193,197,413,689,692 -197,191,195,383,381,692 -256,35,260,693,687,637 -35,256,258,693,601,670 -263,270,250,675,694,611 -53,250,270,561,694,695 -270,52,53,669,121,695 -253,40,173,696,324,697 -189,271,236,698,699,513 -210,211,245,431,691,537 -173,175,253,332,627,697 -40,252,39,700,701,108 -40,253,252,696,569,700 -141,177,184,342,656,702 -152,148,156,554,367,300 -234,62,63,703,131,704 -141,184,175,702,357,705 -52,266,216,668,677,706 -229,62,234,707,703,497 -62,229,61,707,489,130 -243,246,241,536,708,709 -139,141,175,276,705,331 -63,250,234,612,559,704 -243,265,245,710,688,532 -265,243,241,710,709,676 -231,57,58,500,126,711 -178,179,204,347,712,404 -182,204,179,420,712,351 -187,244,271,690,672,713 -60,230,59,714,715,128 -230,231,58,484,711,716 -230,58,59,716,127,715 -51,208,50,449,455,119 -151,20,21,402,89,292 -196,200,202,435,400,487 -6,162,5,310,684,74 -271,211,236,671,632,699 -196,206,198,486,717,386 -219,8,9,718,77,506 -48,161,47,719,609,116 -170,19,20,327,88,401 -173,41,174,325,720,330 -149,43,44,295,112,721 -61,230,60,488,714,129 -12,267,11,634,641,80 -235,53,54,562,122,722 -205,17,212,428,723,437 -57,235,54,501,722,283 -42,174,41,334,720,110 -11,268,10,642,664,79 -217,228,219,724,725,456 -10,223,9,663,726,78 -201,46,214,463,727,441 -251,37,38,567,106,728 -251,39,252,729,701,563 -39,251,38,729,728,107 -187,271,189,713,698,361 -209,45,46,427,114,462 -226,218,224,510,730,478 -33,262,32,606,620,101 -30,262,232,622,603,494 -228,217,164,724,515,481 -228,8,219,482,718,725 -212,17,18,723,86,466 -215,18,171,467,731,443 -215,198,206,446,717,465 -50,213,49,732,626,118 -214,46,47,727,115,608 -194,159,169,733,734,582 -233,27,28,490,96,624 -45,192,44,426,376,113 -159,194,158,733,735,305 -157,158,194,556,735,378 -48,168,161,338,736,719 -186,13,14,737,82,473 -19,171,18,326,731,87 -199,12,13,633,81,394 -159,168,169,738,319,734 -43,138,42,294,335,111 -241,246,240,708,681,529 -50,180,213,454,439,732 -144,13,186,395,737,358 -168,159,161,738,307,736 -240,220,226,679,511,739 -225,222,203,514,662,625 -44,193,149,375,411,721 -226,238,240,524,527,739 -195,176,141,380,343,436 -52,216,51,706,450,120 -237,163,164,546,312,517 -237,220,248,518,682,542 -221,224,218,472,730,505 -221,9,223,507,726,468 +138,139,140,274,273,272 +139,141,140,276,275,273 +142,143,144,279,278,277 +56,55,57,124,280,125 +142,145,143,282,281,279 +55,54,57,123,283,280 +146,147,148,286,285,284 +149,138,140,288,272,287 +150,151,152,291,290,289 +150,21,151,293,292,291 +149,43,138,295,294,288 +153,154,155,298,297,296 +156,152,151,300,290,299 +154,157,155,302,301,297 +158,159,160,305,304,303 +159,161,160,307,306,304 +6,162,163,310,309,308 +163,164,6,312,311,308 +165,155,166,315,314,313 +155,167,166,317,316,314 +49,168,169,320,319,318 +170,171,172,323,322,321 +173,41,40,325,109,324 +170,19,171,327,326,323 +174,173,139,330,329,328 +173,175,139,332,331,329 +138,174,139,333,328,274 +138,42,174,335,334,333 +143,157,154,337,302,336 +49,48,168,117,338,320 +176,177,172,341,340,339 +176,141,177,343,342,341 +6,164,7,311,344,75 +178,179,142,347,346,345 +180,181,182,350,349,348 +181,179,182,352,351,349 +176,172,183,339,354,353 +184,175,185,357,356,355 +144,143,186,278,359,358 +143,145,157,281,360,337 +187,188,189,363,362,361 +188,2,189,365,364,362 +184,148,156,368,367,366 +190,167,160,371,370,369 +191,176,183,373,353,372 +192,44,193,376,375,374 +143,154,186,336,377,359 +145,194,157,379,378,360 +191,195,176,381,380,373 +183,172,171,354,322,382 +191,196,197,385,384,383 +191,198,196,387,386,385 +185,148,184,388,368,355 +177,151,170,391,390,389 +142,144,199,277,393,392 +144,13,199,395,394,393 +200,190,201,398,397,396 +200,202,190,400,399,398 +170,151,20,390,402,401 +203,204,178,405,404,403 +205,166,206,408,407,406 +166,202,206,410,409,407 +149,140,193,287,412,411 +140,195,193,414,413,412 +172,177,170,340,389,321 +205,165,166,415,313,408 +182,207,208,418,417,416 +182,204,207,420,419,418 +205,16,165,422,421,415 +209,192,197,425,424,423 +209,45,192,427,426,425 +17,16,205,85,422,428 +66,210,67,430,429,135 +211,67,210,432,429,431 +209,197,200,423,434,433 +197,196,200,384,435,434 +140,141,195,275,436,414 +205,206,212,406,438,437 +213,181,180,440,350,439 +201,190,214,397,442,441 +215,183,171,444,382,443 +215,198,183,446,445,444 +216,208,207,448,417,447 +216,51,208,450,449,448 +67,211,68,432,451,136 +153,186,154,452,377,298 +208,180,182,453,348,416 +208,50,180,455,454,453 +217,218,219,458,457,456 +217,220,218,460,459,458 +200,201,209,396,461,433 +201,46,209,463,462,461 +206,215,212,465,464,438 +215,18,212,467,466,464 +221,222,223,470,469,468 +221,224,222,472,471,470 +153,14,186,474,473,452 +225,226,227,477,476,475 +225,224,226,479,478,477 +164,228,7,481,480,344 +228,8,7,482,76,480 +229,230,231,485,484,483 +202,196,206,487,486,409 +229,61,230,489,488,485 +27,232,233,492,491,490 +233,232,30,491,494,493 +232,27,26,492,95,495 +234,229,231,497,483,496 +198,191,183,387,372,445 +231,235,234,499,498,496 +57,235,231,501,499,500 +153,15,14,502,83,474 +178,142,199,345,392,503 +218,221,219,505,504,457 +221,9,219,507,506,504 +0,236,1,509,508,69 +220,226,218,511,510,459 +236,189,1,513,512,508 +224,225,222,479,514,471 +164,237,217,517,516,515 +237,220,217,518,460,516 +216,207,227,447,520,519 +207,225,227,521,475,520 +238,216,227,523,519,522 +2,1,189,70,512,364 +238,227,226,522,476,524 +152,239,150,526,525,289 +240,241,238,529,528,527 +239,152,242,526,531,530 +243,244,245,534,533,532 +243,246,244,536,535,534 +210,247,245,539,538,537 +210,65,247,541,540,539 +237,188,248,544,543,542 +237,163,188,546,545,544 +187,249,248,549,548,547 +187,246,249,551,550,549 +248,188,187,543,363,547 +147,242,152,553,531,552 +147,152,148,552,554,285 +167,158,160,555,303,370 +167,157,158,557,556,555 +155,157,167,301,557,317 +0,68,236,137,558,509 +234,235,250,498,560,559 +235,53,250,562,561,560 +251,146,252,565,564,563 +251,37,146,567,566,565 +185,253,252,570,569,568 +254,24,255,573,572,571 +256,257,254,576,575,574 +257,25,254,578,577,575 +252,146,185,564,579,568 +185,146,148,579,284,388 +181,213,169,440,581,580 +194,181,169,583,580,582 +194,145,181,379,584,583 +258,34,259,587,586,585 +260,254,255,589,571,588 +232,26,257,495,591,590 +260,255,261,588,593,592 +260,261,36,592,595,594 +161,214,160,597,596,306 +255,23,242,600,599,598 +258,257,256,602,576,601 +232,259,262,605,604,603 +259,33,262,607,606,604 +214,190,160,442,369,596 +161,47,214,609,608,597 +232,257,258,590,602,610 +263,63,250,613,612,611 +232,258,259,610,585,605 +207,204,225,419,614,521 +247,264,265,617,616,615 +247,64,264,619,618,617 +31,262,32,621,620,100 +31,30,262,99,622,621 +29,233,30,623,493,98 +29,28,233,97,624,623 +204,203,225,405,625,614 +213,49,169,626,318,581 +253,185,175,570,356,627 +241,266,238,629,628,528 +267,178,199,631,503,630 +236,68,211,558,451,632 +12,267,199,634,630,633 +165,153,155,635,296,315 +165,15,153,636,502,635 +16,15,165,84,636,421 +260,256,254,637,574,589 +25,24,254,93,573,577 +267,203,178,638,403,631 +203,267,268,638,640,639 +11,268,267,642,640,641 +66,65,210,134,541,430 +166,167,202,316,643,410 +202,167,190,643,371,399 +261,255,242,593,598,644 +242,147,261,553,645,644 +24,23,255,92,600,572 +261,147,37,645,647,646 +26,25,257,94,578,591 +2,188,269,365,649,648 +34,33,259,102,607,586 +242,23,239,599,650,530 +269,163,162,652,309,651 +239,23,22,650,91,653 +269,3,2,654,71,648 +151,177,156,391,655,299 +184,156,177,366,655,656 +64,63,264,132,657,618 +263,265,264,659,616,658 +222,268,223,661,660,469 +268,222,203,661,662,639 +264,63,263,657,613,658 +268,10,223,664,663,660 +179,145,142,665,282,346 +181,145,179,584,665,352 +241,270,266,667,666,629 +266,270,52,666,669,668 +258,35,34,670,103,587 +211,244,271,673,672,671 +163,269,188,652,649,545 +265,263,270,659,675,674 +270,241,265,667,676,674 +266,216,238,677,523,628 +249,240,220,680,679,678 +246,240,249,681,680,550 +220,248,249,682,548,678 +150,22,21,683,90,293 +150,239,22,525,653,683 +5,4,162,73,685,684 +162,3,269,686,654,651 +37,36,261,105,595,646 +147,146,37,286,566,647 +162,4,3,685,72,686 +260,36,35,594,104,687 +265,245,247,688,538,615 +65,64,247,133,619,540 +192,193,197,374,689,424 +246,187,244,551,690,535 +211,245,244,691,533,673 +195,197,193,692,689,413 +197,195,191,692,381,383 +256,260,35,637,687,693 +35,258,256,670,601,693 +263,250,270,611,694,675 +53,270,250,695,694,561 +270,53,52,695,121,669 +253,173,40,697,324,696 +189,236,271,513,699,698 +210,245,211,537,691,431 +173,253,175,697,627,332 +40,39,252,108,701,700 +40,252,253,700,569,696 +141,184,177,702,656,342 +152,156,148,300,367,554 +234,63,62,704,131,703 +141,175,184,705,357,702 +52,216,266,706,677,668 +229,234,62,497,703,707 +62,61,229,130,489,707 +243,241,246,709,708,536 +139,175,141,331,705,276 +63,234,250,704,559,612 +243,245,265,532,688,710 +265,241,243,676,709,710 +231,58,57,711,126,500 +178,204,179,404,712,347 +182,179,204,351,712,420 +187,271,244,713,672,690 +60,59,230,128,715,714 +230,58,231,716,711,484 +230,59,58,715,127,716 +51,50,208,119,455,449 +151,21,20,292,89,402 +196,202,200,487,400,435 +6,5,162,74,684,310 +271,236,211,699,632,671 +196,198,206,386,717,486 +219,9,8,506,77,718 +48,47,161,116,609,719 +170,20,19,401,88,327 +173,174,41,330,720,325 +149,44,43,721,112,295 +61,60,230,129,714,488 +12,11,267,80,641,634 +235,54,53,722,122,562 +205,212,17,437,723,428 +57,54,235,283,722,501 +42,41,174,110,720,334 +11,10,268,79,664,642 +217,219,228,456,725,724 +10,9,223,78,726,663 +201,214,46,441,727,463 +251,38,37,728,106,567 +251,252,39,563,701,729 +39,38,251,107,728,729 +187,189,271,361,698,713 +209,46,45,462,114,427 +226,224,218,478,730,510 +33,32,262,101,620,606 +30,232,262,494,603,622 +228,164,217,481,515,724 +228,219,8,725,718,482 +212,18,17,466,86,723 +215,171,18,443,731,467 +215,206,198,465,717,446 +50,49,213,118,626,732 +214,47,46,608,115,727 +194,169,159,582,734,733 +233,28,27,624,96,490 +45,44,192,113,376,426 +159,158,194,305,735,733 +157,194,158,378,735,556 +48,161,168,719,736,338 +186,14,13,473,82,737 +19,18,171,87,731,326 +199,13,12,394,81,633 +159,169,168,734,319,738 +43,42,138,111,335,294 +241,240,246,529,681,708 +50,213,180,732,439,454 +144,186,13,358,737,395 +168,161,159,736,307,738 +240,226,220,739,511,679 +225,203,222,625,662,514 +44,149,193,721,411,375 +226,240,238,739,527,524 +195,141,176,436,343,380 +52,51,216,120,450,706 +237,164,163,517,312,546 +237,248,220,542,682,518 +221,218,224,505,730,472 +221,223,9,468,726,507 diff --git a/data/rabbits/riley_quad4/connectivity.csv b/data/rabbits/riley_quad4/connectivity.csv index 3dcfca1c..b21b0466 100644 --- a/data/rabbits/riley_quad4/connectivity.csv +++ b/data/rabbits/riley_quad4/connectivity.csv @@ -1,196 +1,196 @@ -139,35,36,105 -194,224,233,232 -178,234,229,219 -92,101,188,135 -160,179,208,93 -108,154,178,137 -100,121,113,68 -102,3,4,108 -237,225,236,210 -163,37,38,106 -107,124,73,74 -105,163,43,44 -177,134,109,91 -94,141,215,174 -192,213,204,175 -192,175,103,162 -171,214,173,133 -170,205,237,226 -91,143,218,177 -134,167,152,109 -100,70,107,122 -34,117,32,33 -226,207,201,170 -135,180,140,92 -224,227,199,233 -168,217,156,95 -181,146,110,129 -119,31,32,117 -151,186,136,47 -47,48,49,151 -101,92,80,81 -91,109,26,27 -201,197,130,181 -154,220,234,178 -93,208,218,143 -182,202,200,128 -197,201,207,228 -183,173,214,196 -144,129,110,83 -88,150,21,22 -210,182,111,145 -169,115,147,206 -213,167,134,204 -206,230,228,169 -70,100,68,69 -188,219,229,135 -140,189,142,97 -142,95,66,67 -208,136,186,218 -172,149,85,211 -117,99,160,119 -133,173,150,88 -203,197,228,230 -41,120,39,40 -177,218,186,174 -131,98,149,172 -49,50,94,151 -8,126,6,7 -121,97,142,113 -196,205,144,183 -164,148,114,184 -84,16,17,146 -83,110,18,19 -97,121,77,78 -224,211,164,227 -182,128,82,111 -126,8,9,104 -66,161,89,65 -46,47,136,96 -107,74,75,122 -184,203,227,164 -229,234,223,235 -140,79,80,92 -185,210,145,116 -153,57,58,82 -157,55,56,118 -141,94,50,51 -173,183,112,150 -29,119,160,93 -220,191,131,212 -181,129,170,201 -232,221,156,217 -154,108,4,5 -123,10,11,98 -158,101,81,0 -115,87,62,63 -212,194,232,223 -122,75,76,159 -117,34,35,99 -212,131,172,194 -115,169,132,87 -140,97,78,79 -121,159,76,77 -227,203,230,231 -98,11,12,149 -211,85,148,164 -85,13,14,148 -228,207,132,169 -66,95,156,161 -149,12,13,85 -139,105,44,45 -152,25,26,109 -43,163,106,42 -174,186,151,94 -230,206,166,231 -84,130,184,114 -203,184,130,197 -148,14,15,114 -89,166,206,147 -65,89,147,64 -198,176,128,200 -147,115,63,64 -118,56,57,153 -153,82,128,176 -83,112,183,144 -226,185,132,207 -111,82,58,59 -116,61,62,87 -87,132,185,116 -146,17,18,110 -129,144,205,170 -237,210,185,226 -145,111,59,60 -112,83,19,20 -114,15,16,84 -145,60,61,116 -86,54,55,157 -210,236,202,182 -150,112,20,21 -138,104,123,191 -0,1,102,158 -215,141,103,175 -195,189,140,180 -54,86,162,53 -152,90,24,25 -146,181,130,84 -104,9,10,123 -213,209,187,167 -155,187,209,216 -236,225,196,214 -106,38,39,120 -208,179,96,136 -211,224,194,172 -191,123,98,131 -158,102,108,137 -227,231,166,199 -177,222,204,134 -205,196,225,237 -127,88,22,23 -1,2,3,102 -167,187,90,152 -155,133,88,127 -90,127,23,24 -141,51,52,103 -189,195,217,168 -45,46,96,139 -139,96,179,190 -216,171,133,155 -187,155,127,90 -31,119,29,30 -188,137,178,219 -191,220,154,138 -95,142,189,168 -124,107,70,71 -229,235,217,195 -53,162,103,52 -29,93,143,28 -166,89,161,193 -143,91,27,28 -162,86,157,192 -118,165,192,157 -42,106,120,41 -135,229,195,180 -121,100,122,159 -5,125,138,154 -105,36,37,163 -6,126,125,5 -137,188,101,158 -232,233,199,221 -223,234,220,212 -221,199,166,193 -142,67,68,113 -221,193,161,156 -73,124,71,72 -175,204,222,215 -217,235,223,232 -99,190,179,160 -138,125,126,104 -99,35,139,190 -153,176,165,118 -192,165,176,198 -236,214,171,202 -215,222,177,174 -209,213,192,198 -200,216,209,198 -202,171,216,200 +139,105,36,35 +194,232,233,224 +178,219,229,234 +92,135,188,101 +160,93,208,179 +108,137,178,154 +100,68,113,121 +102,108,4,3 +237,210,236,225 +163,106,38,37 +107,74,73,124 +105,44,43,163 +177,91,109,134 +94,174,215,141 +192,175,204,213 +192,162,103,175 +171,133,173,214 +170,226,237,205 +91,177,218,143 +134,109,152,167 +100,122,107,70 +34,33,32,117 +226,170,201,207 +135,92,140,180 +224,233,199,227 +168,95,156,217 +181,129,110,146 +119,117,32,31 +151,47,136,186 +47,151,49,48 +101,81,80,92 +91,27,26,109 +201,181,130,197 +154,178,234,220 +93,143,218,208 +182,128,200,202 +197,228,207,201 +183,196,214,173 +144,83,110,129 +88,22,21,150 +210,145,111,182 +169,206,147,115 +213,204,134,167 +206,169,228,230 +70,69,68,100 +188,135,229,219 +140,97,142,189 +142,67,66,95 +208,218,186,136 +172,211,85,149 +117,119,160,99 +133,88,150,173 +203,230,228,197 +41,40,39,120 +177,174,186,218 +131,172,149,98 +49,151,94,50 +8,7,6,126 +121,113,142,97 +196,183,144,205 +164,184,114,148 +84,146,17,16 +83,19,18,110 +97,78,77,121 +224,227,164,211 +182,111,82,128 +126,104,9,8 +66,65,89,161 +46,96,136,47 +107,122,75,74 +184,164,227,203 +229,235,223,234 +140,92,80,79 +185,116,145,210 +153,82,58,57 +157,118,56,55 +141,51,50,94 +173,150,112,183 +29,93,160,119 +220,212,131,191 +181,201,170,129 +232,217,156,221 +154,5,4,108 +123,98,11,10 +158,0,81,101 +115,63,62,87 +212,223,232,194 +122,159,76,75 +117,99,35,34 +212,194,172,131 +115,87,132,169 +140,79,78,97 +121,77,76,159 +227,231,230,203 +98,149,12,11 +211,164,148,85 +85,148,14,13 +228,169,132,207 +66,161,156,95 +149,85,13,12 +139,45,44,105 +152,109,26,25 +43,42,106,163 +174,94,151,186 +230,231,166,206 +84,114,184,130 +203,197,130,184 +148,114,15,14 +89,147,206,166 +65,64,147,89 +198,200,128,176 +147,64,63,115 +118,153,57,56 +153,176,128,82 +83,144,183,112 +226,207,132,185 +111,59,58,82 +116,87,62,61 +87,116,185,132 +146,110,18,17 +129,170,205,144 +237,226,185,210 +145,60,59,111 +112,20,19,83 +114,84,16,15 +145,116,61,60 +86,157,55,54 +210,182,202,236 +150,21,20,112 +138,191,123,104 +0,158,102,1 +215,175,103,141 +195,180,140,189 +54,53,162,86 +152,25,24,90 +146,84,130,181 +104,123,10,9 +213,167,187,209 +155,216,209,187 +236,214,196,225 +106,120,39,38 +208,136,96,179 +211,172,194,224 +191,131,98,123 +158,137,108,102 +227,199,166,231 +177,134,204,222 +205,237,225,196 +127,23,22,88 +1,102,3,2 +167,152,90,187 +155,127,88,133 +90,24,23,127 +141,103,52,51 +189,168,217,195 +45,139,96,46 +139,190,179,96 +216,155,133,171 +187,90,127,155 +31,30,29,119 +188,219,178,137 +191,138,154,220 +95,168,189,142 +124,71,70,107 +229,195,217,235 +53,52,103,162 +29,28,143,93 +166,193,161,89 +143,28,27,91 +162,192,157,86 +118,157,192,165 +42,41,120,106 +135,180,195,229 +121,159,122,100 +5,154,138,125 +105,163,37,36 +6,5,125,126 +137,158,101,188 +232,221,199,233 +223,212,220,234 +221,193,166,199 +142,113,68,67 +221,156,161,193 +73,72,71,124 +175,215,222,204 +217,232,223,235 +99,160,179,190 +138,104,126,125 +99,190,139,35 +153,118,165,176 +192,198,176,165 +236,202,171,214 +215,174,177,222 +209,198,192,213 +200,198,209,216 +202,200,216,171 diff --git a/data/rabbits/riley_quad8/connectivity.csv b/data/rabbits/riley_quad8/connectivity.csv index 2ec8f1fa..190b6038 100644 --- a/data/rabbits/riley_quad8/connectivity.csv +++ b/data/rabbits/riley_quad8/connectivity.csv @@ -1,196 +1,196 @@ -221,35,36,187,320,117,321,322 -276,306,315,314,323,324,325,326 -260,316,311,301,327,328,329,330 -174,183,270,217,331,332,333,334 -242,261,290,175,335,336,337,338 -190,236,260,219,339,340,341,342 -182,203,195,68,343,344,345,346 -184,3,4,190,347,85,348,349 -319,307,318,292,350,351,352,353 -245,37,38,188,354,119,355,356 -189,206,73,74,357,358,155,359 -187,245,43,44,360,361,125,362 -259,216,191,173,363,364,365,366 -176,223,297,256,367,368,369,370 -274,295,286,257,371,372,373,374 -274,257,185,244,374,375,376,377 -253,296,255,215,378,379,380,381 -252,287,319,308,382,383,384,385 -173,225,300,259,386,387,388,366 -216,249,234,191,389,390,391,364 -182,70,189,204,392,393,394,395 -34,199,32,33,396,397,114,115 -308,289,283,252,398,399,400,385 -217,262,222,174,401,402,403,334 -306,309,281,315,404,405,406,324 -250,299,238,177,407,408,409,410 -263,228,192,211,411,412,413,414 -201,31,32,199,415,113,397,416 -233,268,218,47,417,418,419,420 -47,48,49,233,129,130,421,420 -183,174,80,81,331,422,162,423 -173,191,26,27,365,424,108,425 -283,279,212,263,426,427,428,429 -236,302,316,260,430,431,327,340 -175,290,300,225,337,432,387,433 -264,284,282,210,434,435,436,437 -279,283,289,310,426,399,438,439 -265,255,296,278,440,379,441,442 -226,211,192,165,443,413,444,445 -170,232,21,22,446,447,103,448 -292,264,193,227,449,450,451,452 -251,197,229,288,453,454,455,456 -295,249,216,286,457,389,458,372 -288,312,310,251,459,460,461,456 -70,182,68,69,392,346,150,151 -270,301,311,217,462,329,463,333 -222,271,224,179,464,465,466,467 -224,177,66,67,468,469,148,470 -290,218,268,300,471,418,472,432 -254,231,167,293,473,474,475,476 -199,181,242,201,477,478,479,416 -215,255,232,170,380,480,446,481 -285,279,310,312,482,439,460,483 -41,202,39,40,484,485,121,122 -259,300,268,256,388,472,486,487 -213,180,231,254,488,489,473,490 -49,50,176,233,131,491,492,421 -8,208,6,7,493,494,88,89 -203,179,224,195,495,466,496,344 -278,287,226,265,497,498,499,442 -246,230,196,266,500,501,502,503 -166,16,17,228,504,98,505,506 -165,192,18,19,444,507,100,508 -179,203,77,78,495,509,159,510 -306,293,246,309,511,512,513,404 -264,210,164,193,437,514,515,450 -208,8,9,186,493,90,516,517 -66,243,171,65,518,519,520,147 -46,47,218,178,128,419,521,522 -189,74,75,204,359,156,523,394 -266,285,309,246,524,525,513,503 -311,316,305,317,328,526,527,528 -222,79,80,174,529,161,422,403 -267,292,227,198,530,452,531,532 -235,57,58,164,533,139,534,535 -239,55,56,200,536,137,537,538 -223,176,50,51,367,491,132,539 -255,265,194,232,440,540,541,480 -29,201,242,175,542,479,338,543 -302,273,213,294,544,545,546,547 -263,211,252,283,414,548,400,429 -314,303,238,299,549,550,408,551 -236,190,4,5,339,348,86,552 -205,10,11,180,553,92,554,555 -240,183,81,0,556,423,163,557 -197,169,62,63,558,559,144,560 -294,276,314,305,561,326,562,563 -204,75,76,241,523,157,564,565 -199,34,35,181,396,116,566,477 -294,213,254,276,546,490,567,561 -197,251,214,169,453,568,569,558 -222,179,78,79,467,510,160,529 -203,241,76,77,570,564,158,509 -309,285,312,313,525,483,571,572 -180,11,12,231,554,93,573,489 -293,167,230,246,475,574,500,512 -167,13,14,230,575,95,576,574 -310,289,214,251,438,577,568,461 -66,177,238,243,469,409,578,518 -231,12,13,167,573,94,575,474 -221,187,44,45,322,362,126,579 -234,25,26,191,580,107,424,391 -43,245,188,42,361,356,581,124 -256,268,233,176,486,417,492,370 -312,288,248,313,459,582,583,571 -166,212,266,196,584,585,502,586 -285,266,212,279,524,585,427,482 -230,14,15,196,576,96,587,501 -171,248,288,229,588,582,455,589 -65,171,229,64,520,589,590,146 -280,258,210,282,591,592,436,593 -229,197,63,64,454,560,145,590 -200,56,57,235,537,138,533,594 -235,164,210,258,535,514,592,595 -165,194,265,226,596,540,499,445 -308,267,214,289,597,598,577,398 -193,164,58,59,515,534,140,599 -198,61,62,169,600,143,559,601 -169,214,267,198,569,598,532,601 -228,17,18,192,505,99,507,412 -211,226,287,252,443,498,382,548 -319,292,267,308,353,530,597,384 -227,193,59,60,451,599,141,602 -194,165,19,20,596,508,101,603 -196,15,16,166,587,97,504,586 -227,60,61,198,602,142,600,531 -168,54,55,239,604,136,536,605 -292,318,284,264,352,606,434,449 -232,194,20,21,541,603,102,447 -220,186,205,273,607,608,609,610 -0,1,184,240,82,611,612,557 -297,223,185,257,368,613,375,614 -277,271,222,262,615,464,402,616 -54,168,244,53,604,617,618,135 -234,172,24,25,619,620,106,580 -228,263,212,166,411,428,584,506 -186,9,10,205,516,91,553,608 -295,291,269,249,621,622,623,457 -237,269,291,298,624,622,625,626 -318,307,278,296,351,627,441,628 -188,38,39,202,355,120,485,629 -290,261,178,218,336,630,521,471 -293,306,276,254,511,323,567,476 -273,205,180,213,609,555,488,545 -240,184,190,219,612,349,342,631 -309,313,248,281,572,583,632,405 -259,304,286,216,633,634,458,363 -287,278,307,319,497,627,350,383 -209,170,22,23,635,448,104,636 -1,2,3,184,83,84,347,611 -249,269,172,234,623,637,619,390 -237,215,170,209,638,481,635,639 -172,209,23,24,640,636,105,620 -223,51,52,185,539,133,641,613 -271,277,299,250,615,642,407,643 -45,46,178,221,127,522,644,579 -221,178,261,272,644,630,645,646 -298,253,215,237,647,381,638,626 -269,237,209,172,624,639,640,637 -31,201,29,30,415,542,111,112 -270,219,260,301,648,341,330,462 -273,302,236,220,544,430,649,610 -177,224,271,250,468,465,643,410 -206,189,70,71,357,393,152,650 -311,317,299,277,528,651,642,652 -53,244,185,52,618,376,641,134 -29,175,225,28,543,433,653,110 -248,171,243,275,588,519,654,655 -225,173,27,28,386,425,109,653 -244,168,239,274,617,605,656,377 -200,247,274,239,657,658,656,538 -42,188,202,41,581,629,484,123 -217,311,277,262,463,652,616,401 -203,182,204,241,343,395,565,570 -5,207,220,236,659,660,649,552 -187,36,37,245,321,118,354,360 -6,208,207,5,494,661,659,87 -219,270,183,240,648,332,556,631 -314,315,281,303,325,406,662,549 -305,316,302,294,526,431,547,563 -303,281,248,275,662,632,655,663 -224,67,68,195,470,149,345,496 -303,275,243,238,663,654,578,550 -73,206,71,72,358,650,153,154 -257,286,304,297,373,634,664,614 -299,317,305,314,651,527,562,551 -181,272,261,242,665,645,335,478 -220,207,208,186,660,661,517,607 -181,35,221,272,566,320,646,665 -235,258,247,200,595,666,657,594 -274,247,258,280,658,666,591,667 -318,296,253,284,628,378,668,606 -297,304,259,256,664,633,487,369 -291,295,274,280,621,371,667,669 -282,298,291,280,670,625,669,593 -284,253,298,282,668,647,670,435 +221,187,36,35,322,321,117,320 +276,314,315,306,326,325,324,323 +260,301,311,316,330,329,328,327 +174,217,270,183,334,333,332,331 +242,175,290,261,338,337,336,335 +190,219,260,236,342,341,340,339 +182,68,195,203,346,345,344,343 +184,190,4,3,349,348,85,347 +319,292,318,307,353,352,351,350 +245,188,38,37,356,355,119,354 +189,74,73,206,359,155,358,357 +187,44,43,245,362,125,361,360 +259,173,191,216,366,365,364,363 +176,256,297,223,370,369,368,367 +274,257,286,295,374,373,372,371 +274,244,185,257,377,376,375,374 +253,215,255,296,381,380,379,378 +252,308,319,287,385,384,383,382 +173,259,300,225,366,388,387,386 +216,191,234,249,364,391,390,389 +182,204,189,70,395,394,393,392 +34,33,32,199,115,114,397,396 +308,252,283,289,385,400,399,398 +217,174,222,262,334,403,402,401 +306,315,281,309,324,406,405,404 +250,177,238,299,410,409,408,407 +263,211,192,228,414,413,412,411 +201,199,32,31,416,397,113,415 +233,47,218,268,420,419,418,417 +47,233,49,48,420,421,130,129 +183,81,80,174,423,162,422,331 +173,27,26,191,425,108,424,365 +283,263,212,279,429,428,427,426 +236,260,316,302,340,327,431,430 +175,225,300,290,433,387,432,337 +264,210,282,284,437,436,435,434 +279,310,289,283,439,438,399,426 +265,278,296,255,442,441,379,440 +226,165,192,211,445,444,413,443 +170,22,21,232,448,103,447,446 +292,227,193,264,452,451,450,449 +251,288,229,197,456,455,454,453 +295,286,216,249,372,458,389,457 +288,251,310,312,456,461,460,459 +70,69,68,182,151,150,346,392 +270,217,311,301,333,463,329,462 +222,179,224,271,467,466,465,464 +224,67,66,177,470,148,469,468 +290,300,268,218,432,472,418,471 +254,293,167,231,476,475,474,473 +199,201,242,181,416,479,478,477 +215,170,232,255,481,446,480,380 +285,312,310,279,483,460,439,482 +41,40,39,202,122,121,485,484 +259,256,268,300,487,486,472,388 +213,254,231,180,490,473,489,488 +49,233,176,50,421,492,491,131 +8,7,6,208,89,88,494,493 +203,195,224,179,344,496,466,495 +278,265,226,287,442,499,498,497 +246,266,196,230,503,502,501,500 +166,228,17,16,506,505,98,504 +165,19,18,192,508,100,507,444 +179,78,77,203,510,159,509,495 +306,309,246,293,404,513,512,511 +264,193,164,210,450,515,514,437 +208,186,9,8,517,516,90,493 +66,65,171,243,147,520,519,518 +46,178,218,47,522,521,419,128 +189,204,75,74,394,523,156,359 +266,246,309,285,503,513,525,524 +311,317,305,316,528,527,526,328 +222,174,80,79,403,422,161,529 +267,198,227,292,532,531,452,530 +235,164,58,57,535,534,139,533 +239,200,56,55,538,537,137,536 +223,51,50,176,539,132,491,367 +255,232,194,265,480,541,540,440 +29,175,242,201,543,338,479,542 +302,294,213,273,547,546,545,544 +263,283,252,211,429,400,548,414 +314,299,238,303,551,408,550,549 +236,5,4,190,552,86,348,339 +205,180,11,10,555,554,92,553 +240,0,81,183,557,163,423,556 +197,63,62,169,560,144,559,558 +294,305,314,276,563,562,326,561 +204,241,76,75,565,564,157,523 +199,181,35,34,477,566,116,396 +294,276,254,213,561,567,490,546 +197,169,214,251,558,569,568,453 +222,79,78,179,529,160,510,467 +203,77,76,241,509,158,564,570 +309,313,312,285,572,571,483,525 +180,231,12,11,489,573,93,554 +293,246,230,167,512,500,574,475 +167,230,14,13,574,576,95,575 +310,251,214,289,461,568,577,438 +66,243,238,177,518,578,409,469 +231,167,13,12,474,575,94,573 +221,45,44,187,579,126,362,322 +234,191,26,25,391,424,107,580 +43,42,188,245,124,581,356,361 +256,176,233,268,370,492,417,486 +312,313,248,288,571,583,582,459 +166,196,266,212,586,502,585,584 +285,279,212,266,482,427,585,524 +230,196,15,14,501,587,96,576 +171,229,288,248,589,455,582,588 +65,64,229,171,146,590,589,520 +280,282,210,258,593,436,592,591 +229,64,63,197,590,145,560,454 +200,235,57,56,594,533,138,537 +235,258,210,164,595,592,514,535 +165,226,265,194,445,499,540,596 +308,289,214,267,398,577,598,597 +193,59,58,164,599,140,534,515 +198,169,62,61,601,559,143,600 +169,198,267,214,601,532,598,569 +228,192,18,17,412,507,99,505 +211,252,287,226,548,382,498,443 +319,308,267,292,384,597,530,353 +227,60,59,193,602,141,599,451 +194,20,19,165,603,101,508,596 +196,166,16,15,586,504,97,587 +227,198,61,60,531,600,142,602 +168,239,55,54,605,536,136,604 +292,264,284,318,449,434,606,352 +232,21,20,194,447,102,603,541 +220,273,205,186,610,609,608,607 +0,240,184,1,557,612,611,82 +297,257,185,223,614,375,613,368 +277,262,222,271,616,402,464,615 +54,53,244,168,135,618,617,604 +234,25,24,172,580,106,620,619 +228,166,212,263,506,584,428,411 +186,205,10,9,608,553,91,516 +295,249,269,291,457,623,622,621 +237,298,291,269,626,625,622,624 +318,296,278,307,628,441,627,351 +188,202,39,38,629,485,120,355 +290,218,178,261,471,521,630,336 +293,254,276,306,476,567,323,511 +273,213,180,205,545,488,555,609 +240,219,190,184,631,342,349,612 +309,281,248,313,405,632,583,572 +259,216,286,304,363,458,634,633 +287,319,307,278,383,350,627,497 +209,23,22,170,636,104,448,635 +1,184,3,2,611,347,84,83 +249,234,172,269,390,619,637,623 +237,209,170,215,639,635,481,638 +172,24,23,209,620,105,636,640 +223,185,52,51,613,641,133,539 +271,250,299,277,643,407,642,615 +45,221,178,46,579,644,522,127 +221,272,261,178,646,645,630,644 +298,237,215,253,626,638,381,647 +269,172,209,237,637,640,639,624 +31,30,29,201,112,111,542,415 +270,301,260,219,462,330,341,648 +273,220,236,302,610,649,430,544 +177,250,271,224,410,643,465,468 +206,71,70,189,650,152,393,357 +311,277,299,317,652,642,651,528 +53,52,185,244,134,641,376,618 +29,28,225,175,110,653,433,543 +248,275,243,171,655,654,519,588 +225,28,27,173,653,109,425,386 +244,274,239,168,377,656,605,617 +200,239,274,247,538,656,658,657 +42,41,202,188,123,484,629,581 +217,262,277,311,401,616,652,463 +203,241,204,182,570,565,395,343 +5,236,220,207,552,649,660,659 +187,245,37,36,360,354,118,321 +6,5,207,208,87,659,661,494 +219,240,183,270,631,556,332,648 +314,303,281,315,549,662,406,325 +305,294,302,316,563,547,431,526 +303,275,248,281,663,655,632,662 +224,195,68,67,496,345,149,470 +303,238,243,275,550,578,654,663 +73,72,71,206,154,153,650,358 +257,297,304,286,614,664,634,373 +299,314,305,317,551,562,527,651 +181,242,261,272,478,335,645,665 +220,186,208,207,607,517,661,660 +181,272,221,35,665,646,320,566 +235,200,247,258,594,657,666,595 +274,280,258,247,667,591,666,658 +318,284,253,296,606,668,378,628 +297,256,259,304,369,487,633,664 +291,280,274,295,669,667,371,621 +282,280,291,298,593,669,625,670 +284,282,298,253,435,670,647,668 diff --git a/data/rabbits/riley_quad9/connectivity.csv b/data/rabbits/riley_quad9/connectivity.csv index 8b8bf923..60f3af40 100644 --- a/data/rabbits/riley_quad9/connectivity.csv +++ b/data/rabbits/riley_quad9/connectivity.csv @@ -1,196 +1,196 @@ -221,35,36,187,320,117,321,322,323 -276,306,315,314,324,325,326,327,328 -260,316,311,301,329,330,331,332,333 -174,183,270,217,334,335,336,337,338 -242,261,290,175,339,340,341,342,343 -190,236,260,219,344,345,346,347,348 -182,203,195,68,349,350,351,352,353 -184,3,4,190,354,85,355,356,357 -319,307,318,292,358,359,360,361,362 -245,37,38,188,363,119,364,365,366 -189,206,73,74,367,368,155,369,370 -187,245,43,44,371,372,125,373,374 -259,216,191,173,375,376,377,378,379 -176,223,297,256,380,381,382,383,384 -274,295,286,257,385,386,387,388,389 -274,257,185,244,388,390,391,392,393 -253,296,255,215,394,395,396,397,398 -252,287,319,308,399,400,401,402,403 -173,225,300,259,404,405,406,378,407 -216,249,234,191,408,409,410,376,411 -182,70,189,204,412,413,414,415,416 -34,199,32,33,417,418,114,115,419 -308,289,283,252,420,421,422,402,423 -217,262,222,174,424,425,426,337,427 -306,309,281,315,428,429,430,325,431 -250,299,238,177,432,433,434,435,436 -263,228,192,211,437,438,439,440,441 -201,31,32,199,442,113,418,443,444 -233,268,218,47,445,446,447,448,449 -47,48,49,233,129,130,450,448,451 -183,174,80,81,334,452,162,453,454 -173,191,26,27,377,455,108,456,457 -283,279,212,263,458,459,460,461,462 -236,302,316,260,463,464,329,345,465 -175,290,300,225,341,466,405,467,468 -264,284,282,210,469,470,471,472,473 -279,283,289,310,458,421,474,475,476 -265,255,296,278,477,395,478,479,480 -226,211,192,165,481,439,482,483,484 -170,232,21,22,485,486,103,487,488 -292,264,193,227,489,490,491,492,493 -251,197,229,288,494,495,496,497,498 -295,249,216,286,499,408,500,386,501 -288,312,310,251,502,503,504,497,505 -70,182,68,69,412,352,150,151,506 -270,301,311,217,507,331,508,336,509 -222,271,224,179,510,511,512,513,514 -224,177,66,67,515,516,148,517,518 -290,218,268,300,519,446,520,466,521 -254,231,167,293,522,523,524,525,526 -199,181,242,201,527,528,529,443,530 -215,255,232,170,396,531,485,532,533 -285,279,310,312,534,475,503,535,536 -41,202,39,40,537,538,121,122,539 -259,300,268,256,406,520,540,541,542 -213,180,231,254,543,544,522,545,546 -49,50,176,233,131,547,548,450,549 -8,208,6,7,550,551,88,89,552 -203,179,224,195,553,512,554,350,555 -278,287,226,265,556,557,558,479,559 -246,230,196,266,560,561,562,563,564 -166,16,17,228,565,98,566,567,568 -165,192,18,19,482,569,100,570,571 -179,203,77,78,553,572,159,573,574 -306,293,246,309,575,576,577,428,578 -264,210,164,193,472,579,580,490,581 -208,8,9,186,550,90,582,583,584 -66,243,171,65,585,586,587,147,588 -46,47,218,178,128,447,589,590,591 -189,74,75,204,369,156,592,414,593 -266,285,309,246,594,595,577,563,596 -311,316,305,317,330,597,598,599,600 -222,79,80,174,601,161,452,426,602 -267,292,227,198,603,492,604,605,606 -235,57,58,164,607,139,608,609,610 -239,55,56,200,611,137,612,613,614 -223,176,50,51,380,547,132,615,616 -255,265,194,232,477,617,618,531,619 -29,201,242,175,620,529,342,621,622 -302,273,213,294,623,624,625,626,627 -263,211,252,283,440,628,422,461,629 -314,303,238,299,630,631,433,632,633 -236,190,4,5,344,355,86,634,635 -205,10,11,180,636,92,637,638,639 -240,183,81,0,640,453,163,641,642 -197,169,62,63,643,644,144,645,646 -294,276,314,305,647,327,648,649,650 -204,75,76,241,592,157,651,652,653 -199,34,35,181,417,116,654,527,655 -294,213,254,276,625,545,656,647,657 -197,251,214,169,494,658,659,643,660 -222,179,78,79,513,573,160,601,661 -203,241,76,77,662,651,158,572,663 -309,285,312,313,595,535,664,665,666 -180,11,12,231,637,93,667,544,668 -293,167,230,246,524,669,560,576,670 -167,13,14,230,671,95,672,669,673 -310,289,214,251,474,674,658,504,675 -66,177,238,243,516,434,676,585,677 -231,12,13,167,667,94,671,523,678 -221,187,44,45,322,373,126,679,680 -234,25,26,191,681,107,455,410,682 -43,245,188,42,372,365,683,124,684 -256,268,233,176,540,445,548,383,685 -312,288,248,313,502,686,687,664,688 -166,212,266,196,689,690,562,691,692 -285,266,212,279,594,690,459,534,693 -230,14,15,196,672,96,694,561,695 -171,248,288,229,696,686,496,697,698 -65,171,229,64,587,697,699,146,700 -280,258,210,282,701,702,471,703,704 -229,197,63,64,495,645,145,699,705 -200,56,57,235,612,138,607,706,707 -235,164,210,258,609,579,702,708,709 -165,194,265,226,710,617,558,483,711 -308,267,214,289,712,713,674,420,714 -193,164,58,59,580,608,140,715,716 -198,61,62,169,717,143,644,718,719 -169,214,267,198,659,713,605,718,720 -228,17,18,192,566,99,569,438,721 -211,226,287,252,481,557,399,628,722 -319,292,267,308,361,603,712,401,723 -227,193,59,60,491,715,141,724,725 -194,165,19,20,710,570,101,726,727 -196,15,16,166,694,97,565,691,728 -227,60,61,198,724,142,717,604,729 -168,54,55,239,730,136,611,731,732 -292,318,284,264,360,733,469,489,734 -232,194,20,21,618,726,102,486,735 -220,186,205,273,736,737,738,739,740 -0,1,184,240,82,741,742,641,743 -297,223,185,257,381,744,390,745,746 -277,271,222,262,747,510,425,748,749 -54,168,244,53,730,750,751,135,752 -234,172,24,25,753,754,106,681,755 -228,263,212,166,437,460,689,567,756 -186,9,10,205,582,91,636,737,757 -295,291,269,249,758,759,760,499,761 -237,269,291,298,762,759,763,764,765 -318,307,278,296,359,766,478,767,768 -188,38,39,202,364,120,538,769,770 -290,261,178,218,340,771,589,519,772 -293,306,276,254,575,324,656,525,773 -273,205,180,213,738,638,543,624,774 -240,184,190,219,742,356,347,775,776 -309,313,248,281,665,687,777,429,778 -259,304,286,216,779,780,500,375,781 -287,278,307,319,556,766,358,400,782 -209,170,22,23,783,487,104,784,785 -1,2,3,184,83,84,354,741,786 -249,269,172,234,760,787,753,409,788 -237,215,170,209,789,532,783,790,791 -172,209,23,24,792,784,105,754,793 -223,51,52,185,615,133,794,744,795 -271,277,299,250,747,796,432,797,798 -45,46,178,221,127,590,799,679,800 -221,178,261,272,799,771,801,802,803 -298,253,215,237,804,397,789,764,805 -269,237,209,172,762,790,792,787,806 -31,201,29,30,442,620,111,112,807 -270,219,260,301,808,346,332,507,809 -273,302,236,220,623,463,810,739,811 -177,224,271,250,515,511,797,435,812 -206,189,70,71,367,413,152,813,814 -311,317,299,277,599,815,796,816,817 -53,244,185,52,751,391,794,134,818 -29,175,225,28,621,467,819,110,820 -248,171,243,275,696,586,821,822,823 -225,173,27,28,404,456,109,819,824 -244,168,239,274,750,731,825,392,826 -200,247,274,239,827,828,825,613,829 -42,188,202,41,683,769,537,123,830 -217,311,277,262,508,816,748,424,831 -203,182,204,241,349,415,652,662,832 -5,207,220,236,833,834,810,634,835 -187,36,37,245,321,118,363,371,836 -6,208,207,5,551,837,833,87,838 -219,270,183,240,808,335,640,775,839 -314,315,281,303,326,430,840,630,841 -305,316,302,294,597,464,626,649,842 -303,281,248,275,840,777,822,843,844 -224,67,68,195,517,149,351,554,845 -303,275,243,238,843,821,676,631,846 -73,206,71,72,368,813,153,154,847 -257,286,304,297,387,780,848,745,849 -299,317,305,314,815,598,648,632,850 -181,272,261,242,851,801,339,528,852 -220,207,208,186,834,837,583,736,853 -181,35,221,272,654,320,802,851,854 -235,258,247,200,708,855,827,706,856 -274,247,258,280,828,855,701,857,858 -318,296,253,284,767,394,859,733,860 -297,304,259,256,848,779,541,382,861 -291,295,274,280,758,385,857,862,863 -282,298,291,280,864,763,862,703,865 -284,253,298,282,859,804,864,470,866 +221,187,36,35,322,321,117,320,323 +276,314,315,306,327,326,325,324,328 +260,301,311,316,332,331,330,329,333 +174,217,270,183,337,336,335,334,338 +242,175,290,261,342,341,340,339,343 +190,219,260,236,347,346,345,344,348 +182,68,195,203,352,351,350,349,353 +184,190,4,3,356,355,85,354,357 +319,292,318,307,361,360,359,358,362 +245,188,38,37,365,364,119,363,366 +189,74,73,206,369,155,368,367,370 +187,44,43,245,373,125,372,371,374 +259,173,191,216,378,377,376,375,379 +176,256,297,223,383,382,381,380,384 +274,257,286,295,388,387,386,385,389 +274,244,185,257,392,391,390,388,393 +253,215,255,296,397,396,395,394,398 +252,308,319,287,402,401,400,399,403 +173,259,300,225,378,406,405,404,407 +216,191,234,249,376,410,409,408,411 +182,204,189,70,415,414,413,412,416 +34,33,32,199,115,114,418,417,419 +308,252,283,289,402,422,421,420,423 +217,174,222,262,337,426,425,424,427 +306,315,281,309,325,430,429,428,431 +250,177,238,299,435,434,433,432,436 +263,211,192,228,440,439,438,437,441 +201,199,32,31,443,418,113,442,444 +233,47,218,268,448,447,446,445,449 +47,233,49,48,448,450,130,129,451 +183,81,80,174,453,162,452,334,454 +173,27,26,191,456,108,455,377,457 +283,263,212,279,461,460,459,458,462 +236,260,316,302,345,329,464,463,465 +175,225,300,290,467,405,466,341,468 +264,210,282,284,472,471,470,469,473 +279,310,289,283,475,474,421,458,476 +265,278,296,255,479,478,395,477,480 +226,165,192,211,483,482,439,481,484 +170,22,21,232,487,103,486,485,488 +292,227,193,264,492,491,490,489,493 +251,288,229,197,497,496,495,494,498 +295,286,216,249,386,500,408,499,501 +288,251,310,312,497,504,503,502,505 +70,69,68,182,151,150,352,412,506 +270,217,311,301,336,508,331,507,509 +222,179,224,271,513,512,511,510,514 +224,67,66,177,517,148,516,515,518 +290,300,268,218,466,520,446,519,521 +254,293,167,231,525,524,523,522,526 +199,201,242,181,443,529,528,527,530 +215,170,232,255,532,485,531,396,533 +285,312,310,279,535,503,475,534,536 +41,40,39,202,122,121,538,537,539 +259,256,268,300,541,540,520,406,542 +213,254,231,180,545,522,544,543,546 +49,233,176,50,450,548,547,131,549 +8,7,6,208,89,88,551,550,552 +203,195,224,179,350,554,512,553,555 +278,265,226,287,479,558,557,556,559 +246,266,196,230,563,562,561,560,564 +166,228,17,16,567,566,98,565,568 +165,19,18,192,570,100,569,482,571 +179,78,77,203,573,159,572,553,574 +306,309,246,293,428,577,576,575,578 +264,193,164,210,490,580,579,472,581 +208,186,9,8,583,582,90,550,584 +66,65,171,243,147,587,586,585,588 +46,178,218,47,590,589,447,128,591 +189,204,75,74,414,592,156,369,593 +266,246,309,285,563,577,595,594,596 +311,317,305,316,599,598,597,330,600 +222,174,80,79,426,452,161,601,602 +267,198,227,292,605,604,492,603,606 +235,164,58,57,609,608,139,607,610 +239,200,56,55,613,612,137,611,614 +223,51,50,176,615,132,547,380,616 +255,232,194,265,531,618,617,477,619 +29,175,242,201,621,342,529,620,622 +302,294,213,273,626,625,624,623,627 +263,283,252,211,461,422,628,440,629 +314,299,238,303,632,433,631,630,633 +236,5,4,190,634,86,355,344,635 +205,180,11,10,638,637,92,636,639 +240,0,81,183,641,163,453,640,642 +197,63,62,169,645,144,644,643,646 +294,305,314,276,649,648,327,647,650 +204,241,76,75,652,651,157,592,653 +199,181,35,34,527,654,116,417,655 +294,276,254,213,647,656,545,625,657 +197,169,214,251,643,659,658,494,660 +222,79,78,179,601,160,573,513,661 +203,77,76,241,572,158,651,662,663 +309,313,312,285,665,664,535,595,666 +180,231,12,11,544,667,93,637,668 +293,246,230,167,576,560,669,524,670 +167,230,14,13,669,672,95,671,673 +310,251,214,289,504,658,674,474,675 +66,243,238,177,585,676,434,516,677 +231,167,13,12,523,671,94,667,678 +221,45,44,187,679,126,373,322,680 +234,191,26,25,410,455,107,681,682 +43,42,188,245,124,683,365,372,684 +256,176,233,268,383,548,445,540,685 +312,313,248,288,664,687,686,502,688 +166,196,266,212,691,562,690,689,692 +285,279,212,266,534,459,690,594,693 +230,196,15,14,561,694,96,672,695 +171,229,288,248,697,496,686,696,698 +65,64,229,171,146,699,697,587,700 +280,282,210,258,703,471,702,701,704 +229,64,63,197,699,145,645,495,705 +200,235,57,56,706,607,138,612,707 +235,258,210,164,708,702,579,609,709 +165,226,265,194,483,558,617,710,711 +308,289,214,267,420,674,713,712,714 +193,59,58,164,715,140,608,580,716 +198,169,62,61,718,644,143,717,719 +169,198,267,214,718,605,713,659,720 +228,192,18,17,438,569,99,566,721 +211,252,287,226,628,399,557,481,722 +319,308,267,292,401,712,603,361,723 +227,60,59,193,724,141,715,491,725 +194,20,19,165,726,101,570,710,727 +196,166,16,15,691,565,97,694,728 +227,198,61,60,604,717,142,724,729 +168,239,55,54,731,611,136,730,732 +292,264,284,318,489,469,733,360,734 +232,21,20,194,486,102,726,618,735 +220,273,205,186,739,738,737,736,740 +0,240,184,1,641,742,741,82,743 +297,257,185,223,745,390,744,381,746 +277,262,222,271,748,425,510,747,749 +54,53,244,168,135,751,750,730,752 +234,25,24,172,681,106,754,753,755 +228,166,212,263,567,689,460,437,756 +186,205,10,9,737,636,91,582,757 +295,249,269,291,499,760,759,758,761 +237,298,291,269,764,763,759,762,765 +318,296,278,307,767,478,766,359,768 +188,202,39,38,769,538,120,364,770 +290,218,178,261,519,589,771,340,772 +293,254,276,306,525,656,324,575,773 +273,213,180,205,624,543,638,738,774 +240,219,190,184,775,347,356,742,776 +309,281,248,313,429,777,687,665,778 +259,216,286,304,375,500,780,779,781 +287,319,307,278,400,358,766,556,782 +209,23,22,170,784,104,487,783,785 +1,184,3,2,741,354,84,83,786 +249,234,172,269,409,753,787,760,788 +237,209,170,215,790,783,532,789,791 +172,24,23,209,754,105,784,792,793 +223,185,52,51,744,794,133,615,795 +271,250,299,277,797,432,796,747,798 +45,221,178,46,679,799,590,127,800 +221,272,261,178,802,801,771,799,803 +298,237,215,253,764,789,397,804,805 +269,172,209,237,787,792,790,762,806 +31,30,29,201,112,111,620,442,807 +270,301,260,219,507,332,346,808,809 +273,220,236,302,739,810,463,623,811 +177,250,271,224,435,797,511,515,812 +206,71,70,189,813,152,413,367,814 +311,277,299,317,816,796,815,599,817 +53,52,185,244,134,794,391,751,818 +29,28,225,175,110,819,467,621,820 +248,275,243,171,822,821,586,696,823 +225,28,27,173,819,109,456,404,824 +244,274,239,168,392,825,731,750,826 +200,239,274,247,613,825,828,827,829 +42,41,202,188,123,537,769,683,830 +217,262,277,311,424,748,816,508,831 +203,241,204,182,662,652,415,349,832 +5,236,220,207,634,810,834,833,835 +187,245,37,36,371,363,118,321,836 +6,5,207,208,87,833,837,551,838 +219,240,183,270,775,640,335,808,839 +314,303,281,315,630,840,430,326,841 +305,294,302,316,649,626,464,597,842 +303,275,248,281,843,822,777,840,844 +224,195,68,67,554,351,149,517,845 +303,238,243,275,631,676,821,843,846 +73,72,71,206,154,153,813,368,847 +257,297,304,286,745,848,780,387,849 +299,314,305,317,632,648,598,815,850 +181,242,261,272,528,339,801,851,852 +220,186,208,207,736,583,837,834,853 +181,272,221,35,851,802,320,654,854 +235,200,247,258,706,827,855,708,856 +274,280,258,247,857,701,855,828,858 +318,284,253,296,733,859,394,767,860 +297,256,259,304,382,541,779,848,861 +291,280,274,295,862,857,385,758,863 +282,280,291,298,703,862,763,864,865 +284,282,298,253,470,864,804,859,866 diff --git a/data/rabbits/riley_tri3/connectivity.csv b/data/rabbits/riley_tri3/connectivity.csv index 8706929d..00680290 100644 --- a/data/rabbits/riley_tri3/connectivity.csv +++ b/data/rabbits/riley_tri3/connectivity.csv @@ -1,323 +1,323 @@ -67,69,68 -68,69,70 -71,73,72 -72,73,74 -75,77,76 -78,79,23 -80,81,16 -82,84,83 -85,86,79 -85,79,87 -82,83,88 -89,91,90 -90,91,92 -93,68,70 -93,70,20 -94,96,95 -67,68,97 -67,97,98 -95,96,45 -76,77,53 -77,52,53 -99,77,75 -99,75,100 -101,103,102 -102,103,100 -104,106,105 -107,109,108 -108,109,110 -111,113,112 -112,113,114 -113,111,115 -113,115,116 -117,111,112 -117,112,65 -118,120,119 -71,122,121 -123,124,99 -123,99,125 -106,104,70 -45,96,44 -103,126,125 -127,105,106 -128,130,129 -71,121,131 -132,80,133 -133,80,17 -134,126,103 -104,105,19 -134,103,101 -134,101,13 -121,135,123 -121,123,136 -99,137,77 -77,137,52 -138,126,134 -139,101,102 -138,140,126 -126,140,136 -122,71,72 -122,72,141 -71,131,73 -73,131,142 -138,134,14 -143,132,142 -123,144,124 -124,144,51 -72,145,141 -141,145,48 -143,81,132 -143,142,131 -146,127,147 -129,130,148 -121,149,135 -135,149,50 -80,132,81 -129,148,150 -145,130,128 -145,128,47 -147,127,74 -124,137,99 -145,72,130 -130,72,74 -122,141,149 -151,88,83 -151,83,42 -140,81,143 -140,143,136 -135,144,123 -133,147,132 -120,110,152 -153,128,129 -133,146,147 -122,149,121 -152,110,154 -141,49,149 -140,138,81 -147,74,73 -146,105,127 -107,155,8 -9,107,8 -156,157,154 -157,158,154 -91,160,159 -91,159,116 -160,108,159 -159,108,110 -160,91,161 -107,9,162 -94,163,96 -163,94,151 -94,98,151 -164,165,38 -166,168,167 -166,167,28 -169,170,161 -169,161,4 -171,173,172 -174,172,173 -171,37,173 -38,173,37 -166,28,172 -172,28,29 -162,9,10 -175,118,176 -139,102,177 -171,172,29 -139,177,12 -112,64,65 -178,114,175 -178,175,55 -112,114,178 -59,60,179 -59,179,58 -178,64,112 -177,102,157 -100,158,102 -157,102,158 -84,82,180 -84,180,165 -180,79,78 -87,79,180 -180,82,87 -100,75,158 -175,176,54 -2,3,89 -89,181,2 -89,3,4 -150,69,182 -183,119,152 -129,150,182 -88,87,82 -126,136,125 -174,173,184 -136,123,125 -185,36,171 -29,185,171 -151,98,88 -88,98,97 -88,97,85 -183,152,158 -54,76,53 -35,185,186 -185,30,186 -159,110,120 -116,159,120 -155,161,170 -161,155,160 -27,167,26 -167,168,25 -167,25,26 -187,92,117 -70,69,106 -175,114,118 -175,54,55 -116,115,91 -92,115,111 -115,92,91 -165,180,184 -66,187,117 -136,143,131 -131,121,136 -62,189,188 -189,56,188 -68,93,190 -68,190,97 -180,78,184 -21,190,93 -7,169,6 -187,66,0 -187,90,92 -169,5,6 -89,90,181 -164,84,165 -164,40,84 -23,79,86 -41,84,40 -177,157,156 -177,156,11 -42,83,41 -33,191,32 -172,174,166 -191,31,32 -168,166,192 -174,192,166 -168,192,24 -86,22,23 -24,192,23 -63,178,189 -178,55,189 -153,129,95 -153,95,46 -106,69,150 -152,154,158 -117,92,111 -117,65,66 -113,116,118 -114,113,118 -184,173,165 -173,38,165 -156,154,109 -152,119,120 -88,85,87 -170,8,155 -8,170,7 -85,190,86 -168,24,25 -190,85,97 -190,21,22 -190,22,86 -93,20,21 -103,125,100 -125,99,100 -184,192,174 -192,184,78 -23,192,78 -63,189,62 -56,189,55 -30,185,29 -36,185,35 -84,41,83 -151,42,163 -148,127,150 -2,181,1 -62,188,61 -57,188,56 -181,187,1 -187,181,90 -37,171,36 -187,0,1 -31,186,30 -35,186,34 -148,130,74 -15,138,14 -127,148,74 -13,139,12 -28,167,27 -76,183,75 -183,76,119 -104,19,20 -64,178,63 -142,132,147 -183,158,75 -138,15,81 -182,193,129 -135,50,144 -163,42,43 -44,96,163 -44,163,43 -155,108,160 -194,57,58 -194,61,188 -108,155,107 -57,194,188 -47,128,153 -18,146,133 -146,18,105 -14,134,13 -137,51,52 -177,11,12 -48,145,47 -95,193,94 -193,95,129 -124,51,137 -193,182,94 -81,15,16 -20,70,104 -95,45,46 -147,73,142 -13,101,139 -110,109,154 -179,61,194 -61,179,60 -58,179,194 -169,4,5 -169,7,170 -118,116,120 -144,50,51 -162,10,156 -191,34,186 -34,191,33 -31,191,186 -47,153,46 -49,141,48 -94,182,98 -156,10,11 -18,133,17 -162,109,107 -149,49,50 -109,162,156 -164,38,39 -80,16,17 -40,164,39 -105,18,19 -67,182,69 -182,67,98 -195,4,161 -195,91,89 -91,195,161 -4,195,89 -127,106,150 -176,119,76 -119,176,118 -54,176,76 +67,68,69 +68,70,69 +71,72,73 +72,74,73 +75,76,77 +78,23,79 +80,16,81 +82,83,84 +85,79,86 +85,87,79 +82,88,83 +89,90,91 +90,92,91 +93,70,68 +93,20,70 +94,95,96 +67,97,68 +67,98,97 +95,45,96 +76,53,77 +77,53,52 +99,75,77 +99,100,75 +101,102,103 +102,100,103 +104,105,106 +107,108,109 +108,110,109 +111,112,113 +112,114,113 +113,115,111 +113,116,115 +117,112,111 +117,65,112 +118,119,120 +71,121,122 +123,99,124 +123,125,99 +106,70,104 +45,44,96 +103,125,126 +127,106,105 +128,129,130 +71,131,121 +132,133,80 +133,17,80 +134,103,126 +104,19,105 +134,101,103 +134,13,101 +121,123,135 +121,136,123 +99,77,137 +77,52,137 +138,134,126 +139,102,101 +138,126,140 +126,136,140 +122,72,71 +122,141,72 +71,73,131 +73,142,131 +138,14,134 +143,142,132 +123,124,144 +124,51,144 +72,141,145 +141,48,145 +143,132,81 +143,131,142 +146,147,127 +129,148,130 +121,135,149 +135,50,149 +80,81,132 +129,150,148 +145,128,130 +145,47,128 +147,74,127 +124,99,137 +145,130,72 +130,74,72 +122,149,141 +151,83,88 +151,42,83 +140,143,81 +140,136,143 +135,123,144 +133,132,147 +120,152,110 +153,129,128 +133,147,146 +122,121,149 +152,154,110 +141,149,49 +140,81,138 +147,73,74 +146,127,105 +107,8,155 +9,8,107 +156,154,157 +157,154,158 +91,159,160 +91,116,159 +160,159,108 +159,110,108 +160,161,91 +107,162,9 +94,96,163 +163,151,94 +94,151,98 +164,38,165 +166,167,168 +166,28,167 +169,161,170 +169,4,161 +171,172,173 +174,173,172 +171,173,37 +38,37,173 +166,172,28 +172,29,28 +162,10,9 +175,176,118 +139,177,102 +171,29,172 +139,12,177 +112,65,64 +178,175,114 +178,55,175 +112,178,114 +59,179,60 +59,58,179 +178,112,64 +177,157,102 +100,102,158 +157,158,102 +84,180,82 +84,165,180 +180,78,79 +87,180,79 +180,87,82 +100,158,75 +175,54,176 +2,89,3 +89,2,181 +89,4,3 +150,182,69 +183,152,119 +129,182,150 +88,82,87 +126,125,136 +174,184,173 +136,125,123 +185,171,36 +29,171,185 +151,88,98 +88,97,98 +88,85,97 +183,158,152 +54,53,76 +35,186,185 +185,186,30 +159,120,110 +116,120,159 +155,170,161 +161,160,155 +27,26,167 +167,25,168 +167,26,25 +187,117,92 +70,106,69 +175,118,114 +175,55,54 +116,91,115 +92,111,115 +115,91,92 +165,184,180 +66,117,187 +136,131,143 +131,136,121 +62,188,189 +189,188,56 +68,190,93 +68,97,190 +180,184,78 +21,93,190 +7,6,169 +187,0,66 +187,92,90 +169,6,5 +89,181,90 +164,165,84 +164,84,40 +23,86,79 +41,40,84 +177,156,157 +177,11,156 +42,41,83 +33,32,191 +172,166,174 +191,32,31 +168,192,166 +174,166,192 +168,24,192 +86,23,22 +24,23,192 +63,189,178 +178,189,55 +153,95,129 +153,46,95 +106,150,69 +152,158,154 +117,111,92 +117,66,65 +113,118,116 +114,118,113 +184,165,173 +173,165,38 +156,109,154 +152,120,119 +88,87,85 +170,155,8 +8,7,170 +85,86,190 +168,25,24 +190,97,85 +190,22,21 +190,86,22 +93,21,20 +103,100,125 +125,100,99 +184,174,192 +192,78,184 +23,78,192 +63,62,189 +56,55,189 +30,29,185 +36,35,185 +84,83,41 +151,163,42 +148,150,127 +2,1,181 +62,61,188 +57,56,188 +181,1,187 +187,90,181 +37,36,171 +187,1,0 +31,30,186 +35,34,186 +148,74,130 +15,14,138 +127,74,148 +13,12,139 +28,27,167 +76,75,183 +183,119,76 +104,20,19 +64,63,178 +142,147,132 +183,75,158 +138,81,15 +182,129,193 +135,144,50 +163,43,42 +44,163,96 +44,43,163 +155,160,108 +194,58,57 +194,188,61 +108,107,155 +57,188,194 +47,153,128 +18,133,146 +146,105,18 +14,13,134 +137,52,51 +177,12,11 +48,47,145 +95,94,193 +193,129,95 +124,137,51 +193,94,182 +81,16,15 +20,104,70 +95,46,45 +147,142,73 +13,139,101 +110,154,109 +179,194,61 +61,60,179 +58,194,179 +169,5,4 +169,170,7 +118,120,116 +144,51,50 +162,156,10 +191,186,34 +34,33,191 +31,186,191 +47,46,153 +49,48,141 +94,98,182 +156,11,10 +18,17,133 +162,107,109 +149,50,49 +109,156,162 +164,39,38 +80,17,16 +40,39,164 +105,19,18 +67,69,182 +182,98,67 +195,161,4 +195,89,91 +91,161,195 +4,89,195 +127,150,106 +176,76,119 +119,118,176 +54,76,176 diff --git a/data/rabbits/riley_tri6/connectivity.csv b/data/rabbits/riley_tri6/connectivity.csv index 506d2345..866fc5fc 100644 --- a/data/rabbits/riley_tri6/connectivity.csv +++ b/data/rabbits/riley_tri6/connectivity.csv @@ -1,323 +1,323 @@ -134,136,135,263,264,265 -135,136,137,264,266,267 -138,140,139,268,269,270 -139,140,141,269,271,272 -142,144,143,273,274,275 -145,146,23,276,277,278 -147,148,16,279,280,281 -149,151,150,282,283,284 -152,153,146,285,286,287 -152,146,154,287,288,289 -149,150,155,284,290,291 -156,158,157,292,293,294 -157,158,159,293,295,296 -160,135,137,297,267,298 -160,137,20,298,299,300 -161,163,162,301,302,303 -134,135,164,265,304,305 -134,164,165,305,306,307 -162,163,45,302,308,309 -143,144,53,274,310,311 -144,52,53,312,119,310 -166,144,142,313,273,314 -166,142,167,314,315,316 -168,170,169,317,318,319 -169,170,167,318,320,321 -171,173,172,322,323,324 -174,176,175,325,326,327 -175,176,177,326,328,329 -178,180,179,330,331,332 -179,180,181,331,333,334 -180,178,182,330,335,336 -180,182,183,336,337,338 -184,178,179,339,332,340 -184,179,65,340,341,342 -185,187,186,343,344,345 -138,189,188,346,347,348 -190,191,166,349,350,351 -190,166,192,351,352,353 -173,171,137,322,354,355 -45,163,44,308,356,111 -170,193,192,357,358,359 -194,172,173,360,323,361 -195,197,196,362,363,364 -138,188,198,348,365,366 -199,147,200,367,368,369 -200,147,17,368,370,371 -201,193,170,372,357,373 -171,172,19,324,374,375 -201,170,168,373,317,376 -201,168,13,376,377,378 -188,202,190,379,380,381 -188,190,203,381,382,383 -166,204,144,384,385,313 -144,204,52,385,386,312 -205,193,201,387,372,388 -206,168,169,389,319,390 -205,207,193,391,392,387 -193,207,203,392,393,394 -189,138,139,346,270,395 -189,139,208,395,396,397 -138,198,140,366,398,268 -140,198,209,398,399,400 -205,201,14,388,401,402 -210,199,209,403,404,405 -190,211,191,406,407,349 -191,211,51,407,408,409 -139,212,208,410,411,396 -208,212,48,411,412,413 -210,148,199,414,415,403 -210,209,198,405,399,416 -213,194,214,417,418,419 -196,197,215,363,420,421 -188,216,202,422,423,379 -202,216,50,423,424,425 -147,199,148,367,415,279 -196,215,217,421,426,427 -212,197,195,428,362,429 -212,195,47,429,430,431 -214,194,141,418,432,433 -191,204,166,434,384,350 -212,139,197,410,435,428 -197,139,141,435,272,436 -189,208,216,397,437,438 -218,155,150,439,290,440 -218,150,42,440,441,442 -207,148,210,443,414,444 -207,210,203,444,445,393 -202,211,190,446,406,380 -200,214,199,447,448,369 -187,177,219,449,450,451 -220,195,196,452,364,453 -200,213,214,454,419,447 -189,216,188,438,422,347 -219,177,221,450,455,456 -208,49,216,457,458,437 -207,205,148,391,459,443 -214,141,140,433,271,460 -213,172,194,461,360,417 -174,222,8,462,463,464 -9,174,8,465,464,75 -223,224,221,466,467,468 -224,225,221,469,470,467 -158,227,226,471,472,473 -158,226,183,473,474,475 -227,175,226,476,477,472 -226,175,177,477,329,478 -227,158,228,471,479,480 -174,9,229,465,481,482 -161,230,163,483,484,301 -230,161,218,483,485,486 -161,165,218,487,488,485 -231,232,38,489,490,491 -233,235,234,492,493,494 -233,234,28,494,495,496 -236,237,228,497,498,499 -236,228,4,499,500,501 -238,240,239,502,503,504 -241,239,240,505,503,506 -238,37,240,507,508,502 -38,240,37,509,508,104 -233,28,239,496,510,511 -239,28,29,510,95,512 -229,9,10,481,76,513 -242,185,243,514,515,516 -206,169,244,390,517,518 -238,239,29,504,512,519 -206,244,12,518,520,521 -179,64,65,522,131,341 -245,181,242,523,524,525 -245,242,55,525,526,527 -179,181,245,334,523,528 -59,60,246,126,529,530 -59,246,58,530,531,125 -245,64,179,532,522,528 -244,169,224,517,533,534 -167,225,169,535,536,321 -224,169,225,533,536,469 -151,149,247,282,537,538 -151,247,232,538,539,540 -247,146,145,541,276,542 -154,146,247,288,541,543 -247,149,154,537,544,543 -167,142,225,315,545,535 -242,243,54,516,546,547 -2,3,156,69,548,549 -156,248,2,550,551,549 -156,3,4,548,70,552 -217,136,249,553,554,555 -250,186,219,556,557,558 -196,217,249,427,555,559 -155,154,149,560,544,291 -193,203,192,394,561,358 -241,240,251,506,562,563 -203,190,192,382,353,561 -252,36,238,564,565,566 -29,252,238,567,566,519 -218,165,155,488,568,439 -155,165,164,568,306,569 -155,164,152,569,570,571 -250,219,225,558,572,573 -54,143,53,574,311,120 -35,252,253,575,576,577 -252,30,253,578,579,576 -226,177,187,478,449,580 -183,226,187,474,580,581 -222,228,237,582,498,583 -228,222,227,582,584,480 -27,234,26,585,586,93 -234,235,25,493,587,588 -234,25,26,588,92,586 -254,159,184,589,590,591 -137,136,173,266,592,355 -242,181,185,524,593,514 -242,54,55,547,121,526 -183,182,158,337,594,475 -159,182,178,595,335,596 -182,159,158,595,295,594 -232,247,251,539,597,598 -66,254,184,599,591,600 -203,210,198,445,416,601 -198,188,203,365,383,601 -62,256,255,602,603,604 -256,56,255,605,606,603 -135,160,257,297,607,608 -135,257,164,608,609,304 -247,145,251,542,610,597 -21,257,160,611,607,612 -7,236,6,613,614,73 -254,66,0,599,133,615 -254,157,159,616,296,589 -236,5,6,617,72,614 -156,157,248,294,618,550 -231,151,232,619,540,489 -231,40,151,620,621,619 -23,146,153,277,286,622 -41,151,40,623,621,107 -244,224,223,534,466,624 -244,223,11,624,625,626 -42,150,41,441,627,108 -33,258,32,628,629,99 -239,241,233,505,630,511 -258,31,32,631,98,629 -235,233,259,492,632,633 -241,259,233,634,632,630 -235,259,24,633,635,636 -153,22,23,637,89,622 -24,259,23,635,638,90 -63,245,256,639,640,641 -245,55,256,527,642,640 -220,196,162,453,643,644 -220,162,46,644,645,646 -173,136,217,592,553,647 -219,221,225,456,470,572 -184,159,178,590,596,339 -184,65,66,342,132,600 -180,183,185,338,648,649 -181,180,185,333,649,593 -251,240,232,562,650,598 -240,38,232,509,490,650 -223,221,176,468,651,652 -219,186,187,557,344,451 -155,152,154,571,289,560 -237,8,222,653,463,583 -8,237,7,653,654,74 -152,257,153,655,656,285 -235,24,25,636,91,587 -257,152,164,655,570,609 -257,21,22,611,88,657 -257,22,153,657,637,656 -160,20,21,300,87,612 -170,192,167,359,658,320 -192,166,167,352,316,658 -251,259,241,659,634,563 -259,251,145,659,610,660 -23,259,145,638,660,278 -63,256,62,641,602,129 -56,256,55,605,642,122 -30,252,29,578,567,96 -36,252,35,564,575,102 -151,41,150,623,627,283 -218,42,230,442,661,486 -215,194,217,662,663,426 -2,248,1,551,664,68 -62,255,61,604,665,128 -57,255,56,666,606,123 -248,254,1,667,668,664 -254,248,157,667,618,616 -37,238,36,507,565,103 -254,0,1,615,67,668 -31,253,30,669,579,97 -35,253,34,577,670,101 -215,197,141,420,436,671 -15,205,14,672,402,81 -194,215,141,662,671,432 -13,206,12,673,521,79 -28,234,27,495,585,94 -143,250,142,674,675,275 -250,143,186,674,676,556 -171,19,20,375,86,677 -64,245,63,532,639,130 -209,199,214,404,448,678 -250,225,142,573,545,675 -205,15,148,672,679,459 -249,260,196,680,681,559 -202,50,211,425,682,446 -230,42,43,661,109,683 -44,163,230,356,484,684 -44,230,43,684,683,110 -222,175,227,685,476,584 -261,57,58,686,124,687 -261,61,255,688,665,689 -175,222,174,685,462,327 -57,261,255,686,689,666 -47,195,220,430,452,690 -18,213,200,691,454,692 -213,18,172,691,693,461 -14,201,13,401,378,80 -204,51,52,694,118,386 -244,11,12,626,78,520 -48,212,47,412,431,114 -162,260,161,695,696,303 -260,162,196,695,643,681 -191,51,204,409,694,434 -260,249,161,680,697,696 -148,15,16,679,82,280 -20,137,171,299,354,677 -162,45,46,309,112,645 -214,140,209,460,400,678 -13,168,206,377,389,673 -177,176,221,328,651,455 -246,61,261,698,688,699 -61,246,60,698,529,127 -58,246,261,531,699,687 -236,4,5,501,71,617 -236,7,237,613,654,497 -185,183,187,648,581,343 -211,50,51,682,117,408 -229,10,223,513,700,701 -258,34,253,702,670,703 -34,258,33,702,628,100 -31,258,253,631,703,669 -47,220,46,690,646,113 -49,208,48,457,413,115 -161,249,165,697,704,487 -223,10,11,700,77,625 -18,200,17,692,371,84 -229,176,174,705,325,482 -216,49,50,458,116,424 -176,229,223,705,701,652 -231,38,39,491,105,706 -147,16,17,281,83,370 -40,231,39,620,706,106 -172,18,19,693,85,374 -134,249,136,707,554,263 -249,134,165,707,307,704 -262,4,228,708,500,709 -262,158,156,710,292,711 -158,262,228,710,709,479 -4,262,156,708,711,552 -194,173,217,361,647,663 -243,186,143,712,676,713 -186,243,185,712,515,345 -54,243,143,546,713,574 +134,135,136,265,264,263 +135,137,136,267,266,264 +138,139,140,270,269,268 +139,141,140,272,271,269 +142,143,144,275,274,273 +145,23,146,278,277,276 +147,16,148,281,280,279 +149,150,151,284,283,282 +152,146,153,287,286,285 +152,154,146,289,288,287 +149,155,150,291,290,284 +156,157,158,294,293,292 +157,159,158,296,295,293 +160,137,135,298,267,297 +160,20,137,300,299,298 +161,162,163,303,302,301 +134,164,135,305,304,265 +134,165,164,307,306,305 +162,45,163,309,308,302 +143,53,144,311,310,274 +144,53,52,310,119,312 +166,142,144,314,273,313 +166,167,142,316,315,314 +168,169,170,319,318,317 +169,167,170,321,320,318 +171,172,173,324,323,322 +174,175,176,327,326,325 +175,177,176,329,328,326 +178,179,180,332,331,330 +179,181,180,334,333,331 +180,182,178,336,335,330 +180,183,182,338,337,336 +184,179,178,340,332,339 +184,65,179,342,341,340 +185,186,187,345,344,343 +138,188,189,348,347,346 +190,166,191,351,350,349 +190,192,166,353,352,351 +173,137,171,355,354,322 +45,44,163,111,356,308 +170,192,193,359,358,357 +194,173,172,361,323,360 +195,196,197,364,363,362 +138,198,188,366,365,348 +199,200,147,369,368,367 +200,17,147,371,370,368 +201,170,193,373,357,372 +171,19,172,375,374,324 +201,168,170,376,317,373 +201,13,168,378,377,376 +188,190,202,381,380,379 +188,203,190,383,382,381 +166,144,204,313,385,384 +144,52,204,312,386,385 +205,201,193,388,372,387 +206,169,168,390,319,389 +205,193,207,387,392,391 +193,203,207,394,393,392 +189,139,138,395,270,346 +189,208,139,397,396,395 +138,140,198,268,398,366 +140,209,198,400,399,398 +205,14,201,402,401,388 +210,209,199,405,404,403 +190,191,211,349,407,406 +191,51,211,409,408,407 +139,208,212,396,411,410 +208,48,212,413,412,411 +210,199,148,403,415,414 +210,198,209,416,399,405 +213,214,194,419,418,417 +196,215,197,421,420,363 +188,202,216,379,423,422 +202,50,216,425,424,423 +147,148,199,279,415,367 +196,217,215,427,426,421 +212,195,197,429,362,428 +212,47,195,431,430,429 +214,141,194,433,432,418 +191,166,204,350,384,434 +212,197,139,428,435,410 +197,141,139,436,272,435 +189,216,208,438,437,397 +218,150,155,440,290,439 +218,42,150,442,441,440 +207,210,148,444,414,443 +207,203,210,393,445,444 +202,190,211,380,406,446 +200,199,214,369,448,447 +187,219,177,451,450,449 +220,196,195,453,364,452 +200,214,213,447,419,454 +189,188,216,347,422,438 +219,221,177,456,455,450 +208,216,49,437,458,457 +207,148,205,443,459,391 +214,140,141,460,271,433 +213,194,172,417,360,461 +174,8,222,464,463,462 +9,8,174,75,464,465 +223,221,224,468,467,466 +224,221,225,467,470,469 +158,226,227,473,472,471 +158,183,226,475,474,473 +227,226,175,472,477,476 +226,177,175,478,329,477 +227,228,158,480,479,471 +174,229,9,482,481,465 +161,163,230,301,484,483 +230,218,161,486,485,483 +161,218,165,485,488,487 +231,38,232,491,490,489 +233,234,235,494,493,492 +233,28,234,496,495,494 +236,228,237,499,498,497 +236,4,228,501,500,499 +238,239,240,504,503,502 +241,240,239,506,503,505 +238,240,37,502,508,507 +38,37,240,104,508,509 +233,239,28,511,510,496 +239,29,28,512,95,510 +229,10,9,513,76,481 +242,243,185,516,515,514 +206,244,169,518,517,390 +238,29,239,519,512,504 +206,12,244,521,520,518 +179,65,64,341,131,522 +245,242,181,525,524,523 +245,55,242,527,526,525 +179,245,181,528,523,334 +59,246,60,530,529,126 +59,58,246,125,531,530 +245,179,64,528,522,532 +244,224,169,534,533,517 +167,169,225,321,536,535 +224,225,169,469,536,533 +151,247,149,538,537,282 +151,232,247,540,539,538 +247,145,146,542,276,541 +154,247,146,543,541,288 +247,154,149,543,544,537 +167,225,142,535,545,315 +242,54,243,547,546,516 +2,156,3,549,548,69 +156,2,248,549,551,550 +156,4,3,552,70,548 +217,249,136,555,554,553 +250,219,186,558,557,556 +196,249,217,559,555,427 +155,149,154,291,544,560 +193,192,203,358,561,394 +241,251,240,563,562,506 +203,192,190,561,353,382 +252,238,36,566,565,564 +29,238,252,519,566,567 +218,155,165,439,568,488 +155,164,165,569,306,568 +155,152,164,571,570,569 +250,225,219,573,572,558 +54,53,143,120,311,574 +35,253,252,577,576,575 +252,253,30,576,579,578 +226,187,177,580,449,478 +183,187,226,581,580,474 +222,237,228,583,498,582 +228,227,222,480,584,582 +27,26,234,93,586,585 +234,25,235,588,587,493 +234,26,25,586,92,588 +254,184,159,591,590,589 +137,173,136,355,592,266 +242,185,181,514,593,524 +242,55,54,526,121,547 +183,158,182,475,594,337 +159,178,182,596,335,595 +182,158,159,594,295,595 +232,251,247,598,597,539 +66,184,254,600,591,599 +203,198,210,601,416,445 +198,203,188,601,383,365 +62,255,256,604,603,602 +256,255,56,603,606,605 +135,257,160,608,607,297 +135,164,257,304,609,608 +247,251,145,597,610,542 +21,160,257,612,607,611 +7,6,236,73,614,613 +254,0,66,615,133,599 +254,159,157,589,296,616 +236,6,5,614,72,617 +156,248,157,550,618,294 +231,232,151,489,540,619 +231,151,40,619,621,620 +23,153,146,622,286,277 +41,40,151,107,621,623 +244,223,224,624,466,534 +244,11,223,626,625,624 +42,41,150,108,627,441 +33,32,258,99,629,628 +239,233,241,511,630,505 +258,32,31,629,98,631 +235,259,233,633,632,492 +241,233,259,630,632,634 +235,24,259,636,635,633 +153,23,22,622,89,637 +24,23,259,90,638,635 +63,256,245,641,640,639 +245,256,55,640,642,527 +220,162,196,644,643,453 +220,46,162,646,645,644 +173,217,136,647,553,592 +219,225,221,572,470,456 +184,178,159,339,596,590 +184,66,65,600,132,342 +180,185,183,649,648,338 +181,185,180,593,649,333 +251,232,240,598,650,562 +240,232,38,650,490,509 +223,176,221,652,651,468 +219,187,186,451,344,557 +155,154,152,560,289,571 +237,222,8,583,463,653 +8,7,237,74,654,653 +152,153,257,285,656,655 +235,25,24,587,91,636 +257,164,152,609,570,655 +257,22,21,657,88,611 +257,153,22,656,637,657 +160,21,20,612,87,300 +170,167,192,320,658,359 +192,167,166,658,316,352 +251,241,259,563,634,659 +259,145,251,660,610,659 +23,145,259,278,660,638 +63,62,256,129,602,641 +56,55,256,122,642,605 +30,29,252,96,567,578 +36,35,252,102,575,564 +151,150,41,283,627,623 +218,230,42,486,661,442 +215,217,194,426,663,662 +2,1,248,68,664,551 +62,61,255,128,665,604 +57,56,255,123,606,666 +248,1,254,664,668,667 +254,157,248,616,618,667 +37,36,238,103,565,507 +254,1,0,668,67,615 +31,30,253,97,579,669 +35,34,253,101,670,577 +215,141,197,671,436,420 +15,14,205,81,402,672 +194,141,215,432,671,662 +13,12,206,79,521,673 +28,27,234,94,585,495 +143,142,250,275,675,674 +250,186,143,556,676,674 +171,20,19,677,86,375 +64,63,245,130,639,532 +209,214,199,678,448,404 +250,142,225,675,545,573 +205,148,15,459,679,672 +249,196,260,559,681,680 +202,211,50,446,682,425 +230,43,42,683,109,661 +44,230,163,684,484,356 +44,43,230,110,683,684 +222,227,175,584,476,685 +261,58,57,687,124,686 +261,255,61,689,665,688 +175,174,222,327,462,685 +57,255,261,666,689,686 +47,220,195,690,452,430 +18,200,213,692,454,691 +213,172,18,461,693,691 +14,13,201,80,378,401 +204,52,51,386,118,694 +244,12,11,520,78,626 +48,47,212,114,431,412 +162,161,260,303,696,695 +260,196,162,681,643,695 +191,204,51,434,694,409 +260,161,249,696,697,680 +148,16,15,280,82,679 +20,171,137,677,354,299 +162,46,45,645,112,309 +214,209,140,678,400,460 +13,206,168,673,389,377 +177,221,176,455,651,328 +246,261,61,699,688,698 +61,60,246,127,529,698 +58,261,246,687,699,531 +236,5,4,617,71,501 +236,237,7,497,654,613 +185,187,183,343,581,648 +211,51,50,408,117,682 +229,223,10,701,700,513 +258,253,34,703,670,702 +34,33,258,100,628,702 +31,253,258,669,703,631 +47,46,220,113,646,690 +49,48,208,115,413,457 +161,165,249,487,704,697 +223,11,10,625,77,700 +18,17,200,84,371,692 +229,174,176,482,325,705 +216,50,49,424,116,458 +176,223,229,652,701,705 +231,39,38,706,105,491 +147,17,16,370,83,281 +40,39,231,106,706,620 +172,19,18,374,85,693 +134,136,249,263,554,707 +249,165,134,704,307,707 +262,228,4,709,500,708 +262,156,158,711,292,710 +158,228,262,479,709,710 +4,156,262,552,711,708 +194,217,173,663,647,361 +243,143,186,713,676,712 +186,185,243,345,515,712 +54,143,243,574,713,546 diff --git a/data/simple/gendata.py b/data/simple/gendata.py index a9048384..501b8167 100644 --- a/data/simple/gendata.py +++ b/data/simple/gendata.py @@ -2,12 +2,20 @@ import os from pathlib import Path +from riley.python import meshconv + # Coordinate System: Right-handed Cartesian (X right, Y up, Z towards viewer). # Vertex Winding: All elements MUST follow Counter-Clockwise (CCW) winding. # This ensures positive signed area calculation in the rasterizer, which is # critical for correct shape function interpolation and weight distribution. def save_case(base_dir, name, coords, connect, disp_x, disp_y, disp_z): + mesh = meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + connect = meshconv.enforce_mesh_convention(mesh).connect["connect1"] out_dir = Path(base_dir) / name out_dir.mkdir(parents=True, exist_ok=True) np.savetxt(out_dir / "coords.csv", coords, delimiter=",") diff --git a/data/small/gendata.py b/data/small/gendata.py index a9048384..501b8167 100644 --- a/data/small/gendata.py +++ b/data/small/gendata.py @@ -2,12 +2,20 @@ import os from pathlib import Path +from riley.python import meshconv + # Coordinate System: Right-handed Cartesian (X right, Y up, Z towards viewer). # Vertex Winding: All elements MUST follow Counter-Clockwise (CCW) winding. # This ensures positive signed area calculation in the rasterizer, which is # critical for correct shape function interpolation and weight distribution. def save_case(base_dir, name, coords, connect, disp_x, disp_y, disp_z): + mesh = meshconv.MeshData( + coords=np.ascontiguousarray(coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + connect = meshconv.enforce_mesh_convention(mesh).connect["connect1"] out_dir = Path(base_dir) / name out_dir.mkdir(parents=True, exist_ok=True) np.savetxt(out_dir / "coords.csv", coords, delimiter=",") diff --git a/data/tilt/gen_tilt_data.py b/data/tilt/gen_tilt_data.py index 2e769564..6607435e 100644 --- a/data/tilt/gen_tilt_data.py +++ b/data/tilt/gen_tilt_data.py @@ -1,6 +1,8 @@ import numpy as np import os +from riley.python import meshconv + def save_csv(path, data): os.makedirs(os.path.dirname(path), exist_ok=True) np.savetxt( @@ -108,6 +110,13 @@ def generate_fullscreen_tilt(etype, out_dir): connect = np.array([[0, 1, 2, 3]]) tilted_coords = apply_tilt(coords) + connect = meshconv.enforce_mesh_convention( + meshconv.MeshData( + coords=np.ascontiguousarray(tilted_coords, dtype=np.float64), + connect={"connect1": np.ascontiguousarray(connect, dtype=np.int64)}, + mesh_type="surface", + ) + ).connect["connect1"] save_csv(f"{out_dir}/coords.csv", tilted_coords) save_csv(f"{out_dir}/connect.csv", connect) save_csv(f"{out_dir}/field.csv", compute_rgb_fields(tilted_coords)) diff --git a/dev/MESHCONVENTION.md b/dev/MESHCONVENTION.md new file mode 100644 index 00000000..ad2d6eed --- /dev/null +++ b/dev/MESHCONVENTION.md @@ -0,0 +1,136 @@ +# Riley mesh convention + +This document defines the mesh convention expected by Riley's rasteriser and +shape functions. A mesh must obey this convention before it is passed to the +Zig rendering path. + +`riley.python.meshconv` is the authoritative implementation and validator. + +## Mesh representation + +- Coordinates are an `N x 3` array named `coords`. +- A connectivity table is a row-major `E x P` integer array: one element per + row and one local node slot per column. +- Connectivity is zero-based. Every index must satisfy `0 <= index < N`. +- A mesh must use either surface or volume connectivity, not both. Mixed + element meshes are not supported by the convention converter. +- Local node reordering does not renumber global nodes. Nodal UVs, + displacements, and other node fields remain indexed by global node ID. + +## Element types + +Riley supports these element families: + +| Family | Types | Corner slots | Remaining local slots | +| --- | --- | --- | --- | +| Triangle | TRI3, TRI6, TRI7 | `0..2` | TRI6 edges `3..5`; TRI7 edges `3..5`, centre `6` | +| Quadrilateral | QUAD4, QUAD8, QUAD9 | `0..3` | QUAD8 edges `4..7`; QUAD9 edges `4..7`, centre `8` | +| Tetrahedron | TET4, TET10 | `0..3` | TET10 edges `4..9` | +| Hexahedron | HEX8, HEX20, HEX27 | `0..7` | HEX20 edges `8..19`; HEX27 edges `8..19`, faces `20..25`, cell centre `26` | + +Other node counts and topologies are rejected. Degenerate elements are input +errors and must not be repaired by reordering. + +## Orientation + +### Surface elements + +Surface connectivity must be consistently material-facing: + +- A closed exterior shell has outward pointing normals. +- A cavity boundary has normals pointing into the void. This is correct for a + plate-with-hole bore wall and must not be reversed merely because it points + towards the model centre. +- Adjacent faces must traverse a shared edge in opposite directions. +- Open planar surfaces use counter-clockwise winding when viewed from their + visible/material facing side. +- An open non-planar surface has no intrinsic exterior. The source must define + its material facing side. + +Riley tracks topology by global node identity, not coordinate equality. +Coincident-coordinate nodes at UV seams and poles are valid if they are +distinct node IDs. Non-manifold face sets do not have a unique shell +orientation. + +### Volume elements + +TET and HEX connectivity must have a positive right-handed signed metric. +This local ordering is significant: it determines the shape-function +coordinate system, interpolation of nodal fields, and extracted surface faces. + +## High-order node roles + +Higher-order nodes are not interchangeable. Their slots must match the +reference element's edge, face, and centre roles. Moving a mid-edge node to a +different edge slot changes interpolation even though the element contains the +same global node IDs. + +For a supplied known source convention, use `MeshConvention` to map each Riley +target slot to the source row slot. Do not create one-off exporter shortcuts in +Riley; keep exporter specific mappings at the import boundary (for example, +the PyVale Exodus adapter). + +## HEX27 and VTK + +Riley adopts VTK HEX27 local roles: + +| Slots | Role | +| --- | --- | +| `0..7` | corners | +| `8..19` | edge nodes | +| `20` | front face centre, corners `(0, 1, 5, 4)` | +| `21` | right face centre, corners `(1, 2, 6, 5)` | +| `22` | back face centre, corners `(2, 3, 7, 6)` | +| `23` | left face centre, corners `(3, 0, 4, 7)` | +| `24` | bottom face centre, corners `(0, 1, 2, 3)` | +| `25` | top face centre, corners `(4, 5, 6, 7)` | +| `26` | cell centre | + +This is not compatible with every Exodus style HEX27 ordering. Such data needs +an explicit source-to-Riley permutation before use. + +## Validation and enforcement + +Use the Python API before emitting or consuming mesh CSV data: + +```python +from riley.python import meshconv + +report = meshconv.check_mesh_convention(mesh) +if report: + raise ValueError(report) + +mesh = meshconv.enforce_mesh_convention(mesh) +``` + +For a known non-Riley source order, declare it explicitly: + +```python +source = meshconv.MeshConvention({ + meshconv.EElementType.HEX20: ( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 16, 17, 18, 19, 12, 13, 14, 15, + ), +}) +mesh = meshconv.enforce_mesh_convention(mesh, source) +``` + +`infer_mesh_convention(mesh)` is an opt in diagnostic. It can infer some +simple affine layouts and rejects ambiguous layouts. It is not yet the default +source order conversion path for `check_mesh_convention` or +`enforce_mesh_convention`; callers with a known source convention should pass +it explicitly. + +## Data-generation rule + +Prefer this order of work: + +1. Enforce/emit Riley-conforming connectivity. +2. Generate UVs, displacement fields, and other nodal data against that mesh. +3. Render and compare against the relevant regression baseline. + +If existing connectivity is locally reordered, global node fields do not need +to be reordered. They do need to be regenerated or visually checked when their +meaning depends on the element's local reference coordinates. Regenerate gold +only after render parity has been reviewed; save scaled TIFF alongside FIMG for +new gold generation. diff --git a/dev/PYTHONSTYLEGUIDE.md b/dev/PYTHONSTYLEGUIDE.md new file mode 100644 index 00000000..3b39e07a --- /dev/null +++ b/dev/PYTHONSTYLEGUIDE.md @@ -0,0 +1,134 @@ +# Riley: Python Style Guide + +## General Coding Guidance +- Prioritise an easy to remember and intuitive user API and performant code under the hood. +- Follow the PEP8 style guide: https://peps.python.org/pep-0008/ +- Format your code so it is readable, use an 80 character line length and put blank lines around logical groups of statements +- Use descriptive variable names, no single letter variables (double letters for iterators in numpy style are ok) single letter variables for indices / iterators are ok. +- Abbreviations are ok in variable names as long as they are not ambiguous for examples `calc` for `calculate`. +- Functions should have a verb as the first word in the function name that indicates what the function actually does. +- Avoid using magic numbers in code. If you need to use magic numbers, make + them a named module constant with a descriptive name and add a comment when + the name is not self-explanatory. +- Keep comprehensions to one line with one `for` loop and at most one function + call. Split comprehensions containing filters, nested loops, nested + comprehensions or multiple function calls into explicit statements and + loops. +- Keep `if` conditions to at most two lines and avoid nested function calls in + conditions. Calculate complex predicates in clearly named intermediate + statements before the `if`. +- Use major function first variable names: e.g. `FieldScalar`, `FieldVector` and `FieldTensor` instead of `ScalarField`, `VectorField` and `TensorField`. +- Type hint everything: e.g. `def add_ints(a: int, b: int) -> int:`. This makes your code easier to understand and you have the possibility of compiling things if you need. +- `pylint` is a slow linter but will help you if you have type hinted everything. `Ruff` is another good option, it is faster but doesn't pick up type hints as well. +- Use guard clauses (if statements) with returns at the top of functions to reduce the number of nested if/else structures. +- Default mutable data types (lists, dicts, objects) to `None` and then set them with an if statement guard clause +- Use `pathlib` and the `Path` class to manage all file io in preference to manual string handling or the `os` module. +- `numpy` and `scipy` are your friend - avoid for/while loops. Push everything you can down into C. Unless you are writing Cython then loops are great! +- Minimise dependencies as much as possible. +- Avoid decorators unless absolutely necessary (`@dataclass`, `@abstractmethod` and `@staticmethod` are examples that are ok) +- Don't use `@property`. It is normally used to hide complicated variable initialisation behind the `.` notation - just avoid `@property` altogether and just use a `@dataclass` for data only classes. +- No inheritance unless it is a purely abstract interface (python abstract base class `ABC`) - use composition / dependency injection. See this [video](https://www.youtube.com/watch?v=hxGOiiR9ZKg&t=3s) and thie [video](https://www.youtube.com/watch?v=J1f5b4vcxCQ&t=2s). +- Only use one layer of abstraction - don't inherit from multiple interfaces and don't use mix-ins. +- For interfaces (abstract base classes) prefix the name of the class with a capital `I` e.g. `ISensor` +- For enumerations prefix the name with a capital `E` so `EGeneratorType`. +- Only use abstraction/interfaces when if/else or switch has at least 3 implementations and/or becomes annoying. +- Use a mixture of plain functions and classes with methods where and when they make sense. +- Imports requiring many `.`'s are annoying and the user finds the layers hard to remember. Bring everything to the top level so it can be accessed with `pyvale.` +- Setup good defaults for variables where possible so that the user can get started with minimal input. +- Prefer dataclasses (`@dataclass`) to dictionaries as they tell the user what parameters are needed and can have sensible defaults. +- When using dataclasses `def __post_init__():` is useful for setting defaults for mutable data types. +- Use classes with `__slots__ = ("var1","var2",)` as it is more memory efficient, faster and stops member variables being added dynamically. For dataclasses use: `@dataclass(slots=True)` +- Write docstrings when the code is ready for sharing and use autodocstring to help. For `pyvale` we use `numpy` style docstrings. + +## Function Verb Meanings + +- `apply`: apply an already calculated operation, permutation or mask to data. +- `build`: assemble and return a compound structure, such as topology or an + adjacency map. +- `calc`: derive and return a new numeric value, array, mask or permutation. +- `check`: test a condition and return a boolean result without modifying the + input. Use `validate` instead when invalid input raises an exception. +- `convert`: change the representation, data type or shape of a value and + return the converted value. +- `copy`: return a new copy of an object, optionally replacing selected data. +- `enforce`: return data transformed to satisfy Riley's standard convention. +- `extract`: select and return a meaningful subset of existing data. +- `find`: search for and return a value whose location or existence is not + already known. +- `get`: retrieve or cheaply look up existing data or metadata. +- `infer`: determine semantic information from geometry, connectivity or other + evidence where the answer is not stored explicitly. +- `load`: read data from an external source and return its in-memory form. +- `match`: associate candidates with target roles according to stated rules. +- `normalise`: return an equivalent value in a standard representation or + numerical range. +- `order`: determine or apply a meaningful sequence to existing values. +- `prepare`: perform the named prerequisite transformations for a subsequent + operation and return the prepared data. +- `process`: avoid this verb when a more precise verb describes the operation; + use it only for a genuine multi-stage pipeline. +- `restore`: transform standard or working data back to its source + representation. +- `reverse`: return values with the relevant ordering or orientation reversed. +- `save`: write in-memory data to an external destination. +- `update`: modify an object's stored state in place. +- `validate`: verify input requirements and raise a clear exception when they + are not satisfied. + +## Abbreviations + +- array, Array -> arr, Arr +- boolean, Boolean -> bool, Bool +- calculate, Calculate -> calc, Calc +- component, Component -> comp, Comp +- configuration, Configuration -> config, Config +- connectivity, Connectivity -> connect, Connect +- coordinate, Coordinate -> coord, Coord +- convert, Convert -> conv, Conv +- destination, Destination -> dest, Dest +- dimension, Dimension -> dim, Dim +- displacement, Displacement -> disp, Disp +- direction, Direction -> direct, Direct +- element, Element -> elem, Elem +- error, Error -> err, Err +- equivalent, Equivalent -> equiv, Equiv +- geometry, Geometry -> geom, Geom +- global, Global -> glob, Glob +- identifier, Identifier -> id, Id +- image, Image -> img, Img +- independent, Independent -> indep, Indep +- index, Index -> idx, Idx +- indices, Indices -> idxs, Idxs +- local, Local -> loc, Loc +- maximum, Maximum -> max, Max +- minimum, Minimum -> min, Min +- number, Number -> num, Num +- orientation, Orientation -> orient, Orient +- parameter, Parameter -> param, Param +- permutation, Permutation -> perm, Perm +- pixel, Pixel -> px, Px +- projection, Projection -> proj, Proj +- reference, Reference -> ref, Ref +- relative, Relative -> rel, Rel +- simulation, Simulation -> sim, Sim +- source, Source -> src, Src +- specification, Specification -> spec, Spec +- standard, Standard -> std, Std +- surface, Surface -> surf, Surf +- temporary, Temporary -> temp, Temp +- texture, Texture -> tex, Tex +- transformation, Transformation -> transf, Transf +- vector, Vector -> vec, Vec +- volume, Volume -> vol, Vol + +Avoid abbreviations that are ambiguous in context. In particular, do not use +`norm` for `normal`, because it can also mean a vector or matrix norm. + +## Variable Suffixes + +- `_in`: a function input converted, copied or otherwise prepared internally. +- `_out`: a value constructed for return from a function. +- `_raw`: an unvalidated or unprocessed source value. +- `_std`: a value in Riley's standard convention. +- `_loc`: a local index or value in an element or other containing structure. +- `_glob`: a global index or value in the complete mesh or scene. diff --git a/dev/README.md b/dev/README.md index 531c42e5..63ddca40 100644 --- a/dev/README.md +++ b/dev/README.md @@ -223,6 +223,7 @@ Run a packaged Python demo directly with: ```shell python -m riley demo_sphere200 +python -m riley demo_psf python -m riley demo_rabbits python -m riley demo_dicuq python -m riley demo_dic_from_exodus diff --git a/dev/spec_procedural_speckles.md b/dev/designspecs/spec_procedural_speckles.md similarity index 100% rename from dev/spec_procedural_speckles.md rename to dev/designspecs/spec_procedural_speckles.md diff --git a/gold/min/sphere200/base/quad4newton_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_nodal_grey/cam0_frame0_field0.fimg index 62359136..74314cf0 100644 Binary files a/gold/min/sphere200/base/quad4newton_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg index d86603eb..71eb5d09 100644 Binary files a/gold/min/sphere200/base/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index a6a3a494..ca10488b 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index 9f336ea4..37bba6d0 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index c155469d..0733d159 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 5ad31ed5..642d15a9 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index e04ccd55..8dcd33f1 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg index 7cce8ab2..2157e23e 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index ef62c7eb..7d179447 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index 0749b4f2..ba5fab2c 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index 4f7142eb..f13c5a3b 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index e0e6b418..f3c1744d 100644 Binary files a/gold/min/sphere200/base/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200/base/quad8_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_nodal_grey/cam0_frame0_field0.fimg index b259514d..125d02ec 100644 Binary files a/gold/min/sphere200/base/quad8_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg index b191875f..539a5a36 100644 Binary files a/gold/min/sphere200/base/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index 02927e2a..51360a2e 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index effd0f1a..95395bc1 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index 20389608..b1450b28 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 7600674d..87cb27b3 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index 0f39e72a..a4f42d67 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg index 539d6915..a8d5f91c 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index 0c9be310..e5d03659 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index 881243fa..d1efff59 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index 4a23f3bb..cfdc1038 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index ddedda55..201aeba7 100644 Binary files a/gold/min/sphere200/base/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200/base/quad9_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_nodal_grey/cam0_frame0_field0.fimg index eabfaad1..b53a0fa1 100644 Binary files a/gold/min/sphere200/base/quad9_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg index fe50bd4d..a786474f 100644 Binary files a/gold/min/sphere200/base/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index 1b7c59b7..7809261f 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index 8e18eecb..321f89f2 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index a4cc19c8..05e33b89 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 4b2796cd..db36a92f 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index 062658b0..88b7b342 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg index 70547d83..714b00dc 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index c6f45a41..505b0d6a 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index 61b8a86f..b4eb9658 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index b4198ed7..8094eba3 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200/base/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200/base/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200/base/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index fac72e08..c6369235 100644 Binary files a/gold/min/sphere200/base/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200/base/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_nodal_grey/cam0_frame0_field0.fimg index c77a7c32..c2adecbd 100644 Binary files a/gold/min/sphere200multicull/quad4newton_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg index 96454f60..0cfdffe6 100644 Binary files a/gold/min/sphere200multicull/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad4newton_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index 6316739a..5fdd193b 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index 09da5614..1df34aa7 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index e55c3a9e..aba782d9 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 9a877da2..92225775 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index 6d37d07a..c28da763 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg index aa345384..086363ed 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index 0b1abc47..b1d8d3a5 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index 87bdcc18..63cc1b1e 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index 051768d6..674a05b0 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index a2c89f42..8bc667bf 100644 Binary files a/gold/min/sphere200multicull/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad4newton_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad8_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_nodal_grey/cam0_frame0_field0.fimg index e76a067b..22e6ca19 100644 Binary files a/gold/min/sphere200multicull/quad8_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg index 41fd4344..640c70d3 100644 Binary files a/gold/min/sphere200multicull/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad8_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index 7d9ca147..1f880cfd 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index 123cf617..58b66d10 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index 3839eccc..fc5c8429 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 4356ddcb..9a023255 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index d96bebfc..c128b905 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg index a883b1ee..d4c40241 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index f5b5f43e..e817c4ff 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index 997aad81..de8b965e 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index 2d7ca169..31d3e404 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad8_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index 85befa7c..1ef49521 100644 Binary files a/gold/min/sphere200multicull/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad8_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad9_nodal_grey/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_nodal_grey/cam0_frame0_field0.fimg index d7285401..908d8eab 100644 Binary files a/gold/min/sphere200multicull/quad9_nodal_grey/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_nodal_grey/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg index 19589369..bc3378e5 100644 Binary files a/gold/min/sphere200multicull/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad9_nodal_rgb/cam0_frame0_field0_rgb.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg index e7d580d3..a210ccbb 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg index b28c0dad..9de04552 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg index d72406fa..0ba487dd 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_catmull_rom_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg index 721de448..6148cb88 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_cubic_mitchell_netravali_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg index 14825a45..c7683819 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_lanczos3_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg index 008606c5..b11aec3e 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_linear_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg index 6d48dfaf..0a9a9f75 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_nearest_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg index eb3d55b0..38e851a4 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_direct/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg b/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg index f5091e98..2d9d3b7e 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg and b/gold/min/sphere200multicull/quad9_tex8_grey_quintic_bspline_lut_lerp/cam0_frame0_field0.fimg differ diff --git a/gold/min/sphere200multicull/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg b/gold/min/sphere200multicull/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg index 33090a76..441a2572 100644 Binary files a/gold/min/sphere200multicull/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg and b/gold/min/sphere200multicull/quad9_tex8_rgb_cubic_catmull_rom_lut_lerp/cam0_frame0_field0_rgb.fimg differ diff --git a/pyproject.toml b/pyproject.toml index 9ec15b8d..0b3a3100 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "riley-raster" -version = "2026.7.1" +version = "2026.9.0" description = "A python->Zig CPU rasteriser." authors = [ { name = "scepticalrabbit (Lloyd Fletcher)", email = "thescepticalrabbit@gmail.com" }, ] -license = "MIT" +license = { text = "MIT" } readme = "README.md" requires-python = ">=3.11.0" dependencies = [ diff --git a/requirements.txt b/requirements.txt index ef14b81a..a14e94cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,7 +41,6 @@ PyQt6_sip==13.10.2 pytest==8.4.0 python-dateutil==2.9.0.post0 pytz==2025.2 --e git+ssh://git@github.com/Computer-Aided-Validation-Laboratory/pyvale.git@acf8b92b911306fc1c7d0b73c3677361d68310bb#egg=pyvale pyvista==0.45.2 PyYAML==6.0.2 requests==2.32.3 diff --git a/scripts/python_package_data.toml b/scripts/python_package_data.toml index 8520713e..8bea9e28 100644 --- a/scripts/python_package_data.toml +++ b/scripts/python_package_data.toml @@ -26,6 +26,110 @@ dest = "min/tri6_sphere200/field.csv" source = "data/min/tri6_sphere200/uvs.csv" dest = "min/tri6_sphere200/uvs.csv" +[[copy]] +source = "data/min/tri3_sphere200/connect.csv" +dest = "min/tri3_sphere200/connect.csv" + +[[copy]] +source = "data/min/tri3_sphere200/coords.csv" +dest = "min/tri3_sphere200/coords.csv" + +[[copy]] +source = "data/min/tri3_sphere200/field.csv" +dest = "min/tri3_sphere200/field.csv" + +[[copy]] +source = "data/min/tri3_sphere200/uvs.csv" +dest = "min/tri3_sphere200/uvs.csv" + +[[copy]] +source = "data/min/quad4newton_sphere200/connect.csv" +dest = "min/quad4newton_sphere200/connect.csv" + +[[copy]] +source = "data/min/quad4newton_sphere200/coords.csv" +dest = "min/quad4newton_sphere200/coords.csv" + +[[copy]] +source = "data/min/quad4newton_sphere200/field.csv" +dest = "min/quad4newton_sphere200/field.csv" + +[[copy]] +source = "data/min/quad4newton_sphere200/uvs.csv" +dest = "min/quad4newton_sphere200/uvs.csv" + +[[copy]] +source = "data/min/quad8_sphere200/connect.csv" +dest = "min/quad8_sphere200/connect.csv" + +[[copy]] +source = "data/min/quad8_sphere200/coords.csv" +dest = "min/quad8_sphere200/coords.csv" + +[[copy]] +source = "data/min/quad8_sphere200/field.csv" +dest = "min/quad8_sphere200/field.csv" + +[[copy]] +source = "data/min/quad8_sphere200/uvs.csv" +dest = "min/quad8_sphere200/uvs.csv" + +[[copy]] +source = "data/min/quad9_sphere200/connect.csv" +dest = "min/quad9_sphere200/connect.csv" + +[[copy]] +source = "data/min/quad9_sphere200/coords.csv" +dest = "min/quad9_sphere200/coords.csv" + +[[copy]] +source = "data/min/quad9_sphere200/field.csv" +dest = "min/quad9_sphere200/field.csv" + +[[copy]] +source = "data/min/quad9_sphere200/uvs.csv" +dest = "min/quad9_sphere200/uvs.csv" + +[[copy]] +source = "data/cubes/tet4/coords.csv" +dest = "cubes/tet4/coords.csv" + +[[copy]] +source = "data/cubes/tet4/connectivity.csv" +dest = "cubes/tet4/connectivity.csv" + +[[copy]] +source = "data/cubes/tet10/coords.csv" +dest = "cubes/tet10/coords.csv" + +[[copy]] +source = "data/cubes/tet10/connectivity.csv" +dest = "cubes/tet10/connectivity.csv" + +[[copy]] +source = "data/cubes/hex8/coords.csv" +dest = "cubes/hex8/coords.csv" + +[[copy]] +source = "data/cubes/hex8/connectivity.csv" +dest = "cubes/hex8/connectivity.csv" + +[[copy]] +source = "data/cubes/hex20/coords.csv" +dest = "cubes/hex20/coords.csv" + +[[copy]] +source = "data/cubes/hex20/connectivity.csv" +dest = "cubes/hex20/connectivity.csv" + +[[copy]] +source = "data/cubes/hex27/coords.csv" +dest = "cubes/hex27/coords.csv" + +[[copy]] +source = "data/cubes/hex27/connectivity.csv" +dest = "cubes/hex27/connectivity.csv" + [[copy]] source = "data/FE/platehole3d_2mr_63f/connect.csv" dest = "fe/platehole3d_2mr_63f/connect.csv" diff --git a/setup.py b/setup.py index c7b857fb..51c62367 100644 --- a/setup.py +++ b/setup.py @@ -386,7 +386,7 @@ def build_extension(self, ext): H_DIRS = [ numpy.get_include(), str(PROJECT_ROOT / "src"), - str(PROJECT_ROOT / "src" / "riley" / "cyth"), + str(PROJECT_ROOT / "src" / "riley" / "cython"), str(PROJECT_ROOT / "src" / "riley" / "zig"), ] @@ -414,8 +414,8 @@ def build_extension(self, ext): # cython extension linking zig ext_cython = Extension( - name="riley.cyth.riley", - sources=["src/riley/cyth/riley.py",], + name="riley.cython.riley", + sources=["src/riley/cython/riley.py",], include_dirs=H_DIRS, libraries=["c_riley"], library_dirs=[], # populated by run() above @@ -441,7 +441,7 @@ def build_extension(self, ext): zip_safe=False, package_data={ "riley": [f"*{PLATFORM_INFO['lib_ext']}"], - "riley.cyth": [f"*{PLATFORM_INFO['lib_ext']}"], + "riley.cython": [f"*{PLATFORM_INFO['lib_ext']}"], "riley.zig": [f"*{PLATFORM_INFO['lib_ext']}"], "": [f"*{PLATFORM_INFO['lib_ext']}"], }, diff --git a/src/demo_dicuq.zig b/src/demo_dicuq.zig index 8432ea65..5abab0b8 100644 --- a/src/demo_dicuq.zig +++ b/src/demo_dicuq.zig @@ -9,6 +9,7 @@ const std = @import("std"); const print = std.debug.print; +const demoframes = @import("dev_support/demoframes.zig"); const buildconfig = @import("riley/zig/buildconfig.zig"); const riley = @import("riley/zig/riley.zig"); const RasterConfig = riley.RasterConfig; @@ -152,6 +153,16 @@ pub fn main(init: std.process.Init) !void { null, field_files, ); + const disp_source = sim_data.disp orelse return error.MissingDisplacement; + const frame_indices = try demoframes.firstLastIndices( + aa, + disp_source.getTimeN(), + ); + const selected_disp = try demoframes.selectFieldFrames( + aa, + &disp_source, + frame_indices, + ); // 3. Load UV map for the texture std.debug.print("Loading UV map...\n", .{}); @@ -175,7 +186,7 @@ pub fn main(init: std.process.Init) !void { .mesh_type = .quad8, .coords = sim_data.coords, .connect = sim_data.connect, - .disp = sim_data.disp, + .disp = selected_disp, .shader = .{ .tex_u8 = .{ .uvs = uvs.array, .tex = texture, @@ -253,6 +264,10 @@ pub fn main(init: std.process.Init) !void { const meshes = [_]MeshInput{mesh_input}; const cams_in = [_]CameraInput{ cam0_in, cam1_in }; + std.Io.Dir.cwd().deleteTree(io, OUT_DIR_ROOT) catch |err| { + if (err != error.FileNotFound) return err; + }; + const images = try riley.raster( aa, render_groups, diff --git a/src/demo_psf.zig b/src/demo_psf.zig new file mode 100644 index 00000000..cd58e1f0 --- /dev/null +++ b/src/demo_psf.zig @@ -0,0 +1,147 @@ +// -------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------- +const std = @import("std"); + +const buildconfig = @import("riley/zig/buildconfig.zig"); +const riley = @import("riley/zig/riley.zig"); +const meshio = @import("riley/zig/meshio.zig"); +const uvio = @import("riley/zig/uvio.zig"); +const iio = @import("riley/zig/imageio.zig"); +const mo = @import("riley/zig/meshpipeline.zig"); +const camera = @import("riley/zig/camera.zig"); +const cameraops = @import("riley/zig/cameraops.zig"); +const sceneops = @import("riley/zig/sceneops.zig"); +const Rotation = @import("riley/zig/rotation.zig").Rotation; + +const F = buildconfig.F; +const raster_threads: u16 = 8; + +pub fn main(init: std.process.Init) !void { + const outer_alloc = init.gpa; + var arena = std.heap.ArenaAllocator.init(outer_alloc); + defer arena.deinit(); + const aa = arena.allocator(); + + const config_base = riley.RasterConfig{ + .save_strategy = .disk, + .total_threads = raster_threads, + .max_raster_workers_per_job = raster_threads, + .image_save_opts = &[_]iio.ImageSaveOpts{ + .{ .format = .bmp, .bits = 8, .scaling = .auto }, + }, + .report = .bench, + }; + var threaded_io = riley.getThreadedIo( + aa, + init.minimal, + config_base.total_threads, + ); + defer threaded_io.deinit(); + const io = threaded_io.io(); + + const data_dir = "data/min/tri6_sphere200/"; + const out_dir_root = "./out/demo-psf"; + const pixel_num = [_]u32{ 800, 500 }; + + std.debug.print( + "Loading sphere simulation data from {s} with {d} raster threads...\n", + .{ data_dir, raster_threads }, + ); + const sim_data = try meshio.loadSimData( + aa, + io, + data_dir ++ "coords.csv", + data_dir ++ "connect.csv", + null, + null, + ); + const uvs = try uvio.loadUVMap(aa, io, data_dir ++ "uvs.csv"); + const texture = try iio.loadImage( + u8, + 1, + aa, + io, + "texture/speckle.bmp", + .bmp, + ); + const mesh = mo.MeshInput{ + .mesh_type = .tri6, + .coords = sim_data.coords, + .connect = sim_data.connect, + .disp = null, + .shader = .{ .tex_u8 = .{ + .uvs = uvs.array, + .tex = texture, + .samp_cfg = .{ + .sample = .cubic_catmull_rom, + .mode = .lut_lerp, + }, + .bits = 8, + .scaling = .none, + } }, + }; + + const pixel_size = [_]F{ @floatCast(5.3e-6), @floatCast(5.3e-6) }; + const focal_length: F = @floatCast(50.0e-3); + const rotation = Rotation.init(0, 0, 0); + const roi_cent_world = sceneops.boundsCenter(&sim_data.coords); + const pos_world = cameraops.posFillFrameFromRot( + &sim_data.coords, + pixel_num, + pixel_size, + focal_length, + rotation, + 1.0, + ); + const camera_input = camera.CameraInput{ + .pixels_num = pixel_num, + .pixels_size = pixel_size, + .pos_world = pos_world, + .rot_world = rotation, + .roi_cent_world = roi_cent_world, + .focal_length = focal_length, + .sub_sample = 2, + .psf = .{ .gaussian = .{ + .sigma_px = 1.0, + .supp_rad_px = 3.0, + .separable = .yes, + } }, + }; + const render_groups = [_]riley.RenderGroupSpec{ + .{ .io = io, .workers = config_base.total_threads }, + }; + const modes = [_]riley.BufferMode{ + .global_subpx_full, + .global_subpx_stripe, + }; + + for (modes) |mode| { + var config = config_base; + config.buffer_mode = mode; + const out_dir = try std.fs.path.join( + aa, + &[_][]const u8{ out_dir_root, @tagName(mode) }, + ); + std.debug.print("Rendering PSF sphere with {s}...\n", .{@tagName(mode)}); + if (try riley.raster( + aa, + &render_groups, + &[_]camera.CameraInput{camera_input}, + &[_]mo.MeshInput{mesh}, + config, + out_dir, + )) |image| { + aa.free(image.slice); + var image_mut = image; + image_mut.deinit(aa); + } + } + + std.debug.print("Demo complete. Images saved to {s}/\n", .{out_dir_root}); +} diff --git a/src/demo_rabbits.zig b/src/demo_rabbits.zig index 97c8d298..b5d50fe8 100644 --- a/src/demo_rabbits.zig +++ b/src/demo_rabbits.zig @@ -34,7 +34,7 @@ const F = buildconfig.F; const rabbit_mesh_types = [_]gk.MeshType{ .tri3, .tri6, - .quad4ibi, + .quad4newton, .quad8, .quad9, }; @@ -258,7 +258,8 @@ pub fn main(init: std.process.Init) !void { ); const mesh_inputs = try buildRabbitPairScene(aa, io, texture); - const rot = Rotation.init(0.0, std.math.pi, 0.0); + // Canonical rabbit winding exposes the opposite side from the legacy data. + const rot = Rotation.init(0.0, 0.0, 0.0); const roi_pos = sceneops.boundsCenterOverMeshes(mesh_inputs); const cam_pos = cameraops.posFillFrameFromRotOverMeshes( mesh_inputs, diff --git a/src/demo_rabbits_fields.zig b/src/demo_rabbits_fields.zig index 3a79f927..28f7f10c 100644 --- a/src/demo_rabbits_fields.zig +++ b/src/demo_rabbits_fields.zig @@ -34,7 +34,7 @@ const F = buildconfig.F; const rabbit_mesh_types = [_]gk.MeshType{ .tri3, .tri6, - .quad4ibi, + .quad4newton, .quad8, .quad9, }; diff --git a/src/demo_rabbits_rgb.zig b/src/demo_rabbits_rgb.zig index 6b912ac1..acfef27a 100644 --- a/src/demo_rabbits_rgb.zig +++ b/src/demo_rabbits_rgb.zig @@ -34,7 +34,7 @@ const F = buildconfig.F; const rabbit_mesh_types = [_]gk.MeshType{ .tri3, .tri6, - .quad4ibi, + .quad4newton, .quad8, .quad9, }; diff --git a/src/demo_stereocal.zig b/src/demo_stereocal.zig index e91ef6f8..4c1fb7bb 100644 --- a/src/demo_stereocal.zig +++ b/src/demo_stereocal.zig @@ -8,6 +8,7 @@ // -------------------------------------------------------------------------- const std = @import("std"); +const demoframes = @import("dev_support/demoframes.zig"); const buildconfig = @import("riley/zig/buildconfig.zig"); const riley = @import("riley/zig/riley.zig"); const RasterConfig = riley.RasterConfig; @@ -20,6 +21,7 @@ const camera_mod = @import("riley/zig/camera.zig"); const cameraio = @import("riley/zig/cameraio.zig"); const cameraops = @import("riley/zig/cameraops.zig"); const sceneops = @import("riley/zig/sceneops.zig"); +const vecstack = @import("riley/zig/vecstack.zig"); const Rotation = @import("riley/zig/rotation.zig").Rotation; const DistortionModel = camera_mod.DistortionModel; const BrownConrady = camera_mod.BrownConrady; @@ -39,7 +41,23 @@ const FOCAL_LENGTH: F = @floatCast(50.0e-3); const FOV_SCALE_FACTOR: F = 1.0; const STEREO_ANGLE_DEG: F = 20.0; const SUB_SAMPLE: u32 = 2; -const DICUQ_CAMERA_DIR = "./out/demo-dicuq"; +const FRAMES_MAX: usize = 8; + +const MATCHED_ROI = [3]F{ + @floatCast(0.0125), + @floatCast(0.0175), + @floatCast(0.0005), +}; +const MATCHED_CAM0_POS = [3]F{ + @floatCast(0.0125), + @floatCast(0.0175), + @floatCast(0.160864856482), +}; +const MATCHED_CAM1_POS = [3]F{ + @floatCast(0.067348011198), + @floatCast(0.0175), + @floatCast(0.151193672270), +}; const TOTAL_THREADS: u16 = 8; const RENDER_GROUP_COUNT: u16 = 8; @@ -53,12 +71,7 @@ const DistortionCase = enum { const DISTORTION_CASE: DistortionCase = .brown_conrady; -const CameraPlacementMode = enum { - auto_fov, - load_stereo_pair, -}; - -const CAMERA_PLACEMENT_MODE: CameraPlacementMode = .load_stereo_pair; +const STEREO_FILE_NAME = "stereo_data_opengl.csv"; fn buildDistortion() DistortionModel { return switch (DISTORTION_CASE) { @@ -105,8 +118,8 @@ pub fn main(init: std.process.Init) !void { const config = RasterConfig{ .render_mode = .offline, .total_threads = TOTAL_THREADS, - .frame_batch_size_per_group = 8, - .max_geom_jobs_in_flight_per_group = 8, + .frame_batch_size_per_group = FRAMES_MAX, + .max_geom_jobs_in_flight_per_group = FRAMES_MAX, .max_geom_workers_per_job = 1, .geom_scheduling_mode = .spread, .max_raster_workers_per_job = 1, @@ -160,6 +173,9 @@ pub fn main(init: std.process.Init) !void { cwd.createDir(io, "out", .default_dir) catch |err| { if (err != error.PathAlreadyExists) return err; }; + cwd.deleteTree(io, OUT_DIR_ROOT) catch |err| { + if (err != error.FileNotFound) return err; + }; cwd.createDir(io, OUT_DIR_ROOT, .default_dir) catch |err| { if (err != error.PathAlreadyExists) return err; }; @@ -178,6 +194,18 @@ pub fn main(init: std.process.Init) !void { disp_paths, ); defer sim_data.deinit(aa); + const disp_source = sim_data.disp orelse return error.MissingDisplacement; + const frame_indices = try demoframes.evenlySpacedIndices( + aa, + disp_source.getTimeN(), + FRAMES_MAX, + ); + var selected_disp = try demoframes.selectFieldFrames( + aa, + &disp_source, + frame_indices, + ); + defer selected_disp.deinit(aa); var uvs = try uvio.loadUVMap(aa, io, uv_path); defer uvs.deinit(aa); @@ -194,106 +222,94 @@ pub fn main(init: std.process.Init) !void { const distortion = buildDistortion(); - var roi_pos = sceneops.boundsCenter(&sim_data.coords); + const target_roi = vecstack.initVec3( + F, + MATCHED_ROI[0], + MATCHED_ROI[1], + MATCHED_ROI[2], + ); + const roi_pos_orig = sceneops.boundsCenter(&sim_data.coords); + const roi_shift = target_roi.sub(roi_pos_orig); + for (0..sim_data.coords.mat.rows_num) |nn| { + sim_data.coords.mat.set( + nn, + 0, + sim_data.coords.mat.get(nn, 0) + roi_shift.get(0), + ); + sim_data.coords.mat.set( + nn, + 1, + sim_data.coords.mat.get(nn, 1) + roi_shift.get(1), + ); + sim_data.coords.mat.set( + nn, + 2, + sim_data.coords.mat.get(nn, 2) + roi_shift.get(2), + ); + } + const roi_pos = sceneops.boundsCenter(&sim_data.coords); - var stereo_pair = switch (CAMERA_PLACEMENT_MODE) { - .auto_fov => blk: { - const cam0_rot = Rotation.init( - std.math.degreesToRadians(0.0), - std.math.degreesToRadians(0.0), - std.math.degreesToRadians(0.0), - ); - const cam1_rot = Rotation.init( - std.math.degreesToRadians(0.0), - std.math.degreesToRadians(STEREO_ANGLE_DEG), - std.math.degreesToRadians(0.0), - ); + // Create stereo camera pair matching the DICUQ example setup + const cam0_rot = Rotation.init( + std.math.degreesToRadians(0.0), + std.math.degreesToRadians(0.0), + std.math.degreesToRadians(0.0), + ); + const cam1_rot = Rotation.init( + std.math.degreesToRadians(0.0), + std.math.degreesToRadians(STEREO_ANGLE_DEG), + std.math.degreesToRadians(0.0), + ); - const cam0_pos = cameraops.posFillFrameFromRot( - &sim_data.coords, - PIXELS_NUM, - PIXELS_SIZE, - FOCAL_LENGTH, - cam0_rot, - FOV_SCALE_FACTOR, - ); - const cam1_pos = cameraops.posFillFrameFromRot( - &sim_data.coords, - PIXELS_NUM, - PIXELS_SIZE, - FOCAL_LENGTH, - cam1_rot, - FOV_SCALE_FACTOR, - ); + const cam0_pos = vecstack.initVec3( + F, + MATCHED_CAM0_POS[0], + MATCHED_CAM0_POS[1], + MATCHED_CAM0_POS[2], + ); + const cam1_pos = vecstack.initVec3( + F, + MATCHED_CAM1_POS[0], + MATCHED_CAM1_POS[1], + MATCHED_CAM1_POS[2], + ); - break :blk StereoPairInput{ - .cameras = .{ - .{ - .pixels_num = PIXELS_NUM, - .pixels_size = PIXELS_SIZE, - .pos_world = cam0_pos, - .rot_world = cam0_rot, - .roi_cent_world = roi_pos, - .focal_length = FOCAL_LENGTH, - .sub_sample = SUB_SAMPLE, - .distortion = distortion, - }, - .{ - .pixels_num = PIXELS_NUM, - .pixels_size = PIXELS_SIZE, - .pos_world = cam1_pos, - .rot_world = cam1_rot, - .roi_cent_world = roi_pos, - .focal_length = FOCAL_LENGTH, - .sub_sample = SUB_SAMPLE, - .distortion = distortion, - }, - }, - }; - }, - .load_stereo_pair => blk: { - var stereo_in_dir = try cwd.openDir(io, DICUQ_CAMERA_DIR, .{}); - defer stereo_in_dir.close(io); - break :blk try cameraio.loadStereoPair( - aa, - io, - stereo_in_dir, - stereo_file_name, - ); + var stereo_pair = StereoPairInput{ + .cameras = .{ + .{ + .pixels_num = PIXELS_NUM, + .pixels_size = PIXELS_SIZE, + .pos_world = cam0_pos, + .rot_world = cam0_rot, + .roi_cent_world = roi_pos, + .focal_length = FOCAL_LENGTH, + .sub_sample = SUB_SAMPLE, + .distortion = distortion, + }, + .{ + .pixels_num = PIXELS_NUM, + .pixels_size = PIXELS_SIZE, + .pos_world = cam1_pos, + .rot_world = cam1_rot, + .roi_cent_world = roi_pos, + .focal_length = FOCAL_LENGTH, + .sub_sample = SUB_SAMPLE, + .distortion = distortion, + }, }, }; - if (CAMERA_PLACEMENT_MODE == .load_stereo_pair) { - const target_roi = stereo_pair.cameras[0].roi_cent_world; - const roi_shift = target_roi.sub(roi_pos); - for (0..sim_data.coords.mat.rows_num) |nn| { - sim_data.coords.mat.set( - nn, - 0, - sim_data.coords.mat.get(nn, 0) + roi_shift.get(0), - ); - sim_data.coords.mat.set( - nn, - 1, - sim_data.coords.mat.get(nn, 1) + roi_shift.get(1), - ); - sim_data.coords.mat.set( - nn, - 2, - sim_data.coords.mat.get(nn, 2) + roi_shift.get(2), - ); - } - roi_pos = sceneops.boundsCenter(&sim_data.coords); - stereo_pair.cameras[0].roi_cent_world = roi_pos; - stereo_pair.cameras[1].roi_cent_world = roi_pos; - } + // Save stereo pair to output directory try cameraio.saveStereoPair(io, out_dir, stereo_file_name, stereo_pair); + // Load stereo pair back from output directory (standalone test) + stereo_pair = try cameraio.loadStereoPair(aa, io, out_dir, stereo_file_name); + const mesh_input = MeshInput{ .mesh_type = .tri3, .coords = sim_data.coords, .connect = sim_data.connect, - .disp = sim_data.disp, + .disp = selected_disp, .shader = .{ .tex_u8 = .{ .uvs = uvs.array, .tex = texture, diff --git a/src/dev_support/benchcommon.zig b/src/dev_support/benchcommon.zig index dbfae036..a1362958 100644 --- a/src/dev_support/benchcommon.zig +++ b/src/dev_support/benchcommon.zig @@ -131,7 +131,7 @@ pub fn calcMetrics( frame_times: report.FrameTimes, bench_log: report.BenchLog, ) CalculatedMetrics { - const raster_sec = frame_times.raster_loop / 1e9; + const raster_sec = report.rasterStageTime(frame_times) / 1e9; const geom_tiling_sec = (frame_times.geometry_prep + frame_times.tile_overlap) / 1e9; const active_sec = frame_times.active_time / 1e9; @@ -1060,10 +1060,7 @@ fn runBenchmarkInternal( } var bench_capture_storage: [1]report.FrameBenchCapture = undefined; - const bench_capture: ?[]report.FrameBenchCapture = if (report_mode == .bench) - bench_capture_storage[0..] - else - null; + const bench_capture: ?[]report.FrameBenchCapture = bench_capture_storage[0..]; const needs_images_arr = config_run.save_strategy == .memory or config_run.save_strategy == .both; @@ -1105,7 +1102,7 @@ fn runBenchmarkInternal( else 0.0; const raster_ms = if (report_mode == .bench) - bench_capture_storage[0].bench_log.frame_times.raster_loop / 1e6 + report.rasterStageTime(bench_capture_storage[0].bench_log.frame_times) / 1e6 else 0.0; const metrics = if (report_mode == .bench) @@ -1148,10 +1145,7 @@ fn runBenchmarkInternal( images_mut.deinit(outer_alloc); } - const pipeline_times = if (report_mode == .bench) - bench_capture_storage[0].bench_log.frame_times - else - report.FrameTimes{}; + const pipeline_times = bench_capture_storage[0].bench_log.frame_times; return .{ .e2e_ms = e2e_ms, diff --git a/src/dev_support/benchdicuq.zig b/src/dev_support/benchdicuq.zig index b85a179b..a44e6faa 100644 --- a/src/dev_support/benchdicuq.zig +++ b/src/dev_support/benchdicuq.zig @@ -396,7 +396,7 @@ pub fn runBenchmark( .e2e_ms = e2e_ms, .geom_ms = (frame_times.geometry_prep + frame_times.tile_overlap) / 1e6, - .raster_ms = frame_times.raster_loop / 1e6, + .raster_ms = report.rasterStageTime(frame_times) / 1e6, .cam_ms = frame_times.cam_invert / 1e6, .resolve_ms = frame_times.scratch_resolve / 1e6, .fps = if (e2e_ms > 0) @@ -438,6 +438,16 @@ fn aggregateFrameTimes( capture.bench_log.frame_times.tile_overlap; frame_times.raster_loop += capture.bench_log.frame_times.raster_loop; + frame_times.global_subpx_times.buffer_setup += + capture.bench_log.frame_times.global_subpx_times.buffer_setup; + frame_times.global_subpx_times.tile_raster += + capture.bench_log.frame_times.global_subpx_times.tile_raster; + frame_times.global_subpx_times.resolve += + capture.bench_log.frame_times.global_subpx_times.resolve; + if (frame_times.global_subpx_stats == null) { + frame_times.global_subpx_stats = + capture.bench_log.frame_times.global_subpx_stats; + } frame_times.cam_invert += capture.bench_log.frame_times.cam_invert; frame_times.elem_loop += @@ -479,7 +489,7 @@ fn calcDicuqMetrics( frame_times: report.FrameTimes, bench_log: report.BenchLog, ) common.CalculatedMetrics { - const raster_sec = frame_times.raster_loop / 1e9; + const raster_sec = report.rasterStageTime(frame_times) / 1e9; const geom_tiling_sec = (frame_times.geometry_prep + frame_times.tile_overlap) / 1e9; const active_sec = frame_times.active_time / 1e9; @@ -617,7 +627,7 @@ fn buildFrameRows( .cam_time_ms = capture.bench_log.frame_times.cam_invert / 1e6, .elem_loop_time_ms = capture.bench_log.frame_times.elem_loop / 1e6, .resolve_time_ms = capture.bench_log.frame_times.scratch_resolve / 1e6, - .raster_time_ms = capture.bench_log.frame_times.raster_loop / 1e6, + .raster_time_ms = report.rasterStageTime(capture.bench_log.frame_times) / 1e6, .save_time_ms = capture.bench_log.frame_times.save_frame / 1e6, .frame_time_ms = capture.bench_log.frame_times.active_time / 1e6, .e2e_time_ms = null, @@ -627,7 +637,7 @@ fn buildFrameRows( ), .raster_tpx_mpx_s = calcFrameMPxPerSec( camera_inputs[capture.camera_idx], - capture.bench_log.frame_times.raster_loop, + report.rasterStageTime(capture.bench_log.frame_times), ), .frame_tpx_mpx_s = calcFrameActiveMPxPerSec( camera_inputs[capture.camera_idx], diff --git a/src/dev_support/demoframes.zig b/src/dev_support/demoframes.zig new file mode 100644 index 00000000..787c70d6 --- /dev/null +++ b/src/dev_support/demoframes.zig @@ -0,0 +1,136 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); + +const meshio = @import("../riley/zig/meshio.zig"); + +// -------------------------------------------------------------------------------------- +// Public Entry-Point Functions +// -------------------------------------------------------------------------------------- + +pub fn firstLastIndices( + outer_alloc: std.mem.Allocator, + frames_num: usize, +) ![]usize { + if (frames_num == 0) return error.NoFrames; + + const selected_num: usize = if (frames_num == 1) 1 else 2; + const indices = try outer_alloc.alloc(usize, selected_num); + indices[0] = 0; + if (selected_num == 2) indices[1] = frames_num - 1; + return indices; +} + +pub fn evenlySpacedIndices( + outer_alloc: std.mem.Allocator, + frames_num: usize, + frames_max: usize, +) ![]usize { + if (frames_num == 0) return error.NoFrames; + if (frames_max == 0) return error.ZeroFrameLimit; + + const selected_num = @min(frames_num, frames_max); + const indices = try outer_alloc.alloc(usize, selected_num); + if (selected_num == 1) { + indices[0] = 0; + return indices; + } + + for (0..selected_num) |ii| { + indices[ii] = ii * (frames_num - 1) / (selected_num - 1); + } + return indices; +} + +pub fn selectFieldFrames( + outer_alloc: std.mem.Allocator, + field: *const meshio.Field, + frame_indices: []const usize, +) !meshio.Field { + if (frame_indices.len == 0) return error.NoFrames; + + var selected = try meshio.Field.initAlloc( + outer_alloc, + frame_indices.len, + field.getCoordN(), + field.getFieldsN(), + ); + errdefer selected.deinit(outer_alloc); + + for (frame_indices, 0..) |source_frame, target_frame| { + if (source_frame >= field.getTimeN()) return error.FrameOutOfBounds; + for (0..field.getCoordN()) |nn| { + for (0..field.getFieldsN()) |ff| { + const value = field.array.get( + &[_]usize{ source_frame, nn, ff }, + ); + selected.array.set( + &[_]usize{ target_frame, nn, ff }, + value, + ); + } + } + } + return selected; +} + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +test "first and last indices retain both endpoints" { + const alloc = std.testing.allocator; + const indices = try firstLastIndices(alloc, 64); + defer alloc.free(indices); + + try std.testing.expectEqualSlices(usize, &[_]usize{ 0, 63 }, indices); +} + +test "even frame selection caps and spans the source sequence" { + const alloc = std.testing.allocator; + const indices = try evenlySpacedIndices(alloc, 100, 8); + defer alloc.free(indices); + + try std.testing.expectEqualSlices( + usize, + &[_]usize{ 0, 14, 28, 42, 56, 70, 84, 99 }, + indices, + ); +} + +test "even frame selection retains short sequences" { + const alloc = std.testing.allocator; + const indices = try evenlySpacedIndices(alloc, 3, 8); + defer alloc.free(indices); + + try std.testing.expectEqualSlices(usize, &[_]usize{ 0, 1, 2 }, indices); +} + +test "field selection copies the requested source frames" { + const alloc = std.testing.allocator; + var field = try meshio.Field.initAlloc(alloc, 4, 2, 1); + defer field.deinit(alloc); + for (0..4) |frame_idx| { + for (0..2) |node_idx| { + field.array.set( + &[_]usize{ frame_idx, node_idx, 0 }, + @floatFromInt(10 * frame_idx + node_idx), + ); + } + } + + var selected = try selectFieldFrames(alloc, &field, &[_]usize{ 0, 3 }); + defer selected.deinit(alloc); + + try std.testing.expectEqual(@as(usize, 2), selected.getTimeN()); + try std.testing.expectEqual(@as(f64, 0.0), selected.array.get(&.{ 0, 0, 0 })); + try std.testing.expectEqual(@as(f64, 1.0), selected.array.get(&.{ 0, 1, 0 })); + try std.testing.expectEqual(@as(f64, 30.0), selected.array.get(&.{ 1, 0, 0 })); + try std.testing.expectEqual(@as(f64, 31.0), selected.array.get(&.{ 1, 1, 0 })); +} diff --git a/src/dev_support/orchestration.zig b/src/dev_support/orchestration.zig index 52ec5529..4922ba89 100644 --- a/src/dev_support/orchestration.zig +++ b/src/dev_support/orchestration.zig @@ -26,7 +26,7 @@ const policy = @import("testpolicy.zig"); pub const default_multimesh_mesh_types = [_]gk.MeshType{ .tri3, .tri6, - .quad4ibi, + .quad4newton, .quad8, .quad9, }; diff --git a/src/dev_support/psfsuite.zig b/src/dev_support/psfsuite.zig index 923636b4..d9e32145 100644 --- a/src/dev_support/psfsuite.zig +++ b/src/dev_support/psfsuite.zig @@ -191,7 +191,7 @@ fn baseRasterConfig( image_save_opts: []const iio.ImageSaveOpts, background_value: F, ) rastcfg.RasterConfig { - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.testing); config.save_strategy = save_strategy; config.tile_size_override = tile_size_override; config.background_value = background_value; @@ -220,6 +220,41 @@ pub fn renderCaseWithRasterHalo( render_case: RenderCase, tile_size_override: ?u16, raster_halo_px_override: ?u16, +) !NDArray(F) { + return renderCaseWithOptions( + outer_alloc, + io, + render_case, + tile_size_override, + raster_halo_px_override, + .tile_local, + ); +} + +pub fn renderCaseWithBufferMode( + outer_alloc: std.mem.Allocator, + io: std.Io, + render_case: RenderCase, + tile_size_override: ?u16, + buffer_mode: rastcfg.BufferMode, +) !NDArray(F) { + return renderCaseWithOptions( + outer_alloc, + io, + render_case, + tile_size_override, + null, + buffer_mode, + ); +} + +fn renderCaseWithOptions( + outer_alloc: std.mem.Allocator, + io: std.Io, + render_case: RenderCase, + tile_size_override: ?u16, + raster_halo_px_override: ?u16, + buffer_mode: rastcfg.BufferMode, ) !NDArray(F) { var arena = std.heap.ArenaAllocator.init(outer_alloc); defer arena.deinit(); @@ -246,6 +281,7 @@ pub fn renderCaseWithRasterHalo( render_case.shader_case.background_value, ); config.raster_halo_px_override = raster_halo_px_override; + config.buffer_mode = buffer_mode; const render_groups = [_]riley.RenderGroupSpec{ .{ .io = io, .workers = @max(@as(u16, 1), config.total_threads) }, }; diff --git a/src/dev_support/ssaasuite.zig b/src/dev_support/ssaasuite.zig index bc38af8f..0dd48479 100644 --- a/src/dev_support/ssaasuite.zig +++ b/src/dev_support/ssaasuite.zig @@ -201,7 +201,7 @@ pub fn renderCase( camera_input.sub_sample = ssaa; camera_input.distortion = getDistortionModel(distortion_case); - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.testing); config.save_strategy = .memory; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .csv, .bits = null, .scaling = .none }, diff --git a/src/dev_support/testconfig.zig b/src/dev_support/testconfig.zig index 4394c0a7..52310510 100644 --- a/src/dev_support/testconfig.zig +++ b/src/dev_support/testconfig.zig @@ -16,17 +16,17 @@ pub const REL_TOL: F = if (F == f32) 1.0e-3 else 1e-6; pub const ABS_TOL: F = if (F == f32) 1.0e-3 else 1e-6; pub const RENDER_MODE: RenderMode = .in_order; pub const HULL_MODE: HullMode = .on_no_fallback; -// Includes the caller thread. TOTAL_THREADS = 2 means caller + 1 helper. -pub const TOTAL_THREADS: u16 = 1; +// Includes the caller thread. TOTAL_THREADS = 3 means caller + 2 helpers. +pub const TOTAL_THREADS: u16 = 3; pub const FRAME_BATCH_SIZE_PER_GROUP: u16 = 1; pub const MAX_GEOM_JOBS_IN_FLIGHT_PER_GROUP: u16 = 1; pub const MAX_GEOM_WORKERS_PER_JOB: u16 = 1; -pub const MAX_RASTER_WORKERS_PER_JOB: u16 = 1; +pub const MAX_RASTER_WORKERS_PER_JOB: u16 = 3; pub const GEOM_SCHEDULING_MODE: rastcfg.GeometrySchedulingMode = .auto; pub const TEST_CASE_VERBOSE: bool = false; pub const RasterConfigMode = enum { - gold, + gold_gen, preview, testing, bench, @@ -44,7 +44,7 @@ pub fn getRasterConfig(mode: RasterConfigMode) rastcfg.RasterConfig { }; switch (mode) { - .gold, .preview => { + .gold_gen, .preview => { config.total_threads = 1; config.max_geom_workers_per_job = 1; config.max_raster_workers_per_job = 1; diff --git a/src/dev_support/tests.zig b/src/dev_support/tests.zig index d9e3fffa..9882dc99 100644 --- a/src/dev_support/tests.zig +++ b/src/dev_support/tests.zig @@ -598,6 +598,10 @@ pub fn saveComparisonArtifactsFromResult( try saveImageArtifacts(allocator, io, out_dir, base_name, &actual); + const ref_name = try std.fmt.allocPrint(allocator, "{s}_ref", .{base_name}); + defer allocator.free(ref_name); + try saveImageArtifacts(allocator, io, out_dir, ref_name, &gold); + const diff_name = try std.fmt.allocPrint(allocator, "{s}_diff", .{base_name}); defer allocator.free(diff_name); try saveImageArtifacts(allocator, io, out_dir, diff_name, &diff); @@ -630,6 +634,7 @@ pub fn saveComparisonArtifactsFromImages( } try saveImageArtifacts(allocator, io, out_dir, "cam0_frame0_field0", actual); + try saveImageArtifacts(allocator, io, out_dir, "cam0_frame0_field0_ref", gold); try saveImageArtifacts(allocator, io, out_dir, "cam0_frame0_field0_diff", &diff); } @@ -1039,6 +1044,7 @@ pub fn runMultimeshTest( .{ 1200, 800 }, rel_tol, abs_tol, + .tile_local, ); } @@ -1050,6 +1056,7 @@ pub fn runMultimeshTestExt( pixel_num: [2]u32, rel_tol: F, abs_tol: F, + buffer_mode: rastcfg.BufferMode, ) !void { var arena = std.heap.ArenaAllocator.init(outer_alloc); defer arena.deinit(); @@ -1076,6 +1083,7 @@ pub fn runMultimeshTestExt( defer camera.deinit(aa); var config = tcfg.getRasterConfig(.testing); + config.buffer_mode = buffer_mode; config.save_strategy = .memory; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .csv, .bits = null, .scaling = .none }, @@ -1185,6 +1193,7 @@ pub fn runMultimeshMixedTest( .{ 1600, 800 }, rel_tol, abs_tol, + .tile_local, ); } @@ -1196,6 +1205,7 @@ pub fn runMultimeshMixedTestExt( pixel_num: [2]u32, rel_tol: F, abs_tol: F, + buffer_mode: rastcfg.BufferMode, ) !void { var arena = std.heap.ArenaAllocator.init(outer_alloc); defer arena.deinit(); @@ -1227,6 +1237,7 @@ pub fn runMultimeshMixedTestExt( defer camera.deinit(aa); var config = tcfg.getRasterConfig(.testing); + config.buffer_mode = buffer_mode; config.save_strategy = .memory; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .csv, .bits = null, .scaling = .none }, @@ -1314,6 +1325,7 @@ pub fn runMultimeshMixedRGBTest( .{ 1200, 800 }, rel_tol, abs_tol, + .tile_local, ); } @@ -1325,6 +1337,7 @@ pub fn runMultimeshMixedRGBTestExt( pixel_num: [2]u32, rel_tol: F, abs_tol: F, + buffer_mode: rastcfg.BufferMode, ) !void { var arena = std.heap.ArenaAllocator.init(outer_alloc); defer arena.deinit(); @@ -1356,6 +1369,7 @@ pub fn runMultimeshMixedRGBTestExt( defer camera.deinit(aa); var config_rgb = tcfg.getRasterConfig(.testing); + config_rgb.buffer_mode = buffer_mode; config_rgb.save_strategy = .memory; config_rgb.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .csv, .bits = null, .scaling = .none, .channels = 3 }, diff --git a/src/gen_gold_min.zig b/src/gen_gold_min.zig index 0727d04b..c21a3557 100644 --- a/src/gen_gold_min.zig +++ b/src/gen_gold_min.zig @@ -86,11 +86,11 @@ pub fn main(init: std.process.Init) !void { .{ .sample = .quintic_bspline, .mode = .lut_lerp }, }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print("Generating MIN Gold Data (sphere200/base)...\n", .{}); diff --git a/src/gengold/gen_gold_bench_ssaa1.zig b/src/gengold/gen_gold_bench_ssaa1.zig index c50a19ce..a5abc1cd 100644 --- a/src/gengold/gen_gold_bench_ssaa1.zig +++ b/src/gengold/gen_gold_bench_ssaa1.zig @@ -166,7 +166,7 @@ fn generateCases( .channels = 3, }, .{ - .format = .bmp, + .format = .tiff, .bits = 8, .scaling = .auto, .channels = 3, @@ -180,7 +180,7 @@ fn generateCases( .scaling = .none, }, .{ - .format = .bmp, + .format = .tiff, .bits = 8, .scaling = .auto, }, diff --git a/src/gengold/gen_gold_edge.zig b/src/gengold/gen_gold_edge.zig index e964b751..53b81cc9 100644 --- a/src/gengold/gen_gold_edge.zig +++ b/src/gengold/gen_gold_edge.zig @@ -51,11 +51,11 @@ pub fn main(init: std.process.Init) !void { const pixel_num = [_]u32{ 320, 200 }; const pixel_num_distort_midside = [_]u32{ 800, 500 }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print("Generating Edge Cases to gold/edge/...\n", .{}); diff --git a/src/gengold/gen_gold_fullscreen.zig b/src/gengold/gen_gold_fullscreen.zig index 1b70331d..41bb5726 100644 --- a/src/gengold/gen_gold_fullscreen.zig +++ b/src/gengold/gen_gold_fullscreen.zig @@ -103,12 +103,12 @@ pub fn main(init: std.process.Init) !void { r_config.image_save_opts = if (is_rgb) &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none, .channels = 3 }, - .{ .format = .bmp, .bits = 8, .scaling = .auto, .channels = 3 }, + .{ .format = .tiff, .bits = 8, .scaling = .auto, .channels = 3 }, } else &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; const case_out_dir = try std.fs.path.join( aa, diff --git a/src/gengold/gen_gold_hull.zig b/src/gengold/gen_gold_hull.zig index 61cabdfc..d948c59f 100644 --- a/src/gengold/gen_gold_hull.zig +++ b/src/gengold/gen_gold_hull.zig @@ -29,11 +29,11 @@ pub fn main(init: std.process.Init) !void { }; const midside_mesh_types = [_]gk.MeshType{ .tri6, .quad8, .quad9 }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print( diff --git a/src/gengold/gen_gold_multicamera.zig b/src/gengold/gen_gold_multicamera.zig index 4d9464bf..0f568b90 100644 --- a/src/gengold/gen_gold_multicamera.zig +++ b/src/gengold/gen_gold_multicamera.zig @@ -181,7 +181,7 @@ pub fn main(init: std.process.Init) !void { }, }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ @@ -190,6 +190,12 @@ pub fn main(init: std.process.Init) !void { .scaling = .none, .channels = render_case.channels, }, + .{ + .format = .tiff, + .bits = 8, + .scaling = .auto, + .channels = render_case.channels, + }, }; const out_dir_path = try std.fs.path.join( diff --git a/src/gengold/gen_gold_multimesh.zig b/src/gengold/gen_gold_multimesh.zig index 8b766b90..1b72eab8 100644 --- a/src/gengold/gen_gold_multimesh.zig +++ b/src/gengold/gen_gold_multimesh.zig @@ -18,11 +18,11 @@ pub fn main(init: std.process.Init) !void { defer arena.deinit(); const aa = arena.allocator(); - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print("Generating Multimesh Gold Data...\n", .{}); diff --git a/src/gengold/gen_gold_simple.zig b/src/gengold/gen_gold_simple.zig index e5ab4dac..bfc7ca25 100644 --- a/src/gengold/gen_gold_simple.zig +++ b/src/gengold/gen_gold_simple.zig @@ -50,11 +50,11 @@ pub fn main(init: std.process.Init) !void { .{ .sample = .quintic_bspline, .mode = .lut_lerp }, }; const pixel_num = [_]u32{ 640, 400 }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print("Generating Simple Gold Data (Two Elements only)...\n", .{}); diff --git a/src/gengold/gen_gold_small.zig b/src/gengold/gen_gold_small.zig index 1c83e6e0..fcb92146 100644 --- a/src/gengold/gen_gold_small.zig +++ b/src/gengold/gen_gold_small.zig @@ -58,11 +58,11 @@ pub fn main(init: std.process.Init) !void { .{ .sample = .quintic_bspline, .mode = .lut_lerp }, }; const pixel_num = [_]u32{ 160, 100 }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; std.debug.print("Generating ALL Small Gold Data...\n", .{}); diff --git a/src/gengold/gen_gold_sphere.zig b/src/gengold/gen_gold_sphere.zig index 3f58a7cc..372632b4 100644 --- a/src/gengold/gen_gold_sphere.zig +++ b/src/gengold/gen_gold_sphere.zig @@ -137,12 +137,12 @@ pub fn main(init: std.process.Init) !void { r_config.image_save_opts = if (is_rgb) &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none, .channels = 3 }, - .{ .format = .bmp, .bits = 8, .scaling = .auto, .channels = 3 }, + .{ .format = .tiff, .bits = 8, .scaling = .auto, .channels = 3 }, } else &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; const case_out_dir = try std.fs.path.join( aa, diff --git a/src/gengold/gen_gold_ssaa.zig b/src/gengold/gen_gold_ssaa.zig index aaf5c1e9..5af0377d 100644 --- a/src/gengold/gen_gold_ssaa.zig +++ b/src/gengold/gen_gold_ssaa.zig @@ -80,6 +80,19 @@ pub fn main(init: std.process.Init) !void { .channels = 1, }, ); + try iio.saveImage( + io, + out_dir, + "cam0_frame0_field0", + &image, + 0, + .{ + .format = .tiff, + .bits = 8, + .scaling = .auto, + .channels = 1, + }, + ); } } } diff --git a/src/gengold/gen_gold_texfunc.zig b/src/gengold/gen_gold_texfunc.zig index 3e3922e1..00df051a 100644 --- a/src/gengold/gen_gold_texfunc.zig +++ b/src/gengold/gen_gold_texfunc.zig @@ -145,11 +145,11 @@ pub fn mainWithOutputRoot( .lambertian_normal_z, }; const coord_modes = [_]CoordMode{ .uv, .param }; - var config = tcfg.getRasterConfig(.gold); + var config = tcfg.getRasterConfig(.gold_gen); config.save_strategy = .disk; config.image_save_opts = &[_]iio.ImageSaveOpts{ .{ .format = .fimg, .bits = null, .scaling = .none }, - .{ .format = .bmp, .bits = 8, .scaling = .auto }, + .{ .format = .tiff, .bits = 8, .scaling = .auto }, }; for (mesh_types) |mesh_type| { diff --git a/src/riley/__init__.py b/src/riley/__init__.py index b9395fe3..4303b342 100644 --- a/src/riley/__init__.py +++ b/src/riley/__init__.py @@ -15,8 +15,8 @@ # Add DLL directory to the search path on Windows to avoid "DLL load failed" if platform.system().lower() == "windows": _current_dir = Path(__file__).resolve().parent - # Search zig/ and cyth/ subdirectories where DLLs are located - for _sub_dir in ("zig", "cyth"): + # Search zig/ and cython/ subdirectories where DLLs are located + for _sub_dir in ("zig", "cython"): _dll_dir = _current_dir / _sub_dir if _dll_dir.is_dir(): try: @@ -25,10 +25,11 @@ # Fallback for older Python versions pass -from riley.cyth.riley import ( +from riley.cython.riley import ( Camera, CameraInput, CameraCoordSys, + BufferMode, HullMode, Mesh, MeshInput, @@ -54,6 +55,16 @@ TextureSampleMode, PsfType, ImageFormat, + FrameFitMode, + EFrameFit, + coverage_to_fov_scale, + fov_scale_to_coverage, + pos_frame_coords, + pos_frame_mesh, + pos_frame_meshes, + pos_orbit_cam, + pos_stereo_pair, + calc_pixel_resolution, load_camera, load_stereo_pair, pos_fill_frame_from_rot, @@ -64,15 +75,16 @@ save_camera, save_stereo_pair, ) -from riley.python.enums import ( - ConnectCsvOrientation, - ConnectIndexing, - CoordCsvOrientation, - FieldCsvOrientation, - PlanarProjectionMode, - ProjectionPlane, +from riley.python.meshio import ( + EConnectIndexing, + ECsvOrient, + SimCsvData, +) +from riley.python.helpers import ( + create_raster_config, + load_texture_u16, + load_texture_u8, ) -from riley.python.helpers import create_raster_config, load_texture from riley.python.meshio import ( load_connect_csv, load_coord_csv, @@ -81,9 +93,24 @@ load_field_csvs, load_sim_csvs, ) -from riley.python.meshtools import ( +from riley.python.meshconv import ( + MeshCheckCode, + EElementType, + EMeshType, + MeshConvention, + MeshConvErr, + MeshConvCheck, + SimData, + check_mesh_convention, enforce_mesh_convention, - extract_surface_mesh, + extract_surf_between, + extract_surf_mesh, + infer_mesh_convention, +) +from riley.python.uvtools import ( + EPlanarProjMode, + EProjPlane, + ProjPlane, project_uvs_planar_bbox, project_uvs_planar_centered, ) @@ -92,13 +119,13 @@ "Camera", "CameraInput", "CameraCoordSys", - "ConnectCsvOrientation", - "ConnectIndexing", - "CoordCsvOrientation", + "BufferMode", + "EConnectIndexing", + "ECsvOrient", "data", - "FieldCsvOrientation", - "PlanarProjectionMode", - "ProjectionPlane", + "EPlanarProjMode", + "EProjPlane", + "ProjPlane", "HullMode", "Mesh", "MeshInput", @@ -125,15 +152,37 @@ "TextureSampleMode", "PsfType", "ImageFormat", + "FrameFitMode", + "EFrameFit", + "coverage_to_fov_scale", + "fov_scale_to_coverage", + "pos_frame_coords", + "pos_frame_mesh", + "pos_frame_meshes", + "pos_orbit_cam", + "pos_stereo_pair", + "calc_pixel_resolution", + "MeshCheckCode", + "EElementType", + "EMeshType", + "MeshConvention", + "MeshConvErr", + "MeshConvCheck", + "SimData", + "SimCsvData", + "check_mesh_convention", "enforce_mesh_convention", - "extract_surface_mesh", + "extract_surf_between", + "extract_surf_mesh", + "infer_mesh_convention", "load_connect_csv", "load_coord_csv", "load_disp_csvs", "load_field_csv", "load_field_csvs", "load_sim_csvs", - "load_texture", + "load_texture_u16", + "load_texture_u8", "load_camera", "load_stereo_pair", "project_uvs_planar_bbox", diff --git a/src/riley/__main__.py b/src/riley/__main__.py index d83b3f1b..01ab7cad 100644 --- a/src/riley/__main__.py +++ b/src/riley/__main__.py @@ -15,6 +15,7 @@ _DEMO_FUNCS = { "demo_sphere200": "riley.pydemos.demo_sphere200", + "demo_psf": "riley.pydemos.demo_psf", "demo_rabbits": "riley.pydemos.demo_rabbits", "demo_dicuq": "riley.pydemos.demo_dicuq", "demo_dic_from_exodus": "riley.pydemos.demo_dic_from_exodus", diff --git a/src/riley/cyth/__init__.py b/src/riley/cython/__init__.py similarity index 91% rename from src/riley/cyth/__init__.py rename to src/riley/cython/__init__.py index 9dcb2c73..d1f72fda 100644 --- a/src/riley/cyth/__init__.py +++ b/src/riley/cython/__init__.py @@ -6,4 +6,4 @@ # # Authors: scepticalrabbit (Lloyd Fletcher) # -------------------------------------------------------------------------- -from riley.cyth.riley import * +from riley.cython.riley import * diff --git a/src/riley/cyth/riley.h b/src/riley/cython/riley.h similarity index 81% rename from src/riley/cyth/riley.h rename to src/riley/cython/riley.h index 77600fb5..c817da0b 100644 --- a/src/riley/cyth/riley.h +++ b/src/riley/cython/riley.h @@ -239,6 +239,12 @@ typedef struct c_raster_config { double background_value; uint8_t disk_save_overlap; uint16_t tile_size_override; + uint16_t global_subpx_tile_size_min; + uint16_t global_subpx_tile_size_max; + uint16_t global_subpx_tile_size_override; + uint16_t global_subpx_stripe_size_min; + uint16_t global_subpx_stripe_size_max; + uint16_t global_subpx_stripe_size_override; size_t save_frame_buff_count; uint32_t save_format; uint32_t save_bits; @@ -258,6 +264,7 @@ typedef struct c_raster_config { uint8_t full_stats_save_earlyout_map; uint8_t full_stats_save_pixel_occupancy_map; uint8_t full_stats_save_normals_map; + uint32_t buffer_mode; } CRasterConfig; size_t rileyGetLastError(uint8_t* out_buf, size_t out_buf_len); @@ -294,6 +301,83 @@ int rileyPosFillFrameFromRotOverMeshes( CVec3F64* out_pos ); +int rileyPosFrameCoords( + const CArray2DF64* in_coords, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos +); + +int rileyPosFrameCoordsTarg( + const CArray2DF64* in_coords, + CVec3F64 targ_world, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos +); + +int rileyPosFrameMeshes( + const CMeshInput* in_meshes, + size_t meshes_len, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos +); + +int rileyPosFrameMeshesTarg( + const CMeshInput* in_meshes, + size_t meshes_len, + CVec3F64 targ_world, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos +); + +double rileyCoverageToFovScale(double coverage); +double rileyFovScaleToCoverage(double fov_scale); + +int rileyPosOrbitCam( + CVec3F64 targ_world, + double azimuth_rad, + double elevation_rad, + double dist, + CVec3F64* out_pos, + CVec3F64* out_rot +); + +int rileyPosStereoPair( + CVec3F64 targ_world, + double dist, + double stereo_angle_rad, + double baseline_angle_rad, + CVec3F64* out_cam0_pos, + CVec3F64* out_cam0_rot, + CVec3F64* out_cam1_pos, + CVec3F64* out_cam1_rot +); + +int rileyCalcPixelResolution( + const CCameraInput* in_camera, + CVec3F64 targ_world, + double* out_res +); + int rileyCalcOutputDimsScene( const CMeshInput* in_meshes, size_t meshes_len, diff --git a/src/riley/cyth/riley.pxd b/src/riley/cython/riley.pxd similarity index 80% rename from src/riley/cyth/riley.pxd rename to src/riley/cython/riley.pxd index fe829807..7f7b9d36 100644 --- a/src/riley/cyth/riley.pxd +++ b/src/riley/cython/riley.pxd @@ -223,6 +223,12 @@ cdef extern from "riley.h": double background_value uint8_t disk_save_overlap uint16_t tile_size_override + uint16_t global_subpx_tile_size_min + uint16_t global_subpx_tile_size_max + uint16_t global_subpx_tile_size_override + uint16_t global_subpx_stripe_size_min + uint16_t global_subpx_stripe_size_max + uint16_t global_subpx_stripe_size_override size_t save_frame_buff_count uint32_t save_format uint32_t save_bits @@ -242,6 +248,7 @@ cdef extern from "riley.h": uint8_t full_stats_save_earlyout_map uint8_t full_stats_save_pixel_occupancy_map uint8_t full_stats_save_normals_map + uint32_t buffer_mode size_t rileyGetLastError(uint8_t* out_buf, size_t out_buf_len) @@ -277,6 +284,83 @@ cdef extern from "riley.h": CVec3F64* out_pos, ) + int rileyPosFrameCoords( + const CArray2DF64* in_coords, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos, + ) + + int rileyPosFrameCoordsTarg( + const CArray2DF64* in_coords, + CVec3F64 targ_world, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos, + ) + + int rileyPosFrameMeshes( + const CMeshInput* in_meshes, + size_t meshes_len, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos, + ) + + int rileyPosFrameMeshesTarg( + const CMeshInput* in_meshes, + size_t meshes_len, + CVec3F64 targ_world, + CVec2U32 pixels_num, + CVec2F64 pixels_size, + double focal_length, + CVec3F64 rot_world, + double fov_scale, + uint32_t fit_mode, + CVec3F64* out_pos, + ) + + double rileyCoverageToFovScale(double coverage) + double rileyFovScaleToCoverage(double fov_scale) + + int rileyPosOrbitCam( + CVec3F64 targ_world, + double azimuth_rad, + double elevation_rad, + double dist, + CVec3F64* out_pos, + CVec3F64* out_rot, + ) + + int rileyPosStereoPair( + CVec3F64 targ_world, + double dist, + double stereo_angle_rad, + double baseline_angle_rad, + CVec3F64* out_cam0_pos, + CVec3F64* out_cam0_rot, + CVec3F64* out_cam1_pos, + CVec3F64* out_cam1_rot, + ) + + int rileyCalcPixelResolution( + const CCameraInput* in_camera, + CVec3F64 targ_world, + double* out_res, + ) + int rileyCalcOutputDimsScene( const CMeshInput* in_meshes, size_t meshes_len, diff --git a/src/riley/cyth/riley.py b/src/riley/cython/riley.py similarity index 84% rename from src/riley/cyth/riley.py rename to src/riley/cython/riley.py index 385ef46d..af27e1e6 100644 --- a/src/riley/cyth/riley.py +++ b/src/riley/cython/riley.py @@ -7,6 +7,7 @@ # Authors: scepticalrabbit (Lloyd Fletcher) # -------------------------------------------------------------------------- import cython +import warnings from dataclasses import dataclass, field from enum import IntEnum from pathlib import Path @@ -14,7 +15,7 @@ import numpy as np from cython.cimports.libc.stdlib import free, malloc -from cython.cimports.riley.cyth import riley as cr +from cython.cimports.riley.cython import riley as cr @dataclass(slots=True) @@ -174,6 +175,12 @@ class RasterConfig: background_value: float = 0.0 disk_save_overlap: bool = False tile_size_override: int = 0 + global_subpx_tile_size_min: int = 64 + global_subpx_tile_size_max: int = 1024 + global_subpx_tile_size_override: int = 0 + global_subpx_stripe_size_min: int = 256 + global_subpx_stripe_size_max: int = 4096 + global_subpx_stripe_size_override: int = 0 save_frame_buffer_count: int = 3 save_format: int = 3 save_bits: int = 8 @@ -193,6 +200,7 @@ class RasterConfig: full_stats_save_earlyout_map: bool = True full_stats_save_pixel_occupancy_map: bool = True full_stats_save_normals_map: bool = False + buffer_mode: int = 0 class MeshType(IntEnum): @@ -250,6 +258,12 @@ class ReportMode(IntEnum): full_stats = 2 +class BufferMode(IntEnum): + tile_local = 0 + global_subpx_full = 1 + global_subpx_stripe = 2 + + class SubPixelCenterMap(IntEnum): full_in_mem = 0 per_tile = 1 @@ -345,6 +359,33 @@ class ImageFormat(IntEnum): tiff = 4 +class FrameFitMode(IntEnum): + contain = 0 + cover = 1 + horizontal = 2 + vertical = 3 + + +EFrameFit = FrameFitMode + + +def _fit_mode_to_int(fit_mode: FrameFitMode | str | int) -> int: + if isinstance(fit_mode, (FrameFitMode, int)): + return int(fit_mode) + if isinstance(fit_mode, str): + fit_lower = fit_mode.lower() + if fit_lower in ("contain", "fit_max", "max"): + return int(FrameFitMode.contain) + if fit_lower in ("cover", "fit_min", "min"): + return int(FrameFitMode.cover) + if fit_lower in ("horizontal", "x", "width"): + return int(FrameFitMode.horizontal) + if fit_lower in ("vertical", "y", "height"): + return int(FrameFitMode.vertical) + raise ValueError(f"Unknown fit_mode: '{fit_mode}'") + raise TypeError(f"Invalid fit_mode type: {type(fit_mode)}") + + @cython.cfunc def _make_cvec3(vec_in: tuple[float, float, float]) -> cr.CVec3F64: return cr.CVec3F64( @@ -496,6 +537,20 @@ def _make_raster_config(config: Any) -> cr.CRasterConfig: config_out.background_value = float(config.background_value) config_out.disk_save_overlap = 1 if config.disk_save_overlap else 0 config_out.tile_size_override = int(config.tile_size_override) + config_out.global_subpx_tile_size_min = int(config.global_subpx_tile_size_min) + config_out.global_subpx_tile_size_max = int(config.global_subpx_tile_size_max) + config_out.global_subpx_tile_size_override = int( + config.global_subpx_tile_size_override, + ) + config_out.global_subpx_stripe_size_min = int( + config.global_subpx_stripe_size_min, + ) + config_out.global_subpx_stripe_size_max = int( + config.global_subpx_stripe_size_max, + ) + config_out.global_subpx_stripe_size_override = int( + config.global_subpx_stripe_size_override, + ) config_out.save_frame_buff_count = int(config.save_frame_buffer_count) config_out.save_format = int(config.save_format) config_out.save_bits = int(config.save_bits) @@ -541,6 +596,7 @@ def _make_raster_config(config: Any) -> cr.CRasterConfig: config_out.full_stats_save_normals_map = int( config.full_stats_save_normals_map, ) + config_out.buffer_mode = int(config.buffer_mode) return config_out @@ -890,15 +946,25 @@ def roi_cent_from_coords(coords_in: Any) -> tuple[float, float, float]: return (out_cent.x, out_cent.y, out_cent.z) +def coverage_to_fov_scale(coverage: float) -> float: + return float(cr.rileyCoverageToFovScale(float(coverage))) + + +def fov_scale_to_coverage(fov_scale: float) -> float: + return float(cr.rileyFovScaleToCoverage(float(fov_scale))) + + @cython.boundscheck(False) @cython.wraparound(False) -def pos_fill_frame_from_rot( +def pos_frame_coords( coords_in: Any, pixels_num: tuple[int, int], pixels_size: tuple[float, float], focal_length: float, rot_world: tuple[float, float, float], - frame_fill: float = 1.0, + fov_scale: float = 1.0, + fit_mode: FrameFitMode | str | int = FrameFitMode.contain, + target: tuple[float, float, float] | None = None, ) -> tuple[float, float, float]: coords_np = _contig_f64_2d(coords_in, "coords") rows_num, cols_num = _as_shape_2d(coords_np) @@ -908,21 +974,84 @@ def pos_fill_frame_from_rot( coords_view: cython.double[:, ::1] = coords_np coords_c = _make_array_2d_f64(coords_view, rows_num, cols_num) out_pos: cr.CVec3F64 + mode_int: cython.uint = _fit_mode_to_int(fit_mode) - if cr.rileyPosFillFrameFromRot( - cython.address(coords_c), - _make_cvec2_u32(tuple(pixels_num)), - _make_cvec2_f64(tuple(pixels_size)), - float(focal_length), - _make_cvec3(tuple(rot_world)), - float(frame_fill), - cython.address(out_pos), - ) != 0: - _raise_last_error() + if target is None: + if cr.rileyPosFrameCoords( + cython.address(coords_c), + _make_cvec2_u32(tuple(pixels_num)), + _make_cvec2_f64(tuple(pixels_size)), + float(focal_length), + _make_cvec3(tuple(rot_world)), + float(fov_scale), + mode_int, + cython.address(out_pos), + ) != 0: + _raise_last_error() + else: + if cr.rileyPosFrameCoordsTarg( + cython.address(coords_c), + _make_cvec3(tuple(target)), + _make_cvec2_u32(tuple(pixels_num)), + _make_cvec2_f64(tuple(pixels_size)), + float(focal_length), + _make_cvec3(tuple(rot_world)), + float(fov_scale), + mode_int, + cython.address(out_pos), + ) != 0: + _raise_last_error() return (out_pos.x, out_pos.y, out_pos.z) +def pos_frame_mesh( + mesh_in: Any, + pixels_num: tuple[int, int], + pixels_size: tuple[float, float], + focal_length: float, + rot_world: tuple[float, float, float], + fov_scale: float = 1.0, + fit_mode: FrameFitMode | str | int = FrameFitMode.contain, + target: tuple[float, float, float] | None = None, +) -> tuple[float, float, float]: + coords = getattr(mesh_in, "coords", mesh_in) + return pos_frame_coords( + coords, + pixels_num, + pixels_size, + focal_length, + rot_world, + fov_scale=fov_scale, + fit_mode=fit_mode, + target=target, + ) + + +def pos_fill_frame_from_rot( + coords_in: Any, + pixels_num: tuple[int, int], + pixels_size: tuple[float, float], + focal_length: float, + rot_world: tuple[float, float, float], + frame_fill: float = 1.0, +) -> tuple[float, float, float]: + warnings.warn( + "pos_fill_frame_from_rot is deprecated, use pos_frame_coords instead", + DeprecationWarning, + stacklevel=2, + ) + return pos_frame_coords( + coords_in, + pixels_num, + pixels_size, + focal_length, + rot_world, + fov_scale=frame_fill, + fit_mode=FrameFitMode.contain, + ) + + @cython.cfunc def _fill_mesh_array( mesh_list: list[Any], @@ -1093,13 +1222,15 @@ def roi_cent_over_meshes(meshes: Any) -> tuple[float, float, float]: @cython.boundscheck(False) @cython.wraparound(False) -def pos_fill_frame_from_rot_over_meshes( +def pos_frame_meshes( meshes: Any, pixels_num: tuple[int, int], pixels_size: tuple[float, float], focal_length: float, rot_world: tuple[float, float, float], - frame_fill: float = 1.0, + fov_scale: float = 1.0, + fit_mode: FrameFitMode | str | int = FrameFitMode.contain, + target: tuple[float, float, float] | None = None, ) -> tuple[float, float, float]: mesh_list = _normalize_meshes(meshes) meshes_len: cython.size_t = len(mesh_list) @@ -1111,24 +1242,138 @@ def pos_fill_frame_from_rot_over_meshes( keepalive: list[Any] = [] if mesh_array == cython.NULL: raise MemoryError() + mode_int: cython.uint = _fit_mode_to_int(fit_mode) try: _fill_mesh_array(mesh_list, mesh_array, keepalive) - if cr.rileyPosFillFrameFromRotOverMeshes( - mesh_array, - meshes_len, - _make_cvec2_u32(tuple(pixels_num)), - _make_cvec2_f64(tuple(pixels_size)), - float(focal_length), - _make_cvec3(tuple(rot_world)), - float(frame_fill), - cython.address(out_pos), - ) != 0: - _raise_last_error() + if target is None: + if cr.rileyPosFrameMeshes( + mesh_array, + meshes_len, + _make_cvec2_u32(tuple(pixels_num)), + _make_cvec2_f64(tuple(pixels_size)), + float(focal_length), + _make_cvec3(tuple(rot_world)), + float(fov_scale), + mode_int, + cython.address(out_pos), + ) != 0: + _raise_last_error() + else: + if cr.rileyPosFrameMeshesTarg( + mesh_array, + meshes_len, + _make_cvec3(tuple(target)), + _make_cvec2_u32(tuple(pixels_num)), + _make_cvec2_f64(tuple(pixels_size)), + float(focal_length), + _make_cvec3(tuple(rot_world)), + float(fov_scale), + mode_int, + cython.address(out_pos), + ) != 0: + _raise_last_error() finally: free(mesh_array) return (out_pos.x, out_pos.y, out_pos.z) +def pos_fill_frame_from_rot_over_meshes( + meshes: Any, + pixels_num: tuple[int, int], + pixels_size: tuple[float, float], + focal_length: float, + rot_world: tuple[float, float, float], + frame_fill: float = 1.0, +) -> tuple[float, float, float]: + warnings.warn( + "pos_fill_frame_from_rot_over_meshes is deprecated, " + "use pos_frame_meshes instead", + DeprecationWarning, + stacklevel=2, + ) + return pos_frame_meshes( + meshes, + pixels_num, + pixels_size, + focal_length, + rot_world, + fov_scale=frame_fill, + fit_mode=FrameFitMode.contain, + ) + + +def pos_orbit_cam( + target: tuple[float, float, float], + azimuth_rad: float, + elevation_rad: float, + distance: float, +) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + out_pos: cr.CVec3F64 + out_rot: cr.CVec3F64 + if cr.rileyPosOrbitCam( + _make_cvec3(tuple(target)), + float(azimuth_rad), + float(elevation_rad), + float(distance), + cython.address(out_pos), + cython.address(out_rot), + ) != 0: + _raise_last_error() + return ( + (out_pos.x, out_pos.y, out_pos.z), + (out_rot.x, out_rot.y, out_rot.z), + ) + + +def pos_stereo_pair( + target: tuple[float, float, float], + distance: float, + stereo_angle_rad: float, + baseline_angle_rad: float = 0.0, +) -> tuple[ + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], +]: + cam0_pos: cr.CVec3F64 + cam0_rot: cr.CVec3F64 + cam1_pos: cr.CVec3F64 + cam1_rot: cr.CVec3F64 + if cr.rileyPosStereoPair( + _make_cvec3(tuple(target)), + float(distance), + float(stereo_angle_rad), + float(baseline_angle_rad), + cython.address(cam0_pos), + cython.address(cam0_rot), + cython.address(cam1_pos), + cython.address(cam1_rot), + ) != 0: + _raise_last_error() + return ( + (cam0_pos.x, cam0_pos.y, cam0_pos.z), + (cam0_rot.x, cam0_rot.y, cam0_rot.z), + (cam1_pos.x, cam1_pos.y, cam1_pos.z), + (cam1_rot.x, cam1_rot.y, cam1_rot.z), + ) + + +def calc_pixel_resolution( + camera: Any, + target: tuple[float, float, float], +) -> float: + cam_c = _make_camera_input(camera) + out_res: cython.double = 0.0 + if cr.rileyCalcPixelResolution( + cython.address(cam_c), + _make_cvec3(tuple(target)), + cython.address(out_res), + ) != 0: + _raise_last_error() + return float(out_res) + + def save_stereo_pair( out_dir: str, stereo_file_name: str, @@ -1303,6 +1548,7 @@ def raster( "Camera", "CameraInput", "CameraCoordSys", + "BufferMode", "HullMode", "Mesh", "MeshInput", diff --git a/src/riley/data/__init__.py b/src/riley/data/__init__.py index a162deb4..4c68e791 100644 --- a/src/riley/data/__init__.py +++ b/src/riley/data/__init__.py @@ -12,6 +12,16 @@ from pathlib import Path +_CUBE_CASE_NAMES = ("tet4", "tet10", "hex8", "hex20", "hex27") +_SPHERE200_CASE_NAMES = ( + "tri3_sphere200", + "tri6_sphere200", + "quad4newton_sphere200", + "quad8_sphere200", + "quad9_sphere200", +) + + def _package_data_root_path() -> Path: return Path(str(files("riley.data"))) @@ -55,10 +65,27 @@ def cal_target_texture_path() -> Path: ) -def sphere200_case_path() -> Path: +def cube_case_path(case_name: str) -> Path: + if case_name not in _CUBE_CASE_NAMES: + raise ValueError( + f"Unsupported cube data case: {case_name!r}. " + f"Expected one of {_CUBE_CASE_NAMES}.", + ) + return _resolve_data_path( + f"cubes/{case_name}", + f"data/cubes/{case_name}", + ) + + +def sphere200_case_path(case_name: str = "tri6_sphere200") -> Path: + if case_name not in _SPHERE200_CASE_NAMES: + raise ValueError( + f"Unsupported sphere200 data case: {case_name!r}. " + f"Expected one of {_SPHERE200_CASE_NAMES}.", + ) return _resolve_data_path( - "min/tri6_sphere200", - "data/min/tri6_sphere200", + f"min/{case_name}", + f"data/min/{case_name}", ) @@ -101,6 +128,7 @@ def rabbit_case_path( __all__ = [ "cal_target_texture_path", + "cube_case_path", "platehole_csv_case_path", "platehole_exodus_path", "rabbit_case_path", diff --git a/src/riley/pydemos/common.py b/src/riley/pydemos/common.py index ba0b58eb..a5fdda48 100644 --- a/src/riley/pydemos/common.py +++ b/src/riley/pydemos/common.py @@ -11,6 +11,53 @@ import shutil from pathlib import Path +import numpy as np + + +def first_last_frame_indices(frames_num: int) -> np.ndarray: + """Return the first and last frame indices without duplicating one frame.""" + if frames_num < 1: + raise ValueError("At least one frame is required.") + if frames_num == 1: + return np.array((0,), dtype=np.intp) + return np.array((0, frames_num - 1), dtype=np.intp) + + +def evenly_spaced_frame_indices( + frames_num: int, + frames_max: int, +) -> np.ndarray: + """Return at most ``frames_max`` indices spanning the full sequence.""" + if frames_num < 1: + raise ValueError("At least one frame is required.") + if frames_max < 1: + raise ValueError("The frame limit must be positive.") + + selected_num = min(frames_num, frames_max) + if selected_num == 1: + return np.array((0,), dtype=np.intp) + return np.array( + [ + frame * (frames_num - 1) // (selected_num - 1) + for frame in range(selected_num) + ], + dtype=np.intp, + ) + + +def select_frames( + values: np.ndarray, + frame_indices: np.ndarray, +) -> np.ndarray: + """Return a contiguous copy of selected frames from an array.""" + if values.ndim < 1 or values.shape[0] < 1: + raise ValueError("Frame data must contain at least one frame.") + if frame_indices.ndim != 1 or frame_indices.size < 1: + raise ValueError("Frame indices must be a non-empty 1D array.") + if np.any(frame_indices < 0) or np.any(frame_indices >= values.shape[0]): + raise IndexError("A selected frame index is out of bounds.") + return np.ascontiguousarray(values[frame_indices]) + def make_demo_out_dir(case_name: str, *, clean: bool = True) -> Path: out_dir = Path.cwd() / "out-riley-py" / case_name diff --git a/src/riley/pydemos/demo_dic_from_exodus.py b/src/riley/pydemos/demo_dic_from_exodus.py index 1eaa2eed..f70c449b 100644 --- a/src/riley/pydemos/demo_dic_from_exodus.py +++ b/src/riley/pydemos/demo_dic_from_exodus.py @@ -17,13 +17,18 @@ from pyvale.sensorsim import extract_surf_mesh import riley -from riley.pydemos.common import make_demo_out_dir +from riley.pydemos.common import ( + first_last_frame_indices, + make_demo_out_dir, + select_frames, +) def load_surface_sim( exodus_path: Path, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - sim_data = ExodusLoader(exodus_path, enforce_convention=True).load_all_sim_data() + loader = ExodusLoader(exodus_path, enforce_convention=True) + sim_data = loader.load_all_sim_data() surface_data = extract_surf_mesh(sim_data, enforce_convention=True) connect_keys = sorted(surface_data.connect.keys()) @@ -69,16 +74,18 @@ def main() -> None: } coords, connect, disp = load_surface_sim(exodus_path) + frame_indices = first_last_frame_indices(disp.shape[0]) + disp = select_frames(disp, frame_indices) uvs = riley.project_uvs_planar_centered( coords, pixels_num, uv_span_max=0.8, - projection_plane=( + proj_plane=( np.array((0.0, 0.0, -1.0), dtype=np.float64), np.array((0.0, 0.0, 0.0), dtype=np.float64), ), ) - texture = riley.load_texture(texture_path) + texture = riley.load_texture_u8(texture_path) mesh = riley.Mesh( mesh_type=riley.MeshType.quad8, @@ -134,7 +141,7 @@ def main() -> None: ) config = riley.create_raster_config( - num_frames=2, + num_frames=disp.shape[0], total_threads=total_threads, save_strategy=riley.SaveStrategy.disk, ) @@ -147,7 +154,12 @@ def main() -> None: elapsed_time = perf_counter() - start_time print(f"render time: {elapsed_time:.6f} s") - riley.save_stereo_pair(str(out_dir), "stereo_data_opengl.csv", camera_0, camera_1) + riley.save_stereo_pair( + str(out_dir), + "stereo_data_opengl.csv", + camera_0, + camera_1, + ) riley.save_stereo_pair( str(out_dir), "stereo_data_opencv.csv", diff --git a/src/riley/pydemos/demo_dicuq.py b/src/riley/pydemos/demo_dicuq.py index f1f0680a..92a8250f 100644 --- a/src/riley/pydemos/demo_dicuq.py +++ b/src/riley/pydemos/demo_dicuq.py @@ -14,7 +14,11 @@ import numpy as np import riley -from riley.pydemos.common import make_demo_out_dir +from riley.pydemos.common import ( + first_last_frame_indices, + make_demo_out_dir, + select_frames, +) def main() -> None: @@ -38,7 +42,9 @@ def main() -> None: } coords, connect, uvs, disp = riley.load_sim_csvs(data_dir) - texture = riley.load_texture(texture_path) + frame_indices = first_last_frame_indices(disp.shape[0]) + disp = select_frames(disp, frame_indices) + texture = riley.load_texture_u8(texture_path) mesh = riley.Mesh( mesh_type=riley.MeshType.quad8, @@ -94,7 +100,7 @@ def main() -> None: ) config = riley.create_raster_config( - num_frames=2, + num_frames=disp.shape[0], total_threads=total_threads, save_strategy=riley.SaveStrategy.disk, ) @@ -107,7 +113,12 @@ def main() -> None: elapsed_time = perf_counter() - start_time print(f"render time: {elapsed_time:.6f} s") - riley.save_stereo_pair(str(out_dir), "stereo_data_opengl.csv", camera_0, camera_1) + riley.save_stereo_pair( + str(out_dir), + "stereo_data_opengl.csv", + camera_0, + camera_1, + ) riley.save_stereo_pair( str(out_dir), "stereo_data_opencv.csv", diff --git a/src/riley/pydemos/demo_psf.py b/src/riley/pydemos/demo_psf.py new file mode 100644 index 00000000..a5097734 --- /dev/null +++ b/src/riley/pydemos/demo_psf.py @@ -0,0 +1,93 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +from __future__ import annotations + +from time import perf_counter + +import riley + +from riley.pydemos.common import make_demo_out_dir + + +RASTER_THREADS = 8 + + +def main() -> None: + data_dir = riley.data.sphere200_case_path() + texture_path = riley.data.speckle_texture_path() + out_dir_root = make_demo_out_dir("demo-psf") + pixels_num = (800, 500) + pixels_size = (5.3e-6, 5.3e-6) + focal_length = 50.0e-3 + rot_world = (0.0, 0.0, 0.0) + + coords, connect, uvs, _ = riley.load_sim_csvs(data_dir) + texture = riley.load_texture_u8(texture_path) + roi_cent_world = riley.roi_cent_from_coords(coords) + pos_world = riley.pos_fill_frame_from_rot( + coords, + pixels_num, + pixels_size, + focal_length, + rot_world, + 1.0, + ) + mesh = riley.Mesh( + mesh_type=riley.MeshType.tri6, + coords=coords, + connect=connect, + uvs=uvs, + texture=texture, + sample=riley.TextureSample.cubic_catmull_rom, + sample_mode=riley.TextureSampleMode.lut_lerp, + bits=8, + scaling_type=riley.ScaleStrategy.none, + ) + camera = riley.Camera( + pixels_num=pixels_num, + pixels_size=pixels_size, + pos_world=pos_world, + rot_world=rot_world, + roi_cent_world=roi_cent_world, + focal_length=focal_length, + sub_sample=2, + coord_sys=riley.CameraCoordSys.opengl, + psf_type=riley.PsfType.gaussian, + psf_sigma_x=1.0, + psf_support_rad=3.0, + psf_separable=1, + ) + + for mode in ( + riley.BufferMode.global_subpx_full, + riley.BufferMode.global_subpx_stripe, + ): + out_dir = out_dir_root / mode.name + out_dir.mkdir(parents=True, exist_ok=True) + config = riley.create_raster_config( + num_frames=1, + total_threads=RASTER_THREADS, + save_strategy=riley.SaveStrategy.disk, + ) + config.buffer_mode = mode + + print(f"Rendering {mode.name} with {RASTER_THREADS} raster threads...") + start_time = perf_counter() + image_array = riley.raster(mesh, camera, config, out_dir=str(out_dir)) + elapsed_time = perf_counter() - start_time + print(f"{mode.name}: {elapsed_time:.6f} s") + if image_array is not None: + print( + f"rendered image array with shape {image_array.shape} " + f"to {out_dir}" + ) + + +if __name__ == "__main__": + main() diff --git a/src/riley/pydemos/demo_rabbits.py b/src/riley/pydemos/demo_rabbits.py index cd057f83..7ec77004 100644 --- a/src/riley/pydemos/demo_rabbits.py +++ b/src/riley/pydemos/demo_rabbits.py @@ -38,7 +38,11 @@ def build_rabbit_dir(rabbit_name: str, mesh_type: riley.MeshType) -> Path: def load_static_mesh(data_dir: Path) -> tuple[np.ndarray, np.ndarray]: - coords = np.loadtxt(data_dir / "coords.csv", delimiter=",", dtype=np.float64) + coords = np.loadtxt( + data_dir / "coords.csv", + delimiter=",", + dtype=np.float64, + ) connect_float = np.loadtxt( data_dir / "connectivity.csv", delimiter=",", @@ -63,7 +67,10 @@ def make_grey_mesh_input( shader_idx = mesh_idx % 3 mesh_kwargs = { "mesh_type": mesh_type, - "coords": np.ascontiguousarray(np.array(coords, copy=True), dtype=np.float64), + "coords": np.ascontiguousarray( + np.array(coords, copy=True), + dtype=np.float64, + ), "connect": connect, "bits": 8, "normal_type": riley.NormalType.none, @@ -108,17 +115,18 @@ def main() -> None: out_dir = make_demo_out_dir("demo-rabbits") default_pixel_size = (5.3e-6, 5.3e-6) default_focal_length = 50.0e-3 - rot_world = (0.0, np.pi, 0.0) + # Canonical rabbit winding exposes the opposite side from the legacy data. + rot_world = (0.0, 0.0, 0.0) texture_path = riley.data.speckle_texture_path() rabbit_mesh_types = [ riley.MeshType.tri3, riley.MeshType.tri6, - riley.MeshType.quad4ibi, + riley.MeshType.quad4newton, riley.MeshType.quad8, riley.MeshType.quad9, ] - texture = riley.load_texture(texture_path) + texture = riley.load_texture_u8(texture_path) mesh_inputs: list[riley.Mesh] = [] group_list: list[sceneops.MeshGroup] = [] @@ -160,10 +168,10 @@ def main() -> None: sceneops.BoundsOverlapSpec( overlap_frac=(0.85, 0.8, 0.0), enabled_axes=(True, True, False), - direction=( - sceneops.OverlapDirection.POSITIVE, - sceneops.OverlapDirection.NEGATIVE, - sceneops.OverlapDirection.CURRENT, + direct=( + sceneops.EOverlapDirect.POSITIVE, + sceneops.EOverlapDirect.NEGATIVE, + sceneops.EOverlapDirect.CURRENT, ), ), ) diff --git a/src/riley/pydemos/demo_sphere200.py b/src/riley/pydemos/demo_sphere200.py index f5d40c06..e5f65729 100644 --- a/src/riley/pydemos/demo_sphere200.py +++ b/src/riley/pydemos/demo_sphere200.py @@ -26,7 +26,7 @@ def main() -> None: frame_fill = 1.0 coords, connect, uvs, _ = riley.load_sim_csvs(data_dir) - texture = riley.load_texture(texture_path) + texture = riley.load_texture_u8(texture_path) roi_cent_world = riley.roi_cent_from_coords(coords) pos_world = riley.pos_fill_frame_from_rot( @@ -73,7 +73,10 @@ def main() -> None: if image_array is None: print(f"rendered disk output to {out_dir}") else: - print(f"rendered image array with shape {image_array.shape} to {out_dir}") + print( + f"rendered image array with shape {image_array.shape} " + f"to {out_dir}" + ) if __name__ == "__main__": diff --git a/src/riley/pydemos/demo_stereocal.py b/src/riley/pydemos/demo_stereocal.py index 2f4dbc0c..83b37020 100644 --- a/src/riley/pydemos/demo_stereocal.py +++ b/src/riley/pydemos/demo_stereocal.py @@ -15,31 +15,96 @@ import numpy as np import riley -from riley.pydemos.common import make_demo_out_dir +from riley.pydemos.common import ( + evenly_spaced_frame_indices, + make_demo_out_dir, + select_frames, +) + + +FRAMES_MAX = 8 + +MATCHED_ROI = (0.0125, 0.0175, 0.0005) +MATCHED_CAM0_POS = (0.0125, 0.0175, 0.160864856482) +MATCHED_CAM1_POS = (0.067348011198, 0.0175, 0.151193672270) + + +def create_stereo_cameras( + roi_pos: tuple[float, float, float] | np.ndarray, +) -> tuple[riley.Camera, riley.Camera]: + """Create stereo camera pair matching the DICUQ demo parameters.""" + pixels_num = (2464, 2056) + pixels_size = (3.45e-6, 3.45e-6) + focal_length = 50.0e-3 + stereo_angle_deg = 20.0 + sub_sample = 2 + + # Brown-Conrady distortion (k1=-0.2, k2=0.1, p1=0.0001, p2=-0.0001) + # distortion_model: 0=none, 1=brown_conrady, 2=brown_conrady_ext, etc. + distortion_model = { + "distortion_model": 1, + "distortion_k1": -0.2, + "distortion_k2": 0.1, + "distortion_k3": 0.0, + "distortion_p1": 0.0001, + "distortion_p2": -0.0001, + } + + # Camera 0: face on + cam0_rot = (0.0, 0.0, 0.0) + camera_0 = riley.Camera( + pixels_num=pixels_num, + pixels_size=pixels_size, + pos_world=MATCHED_CAM0_POS, + rot_world=cam0_rot, + roi_cent_world=tuple(roi_pos), + focal_length=focal_length, + sub_sample=sub_sample, + **distortion_model, + ) + + # Camera 1: stereo angle + cam1_rot = (0.0, np.deg2rad(stereo_angle_deg), 0.0) + camera_1 = riley.Camera( + pixels_num=pixels_num, + pixels_size=pixels_size, + pos_world=MATCHED_CAM1_POS, + rot_world=cam1_rot, + roi_cent_world=tuple(roi_pos), + focal_length=focal_length, + sub_sample=sub_sample, + **distortion_model, + ) + + return camera_0, camera_1 def main() -> None: data_dir = riley.data.stereocal_case_path() texture_path = riley.data.cal_target_texture_path() out_dir = make_demo_out_dir("demo-stereocal") - dicuq_camera_dir = Path.cwd() / "out-riley-py" / "demo-dicuq" total_threads = 8 coords, connect, uvs, disp = riley.load_sim_csvs(data_dir) - texture = riley.load_texture(texture_path) - - camera_0, camera_1 = riley.load_stereo_pair( - str(dicuq_camera_dir), - "stereo_data_opengl.csv", - ) + frame_indices = evenly_spaced_frame_indices(disp.shape[0], FRAMES_MAX) + disp = select_frames(disp, frame_indices) + texture = riley.load_texture_u8(texture_path) - roi_pos = np.asarray(riley.roi_cent_from_coords(coords), dtype=np.float64) - target_roi = np.asarray(camera_0.roi_cent_world, dtype=np.float64) - roi_shift = target_roi - roi_pos - coords = np.ascontiguousarray(coords + roi_shift, dtype=np.float64) + # Shift calibration plate to match the DICUQ specimen center + roi_pos_orig = riley.roi_cent_from_coords(coords) + roi_shift = np.array(MATCHED_ROI) - np.array(roi_pos_orig) + coords = coords + roi_shift roi_pos = riley.roi_cent_from_coords(coords) - camera_0 = replace(camera_0, roi_cent_world=roi_pos) - camera_1 = replace(camera_1, roi_cent_world=roi_pos) + + # Create stereo cameras programmatically + camera_0, camera_1 = create_stereo_cameras(roi_pos) + + # Save stereo pair to output directory + stereo_file = "stereo_data_opengl.csv" + riley.save_stereo_pair(str(out_dir), stereo_file, camera_0, camera_1) + + # Load stereo pair back from output directory (standalone test) + camera_0, camera_1 = riley.load_stereo_pair(str(out_dir), stereo_file) mesh = riley.Mesh( mesh_type=riley.MeshType.tri3, @@ -56,7 +121,7 @@ def main() -> None: ) config = riley.create_raster_config( - num_frames=2, + num_frames=disp.shape[0], total_threads=total_threads, save_strategy=riley.SaveStrategy.disk, ) diff --git a/src/riley/pytests/test_cameraops.py b/src/riley/pytests/test_cameraops.py new file mode 100644 index 00000000..df3d8fd8 --- /dev/null +++ b/src/riley/pytests/test_cameraops.py @@ -0,0 +1,159 @@ +"""Tests for camera framing and positioning operations.""" + +import math +import numpy as np +import pytest + +import riley + + +def test_coverage_and_fov_scale_roundtrip() -> None: + coverage = 0.8 + fov_scale = riley.coverage_to_fov_scale(coverage) + assert fov_scale == pytest.approx(1.25) + roundtrip = riley.fov_scale_to_coverage(fov_scale) + assert roundtrip == pytest.approx(0.8) + + +def test_pos_frame_coords_fit_modes() -> None: + coords = np.array( + [ + [-10.0, -5.0, 0.0], + [10.0, -5.0, 0.0], + [10.0, 5.0, 0.0], + [-10.0, 5.0, 0.0], + ], + dtype=np.float64, + ) + pixels_num = (100, 100) + pixels_size = (0.1, 0.1) + focal_length = 10.0 + rot = (0.0, 0.0, 0.0) + + # contain mode should fit the wider dimension (X=20 => dist=20) + pos_contain = riley.pos_frame_coords( + coords, + pixels_num, + pixels_size, + focal_length, + rot, + fov_scale=1.0, + fit_mode=riley.FrameFitMode.contain, + ) + np.testing.assert_allclose(pos_contain, (0.0, 0.0, 20.0), atol=1e-5) + + # cover mode should fit the smaller dimension (Y=10 => dist=10) + pos_cover = riley.pos_frame_coords( + coords, + pixels_num, + pixels_size, + focal_length, + rot, + fov_scale=1.0, + fit_mode="cover", + ) + np.testing.assert_allclose(pos_cover, (0.0, 0.0, 10.0), atol=1e-5) + + # horizontal mode should fit X=20 => dist=20 + pos_horiz = riley.pos_frame_coords( + coords, + pixels_num, + pixels_size, + focal_length, + rot, + fov_scale=1.0, + fit_mode="horizontal", + ) + np.testing.assert_allclose(pos_horiz, (0.0, 0.0, 20.0), atol=1e-5) + + # vertical mode should fit Y=10 => dist=10 + pos_vert = riley.pos_frame_coords( + coords, + pixels_num, + pixels_size, + focal_length, + rot, + fov_scale=1.0, + fit_mode="vertical", + ) + np.testing.assert_allclose(pos_vert, (0.0, 0.0, 10.0), atol=1e-5) + + +def test_pos_frame_coords_with_target() -> None: + coords = np.array( + [ + [0.0, 0.0, 0.0], + [20.0, 0.0, 0.0], + ], + dtype=np.float64, + ) + target = (10.0, 0.0, 0.0) + pos = riley.pos_frame_coords( + coords, + (100, 100), + (0.1, 0.1), + 10.0, + (0.0, 0.0, 0.0), + fov_scale=1.0, + target=target, + ) + np.testing.assert_allclose(pos, (10.0, 0.0, 20.0), atol=1e-5) + + +def test_pos_orbit_cam_placement() -> None: + target = (0.0, 0.0, 0.0) + distance = 100.0 + pos, rot = riley.pos_orbit_cam(target, 0.0, 0.0, distance) + np.testing.assert_allclose(pos, (100.0, 0.0, 0.0), atol=1e-5) + + +def test_pos_stereo_pair_symmetry() -> None: + target = (0.0, 0.0, 0.0) + distance = 100.0 + stereo_angle = math.pi / 6.0 + cam0_pos, cam0_rot, cam1_pos, cam1_rot = riley.pos_stereo_pair( + target, + distance, + stereo_angle, + 0.0, + ) + diff = np.array(cam0_pos) - np.array(cam1_pos) + baseline = np.linalg.norm(diff) + expected_baseline = 2.0 * distance * math.sin(0.5 * stereo_angle) + assert baseline == pytest.approx(expected_baseline, rel=1e-5) + + +def test_calc_pixel_resolution() -> None: + cam = riley.Camera( + pixels_num=(100, 100), + pixels_size=(0.1, 0.1), + pos_world=(0.0, 0.0, 20.0), + rot_world=(0.0, 0.0, 0.0), + roi_cent_world=(0.0, 0.0, 0.0), + focal_length=10.0, + sub_sample=1, + ) + res = riley.calc_pixel_resolution(cam, (0.0, 0.0, 0.0)) + # Distance is 20, focal is 10 => magnification is 10/20 = 0.5. + # Pixel size is 0.1 => mm per pixel = 0.1 / 0.5 = 0.2. + assert res == pytest.approx(0.2, rel=1e-5) + + +def test_pos_fill_frame_from_rot_deprecated_wrapper() -> None: + coords = np.array( + [ + [-10.0, -5.0, 0.0], + [10.0, 5.0, 0.0], + ], + dtype=np.float64, + ) + with pytest.deprecated_call(): + pos = riley.pos_fill_frame_from_rot( + coords, + (100, 100), + (0.1, 0.1), + 10.0, + (0.0, 0.0, 0.0), + frame_fill=1.0, + ) + np.testing.assert_allclose(pos, (0.0, 0.0, 20.0), atol=1e-5) diff --git a/src/riley/pytests/test_demoframes.py b/src/riley/pytests/test_demoframes.py new file mode 100644 index 00000000..5d1c43cc --- /dev/null +++ b/src/riley/pytests/test_demoframes.py @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +from __future__ import annotations + +import numpy as np +import pytest + +from riley.pydemos.common import ( + evenly_spaced_frame_indices, + first_last_frame_indices, + select_frames, +) + + +def test_first_last_indices_retain_both_endpoints() -> None: + indices = first_last_frame_indices(64) + + np.testing.assert_array_equal(indices, (0, 63)) + + +def test_first_last_indices_do_not_duplicate_one_frame() -> None: + indices = first_last_frame_indices(1) + + np.testing.assert_array_equal(indices, (0,)) + + +def test_even_selection_caps_and_spans_the_source_sequence() -> None: + indices = evenly_spaced_frame_indices(100, 8) + + np.testing.assert_array_equal(indices, (0, 14, 28, 42, 56, 70, 84, 99)) + + +def test_even_selection_retains_short_sequences() -> None: + indices = evenly_spaced_frame_indices(3, 8) + + np.testing.assert_array_equal(indices, (0, 1, 2)) + + +def test_select_frames_returns_an_independent_contiguous_copy() -> None: + source = np.arange(24, dtype=np.float64).reshape(4, 3, 2) + + selected = select_frames(source, np.array((0, 3), dtype=np.intp)) + source[0, 0, 0] = -1.0 + + assert selected.flags.c_contiguous + np.testing.assert_array_equal(selected[0], np.arange(6).reshape(3, 2)) + np.testing.assert_array_equal(selected[1], source[3]) + + +@pytest.mark.parametrize("frames_num", (0, -1)) +def test_frame_index_helpers_reject_empty_sequences(frames_num: int) -> None: + with pytest.raises(ValueError, match="At least one"): + first_last_frame_indices(frames_num) + + with pytest.raises(ValueError, match="At least one"): + evenly_spaced_frame_indices(frames_num, 8) diff --git a/src/riley/pytests/test_helpers.py b/src/riley/pytests/test_helpers.py new file mode 100644 index 00000000..a85f2d96 --- /dev/null +++ b/src/riley/pytests/test_helpers.py @@ -0,0 +1,71 @@ +"""Tests for Python convenience helpers.""" + +from pathlib import Path + +import numpy as np +import pytest +from PIL import Image + +import riley + + +def test_load_texture_u8_converts_to_contiguous_greyscale( + tmp_path: Path, +) -> None: + texture_path = tmp_path / "texture.png" + Image.fromarray( + np.array([[(255, 0, 0), (0, 255, 0)]], dtype=np.uint8), + ).save(texture_path) + + texture = riley.load_texture_u8(texture_path) + + assert texture.shape == (1, 2) + assert texture.dtype == np.uint8 + assert texture.flags.c_contiguous + + +def test_load_texture_u16_preserves_sixteen_bit_values(tmp_path: Path) -> None: + texture_path = tmp_path / "texture_u16.png" + expected = np.array(((0, 1024, 65535),), dtype=np.uint16) + Image.fromarray(expected).save(texture_path) + + texture = riley.load_texture_u16(texture_path) + + assert np.array_equal(texture, expected) + assert texture.dtype == np.uint16 + assert texture.flags.c_contiguous + + +def test_load_texture_u16_expands_eight_bit_range(tmp_path: Path) -> None: + texture_path = tmp_path / "texture_u8.png" + image_u8 = np.array(((0, 255),), dtype=np.uint8) + Image.fromarray(image_u8).save(texture_path) + + texture = riley.load_texture_u16(texture_path) + + expected = np.array(((0, 65535),), dtype=np.uint16) + assert np.array_equal(texture, expected) + + +@pytest.mark.parametrize( + ("num_frames", "total_threads"), + [(0, 1), (-1, 1), (1, 0), (1, -1)], +) +def test_create_raster_config_rejects_non_positive_counts( + num_frames: int, + total_threads: int, +) -> None: + with pytest.raises(ValueError): + riley.create_raster_config(num_frames, total_threads) + + +def test_create_raster_config_uses_enums_and_balances_workers() -> None: + config = riley.create_raster_config( + num_frames=4, + total_threads=8, + save_strategy=riley.SaveStrategy.memory, + ) + + assert config.render_mode is riley.RenderMode.offline + assert config.save_strategy is riley.SaveStrategy.memory + assert config.max_raster_workers_per_job == 2 diff --git a/src/riley/pytests/test_meshconv.py b/src/riley/pytests/test_meshconv.py new file mode 100644 index 00000000..cfa4fe12 --- /dev/null +++ b/src/riley/pytests/test_meshconv.py @@ -0,0 +1,1084 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import numpy as np +import pytest + +from riley import data +from riley.python import _meshconv, meshconv + + +SUPPORTED_CUBES = ("tet4", "tet10", "hex8", "hex20", "hex27") +SPHERE_MESHES = ( + "tri3_sphere200", + "tri6_sphere200", + "quad4newton_sphere200", + "quad8_sphere200", + "quad9_sphere200", +) +SURF_ELEM_TYPES = ( + meshconv.EElementType.TRI3, + meshconv.EElementType.TRI6, + meshconv.EElementType.TRI7, + meshconv.EElementType.QUAD4, + meshconv.EElementType.QUAD8, + meshconv.EElementType.QUAD9, +) +_HEX_EDGE_CORNER_IDXS = ( + (0, 1), (1, 2), (2, 3), (3, 0), + (4, 5), (5, 6), (6, 7), (7, 4), + (0, 4), (1, 5), (2, 6), (3, 7), +) +_HEX_FACE_CORNER_IDXS = ( + (0, 1, 2, 3), (0, 3, 7, 4), (4, 5, 6, 7), + (1, 2, 6, 5), (0, 1, 5, 4), (3, 2, 6, 7), +) +_HEX_TO_TET_CORNER_IDXS = ( + (0, 1, 2, 6), (0, 2, 3, 6), (0, 3, 7, 6), + (0, 7, 4, 6), (0, 4, 5, 6), (0, 5, 1, 6), +) +_TET_EDGE_CORNER_IDXS = ( + (0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3), +) +_HIGH_ORDER_HEX_TYPES = { + meshconv.EElementType.HEX20, + meshconv.EElementType.HEX27, +} + + +def test_element_specs_are_complete_and_mapping_is_read_only() -> None: + node_counts: set[int] = set() + for spec in _meshconv.ELEMENT_SPECS.values(): + node_counts.add(spec.nodes_per_elem) + assert node_counts == {3, 4, 6, 7, 8, 9, 10, 20, 27} + assert ( + _meshconv.ELEMENT_SPECS[meshconv.EElementType.HEX27].centre_idx + is None + ) + + with pytest.raises(TypeError): + _meshconv.ELEMENT_SPECS[meshconv.EElementType.TRI3] = ( + _meshconv.ELEMENT_SPECS[meshconv.EElementType.TRI3] + ) + + +def test_check_mesh_convention_passes_for_std_quad() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((0, 1, 2, 3),), dtype=np.int64)}, + ) + mesh.update_mesh_type() + + report = meshconv.check_mesh_convention(mesh) + + assert mesh.mesh_type is meshconv.EMeshType.SURF + assert report == {} + + +def test_enforce_mesh_convention_corrects_legacy_connectivity() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((1,), (2,), (3,), (4,)))}, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + assert np.array_equal( + mesh_out.connect["connect1"], + np.array(((0, 1, 2, 3),), dtype=np.int64), + ) + assert not meshconv.check_mesh_convention(mesh_out) + + +@pytest.mark.parametrize( + "operation", + (meshconv.check_mesh_convention, meshconv.enforce_mesh_convention), +) +def test_mesh_convention_rejects_mixed_indexing_between_tables( + operation: Callable[[meshconv.SimData], object], +) -> None: + coords = np.vstack((_quad_coords(), _quad_coords() + (2.0, 0.0, 0.0))) + mesh = meshconv.SimData( + coords=coords, + connect={ + "connect_zero_based": np.array(((0, 1, 2, 3),)), + "connect_one_based": np.array(((5, 6, 7, 8),)), + }, + mesh_type=meshconv.EMeshType.SURF, + ) + + with pytest.raises(ValueError, match="Mixed zero-based and one-based"): + operation(mesh) + + +def test_check_mesh_convention_reports_failed_checks() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((1,), (4,), (3,), (2,)))}, + ) + + report = meshconv.check_mesh_convention(mesh) + + assert report["connect1"] == [ + meshconv.MeshCheckCode.ROW_MAJOR_CONNECTIVITY, + meshconv.MeshCheckCode.ZERO_BASED_INDEXING, + meshconv.MeshCheckCode.CCW_WINDING, + meshconv.MeshCheckCode.RIGHT_HANDED_GEOMETRY, + ] + + +def test_enforce_mesh_convention_raises_for_invalid_indices() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((0, 1, 2, 10),), dtype=np.int64)}, + ) + + with pytest.raises(ValueError, match="invalid|outside"): + meshconv.enforce_mesh_convention(mesh) + + +def test_enforce_mesh_convention_fixes_tet_handedness() -> None: + mesh = meshconv.SimData( + coords=np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), + dtype=np.float64, + ), + connect={"connect1": np.array(((0, 2, 1, 3),), dtype=np.int64)}, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert not meshconv.check_mesh_convention(mesh_out) + assert np.array_equal( + mesh_out.connect["connect1"], + np.array(((0, 1, 2, 3),), dtype=np.int64), + ) + + +def test_enforce_returns_same_object_when_mesh_conforms() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((0, 1, 2, 3),), dtype=np.int64)}, + ) + mesh.update_mesh_type() + + assert meshconv.enforce_mesh_convention(mesh) is mesh + + +def test_enforce_emits_conforming_sibling_tables_untouched() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), + (2.0, 0.0, 0.0), (3.0, 0.0, 0.0), (3.0, 1.0, 0.0), (2.0, 1.0, 0.0)), + dtype=np.float64, + ) + good_connect = np.array(((0, 1, 2, 3),), dtype=np.int64) + bad_connect = np.array(((4, 7, 6, 5),), dtype=np.int64) + fixed_connect = np.array(((4, 5, 6, 7),), dtype=np.int64) + mesh = meshconv.SimData( + coords=coords, + connect={"connect_good": good_connect, "connect_bad": bad_connect}, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out is not mesh + assert mesh_out.connect is not None + assert np.array_equal(mesh_out.connect["connect_good"], good_connect) + assert np.array_equal(mesh_out.connect["connect_bad"], fixed_connect) + assert mesh.connect is not None + assert np.array_equal(mesh.connect["connect_bad"], bad_connect) + + +def test_enforce_reports_indices_outside_coordinate_array() -> None: + mesh = meshconv.SimData( + coords=_quad_coords(), + connect={"connect1": np.array(((0, 1, 2, 10),), dtype=np.int64)}, + ) + + with pytest.raises( + ValueError, + match="contains indices outside the coordinate array", + ): + meshconv.enforce_mesh_convention(mesh) + + +def test_enforce_propagates_zero_volume_topology_errors() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (2.0, 0.5, 0.0)), + dtype=np.float64, + ) + mesh = meshconv.SimData( + coords=coords, + connect={ + "connect1": np.array( + ((0, 1, 2), (0, 3, 1), (0, 2, 3), (1, 3, 2)), dtype=np.int64, + ), + }, + mesh_type=meshconv.EMeshType.SURF, + ) + + with pytest.raises(ValueError, match="zero signed volume"): + meshconv.enforce_mesh_convention(mesh) + + +def test_enforce_tolerates_nonmanifold_surface_slices() -> None: + """Non-manifold slices have no orientable shell; consistently wound input + falls back to the per-face behaviour and passes untouched.""" + + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), + (0.0, -1.0, 0.5), (0.0, 0.0, -1.0)), + dtype=np.float64, + ) + mesh = meshconv.SimData( + coords=coords, + connect={ + "connect1": np.array( + ((0, 1, 2), (0, 3, 1), (0, 1, 4)), + dtype=np.int64, + ), + }, + mesh_type=meshconv.EMeshType.SURF, + ) + + assert not meshconv.check_mesh_convention(mesh) + assert meshconv.enforce_mesh_convention(mesh) is mesh + + +@pytest.mark.parametrize( + ("elem_type", "opposite_orient"), + ( + (meshconv.EElementType.TRI6, False), + (meshconv.EElementType.TRI6, True), + (meshconv.EElementType.QUAD8, False), + (meshconv.EElementType.QUAD8, True), + ), +) +def test_duplicate_surf_faces_finish_with_matching_orient( + elem_type: meshconv.EElementType, + opposite_orient: bool, +) -> None: + coords, connect = _build_cube_ring_surf(elem_type) + first = connect[0] + duplicate = first.copy() + if opposite_orient: + duplicate = _meshconv._reverse_surf_row(duplicate) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": np.vstack((first, duplicate))}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + connect_out = mesh_out.connect["connect1"] + assert np.array_equal(connect_out[0], connect_out[1]) + assert not meshconv.check_mesh_convention(mesh_out) + + +def test_enforce_fixes_mirrored_hex_handedness_and_is_idempotent() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), + (0.0, 0.0, 1.0), (1.0, 0.0, 1.0), (1.0, 1.0, 1.0), (0.0, 1.0, 1.0)), + dtype=np.float64, + ) + mirrored_row = np.array((0, 3, 2, 1, 4, 7, 6, 5), dtype=np.int64)[None, :] + report = meshconv.check_mesh_convention( + meshconv.SimData(coords=coords, connect={"connect1": mirrored_row}), + ) + expected = {meshconv.MeshCheckCode.RIGHT_HANDED_GEOMETRY} + assert set(report["connect1"]) == expected + + def hex_volume(row: np.ndarray) -> float: + points = coords[row[0, :]] + return float(np.linalg.det(np.column_stack(( + points[1] - points[0], points[3] - points[0], points[4] - points[0], + )))) + + assert hex_volume(mirrored_row) < 0.0 + + mesh = meshconv.SimData(coords=coords, connect={"connect1": mirrored_row}) + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + assert not meshconv.check_mesh_convention(mesh_out) + assert hex_volume(mesh_out.connect["connect1"]) > 0.0 + assert np.array_equal( + meshconv.enforce_mesh_convention(mesh_out).connect["connect1"], + mesh_out.connect["connect1"], + ) + + +def test_explicit_mesh_convention_reorders_source_slots() -> None: + mesh = _load_cube("hex20") + assert mesh.connect is not None + std = mesh.connect["connect1"] + source_to_riley = ( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 16, 17, 18, 19, 12, 13, 14, 15, + ) + mesh.connect["connect1"] = std[:, np.argsort(source_to_riley)] + convention = meshconv.MeshConvention({ + meshconv.EElementType.HEX20: source_to_riley, + }) + + assert meshconv.MeshCheckCode.NODE_ORDER in meshconv.check_mesh_convention( + mesh, + convention, + )["connect1"] + mesh_out = meshconv.enforce_mesh_convention(mesh, convention) + + assert mesh_out.connect is not None + assert np.array_equal(mesh_out.connect["connect1"], std) + assert not meshconv.check_mesh_convention(mesh_out) + + +@pytest.mark.parametrize( + ("permutation", "error", "message"), + ( + ((0, 1, 2), ValueError, "requires a 4-slot"), + ((0, 1, 1, 3), ValueError, "exactly once"), + ((0, 1, 2, 3.0), TypeError, "integers"), + ), +) +def test_mesh_convention_rejects_invalid_permutations( + permutation: tuple[object, ...], + error: type[Exception], + message: str, +) -> None: + with pytest.raises(error, match=message): + meshconv.MeshConvention({ + meshconv.EElementType.QUAD4: permutation, + }) + + +def test_mesh_convention_defensively_copies_its_mapping() -> None: + permutations = { + meshconv.EElementType.QUAD4: (0, 1, 2, 3), + } + convention = meshconv.MeshConvention(permutations) + + permutations[meshconv.EElementType.QUAD4] = (0, 3, 2, 1) + + assert convention.get_src_perm(meshconv.EElementType.QUAD4) == ( + 0, + 1, + 2, + 3, + ) + with pytest.raises(TypeError): + convention.src_to_riley_perms[ + meshconv.EElementType.QUAD4 + ] = (0, 3, 2, 1) + + +def test_coincident_nodes_do_not_bypass_node_role_validation() -> None: + coords = np.array( + ( + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + (0.5, 0.5, 0.0), + (0.5, 0.0, 0.0), + (0.5, 0.0, 0.0), + ), + dtype=np.float64, + ) + spec = _meshconv.ELEMENT_SPECS[meshconv.EElementType.TRI6] + + assert not _meshconv._check_std_node_roles(coords, spec) + + +@pytest.mark.parametrize("cube_name", SUPPORTED_CUBES) +def test_std_cube_meshes_pass_and_enforcement_is_idempotent( + cube_name: str, +) -> None: + mesh = _load_cube(cube_name) + + assert not meshconv.check_mesh_convention(mesh) + enforced_once = meshconv.enforce_mesh_convention(mesh) + enforced_twice = meshconv.enforce_mesh_convention(enforced_once) + + assert not meshconv.check_mesh_convention(enforced_once) + assert enforced_once.connect is not None + assert enforced_twice.connect is not None + for name, connect in enforced_once.connect.items(): + assert np.array_equal(connect, enforced_twice.connect[name]) + + +def test_tet14_cube_is_explicitly_unsupported() -> None: + with pytest.raises( + NotImplementedError, + match="supported nodes-per-element", + ): + meshconv.check_mesh_convention( + meshconv.SimData( + coords=np.zeros((14, 3), dtype=np.float64), + connect={ + "connect1": np.arange(14, dtype=np.int64).reshape(1, 14), + }, + ) + ) + + +@pytest.mark.parametrize("cube_name", SUPPORTED_CUBES) +def test_extracted_cube_surface_passes_convention_check(cube_name: str) -> None: + surface = meshconv.extract_surf_mesh( + meshconv.enforce_mesh_convention(_load_cube(cube_name)), + ) + + assert not meshconv.check_mesh_convention(surface) + + +def test_surface_extraction_clears_volume_side_sets() -> None: + mesh = _load_cube("hex8") + mesh.side_sets = {("surface", "connect1"): np.array((0,), dtype=np.int64)} + + surface = meshconv.extract_surf_mesh(mesh) + + assert surface.side_sets is None + + +def test_surface_slice_sets_surface_mesh_type() -> None: + mesh = _load_cube("hex8") + mesh.mesh_type = meshconv.EMeshType.VOL + + surface = meshconv.extract_surf_between( + mesh, + point=(0.0, 0.0, 0.0), + normal=(0.0, 0.0, 1.0), + ) + + assert surface.mesh_type is meshconv.EMeshType.SURF + + +def test_surface_slice_uses_first_three_vector_components() -> None: + mesh = _load_cube("hex8") + + surface = meshconv.extract_surf_between( + mesh, + point=(0.0, 0.0, 0.0, 10.0), + normal=(0.0, 0.0, 1.0, 10.0), + ) + + assert surface.connect is not None + + +@pytest.mark.parametrize( + ("argument", "value", "message"), + ( + ("point", (0.0, 0.0), "at least three"), + ("normal", (0.0, np.inf, 1.0), "finite"), + ("tolerance", -1.0, "non-negative"), + ("distance", np.nan, "finite"), + ), +) +def test_surface_slice_rejects_invalid_arguments( + argument: str, + value: object, + message: str, +) -> None: + mesh = _load_cube("hex8") + arguments: dict[str, object] = { + "point": (0.0, 0.0, 0.0), + "normal": (0.0, 0.0, 1.0), + } + arguments[argument] = value + + with pytest.raises(ValueError, match=message): + meshconv.extract_surf_between(mesh, **arguments) + + +@pytest.mark.parametrize("mesh_name", SPHERE_MESHES) +def test_native_sphere_meshes_normalize_to_an_idempotent_convention( + mesh_name: str, +) -> None: + mesh = _load_native_mesh( + data.sphere200_case_path(mesh_name), + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + mesh_twice = meshconv.enforce_mesh_convention(mesh_out) + + assert not meshconv.check_mesh_convention(mesh_out) + assert np.array_equal(mesh.coords, mesh_out.coords) + assert mesh_out.connect is not None + assert mesh_twice.connect is not None + for name, connect in mesh_out.connect.items(): + assert np.array_equal(connect, mesh_twice.connect[name]) + + +def test_plate_with_hole_keeps_inward_bore_normals() -> None: + """A closed plate surface must retain its material-facing bore wall.""" + + mesh = _load_native_mesh( + data.platehole_csv_case_path(), + mesh_type=meshconv.EMeshType.SURF, + ) + + assert not meshconv.check_mesh_convention(mesh) + mesh_out = meshconv.enforce_mesh_convention(mesh) + assert mesh_out.connect is not None + assert mesh.connect is not None + assert np.array_equal( + mesh_out.connect["connect1"], + mesh.connect["connect1"], + ) + + connect = mesh_out.connect["connect1"] + assert mesh_out.coords is not None + corners = mesh_out.coords[connect[:, :4]] + normals = np.cross( + corners[:, 1] - corners[:, 0], + corners[:, 3] - corners[:, 0], + ) + radial = np.mean(corners, axis=1)[:, :2] - np.array((0.0125, 0.0175)) + radial_norm = np.linalg.norm(radial, axis=1) + wall_rows = np.abs(normals[:, 2]) < 1.0e-12 + bore_radius = radial_norm[wall_rows].min() + bore_rows = wall_rows & np.isclose(radial_norm, bore_radius) + outer_rows = wall_rows & ~bore_rows + + assert np.count_nonzero(bore_rows) == 64 + bore_alignment = np.sum( + normals[bore_rows, :2] * radial[bore_rows], + axis=1, + ) + outer_alignment = np.sum( + normals[outer_rows, :2] * radial[outer_rows], + axis=1, + ) + assert np.all(bore_alignment < 0.0) + assert np.all(outer_alignment > 0.0) + + +@pytest.mark.parametrize("elem_type", SURF_ELEM_TYPES) +def test_cube_ring_orients_bore_into_void( + elem_type: meshconv.EElementType, +) -> None: + coords, connect = _build_cube_ring_surf(elem_type) + reversed_rows: list[np.ndarray] = [] + for row in connect: + reversed_rows.append(_meshconv._reverse_surf_row(row)) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": np.asarray(reversed_rows)}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + assert not meshconv.check_mesh_convention(mesh_out) + corners = _get_surf_corner_coords(mesh_out, elem_type) + normals = np.cross( + corners[:, 1] - corners[:, 0], + corners[:, 2] - corners[:, 1], + ) + centers = np.mean(corners, axis=1) + vertical = np.isclose(normals[:, 2], 0.0) + radial = centers[:, :2] - np.array((1.5, 1.5)) + radial_max = np.max(np.abs(radial), axis=1) + bore = vertical & np.isclose(radial_max, 0.5) + outside = vertical & np.isclose(radial_max, 1.5) + bore_dot = np.sum(normals[bore, :2] * radial[bore], axis=1) + outside_dot = np.sum(normals[outside, :2] * radial[outside], axis=1) + + expected_bore_faces = 8 if elem_type.name.startswith("TRI") else 4 + assert np.count_nonzero(bore) == expected_bore_faces + assert np.all(bore_dot < 0.0) + assert np.all(outside_dot > 0.0) + + +@pytest.mark.parametrize( + "elem_type", + ( + meshconv.EElementType.TET4, + meshconv.EElementType.TET10, + meshconv.EElementType.HEX20, + meshconv.EElementType.HEX27, + ), +) +def test_cube_ring_vol_extracts_complete_oriented_surf( + elem_type: meshconv.EElementType, +) -> None: + mesh = _build_cube_ring_vol(elem_type) + + mesh_std = meshconv.enforce_mesh_convention(mesh) + surf = meshconv.extract_surf_mesh(mesh_std) + + assert surf.coords is not None + assert surf.connect is not None + assert not meshconv.check_mesh_convention(surf) + connect = surf.connect["connect1"] + from_tets = elem_type in ( + meshconv.EElementType.TET4, + meshconv.EElementType.TET10, + ) + expected_faces = 64 if from_tets else 32 + expected_nodes = 6 if elem_type is meshconv.EElementType.TET10 else 3 + if not from_tets: + expected_nodes = 9 if elem_type is meshconv.EElementType.HEX27 else 8 + assert connect.shape == (expected_faces, expected_nodes) + + corner_count = 3 if from_tets else 4 + corners = surf.coords[connect[:, :corner_count]] + normals = np.cross( + corners[:, 1] - corners[:, 0], + corners[:, 2] - corners[:, 1], + ) + centers = np.mean(corners, axis=1) + vertical = np.isclose(normals[:, 2], 0.0) + radial = centers[:, :2] - np.array((1.5, 1.5)) + radial_max = np.max(np.abs(radial), axis=1) + bore = vertical & np.isclose(radial_max, 0.5) + outside = vertical & np.isclose(radial_max, 1.5) + bore_dot = np.sum(normals[bore, :2] * radial[bore], axis=1) + outside_dot = np.sum(normals[outside, :2] * radial[outside], axis=1) + assert np.all(bore_dot < 0.0) + assert np.all(outside_dot > 0.0) + assert meshconv.enforce_mesh_convention(surf) is surf + + +@pytest.mark.parametrize("elem_type", SURF_ELEM_TYPES) +def test_nested_closed_surface_orients_cavity_into_void( + elem_type: meshconv.EElementType, +) -> None: + outer_coords, outer_connect = _cube_surface(2.0, 0) + inner_coords, inner_connect = _cube_surface(1.0, 8) + coords, connect = _upgrade_surf_elem( + np.vstack((outer_coords, inner_coords)), + np.vstack((outer_connect, inner_connect)), + elem_type, + ) + outer_rows = 12 if elem_type.name.startswith("TRI") else 6 + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + assert not meshconv.check_mesh_convention(mesh_out) + connect = mesh_out.connect["connect1"] + corner_idxs = _meshconv.ELEMENT_SPECS[elem_type].corner_idxs + corners = connect[:, corner_idxs] + assert _surface_volume(mesh_out.coords, corners[:outer_rows]) > 0.0 + assert _surface_volume(mesh_out.coords, corners[outer_rows:]) < 0.0 + + +@pytest.mark.parametrize( + "elem_type", + (meshconv.EElementType.TRI3, meshconv.EElementType.QUAD4), +) +def test_three_nested_surfs_alternate_material_orient( + elem_type: meshconv.EElementType, +) -> None: + outer_coords, outer_connect = _cube_surface(3.0, 0) + cavity_coords, cavity_connect = _cube_surface(2.0, 8) + island_coords, island_connect = _cube_surface(1.0, 16) + coords, connect = _upgrade_surf_elem( + np.vstack((outer_coords, cavity_coords, island_coords)), + np.vstack((outer_connect, cavity_connect, island_connect)), + elem_type, + ) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.coords is not None + assert mesh_out.connect is not None + corner_idxs = _meshconv.ELEMENT_SPECS[elem_type].corner_idxs + corners = mesh_out.connect["connect1"][:, corner_idxs] + rows_per_shell = 12 if elem_type is meshconv.EElementType.TRI3 else 6 + outer_end = rows_per_shell + cavity_end = rows_per_shell * 2 + assert _surface_volume(mesh_out.coords, corners[:outer_end]) > 0.0 + assert _surface_volume( + mesh_out.coords, + corners[outer_end:cavity_end], + ) < 0.0 + assert _surface_volume(mesh_out.coords, corners[cavity_end:]) > 0.0 + + +def test_disconnected_closed_surfs_each_orient_outward() -> None: + first_coords, first_connect = _cube_surface(1.0, 0) + second_coords, second_connect = _cube_surface(1.0, 8) + second_coords = second_coords + (4.0, 0.0, 0.0) + reversed_rows: list[np.ndarray] = [] + for row in second_connect: + reversed_rows.append(_meshconv._reverse_surf_row(row)) + second_connect = np.asarray(reversed_rows) + mesh = meshconv.SimData( + coords=np.vstack((first_coords, second_coords)), + connect={"connect1": np.vstack((first_connect, second_connect))}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.coords is not None + assert mesh_out.connect is not None + connect = mesh_out.connect["connect1"] + faces_per_cube = 6 + assert _surface_volume(mesh_out.coords, connect[:faces_per_cube]) > 0.0 + assert _surface_volume(mesh_out.coords, connect[faces_per_cube:]) > 0.0 + assert not meshconv.check_mesh_convention(mesh_out) + + +@pytest.mark.parametrize("nonplanar", (False, True)) +def test_open_surf_component_has_stable_orient(nonplanar: bool) -> None: + if nonplanar: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), + (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), + (1.0, 0.0, 1.0), (1.0, 1.0, 1.0)), + ) + connect = np.array(((0, 1, 2, 3), (1, 4, 5, 2))) + else: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), + (2.0, 0.0, 0.0), (0.0, 1.0, 0.0), + (1.0, 1.0, 0.0), (2.0, 1.0, 0.0)), + ) + connect_std = np.array(((0, 1, 4, 3), (1, 2, 5, 4))) + reversed_rows: list[np.ndarray] = [] + for row in connect_std: + reversed_rows.append(_meshconv._reverse_surf_row(row)) + connect = np.asarray(reversed_rows) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=meshconv.EMeshType.SURF, + ) + + mesh_out = meshconv.enforce_mesh_convention(mesh) + + assert mesh_out.connect is not None + if nonplanar: + assert np.array_equal(mesh_out.connect["connect1"], connect) + else: + assert np.array_equal(mesh_out.connect["connect1"], connect_std) + assert meshconv.enforce_mesh_convention(mesh_out) is mesh_out + + +def _quad_coords() -> np.ndarray: + return np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)), + dtype=np.float64, + ) + + +def _cube_surface( + scale: float, + node_offset: int, +) -> tuple[np.ndarray, np.ndarray]: + coords = scale * np.array( + ( + (-1.0, -1.0, -1.0), (1.0, -1.0, -1.0), + (1.0, 1.0, -1.0), (-1.0, 1.0, -1.0), + (-1.0, -1.0, 1.0), (1.0, -1.0, 1.0), + (1.0, 1.0, 1.0), (-1.0, 1.0, 1.0), + ), + dtype=np.float64, + ) + connect = np.array( + ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), + (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)), + dtype=np.int64, + ) + return coords, connect + node_offset + + +def _build_cube_ring_surf( + elem_type: meshconv.EElementType, +) -> tuple[np.ndarray, np.ndarray]: + coords, connect = _build_hex8_ring() + volume_mesh = meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=meshconv.EMeshType.VOL, + ) + volume_mesh = meshconv.enforce_mesh_convention(volume_mesh) + surf_mesh = meshconv.extract_surf_mesh(volume_mesh) + assert surf_mesh.coords is not None + assert surf_mesh.connect is not None + return _upgrade_surf_elem( + surf_mesh.coords, + surf_mesh.connect["connect1"], + elem_type, + ) + + +def _build_hex8_ring() -> tuple[np.ndarray, np.ndarray]: + nodes_per_grid_row = 4 + nodes_per_grid_layer = 16 + coords_list: list[tuple[float, float, float]] = [] + for zz in range(2): + for yy in range(4): + for xx in range(4): + coords_list.append((float(xx), float(yy), float(zz))) + coords = np.asarray(coords_list, dtype=np.float64) + + def get_node_idx(xx: int, yy: int, zz: int) -> int: + return ( + zz * nodes_per_grid_layer + + yy * nodes_per_grid_row + + xx + ) + + connect_rows: list[tuple[int, ...]] = [] + for yy in range(3): + for xx in range(3): + if xx == 1 and yy == 1: + continue + connect_rows.append(( + get_node_idx(xx, yy, 0), + get_node_idx(xx + 1, yy, 0), + get_node_idx(xx + 1, yy + 1, 0), + get_node_idx(xx, yy + 1, 0), + get_node_idx(xx, yy, 1), + get_node_idx(xx + 1, yy, 1), + get_node_idx(xx + 1, yy + 1, 1), + get_node_idx(xx, yy + 1, 1), + )) + return ( + coords, + np.asarray(connect_rows, dtype=np.int64), + ) + + +def _build_cube_ring_vol( + elem_type: meshconv.EElementType, +) -> meshconv.SimData: + coords, hex8_connect = _build_hex8_ring() + if elem_type in _HIGH_ORDER_HEX_TYPES: + coords, connect = _upgrade_hex_vol(coords, hex8_connect, elem_type) + else: + tet4_rows: list[list[int]] = [] + for hex_row in hex8_connect: + for corner_idxs in _HEX_TO_TET_CORNER_IDXS: + tet_row: list[int] = [] + for corner_idx in corner_idxs: + tet_row.append(int(hex_row[corner_idx])) + tet4_rows.append(tet_row) + connect = np.asarray(tet4_rows, dtype=np.int64) + if elem_type is meshconv.EElementType.TET10: + coords, connect = _upgrade_tet_vol(coords, connect) + return meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=meshconv.EMeshType.VOL, + ) + + +def _get_or_add_edge_node( + node_a: int, + node_b: int, + coords: np.ndarray, + coords_out: list[list[float]], + edge_nodes: dict[tuple[int, int], int], +) -> int: + edge = (min(node_a, node_b), max(node_a, node_b)) + edge_node = edge_nodes.get(edge) + if edge_node is None: + edge_node = len(coords_out) + edge_nodes[edge] = edge_node + midpoint = 0.5 * (coords[node_a] + coords[node_b]) + coords_out.append(midpoint.tolist()) + return edge_node + + +def _upgrade_tet_vol( + coords: np.ndarray, + tet4_connect: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + coords_out = coords.tolist() + edge_nodes: dict[tuple[int, int], int] = {} + connect_out: list[list[int]] = [] + for corners in tet4_connect: + row_out = corners.tolist() + for node_a_idx, node_b_idx in _TET_EDGE_CORNER_IDXS: + edge_node = _get_or_add_edge_node( + int(corners[node_a_idx]), + int(corners[node_b_idx]), + coords, + coords_out, + edge_nodes, + ) + row_out.append(edge_node) + connect_out.append(row_out) + return ( + np.asarray(coords_out, dtype=np.float64), + np.asarray(connect_out, dtype=np.int64), + ) + + +def _upgrade_hex_vol( + coords: np.ndarray, + hex8_connect: np.ndarray, + elem_type: meshconv.EElementType, +) -> tuple[np.ndarray, np.ndarray]: + coords_out = coords.tolist() + edge_nodes: dict[tuple[int, int], int] = {} + face_nodes: dict[tuple[int, ...], int] = {} + connect_out: list[list[int]] = [] + for corners in hex8_connect: + row_out = corners.tolist() + for node_a_idx, node_b_idx in _HEX_EDGE_CORNER_IDXS: + edge_node = _get_or_add_edge_node( + int(corners[node_a_idx]), + int(corners[node_b_idx]), + coords, + coords_out, + edge_nodes, + ) + row_out.append(edge_node) + if elem_type is meshconv.EElementType.HEX27: + spec = _meshconv.ELEMENT_SPECS[elem_type] + unassigned_node = -1 + while len(row_out) < spec.nodes_per_elem: + row_out.append(unassigned_node) + for face_idx, corner_idxs in enumerate(_HEX_FACE_CORNER_IDXS): + face_corner_nodes: list[int] = [] + for corner_idx in corner_idxs: + face_corner_nodes.append(int(corners[corner_idx])) + face_key = tuple(sorted(face_corner_nodes)) + face_node = face_nodes.get(face_key) + if face_node is None: + face_node = len(coords_out) + face_nodes[face_key] = face_node + face_coords = coords[np.asarray(face_corner_nodes)] + coords_out.append(np.mean(face_coords, axis=0).tolist()) + centre_slot = spec.face_centre_idxs[face_idx] + row_out[centre_slot] = face_node + if spec.cell_centre_idx is None: + raise ValueError("HEX27 requires a cell-centre slot.") + row_out[spec.cell_centre_idx] = len(coords_out) + coords_out.append(np.mean(coords[corners], axis=0).tolist()) + connect_out.append(row_out) + return ( + np.asarray(coords_out, dtype=np.float64), + np.asarray(connect_out, dtype=np.int64), + ) + + +def _upgrade_surf_elem( + coords: np.ndarray, + quad_connect: np.ndarray, + elem_type: meshconv.EElementType, +) -> tuple[np.ndarray, np.ndarray]: + linear_rows: list[tuple[int, ...]] = [] + use_tris = elem_type.name.startswith("TRI") + for row in quad_connect: + corner_nodes: list[int] = [] + for node in row[:4]: + corner_nodes.append(int(node)) + corners = tuple(corner_nodes) + if use_tris: + linear_rows.append((corners[0], corners[1], corners[2])) + linear_rows.append((corners[0], corners[2], corners[3])) + else: + linear_rows.append(corners) + + nodes_per_elem = _meshconv.ELEMENT_SPECS[elem_type].nodes_per_elem + corner_count = 3 if use_tris else 4 + if nodes_per_elem == corner_count: + return coords.copy(), np.asarray(linear_rows, dtype=np.int64) + + coords_out = coords.tolist() + edge_nodes: dict[tuple[int, int], int] = {} + connect_out: list[list[int]] = [] + for corners in linear_rows: + row_out = list(corners) + for corner_idx, node_a in enumerate(corners): + node_b = corners[(corner_idx + 1) % corner_count] + edge = (min(node_a, node_b), max(node_a, node_b)) + edge_node = edge_nodes.get(edge) + if edge_node is None: + edge_node = len(coords_out) + edge_nodes[edge] = edge_node + midpoint = 0.5 * (coords[node_a] + coords[node_b]) + coords_out.append(midpoint.tolist()) + row_out.append(edge_node) + if nodes_per_elem == corner_count * 2 + 1: + row_out.append(len(coords_out)) + centre = np.mean(coords[np.asarray(corners)], axis=0) + coords_out.append(centre.tolist()) + connect_out.append(row_out) + return ( + np.asarray(coords_out, dtype=np.float64), + np.asarray(connect_out, dtype=np.int64), + ) + + +def _get_surf_corner_coords( + mesh: meshconv.SimData, + elem_type: meshconv.EElementType, +) -> np.ndarray: + assert mesh.coords is not None + assert mesh.connect is not None + corner_idxs = _meshconv.ELEMENT_SPECS[elem_type].corner_idxs + return mesh.coords[mesh.connect["connect1"][:, corner_idxs]] + + +def _surface_volume(coords: np.ndarray, connect: np.ndarray) -> float: + volume = 0.0 + for row in connect: + points = coords[row] + for point_ind in range(1, points.shape[0] - 1): + volume += np.dot( + points[0], + np.cross(points[point_ind], points[point_ind + 1]), + ) / 6.0 + return float(volume) + + +def _load_cube(name: str) -> meshconv.SimData: + return _load_native_mesh(data.cube_case_path(name)) + + +def _load_native_mesh( + mesh_dir: Path, + *, + mesh_type: meshconv.EMeshType | None = None, +) -> meshconv.SimData: + coords = np.loadtxt( + mesh_dir / "coords.csv", + delimiter=",", + dtype=np.float64, + ) + connect_path = mesh_dir / "connectivity.csv" + if not connect_path.is_file(): + connect_path = mesh_dir / "connect.csv" + connect_raw = np.loadtxt( + connect_path, + delimiter=",", + dtype=np.float64, + ) + connect = connect_raw.astype(np.int64) + return meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=mesh_type, + ) diff --git a/src/riley/pytests/test_meshconvall.py b/src/riley/pytests/test_meshconvall.py new file mode 100644 index 00000000..fb60eac8 --- /dev/null +++ b/src/riley/pytests/test_meshconvall.py @@ -0,0 +1,366 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# -------------------------------------------------------------------------- +"""Hard-coded cross-convention cases for every supported element topology.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from riley.python import _meshconv, meshconv + + +def _tri_coords() -> np.ndarray: + return np.array(((0., 0., 0.), (2., 0., 0.), (0., 1., 0.), + (1., 0., 0.), (1., .5, 0.), (0., .5, 0.), + (2. / 3., 1. / 3., 0.)), dtype=np.float64) + + +def _quad_coords() -> np.ndarray: + return np.array(((0., 0., 0.), (2., 0., 0.), (2., 1., 0.), (0., 1., 0.), + (1., 0., 0.), (2., .5, 0.), (1., 1., 0.), (0., .5, 0.), + (1., .5, 0.)), dtype=np.float64) + + +def _tet_coords() -> np.ndarray: + return np.array(((0., 0., 0.), (2., 0., 0.), (0., 1., 0.), (0., 0., 1.), + (1., 0., 0.), (1., .5, 0.), (0., .5, 0.), (.0, .5, .5), + (1., 0., .5), (0., .5, .5)), dtype=np.float64) + + +def _hex_coords() -> np.ndarray: + points: list[tuple[float, float, float]] = [] + for z in (0., 1.): + for y in (0., 1.): + for x in (0., 2.): + points.append((x, y, z)) + # Riley corner sequence: 0,1,2,3 bottom then 4,5,6,7 top. + corners = (points[0], points[1], points[3], points[2], + points[4], points[5], points[7], points[6]) + edges = ((1., 0., 0.), (2., .5, 0.), (1., 1., 0.), (0., .5, 0.), + (1., 0., 1.), (2., .5, 1.), (1., 1., 1.), (0., .5, 1.), + (0., 0., .5), (2., 0., .5), (2., 1., .5), (0., 1., .5)) + faces = ((1., .5, 0.), (0., .5, .5), (1., .5, 1.), + (2., .5, .5), (1., 0., .5), (1., 1., .5), (1., .5, .5)) + return np.array(corners + edges + faces, dtype=np.float64) + + +_PERMUTATIONS = { + meshconv.EElementType.TRI3: (1, 2, 0), + meshconv.EElementType.TRI6: (2, 0, 1, 5, 3, 4), + meshconv.EElementType.TRI7: (2, 0, 1, 5, 3, 4, 6), + meshconv.EElementType.QUAD4: (2, 3, 0, 1), + meshconv.EElementType.QUAD8: (2, 3, 0, 1, 6, 7, 4, 5), + meshconv.EElementType.QUAD9: (2, 3, 0, 1, 6, 7, 4, 5, 8), + meshconv.EElementType.TET4: (1, 2, 0, 3), + meshconv.EElementType.TET10: (1, 2, 0, 3, 5, 6, 4, 8, 9, 7), + meshconv.EElementType.HEX8: (1, 2, 3, 0, 5, 6, 7, 4), + meshconv.EElementType.HEX20: (1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, + 13, 14, 15, 12, 17, 18, 19, 16), + meshconv.EElementType.HEX27: (1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, + 13, 14, 15, 12, 17, 18, 19, 16, 21, 22, + 23, 20, 25, 26, 24), +} + + +@pytest.mark.parametrize( + ("element_type", "coords", "mesh_type"), + ( + ( + meshconv.EElementType.TRI3, + _tri_coords()[:3], + meshconv.EMeshType.SURF, + ), + ( + meshconv.EElementType.TRI6, + _tri_coords()[:6], + meshconv.EMeshType.SURF, + ), + (meshconv.EElementType.TRI7, _tri_coords(), meshconv.EMeshType.SURF), + ( + meshconv.EElementType.QUAD4, + _quad_coords()[:4], + meshconv.EMeshType.SURF, + ), + ( + meshconv.EElementType.QUAD8, + _quad_coords()[:8], + meshconv.EMeshType.SURF, + ), + (meshconv.EElementType.QUAD9, _quad_coords(), meshconv.EMeshType.SURF), + (meshconv.EElementType.TET4, _tet_coords()[:4], meshconv.EMeshType.VOL), + (meshconv.EElementType.TET10, _tet_coords(), meshconv.EMeshType.VOL), + (meshconv.EElementType.HEX8, _hex_coords()[:8], meshconv.EMeshType.VOL), + ( + meshconv.EElementType.HEX20, + _hex_coords()[:20], + meshconv.EMeshType.VOL, + ), + (meshconv.EElementType.HEX27, _hex_coords(), meshconv.EMeshType.VOL), + ), +) +def test_explicit_conventions_normalise_every_supported_element( + element_type: meshconv.EElementType, + coords: np.ndarray, + mesh_type: meshconv.EMeshType, +) -> None: + std = np.arange(coords.shape[0], dtype=np.int64)[None, :] + permutation = _PERMUTATIONS[element_type] + source = std[:, np.argsort(permutation)] + convention = meshconv.MeshConvention({element_type: permutation}) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": source}, + mesh_type=mesh_type, + ) + + assert meshconv.MeshCheckCode.NODE_ORDER in meshconv.check_mesh_convention( + mesh, convention, + )["connect1"] + mesh_out = meshconv.enforce_mesh_convention(mesh, convention) + + assert mesh_out.connect is not None + assert np.array_equal(mesh_out.connect["connect1"], std) + assert not meshconv.check_mesh_convention(mesh_out) + + +@pytest.mark.parametrize( + ("elem_type", "coords", "mesh_type"), + ( + ( + meshconv.EElementType.TRI6, + _tri_coords()[:6], + meshconv.EMeshType.SURF, + ), + ( + meshconv.EElementType.QUAD8, + _quad_coords()[:8], + meshconv.EMeshType.SURF, + ), + (meshconv.EElementType.TET10, _tet_coords(), meshconv.EMeshType.VOL), + ( + meshconv.EElementType.HEX20, + _hex_coords()[:20], + meshconv.EMeshType.VOL, + ), + ), +) +def test_enforce_repairs_combined_convention_changes( + elem_type: meshconv.EElementType, + coords: np.ndarray, + mesh_type: meshconv.EMeshType, +) -> None: + std = np.arange(coords.shape[0], dtype=np.int64) + spec = _meshconv.ELEMENT_SPECS[elem_type] + if spec.is_surf: + changed_std = _meshconv._reverse_surf_row(std) + else: + changed_std = _meshconv._reverse_handedness_row(std) + src_perm = _PERMUTATIONS[elem_type] + src_slots = np.argsort(src_perm) + connect = (changed_std[src_slots] + 1)[:, None] + src_convention = meshconv.MeshConvention({elem_type: src_perm}) + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": connect}, + mesh_type=mesh_type, + ) + + report = meshconv.check_mesh_convention(mesh, src_convention) + mesh_out = meshconv.enforce_mesh_convention(mesh, src_convention) + + expected_failures = { + meshconv.MeshCheckCode.ROW_MAJOR_CONNECTIVITY, + meshconv.MeshCheckCode.ZERO_BASED_INDEXING, + meshconv.MeshCheckCode.NODE_ORDER, + meshconv.MeshCheckCode.RIGHT_HANDED_GEOMETRY, + } + if spec.is_surf: + expected_failures.add(meshconv.MeshCheckCode.CCW_WINDING) + assert set(report["connect1"]) == expected_failures + assert mesh_out.connect is not None + expected = std[None, :] + assert np.array_equal(mesh_out.connect["connect1"], expected) + assert meshconv.enforce_mesh_convention(mesh_out) is mesh_out + + +def test_infer_mesh_convention_recovers_a_single_affine_source_layout() -> None: + element_type = meshconv.EElementType.TRI6 + permutation = _PERMUTATIONS[element_type] + std = np.arange(6, dtype=np.int64)[None, :] + mesh = meshconv.SimData( + coords=_tri_coords()[:6], + connect={"connect1": std[:, np.argsort(permutation)]}, + mesh_type=meshconv.EMeshType.SURF, + ) + + inferred = meshconv.infer_mesh_convention(mesh) + + assert inferred.get_src_perm(element_type) is not None + + +def test_public_inference_reports_incomplete_mesh() -> None: + with pytest.raises( + meshconv.MeshConvErr, + match="requires coordinates and connectivity", + ): + meshconv.infer_mesh_convention(meshconv.SimData()) + + +def test_inference_accepts_rows_that_differ_only_by_a_valid_rotation() -> None: + coords = np.vstack((_tri_coords()[:6], _tri_coords()[:6] + (3., 0., 0.))) + first = np.arange(6, dtype=np.int64) + rotation = _meshconv._get_elem_symmetries( + meshconv.EElementType.TRI6 + )[1] + second = np.arange(6, 12, dtype=np.int64)[np.argsort(rotation)] + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": np.vstack((first, second))}, + mesh_type=meshconv.EMeshType.SURF, + ) + + inferred = meshconv.infer_mesh_convention(mesh) + + assert inferred.standardise_equiv_orients + assert inferred.get_src_perm(meshconv.EElementType.TRI6) is not None + + +@pytest.mark.parametrize("split_tables", (False, True)) +def test_inference_rejects_conflicting_src_layouts( + split_tables: bool, +) -> None: + coords = np.vstack(( + _tri_coords()[:6], + _tri_coords()[:6] + (3., 0., 0.), + )) + first = np.arange(6, dtype=np.int64) + second = np.arange(6, 12, dtype=np.int64) + second[[4, 5]] = second[[5, 4]] + if split_tables: + connect = { + "connect1": first[None, :], + "connect2": second[None, :], + } + else: + connect = {"connect1": np.vstack((first, second))} + mesh = meshconv.SimData( + coords=coords, + connect=connect, + mesh_type=meshconv.EMeshType.SURF, + ) + + expected = "disagree|multiple source layouts" + with pytest.raises(meshconv.MeshConvErr, match=expected): + meshconv.infer_mesh_convention(mesh) + + +def test_hex27_registry_uses_vtk_face_and_volume_centre_slots() -> None: + spec = _meshconv.ELEMENT_SPECS[meshconv.EElementType.HEX27] + + assert spec.cell_centre_idx == 26 + assert spec.face_centre_idxs == (24, 23, 25, 21, 20, 22) + + +def test_hex27_inference_recovers_swapped_face_centre_slots() -> None: + std = np.arange(27, dtype=np.int64)[None, :] + source = std.copy() + source[:, [20, 21]] = source[:, [21, 20]] + mesh = meshconv.SimData( + coords=meshconv.EElementType.HEX27.calc_ref_coords(), + connect={"connect1": source}, + mesh_type=meshconv.EMeshType.VOL, + ) + + inferred = meshconv.infer_mesh_convention(mesh) + mesh_out = meshconv.enforce_mesh_convention(mesh, inferred) + + assert mesh_out.connect is not None + assert np.array_equal(mesh_out.connect["connect1"], std) + + +@pytest.mark.parametrize( + ("element_type", "expected_count"), + ( + (meshconv.EElementType.TRI3, 3), + (meshconv.EElementType.TRI6, 3), + (meshconv.EElementType.TRI7, 3), + (meshconv.EElementType.QUAD4, 4), + (meshconv.EElementType.QUAD8, 4), + (meshconv.EElementType.QUAD9, 4), + (meshconv.EElementType.TET4, 12), + (meshconv.EElementType.TET10, 12), + (meshconv.EElementType.HEX8, 24), + (meshconv.EElementType.HEX20, 24), + (meshconv.EElementType.HEX27, 24), + ), +) +def test_element_symmetry_registry_has_every_proper_orient( + element_type: meshconv.EElementType, + expected_count: int, +) -> None: + permutations = _meshconv._get_elem_symmetries(element_type) + + assert len(permutations) == expected_count + for permutation in permutations: + perm_slots = set(permutation) + expected_slots = set(range(len(permutation))) + assert perm_slots == expected_slots + + +@pytest.mark.parametrize( + ("element_type", "coords", "mesh_type"), + ( + ( + meshconv.EElementType.TRI3, + _tri_coords()[:3], + meshconv.EMeshType.SURF, + ), + ( + meshconv.EElementType.TRI6, + _tri_coords()[:6], + meshconv.EMeshType.SURF, + ), + (meshconv.EElementType.TRI7, _tri_coords(), meshconv.EMeshType.SURF), + ( + meshconv.EElementType.QUAD4, + _quad_coords()[:4], + meshconv.EMeshType.SURF, + ), + ( + meshconv.EElementType.QUAD8, + _quad_coords()[:8], + meshconv.EMeshType.SURF, + ), + (meshconv.EElementType.QUAD9, _quad_coords(), meshconv.EMeshType.SURF), + (meshconv.EElementType.TET4, _tet_coords()[:4], meshconv.EMeshType.VOL), + (meshconv.EElementType.TET10, _tet_coords(), meshconv.EMeshType.VOL), + (meshconv.EElementType.HEX8, _hex_coords()[:8], meshconv.EMeshType.VOL), + ( + meshconv.EElementType.HEX20, + _hex_coords()[:20], + meshconv.EMeshType.VOL, + ), + (meshconv.EElementType.HEX27, _hex_coords(), meshconv.EMeshType.VOL), + ), +) +def test_every_proper_src_orient_converts_to_riley_slots( + element_type: meshconv.EElementType, + coords: np.ndarray, + mesh_type: meshconv.EMeshType, +) -> None: + std = np.arange(coords.shape[0], dtype=np.int64)[None, :] + for permutation in _meshconv._get_elem_symmetries(element_type): + source = std[:, np.argsort(permutation)] + mesh = meshconv.SimData( + coords=coords, + connect={"connect1": source}, + mesh_type=mesh_type, + ) + src_convention = meshconv.MeshConvention({element_type: permutation}) + + mesh_out = meshconv.enforce_mesh_convention(mesh, src_convention) + + assert mesh_out.connect is not None + assert np.array_equal(mesh_out.connect["connect1"], std) diff --git a/src/riley/pytests/test_meshio.py b/src/riley/pytests/test_meshio.py index 1e4068c1..9d4f90ca 100644 --- a/src/riley/pytests/test_meshio.py +++ b/src/riley/pytests/test_meshio.py @@ -11,14 +11,47 @@ from pathlib import Path import numpy as np +import pytest import riley -def test_packaged_data_paths_exist() -> None: +@pytest.mark.parametrize( + "case_name", + ("tet4", "tet10", "hex8", "hex20", "hex27"), +) +def test_packaged_cube_data_paths_exist(case_name: str) -> None: + case_path = riley.data.cube_case_path(case_name) + assert (case_path / "coords.csv").is_file() + assert (case_path / "connectivity.csv").is_file() + + +@pytest.mark.parametrize( + "case_name", + ( + "tri3_sphere200", + "tri6_sphere200", + "quad4newton_sphere200", + "quad8_sphere200", + "quad9_sphere200", + ), +) +def test_packaged_sphere_data_paths_exist(case_name: str) -> None: + case_path = riley.data.sphere200_case_path(case_name) + for file_name in ("coords.csv", "connect.csv", "field.csv", "uvs.csv"): + assert (case_path / file_name).is_file() + + +def test_packaged_data_paths_reject_unknown_cases() -> None: + with pytest.raises(ValueError, match="Unsupported cube data case"): + riley.data.cube_case_path("tet14") + with pytest.raises(ValueError, match="Unsupported sphere200 data case"): + riley.data.sphere200_case_path("unknown") + + +def test_other_packaged_data_paths_exist() -> None: assert riley.data.speckle_texture_path().is_file() assert riley.data.cal_target_texture_path().is_file() - assert riley.data.sphere200_case_path().is_dir() assert riley.data.platehole_csv_case_path().is_dir() assert riley.data.platehole_exodus_path().is_file() assert riley.data.stereocal_case_path().is_dir() @@ -35,7 +68,7 @@ def test_load_coord_csv_coord_major(tmp_path: Path) -> None: coords_loaded = riley.load_coord_csv( tmp_path / "coords.csv", - orientation=riley.CoordCsvOrientation.coord_major, + orient=riley.ECsvOrient.COORD_MAJOR, ) assert coords_loaded.flags.c_contiguous @@ -48,8 +81,8 @@ def test_load_connect_csv_one_based_node_major(tmp_path: Path) -> None: connect_loaded = riley.load_connect_csv( tmp_path / "connect.csv", - orientation=riley.ConnectCsvOrientation.node_major, - indexing=riley.ConnectIndexing.one_based, + orient=riley.ECsvOrient.NODE_MAJOR, + indexing=riley.EConnectIndexing.ONE_BASED, ) assert connect_loaded.flags.c_contiguous @@ -98,12 +131,74 @@ def test_load_sim_csvs_round_trip(tmp_path: Path) -> None: _save_csv(tmp_path / "field_disp_y.csv", disp_y) _save_csv(tmp_path / "field_disp_z.csv", disp_z) - coords_loaded, connect_loaded, uvs_loaded, disp_loaded = riley.load_sim_csvs( - tmp_path, - ) + sim_data = riley.load_sim_csvs(tmp_path) + coords_loaded, connect_loaded, uvs_loaded, disp_loaded = sim_data + + assert sim_data.coords is coords_loaded np.testing.assert_allclose(coords_loaded, coords) np.testing.assert_array_equal(connect_loaded, connect.astype(np.uintp)) np.testing.assert_allclose(uvs_loaded, uvs) assert disp_loaded is not None assert disp_loaded.shape == (1, 3, 3) + + +def test_load_connect_csv_rejects_fractional_indices(tmp_path: Path) -> None: + _save_csv(tmp_path / "connect.csv", np.array(((0.0, 1.5, 2.0),))) + + with pytest.raises(ValueError, match="integer"): + riley.load_connect_csv(tmp_path / "connect.csv") + + +def test_load_connect_csv_auto_rejects_ambiguous_table(tmp_path: Path) -> None: + _save_csv(tmp_path / "connect.csv", np.array(((1.0, 2.0, 3.0),))) + + with pytest.raises(ValueError, match="ambiguous"): + riley.load_connect_csv(tmp_path / "connect.csv") + + +@pytest.mark.parametrize("indices", [((-1.0, 0.0, 1.0),), ((0.0, 1.0, 3.0),)]) +def test_load_connect_csv_rejects_invalid_range( + tmp_path: Path, + indices: tuple[tuple[float, ...], ...], +) -> None: + _save_csv(tmp_path / "connect.csv", np.asarray(indices)) + + with pytest.raises(ValueError, match="negative|out-of-range"): + riley.load_connect_csv( + tmp_path / "connect.csv", + indexing=riley.EConnectIndexing.ZERO_BASED, + node_count=3, + ) + + +def test_load_disp_csvs_rejects_supplied_missing_path(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="x-component"): + riley.load_disp_csvs(tmp_path / "missing.csv", None, None) + + +def test_load_coord_csv_rejects_non_finite_values(tmp_path: Path) -> None: + _save_csv(tmp_path / "coords.csv", np.array(((0.0, np.nan, 1.0),))) + + with pytest.raises(ValueError, match="non-finite"): + riley.load_coord_csv(tmp_path / "coords.csv") + + +def test_load_sim_csvs_rejects_mismatched_uv_nodes(tmp_path: Path) -> None: + _save_csv(tmp_path / "coords.csv", np.zeros((3, 3))) + _save_csv(tmp_path / "connect.csv", np.array(((0.0, 1.0, 2.0),))) + _save_csv(tmp_path / "uvs.csv", np.zeros((2, 2))) + + with pytest.raises(ValueError, match="UV and coordinate"): + riley.load_sim_csvs(tmp_path) + + +def test_load_sim_csvs_rejects_mismatched_displacement_nodes( + tmp_path: Path, +) -> None: + _save_csv(tmp_path / "coords.csv", np.zeros((3, 3))) + _save_csv(tmp_path / "connect.csv", np.array(((0.0, 1.0, 2.0),))) + _save_csv(tmp_path / "field_disp_x.csv", np.zeros((2, 1))) + + with pytest.raises(ValueError, match="Displacement and coordinate"): + riley.load_sim_csvs(tmp_path) diff --git a/src/riley/pytests/test_meshtools.py b/src/riley/pytests/test_meshtools.py deleted file mode 100644 index c6a1ed17..00000000 --- a/src/riley/pytests/test_meshtools.py +++ /dev/null @@ -1,71 +0,0 @@ -# -------------------------------------------------------------------------- -# Riley: A High Performance Rasteriser for DIC UQ -# -# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) -# Licensed under the MIT License (see LICENSE file for details) -# -# Authors: scepticalrabbit (Lloyd Fletcher) -# -------------------------------------------------------------------------- -from __future__ import annotations - -import numpy as np - -import riley - - -def test_enforce_mesh_convention_flips_clockwise_tri3() -> None: - coords = np.array(((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0))) - connect = np.array(((0, 2, 1),), dtype=np.uintp) - - _, connect_fixed = riley.enforce_mesh_convention(coords, connect) - - np.testing.assert_array_equal( - connect_fixed, - np.array(((0, 1, 2),), dtype=np.uintp), - ) - - -def test_extract_surface_mesh_hex8_cube() -> None: - coords = np.array( - ( - (0.0, 0.0, 0.0), - (1.0, 0.0, 0.0), - (1.0, 1.0, 0.0), - (0.0, 1.0, 0.0), - (0.0, 0.0, 1.0), - (1.0, 0.0, 1.0), - (1.0, 1.0, 1.0), - (0.0, 1.0, 1.0), - ), - dtype=np.float64, - ) - connect = np.array(((0, 1, 2, 3, 4, 5, 6, 7),), dtype=np.uintp) - - surf_coords, surf_connect = riley.extract_surface_mesh(coords, connect) - - assert surf_coords.shape == (8, 3) - assert surf_connect.shape == (6, 4) - assert np.unique(surf_connect).shape[0] == 8 - - -def test_project_uvs_planar_centered_xy() -> None: - coords = np.array( - ( - (0.0, 0.0, 0.0), - (2.0, 0.0, 0.0), - (2.0, 1.0, 0.0), - (0.0, 1.0, 0.0), - ), - dtype=np.float64, - ) - - uvs = riley.project_uvs_planar_centered( - coords, - (200, 100), - uv_span_max=0.8, - projection_plane=riley.ProjectionPlane.xy, - ) - - assert uvs.shape == (4, 2) - assert np.all(uvs >= 0.0) - assert np.all(uvs <= 1.0) diff --git a/src/riley/pytests/test_riley.py b/src/riley/pytests/test_riley.py index 11d4f399..23f8aeb5 100644 --- a/src/riley/pytests/test_riley.py +++ b/src/riley/pytests/test_riley.py @@ -40,34 +40,64 @@ [str(PYTHON_EXE), "-m", "riley", "demo_sphere200"], "out/demo-sphere200", "out-riley-py/demo-sphere200", + None, + ), + ( + "psf", + [str(PYTHON_EXE), "-m", "riley", "demo_psf"], + "out/demo-psf", + "out-riley-py/demo-psf", + None, ), ( "rabbits", [str(PYTHON_EXE), "-m", "riley", "demo_rabbits"], "out/demo-rabbits", "out-riley-py/demo-rabbits", + None, ), ( "dicuq", [str(PYTHON_EXE), "-m", "riley", "demo_dicuq"], "out/demo-dicuq", "out-riley-py/demo-dicuq", + 2, ), ( "dic_from_exodus", [str(PYTHON_EXE), "-m", "riley", "demo_dic_from_exodus"], "out/demo-dicuq", "out-riley-py/demo-dicuq-from-exodus", + 2, ), ( "stereocal", [str(PYTHON_EXE), "-m", "riley", "demo_stereocal"], "out/demo-stereocal", "out-riley-py/demo-stereocal", + 8, ), ) +def test_raster_config_exposes_global_subpixel_sizing() -> None: + """Python callers can tune both global accumulation tile dimensions.""" + import riley + + config = riley.RasterConfig() + assert config.global_subpx_tile_size_min == 64 + assert config.global_subpx_tile_size_max == 1024 + assert config.global_subpx_tile_size_override == 0 + assert config.global_subpx_stripe_size_min == 256 + assert config.global_subpx_stripe_size_max == 4096 + assert config.global_subpx_stripe_size_override == 0 + + config.global_subpx_tile_size_override = 128 + config.global_subpx_stripe_size_override = 512 + assert config.global_subpx_tile_size_override == 128 + assert config.global_subpx_stripe_size_override == 512 + + def _repo_assets_available() -> bool: required_paths = ( PROJECT_ROOT / "src/run_all_demos.zig", @@ -81,6 +111,24 @@ def _has_bmp_renders(dir_path: Path) -> bool: return dir_path.is_dir() and any(dir_path.rglob("*.bmp")) +def _has_expected_demo_renders( + dir_path: Path, + frames_num: int | None, +) -> bool: + if frames_num is None: + return _has_bmp_renders(dir_path) + expected = { + Path(f"cam{camera}_frame{frame}_field0.bmp") + for camera in range(2) + for frame in range(frames_num) + } + actual = { + path.relative_to(dir_path) + for path in dir_path.rglob("*.bmp") + } if dir_path.is_dir() else set() + return actual == expected + + def _run_command(label: str, cmd: list[str], env: dict[str, str]) -> float: print(f"Running {label}: {' '.join(cmd)}") start_time = perf_counter() @@ -106,9 +154,15 @@ def _compare_renders(path_a: Path, path_b: Path) -> None: f"zig={arr_a_raw.shape} python={arr_b_raw.shape}", ) - if EXACT_8BIT_COMPARE and arr_a_raw.dtype == np.uint8 and arr_b_raw.dtype == np.uint8: + if ( + EXACT_8BIT_COMPARE + and arr_a_raw.dtype == np.uint8 + and arr_b_raw.dtype == np.uint8 + ): if not np.array_equal(arr_a_raw, arr_b_raw): - diff = np.abs(arr_a_raw.astype(np.int16) - arr_b_raw.astype(np.int16)) + diff = np.abs( + arr_a_raw.astype(np.int16) - arr_b_raw.astype(np.int16) + ) raise AssertionError( f"render mismatch for {path_a.name}: " f"max_abs_diff={int(np.max(diff))}, " @@ -127,10 +181,16 @@ def _compare_renders(path_a: Path, path_b: Path) -> None: def _compare_render_dirs(dir_a: Path, dir_b: Path) -> None: - files_a = sorted(path_a.relative_to(dir_a) for path_a in dir_a.rglob("*.bmp")) - files_b = sorted(path_b.relative_to(dir_b) for path_b in dir_b.rglob("*.bmp")) + files_a = sorted( + path_a.relative_to(dir_a) for path_a in dir_a.rglob("*.bmp") + ) + files_b = sorted( + path_b.relative_to(dir_b) for path_b in dir_b.rglob("*.bmp") + ) if files_a != files_b: - raise AssertionError(f"output file mismatch: zig={files_a}, python={files_b}") + raise AssertionError( + f"output file mismatch: zig={files_a}, python={files_b}" + ) print(f"Comparing renders in {dir_a} against {dir_b}...") start_time = perf_counter() @@ -157,12 +217,12 @@ def ensure_zig_demo_renders() -> None: force_zig_render = os.environ.get("RILEY_FORCE_ZIG_RENDER", "0") == "1" if force_zig_render: - for _, _, zig_dir, _ in DEMO_CASES: + for _, _, zig_dir, _, _ in DEMO_CASES: shutil.rmtree(PROJECT_ROOT / zig_dir, ignore_errors=True) needs_zig_render = force_zig_render or any( - not _has_bmp_renders(PROJECT_ROOT / zig_dir) - for _, _, zig_dir, _ in DEMO_CASES + not _has_expected_demo_renders(PROJECT_ROOT / zig_dir, frames_num) + for _, _, zig_dir, _, frames_num in DEMO_CASES ) if needs_zig_render: @@ -172,7 +232,7 @@ def ensure_zig_demo_renders() -> None: @pytest.mark.parametrize( - ("case_name", "python_cmd", "zig_dir", "py_dir"), + ("case_name", "python_cmd", "zig_dir", "py_dir", "frames_num"), DEMO_CASES, ids=[case[0] for case in DEMO_CASES], ) @@ -181,9 +241,13 @@ def test_demo_parity( python_cmd: list[str], zig_dir: str, py_dir: str, + frames_num: int | None, ) -> None: + del frames_num if case_name == "dic_from_exodus" and find_spec("pyvale") is None: - pytest.skip("pyvale is required for the exodus Python demo parity test.") + pytest.skip( + "pyvale is required for the exodus Python demo parity test." + ) silent_env = dict(os.environ) silent_env["RILEY_DEMO_SILENT"] = "1" diff --git a/src/riley/pytests/test_sceneops.py b/src/riley/pytests/test_sceneops.py new file mode 100644 index 00000000..12c89c54 --- /dev/null +++ b/src/riley/pytests/test_sceneops.py @@ -0,0 +1,132 @@ +"""Tests for Python scene positioning operations.""" + +from dataclasses import dataclass + +import numpy as np +import pytest + +from riley.python import sceneops + + +@dataclass(slots=True) +class _Mesh: + coords: np.ndarray + + +def _mesh(offset: tuple[float, float, float]) -> _Mesh: + coords = np.array(((0.0, 0.0, 0.0), (2.0, 2.0, 2.0))) + return _Mesh(coords + np.asarray(offset)) + + +def test_bounds_for_meshes_reduces_mesh_bounds() -> None: + bounds = sceneops.bounds_for_meshes( + [_mesh((0.0, 0.0, 0.0)), _mesh((4.0, -2.0, 1.0))], + ) + + np.testing.assert_allclose(bounds.minimum, (0.0, -2.0, 0.0)) + np.testing.assert_allclose(bounds.maximum, (6.0, 2.0, 3.0)) + + +@pytest.mark.parametrize( + ("start", "length"), + [(-1, 1), (0, 0), (0, -1)], +) +def test_mesh_group_span_rejects_invalid_range(start: int, length: int) -> None: + with pytest.raises(ValueError): + sceneops.mesh_group_span(start, length) + + +def test_bounds_for_mesh_group_rejects_out_of_range() -> None: + with pytest.raises(IndexError): + sceneops.bounds_for_mesh_group( + [_mesh((0.0, 0.0, 0.0))], sceneops.MeshGroup(1, 1), + ) + + +def test_bounds_for_coords_rejects_empty_input() -> None: + with pytest.raises(ValueError, match="not be empty"): + sceneops.bounds_for_coords(np.empty((0, 3))) + + +def test_center_mesh_group_at_translates_in_place() -> None: + meshes = [_mesh((2.0, 4.0, 6.0))] + + sceneops.center_mesh_group_at( + meshes, sceneops.mesh_group_single(0), (0.0, 0.0, 0.0), + ) + + np.testing.assert_allclose( + sceneops.bounds_for_meshes(meshes).center, (0.0, 0.0, 0.0), + ) + + +def test_overlap_mesh_group_bounds_respects_direct_and_offset() -> None: + meshes = [_mesh((0.0, 0.0, 0.0)), _mesh((10.0, 0.0, 0.0))] + spec = sceneops.BoundsOverlapSpec( + overlap_frac=(0.5, 0.0, 0.0), + enabled_axes=(True, False, False), + direct=( + sceneops.EOverlapDirect.NEGATIVE, + sceneops.EOverlapDirect.CURRENT, + sceneops.EOverlapDirect.CURRENT, + ), + extra_offset=(0.25, 1.0, 0.0), + ) + + sceneops.overlap_mesh_group_bounds( + meshes, sceneops.mesh_group_single(0), + sceneops.mesh_group_single(1), spec, + ) + + fixed = sceneops.bounds_for_coords(meshes[0].coords) + moving = sceneops.bounds_for_coords(meshes[1].coords) + assert moving.center[0] == pytest.approx(fixed.center[0] - 0.75) + assert moving.center[1] == pytest.approx(2.0) + + +def test_overlap_mesh_group_bounds_rejects_invalid_fraction() -> None: + meshes = [ + _mesh((0.0, 0.0, 0.0)), + _mesh((2.0, 0.0, 0.0)), + ] + + with pytest.raises(ValueError, match="overlap_frac"): + sceneops.overlap_mesh_group_bounds( + meshes, + sceneops.mesh_group_single(0), + sceneops.mesh_group_single(1), + sceneops.BoundsOverlapSpec((1.1, 0.0, 0.0)), + ) + + +def test_arrange_mesh_groups_grid_places_groups() -> None: + meshes = [_mesh((float(index), 0.0, 0.0)) for index in range(4)] + groups = [sceneops.mesh_group_single(index) for index in range(4)] + + sceneops.arrange_mesh_groups_grid( + meshes, + groups, + sceneops.GridSpec(gap=(1.0, 1.0, 1.0), max_divs=(2, 1, 2)), + ) + + centers = [ + sceneops.bounds_for_coords(mesh.coords).center for mesh in meshes + ] + np.testing.assert_allclose( + centers, + ((0.0, 0.0, 0.0), (3.0, 0.0, 0.0), + (0.0, 0.0, 3.0), (3.0, 0.0, 3.0)), + ) + + +@pytest.mark.parametrize("divisions", [(0, 1, 1), (1, 1, 1)]) +def test_arrange_mesh_groups_grid_rejects_invalid_capacity( + divisions: tuple[int, int, int], +) -> None: + meshes = [_mesh((0.0, 0.0, 0.0)), _mesh((2.0, 0.0, 0.0))] + groups = [sceneops.mesh_group_single(0), sceneops.mesh_group_single(1)] + + with pytest.raises(ValueError): + sceneops.arrange_mesh_groups_grid( + meshes, groups, sceneops.GridSpec((0.0, 0.0, 0.0), divisions), + ) diff --git a/src/riley/pytests/test_texture_storage.py b/src/riley/pytests/test_texture_storage.py index c5fed81a..00ac78ff 100644 --- a/src/riley/pytests/test_texture_storage.py +++ b/src/riley/pytests/test_texture_storage.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from riley.cyth import riley as bindings +from riley.cython import riley as bindings def test_texture_storage_accepts_explicit_u8_u16_and_float() -> None: @@ -9,8 +9,10 @@ def test_texture_storage_accepts_explicit_u8_u16_and_float() -> None: u16 = np.zeros((2, 2), dtype=np.uint16) f32 = np.full((2, 2), 0.125, dtype=np.float32) - assert bindings._contig_texture(u8, 1, bindings.TextureStorage.u8).dtype == np.uint8 - assert bindings._contig_texture(u16, 1, bindings.TextureStorage.u16).dtype == np.uint16 + texture_u8 = bindings._contig_texture(u8, 1, bindings.TextureStorage.u8) + texture_u16 = bindings._contig_texture(u16, 1, bindings.TextureStorage.u16) + assert texture_u8.dtype == np.uint8 + assert texture_u16.dtype == np.uint16 float_texture = bindings._contig_texture( f32, 1, diff --git a/src/riley/pytests/test_uvtools.py b/src/riley/pytests/test_uvtools.py new file mode 100644 index 00000000..1726c66b --- /dev/null +++ b/src/riley/pytests/test_uvtools.py @@ -0,0 +1,129 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +from __future__ import annotations + +import numpy as np +import pytest + +import riley + + +def test_project_uvs_planar_centered_xy() -> None: + coords = np.array( + ( + (0.0, 0.0, 0.0), + (2.0, 0.0, 0.0), + (2.0, 1.0, 0.0), + (0.0, 1.0, 0.0), + ), + dtype=np.float64, + ) + + uvs = riley.project_uvs_planar_centered( + coords, + (200, 100), + uv_span_max=0.8, + proj_plane=riley.EProjPlane.XY, + ) + + assert uvs.shape == (4, 2) + assert np.all(uvs >= 0.0) + assert np.all(uvs <= 1.0) + + +@pytest.mark.parametrize( + "plane", + [ + riley.EProjPlane.XY, + riley.EProjPlane.YZ, + riley.EProjPlane.XZ, + ], +) +def test_project_uvs_planar_centered_axis_planes( + plane: riley.EProjPlane, +) -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (2.0, 3.0, 4.0), (1.0, 1.0, 1.0)), + ) + + uvs = riley.project_uvs_planar_centered(coords, (100, 100), 0.8, plane) + + assert uvs.shape == (3, 2) + assert np.all((uvs >= 0.0) & (uvs <= 1.0)) + + +def test_project_uvs_planar_bbox_best_fits_inside_bbox() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (4.0, 1.0, 0.0), (0.0, 1.0, 0.0)), + ) + + uvs = riley.project_uvs_planar_bbox( + coords, + (101, 101), + (20.0, 20.0, 80.0, 80.0), + riley.EProjPlane.XY, + ) + + pixels_x = uvs[:, 0] * 100.0 + pixels_y = (1.0 - uvs[:, 1]) * 100.0 + assert np.all((pixels_x >= 20.0) & (pixels_x <= 80.0)) + assert np.all((pixels_y >= 20.0) & (pixels_y <= 80.0)) + + +def test_project_uvs_planar_custom_plane() -> None: + coords = np.array( + ((0.0, 0.0, 1.0), (1.0, 0.0, 1.0), (0.0, 1.0, 1.0)), + ) + plane = riley.ProjPlane( + normal=np.array((0.0, 0.0, 1.0)), + origin=np.array((0.0, 0.0, 1.0)), + ) + + uvs = riley.project_uvs_planar_centered(coords, (100, 100), 1.0, plane) + + assert np.all(np.isfinite(uvs)) + + +@pytest.mark.parametrize("texture_size", [(1, 10), (10, 1), (0, 10)]) +def test_project_uvs_rejects_invalid_texture_size( + texture_size: tuple[int, int], +) -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)), + ) + + with pytest.raises(ValueError, match="at least 2"): + riley.project_uvs_planar_centered(coords, texture_size) + + +def test_project_uvs_rejects_zero_normal() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)), + ) + + with pytest.raises(ValueError, match="nonzero"): + riley.project_uvs_planar_centered( + coords, + (100, 100), + proj_plane=(np.zeros(3), np.zeros(3)), + ) + + +def test_project_uvs_rejects_degenerate_proj() -> None: + coords = np.array( + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)), + ) + + with pytest.raises(ValueError, match="zero area"): + riley.project_uvs_planar_bbox( + coords, + (100, 100), + (0.0, 0.0, 99.0, 99.0), + riley.EProjPlane.XY, + ) diff --git a/src/riley/python/__init__.py b/src/riley/python/__init__.py index b0cd357c..61eae402 100644 --- a/src/riley/python/__init__.py +++ b/src/riley/python/__init__.py @@ -1,41 +1,79 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- + from riley.python import sceneops -from riley.python.enums import ( - ConnectCsvOrientation, - ConnectIndexing, - CoordCsvOrientation, - FieldCsvOrientation, - PlanarProjectionMode, - ProjectionPlane, -) from riley.python.helpers import ( create_raster_config, - load_texture, + load_texture_u16, + load_texture_u8, ) -from riley.python.meshio import load_connect_csv, load_coord_csv, load_disp_csvs, load_field_csv, load_field_csvs, load_sim_csvs -from riley.python.meshtools import ( +from riley.python.meshio import ( + EConnectIndexing, + ECsvOrient, + SimCsvData, + load_connect_csv, + load_coord_csv, + load_disp_csvs, + load_field_csv, + load_field_csvs, + load_sim_csvs, +) +from riley.python.meshconv import ( + MeshCheckCode, + EElementType, + EMeshType, + MeshConvention, + MeshConvErr, + MeshConvCheck, + SimData, + check_mesh_convention, enforce_mesh_convention, - extract_surface_mesh, + extract_surf_between, + extract_surf_mesh, + infer_mesh_convention, +) +from riley.python.uvtools import ( + EPlanarProjMode, + EProjPlane, + ProjPlane, project_uvs_planar_bbox, project_uvs_planar_centered, ) __all__ = [ - "ConnectCsvOrientation", - "ConnectIndexing", - "CoordCsvOrientation", - "FieldCsvOrientation", - "PlanarProjectionMode", - "ProjectionPlane", + "EConnectIndexing", + "ECsvOrient", + "EPlanarProjMode", + "EProjPlane", + "ProjPlane", "create_raster_config", + "MeshCheckCode", + "EElementType", + "EMeshType", + "MeshConvention", + "MeshConvErr", + "MeshConvCheck", + "SimData", + "SimCsvData", + "check_mesh_convention", "enforce_mesh_convention", - "extract_surface_mesh", + "extract_surf_between", + "extract_surf_mesh", + "infer_mesh_convention", "load_connect_csv", "load_coord_csv", "load_disp_csvs", "load_field_csv", "load_field_csvs", "load_sim_csvs", - "load_texture", + "load_texture_u16", + "load_texture_u8", "project_uvs_planar_bbox", "project_uvs_planar_centered", "sceneops", diff --git a/src/riley/python/_meshconv.py b/src/riley/python/_meshconv.py new file mode 100644 index 00000000..7ffc4c49 --- /dev/null +++ b/src/riley/python/_meshconv.py @@ -0,0 +1,2954 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +"""Implementation of Riley's mesh convention tools. + +The public interface lives in :mod:`riley.python.meshconv`. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from enum import Enum, StrEnum +from functools import cache +from itertools import permutations as perms, product +from numbers import Integral +from types import MappingProxyType +import numpy as np + + +@dataclass(frozen=True, slots=True) +class _Tolerances: + """Store numerical tolerances used by mesh-convention checks.""" + + ref: float = 1.0e-12 + geom: float = 1.0e-12 + quad8_edge: float = 5.0e-2 + role_match: float = 3.5e-1 + + +_TOL = _Tolerances() + +# Fixed, irregular directions avoid rays aligned with the axis-aligned faces, +# edges and vertices common in FE meshes. The values are otherwise arbitrary; +# three well-separated directions provide a deterministic majority vote when +# one ray passes through a numerically ambiguous feature. +_POINT_IN_SURF_RAY_DIRECTS = np.array(( + (0.745, 0.371, 0.553), + (-0.299, 0.877, 0.376), + (0.461, -0.314, 0.830), +)) + + +def _calc_ref_node_perm( + ref: np.ndarray, + transformed: np.ndarray, +) -> tuple[int, ...]: + """Map transformed reference coordinates back to their source slots.""" + slots: list[int] = [] + for point in transformed: + matches = np.flatnonzero( + np.all(np.isclose(ref, point, atol=_TOL.ref), axis=1) + ) + if matches.shape[0] != 1: + raise ValueError( + "Reference transformation does not preserve element roles." + ) + slots.append(int(matches[0])) + unique_slots = set(slots) + if len(unique_slots) != ref.shape[0]: + raise ValueError("Reference transformation is not a node permutation.") + return tuple(slots) + + +def _check_mesh_2d(mesh_in: SimData) -> bool: + """Return whether a mesh represents a two-dimensional topology.""" + if mesh_in.coords is None or mesh_in.connect is None: + return False + return not _check_vol_mesh(mesh_in) + + +def _check_vol_mesh(mesh_in: SimData) -> bool: + """Return whether every connectivity table describes volume elements.""" + if mesh_in.coords is None or mesh_in.connect is None: + return False + + num_coords = mesh_in.coords.shape[0] + shift_all = _check_mesh_needs_zero_based_shift(mesh_in, num_coords) + table_types: set[bool] = set() + + for name, connect_raw in mesh_in.connect.items(): + connect = _enforce_connect_arr_format(connect_raw, name) + if _check_transpose_needed(connect, name, mesh_in): + connect = connect.T + + if _check_table_needs_zero_based_shift(connect, num_coords, shift_all): + connect = connect - 1 + + if not _check_idxs_zero_based(connect, num_coords): + raise ValueError( + f"Connectivity table '{name}' has invalid indices." + ) + + table_types.add( + _check_vol_connect_table(connect, mesh_in.coords) + ) + + if len(table_types) > 1: + raise ValueError( + "A SimData mesh cannot mix surface and volume connectivity tables." + ) + + return bool(table_types and table_types.pop()) + + +def _check_vol_connect_table( + connect: np.ndarray, + coords: np.ndarray, +) -> bool: + """Return whether a connectivity table represents volume elements.""" + nodes_per_elem = connect.shape[1] + _validate_nodes_per_elem(nodes_per_elem) + + if nodes_per_elem in _VOL_ONLY_NODE_COUNTS: + return True + if nodes_per_elem in _SURF_ONLY_NODE_COUNTS: + return False + + if _get_surf_spec(nodes_per_elem) is ELEMENT_SPECS[EElementType.QUAD8]: + all_rows_are_quad8 = True + for row in connect: + if not _check_quad8_surf_row(row, coords): + all_rows_are_quad8 = False + break + if all_rows_are_quad8: + return False + + vol_spec = _get_vol_spec(nodes_per_elem) + corner_idxs = np.asarray(vol_spec.corner_idxs, dtype=np.int64) + for row in connect: + cell_coords = coords[row[corner_idxs]] + metric = _calc_vol_signed_metric(cell_coords) + + if abs(metric) > _TOL.geom: + return True + + return False + + +def _check_quad8_surf_row( + connect_row: np.ndarray, + coords: np.ndarray, +) -> bool: + """Return whether an eight-node row has the QUAD8 midside layout.""" + elem_coords = coords[connect_row] + corners = elem_coords[:4] + midsides = elem_coords[4:] + edge_starts = corners + edge_ends = np.roll(corners, -1, axis=0) + edges = edge_ends - edge_starts + edge_lengths = np.linalg.norm(edges, axis=1) + if np.any(edge_lengths <= _TOL.geom): + return False + + edge_params = np.sum((midsides - edge_starts) * edges, axis=1) + edge_params /= edge_lengths**2 + closest = edge_starts + edge_params[:, None] * edges + distances = np.linalg.norm(midsides - closest, axis=1) + on_edges = distances <= _TOL.quad8_edge * edge_lengths + between_corners = np.logical_and(edge_params >= 0.0, edge_params <= 1.0) + return bool(np.all(np.logical_and(on_edges, between_corners))) + + +def _check_surf_connect_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> bool: + """Return whether a connectivity table represents surface elements.""" + + if surf_only: + return True + return not _check_vol_connect_table(connect, coords) + + +def _calc_surf_orient_flips( + connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Return row reversals required by the std surface convention.""" + + corner_idxs = _get_corner_idxs(connect.shape[1]) + representatives: dict[tuple[int, ...], int] = {} + duplicate_of = np.arange(connect.shape[0], dtype=np.int64) + for row_idx, row in enumerate(connect): + key = tuple(sorted(int(node) for node in row[corner_idxs])) + representative = representatives.setdefault(key, row_idx) + duplicate_of[row_idx] = representative + + try: + topology = _build_surf_topology(connect, np.unique(duplicate_of)) + except ValueError as error: + if "Non-manifold surface edge" not in str(error): + raise + # Surface slices can deliberately contain non-manifold face sets. They + # have no single orientable shell, so retain the historical per-face + # behaviour rather than applying a false cavity interpretation. + return _calc_indep_surf_orient_flips(connect, coords) + + flips = np.zeros(connect.shape[0], dtype=bool) + closed_comps: list[ + tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] + ] = [] + + for rows, edge_keys in topology: + rel = _calc_comp_rel_flips(rows, edge_keys) + flips[rows] = rel + oriented = _apply_surf_flips(connect[rows], rel) + + is_closed = all(len(edge_keys[key]) == 2 for key in edge_keys) + if not is_closed: + # An open non-planar sheet has no intrinsic exterior. Preserve a + # coherent input orientation; planar sheets retain std CCW. + comp_nodes = np.unique(oriented[:, _get_corner_idxs( + oriented.shape[1] + )]) + comp_coords = coords[comp_nodes] + if _check_coplanar(comp_coords): + metric = _calc_first_surf_metric( + oriented, + comp_coords, + coords, + ) + if metric is not None and metric < 0.0: + flips[rows] = ~flips[rows] + continue + + vol = _calc_surf_signed_vol(oriented, coords) + if abs(vol) <= _TOL.geom: + raise ValueError( + "Closed surface component has zero signed volume; cannot " + "select a material exterior." + ) + if vol < 0.0: + flips[rows] = ~flips[rows] + oriented = _apply_surf_flips( + oriented, + np.ones(rows.shape[0], dtype=bool), + ) + comp_points = coords[np.unique(oriented[:, corner_idxs])] + closed_comps.append(( + rows, + oriented, + np.min(comp_points, axis=0), + np.max(comp_points, axis=0), + )) + + # A disconnected closed shell contained by another shell is a cavity. Its + # material-outward normal must point into the void, so its signed volume is + # negative after local edge consistency has been established. + for comp_idx, comp in enumerate(closed_comps): + rows, oriented, _, _ = comp + point = _calc_comp_probe_point(oriented, coords) + depth = 0 + for other_idx, other_comp in enumerate(closed_comps): + _, other_oriented, bounds_min, bounds_max = other_comp + if other_idx == comp_idx: + continue + if not np.all(point >= bounds_min - _TOL.geom): + continue + if not np.all(point <= bounds_max + _TOL.geom): + continue + depth += _check_point_in_closed_surf( + point, + other_oriented, + coords, + ) + if depth % 2: + flips[rows] = ~flips[rows] + + for row_idx, representative in enumerate(duplicate_of): + if row_idx == representative: + continue + same_orient = _check_surf_rows_same_orient( + connect[row_idx], + connect[representative], + coords, + ) + flips[row_idx] = flips[representative] ^ (not same_orient) + + return flips + + +def _calc_indep_surf_orient_flips( + connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Calculate each surface row's orientation flip independently.""" + flips = np.zeros(connect.shape[0], dtype=bool) + for row_idx, row in enumerate(connect): + metric = _calc_winding_metric(row, coords, surf_only=True) + flips[row_idx] = metric is not None and metric < 0.0 + return flips + + +def _build_surf_topology( + connect: np.ndarray, + active_rows: np.ndarray, +) -> list[tuple[np.ndarray, dict]]: + """Build connected surface components keyed by their corner-node edges.""" + + corner_idxs = _get_corner_idxs(connect.shape[1]) + edge_map: dict = {} + for row_idx in active_rows: + row = connect[row_idx] + corners = row[corner_idxs] + for node_a, node_b in zip(corners, np.roll(corners, -1)): + directed = (int(node_a), int(node_b)) + key = tuple(sorted(directed)) + edge_map.setdefault(key, []).append((row_idx, directed)) + + for key, uses in edge_map.items(): + if len(uses) > 2: + raise ValueError( + f"Non-manifold surface edge {key} has {len(uses)} " + "incident faces." + ) + + neighbours: dict[int, set[int]] = {} + for row in active_rows: + neighbours[int(row)] = set() + for uses in edge_map.values(): + if len(uses) == 2: + row_a, _ = uses[0] + row_b, _ = uses[1] + neighbours[row_a].add(row_b) + neighbours[row_b].add(row_a) + + comps: list[tuple[np.ndarray, dict]] = [] + unseen = set(int(row) for row in active_rows) + while unseen: + seed = unseen.pop() + rows = {seed} + stack = [seed] + while stack: + row = stack.pop() + for neighbour in neighbours[row]: + if neighbour in unseen: + unseen.remove(neighbour) + rows.add(neighbour) + stack.append(neighbour) + rows_arr = np.asarray(sorted(rows), dtype=np.int64) + row_set = set(rows_arr.tolist()) + comp_edges = {} + for key, uses in edge_map.items(): + if uses[0][0] in row_set: + comp_edges[key] = uses + comps.append((rows_arr, comp_edges)) + return comps + + +def _check_surf_rows_same_orient( + row_a: np.ndarray, + row_b: np.ndarray, + coords: np.ndarray, +) -> bool: + """Return whether duplicate surface rows have the same directed boundary.""" + + corner_idxs = _get_corner_idxs(row_a.shape[0]) + corners_a = row_a[corner_idxs] + corners_b = row_b[corner_idxs] + edges_b = set() + corners_b_next = np.roll(corners_b, -1) + for node_a, node_b in zip(corners_b, corners_b_next): + edges_b.add((int(node_a), int(node_b))) + for node_a, node_b in zip(corners_a, np.roll(corners_a, -1)): + if (int(node_a), int(node_b)) in edges_b: + return True + if (int(node_b), int(node_a)) in edges_b: + return False + raise ValueError("Duplicate surface faces do not share a boundary edge.") + + +def _calc_comp_rel_flips( + rows: np.ndarray, + edge_keys: dict, +) -> np.ndarray: + """Find local reversals making shared edges traverse opposite ways.""" + + row_set = set(rows.tolist()) + constraints: dict[int, list[tuple[int, bool]]] = {} + for row in row_set: + constraints[row] = [] + for uses in edge_keys.values(): + if len(uses) != 2: + continue + row_a, direct_a = uses[0] + row_b, direct_b = uses[1] + same_direct = direct_a == direct_b + constraints[row_a].append((row_b, same_direct)) + constraints[row_b].append((row_a, same_direct)) + + assigned: dict[int, bool] = {} + for seed in rows: + seed_int = int(seed) + if seed_int in assigned: + continue + assigned[seed_int] = False + stack = [seed_int] + while stack: + row = stack.pop() + for neighbour, xor_flip in constraints[row]: + expected = assigned[row] ^ xor_flip + if neighbour in assigned: + if assigned[neighbour] != expected: + raise ValueError("Surface component is not orientable.") + else: + assigned[neighbour] = expected + stack.append(neighbour) + return np.asarray([assigned[int(row)] for row in rows], dtype=bool) + + +def _apply_surf_flips(connect: np.ndarray, flips: np.ndarray) -> np.ndarray: + """Apply the requested winding reversal to each surface row.""" + out = np.array(connect, copy=True) + for row_idx in np.flatnonzero(flips): + out[row_idx] = _reverse_surf_row(out[row_idx]) + return out + + +def _calc_first_surf_metric( + connect: np.ndarray, + comp_coords: np.ndarray, + coords: np.ndarray, +) -> float | None: + """Calculate the first non-degenerate metric in a surface component.""" + normal = _calc_std_plane_normal(comp_coords) + corner_idxs = _get_corner_idxs(connect.shape[1]) + for row in connect: + metric = _calc_loc_polygon_signed_area( + coords[row[corner_idxs]], + normal, + ) + if metric is not None and abs(metric) > _TOL.geom: + return metric + return None + + +def _calc_surf_signed_vol( + connect: np.ndarray, + coords: np.ndarray, +) -> float: + """Calculate the signed volume enclosed by a triangulated surface.""" + corner_idxs = _get_corner_idxs(connect.shape[1]) + vol = 0.0 + for row in connect: + points = coords[row[corner_idxs]] + for point_idx in range(1, points.shape[0] - 1): + vol += float(np.dot( + points[0], + np.cross(points[point_idx], points[point_idx + 1]), + )) / 6.0 + return vol + + +def _calc_comp_probe_point( + connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Calculate an interior probe point near a surface component.""" + corner_idxs = _get_corner_idxs(connect.shape[1]) + points = coords[connect[0, corner_idxs]] + normal = np.cross(points[1] - points[0], points[2] - points[0]) + normal_norm = np.linalg.norm(normal) + if normal_norm <= _TOL.geom: + return np.mean(points, axis=0) + extent = np.ptp(coords, axis=0) + epsilon = max(float(np.linalg.norm(extent)) * 1.0e-9, _TOL.geom * 10.0) + return np.mean(points, axis=0) + epsilon * normal / normal_norm + + +def _check_point_in_closed_surf( + point: np.ndarray, + connect: np.ndarray, + coords: np.ndarray, +) -> bool: + """Classify a point with parity ray casting against a closed surface.""" + + corner_idxs = _get_corner_idxs(connect.shape[1]) + votes: list[bool] = [] + for direct in _POINT_IN_SURF_RAY_DIRECTS: + direct = direct / np.linalg.norm(direct) + hits = 0 + + for row in connect: + corners = coords[row[corner_idxs]] + + for point_idx in range(1, corners.shape[0] - 1): + intersects = _check_ray_intersects_triangle( + point, + direct, + corners[0], + corners[point_idx], + corners[point_idx + 1], + ) + if intersects: + hits += 1 + + votes.append(bool(hits % 2)) + + return sum(votes) >= 2 + + +def _check_ray_intersects_triangle( + origin: np.ndarray, + direct: np.ndarray, + point_a: np.ndarray, + point_b: np.ndarray, + point_c: np.ndarray, +) -> bool: + """Return whether a forward ray intersects a triangle.""" + + edge_ab = point_b - point_a + edge_ac = point_c - point_a + perpendicular = np.cross(direct, edge_ac) + determinant = float(np.dot(edge_ab, perpendicular)) + if abs(determinant) <= _TOL.geom: + return False + + inv_determinant = 1.0 / determinant + offset = origin - point_a + barycentric_u = inv_determinant * float(np.dot(offset, perpendicular)) + if barycentric_u <= _TOL.geom or barycentric_u >= 1.0 - _TOL.geom: + return False + + cross_offset_edge = np.cross(offset, edge_ab) + barycentric_v = inv_determinant * float( + np.dot(direct, cross_offset_edge) + ) + if ( + barycentric_v <= _TOL.geom + or barycentric_u + barycentric_v >= 1.0 - _TOL.geom + ): + return False + + distance = inv_determinant * float( + np.dot(edge_ac, cross_offset_edge) + ) + + return distance > _TOL.geom + + +def _enforce_surf_orient_table( + connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Return a table with std surface-component orientations.""" + return _apply_surf_flips( + connect, _calc_surf_orient_flips(connect, coords) + ) + + +def _copy_sim_data( + mesh_in: SimData, + connect: dict[str, np.ndarray] | None = None, +) -> SimData: + """Copy simulation data, optionally replacing its connectivity.""" + mesh_out = SimData( + mesh_type=mesh_in.mesh_type, + time=mesh_in.time, + coords=mesh_in.coords, + connect=mesh_in.connect if connect is None else connect, + side_sets=mesh_in.side_sets, + node_vars=mesh_in.node_vars, + elem_vars=mesh_in.elem_vars, + glob_vars=mesh_in.glob_vars, + ) + + return mesh_out + + +def _check_perms_equiv( + perm: tuple[int, ...], + ref_perm: tuple[int, ...], + symmetries: tuple[tuple[int, ...], ...], +) -> bool: + """Return whether permutations differ only by a proper symmetry.""" + for symmetry in symmetries: + + transformed_slots: list[int] = [] + for slot in symmetry: + transformed_slots.append(ref_perm[slot]) + transformed_perm = tuple(transformed_slots) + + if perm == transformed_perm: + return True + + return False + + +def infer_mesh_convention(mesh_in: SimData) -> MeshConvention: + """Infer one source-to-Riley ordering for each element family.""" + if mesh_in.coords is None or mesh_in.connect is None: + raise MeshConvErr( + "Mesh convention inference requires coordinates and connectivity." + ) + + inferred: dict[EElementType, tuple[int, ...]] = {} + for name, connect_raw in mesh_in.connect.items(): + + connect = _enforce_connect_arr_format(connect_raw, name) + if _check_transpose_needed(connect, name, mesh_in): + connect = connect.T + if _check_zero_based_shift_needed(connect, mesh_in.coords.shape[0]): + connect = connect - 1 + if not _check_idxs_zero_based(connect, mesh_in.coords.shape[0]): + raise MeshConvErr( + f"Connectivity table '{name}' has invalid indices." + ) + + nodes_per_elem = connect.shape[1] + if nodes_per_elem in _SURF_ONLY_NODE_COUNTS: + spec = _get_surf_spec(nodes_per_elem) + elif nodes_per_elem in _VOL_ONLY_NODE_COUNTS: + spec = _get_vol_spec(nodes_per_elem) + elif _check_surf_mesh_type(mesh_in.mesh_type): + spec = _get_surf_spec(nodes_per_elem) + elif _check_vol_connect_table(connect, mesh_in.coords): + spec = _get_vol_spec(nodes_per_elem) + else: + spec = _get_surf_spec(nodes_per_elem) + + elem_type = _get_elem_type_from_spec(spec) + + try: + normalised = _enforce_node_order_from_geom( + connect, mesh_in.coords, spec, + ) + except ValueError as error: + raise MeshConvErr( + f"Could not infer '{name}' ({elem_type.value}); supply " + "MeshConvention explicitly." + ) from error + + perms: set[tuple[int, ...]] = set() + for row, target in zip(connect, normalised, strict=True): + perm_slots: list[int] = [] + for node_id in target: + matching_slots = np.flatnonzero(row == node_id) + perm_slots.append(int(matching_slots[0])) + perms.add(tuple(perm_slots)) + + representative = min(perms) + symmetries = _get_elem_symmetries(elem_type) + layouts_equiv = True + + for candidate_perm in perms: + perms_equiv = _check_perms_equiv( + candidate_perm, + representative, + symmetries, + ) + if not perms_equiv: + layouts_equiv = False + break + + if not layouts_equiv: + raise MeshConvErr( + f"Connectivity table '{name}' contains multiple source " + f"layouts for {elem_type.value}; supply MeshConvention." + ) + + perm = representative + previous = inferred.get(elem_type) + layouts_disagree = False + + if previous is not None: + layouts_equiv = _check_perms_equiv( + perm, + previous, + symmetries, + ) + layouts_disagree = not layouts_equiv + + if layouts_disagree: + raise MeshConvErr( + ( + f"Multiple connectivity tables disagree on the " + f"{elem_type.value} source layout; supply " + "MeshConvention." + ) + ) + inferred[elem_type] = perm + + return MeshConvention( + MappingProxyType(inferred), + standardise_equiv_orients=True, + ) + + +def _check_surf_mesh_type(mesh_type: EMeshType | None) -> bool: + """Return whether an explicit mesh type denotes a surface mesh.""" + return mesh_type is EMeshType.SURF + + +def _enforce_connect_arr_format(connect: np.ndarray, name: str) -> np.ndarray: + """Return connectivity as a contiguous two-dimensional integer array.""" + arr = np.asarray(connect) + if arr.ndim != 2: + raise ValueError( + f"Connectivity table '{name}' must be 2D, got shape {arr.shape}." + ) + + return np.ascontiguousarray(arr, dtype=np.int64) + + +def _validate_nodes_per_elem(nodes_per_elem: int) -> None: + """Raise if an element node count is unsupported.""" + if nodes_per_elem not in _SUPPORTED_NODE_COUNTS: + raise NotImplementedError( + "Mesh convention tools do not support elements with " + f"{nodes_per_elem} nodes." + ) + + +def _check_transpose_needed( + connect: np.ndarray, + connect_name: str | None = None, + mesh_in: SimData | None = None, +) -> bool: + """Return whether connectivity must be transposed to row-major form.""" + rows_supported = connect.shape[0] in _SUPPORTED_NODE_COUNTS + cols_supported = connect.shape[1] in _SUPPORTED_NODE_COUNTS + + if rows_supported and not cols_supported: + return True + + if cols_supported and not rows_supported: + return False + + if cols_supported and rows_supported: + mesh_is_vol = mesh_in is not None and mesh_in.mesh_type is EMeshType.VOL + if mesh_is_vol: + rows_vol_only = connect.shape[0] in _VOL_ONLY_NODE_COUNTS + cols_vol_only = connect.shape[1] in _VOL_ONLY_NODE_COUNTS + if rows_vol_only != cols_vol_only: + return rows_vol_only + + mesh_is_surf = ( + mesh_in is not None + and mesh_in.mesh_type is EMeshType.SURF + ) + if mesh_is_surf: + rows_surf_only = connect.shape[0] in _SURF_ONLY_NODE_COUNTS + cols_surf_only = connect.shape[1] in _SURF_ONLY_NODE_COUNTS + if rows_surf_only != cols_surf_only: + return rows_surf_only + + block_rows = _get_elem_var_row_counts(connect_name, mesh_in) + if block_rows is not None: + row_match = connect.shape[0] in block_rows + col_match = connect.shape[1] in block_rows + if row_match and not col_match: + return False + if col_match and not row_match: + return True + + if mesh_in is not None and mesh_in.coords is not None: + return ( + _calc_orient_score(connect.T, mesh_in.coords) + > _calc_orient_score(connect, mesh_in.coords) + ) + + return False + + raise NotImplementedError( + "Could not infer connectivity orientation from shape " + f"{connect.shape}. Expected a supported nodes-per-element dimension." + ) + + +def _check_zero_based_shift_needed( + connect: np.ndarray, + num_coords: int, +) -> bool: + """Return whether connectivity is unambiguously one-based.""" + if connect.size == 0: + return False + + if np.any(connect < 0): + return False + + if np.any(connect == 0): + return False + + return bool(np.any(connect >= num_coords)) + + +def _check_ambiguously_positive(connect: np.ndarray, num_coords: int) -> bool: + """Return whether positive indices could use either indexing convention.""" + if connect.size == 0: + return False + + return bool( + np.all(connect >= 1) + and np.all(connect < num_coords) + and not np.any(connect == 0) + ) + + +def _check_mesh_needs_zero_based_shift( + mesh_in: SimData, + num_coords: int, +) -> bool: + """Return whether all connectivity tables require a one-based shift.""" + any_zero_based = False + any_definitely_one_based = False + + for connect_name, connect_raw in mesh_in.connect.items(): + connect = np.asarray(connect_raw, dtype=np.int64) + + if _check_transpose_needed(connect, connect_name, mesh_in): + connect = connect.T + + if np.any(connect == 0): + any_zero_based = True + + if _check_zero_based_shift_needed(connect, num_coords): + any_definitely_one_based = True + + if any_zero_based and any_definitely_one_based: + raise ValueError( + ( + "Mixed zero-based and one-based connectivity tables " + "detected in the same mesh." + ) + ) + + return any_definitely_one_based and not any_zero_based + + +def _check_table_needs_zero_based_shift( + connect: np.ndarray, + num_coords: int, + shift_all: bool, +) -> bool: + """Return whether one connectivity table requires a one-based shift.""" + if _check_zero_based_shift_needed(connect, num_coords): + return True + return shift_all and _check_ambiguously_positive(connect, num_coords) + + +def _get_elem_var_row_counts( + connect_name: str | None, + mesh_in: SimData | None, +) -> set[int] | None: + """Return element-variable row counts associated with a mesh block.""" + if connect_name is None or mesh_in is None or mesh_in.elem_vars is None: + return None + + try: + block_id = int(connect_name.replace("connect", "")) + except ValueError: + return None + + row_counts: set[int] = set() + for (_field_name, field_block), values in mesh_in.elem_vars.items(): + if field_block == block_id: + row_counts.add(values.shape[0]) + return row_counts or None + + +def _check_idxs_zero_based(connect: np.ndarray, num_coords: int) -> bool: + """Return whether every index is valid zero-based connectivity.""" + if connect.size == 0: + return True + valid_mask = np.logical_and(connect >= 0, connect < num_coords) + return bool(np.all(valid_mask)) + + +def _calc_orient_score( + connect_row_major: np.ndarray, + coords: np.ndarray, +) -> int: + """Score a candidate row-major interpretation by valid element rows.""" + num_coords = coords.shape[0] + connect_eval = np.asarray(connect_row_major, dtype=np.int64) + + if ( + connect_eval.ndim != 2 + or connect_eval.shape[1] not in _SUPPORTED_NODE_COUNTS + ): + return -1 + + if _check_zero_based_shift_needed(connect_eval, num_coords): + connect_eval = connect_eval - 1 + + if not _check_idxs_zero_based(connect_eval, num_coords): + return -1 + + score = 0 + for row in connect_eval: + if np.unique(row).shape[0] != row.shape[0]: + continue + + try: + metric = _calc_row_orient_metric(row, coords) + except ValueError: + continue + + if metric is not None and abs(metric) > _TOL.geom: + score += 1 + + return score + + +def _calc_row_orient_metric( + connect_row: np.ndarray, + coords: np.ndarray, +) -> float | None: + """Return a metric only for resolving row-major connectivity ambiguity.""" + nodes_per_elem = connect_row.shape[0] + + if nodes_per_elem in _SURF_NODE_COUNTS: + corner_coords = coords[connect_row[_get_corner_idxs(nodes_per_elem)]] + if _check_coplanar(corner_coords): + return _calc_polygon_signed_area(corner_coords) + + if nodes_per_elem in _VOL_NODE_COUNTS: + vol_spec = _get_vol_spec(nodes_per_elem) + vol_coords = coords[ + connect_row[np.asarray(vol_spec.corner_idxs, dtype=np.int64)] + ] + metric = _calc_vol_signed_metric(vol_coords) + if ( + nodes_per_elem in _VOL_ONLY_NODE_COUNTS + or abs(metric) > _TOL.geom + ): + return metric + + return None + + +def _enforce_node_order_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, + src_convention: MeshConvention | None = None, +) -> np.ndarray: + """Rebuild higher-order rows from geometry, independent of source order.""" + if connect.shape[1] in _SURF_ONLY_NODE_COUNTS: + spec = _get_surf_spec(connect.shape[1]) + elif connect.shape[1] in _VOL_ONLY_NODE_COUNTS: + spec = _get_vol_spec(connect.shape[1]) + elif surf_only or not _check_vol_connect_table(connect, coords): + spec = _get_surf_spec(connect.shape[1]) + else: + spec = _get_vol_spec(connect.shape[1]) + + if src_convention is not None: + perm = src_convention.get_src_perm( + _get_elem_type_from_spec(spec) + ) + if perm is not None: + perm_size_valid = len(perm) == connect.shape[1] + expected_slots = set(range(connect.shape[1])) + perm_slots_valid = set(perm) == expected_slots + if not perm_size_valid or not perm_slots_valid: + raise ValueError( + "MeshConvention permutation does not match the element " + f"topology {_get_elem_type_from_spec(spec).value}." + ) + normalised = np.ascontiguousarray( + connect[:, np.asarray(perm, dtype=np.int64)], + dtype=np.int64, + ) + if src_convention.standardise_equiv_orients: + return _enforce_std_orients(normalised, spec) + return normalised + + return connect + + +def _enforce_node_order_high_order( + connect: np.ndarray, + coords: np.ndarray, + spec: ElementSpec, +) -> np.ndarray: + """Rebuild higher-order rows from inferred geometric roles.""" + + connect_out = np.empty_like(connect) + for row_idx, row in enumerate(connect): + connect_out[row_idx] = _enforce_node_order_high_order_row( + row, + coords, + spec, + ) + + return np.ascontiguousarray(connect_out, dtype=np.int64) + + +def _enforce_node_order_from_geom( + connect: np.ndarray, + coords: np.ndarray, + spec: ElementSpec, +) -> np.ndarray: + """Infer node roles, then select the unique ID-stable proper orientation.""" + + if connect.shape[1] == len(spec.corner_idxs): + connect_out = np.empty_like(connect) + src_slots = np.arange(connect.shape[1], dtype=np.int64) + + for row_idx, row in enumerate(connect): + if spec.is_surf: + ordered = _order_surf_corners(coords[row], src_slots) + elif len(spec.corner_idxs) == 4: + ordered = _order_tet_corners(coords[row], src_slots) + else: + ordered = _order_hex_corners(coords[row], src_slots) + connect_out[row_idx] = row[ordered] + else: + connect_out = _enforce_node_order_high_order(connect, coords, spec) + + return _enforce_std_orients(connect_out, spec) + + +def _enforce_std_orients( + connect: np.ndarray, + spec: ElementSpec, +) -> np.ndarray: + """Anchor each role-correct row at the lowest valid global-node sequence.""" + elem_type = _get_elem_type_from_spec(spec) + perms_arr = _get_elem_symmetry_arrs(elem_type) + out = np.empty_like(connect) + for row_idx, row in enumerate(connect): + out[row_idx] = min( + (row[perm] for perm in perms_arr), + key=lambda candidate: tuple(int(node) for node in candidate), + ) + return np.ascontiguousarray(out, dtype=np.int64) + + +def _enforce_node_order_high_order_row( + connect_row: np.ndarray, + coords: np.ndarray, + spec: ElementSpec, +) -> np.ndarray: + """Infer std node roles for one higher-order element row.""" + elem_coords = coords[connect_row] + if _check_std_node_roles(elem_coords, spec): + return connect_row + + corner_count = len(spec.corner_idxs) + corner_loc = _infer_corner_nodes(elem_coords, corner_count) + + if spec.is_surf: + corner_loc = _order_surf_corners(elem_coords, corner_loc) + elif corner_count == 4: + corner_loc = _order_tet_corners(elem_coords, corner_loc) + else: + corner_loc = _order_hex_corners(elem_coords, corner_loc) + + corner_coords = elem_coords[corner_loc] + remaining: list[int] = [] + for idx in range(connect_row.shape[0]): + if idx not in corner_loc: + remaining.append(idx) + + edge_pairs = spec.edge_pairs + if not edge_pairs: + edge_pairs_out: list[tuple[int, int]] = [] + for idx in range(corner_count): + edge_pairs_out.append((idx, (idx + 1) % corner_count)) + edge_pairs = tuple(edge_pairs_out) + edge_targets_out: list[np.ndarray] = [] + for start, end in edge_pairs: + edge_targets_out.append( + 0.5 * (corner_coords[start] + corner_coords[end]), + ) + edge_targets = np.asarray(edge_targets_out, dtype=np.float64) + edge_loc = _match_role_nodes( + elem_coords, + remaining, + edge_targets, + "edge", + ) + remaining_out = [] + for idx in remaining: + if idx not in edge_loc: + remaining_out.append(idx) + remaining = remaining_out + + if spec.face_centre_idxs: + face_targets_out: list[np.ndarray] = [] + for face in spec.face_corner_idxs: + face_idxs = list(face) + face_targets_out.append( + np.mean(corner_coords[face_idxs], axis=0), + ) + + face_targets = np.asarray(face_targets_out, dtype=np.float64) + face_loc = _match_role_nodes( + elem_coords, + remaining, + face_targets, + "face centre", + ) + + remaining_out = [] + for idx in remaining: + if idx not in face_loc: + remaining_out.append(idx) + + remaining = remaining_out + + if spec.centre_idx is not None or spec.cell_centre_idx is not None: + centre_loc = _match_role_nodes( + elem_coords, + remaining, + np.mean(corner_coords, axis=0, keepdims=True), + "centre", + ) + + remaining_out = [] + for idx in remaining: + if idx not in centre_loc: + remaining_out.append(idx) + + remaining = remaining_out + + if remaining: + raise ValueError( + "Could not assign every higher-order node to an element role." + ) + + out_loc = np.empty(connect_row.shape[0], dtype=np.int64) + out_loc[np.asarray(spec.corner_idxs, dtype=np.int64)] = corner_loc + edge_slots = np.arange( + corner_count, corner_count + len(edge_pairs), dtype=np.int64 + ) + out_loc[edge_slots] = edge_loc + + if spec.face_centre_idxs: + out_loc[ + np.asarray(spec.face_centre_idxs, dtype=np.int64) + ] = face_loc + + if spec.centre_idx is not None: + out_loc[spec.centre_idx] = centre_loc[0] + + if spec.cell_centre_idx is not None: + out_loc[spec.cell_centre_idx] = centre_loc[0] + + return connect_row[out_loc] + + +def _check_std_node_roles( + elem_coords: np.ndarray, + spec: ElementSpec, +) -> bool: + """Accept an already coherent row, including curved and seam elements.""" + corner_idxs = np.asarray(spec.corner_idxs, dtype=np.int64) + corner_coords = elem_coords[corner_idxs] + + rank_required = 2 if spec.is_surf else 3 + corner_center = np.mean(corner_coords, axis=0) + centered_corners = corner_coords - corner_center + corner_rank = np.linalg.matrix_rank(centered_corners, tol=_TOL.geom) + if corner_rank < rank_required: + return False + + inferred_corners = _infer_corner_nodes(elem_coords, len(corner_idxs)) + if (set(inferred_corners) == set(corner_idxs) + and inferred_corners.shape[0] == len(corner_idxs)): + pass + elif inferred_corners.shape[0] == len(corner_idxs): + return False + + scale = max(float(np.ptp(elem_coords, axis=0).max()), _TOL.geom) + edge_pairs = spec.edge_pairs + if not edge_pairs: + + edge_pairs_out = [] + for idx in range(len(corner_idxs)): + edge_pairs_out.append((idx, (idx + 1) % len(corner_idxs))) + + edge_pairs = tuple(edge_pairs_out) + + edge_start = len(corner_idxs) + edge_stop = edge_start + len(edge_pairs) + edge_nodes = elem_coords[edge_start:edge_stop] + edge_distances_out: list[float] = [] + + for node, (start, end) in zip(edge_nodes, edge_pairs, strict=True): + edge_distances_out.append(_calc_point_segment_distance( + node, + corner_coords[start], + corner_coords[end], + )) + + edge_distances = np.asarray(edge_distances_out) + + if np.any(edge_distances > _TOL.role_match * scale): + return False + + if spec.face_centre_idxs: + face_data = zip( + spec.face_centre_idxs, + spec.face_corner_idxs, + strict=True, + ) + for centre_idx, face_corner_idxs in face_data: + face_coords = corner_coords[np.asarray(face_corner_idxs)] + face_target = np.mean(face_coords, axis=0) + face_error = np.linalg.norm( + elem_coords[centre_idx] - face_target, + ) + if face_error > _TOL.role_match * scale: + return False + + if spec.centre_idx is not None: + centre_error = np.linalg.norm( + elem_coords[spec.centre_idx] - corner_center, + ) + + if centre_error > _TOL.role_match * scale: + return False + + if spec.cell_centre_idx is not None: + cell_centre_error = np.linalg.norm( + elem_coords[spec.cell_centre_idx] - corner_center, + ) + + if cell_centre_error > _TOL.role_match * scale: + return False + + return True + + +def _infer_corner_nodes( + elem_coords: np.ndarray, + corner_count: int, +) -> np.ndarray: + """Find affine-element vertices without depending on their source slots.""" + + scale = max(float(np.ptp(elem_coords, axis=0).max()), _TOL.geom) + midpoint_tol = _TOL.geom * scale * 100.0 + midpoint_nodes: set[int] = set() + + for node_idx, point in enumerate(elem_coords): + for first in range(elem_coords.shape[0]): + for second in range(first + 1, elem_coords.shape[0]): + + if node_idx in (first, second): + continue + + midpoint = 0.5 * (elem_coords[first] + elem_coords[second]) + + if np.linalg.norm(point - midpoint) <= midpoint_tol: + midpoint_nodes.add(node_idx) + break + + if node_idx in midpoint_nodes: + break + + candidates_out: list[int] = [] + for idx in range(elem_coords.shape[0]): + if idx not in midpoint_nodes: + candidates_out.append(idx) + + candidates = np.asarray(candidates_out, dtype=np.int64) + if candidates.shape[0] == corner_count: + return candidates + + distances = np.linalg.norm( + elem_coords - np.mean(elem_coords, axis=0), + axis=1, + ) + + return np.sort(np.argsort(distances)[-corner_count:]) + + +def _calc_point_segment_distance( + point: np.ndarray, + start: np.ndarray, + end: np.ndarray, +) -> float: + """Calculate the shortest distance from a point to a line segment.""" + direct = end - start + length_sq = float(np.dot(direct, direct)) + + if length_sq <= _TOL.geom: + return np.inf + + param = np.clip( + float(np.dot(point - start, direct) / length_sq), 0.0, 1.0 + ) + + return float(np.linalg.norm(point - (start + param * direct))) + + +def _order_surf_corners( + elem_coords: np.ndarray, + corner_loc: np.ndarray, +) -> np.ndarray: + """Return surface-corner slots in counter-clockwise order.""" + + corner_coords = elem_coords[corner_loc] + centred = corner_coords - np.mean(corner_coords, axis=0) + if np.linalg.matrix_rank(centred, tol=_TOL.geom) < 2: + raise ValueError( + "Degenerate surface element has collinear corner nodes." + ) + + _, _, vecs = np.linalg.svd(centred, full_matrices=False) + normal = vecs[-1] + axis_u = vecs[0] + axis_v = np.cross(normal, axis_u) + angles = np.arctan2(centred @ axis_v, centred @ axis_u) + + ordered = corner_loc[np.argsort(angles)] + ordered_coords = elem_coords[ordered] + + signed = np.dot( + np.cross( + ordered_coords[1] - ordered_coords[0], + ordered_coords[2] - ordered_coords[0], + ), + normal, + ) + + if signed < 0.0: + ordered = ordered[[0, *range(len(ordered) - 1, 0, -1)]] + + return ordered + + +def _order_tet_corners( + elem_coords: np.ndarray, + corner_loc: np.ndarray, +) -> np.ndarray: + """Return tetrahedron-corner slots in right-handed order.""" + ordered = corner_loc[np.lexsort(elem_coords[corner_loc].T[::-1])] + corner_coords = elem_coords[ordered] + metric = _calc_tet_signed_vol(corner_coords) + + if abs(metric) <= _TOL.geom: + raise ValueError("Degenerate tetrahedron has zero signed volume.") + + if metric < 0.0: + ordered[[1, 2]] = ordered[[2, 1]] + + return ordered + + +def _order_hex_corners( + elem_coords: np.ndarray, + corner_loc: np.ndarray, +) -> np.ndarray: + """Return hexahedron-corner slots in right-handed Riley order.""" + + corner_coords = elem_coords[corner_loc] + corner_center = np.mean(corner_coords, axis=0) + centered_corners = corner_coords - corner_center + corner_rank = np.linalg.matrix_rank(centered_corners, tol=_TOL.geom) + + if corner_rank < 3: + raise ValueError("Degenerate hexahedron has coplanar corner nodes.") + + origin = corner_loc[np.lexsort(corner_coords.T[::-1])[0]] + candidates = [] + for idx in corner_loc: + if idx != origin: + candidates.append(idx) + + best_order: np.ndarray | None = None + best_error = np.inf + origin_coord = elem_coords[origin] + + for first in candidates: + for second in candidates: + for third in candidates: + if len({first, second, third}) != 3: + continue + + directs = np.array(( + elem_coords[first] - origin_coord, + elem_coords[second] - origin_coord, + elem_coords[third] - origin_coord, + )) + + determinant = np.linalg.det(directs) + if determinant <= _TOL.geom: + continue + + targets = np.array(( + origin_coord, + origin_coord + directs[0], + origin_coord + directs[0] + directs[1], + origin_coord + directs[1], + origin_coord + directs[2], + origin_coord + directs[0] + directs[2], + origin_coord + + directs[0] + + directs[1] + + directs[2], + origin_coord + directs[1] + directs[2], + )) + + assigned = _match_role_nodes( + elem_coords, + list(corner_loc), + targets, + "corner", + validate=False, + ) + + error = float(np.sum( + np.linalg.norm(elem_coords[assigned] - targets, axis=1) + )) + + if error < best_error: + best_error = error + best_order = np.asarray(assigned, dtype=np.int64) + + if best_order is None: + raise ValueError( + "Could not determine a right-handed hexahedron corner order." + ) + + scale = float(np.max( + np.linalg.norm(corner_coords - origin_coord, axis=1) + )) + + if best_error > _TOL.role_match * scale: + raise ValueError("Hexahedron corner nodes do not form a valid element.") + + return best_order + + +def _match_role_nodes( + elem_coords: np.ndarray, + candidate_loc: list[int], + targets: np.ndarray, + role_name: str, + validate: bool = True, +) -> list[int]: + """Match unused local nodes to target geometric roles.""" + + if len(candidate_loc) < targets.shape[0]: + raise ValueError( + f"Not enough nodes available to assign {role_name} roles." + ) + + candidates = np.asarray(candidate_loc, dtype=np.int64) + distances = np.linalg.norm( + elem_coords[candidates, None, :] - targets[None, :, :], + axis=2, + ) + + assigned: list[int] = [] + used: set[int] = set() + for target_idx in range(targets.shape[0]): + ranked = np.argsort(distances[:, target_idx]) + match_idx = None + + for idx in ranked: + if int(idx) not in used: + match_idx = idx + break + + if match_idx is None: + raise ValueError(f"Could not assign a unique {role_name} node.") + + used.add(int(match_idx)) + assigned.append(int(candidates[match_idx])) + + if validate: + scale = max(float(np.ptp(elem_coords, axis=0).max()), _TOL.geom) + errors = np.linalg.norm(elem_coords[assigned] - targets, axis=1) + if np.any(errors > _TOL.role_match * scale): + raise ValueError( + f"Could not match {role_name} nodes to the element geometry." + ) + + return assigned + + +def _calc_active_coord_axes(coords: np.ndarray) -> np.ndarray: + """Return the first two coordinate axes with nonzero extent.""" + axis_range = np.ptp(coords, axis=0) + active = np.flatnonzero(axis_range > _TOL.geom) + + if active.shape[0] < 2: + raise ValueError("At least two active coordinate axes are required.") + + return active[:2] + + +def _calc_polygon_signed_area(coords_elem: np.ndarray) -> float: + """Calculate polygon signed area on its active coordinate plane.""" + axes = _calc_active_coord_axes(coords_elem) + xy = coords_elem[:, axes] + rolled = np.roll(xy, -1, axis=0) + return 0.5 * np.sum(xy[:, 0] * rolled[:, 1] - rolled[:, 0] * xy[:, 1]) + + +def _check_coplanar(coords_elem: np.ndarray) -> bool: + """Return whether coordinates lie on one plane.""" + centred = coords_elem - np.mean(coords_elem, axis=0) + return np.linalg.matrix_rank(centred, tol=_TOL.geom) <= 2 + + +def _calc_tet_signed_vol(coords_elem: np.ndarray) -> float: + """Calculate a tetrahedron's signed volume metric.""" + return float( + np.linalg.det( + np.column_stack(( + coords_elem[1] - coords_elem[0], + coords_elem[2] - coords_elem[0], + coords_elem[3] - coords_elem[0], + )) + ) + ) + + +def _calc_hex_signed_vol(coords_elem: np.ndarray) -> float: + """Calculate a hexahedron's signed corner metric.""" + return float( + np.linalg.det( + np.column_stack(( + coords_elem[1] - coords_elem[0], + coords_elem[3] - coords_elem[0], + coords_elem[4] - coords_elem[0], + )) + ) + ) + + +def _calc_vol_signed_metric(corner_coords: np.ndarray) -> float: + """Calculate the signed metric for tetrahedral or hexahedral corners.""" + if corner_coords.shape[0] == 4: + return _calc_tet_signed_vol(corner_coords) + return _calc_hex_signed_vol(corner_coords) + + +def _calc_winding_metric( + connect_row: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> float | None: + """Calculate a signed winding metric when a row is a surface element.""" + nodes_per_elem = connect_row.shape[0] + if nodes_per_elem not in _SURF_NODE_COUNTS: + return None + + if not surf_only and nodes_per_elem not in _SURF_ONLY_NODE_COUNTS: + vol_spec = _get_vol_spec(nodes_per_elem) + cell_coords = coords[ + connect_row[np.asarray(vol_spec.corner_idxs, dtype=np.int64)] + ] + signed_metric = _calc_vol_signed_metric(cell_coords) + if abs(signed_metric) > _TOL.geom: + return None + + corner_idxs = _get_corner_idxs(nodes_per_elem) + coords_elem = coords[connect_row[corner_idxs]] + + if _check_coplanar(coords): + ref_normal = _calc_std_plane_normal(coords) + else: + face_centroid = np.mean(coords_elem, axis=0) + outward = face_centroid - np.mean(coords, axis=0) + outward_norm = np.linalg.norm(outward) + if outward_norm <= _TOL.geom: + return None + ref_normal = outward / outward_norm + + return _calc_loc_polygon_signed_area(coords_elem, ref_normal) + + +def _calc_std_plane_normal(coords: np.ndarray) -> np.ndarray: + """Calculate a deterministically signed normal for planar coordinates.""" + centred = coords - np.mean(coords, axis=0) + _, _, vecs = np.linalg.svd(centred, full_matrices=False) + normal = vecs[-1] + dominant_axis = int(np.argmax(np.abs(normal))) + + if normal[dominant_axis] < 0.0: + normal = -normal + + return normal / np.linalg.norm(normal) + + +def _calc_loc_polygon_signed_area( + coords_elem: np.ndarray, + ref_normal: np.ndarray, +) -> float | None: + """Calculate polygon area signed against a reference normal.""" + origin = coords_elem[0] + axis_u = None + for point in coords_elem[1:]: + edge = point - origin + edge -= np.dot(edge, ref_normal) * ref_normal + edge_norm = np.linalg.norm(edge) + if edge_norm > _TOL.geom: + axis_u = edge / edge_norm + break + + if axis_u is None: + return None + + axis_v = np.cross(ref_normal, axis_u) + projected = np.column_stack(( + (coords_elem - origin) @ axis_u, + (coords_elem - origin) @ axis_v, + )) + rolled = np.roll(projected, -1, axis=0) + + return float(0.5 * np.sum( + projected[:, 0] * rolled[:, 1] + - rolled[:, 0] * projected[:, 1], + )) + + +def _calc_handedness_metric( + connect_row: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> float | None: + """Calculate the applicable signed handedness metric for an element.""" + nodes_per_elem = connect_row.shape[0] + if surf_only: + return _calc_winding_metric(connect_row, coords, surf_only=True) + + if nodes_per_elem in _VOL_NODE_COUNTS: + vol_spec = _get_vol_spec(nodes_per_elem) + vol_coords = coords[ + connect_row[np.asarray(vol_spec.corner_idxs, dtype=np.int64)] + ] + vol_metric = _calc_vol_signed_metric(vol_coords) + + if nodes_per_elem in _VOL_ONLY_NODE_COUNTS: + return vol_metric + + if abs(vol_metric) > _TOL.geom: + return vol_metric + + if nodes_per_elem in _SURF_NODE_COUNTS: + metric = _calc_winding_metric(connect_row, coords) + + if metric is not None: + return metric + + raise NotImplementedError( + ( + f"Handedness checks are not implemented for " + f"{nodes_per_elem}-node elements." + ) + ) + + +def _check_ccw_winding_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> bool: + """Return whether all applicable rows have counter-clockwise winding.""" + for row in connect: + metric = _calc_winding_metric(row, coords, surf_only=surf_only) + + if metric is None: + continue + + if abs(metric) <= _TOL.geom: + continue + + if metric <= 0.0: + return False + + return True + + +def _check_right_handed_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> bool: + """Return whether all applicable rows have positive handedness.""" + for row in connect: + metric = _calc_handedness_metric(row, coords, surf_only=surf_only) + + if metric is None: + continue + + if abs(metric) <= _TOL.geom: + continue + + if metric <= 0.0: + return False + + return True + + +def _reverse_surf_row(connect_row: np.ndarray) -> np.ndarray: + """Reverse winding while preserving every surface-node role.""" + + spec = _get_surf_spec(connect_row.shape[0]) + if spec.surf_reverse_perm is None: + raise NotImplementedError( + ( + f"Surface reversal is not implemented for " + f"{spec.nodes_per_elem}-node elements." + ) + ) + + perm = np.asarray( + spec.surf_reverse_perm, + dtype=np.int64, + ) + + return connect_row[perm] + + +def _reverse_handedness_row(connect_row: np.ndarray) -> np.ndarray: + """Reverse handedness while preserving every volume-node role.""" + spec = _get_vol_spec(connect_row.shape[0]) + + if spec.handedness_reverse_perm is None: + raise NotImplementedError( + ( + f"Handedness reversal is not implemented for " + f"{spec.nodes_per_elem}-node elements." + ) + ) + + perm = np.asarray( + spec.handedness_reverse_perm, + dtype=np.int64, + ) + + return connect_row[perm] + + +def _get_face_corner_coords(face_coords: np.ndarray) -> np.ndarray: + """Return only the corner coordinates from a surface face.""" + nodes_per_face = face_coords.shape[0] + corner_idxs = np.asarray( + _get_surf_spec(nodes_per_face).corner_idxs, + dtype=np.int64, + ) + + return face_coords[corner_idxs] + + +def _calc_face_normal(face_coords: np.ndarray) -> np.ndarray: + """Calculate a unit normal from a non-degenerate surface face.""" + face_corners = _get_face_corner_coords(face_coords) + face_normal = np.cross( + face_corners[1] - face_corners[0], + face_corners[2] - face_corners[0], + ) + normal_mag = np.linalg.norm(face_normal) + + if normal_mag <= _TOL.geom and face_corners.shape[0] == 4: + face_normal = np.cross( + face_corners[2] - face_corners[0], + face_corners[3] - face_corners[0], + ) + normal_mag = np.linalg.norm(face_normal) + + if normal_mag <= _TOL.geom: + raise ValueError( + "Degenerate face detected while extracting the surface mesh." + ) + + return face_normal / normal_mag + + +def _enforce_surf_face_outward( + face_connect: np.ndarray, + parent_connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Orient an extracted face away from its parent element.""" + face_coords = coords[face_connect] + face_centroid = np.mean(_get_face_corner_coords(face_coords), axis=0) + + parent_corners = _get_vol_corner_idxs(parent_connect.shape[0]) + parent_centroid = np.mean(coords[parent_connect[parent_corners]], axis=0) + + face_normal = _calc_face_normal(face_coords) + outward_dir = face_centroid - parent_centroid + + if np.dot(face_normal, outward_dir) < 0.0: + return _reverse_surf_row(face_connect) + + return face_connect + + +def _enforce_surf_face_node_order( + face_connect: np.ndarray, + coords: np.ndarray, +) -> np.ndarray: + """Restore high-order node roles on an extracted surface face.""" + nodes_per_face = face_connect.shape[0] + face_out = np.copy(face_connect) + face_coords = coords[face_out] + + spec = _get_surf_spec(nodes_per_face) + corner_idxs = np.asarray(spec.corner_idxs, dtype=np.int64) + num_corners = corner_idxs.shape[0] + if nodes_per_face == num_corners: + return face_out + + midside_pool = np.arange(num_corners, nodes_per_face, dtype=np.int64) + edge_corner_pairs_out: list[tuple[int, int]] = [] + for corner_idx in range(num_corners): + edge_corner_pairs_out.append( + (corner_idx, (corner_idx + 1) % num_corners), + ) + + edge_corner_pairs = tuple(edge_corner_pairs_out) + + face_centroid = np.mean(face_coords[corner_idxs, :], axis=0) + mid_pool_coords = face_coords[midside_pool, :] + + if spec.centre_idx is not None: + centroid_dists = np.linalg.norm(mid_pool_coords - face_centroid, axis=1) + center_pool_idx = int(np.argmin(centroid_dists)) + center_loc_idx = int(midside_pool[center_pool_idx]) + edge_pool_mask = np.ones(midside_pool.shape[0], dtype=bool) + edge_pool_mask[center_pool_idx] = False + edge_pool_loc_idxs = midside_pool[edge_pool_mask] + edge_pool_coords = face_coords[edge_pool_loc_idxs, :] + else: + center_loc_idx = -1 + edge_pool_loc_idxs = midside_pool + edge_pool_coords = mid_pool_coords + + edge_midpoints_out: list[np.ndarray] = [] + for start_idx, end_idx in edge_corner_pairs: + edge_midpoints_out.append( + 0.5 * ( + face_coords[start_idx, :] + + face_coords[end_idx, :] + ), + ) + + edge_midpoints = np.asarray(edge_midpoints_out, dtype=np.float64) + edge_dists = np.linalg.norm( + edge_pool_coords[:, None, :] - edge_midpoints[None, :, :], + axis=2, + ) + edge_order = np.argmin(edge_dists, axis=0) + reordered_edge_idxs = edge_pool_loc_idxs[edge_order] + + face_out[ + num_corners:num_corners + num_corners + ] = face_out[reordered_edge_idxs] + + if spec.centre_idx is not None: + face_out[spec.centre_idx] = face_out[center_loc_idx] + + return face_out + + +def _enforce_ccw_winding_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> np.ndarray: + """Return connectivity with applicable rows wound counter-clockwise.""" + + connect_out = np.copy(connect) + for idx, row in enumerate(connect_out): + metric = _calc_winding_metric(row, coords, surf_only=surf_only) + + if metric is not None and metric < 0.0: + connect_out[idx, :] = _reverse_surf_row(row) + + return np.ascontiguousarray(connect_out, dtype=np.int64) + + +def _enforce_right_handed_table( + connect: np.ndarray, + coords: np.ndarray, + surf_only: bool = False, +) -> np.ndarray: + """Return connectivity with positive handedness where applicable.""" + + connect_out = np.copy(connect) + for idx, row in enumerate(connect_out): + + metric = _calc_handedness_metric(row, coords, surf_only=surf_only) + if metric is not None and metric < 0.0: + row_is_surf = row.shape[0] in _SURF_NODE_COUNTS + row_is_coplanar = False + + if row_is_surf: + corner_idxs = _get_corner_idxs(row.shape[0]) + corner_coords = coords[row[corner_idxs]] + row_is_coplanar = _check_coplanar(corner_coords) + + reverse_surf = surf_only or (row_is_surf and row_is_coplanar) + + if reverse_surf: + connect_out[idx, :] = _reverse_surf_row(row) + else: + connect_out[idx, :] = _reverse_handedness_row(row) + + return np.ascontiguousarray(connect_out, dtype=np.int64) + + +def _extract_surf_faces_from_table( + connect: np.ndarray, + coords: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """Extract boundary faces and their parent rows from a volume table.""" + nodes_per_elem = connect.shape[1] + face_map = _get_surf_map(nodes_per_elem) + faces_wound = connect[:, face_map] + faces_flat_wound = faces_wound.reshape((-1, face_map.shape[1])) + faces_flat_sorted = np.sort(faces_flat_wound, axis=1) + + (_, unique_idxs, unique_counts) = np.unique( + faces_flat_sorted, + axis=0, + return_index=True, + return_counts=True, + ) + + ext_face_idxs = unique_idxs[unique_counts == 1] + ext_parent_elem_idxs = np.ascontiguousarray( + ext_face_idxs // face_map.shape[0], + dtype=np.int64, + ) + ext_faces = np.copy(faces_flat_wound[ext_face_idxs]) + + for ff, parent_elem_idx in enumerate(ext_parent_elem_idxs): + ext_faces[ff, :] = _enforce_surf_face_outward( + ext_faces[ff, :], + connect[parent_elem_idx, :], + coords, + ) + + ext_faces[ff, :] = _enforce_surf_face_node_order( + ext_faces[ff, :], + coords, + ) + + ext_faces = np.ascontiguousarray(ext_faces, dtype=np.int64) + + return ext_faces, ext_parent_elem_idxs + + +def _restore_src_connect_style( + mesh_in: SimData, + one_based: bool, + transposed: bool, +) -> SimData: + """Restore the source indexing and table-orientation style.""" + if mesh_in.connect is None: + return mesh_in + + connect_out: dict[str, np.ndarray] = {} + for name, connect in mesh_in.connect.items(): + connect_fmt = np.copy(connect) + + if one_based: + connect_fmt = connect_fmt + 1 + + if transposed: + connect_fmt = connect_fmt.T + + connect_out[name] = np.ascontiguousarray(connect_fmt, dtype=np.int64) + + return _copy_sim_data(mesh_in, connect=connect_out) + + +def _conv_to_vec3( + values: np.ndarray | list[float] | tuple[float, ...], + name: str, +) -> np.ndarray: + """Return the first three finite components of an array-like value.""" + values_arr = np.asarray(values, dtype=np.float64).reshape(-1) + + if values_arr.size < 3: + raise ValueError(f"'{name}' must contain at least three components.") + + vec = np.ascontiguousarray(values_arr[:3]) + finite = np.isfinite(vec) + + if not np.all(finite): + raise ValueError(f"'{name}' must contain only finite values.") + + return vec + + +class MeshCheckCode(StrEnum): + """A single mesh-convention condition that a connectivity table failed.""" + + ROW_MAJOR_CONNECTIVITY = "row_major_connectivity" + ZERO_BASED_INDEXING = "zero_based_indexing" + CONNECTIVITY_INDICES = "connectivity_indices" + CCW_WINDING = "ccw_winding" + RIGHT_HANDED_GEOMETRY = "right_handed_geometry" + SURFACE_TOPOLOGY = "surface_topology" + NODE_ORDER = "node_order" + + +MeshConvCheck = dict[str, list[MeshCheckCode]] + + +class EMeshType(Enum): + """Topological class of a Riley simulation mesh.""" + + VOL = "volume" + SURF = "surface" + + +class EElementType(Enum): + """Supported finite-element topologies.""" + + TRI3 = "tri3" + TRI6 = "tri6" + TRI7 = "tri7" + QUAD4 = "quad4" + QUAD8 = "quad8" + QUAD9 = "quad9" + TET4 = "tet4" + TET10 = "tet10" + HEX8 = "hex8" + HEX20 = "hex20" + HEX27 = "hex27" + + def calc_ref_coords(self) -> np.ndarray: + """Return reference-node coordinates in Riley slot order. + + These coordinates are topology data, not geometry used to inspect a + user mesh. They let us derive every orientation-preserving + automorphism of an element once, instead of maintaining hand-written + permutations in several places. + """ + if self in (EElementType.TRI3, EElementType.TRI6, + EElementType.TRI7): + + points = ((0., 0.), (1., 0.), (0., 1.), (.5, 0.), (.5, .5), + (0., .5)) + + if self is EElementType.TRI3: + points = points[:3] + elif self is EElementType.TRI7: + points += ((1. / 3., 1. / 3.),) + + return np.asarray(points, dtype=np.float64) + + if self in (EElementType.QUAD4, EElementType.QUAD8, + EElementType.QUAD9): + + points = ((0., 0.), (1., 0.), (1., 1.), (0., 1.), + (.5, 0.), (1., .5), (.5, 1.), (0., .5)) + + if self is EElementType.QUAD4: + points = points[:4] + elif self is EElementType.QUAD9: + points += ((.5, .5),) + + return np.asarray(points, dtype=np.float64) + + if self in (EElementType.TET4, EElementType.TET10): + points = ((0., 0., 0.), (1., 0., 0.), (0., 1., 0.), (0., 0., 1.), + (.5, 0., 0.), (.5, .5, 0.), (0., .5, 0.), + (0., 0., .5), (.5, 0., .5), (0., .5, .5)) + + return np.asarray( + points[:4] if self is EElementType.TET4 else points, + dtype=np.float64, + ) + + if self in (EElementType.HEX8, EElementType.HEX20, + EElementType.HEX27): + points = ((0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.), + (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.), + (.5, 0., 0.), (1., .5, 0.), (.5, 1., 0.), (0., .5, 0.), + (.5, 0., 1.), (1., .5, 1.), (.5, 1., 1.), (0., .5, 1.), + (0., 0., .5), (1., 0., .5), (1., 1., .5), (0., 1., .5), + (.5, 0., .5), (1., .5, .5), (.5, 1., .5), (0., .5, .5), + (.5, .5, 0.), (.5, .5, 1.), (.5, .5, .5)) + + count = ELEMENT_SPECS[self].nodes_per_elem + + if self is EElementType.HEX20: + return np.asarray(points[:20], dtype=np.float64) + + return np.asarray(points[:count], dtype=np.float64) + + raise ValueError(f"No reference coordinates for {self.value}.") + + def calc_orient_preserving_perms( + self, + ) -> tuple[tuple[int, ...], ...]: + """Derive all proper topology symmetries in std-slot notation. + + A returned permutation maps a target Riley slot to a source Riley + slot. Applying one preserves all corner, edge, face-centre and + volume-centre roles. Surface reflections and volume inversions are + deliberately absent. + """ + spec = ELEMENT_SPECS[self] + ref = self.calc_ref_coords() + dims = 2 if spec.is_surf else 3 + corners = np.asarray(spec.corner_idxs, dtype=np.int64) + candidates: tuple[tuple[int, ...], ...] + + if len(corners) == 8: + # The proper rotational group of a cube: 3! axis orderings and sign + # changes with positive determinant, for 24 transformations. + candidate_rows: list[tuple[int, ...]] = [] + + for axes in perms(range(3)): + inversion_count = 0 + for idx in range(3): + for next_idx in range(idx + 1, 3): + inversion_count += axes[idx] > axes[next_idx] + + parity = 1 if inversion_count % 2 == 0 else -1 + + for signs in product((-1., 1.), repeat=3): + sign_product = int(np.prod(signs)) + if parity * sign_product < 0: + continue + transformed = ( + (2.0 * ref - 1.0)[:, axes] + * np.asarray(signs) + ) + + transformed = 0.5 * (transformed + 1.0) + candidate_rows.append( + _calc_ref_node_perm(ref, transformed) + ) + + candidates = tuple(candidate_rows) + + else: + rows: list[tuple[int, ...]] = [] + src_corners = ref[corners, :dims] + homogeneous = np.column_stack((src_corners, + np.ones(len(corners)))) + + for corner_perm in perms(range(len(corners))): + target_corners = src_corners[np.asarray(corner_perm)] + transform, _, _, _ = np.linalg.lstsq( + homogeneous, + target_corners, + rcond=None, + ) + + if np.linalg.det(transform[:dims]) <= 0.0: + continue + + transformed = np.column_stack( + (ref[:, :dims], np.ones(ref.shape[0])) + ) @ transform + + try: + rows.append(_calc_ref_node_perm( + ref[:, :dims], + transformed, + )) + except ValueError: + continue + + candidates = tuple(rows) + + return tuple(sorted(set(candidates))) + + +@dataclass(frozen=True, slots=True) +class MeshConvention: + """Caller-declared source ordering for otherwise ambiguous elements. + + Each permutation maps a Riley std slot to the corresponding slot in + the source connectivity row. Omitted element types are assumed to already + use Riley ordering. + """ + + src_to_riley_perms: Mapping[EElementType, tuple[int, ...]] + standardise_equiv_orients: bool = False + + def __post_init__(self) -> None: + """Validate permutations and retain an immutable defensive copy.""" + perms_out: dict[EElementType, tuple[int, ...]] = {} + for elem_type, perm_raw in ( + self.src_to_riley_perms.items() + ): + if not isinstance(elem_type, EElementType): + raise TypeError( + "MeshConvention keys must be EElementType members." + ) + perm = tuple(perm_raw) + node_count = elem_type.calc_ref_coords().shape[0] + if len(perm) != node_count: + raise ValueError( + f"{elem_type.value} requires a {node_count}-slot " + "permutation." + ) + slots_are_ints = True + for slot in perm: + if not isinstance(slot, Integral): + slots_are_ints = False + break + if not slots_are_ints: + raise TypeError("MeshConvention slots must be integers.") + perm = tuple(int(slot) for slot in perm) + perm_slots = set(perm) + expected_slots = set(range(node_count)) + if perm_slots != expected_slots: + raise ValueError( + f"{elem_type.value} perm must contain every " + f"slot from 0 to {node_count - 1} exactly once." + ) + perms_out[elem_type] = perm + + object.__setattr__( + self, + "src_to_riley_perms", + MappingProxyType(perms_out), + ) + + def get_src_perm( + self, + elem_type: EElementType, + ) -> tuple[int, ...] | None: + """Return the declared source permutation for an element type.""" + return self.src_to_riley_perms.get(elem_type) + + +class MeshConvErr(ValueError): + """Raised when source node roles cannot be inferred unambiguously.""" + + +@dataclass(frozen=True, slots=True) +class ElementSpec: + """Static node-ordering metadata for one supported element topology.""" + + nodes_per_elem: int + is_surf: bool + corner_idxs: tuple[int, ...] + surf_reverse_perm: tuple[int, ...] | None = None + handedness_reverse_perm: tuple[int, ...] | None = None + surf_faces: tuple[tuple[int, ...], ...] | None = None + centre_idx: int | None = None + edge_pairs: tuple[tuple[int, int], ...] = () + face_corner_idxs: tuple[tuple[int, ...], ...] = () + face_centre_idxs: tuple[int, ...] = () + cell_centre_idx: int | None = None + + +@dataclass(slots=True) +class SimData: + """Mesh data used by Riley's mesh-convention tools. + + Connectivity tables may initially use either indexing convention and either + table orientation; :func:`enforce_mesh_convention` standardises them. + The optional metadata fields allow external adapters to preserve associated + data without making Riley depend on their data-model types. + """ + + coords: np.ndarray | None = None + connect: dict[str, np.ndarray] | None = None + mesh_type: EMeshType | None = None + time: np.ndarray | None = None + side_sets: dict[tuple[str, str], np.ndarray] | None = None + node_vars: dict[str, np.ndarray] | None = None + elem_vars: dict[tuple[str, int], np.ndarray] | None = None + glob_vars: dict[str, np.ndarray] | None = None + + def update_mesh_type(self) -> None: + """Update the mesh topology from coordinates and connectivity.""" + if self.coords is None or self.connect is None: + self.mesh_type = None + return + if _check_vol_mesh(self): + self.mesh_type = EMeshType.VOL + else: + self.mesh_type = EMeshType.SURF + + +ELEMENT_SPECS = MappingProxyType({ + EElementType.TRI3: ElementSpec(3, True, (0, 1, 2), (0, 2, 1)), + EElementType.TRI6: ElementSpec(6, True, (0, 1, 2), (0, 2, 1, 5, 4, 3)), + EElementType.TRI7: ElementSpec( + 7, True, (0, 1, 2), (0, 2, 1, 5, 4, 3, 6), centre_idx=6, + ), + EElementType.QUAD4: ElementSpec(4, True, (0, 1, 2, 3), (0, 3, 2, 1)), + EElementType.QUAD8: ElementSpec( + 8, + True, + (0, 1, 2, 3), + (0, 3, 2, 1, 7, 6, 5, 4), + ), + EElementType.QUAD9: ElementSpec( + 9, True, (0, 1, 2, 3), (0, 3, 2, 1, 7, 6, 5, 4, 8), centre_idx=8, + ), + EElementType.TET4: ElementSpec( + 4, False, (0, 1, 2, 3), None, (0, 2, 1, 3), + ((0, 1, 2), (0, 3, 1), (0, 2, 3), (1, 3, 2)), + edge_pairs=((0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)), + ), + EElementType.TET10: ElementSpec( + 10, False, (0, 1, 2, 3), None, (0, 2, 1, 3, 6, 5, 4, 7, 9, 8), + ((0, 1, 2, 4, 5, 6), (0, 3, 1, 7, 8, 4), + (0, 2, 3, 6, 9, 7), (1, 3, 2, 8, 9, 5)), + edge_pairs=((0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)), + ), + EElementType.HEX8: ElementSpec( + 8, False, (0, 1, 2, 3, 4, 5, 6, 7), None, (0, 3, 2, 1, 4, 7, 6, 5), + ((0, 1, 2, 3), (0, 3, 7, 4), (4, 7, 6, 5), (1, 5, 6, 2), + (0, 4, 5, 1), (2, 6, 7, 3)), + edge_pairs=((0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), + (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)), + ), + EElementType.HEX20: ElementSpec( + 20, False, (0, 1, 2, 3, 4, 5, 6, 7), None, + (0, 3, 2, 1, 4, 7, 6, 5, 11, 10, 9, 8, 15, 14, 13, 12, 16, 19, 18, 17), + ((0, 1, 2, 3, 8, 9, 10, 11), (0, 3, 7, 4, 11, 15, 19, 16), + (4, 7, 6, 5, 15, 14, 13, 12), (1, 5, 6, 2, 17, 13, 18, 9), + (0, 4, 5, 1, 16, 12, 17, 8), (2, 6, 7, 3, 18, 14, 19, 10)), + edge_pairs=((0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), + (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)), + ), + EElementType.HEX27: ElementSpec( + 27, False, (0, 1, 2, 3, 4, 5, 6, 7), None, + (0, 3, 2, 1, 4, 7, 6, 5, 11, 10, 9, 8, 15, 14, 13, 12, 16, 19, 18, 17, + 20, 21, 22, 23, 24, 25, 26), + ((0, 1, 2, 3, 8, 9, 10, 11, 24), (0, 3, 7, 4, 11, 15, 19, 16, 23), + (4, 7, 6, 5, 15, 14, 13, 12, 25), (1, 5, 6, 2, 17, 13, 18, 9, 21), + (0, 4, 5, 1, 16, 12, 17, 8, 20), (2, 6, 7, 3, 18, 14, 19, 10, 22)), + edge_pairs=((0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), + (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)), + face_corner_idxs=((0, 1, 2, 3), (0, 3, 7, 4), (4, 7, 6, 5), + (1, 5, 6, 2), (0, 4, 5, 1), (2, 6, 7, 3)), + face_centre_idxs=(24, 23, 25, 21, 20, 22), + cell_centre_idx=26, + ), +}) + + +_elem_specs_by_node_count_out: dict[int, tuple[ElementSpec, ...]] = {} +_supported_node_counts_out: set[int] = set() +for _spec in ELEMENT_SPECS.values(): + _supported_node_counts_out.add(_spec.nodes_per_elem) +for _nodes_per_elem in _supported_node_counts_out: + _matching_specs: list[ElementSpec] = [] + for _spec in ELEMENT_SPECS.values(): + if _spec.nodes_per_elem == _nodes_per_elem: + _matching_specs.append(_spec) + _elem_specs_by_node_count_out[_nodes_per_elem] = tuple(_matching_specs) + +_ELEM_SPECS_BY_NODE_COUNT = MappingProxyType(_elem_specs_by_node_count_out) + + +_surf_node_counts_out: set[int] = set() +for _spec in ELEMENT_SPECS.values(): + if _spec.is_surf: + _surf_node_counts_out.add(_spec.nodes_per_elem) + +_SURF_NODE_COUNTS = frozenset(_surf_node_counts_out) + + +_vol_node_counts_out: set[int] = set() +for _spec in ELEMENT_SPECS.values(): + if not _spec.is_surf: + _vol_node_counts_out.add(_spec.nodes_per_elem) + +_VOL_NODE_COUNTS = frozenset(_vol_node_counts_out) +_SUPPORTED_NODE_COUNTS = _SURF_NODE_COUNTS | _VOL_NODE_COUNTS + + +_surf_only_node_counts_out: set[int] = set() +for _nodes_per_elem, _specs in _ELEM_SPECS_BY_NODE_COUNT.items(): + _all_surf = True + for _spec in _specs: + if not _spec.is_surf: + _all_surf = False + break + if _all_surf: + _surf_only_node_counts_out.add(_nodes_per_elem) + +_SURF_ONLY_NODE_COUNTS = frozenset(_surf_only_node_counts_out) + + +_vol_only_node_counts_out: set[int] = set() +for _nodes_per_elem, _specs in _ELEM_SPECS_BY_NODE_COUNT.items(): + _all_vol = True + for _spec in _specs: + if _spec.is_surf: + _all_vol = False + break + if _all_vol: + _vol_only_node_counts_out.add(_nodes_per_elem) + +_VOL_ONLY_NODE_COUNTS = frozenset(_vol_only_node_counts_out) + + +@cache +def _get_surf_spec(nodes_per_elem: int) -> ElementSpec: + """Return surface-element metadata for a node count.""" + for spec in _ELEM_SPECS_BY_NODE_COUNT.get(nodes_per_elem, ()): + if spec.is_surf and spec.nodes_per_elem == nodes_per_elem: + return spec + + raise NotImplementedError( + f"Surface metadata is not implemented for " + f"{nodes_per_elem}-node elements." + ) + + +@cache +def _get_vol_spec(nodes_per_elem: int) -> ElementSpec: + """Return volume-element metadata for a node count.""" + for spec in _ELEM_SPECS_BY_NODE_COUNT.get(nodes_per_elem, ()): + if not spec.is_surf and spec.nodes_per_elem == nodes_per_elem: + return spec + + raise NotImplementedError( + f"Volume metadata is not implemented for " + f"{nodes_per_elem}-node elements." + ) + + +@cache +def _get_elem_type_from_spec(spec: ElementSpec) -> EElementType: + """Return the registered element type for metadata.""" + for elem_type, registered_spec in ELEMENT_SPECS.items(): + if registered_spec is spec: + return elem_type + + raise ValueError("Element specification is not registered.") + + +@cache +def _get_corner_idxs(nodes_per_elem: int) -> np.ndarray: + """Return corner slots for a supported element node count.""" + _validate_nodes_per_elem(nodes_per_elem) + if nodes_per_elem in _SURF_NODE_COUNTS: + return np.asarray( + _get_surf_spec(nodes_per_elem).corner_idxs, + dtype=np.int64, + ) + + return np.asarray( + _get_vol_spec(nodes_per_elem).corner_idxs, + dtype=np.int64, + ) + + +@cache +def _get_vol_corner_idxs(nodes_per_elem: int) -> np.ndarray: + """Return corner slots for a supported volume element.""" + return np.asarray( + _get_vol_spec(nodes_per_elem).corner_idxs, + dtype=np.int64, + ) + + +@cache +def _get_surf_map(nodes_per_elem: int) -> np.ndarray: + """Return the local face-slot map for a volume element.""" + spec = _get_vol_spec(nodes_per_elem) + if spec.surf_faces is None: + raise NotImplementedError( + f"Surface extraction is not implemented for " + f"{spec.nodes_per_elem}-node elements." + ) + + return np.asarray(spec.surf_faces, dtype=np.int64) + + +@cache +def _get_elem_symmetries( + elem_type: EElementType, +) -> tuple[tuple[int, ...], ...]: + """Return lazily calculated proper symmetries for an element type.""" + return elem_type.calc_orient_preserving_perms() + + +@cache +def _get_elem_symmetry_arrs( + elem_type: EElementType, +) -> tuple[np.ndarray, ...]: + """Return cached array forms of an element's proper symmetries.""" + symmetry_arrs: list[np.ndarray] = [] + for perm in _get_elem_symmetries(elem_type): + symmetry_arrs.append(np.asarray(perm, dtype=np.int64)) + return tuple(symmetry_arrs) + + +def _check_or_enforce_connect_table( + connect_raw: np.ndarray, + name: str, + mesh_in: SimData, + shift_all: bool, + src_convention: MeshConvention | None, + enforce: bool, +) -> tuple[np.ndarray, list[MeshCheckCode]]: + """Check one table and optionally return its std representation.""" + if mesh_in.coords is None: + raise ValueError("Mesh convention processing requires coordinates.") + + connect = _enforce_connect_arr_format(connect_raw, name) + failures: list[MeshCheckCode] = [] + surf_only = _check_surf_mesh_type(mesh_in.mesh_type) + + if _check_transpose_needed(connect, name, mesh_in): + failures.append(MeshCheckCode.ROW_MAJOR_CONNECTIVITY) + connect = connect.T + + shift_needed = _check_table_needs_zero_based_shift( + connect, + mesh_in.coords.shape[0], + shift_all, + ) + if shift_needed: + failures.append(MeshCheckCode.ZERO_BASED_INDEXING) + connect = connect - 1 + + if not _check_idxs_zero_based(connect, mesh_in.coords.shape[0]): + failures.append(MeshCheckCode.CONNECTIVITY_INDICES) + return connect, failures + + ordered = _enforce_node_order_table( + connect, + mesh_in.coords, + surf_only=surf_only, + src_convention=src_convention, + ) + + if not np.array_equal(ordered, connect): + failures.append(MeshCheckCode.NODE_ORDER) + + connect = ordered + + table_is_surf = _check_surf_connect_table( + connect, + mesh_in.coords, + surf_only=surf_only, + ) + + if table_is_surf: + try: + flips = _calc_surf_orient_flips(connect, mesh_in.coords) + except ValueError: + failures.append(MeshCheckCode.SURFACE_TOPOLOGY) + if enforce: + connect = _enforce_surf_orient_table( + connect, + mesh_in.coords, + ) + else: + if np.any(flips): + failures.extend(( + MeshCheckCode.CCW_WINDING, + MeshCheckCode.RIGHT_HANDED_GEOMETRY, + )) + if enforce: + connect = _apply_surf_flips(connect, flips) + else: + ccw_winding = _check_ccw_winding_table( + connect, + mesh_in.coords, + surf_only=surf_only, + ) + + if not ccw_winding: + failures.append(MeshCheckCode.CCW_WINDING) + if enforce: + connect = _enforce_ccw_winding_table( + connect, + mesh_in.coords, + surf_only=surf_only, + ) + + right_handed = _check_right_handed_table( + connect, + mesh_in.coords, + surf_only=surf_only, + ) + + if not right_handed: + failures.append(MeshCheckCode.RIGHT_HANDED_GEOMETRY) + if enforce: + connect = _enforce_right_handed_table( + connect, + mesh_in.coords, + surf_only=surf_only, + ) + + return np.ascontiguousarray(connect, dtype=np.int64), failures + + +def check_mesh_convention( + mesh_in: SimData, + src_convention: MeshConvention | None = None, +) -> MeshConvCheck: + """Return failed checks for each non-conforming connectivity table.""" + + if mesh_in.connect is None: + return {} + + if mesh_in.coords is None: + raise ValueError( + "Mesh convention checks require 'coords' to be set.", + ) + + per_table: MeshConvCheck = {} + shift_all = _check_mesh_needs_zero_based_shift( + mesh_in, + mesh_in.coords.shape[0], + ) + + for name, connect_raw in mesh_in.connect.items(): + _, failures = _check_or_enforce_connect_table( + connect_raw, + name, + mesh_in, + shift_all, + src_convention, + enforce=False, + ) + if failures: + per_table[name] = failures + + return per_table + + +def enforce_mesh_convention( + mesh_in: SimData, + src_convention: MeshConvention | None = None, +) -> SimData: + """Return a mesh normalized to Riley's convention.""" + + if mesh_in.connect is None: + return mesh_in + + if mesh_in.coords is None: + raise ValueError("Mesh convention enforcement requires coordinates.") + + shift_all = _check_mesh_needs_zero_based_shift( + mesh_in, + mesh_in.coords.shape[0], + ) + + connect_out: dict[str, np.ndarray] = {} + changed = False + for name, connect_raw in mesh_in.connect.items(): + connect, failure_list = _check_or_enforce_connect_table( + connect_raw, + name, + mesh_in, + shift_all, + src_convention, + enforce=True, + ) + + failures = frozenset(failure_list) + if MeshCheckCode.CONNECTIVITY_INDICES in failures: + raise ValueError( + "Connectivity table " + f"'{name}' contains indices outside the coordinate array after " + "0-based normalization." + ) + + changed = changed or bool(failures) + connect_out[name] = connect + + if not changed: + return mesh_in + + return _copy_sim_data(mesh_in, connect=connect_out) + + +def _prepare_extraction_connect( + mesh_in: SimData, +) -> tuple[dict[str, np.ndarray], bool, bool]: + """Normalize connectivity and report the source table style.""" + if mesh_in.connect is None or mesh_in.coords is None: + raise ValueError("Surface extraction requires a complete mesh.") + + shift_all = _check_mesh_needs_zero_based_shift( + mesh_in, + mesh_in.coords.shape[0], + ) + connect_out: dict[str, np.ndarray] = {} + src_zero_based = True + src_row_major = True + for name, connect_raw in mesh_in.connect.items(): + connect, failures = _check_or_enforce_connect_table( + connect_raw, + name, + mesh_in, + shift_all, + src_convention=None, + enforce=True, + ) + + if MeshCheckCode.CONNECTIVITY_INDICES in failures: + raise ValueError( + f"Connectivity table '{name}' contains invalid indices " + "for surface extraction." + ) + + src_zero_based = src_zero_based and ( + MeshCheckCode.ZERO_BASED_INDEXING not in failures + ) + + src_row_major = src_row_major and ( + MeshCheckCode.ROW_MAJOR_CONNECTIVITY not in failures + ) + + connect_out[name] = connect + + return connect_out, src_zero_based, src_row_major + + +def extract_surf_mesh( + mesh_in: SimData, + enforce_convention: bool = True, +) -> SimData: + """Extracts the external surface mesh from supported 3D volume elements.""" + + if _check_mesh_2d(mesh_in): + raise ValueError( + "Surface extraction is only supported for 3D meshes. " + "The provided mesh appears to be 2D." + ) + + if mesh_in.connect is None: + raise ValueError("Surface extraction requires connectivity tables.") + + if mesh_in.coords is None: + raise ValueError("Surface extraction requires coordinates.") + + connect_norm, src_zero_based, src_row_major = ( + _prepare_extraction_connect(mesh_in) + ) + + surf_connect_glob: dict[str, np.ndarray] = {} + surf_elem_srcs: dict[str, np.ndarray] = {} + surf_node_blocks: list[np.ndarray] = [] + + for name, connect in connect_norm.items(): + (surf_faces, surf_parent_elem_idxs) = _extract_surf_faces_from_table( + connect, + mesh_in.coords, + ) + + surf_connect_glob[name] = surf_faces + surf_elem_srcs[name] = surf_parent_elem_idxs + + if surf_faces.size: + surf_node_blocks.append(surf_faces.reshape(-1)) + + if surf_node_blocks: + surf_node_idxs = np.unique(np.concatenate(surf_node_blocks)) + else: + surf_node_idxs = np.array([], dtype=np.int64) + + surf_coords = np.ascontiguousarray( + mesh_in.coords[surf_node_idxs], + dtype=mesh_in.coords.dtype, + ) + + coord_remap = np.full(mesh_in.coords.shape[0], -1, dtype=np.int64) + coord_remap[surf_node_idxs] = np.arange( + surf_node_idxs.shape[0], + dtype=np.int64, + ) + + surf_connect_loc: dict[str, np.ndarray] = {} + for name, surf_faces in surf_connect_glob.items(): + surf_connect_loc[name] = coord_remap[surf_faces] + + surf_mesh = _copy_sim_data(mesh_in) + surf_mesh.coords = surf_coords + surf_mesh.connect = surf_connect_loc + surf_mesh.side_sets = None + surf_mesh.update_mesh_type() + + if mesh_in.node_vars is not None: + surf_mesh.node_vars = {} + for name, values in mesh_in.node_vars.items(): + surf_mesh.node_vars[name] = values[surf_node_idxs, :] + + if mesh_in.elem_vars is not None: + surf_mesh.elem_vars = {} + for (name, block_id), values in mesh_in.elem_vars.items(): + connect_key = f"connect{block_id}" + if connect_key in surf_elem_srcs: + surf_mesh.elem_vars[(name, block_id)] = values[ + surf_elem_srcs[connect_key], : + ] + + if not enforce_convention: + surf_mesh = _restore_src_connect_style( + surf_mesh, + one_based=not src_zero_based, + transposed=not src_row_major, + ) + + return surf_mesh + + return surf_mesh + + +def extract_surf_between( + mesh_in: SimData, + point: np.ndarray | list[float] | tuple[float, ...], + normal: np.ndarray | list[float] | tuple[float, ...], + distance: float | None = None, + tolerance: float = 1.0e-6, + enforce_convention: bool = True, +) -> SimData: + """Extract a surface mesh between two parallel planes.""" + + if mesh_in.connect is None: + raise ValueError("Surface extraction requires connectivity tables.") + + if mesh_in.coords is None: + raise ValueError("Surface extraction requires coordinates.") + + point_arr = _conv_to_vec3(point, "point") + normal_arr = _conv_to_vec3(normal, "normal") + normal_magnitude = np.linalg.norm(normal_arr) + + if normal_magnitude < _TOL.geom: + raise ValueError("Normal vector cannot be zero.") + + normal_arr = normal_arr / normal_magnitude + + if not np.isfinite(tolerance) or tolerance < 0.0: + raise ValueError("'tolerance' must be a finite non-negative value.") + + if distance is not None and not np.isfinite(distance): + raise ValueError("'distance' must be finite when provided.") + + coord_offsets = mesh_in.coords - point_arr + projs = coord_offsets @ normal_arr + + # Determine bounds + if distance is not None: + plane_distance = float(distance) + min_bound = min(0.0, plane_distance) - tolerance + max_bound = max(0.0, plane_distance) + tolerance + else: + min_bound = -tolerance + max_bound = tolerance + + connect_norm, src_zero_based, src_row_major = ( + _prepare_extraction_connect(mesh_in) + ) + + surf_connect_glob: dict[str, np.ndarray] = {} + surf_elem_srcs: dict[str, np.ndarray] = {} + surf_node_blocks: list[np.ndarray] = [] + + for name, connect in connect_norm.items(): + nodes_per_elem = connect.shape[1] + is_vol = _check_vol_connect_table(connect, mesh_in.coords) + if is_vol: + face_map = _get_surf_map(nodes_per_elem) + faces_per_elem = face_map.shape[0] + faces_wound = connect[:, face_map] + faces_flat_wound = faces_wound.reshape((-1, face_map.shape[1])) + faces_flat_sorted = np.sort(faces_flat_wound, axis=1) + + _, unique_idxs = np.unique( + faces_flat_sorted, + axis=0, + return_index=True, + ) + candidate_faces = faces_flat_wound[unique_idxs] + parent_idxs = unique_idxs // faces_per_elem + + else: + candidate_faces = connect + parent_idxs = np.arange(connect.shape[0], dtype=np.int64) + + if candidate_faces.size == 0: + continue + + face_projs = projs[candidate_faces] + in_bounds = np.all( + (face_projs >= min_bound) + & (face_projs <= max_bound), + axis=1 + ) + + filtered_faces = candidate_faces[in_bounds] + filtered_parents = parent_idxs[in_bounds] + + if filtered_faces.size > 0: + surf_connect_glob[name] = filtered_faces + surf_elem_srcs[name] = filtered_parents + surf_node_blocks.append(filtered_faces.reshape(-1)) + + if surf_node_blocks: + surf_node_idxs = np.unique(np.concatenate(surf_node_blocks)) + else: + surf_node_idxs = np.array([], dtype=np.int64) + + if len(surf_connect_glob) == 0 or surf_node_idxs.size == 0: + raise ValueError( + "No elements/faces found between the specified planes." + ) + + surf_coords = np.ascontiguousarray( + mesh_in.coords[surf_node_idxs], + dtype=mesh_in.coords.dtype + ) + coord_remap = np.full(mesh_in.coords.shape[0], -1, dtype=np.int64) + coord_remap[surf_node_idxs] = np.arange( + surf_node_idxs.shape[0], dtype=np.int64 + ) + + surf_connect_loc: dict[str, np.ndarray] = {} + for name, surf_faces in surf_connect_glob.items(): + surf_connect_loc[name] = coord_remap[surf_faces] + + surf_mesh = _copy_sim_data(mesh_in) + surf_mesh.coords = surf_coords + surf_mesh.connect = surf_connect_loc + surf_mesh.side_sets = None + surf_mesh.update_mesh_type() + + if mesh_in.node_vars is not None: + surf_mesh.node_vars = {} + for name, values in mesh_in.node_vars.items(): + surf_mesh.node_vars[name] = values[surf_node_idxs, :] + + if mesh_in.elem_vars is not None: + surf_mesh.elem_vars = {} + for (var_name, block_id), values in mesh_in.elem_vars.items(): + connect_key = f"connect{block_id}" + if connect_key in surf_elem_srcs: + surf_mesh.elem_vars[(var_name, block_id)] = values[ + surf_elem_srcs[connect_key], : + ] + + if not enforce_convention: + surf_mesh = _restore_src_connect_style( + surf_mesh, + one_based=not src_zero_based, + transposed=not src_row_major, + ) + return surf_mesh + + return enforce_mesh_convention(surf_mesh) diff --git a/src/riley/python/_verifio.py b/src/riley/python/_verifio.py new file mode 100644 index 00000000..1ebdd2cd --- /dev/null +++ b/src/riley/python/_verifio.py @@ -0,0 +1,73 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +"""Shared array verification for Riley's Python IO and geometry tools.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + + +def _validate_finite_f64( + values: object, + name: str, + shape: tuple[int, ...] | None = None, + *, + contiguous: bool = False, +) -> np.ndarray: + """Convert values to a finite float64 array of an optional shape.""" + if contiguous: + values_out = np.ascontiguousarray(values, dtype=np.float64) + else: + values_out = np.asarray(values, dtype=np.float64) + + if shape is not None and values_out.shape != shape: + raise ValueError(f"{name} must have shape {shape}.") + + finite = np.isfinite(values_out) + if not np.all(finite): + raise ValueError(f"{name} must not contain non-finite values.") + + return values_out + + +def _validate_coords( + coords: np.ndarray, + name: str = "coords", + *, + contiguous_f64: bool = False, +) -> np.ndarray: + """Return a finite, non-empty ``(nodes, 3)`` coordinate array.""" + if contiguous_f64: + coords_out = np.ascontiguousarray(coords, dtype=np.float64) + else: + coords_out = np.asarray(coords) + + coords_are_2d = coords_out.ndim == 2 + coords_have_nodes = coords_are_2d and coords_out.shape[0] > 0 + coords_have_xyz = coords_are_2d and coords_out.shape[1] == 3 + if not coords_have_nodes or not coords_have_xyz: + raise ValueError( + f"{name} must have shape (nodes, 3) and not be empty.", + ) + + finite = np.isfinite(coords_out) + if not np.all(finite): + raise ValueError(f"{name} must not contain non-finite values.") + + return coords_out + + +def _validate_vec3( + values: Sequence[float] | np.ndarray, + name: str, +) -> np.ndarray: + """Return a finite float64 vector with three components.""" + return _validate_finite_f64(values, name, (3,)) diff --git a/src/riley/python/enums.py b/src/riley/python/enums.py deleted file mode 100644 index 5f4c570d..00000000 --- a/src/riley/python/enums.py +++ /dev/null @@ -1,54 +0,0 @@ -# -------------------------------------------------------------------------- -# Riley: A High Performance Rasteriser for DIC UQ -# -# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) -# Licensed under the MIT License (see LICENSE file for details) -# -# Authors: scepticalrabbit (Lloyd Fletcher) -# -------------------------------------------------------------------------- -from __future__ import annotations - -from enum import Enum - - -class CoordCsvOrientation(Enum): - node_major = "node_major" - coord_major = "coord_major" - - -class ConnectCsvOrientation(Enum): - elem_major = "elem_major" - node_major = "node_major" - - -class FieldCsvOrientation(Enum): - frame_major = "frame_major" - node_major = "node_major" - - -class ConnectIndexing(Enum): - auto = "auto" - zero_based = "zero_based" - one_based = "one_based" - - -class ProjectionPlane(Enum): - xy = "xy" - yz = "yz" - xz = "xz" - - -class PlanarProjectionMode(Enum): - best = "best" - fit_x = "fit_x" - fit_y = "fit_y" - - -__all__ = [ - "ConnectCsvOrientation", - "ConnectIndexing", - "CoordCsvOrientation", - "FieldCsvOrientation", - "PlanarProjectionMode", - "ProjectionPlane", -] diff --git a/src/riley/python/helpers.py b/src/riley/python/helpers.py index 84740a71..dc3ec38f 100644 --- a/src/riley/python/helpers.py +++ b/src/riley/python/helpers.py @@ -6,28 +6,92 @@ # # Authors: scepticalrabbit (Lloyd Fletcher) # -------------------------------------------------------------------------- -import numpy as np +from __future__ import annotations + +from numbers import Integral from pathlib import Path +from typing import TYPE_CHECKING + +import numpy as np from PIL import Image +if TYPE_CHECKING: + from riley.cython.riley import RasterConfig, SaveStrategy + -def load_texture(texture_path: str | Path) -> np.ndarray: +def load_texture_u8(texture_path: str | Path) -> np.ndarray: + """Load an image as a contiguous eight-bit greyscale texture.""" with Image.open(Path(texture_path)) as image_in: image_grey = image_in.convert("L") image_u8 = np.asarray(image_grey, dtype=np.uint8) + return np.ascontiguousarray(image_u8, dtype=np.uint8) +def load_texture_u16(texture_path: str | Path) -> np.ndarray: + """Load an image as a contiguous sixteen-bit greyscale texture.""" + with Image.open(Path(texture_path)) as image_in: + if image_in.mode.startswith("I;16") or image_in.mode == "I": + image_values = np.asarray(image_in) + values_in_range = np.logical_and( + image_values >= 0, + image_values <= np.iinfo(np.uint16).max, + ) + if not np.all(values_in_range): + raise ValueError( + "Sixteen-bit texture values must lie in [0, 65535].", + ) + image_u16 = image_values.astype(np.uint16, copy=False) + else: + image_grey = image_in.convert("L") + image_u8 = np.asarray(image_grey, dtype=np.uint8) + u8_to_u16_scale = np.iinfo(np.uint16).max // np.iinfo(np.uint8).max + image_u16 = image_u8.astype(np.uint16) * u8_to_u16_scale + + return np.ascontiguousarray(image_u16, dtype=np.uint16) + + def create_raster_config( num_frames: int, total_threads: int = 1, - save_strategy: int = 2, # both -) -> "RasterConfig": - from riley.cyth.riley import RasterConfig + save_strategy: SaveStrategy | int = 2, +) -> RasterConfig: + """Create an offline raster configuration balanced over frames.""" + from riley.cython.riley import ( + GeometrySchedulingMode, + HullMode, + ImageFormat, + ImageSaveMode, + NewtonSeedMode, + NewtonSeedReuse, + RasterConfig, + RenderMode, + ReportMode, + SaveStrategy, + ScaleStrategy, + ) + + if not isinstance(num_frames, Integral) or isinstance(num_frames, bool): + raise TypeError("num_frames must be an integer.") + if ( + not isinstance(total_threads, Integral) + or isinstance(total_threads, bool) + ): + raise TypeError("total_threads must be an integer.") + + if num_frames <= 0: + raise ValueError("num_frames must be positive.") + + if total_threads <= 0: + raise ValueError("total_threads must be positive.") + + if not isinstance(save_strategy, (int, SaveStrategy)): + raise TypeError("save_strategy must be a SaveStrategy value.") + + frames_available = int(num_frames) + total_threads = int(total_threads) - total_threads = max(1, int(total_threads)) - frames_available = max(1, int(num_frames)) if total_threads < frames_available: render_group_count = total_threads else: @@ -35,20 +99,21 @@ def create_raster_config( for group_count in range(1, frames_available + 1): if total_threads % group_count == 0: render_group_count = group_count + workers_per_group = total_threads // render_group_count return RasterConfig( - render_mode=1, # offline + render_mode=RenderMode.offline, total_threads=total_threads, - geom_scheduling_mode=0, # spread + geom_scheduling_mode=GeometrySchedulingMode.spread, max_raster_workers_per_job=workers_per_group, - save_strategy=save_strategy, - image_save_mode=0, # grey - hull_mode=1, # on_no_fallback - newton_seed_mode=0, # centroid - newton_seed_reuse=0, # off - report=1, # bench - save_format=3, # bmp + save_strategy=SaveStrategy(save_strategy), + image_save_mode=ImageSaveMode.grey, + hull_mode=HullMode.on_no_fallback, + newton_seed_mode=NewtonSeedMode.centroid, + newton_seed_reuse=NewtonSeedReuse.off, + report=ReportMode.bench, + save_format=ImageFormat.bmp, save_bits=8, - save_scaling=1, # auto + save_scaling=ScaleStrategy.auto, ) diff --git a/src/riley/python/meshconv.py b/src/riley/python/meshconv.py new file mode 100644 index 00000000..8f8cda58 --- /dev/null +++ b/src/riley/python/meshconv.py @@ -0,0 +1,328 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +"""Riley's public mesh-convention interface. + +:func:`check_mesh_convention` reports the failed convention checks for a +mesh, :func:`enforce_mesh_convention` normalises a mesh to Riley's +convention, :func:`infer_mesh_convention` diagnoses source ordering, and +:func:`extract_surf_mesh`/:func:`extract_surf_between` extract surface +meshes. All implementation details live in the private +:mod:`riley.python._meshconv` module. +""" + +from __future__ import annotations + +import numpy as np + +from riley.python import _meshconv +from riley.python._meshconv import ( + MeshCheckCode, + EElementType, + EMeshType, + MeshConvention, + MeshConvErr, + MeshConvCheck, + SimData, +) + + +def check_mesh_convention( + mesh_in: SimData, + src_convention: MeshConvention | None = None, +) -> MeshConvCheck: + """Check a mesh for conformance to Riley's mesh convention. + + This function inspects all connectivity tables in the mesh and reports + which convention checks fail. An empty dictionary means the mesh fully + conforms to Riley's convention. + + Parameters + ---------- + mesh_in : SimData + The mesh to check. Must have ``coords`` (N x 3 array) and ``connect`` + (dict of connectivity tables). Each connectivity table must be + 2D with one element per row and one local node per column. + src_convention : MeshConvention | None, optional + Source ordering for specific element types. Each permutation satisfies + ``riley_row[target_slot] = source_row[permutation[target_slot]]``. + Omitted element types are assumed to use Riley ordering. If None, all + connectivity is assumed to use Riley ordering. + + Returns + ------- + MeshConvCheck + Dictionary mapping connectivity table names to lists of failed + ``MeshCheckCode`` conditions. Empty dict means the mesh conforms. + Possible failure codes: + + - ``ROW_MAJOR_CONNECTIVITY``: table is column-major, needs transpose + - ``ZERO_BASED_INDEXING``: indices are 1-based, need 0-based shift + - ``CONNECTIVITY_INDICES``: indices outside valid range [0, N-1] + - ``CCW_WINDING``: surface elements not wound counter-clockwise + - ``RIGHT_HANDED_GEOMETRY``: volume elements not right-handed + - ``SURFACE_TOPOLOGY``: non-manifold or unorientable surface + - ``NODE_ORDER``: higher-order nodes in wrong slots for element type + + Examples + -------- + >>> from riley.python import meshconv + >>> import numpy as np + >>> coords = np.array( + ... [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float64 + ... ) + >>> mesh = meshconv.SimData( + ... coords=coords, connect={"connect1": np.array([[0, 1, 2, 3]])} + ... ) + >>> report = meshconv.check_mesh_convention(mesh) + >>> report == {} + True + """ + return _meshconv.check_mesh_convention(mesh_in, src_convention) + + +def enforce_mesh_convention( + mesh_in: SimData, + src_convention: MeshConvention | None = None, +) -> SimData: + """Normalise a mesh to Riley's mesh convention. + + Applies all necessary corrections to bring a mesh into conformance with + Riley's convention. Only fixes the conditions reported by + :func:`check_mesh_convention`, and applies them in a fixed order + so the result is deterministic. + + Parameters + ---------- + mesh_in : SimData + The mesh to normalise. Must have ``coords`` and ``connect`` set. + src_convention : MeshConvention | None, optional + Source ordering for specific element types. Each permutation satisfies + ``riley_row[target_slot] = source_row[permutation[target_slot]]``. + Omitted element types are assumed to use Riley ordering. If None, all + connectivity is assumed to use Riley ordering. + + Returns + ------- + SimData + A new ``SimData`` instance with normalised connectivity tables, or the + original mesh instance if it already conformed (idempotent). + + Notes + ----- + The normalisation enforces: + + - **Row-major connectivity**: each element is one row, + local nodes are columns + - **Zero-based indexing**: all indices in range [0, N-1] where N = num nodes + - **Node order**: higher-order nodes (mid-edge, mid-face, centre) placed in + Riley's standard slots per element type + - **Surface winding**: CCW when viewed from material-facing side (outward + for closed shells, inward for cavity boundaries) + - **Volume handedness**: right-handed coordinate system for TET/HEX elements + - **Index validity**: all indices reference existing coordinate rows + + If ``CONNECTIVITY_INDICES`` would be violated (index out of bounds after + normalization), a ``ValueError`` is raised instead of silently corrupting + the mesh. + + Examples + -------- + >>> from riley.python import meshconv + >>> import numpy as np + >>> coords = np.array( + ... [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float64 + ... ) + >>> # 1-based indexing, needs conversion + >>> mesh = meshconv.SimData( + ... coords=coords, connect={"connect1": np.array([[1, 2, 3, 4]])} + ... ) + >>> mesh_out = meshconv.enforce_mesh_convention(mesh) + >>> mesh_out.connect["connect1"] + array([[0, 1, 2, 3]]) + >>> meshconv.check_mesh_convention(mesh_out) == {} + True + """ + return _meshconv.enforce_mesh_convention(mesh_in, src_convention) + + +def infer_mesh_convention(mesh_in: SimData) -> MeshConvention: + """Infer a source convention from affine element geometry. + + Inference is intentionally conservative and rejects ambiguous or mixed + source layouts. Prefer an explicitly declared :class:`MeshConvention` + whenever the source format is known. + + Parameters + ---------- + mesh_in : SimData + Mesh whose source connectivity order should be inferred. + + Returns + ------- + MeshConvention + Inferred source-slot mapping for each element family in the mesh. + + Raises + ------ + MeshConvErr + If the source node roles cannot be inferred unambiguously. + """ + return _meshconv.infer_mesh_convention(mesh_in) + + +def extract_surf_mesh( + mesh_in: SimData, + enforce_convention: bool = True, +) -> SimData: + """Extract the external surface mesh from a 3D volume mesh. + + For volume element types (TET4, TET10, HEX8, HEX20, HEX27), this function + identifies boundary faces (those belonging to only one element) and returns + them as a surface mesh with proper outward-facing normals. + + Parameters + ---------- + mesh_in : SimData + The input 3D volume mesh. Must have ``coords`` and ``connect`` with + volume element types. + enforce_convention : bool, optional + If True (default), the output surface mesh is normalised to Riley's + convention (0-based, CCW winding, proper node order). If False, the + output retains the input mesh's indexing style (1-based vs 0-based, + row-major vs column-major). + + Returns + ------- + SimData + A surface mesh containing only the boundary faces. The returned mesh + has: + + - ``coords``: subset of input coordinates used by boundary faces + - ``connect``: boundary face connectivity (QUAD4/8/9 or TRI3/6/7) + - ``mesh_type``: ``EMeshType.SURF`` + - ``node_vars``: interpolated nodal variables for surface nodes + - ``elem_vars``: element variables for boundary faces + + Raises + ------ + ValueError + If the input mesh is 2D (surface elements only) or has no connectivity. + + Examples + -------- + >>> from riley.python import meshconv + >>> import numpy as np + >>> coords = np.array(( + ... (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), + ... (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), + ... (0.0, 0.0, 1.0), (1.0, 0.0, 1.0), + ... (1.0, 1.0, 1.0), (0.0, 1.0, 1.0), + ... )) + >>> connect = np.arange(8, dtype=np.int64).reshape(1, 8) + >>> mesh = meshconv.SimData(coords=coords, connect={"connect1": connect}) + >>> surf = meshconv.extract_surf_mesh(mesh) + >>> surf.mesh_type is meshconv.EMeshType.SURF + True + >>> list(surf.connect.values())[0].shape # (faces, nodes per face) + (6, 4) + """ + return _meshconv.extract_surf_mesh(mesh_in, enforce_convention) + + +def extract_surf_between( + mesh_in: SimData, + point: np.ndarray | list[float] | tuple[float, ...], + normal: np.ndarray | list[float] | tuple[float, ...], + distance: float | None = None, + tolerance: float = 1.0e-6, + enforce_convention: bool = True, +) -> SimData: + """Extract a surface mesh from a slice between two parallel planes. + + For volume meshes, extracts internal faces between elements where nodes + lie between the two planes. For surface meshes, extracts faces whose + nodes lie between the planes. The output is a surface mesh with proper + outward-facing normals. + + Parameters + ---------- + mesh_in : SimData + The input simulation data/mesh. Can be volume or surface elements. + point : np.ndarray | list[float] | tuple[float, ...] + A point on the first plane. Only the first 3 components are used. + normal : np.ndarray | list[float] | tuple[float, ...] + The normal vector defining the plane orientation. Must be non-zero. + Only the first 3 components are used. + distance : float | None, optional + Distance along the normal to the second plane. If None (default), + extracts a thin slice at the first plane with ``+/- tolerance`` + thickness. If provided, extracts the slab between the two planes. + tolerance : float, optional + Numerical tolerance for checking if nodes lie between the planes. + Defaults to 1.0e-6. Nodes with projected distance in + ``[min_bound - tol, max_bound + tol]`` are included. + enforce_convention : bool, optional + If True (default), normalises the output mesh to Riley's convention. + If False, retains the input mesh's indexing style. + + Returns + ------- + SimData + The extracted surface mesh containing faces/elements between the + planes. The mesh has ``mesh_type = EMeshType.SURF`` and conforming + connectivity (if ``enforce_convention=True``). + + Raises + ------ + ValueError + If no elements/faces are found between the planes, if normal is zero, + or if mesh lacks required coordinates/connectivity. + + Examples + -------- + >>> from riley.python import meshconv + >>> import numpy as np + >>> coords = np.array(( + ... (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), + ... (1.0, 1.0, 0.0), (0.0, 1.0, 0.0), + ... (0.0, 0.0, 1.0), (1.0, 0.0, 1.0), + ... (1.0, 1.0, 1.0), (0.0, 1.0, 1.0), + ... )) + >>> connect = np.arange(8, dtype=np.int64).reshape(1, 8) + >>> mesh = meshconv.SimData(coords=coords, connect={"connect1": connect}) + >>> slab = meshconv.extract_surf_between( + ... mesh, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0) + ... ) + >>> slab.connect["connect1"].shape + (1, 4) + """ + return _meshconv.extract_surf_between( + mesh_in, + point, + normal, + distance=distance, + tolerance=tolerance, + enforce_convention=enforce_convention, + ) + + +__all__ = [ + "MeshCheckCode", + "MeshConvCheck", + "EMeshType", + "EElementType", + "MeshConvention", + "MeshConvErr", + "SimData", + "check_mesh_convention", + "enforce_mesh_convention", + "extract_surf_mesh", + "infer_mesh_convention", + "extract_surf_between", +] diff --git a/src/riley/python/meshio.py b/src/riley/python/meshio.py index 56f87041..d4b91597 100644 --- a/src/riley/python/meshio.py +++ b/src/riley/python/meshio.py @@ -6,54 +6,98 @@ # # Authors: scepticalrabbit (Lloyd Fletcher) # -------------------------------------------------------------------------- +"""Load Riley mesh and field arrays from CSV files.""" + from __future__ import annotations +from dataclasses import dataclass +from enum import Enum from pathlib import Path -from typing import Mapping +from typing import Iterator, Mapping import numpy as np -from riley.python.enums import ( - ConnectCsvOrientation, - ConnectIndexing, - CoordCsvOrientation, - FieldCsvOrientation, -) +from riley.python._verifio import _validate_finite_f64 + + +class ECsvOrient(Enum): + """Supported row semantics for simulation CSV tables.""" + NODE_MAJOR = "node_major" + COORD_MAJOR = "coord_major" + ELEM_MAJOR = "elem_major" + FRAME_MAJOR = "frame_major" + + +class EConnectIndexing(Enum): + """Connectivity index convention.""" + AUTO = "auto" + ZERO_BASED = "zero_based" + ONE_BASED = "one_based" + + +@dataclass(frozen=True, slots=True) +class SimCsvData: + """Simulation arrays loaded from a directory of CSV files. + + The object remains iterable for compatibility with tuple unpacking. + """ + coords: np.ndarray + connect: np.ndarray + uvs: np.ndarray | None + disp: np.ndarray | None + + def __iter__(self) -> Iterator[np.ndarray | None]: + """Iterate over arrays in the legacy return order.""" + yield self.coords + yield self.connect + yield self.uvs + yield self.disp def _load_csv_matrix(path: str | Path, skip_rows: int) -> np.ndarray: + """Load a finite, two-dimensional floating-point CSV table.""" + if skip_rows < 0: + raise ValueError("skip_rows must be non-negative.") + matrix = np.loadtxt( - Path(path), - delimiter=",", - dtype=np.float64, - ndmin=2, + Path(path), delimiter=",", dtype=np.float64, ndmin=2, skiprows=skip_rows, ) - return np.asarray(matrix, dtype=np.float64) - -def _ensure_contiguous_f64(array_in: np.ndarray) -> np.ndarray: - return np.ascontiguousarray(array_in, dtype=np.float64) + return _validate_finite_f64(matrix, f"CSV table '{path}'") -def _infer_one_based(connect: np.ndarray) -> bool: - if connect.size == 0: - return False - if np.any(connect == 0): +def _infer_one_based(connect: np.ndarray, node_count: int | None) -> bool: + """Infer one-based indexing when the evidence is conclusive.""" + if connect.size == 0 or np.any(connect == 0): return False - return bool(np.min(connect) >= 1) + + if node_count is not None: + return bool(np.max(connect) == node_count) + + raise ValueError( + "AUTO connectivity indexing is ambiguous without a node count; " + "select ZERO_BASED or ONE_BASED explicitly.", + ) def _normalise_point_table( matrix: np.ndarray, - orientation: CoordCsvOrientation, + orient: ECsvOrient, output_dims: int, ) -> np.ndarray: - points = matrix - if orientation == CoordCsvOrientation.coord_major: - points = points.T - if points.ndim != 2: - raise ValueError(f"Expected a 2D point table, got shape {points.shape}.") + """Orient and zero-pad a point table to the requested dimensions.""" + + if orient is ECsvOrient.COORD_MAJOR: + points = matrix.T + elif orient is ECsvOrient.NODE_MAJOR: + points = matrix + else: + raise ValueError(f"Unsupported point CSV orientation: {orient}.") + + if points.shape[0] == 0 or points.shape[1] == 0: + raise ValueError("Point table must not be empty.") + if points.shape[1] > output_dims: raise ValueError( f"Point table has {points.shape[1]} columns, expected at most " @@ -61,68 +105,96 @@ def _normalise_point_table( ) points_out = np.zeros((points.shape[0], output_dims), dtype=np.float64) - points_out[:, :points.shape[1]] = points - return _ensure_contiguous_f64(points_out) + points_out[:, : points.shape[1]] = points + + return points_out def load_coord_csv( path: str | Path, - *, skip_rows: int = 0, - orientation: CoordCsvOrientation = CoordCsvOrientation.node_major, + orient: ECsvOrient = ECsvOrient.NODE_MAJOR, ) -> np.ndarray: - coords_raw = _load_csv_matrix(path, skip_rows) - return _normalise_point_table(coords_raw, orientation, 3) + """Load coordinates as a contiguous ``(nodes, 3)`` array.""" + return _normalise_point_table( + _load_csv_matrix(path, skip_rows), orient, 3, + ) def load_connect_csv( path: str | Path, - *, skip_rows: int = 0, - orientation: ConnectCsvOrientation = ConnectCsvOrientation.elem_major, - indexing: ConnectIndexing = ConnectIndexing.auto, + orient: ECsvOrient = ECsvOrient.ELEM_MAJOR, + indexing: EConnectIndexing = EConnectIndexing.AUTO, + node_count: int | None = None, ) -> np.ndarray: + """Load connectivity as a contiguous platform-index array. + + ``AUTO`` requires ``node_count`` unless the table contains zero, because a + positive-only table is otherwise ambiguous. + """ + connect_raw = _load_csv_matrix(path, skip_rows) - if orientation == ConnectCsvOrientation.node_major: + if orient is ECsvOrient.NODE_MAJOR: connect_raw = connect_raw.T + elif orient is not ECsvOrient.ELEM_MAJOR: + raise ValueError(f"Unsupported connectivity CSV orientation: {orient}.") - connect = np.rint(connect_raw).astype(np.int64, copy=False) - if indexing == ConnectIndexing.one_based: - connect = connect - 1 - elif indexing == ConnectIndexing.auto and _infer_one_based(connect): + rounded = np.rint(connect_raw) + if not np.all(connect_raw == rounded): + raise ValueError("Connectivity must contain integer node indices.") + + connect = connect_raw.astype(np.int64, copy=False) + + if indexing is EConnectIndexing.ONE_BASED: connect = connect - 1 + elif indexing is EConnectIndexing.AUTO: + if _infer_one_based(connect, node_count): + connect = connect - 1 + elif indexing is not EConnectIndexing.ZERO_BASED: + raise ValueError(f"Unsupported connectivity indexing: {indexing}.") if np.any(connect < 0): raise ValueError("Connectivity contains negative node indices.") + node_count_invalid = node_count is not None and node_count <= 0 + + if node_count is not None: + node_count_invalid |= np.any(connect >= node_count) + + if node_count_invalid: + raise ValueError("Connectivity contains an out-of-range node index.") + return np.ascontiguousarray(connect, dtype=np.uintp) def load_field_csv( path: str | Path, - *, skip_rows: int = 0, - orientation: FieldCsvOrientation = FieldCsvOrientation.node_major, + orient: ECsvOrient = ECsvOrient.NODE_MAJOR, ) -> np.ndarray: + """Load a scalar field as a ``(frames, nodes)`` array.""" + field_raw = _load_csv_matrix(path, skip_rows) - if orientation == FieldCsvOrientation.node_major: + if orient is ECsvOrient.NODE_MAJOR: field_raw = field_raw.T - return _ensure_contiguous_f64(field_raw) + elif orient is not ECsvOrient.FRAME_MAJOR: + raise ValueError(f"Unsupported field CSV orientation: {orient}.") + + return np.ascontiguousarray(field_raw, dtype=np.float64) def load_field_csvs( field_paths: Mapping[str, str | Path], - *, skip_rows: int = 0, - orientation: FieldCsvOrientation = FieldCsvOrientation.node_major, + orient: ECsvOrient = ECsvOrient.NODE_MAJOR, ) -> dict[str, np.ndarray]: + """Load several named scalar fields.""" + fields_out: dict[str, np.ndarray] = {} - for field_name, field_path in field_paths.items(): - fields_out[field_name] = load_field_csv( - field_path, - skip_rows=skip_rows, - orientation=orientation, - ) + for name, path in field_paths.items(): + fields_out[name] = load_field_csv(path, skip_rows, orient) + return fields_out @@ -130,42 +202,46 @@ def load_disp_csvs( path_x: str | Path | None, path_y: str | Path | None, path_z: str | Path | None, - *, skip_rows: int = 0, - orientation: FieldCsvOrientation = FieldCsvOrientation.node_major, + orient: ECsvOrient = ECsvOrient.NODE_MAJOR, ) -> np.ndarray | None: - disp_paths = { - axis_name: axis_path - for axis_name, axis_path in ( - ("x", path_x), - ("y", path_y), - ("z", path_z), - ) - if axis_path is not None and Path(axis_path).is_file() - } + """Load displacement components as ``(frames, nodes, 3)``.""" + + paths_in = {"x": path_x, "y": path_y, "z": path_z} + for axis_name, path in paths_in.items(): + path_exists = True + + if path is not None: + path_exists = Path(path).is_file() + + if not path_exists: + raise FileNotFoundError( + f"Displacement {axis_name}-component CSV not found: {path}", + ) + + disp_paths: dict[str, str | Path] = {} + for name, path in paths_in.items(): + if path is not None: + disp_paths[name] = path + if not disp_paths: return None - disp_fields = load_field_csvs( - disp_paths, - skip_rows=skip_rows, - orientation=orientation, - ) - disp_shape = next(iter(disp_fields.values())).shape - disp = np.zeros((disp_shape[0], disp_shape[1], 3), dtype=np.float64) + fields = load_field_csvs(disp_paths, skip_rows, orient) + shape = next(iter(fields.values())).shape + disp = np.zeros((*shape, 3), dtype=np.float64) + axis_indices = {"x": 0, "y": 1, "z": 2} - axis_inds = {"x": 0, "y": 1, "z": 2} - for axis_name, values in disp_fields.items(): - if values.shape != disp_shape: + for name, values in fields.items(): + if values.shape != shape: raise ValueError("All displacement CSVs must have the same shape.") - disp[:, :, axis_inds[axis_name]] = values + disp[:, :, axis_indices[name]] = values - return _ensure_contiguous_f64(disp) + return disp def load_sim_csvs( data_dir: str | Path, - *, coords_name: str = "coords.csv", connect_name: str = "connect.csv", uvs_name: str = "uvs.csv", @@ -173,48 +249,57 @@ def load_sim_csvs( disp_y_name: str = "field_disp_y.csv", disp_z_name: str = "field_disp_z.csv", skip_rows: int = 0, - coord_orientation: CoordCsvOrientation = CoordCsvOrientation.node_major, - connect_orientation: ConnectCsvOrientation = ConnectCsvOrientation.elem_major, - connect_indexing: ConnectIndexing = ConnectIndexing.auto, - uv_orientation: CoordCsvOrientation = CoordCsvOrientation.node_major, - field_orientation: FieldCsvOrientation = FieldCsvOrientation.node_major, -) -> tuple[np.ndarray, np.ndarray, np.ndarray | None, np.ndarray | None]: + coord_orient: ECsvOrient = ECsvOrient.NODE_MAJOR, + connect_orient: ECsvOrient = ( + ECsvOrient.ELEM_MAJOR + ), + connect_indexing: EConnectIndexing = EConnectIndexing.AUTO, + uv_orient: ECsvOrient = ECsvOrient.NODE_MAJOR, + field_orient: ECsvOrient = ECsvOrient.NODE_MAJOR, +) -> SimCsvData: + """Load and cross-validate a simulation CSV directory.""" data_path = Path(data_dir) coords = load_coord_csv( - data_path / coords_name, - skip_rows=skip_rows, - orientation=coord_orientation, + data_path / coords_name, skip_rows, coord_orient, ) + connect = load_connect_csv( - data_path / connect_name, - skip_rows=skip_rows, - orientation=connect_orientation, - indexing=connect_indexing, + data_path / connect_name, skip_rows, connect_orient, + connect_indexing, node_count=coords.shape[0], ) - uvs: np.ndarray | None = None + uvs = None uvs_path = data_path / uvs_name if uvs_path.is_file(): - uvs_raw = _load_csv_matrix(uvs_path, skip_rows) - uvs = _normalise_point_table(uvs_raw, uv_orientation, 2)[:, :2] + uvs = _normalise_point_table( + _load_csv_matrix(uvs_path, skip_rows), uv_orient, 2, + ) + if uvs.shape[0] != coords.shape[0]: + raise ValueError("UV and coordinate node counts must match.") + + disp_paths_out: list[Path] = [] + for name in (disp_x_name, disp_y_name, disp_z_name): + disp_paths_out.append(data_path / name) + + disp_paths = tuple(disp_paths_out) + disp_paths_optional: list[Path | None] = [] + for path in disp_paths: + disp_paths_optional.append(path if path.is_file() else None) disp = load_disp_csvs( - data_path / disp_x_name, - data_path / disp_y_name, - data_path / disp_z_name, - skip_rows=skip_rows, - orientation=field_orientation, + *disp_paths_optional, + skip_rows=skip_rows, orient=field_orient, ) - return coords, connect, uvs, disp + if disp is not None and disp.shape[1] != coords.shape[0]: + raise ValueError("Displacement and coordinate node counts must match.") + + return SimCsvData(coords, connect, uvs, disp) __all__ = [ - "load_connect_csv", - "load_coord_csv", - "load_disp_csvs", - "load_field_csv", - "load_field_csvs", - "load_sim_csvs", + "EConnectIndexing", "ECsvOrient", "SimCsvData", "load_connect_csv", + "load_coord_csv", "load_disp_csvs", "load_field_csv", + "load_field_csvs", "load_sim_csvs", ] diff --git a/src/riley/python/meshtools.py b/src/riley/python/meshtools.py deleted file mode 100644 index 6cea9569..00000000 --- a/src/riley/python/meshtools.py +++ /dev/null @@ -1,677 +0,0 @@ -# -------------------------------------------------------------------------- -# Riley: A High Performance Rasteriser for DIC UQ -# -# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) -# Licensed under the MIT License (see LICENSE file for details) -# -# Authors: scepticalrabbit (Lloyd Fletcher) -# -------------------------------------------------------------------------- -from __future__ import annotations - -from pathlib import Path - -import numpy as np - -from riley.python.enums import ( - ConnectIndexing, - PlanarProjectionMode, - ProjectionPlane, -) - - -_SURFACE_NODE_COUNTS = frozenset((3, 4, 6, 7, 8, 9)) -_TOL = 1.0e-12 - - -def enforce_mesh_convention( - coords: np.ndarray, - connect: np.ndarray, - *, - indexing: ConnectIndexing = ConnectIndexing.auto, -) -> tuple[np.ndarray, np.ndarray]: - coords_out = np.ascontiguousarray(coords, dtype=np.float64) - connect_out = np.ascontiguousarray(connect, dtype=np.int64) - - if indexing == ConnectIndexing.one_based: - connect_out = connect_out - 1 - elif indexing == ConnectIndexing.auto and _needs_zero_based_shift( - connect_out, - coords_out.shape[0], - ): - connect_out = connect_out - 1 - - if not _check_indices_zero_based(connect_out, coords_out.shape[0]): - raise ValueError("Connectivity contains indices outside the coordinate array.") - - connect_out = _enforce_right_handed_table( - _enforce_ccw_winding_table(connect_out, coords_out), - coords_out, - ) - - return coords_out, np.ascontiguousarray(connect_out, dtype=np.uintp) - - -def is_mesh_2d(coords: np.ndarray, connect: np.ndarray) -> bool: - # 1. Check coordinate flatness - coord_ranges = np.ptp(coords, axis=0) - if np.any(coord_ranges < 1e-12): - return True - - # 2. Check element nodes - nodes_per_elem = connect.shape[1] - if nodes_per_elem in (3, 6, 7, 9): - return True - if nodes_per_elem in (10, 20, 27): - return False - - if nodes_per_elem == 4: - num_check = min(10, connect.shape[0]) - is_tet = False - for i in range(num_check): - elem = connect[i] - v = coords[elem] - vol = np.abs( - np.dot(v[1] - v[0], np.cross(v[2] - v[0], v[3] - v[0])) - ) - if vol > 1e-10: - is_tet = True - break - if not is_tet: - return True - - if nodes_per_elem == 8: - num_check = min(10, connect.shape[0]) - is_hex = False - for i in range(num_check): - elem = connect[i] - v = coords[elem] - vol = np.abs( - np.dot(v[1] - v[0], np.cross(v[2] - v[0], v[4] - v[0])) - ) - if vol > 1e-10: - is_hex = True - break - if not is_hex: - return True - - return False - - -def extract_surface_mesh( - coords: np.ndarray, - connect: np.ndarray, - *, - indexing: ConnectIndexing = ConnectIndexing.auto, - enforce_convention: bool = True, -) -> tuple[np.ndarray, np.ndarray]: - coords_norm, connect_norm = enforce_mesh_convention( - coords, - connect, - indexing=indexing, - ) - connect_work = np.ascontiguousarray(connect_norm, dtype=np.int64) - - if is_mesh_2d(coords_norm, connect_norm): - raise ValueError( - "Surface extraction is only supported for 3D meshes. " - "The provided mesh appears to be 2D." - ) - - if connect_work.shape[1] not in (4, 8, 10, 20, 27): - raise NotImplementedError( - "Surface extraction is only implemented for tet and hex element " - "families.", - ) - - surf_faces, _ = _extract_surface_faces_from_table(connect_work, coords_norm) - surf_node_inds = np.unique(surf_faces) - surf_coords = np.ascontiguousarray(coords_norm[surf_node_inds], dtype=np.float64) - - coord_remap = np.full(coords_norm.shape[0], -1, dtype=np.int64) - coord_remap[surf_node_inds] = np.arange(surf_node_inds.shape[0], dtype=np.int64) - surf_connect = coord_remap[surf_faces] - - if enforce_convention: - surf_coords, surf_connect = enforce_mesh_convention( - surf_coords, - surf_connect, - indexing=ConnectIndexing.zero_based, - ) - - return surf_coords, np.ascontiguousarray(surf_connect, dtype=np.uintp) - - -def project_uvs_planar_bbox( - coords: np.ndarray, - texture_size: tuple[int, int] | tuple[float, float], - px_bbox: tuple[float, float, float, float], - projection_plane: ProjectionPlane | tuple[np.ndarray, np.ndarray], - *, - mode: PlanarProjectionMode = PlanarProjectionMode.best, -) -> np.ndarray: - coords_in = np.ascontiguousarray(coords, dtype=np.float64) - origin, u_axis, v_axis = _resolve_projection_axes(projection_plane) - - diff = coords_in - origin - x_proj = diff @ u_axis - y_proj = diff @ v_axis - - x_min = np.min(x_proj) - x_max = np.max(x_proj) - y_min = np.min(y_proj) - y_max = np.max(y_proj) - - mesh_w = x_max - x_min - mesh_h = y_max - y_min - - px_x_l, px_y_l, px_x_u, px_y_u = px_bbox - px_w = px_x_u - px_x_l - px_h = px_y_u - px_y_l - - scale_x = px_w / mesh_w if mesh_w > 0.0 else 1.0 - scale_y = px_h / mesh_h if mesh_h > 0.0 else 1.0 - - if mode == PlanarProjectionMode.fit_x: - scale = scale_x - elif mode == PlanarProjectionMode.fit_y: - scale = scale_y - elif mode == PlanarProjectionMode.best: - scale = 0.5 * (scale_x + scale_y) - else: - raise ValueError(f"Unsupported planar projection mode: {mode}.") - - mesh_cx = 0.5 * (x_min + x_max) - mesh_cy = 0.5 * (y_min + y_max) - px_cx = 0.5 * (px_x_l + px_x_u) - px_cy = 0.5 * (px_y_l + px_y_u) - - px_x = px_cx + (x_proj - mesh_cx) * scale - px_y = px_cy + (y_proj - mesh_cy) * scale - - tex_w, tex_h = texture_size - uvs = np.zeros((coords_in.shape[0], 2), dtype=np.float64) - uvs[:, 0] = px_x / float(tex_w - 1.0) - uvs[:, 1] = 1.0 - (px_y / float(tex_h - 1.0)) - return np.ascontiguousarray(uvs, dtype=np.float64) - - -def project_uvs_planar_centered( - coords: np.ndarray, - texture_size: tuple[int, int] | tuple[float, float], - *, - uv_span_max: float = 1.0, - projection_plane: ProjectionPlane | tuple[np.ndarray, np.ndarray] = ( - ProjectionPlane.xy - ), -) -> np.ndarray: - coords_in = np.ascontiguousarray(coords, dtype=np.float64) - tex_w = float(texture_size[0]) - tex_h = float(texture_size[1]) - - if isinstance(projection_plane, ProjectionPlane): - if projection_plane == ProjectionPlane.xy: - proj_coords = coords_in[:, :2] - elif projection_plane == ProjectionPlane.yz: - proj_coords = coords_in[:, 1:3] - elif projection_plane == ProjectionPlane.xz: - proj_coords = coords_in[:, (0, 2)] - else: - raise ValueError(f"Unsupported projection plane: {projection_plane}.") - else: - origin, u_axis, v_axis = _resolve_projection_axes(projection_plane) - diff = coords_in - origin - proj_coords = np.column_stack((diff @ u_axis, diff @ v_axis)) - - x_min = np.min(proj_coords[:, 0]) - x_max = np.max(proj_coords[:, 0]) - y_min = np.min(proj_coords[:, 1]) - y_max = np.max(proj_coords[:, 1]) - - mesh_w = x_max - x_min - mesh_h = y_max - y_min - if mesh_w <= 0.0 or mesh_h <= 0.0: - raise ValueError("Projected mesh has zero area in the chosen plane.") - - mesh_ar = mesh_w / mesh_h - tex_ar = tex_w / tex_h - aspect_ratio_ratio = mesh_ar / tex_ar - - if aspect_ratio_ratio > 1.0: - d_u = uv_span_max - d_v = d_u / aspect_ratio_ratio - mode = PlanarProjectionMode.fit_x - else: - d_v = uv_span_max - d_u = d_v * aspect_ratio_ratio - mode = PlanarProjectionMode.fit_y - - u_min = 0.5 * (1.0 - d_u) - u_max = 1.0 - u_min - v_min = 0.5 * (1.0 - d_v) - v_max = 1.0 - v_min - - px_bbox = ( - u_min * (tex_w - 1.0), - (1.0 - v_max) * (tex_h - 1.0), - u_max * (tex_w - 1.0), - (1.0 - v_min) * (tex_h - 1.0), - ) - return project_uvs_planar_bbox( - coords_in, - texture_size, - px_bbox, - projection_plane, - mode=mode, - ) - - -def _resolve_projection_axes( - projection_plane: ProjectionPlane | tuple[np.ndarray, np.ndarray], -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - if isinstance(projection_plane, ProjectionPlane): - if projection_plane == ProjectionPlane.xy: - origin = np.array((0.0, 0.0, 0.0), dtype=np.float64) - u_axis = np.array((1.0, 0.0, 0.0), dtype=np.float64) - v_axis = np.array((0.0, 1.0, 0.0), dtype=np.float64) - elif projection_plane == ProjectionPlane.yz: - origin = np.array((0.0, 0.0, 0.0), dtype=np.float64) - u_axis = np.array((0.0, 1.0, 0.0), dtype=np.float64) - v_axis = np.array((0.0, 0.0, 1.0), dtype=np.float64) - elif projection_plane == ProjectionPlane.xz: - origin = np.array((0.0, 0.0, 0.0), dtype=np.float64) - u_axis = np.array((1.0, 0.0, 0.0), dtype=np.float64) - v_axis = np.array((0.0, 0.0, 1.0), dtype=np.float64) - else: - raise ValueError(f"Unsupported projection plane: {projection_plane}.") - return origin, u_axis, v_axis - - normal, origin_in = projection_plane - normal_vec = np.asarray(normal, dtype=np.float64) - origin = np.asarray(origin_in, dtype=np.float64) - normal_vec = normal_vec / np.linalg.norm(normal_vec) - - if np.abs(normal_vec[2]) < 0.999: - u_axis = np.cross( - np.array((0.0, 0.0, 1.0), dtype=np.float64), - normal_vec, - ) - else: - u_axis = np.cross( - normal_vec, - np.array((0.0, 1.0, 0.0), dtype=np.float64), - ) - u_axis = u_axis / np.linalg.norm(u_axis) - v_axis = np.cross(normal_vec, u_axis) - v_axis = v_axis / np.linalg.norm(v_axis) - return origin, u_axis, v_axis - - -def _needs_zero_based_shift(connect: np.ndarray, num_coords: int) -> bool: - if connect.size == 0: - return False - if np.any(connect < 0): - return False - if np.any(connect == 0): - return False - return bool(np.any(connect >= num_coords)) - - -def _check_indices_zero_based(connect: np.ndarray, num_coords: int) -> bool: - if connect.size == 0: - return True - return bool(np.all((connect >= 0) & (connect < num_coords))) - - -def _get_corner_indices(nodes_per_elem: int) -> np.ndarray: - if nodes_per_elem in (3, 6, 7): - return np.array((0, 1, 2), dtype=np.int64) - if nodes_per_elem in (4, 8, 9): - return np.array((0, 1, 2, 3), dtype=np.int64) - if nodes_per_elem == 10: - return np.array((0, 1, 2, 3), dtype=np.int64) - if nodes_per_elem in (20, 27): - return np.array((0, 1, 2, 3, 4, 5, 6, 7), dtype=np.int64) - raise NotImplementedError( - f"Unsupported element type with {nodes_per_elem} nodes.", - ) - - -def _get_volume_corner_indices(nodes_per_elem: int) -> np.ndarray: - if nodes_per_elem in (4, 10): - return np.array((0, 1, 2, 3), dtype=np.int64) - if nodes_per_elem in (8, 20, 27): - return np.array((0, 1, 2, 3, 4, 5, 6, 7), dtype=np.int64) - raise NotImplementedError( - f"Unsupported volume element with {nodes_per_elem} nodes.", - ) - - -def _active_coord_axes(coords: np.ndarray) -> np.ndarray: - axis_range = np.ptp(coords, axis=0) - active = np.flatnonzero(axis_range > _TOL) - if active.shape[0] < 2: - raise ValueError("At least two active coordinate axes are required.") - return active[:2] - - -def _polygon_signed_area(coords_elem: np.ndarray) -> float: - axes = _active_coord_axes(coords_elem) - xy = coords_elem[:, axes] - rolled = np.roll(xy, -1, axis=0) - return float( - 0.5 * np.sum(xy[:, 0] * rolled[:, 1] - rolled[:, 0] * xy[:, 1]), - ) - - -def _is_coplanar(coords_elem: np.ndarray) -> bool: - centred = coords_elem - np.mean(coords_elem, axis=0) - return np.linalg.matrix_rank(centred, tol=_TOL) <= 2 - - -def _tet_signed_volume(coords_elem: np.ndarray) -> float: - return float( - np.linalg.det( - np.column_stack(( - coords_elem[1] - coords_elem[0], - coords_elem[2] - coords_elem[0], - coords_elem[3] - coords_elem[0], - )), - ), - ) - - -def _hex_signed_volume(coords_elem: np.ndarray) -> float: - return float( - np.linalg.det( - np.column_stack(( - coords_elem[1] - coords_elem[0], - coords_elem[3] - coords_elem[0], - coords_elem[4] - coords_elem[0], - )), - ), - ) - - -def _winding_metric(connect_row: np.ndarray, coords: np.ndarray) -> float | None: - nodes_per_elem = connect_row.shape[0] - if nodes_per_elem not in _SURFACE_NODE_COUNTS: - return None - corner_inds = _get_corner_indices(nodes_per_elem) - coords_elem = coords[connect_row[corner_inds]] - if not _is_coplanar(coords_elem): - return None - return _polygon_signed_area(coords_elem) - - -def _handedness_metric(connect_row: np.ndarray, coords: np.ndarray) -> float | None: - nodes_per_elem = connect_row.shape[0] - if nodes_per_elem in _SURFACE_NODE_COUNTS: - metric = _winding_metric(connect_row, coords) - if metric is not None: - return metric - - corner_inds = _get_corner_indices(nodes_per_elem) - coords_elem = coords[connect_row[corner_inds]] - if nodes_per_elem in (4, 10): - return _tet_signed_volume(coords_elem) - if nodes_per_elem in (8, 20, 27): - return _hex_signed_volume(coords_elem) - - raise NotImplementedError( - f"Unsupported handedness check for {nodes_per_elem}-node elements.", - ) - - -def _reverse_surface_row(connect_row: np.ndarray) -> np.ndarray: - perms = { - 3: np.array((0, 2, 1)), - 4: np.array((0, 3, 2, 1)), - 6: np.array((0, 2, 1, 5, 4, 3)), - 7: np.array((0, 2, 1, 5, 4, 3, 6)), - 8: np.array((0, 3, 2, 1, 7, 6, 5, 4)), - 9: np.array((0, 3, 2, 1, 7, 6, 5, 4, 8)), - } - return connect_row[perms[connect_row.shape[0]]] - - -def _reverse_handedness_row(connect_row: np.ndarray) -> np.ndarray: - perms = { - 4: np.array((0, 2, 1, 3)), - 10: np.array((0, 2, 1, 3, 6, 5, 4, 7, 9, 8)), - 8: np.array((0, 3, 2, 1, 4, 7, 6, 5)), - 20: np.array(( - 0, 3, 2, 1, 4, 7, 6, 5, 11, 10, 9, 8, 15, 14, 13, 12, 16, 19, 18, 17, - )), - 27: np.array(( - 0, 3, 2, 1, 4, 7, 6, 5, 11, 10, 9, 8, 15, 14, 13, 12, 16, 19, 18, 17, - 20, 21, 22, 23, 24, 25, 26, - )), - } - return connect_row[perms[connect_row.shape[0]]] - - -def _enforce_ccw_winding_table(connect: np.ndarray, coords: np.ndarray) -> np.ndarray: - connect_out = np.copy(connect) - for row_ind, row in enumerate(connect_out): - metric = _winding_metric(row, coords) - if metric is not None and metric < 0.0: - connect_out[row_ind, :] = _reverse_surface_row(row) - return np.ascontiguousarray(connect_out, dtype=np.int64) - - -def _enforce_right_handed_table( - connect: np.ndarray, - coords: np.ndarray, -) -> np.ndarray: - connect_out = np.copy(connect) - for row_ind, row in enumerate(connect_out): - metric = _handedness_metric(row, coords) - if metric is None or metric >= 0.0: - continue - - row_corners = coords[row[_get_corner_indices(row.shape[0])]] - if row.shape[0] in _SURFACE_NODE_COUNTS and _is_coplanar(row_corners): - connect_out[row_ind, :] = _reverse_surface_row(row) - else: - connect_out[row_ind, :] = _reverse_handedness_row(row) - return np.ascontiguousarray(connect_out, dtype=np.int64) - - -def _get_face_corner_coords(face_coords: np.ndarray) -> np.ndarray: - if face_coords.shape[0] in (3, 6, 7): - return face_coords[:3, :] - if face_coords.shape[0] in (4, 8, 9): - return face_coords[:4, :] - raise NotImplementedError( - f"Unsupported surface face with {face_coords.shape[0]} nodes.", - ) - - -def _calc_face_normal(face_coords: np.ndarray) -> np.ndarray: - face_corners = _get_face_corner_coords(face_coords) - face_normal = np.cross( - face_corners[1] - face_corners[0], - face_corners[2] - face_corners[0], - ) - normal_mag = np.linalg.norm(face_normal) - - if normal_mag <= _TOL and face_corners.shape[0] == 4: - face_normal = np.cross( - face_corners[2] - face_corners[0], - face_corners[3] - face_corners[0], - ) - normal_mag = np.linalg.norm(face_normal) - - if normal_mag <= _TOL: - raise ValueError("Degenerate face detected while extracting a surface.") - - return face_normal / normal_mag - - -def _orient_surface_face_outward( - face_connect: np.ndarray, - parent_connect: np.ndarray, - coords: np.ndarray, -) -> np.ndarray: - face_coords = coords[face_connect] - face_centroid = np.mean(_get_face_corner_coords(face_coords), axis=0) - - parent_corners = _get_volume_corner_indices(parent_connect.shape[0]) - parent_centroid = np.mean(coords[parent_connect[parent_corners]], axis=0) - - face_normal = _calc_face_normal(face_coords) - outward_dir = face_centroid - parent_centroid - - if np.dot(face_normal, outward_dir) < 0.0: - return _reverse_surface_row(face_connect) - return face_connect - - -def _normalise_surface_face_node_order( - face_connect: np.ndarray, - coords: np.ndarray, -) -> np.ndarray: - nodes_per_face = face_connect.shape[0] - face_out = np.copy(face_connect) - face_coords = coords[face_out] - - if nodes_per_face in (6, 7): - corner_inds = np.array((0, 1, 2), dtype=np.int64) - midside_pool = np.arange(3, nodes_per_face, dtype=np.int64) - edge_corner_pairs = ((0, 1), (1, 2), (2, 0)) - elif nodes_per_face in (8, 9): - corner_inds = np.array((0, 1, 2, 3), dtype=np.int64) - midside_pool = np.arange(4, nodes_per_face, dtype=np.int64) - edge_corner_pairs = ((0, 1), (1, 2), (2, 3), (3, 0)) - else: - return face_out - - face_centroid = np.mean(face_coords[corner_inds, :], axis=0) - mid_pool_coords = face_coords[midside_pool, :] - - if nodes_per_face in (7, 9): - centroid_dists = np.linalg.norm(mid_pool_coords - face_centroid, axis=1) - center_pool_ind = int(np.argmin(centroid_dists)) - center_local_ind = int(midside_pool[center_pool_ind]) - edge_pool_mask = np.ones(midside_pool.shape[0], dtype=bool) - edge_pool_mask[center_pool_ind] = False - edge_pool_local_inds = midside_pool[edge_pool_mask] - edge_pool_coords = face_coords[edge_pool_local_inds, :] - else: - center_local_ind = -1 - edge_pool_local_inds = midside_pool - edge_pool_coords = mid_pool_coords - - edge_midpoints = np.array( - [ - 0.5 * (face_coords[start_ind, :] + face_coords[end_ind, :]) - for start_ind, end_ind in edge_corner_pairs - ], - dtype=np.float64, - ) - edge_dists = np.linalg.norm( - edge_pool_coords[:, None, :] - edge_midpoints[None, :, :], - axis=2, - ) - edge_order = np.argmin(edge_dists, axis=0) - reordered_edge_inds = edge_pool_local_inds[edge_order] - - if nodes_per_face == 6: - face_out[3:6] = face_out[reordered_edge_inds] - elif nodes_per_face == 7: - face_out[3:6] = face_out[reordered_edge_inds] - face_out[6] = face_out[center_local_ind] - elif nodes_per_face == 8: - face_out[4:8] = face_out[reordered_edge_inds] - elif nodes_per_face == 9: - face_out[4:8] = face_out[reordered_edge_inds] - face_out[8] = face_out[center_local_ind] - - return face_out - - -def _get_surface_map(nodes_per_elem: int) -> np.ndarray: - if nodes_per_elem == 4: - return np.array(((0, 1, 2), (0, 3, 1), (0, 2, 3), (1, 3, 2))) - if nodes_per_elem == 8: - return np.array(( - (0, 1, 2, 3), - (0, 3, 7, 4), - (4, 7, 6, 5), - (1, 5, 6, 2), - (0, 4, 5, 1), - (2, 6, 7, 3), - )) - if nodes_per_elem == 10: - return np.array(( - (0, 1, 2, 4, 5, 6), - (0, 3, 1, 7, 8, 4), - (0, 2, 3, 6, 9, 7), - (1, 3, 2, 8, 9, 5), - )) - if nodes_per_elem == 20: - return np.array(( - (0, 1, 2, 3, 8, 9, 10, 11), - (0, 3, 7, 4, 11, 15, 19, 16), - (4, 7, 6, 5, 15, 14, 13, 12), - (1, 5, 6, 2, 17, 13, 18, 9), - (0, 4, 5, 1, 16, 12, 17, 8), - (2, 6, 7, 3, 18, 14, 19, 10), - )) - if nodes_per_elem == 27: - return np.array(( - (0, 1, 2, 3, 8, 9, 10, 11, 24), - (0, 3, 7, 4, 11, 15, 19, 16, 26), - (4, 7, 6, 5, 15, 14, 13, 12, 25), - (1, 5, 6, 2, 17, 13, 18, 9, 21), - (0, 4, 5, 1, 16, 12, 17, 8, 22), - (2, 6, 7, 3, 18, 14, 19, 10, 20), - )) - raise NotImplementedError( - "Surface extraction is only implemented for tet and hex element families.", - ) - - -def _extract_surface_faces_from_table( - connect: np.ndarray, - coords: np.ndarray, -) -> tuple[np.ndarray, np.ndarray]: - nodes_per_elem = connect.shape[1] - face_map = _get_surface_map(nodes_per_elem) - faces_wound = connect[:, face_map] - faces_flat_wound = faces_wound.reshape((-1, face_map.shape[1])) - faces_flat_sorted = np.sort(faces_flat_wound, axis=1) - - _, unique_inds, unique_counts = np.unique( - faces_flat_sorted, - axis=0, - return_index=True, - return_counts=True, - ) - ext_face_inds = unique_inds[unique_counts == 1] - ext_parent_elem_inds = np.ascontiguousarray( - ext_face_inds // face_map.shape[0], - dtype=np.int64, - ) - ext_faces = np.copy(faces_flat_wound[ext_face_inds]) - - for face_ind, parent_elem_ind in enumerate(ext_parent_elem_inds): - ext_faces[face_ind, :] = _orient_surface_face_outward( - ext_faces[face_ind, :], - connect[parent_elem_ind, :], - coords, - ) - ext_faces[face_ind, :] = _normalise_surface_face_node_order( - ext_faces[face_ind, :], - coords, - ) - - return np.ascontiguousarray(ext_faces, dtype=np.int64), ext_parent_elem_inds - - -__all__ = [ - "enforce_mesh_convention", - "extract_surface_mesh", - "project_uvs_planar_bbox", - "project_uvs_planar_centered", -] diff --git a/src/riley/python/sceneops.py b/src/riley/python/sceneops.py index 402079b7..07221e80 100644 --- a/src/riley/python/sceneops.py +++ b/src/riley/python/sceneops.py @@ -1,174 +1,291 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +"""Operations for positioning groups of meshes in a scene.""" + from __future__ import annotations from dataclasses import dataclass from enum import Enum -from typing import Sequence +from typing import Protocol, Sequence import numpy as np - -class AxisAnchor(Enum): - MIN = "min" - CENTER = "center" - MAX = "max" +from riley.python._verifio import _validate_coords, _validate_vec3 -class OverlapDirection(Enum): +class EOverlapDirect(Enum): + """Direction in which to place an overlapping mesh group.""" NEGATIVE = "negative" CURRENT = "current" POSITIVE = "positive" +class MeshCoords(Protocol): + """Structural type required by scene positioning operations.""" + coords: np.ndarray + + @dataclass(frozen=True, slots=True) class Bounds3D: - min: np.ndarray - max: np.ndarray + """Axis-aligned three-dimensional bounds.""" + minimum: np.ndarray + maximum: np.ndarray center: np.ndarray extent: np.ndarray @dataclass(frozen=True, slots=True) class MeshGroup: + """Contiguous range of meshes in a scene.""" mesh_start: int mesh_len: int @dataclass(frozen=True, slots=True) class GridSpec: + """Spacing and capacity of a three-dimensional group grid.""" gap: tuple[float, float, float] max_divs: tuple[int, int, int] @dataclass(frozen=True, slots=True) class BoundsOverlapSpec: + """Per-axis settings for overlapping two mesh group bounds.""" overlap_frac: tuple[float, float, float] enabled_axes: tuple[bool, bool, bool] = (True, True, True) - direction: tuple[OverlapDirection, OverlapDirection, OverlapDirection] = ( - OverlapDirection.CURRENT, - OverlapDirection.CURRENT, - OverlapDirection.CURRENT, + direct: tuple[ + EOverlapDirect, EOverlapDirect, EOverlapDirect + ] = ( + EOverlapDirect.CURRENT, + EOverlapDirect.CURRENT, + EOverlapDirect.CURRENT, ) extra_offset: tuple[float, float, float] = (0.0, 0.0, 0.0) def mesh_group_span(mesh_start: int, mesh_len: int) -> MeshGroup: + """Create a validated contiguous mesh group.""" + + if mesh_start < 0: + raise ValueError("mesh_start must be non-negative.") + + if mesh_len <= 0: + raise ValueError("mesh_len must be positive.") + return MeshGroup(mesh_start=mesh_start, mesh_len=mesh_len) def mesh_group_single(mesh_idx: int) -> MeshGroup: + """Create a group containing one mesh.""" return mesh_group_span(mesh_idx, 1) -def _group_indices(group: MeshGroup) -> range: - return range(group.mesh_start, group.mesh_start + group.mesh_len) +def _group_indices(meshes: Sequence[MeshCoords], group: MeshGroup) -> range: + """Return validated indices for a mesh group.""" + + if group.mesh_start < 0 or group.mesh_len <= 0: + raise ValueError( + "Mesh groups require a non-negative start and positive length.", + ) + + group_end = group.mesh_start + group.mesh_len + + if group_end > len(meshes): + raise IndexError("Mesh group extends beyond the mesh sequence.") + + return range(group.mesh_start, group_end) def bounds_for_coords(coords: np.ndarray) -> Bounds3D: - min_vals = np.min(coords, axis=0) - max_vals = np.max(coords, axis=0) + """Calculate axis-aligned bounds for coordinates.""" + + coords_in = _validate_coords(coords, "Mesh coordinates") + minimum = np.min(coords_in, axis=0) + maximum = np.max(coords_in, axis=0) + return Bounds3D( - min=min_vals, - max=max_vals, - center=0.5 * (min_vals + max_vals), - extent=max_vals - min_vals, + minimum=minimum, + maximum=maximum, + center=0.5 * (minimum + maximum), + extent=maximum - minimum, ) -def bounds_for_meshes(meshes: Sequence[object]) -> Bounds3D: - all_coords = np.concatenate([mesh.coords for mesh in meshes], axis=0) - return bounds_for_coords(all_coords) +def _bounds_for_indices( + meshes: Sequence[MeshCoords], + indices: range, +) -> Bounds3D: + """Reduce bounds over meshes without concatenating their coordinates.""" + + mesh_bounds = [bounds_for_coords(meshes[index].coords) for index in indices] + minimum = np.min([bounds.minimum for bounds in mesh_bounds], axis=0) + maximum = np.max([bounds.maximum for bounds in mesh_bounds], axis=0) + + return Bounds3D( + minimum=minimum, + maximum=maximum, + center=0.5 * (minimum + maximum), + extent=maximum - minimum, + ) -def bounds_for_mesh_group(meshes: Sequence[object], group: MeshGroup) -> Bounds3D: - coords = np.concatenate([meshes[idx].coords for idx in _group_indices(group)], axis=0) - return bounds_for_coords(coords) +def bounds_for_meshes(meshes: Sequence[MeshCoords]) -> Bounds3D: + """Calculate combined bounds without copying all mesh coordinates.""" + if not meshes: + raise ValueError("At least one mesh is required.") + + return _bounds_for_indices(meshes, range(len(meshes))) + + +def bounds_for_mesh_group( + meshes: Sequence[MeshCoords], + group: MeshGroup, +) -> Bounds3D: + """Calculate combined bounds for a contiguous mesh group.""" + return _bounds_for_indices(meshes, _group_indices(meshes, group)) def translate_mesh_group( - meshes: Sequence[object], + meshes: Sequence[MeshCoords], group: MeshGroup, translation: tuple[float, float, float] | np.ndarray, ) -> None: - translation_arr = np.asarray(translation, dtype=np.float64) - for idx in _group_indices(group): - meshes[idx].coords[:, :] += translation_arr + """Translate a mesh group in place.""" + translation_array = _validate_vec3(translation, "translation") + for index in _group_indices(meshes, group): + coords = _validate_coords(meshes[index].coords, "Mesh coordinates") + if not np.issubdtype(coords.dtype, np.floating): + raise TypeError("Mesh coordinates must use a floating-point dtype.") + coords += translation_array def center_mesh_group_at( - meshes: Sequence[object], + meshes: Sequence[MeshCoords], group: MeshGroup, target_center: tuple[float, float, float] | np.ndarray, ) -> None: + """Translate a mesh group so its bounds have the requested center.""" bounds = bounds_for_mesh_group(meshes, group) - target_arr = np.asarray(target_center, dtype=np.float64) - translate_mesh_group(meshes, group, target_arr - bounds.center) + target = _validate_vec3(target_center, "target_center") + translate_mesh_group(meshes, group, target - bounds.center) + +def _calc_overlap_sign( + current_sep: float, + direct: EOverlapDirect, +) -> float: + """Resolve an overlap direction to a signed separation.""" -def _overlap_sign(current_sep: float, direction: OverlapDirection) -> float: - if direction is OverlapDirection.NEGATIVE: + if direct is EOverlapDirect.NEGATIVE: return -1.0 - if direction is OverlapDirection.POSITIVE: + + if direct is EOverlapDirect.POSITIVE: return 1.0 - return -1.0 if current_sep < 0.0 else 1.0 + + if direct is EOverlapDirect.CURRENT: + return -1.0 if current_sep < 0.0 else 1.0 + + raise ValueError(f"Unsupported overlap direction: {direct}.") def overlap_mesh_group_bounds( - meshes: Sequence[object], + meshes: Sequence[MeshCoords], fixed_group: MeshGroup, moving_group: MeshGroup, spec: BoundsOverlapSpec, ) -> None: + """Translate one group to achieve the requested bounds overlap.""" + + overlap = _validate_vec3(spec.overlap_frac, "overlap_frac") + if np.any((overlap < 0.0) | (overlap > 1.0)): + raise ValueError("overlap_frac values must lie in [0, 1].") + + enabled = np.asarray(spec.enabled_axes) + if enabled.shape != (3,) or enabled.dtype != np.bool_: + raise ValueError("enabled_axes must contain three boolean values.") + + if len(spec.direct) != 3: + raise ValueError("direct must contain three values.") + + extra_offset = _validate_vec3(spec.extra_offset, "extra_offset") fixed_bounds = bounds_for_mesh_group(meshes, fixed_group) moving_bounds = bounds_for_mesh_group(meshes, moving_group) - translation = np.asarray(spec.extra_offset, dtype=np.float64) + translation = extra_offset.copy() for axis in range(3): - if not spec.enabled_axes[axis]: + if not enabled[axis]: continue - desired_overlap = spec.overlap_frac[axis] * min( - fixed_bounds.extent[axis], - moving_bounds.extent[axis], + desired_overlap = overlap[axis] * min( + fixed_bounds.extent[axis], moving_bounds.extent[axis], ) - center_sep_mag = ( + + separation = ( 0.5 * (fixed_bounds.extent[axis] + moving_bounds.extent[axis]) - desired_overlap ) + current_sep = moving_bounds.center[axis] - fixed_bounds.center[axis] - sep_sign = _overlap_sign(current_sep, spec.direction[axis]) - target_center = ( + target = ( fixed_bounds.center[axis] - + sep_sign * center_sep_mag - + spec.extra_offset[axis] + + _calc_overlap_sign(current_sep, spec.direct[axis]) * separation + + extra_offset[axis] ) - translation[axis] = target_center - moving_bounds.center[axis] + + translation[axis] = target - moving_bounds.center[axis] translate_mesh_group(meshes, moving_group, translation) def arrange_mesh_groups_grid( - meshes: Sequence[object], + meshes: Sequence[MeshCoords], groups: Sequence[MeshGroup], spec: GridSpec, ) -> None: - max_extent = np.zeros((3,), dtype=np.float64) - for group in groups: - bounds = bounds_for_mesh_group(meshes, group) - max_extent = np.maximum(max_extent, bounds.extent) + """Center mesh groups on a bounded three-dimensional grid.""" + + if not groups: + return + + gap = _validate_vec3(spec.gap, "gap") + divisions = np.asarray(spec.max_divs) + if ( + divisions.shape != (3,) + or not np.issubdtype(divisions.dtype, np.integer) + ): + raise ValueError("max_divs must contain three integers.") - stride = max_extent + np.asarray(spec.gap, dtype=np.float64) - x_divs, y_divs, _ = spec.max_divs + if np.any(divisions <= 0): + raise ValueError("max_divs values must be positive.") + + grid_capacity = int(np.prod(divisions)) + if len(groups) > grid_capacity: + raise ValueError("Mesh groups exceed the grid capacity.") + + group_bounds = [bounds_for_mesh_group(meshes, group) for group in groups] + max_extent = np.max([bounds.extent for bounds in group_bounds], axis=0) + stride = max_extent + gap + x_divs, y_divs, _ = (int(value) for value in divisions) for index, group in enumerate(groups): - xx = index % x_divs - yy = (index // x_divs) % y_divs - zz = index // (x_divs * y_divs) - center_mesh_group_at( - meshes, - group, - ( - float(xx) * stride[0], - float(yy) * stride[1], - float(zz) * stride[2], - ), + grid_index = ( + index % x_divs, + (index // x_divs) % y_divs, + index // (x_divs * y_divs), ) + center_mesh_group_at(meshes, group, np.asarray(grid_index) * stride) + + +__all__ = [ + "Bounds3D", "BoundsOverlapSpec", "EOverlapDirect", "GridSpec", + "MeshGroup", "arrange_mesh_groups_grid", + "bounds_for_coords", "bounds_for_mesh_group", "bounds_for_meshes", + "center_mesh_group_at", "mesh_group_single", "mesh_group_span", + "overlap_mesh_group_bounds", "translate_mesh_group", +] diff --git a/src/riley/python/uvtools.py b/src/riley/python/uvtools.py new file mode 100644 index 00000000..94b8215f --- /dev/null +++ b/src/riley/python/uvtools.py @@ -0,0 +1,349 @@ +# -------------------------------------------------------------------------- +# Riley: A High Performance Rasteriser for DIC UQ +# +# Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +# Licensed under the MIT License (see LICENSE file for details) +# +# Authors: scepticalrabbit (Lloyd Fletcher) +# -------------------------------------------------------------------------- +"""Planar UV projection utilities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +import numpy as np + +from riley.python._verifio import _validate_coords, _validate_finite_f64 + + +class EProjPlane(Enum): + """Axis-aligned plane used for planar UV projection. + + Attributes + ---------- + XY : str + Project using the x and y coordinate axes. + YZ : str + Project using the y and z coordinate axes. + XZ : str + Project using the x and z coordinate axes. + """ + XY = "xy" + YZ = "yz" + XZ = "xz" + + +class EPlanarProjMode(Enum): + """Scaling rule for fitting projected coordinates into pixel bounds. + + Attributes + ---------- + BEST : str + Use the smaller axis scale so the complete projection fits. + FIT_X : str + Fit the projected x extent to the horizontal pixel bounds. + FIT_Y : str + Fit the projected y extent to the vertical pixel bounds. + """ + BEST = "best" + FIT_X = "fit_x" + FIT_Y = "fit_y" + + +@dataclass(frozen=True, slots=True) +class ProjPlane: + """Arbitrary plane used for planar UV projection. + + Parameters + ---------- + normal : numpy.ndarray + Nonzero three-component plane normal. + origin : numpy.ndarray + Three-component point defining the projection-plane origin. + + Notes + ----- + Riley calculates a deterministic orthonormal basis from ``normal``. The + normal does not need to be normalized by the caller. + """ + normal: np.ndarray + origin: np.ndarray + + +ProjPlaneLike = EProjPlane | ProjPlane | tuple[ + np.ndarray, np.ndarray +] + + +def _validate_texture_size( + texture_size: tuple[int, int] | tuple[float, float], +) -> tuple[float, float]: + """Return a validated texture width and height.""" + + texture = _validate_finite_f64(texture_size, "texture_size", (2,)) + + if np.any(texture < 2.0): + raise ValueError("Texture width and height must both be at least 2.") + + return float(texture[0]), float(texture[1]) + + +def _resolve_proj_axes( + proj_plane: ProjPlaneLike, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Resolve a projection plane to an origin and orthonormal basis.""" + + zero = np.zeros(3, dtype=np.float64) + + if proj_plane is EProjPlane.XY: + return zero, np.array((1.0, 0.0, 0.0)), np.array((0.0, 1.0, 0.0)) + + if proj_plane is EProjPlane.YZ: + return zero, np.array((0.0, 1.0, 0.0)), np.array((0.0, 0.0, 1.0)) + + if proj_plane is EProjPlane.XZ: + return zero, np.array((1.0, 0.0, 0.0)), np.array((0.0, 0.0, 1.0)) + + if isinstance(proj_plane, EProjPlane): + raise ValueError(f"Unsupported projection plane: {proj_plane}.") + + if isinstance(proj_plane, ProjPlane): + normal_in = proj_plane.normal + origin_in = proj_plane.origin + else: + try: + normal_in, origin_in = proj_plane + except (TypeError, ValueError) as error: + raise ValueError( + "A custom projection plane must be (normal, origin).", + ) from error + + normal = _validate_finite_f64(normal_in, "Projection normal", (3,)) + origin = _validate_finite_f64(origin_in, "Projection origin", (3,)) + normal_norm = np.linalg.norm(normal) + + if normal_norm == 0.0: + raise ValueError("Projection normal must be nonzero.") + + normal = normal / normal_norm + + if abs(normal[2]) < 0.999: + u_axis = np.cross(np.array((0.0, 0.0, 1.0)), normal) + else: + u_axis = np.cross(normal, np.array((0.0, 1.0, 0.0))) + + u_axis /= np.linalg.norm(u_axis) + v_axis = np.cross(normal, u_axis) + v_axis /= np.linalg.norm(v_axis) + + return origin, u_axis, v_axis + + +def _project_coords( + coords: np.ndarray, + proj_plane: ProjPlaneLike, +) -> np.ndarray: + """Project coordinates onto a two-dimensional plane.""" + + if proj_plane is EProjPlane.XY: + return coords[:, :2] + + if proj_plane is EProjPlane.YZ: + return coords[:, 1:3] + + if proj_plane is EProjPlane.XZ: + return coords[:, (0, 2)] + + origin, u_axis, v_axis = _resolve_proj_axes(proj_plane) + difference = coords - origin + + return np.column_stack((difference @ u_axis, difference @ v_axis)) + + +def _proj_bounds( + projected: np.ndarray, +) -> tuple[float, float, float, float]: + """Return finite projection bounds, rejecting zero-area projections.""" + minimum = np.min(projected, axis=0) + maximum = np.max(projected, axis=0) + extent = maximum - minimum + if np.any(extent <= 0.0): + raise ValueError("Projected mesh has zero area in the chosen plane.") + return ( + float(minimum[0]), float(maximum[0]), + float(minimum[1]), float(maximum[1]), + ) + + +def _uvs_from_proj( + projected: np.ndarray, + texture_size: tuple[float, float], + px_bbox: tuple[float, float, float, float], + mode: EPlanarProjMode, +) -> np.ndarray: + """Map projected coordinates into a pixel bounding box.""" + x_min, x_max, y_min, y_max = _proj_bounds(projected) + px_bounds = _validate_finite_f64(px_bbox, "px_bbox", (4,)) + px_x_lower, px_y_lower, px_x_upper, px_y_upper = px_bounds + if px_x_upper <= px_x_lower or px_y_upper <= px_y_lower: + raise ValueError("px_bbox upper bounds must exceed lower bounds.") + scale_x = (px_x_upper - px_x_lower) / (x_max - x_min) + scale_y = (px_y_upper - px_y_lower) / (y_max - y_min) + if mode is EPlanarProjMode.FIT_X: + scale = scale_x + elif mode is EPlanarProjMode.FIT_Y: + scale = scale_y + elif mode is EPlanarProjMode.BEST: + scale = min(scale_x, scale_y) + else: + raise ValueError(f"Unsupported planar projection mode: {mode}.") + pixel_center = 0.5 * np.array( + (px_x_lower + px_x_upper, px_y_lower + px_y_upper), + ) + mesh_center = 0.5 * np.array((x_min + x_max, y_min + y_max)) + pixels = pixel_center + (projected - mesh_center) * scale + texture_width, texture_height = texture_size + uvs = np.empty((projected.shape[0], 2), dtype=np.float64) + uvs[:, 0] = pixels[:, 0] / (texture_width - 1.0) + uvs[:, 1] = 1.0 - pixels[:, 1] / (texture_height - 1.0) + return uvs + + +def project_uvs_planar_bbox( + coords: np.ndarray, + texture_size: tuple[int, int] | tuple[float, float], + px_bbox: tuple[float, float, float, float], + proj_plane: ProjPlaneLike, + mode: EPlanarProjMode = EPlanarProjMode.BEST, +) -> np.ndarray: + """Project mesh coordinates into a texture-space pixel bounding box. + + Parameters + ---------- + coords : numpy.ndarray + Finite mesh coordinates with shape ``(nodes, 3)``. + texture_size : tuple[int, int] or tuple[float, float] + Texture width and height in pixels. Both dimensions must be at least + two pixels. + px_bbox : tuple[float, float, float, float] + Lower x, lower y, upper x and upper y pixel coordinates. Upper bounds + must exceed their corresponding lower bounds. + proj_plane : EProjPlane, ProjPlane or tuple[numpy.ndarray, numpy.ndarray] + Axis-aligned plane or a custom ``(normal, origin)`` plane. + mode : EPlanarProjMode, optional + Rule used to fit the projected mesh into ``px_bbox``. The default is + :attr:`EPlanarProjMode.BEST`. + + Returns + ------- + numpy.ndarray + Contiguous float64 UV coordinates with shape ``(nodes, 2)``. + + Raises + ------ + ValueError + If an input has an invalid shape, contains non-finite values, defines + a degenerate projection, or specifies invalid pixel bounds. + + Examples + -------- + >>> import numpy as np + >>> from riley.python.uvtools import EProjPlane + >>> from riley.python.uvtools import project_uvs_planar_bbox + >>> coords = np.array(((0., 0., 0.), (2., 0., 0.), + ... (2., 1., 0.), (0., 1., 0.))) + >>> uvs = project_uvs_planar_bbox( + ... coords, (201, 101), (0., 0., 200., 100.), EProjPlane.XY, + ... ) + >>> uvs.shape + (4, 2) + >>> np.all((uvs >= 0.) & (uvs <= 1.)) + np.True_ + """ + coords_in = _validate_coords(coords, contiguous_f64=True) + texture_size_in = _validate_texture_size(texture_size) + projected = _project_coords(coords_in, proj_plane) + return _uvs_from_proj(projected, texture_size_in, px_bbox, mode) + + +def project_uvs_planar_centered( + coords: np.ndarray, + texture_size: tuple[int, int] | tuple[float, float], + uv_span_max: float = 1.0, + proj_plane: ProjPlaneLike = EProjPlane.XY, +) -> np.ndarray: + """Project coordinates into a centered, aspect-preserving UV region. + + Parameters + ---------- + coords : numpy.ndarray + Finite mesh coordinates with shape ``(nodes, 3)``. + texture_size : tuple[int, int] or tuple[float, float] + Texture width and height in pixels. Both dimensions must be at least + two pixels. + uv_span_max : float, optional + Maximum normalized span used by either UV axis. Must lie in ``(0, 1]``. + The default is ``1.0``. + proj_plane : EProjPlane, ProjPlane or tuple of numpy.ndarray, optional + Axis-aligned plane or a custom ``(normal, origin)`` plane. The default + is :attr:`EProjPlane.XY`. + + Returns + ------- + numpy.ndarray + Contiguous float64 UV coordinates with shape ``(nodes, 2)``. + + Raises + ------ + ValueError + If an input is invalid or the selected projection has zero area. + + Examples + -------- + >>> import numpy as np + >>> from riley.python.uvtools import project_uvs_planar_centered + >>> coords = np.array(((0., 0., 0.), (2., 0., 0.), + ... (2., 1., 0.), (0., 1., 0.))) + >>> uvs = project_uvs_planar_centered(coords, (201, 101), 0.8) + >>> np.round(np.ptp(uvs, axis=0), 2) + array([0.8, 0.8]) + """ + coords_in = _validate_coords(coords, contiguous_f64=True) + texture_width, texture_height = _validate_texture_size(texture_size) + if not np.isfinite(uv_span_max) or not 0.0 < uv_span_max <= 1.0: + raise ValueError( + "uv_span_max must be finite and in the interval (0, 1].", + ) + projected = _project_coords(coords_in, proj_plane) + x_min, x_max, y_min, y_max = _proj_bounds(projected) + aspect_ratio_ratio = ( + (x_max - x_min) / (y_max - y_min) + / (texture_width / texture_height) + ) + if aspect_ratio_ratio > 1.0: + u_span = uv_span_max + v_span = u_span / aspect_ratio_ratio + mode = EPlanarProjMode.FIT_X + else: + v_span = uv_span_max + u_span = v_span * aspect_ratio_ratio + mode = EPlanarProjMode.FIT_Y + u_min = 0.5 * (1.0 - u_span) + v_min = 0.5 * (1.0 - v_span) + px_bbox = ( + u_min * (texture_width - 1.0), + v_min * (texture_height - 1.0), + (1.0 - u_min) * (texture_width - 1.0), + (1.0 - v_min) * (texture_height - 1.0), + ) + return _uvs_from_proj( + projected, (texture_width, texture_height), px_bbox, mode, + ) + + +__all__ = [ + "EPlanarProjMode", "EProjPlane", "ProjPlane", + "project_uvs_planar_bbox", "project_uvs_planar_centered", +] diff --git a/src/riley/zig/c-riley.zig b/src/riley/zig/c-riley.zig index 046e6a65..25f6bf6b 100644 --- a/src/riley/zig/c-riley.zig +++ b/src/riley/zig/c-riley.zig @@ -286,6 +286,12 @@ pub const CRasterConfig = extern struct { background_value: F, disk_save_overlap: u8, tile_size_override: u16, + global_subpx_tile_size_min: u16, + global_subpx_tile_size_max: u16, + global_subpx_tile_size_override: u16, + global_subpx_stripe_size_min: u16, + global_subpx_stripe_size_max: u16, + global_subpx_stripe_size_override: u16, save_frame_buff_count: usize, save_format: u32, save_bits: u32, @@ -305,6 +311,7 @@ pub const CRasterConfig = extern struct { full_stats_save_earlyout_map: u8, full_stats_save_pixel_occupancy_map: u8, full_stats_save_normals_map: u8, + buffer_mode: u32, }; const MeshInputBuilt = struct { @@ -504,6 +511,15 @@ fn reportModeFromC(report_mode: u32) !riley.ReportMode { }; } +fn bufferModeFromC(buffer_mode: u32) !rastcfg.BufferMode { + return switch (buffer_mode) { + @intFromEnum(rastcfg.BufferMode.tile_local) => .tile_local, + @intFromEnum(rastcfg.BufferMode.global_subpx_full) => .global_subpx_full, + @intFromEnum(rastcfg.BufferMode.global_subpx_stripe) => .global_subpx_stripe, + else => error.InvalidBufferMode, + }; +} + fn subpxCenterMapFromC(subpx_map: u32) !cam.SubPixelCenterMap { return switch (subpx_map) { @intFromEnum(cam.SubPixelCenterMap.full_in_mem) => .full_in_mem, @@ -1499,6 +1515,7 @@ fn buildRasterConfig( in_config.newton_seed_reuse, ); config.report = try reportModeFromC(in_config.report); + config.buffer_mode = try bufferModeFromC(in_config.buffer_mode); config.tile_size_min = if (in_config.tile_size_min == 0) config.tile_size_min else @@ -1513,6 +1530,32 @@ fn buildRasterConfig( null else in_config.tile_size_override; + config.global_subpx_tile_size_min = if (in_config.global_subpx_tile_size_min == 0) + config.global_subpx_tile_size_min + else + in_config.global_subpx_tile_size_min; + config.global_subpx_tile_size_max = if (in_config.global_subpx_tile_size_max == 0) + config.global_subpx_tile_size_max + else + in_config.global_subpx_tile_size_max; + config.global_subpx_tile_size_override = + if (in_config.global_subpx_tile_size_override == 0) + null + else + in_config.global_subpx_tile_size_override; + config.global_subpx_stripe_size_min = if (in_config.global_subpx_stripe_size_min == 0) + config.global_subpx_stripe_size_min + else + in_config.global_subpx_stripe_size_min; + config.global_subpx_stripe_size_max = if (in_config.global_subpx_stripe_size_max == 0) + config.global_subpx_stripe_size_max + else + in_config.global_subpx_stripe_size_max; + config.global_subpx_stripe_size_override = + if (in_config.global_subpx_stripe_size_override == 0) + null + else + in_config.global_subpx_stripe_size_override; if (in_config.save_frame_buff_count != 0) { config.save_frame_buff_count = in_config.save_frame_buff_count; } @@ -1990,6 +2033,265 @@ pub export fn rileyPosFillFrameFromRotOverMeshes( return 0; } +pub export fn rileyPosFrameCoords( + in_coords: *const CArray2DF64, + pixels_num: CVec2U32, + pixels_size: CVec2F64, + focal_length: F, + rot_world: CVec3F64, + fov_scale: F, + fit_mode: u32, + out_pos: *CVec3F64, +) c_int { + clearLastError(); + + const coords = buildCoordsFromC(in_coords) catch |err| { + setLastError(err); + return 1; + }; + const rot = rotation.Rotation.init( + rot_world.x, + rot_world.y, + rot_world.z, + ); + const fit_enum: cameraops.FrameFitMode = @enumFromInt(fit_mode); + const cam_pos = cameraops.posFrameCoords( + &coords, + .{ pixels_num.x, pixels_num.y }, + .{ pixels_size.x, pixels_size.y }, + focal_length, + rot, + fov_scale, + fit_enum, + ); + out_pos.* = vec3ToCVec3(cam_pos); + return 0; +} + +pub export fn rileyPosFrameCoordsTarg( + in_coords: *const CArray2DF64, + targ_world: CVec3F64, + pixels_num: CVec2U32, + pixels_size: CVec2F64, + focal_length: F, + rot_world: CVec3F64, + fov_scale: F, + fit_mode: u32, + out_pos: *CVec3F64, +) c_int { + clearLastError(); + + const coords = buildCoordsFromC(in_coords) catch |err| { + setLastError(err); + return 1; + }; + const rot = rotation.Rotation.init( + rot_world.x, + rot_world.y, + rot_world.z, + ); + const targ = cVec3ToVec3(targ_world); + const fit_enum: cameraops.FrameFitMode = @enumFromInt(fit_mode); + const cam_pos = cameraops.posFrameCoordsTarg( + &coords, + targ, + .{ pixels_num.x, pixels_num.y }, + .{ pixels_size.x, pixels_size.y }, + focal_length, + rot, + fov_scale, + fit_enum, + ); + out_pos.* = vec3ToCVec3(cam_pos); + return 0; +} + +pub export fn rileyPosFrameMeshes( + in_meshes: [*c]const CMeshInput, + meshes_len: usize, + pixels_num: CVec2U32, + pixels_size: CVec2F64, + focal_length: F, + rot_world: CVec3F64, + fov_scale: F, + fit_mode: u32, + out_pos: *CVec3F64, +) c_int { + clearLastError(); + + var arena = std.heap.ArenaAllocator.init(std.heap.smp_allocator); + defer arena.deinit(); + const aa = arena.allocator(); + + const built_meshes = buildMeshInputSlice( + aa, + in_meshes, + meshes_len, + ) catch |err| { + setLastError(err); + return 1; + }; + defer deinitMeshInputSlice(aa, built_meshes); + + const mesh_inputs = extractMeshInputs(aa, built_meshes) catch |err| { + setLastError(err); + return 1; + }; + + const rot = rotation.Rotation.init( + rot_world.x, + rot_world.y, + rot_world.z, + ); + const fit_enum: cameraops.FrameFitMode = @enumFromInt(fit_mode); + const cam_pos = cameraops.posFrameMeshes( + mesh_inputs, + .{ pixels_num.x, pixels_num.y }, + .{ pixels_size.x, pixels_size.y }, + focal_length, + rot, + fov_scale, + fit_enum, + ); + out_pos.* = vec3ToCVec3(cam_pos); + return 0; +} + +pub export fn rileyPosFrameMeshesTarg( + in_meshes: [*c]const CMeshInput, + meshes_len: usize, + targ_world: CVec3F64, + pixels_num: CVec2U32, + pixels_size: CVec2F64, + focal_length: F, + rot_world: CVec3F64, + fov_scale: F, + fit_mode: u32, + out_pos: *CVec3F64, +) c_int { + clearLastError(); + + var arena = std.heap.ArenaAllocator.init(std.heap.smp_allocator); + defer arena.deinit(); + const aa = arena.allocator(); + + const built_meshes = buildMeshInputSlice( + aa, + in_meshes, + meshes_len, + ) catch |err| { + setLastError(err); + return 1; + }; + defer deinitMeshInputSlice(aa, built_meshes); + + const mesh_inputs = extractMeshInputs(aa, built_meshes) catch |err| { + setLastError(err); + return 1; + }; + + const rot = rotation.Rotation.init( + rot_world.x, + rot_world.y, + rot_world.z, + ); + const targ = cVec3ToVec3(targ_world); + const fit_enum: cameraops.FrameFitMode = @enumFromInt(fit_mode); + const cam_pos = cameraops.posFrameMeshesTarg( + mesh_inputs, + targ, + .{ pixels_num.x, pixels_num.y }, + .{ pixels_size.x, pixels_size.y }, + focal_length, + rot, + fov_scale, + fit_enum, + ); + out_pos.* = vec3ToCVec3(cam_pos); + return 0; +} + +pub export fn rileyCoverageToFovScale(coverage: F) F { + return cameraops.coverageToFovScale(coverage); +} + +pub export fn rileyFovScaleToCoverage(fov_scale: F) F { + return cameraops.fovScaleToCoverage(fov_scale); +} + +pub export fn rileyPosOrbitCam( + targ_world: CVec3F64, + azimuth_rad: F, + elevation_rad: F, + dist: F, + out_pos: *CVec3F64, + out_rot: *CVec3F64, +) c_int { + clearLastError(); + const targ = cVec3ToVec3(targ_world); + const orbit = cameraops.posOrbitCam( + targ, + azimuth_rad, + elevation_rad, + dist, + ); + out_pos.* = vec3ToCVec3(orbit.pos); + out_rot.* = .{ + .x = orbit.rot.alpha_z, + .y = orbit.rot.beta_y, + .z = orbit.rot.gamma_x, + }; + return 0; +} + +pub export fn rileyPosStereoPair( + targ_world: CVec3F64, + dist: F, + stereo_angle_rad: F, + baseline_angle_rad: F, + out_cam0_pos: *CVec3F64, + out_cam0_rot: *CVec3F64, + out_cam1_pos: *CVec3F64, + out_cam1_rot: *CVec3F64, +) c_int { + clearLastError(); + const targ = cVec3ToVec3(targ_world); + const stereo = cameraops.posStereoPair( + targ, + dist, + stereo_angle_rad, + baseline_angle_rad, + ); + out_cam0_pos.* = vec3ToCVec3(stereo.cam0_pos); + out_cam0_rot.* = .{ + .x = stereo.cam0_rot.alpha_z, + .y = stereo.cam0_rot.beta_y, + .z = stereo.cam0_rot.gamma_x, + }; + out_cam1_pos.* = vec3ToCVec3(stereo.cam1_pos); + out_cam1_rot.* = .{ + .x = stereo.cam1_rot.alpha_z, + .y = stereo.cam1_rot.beta_y, + .z = stereo.cam1_rot.gamma_x, + }; + return 0; +} + +pub export fn rileyCalcPixelResolution( + in_camera: *const CCameraInput, + targ_world: CVec3F64, + out_res: *F, +) c_int { + clearLastError(); + const cam_input = buildCameraInput(in_camera) catch |err| { + setLastError(err); + return 1; + }; + const targ = cVec3ToVec3(targ_world); + out_res.* = cameraops.calcPixelResolution(cam_input, targ); + return 0; +} + pub export fn rileyCalcOutputDimsScene( in_meshes: [*c]const CMeshInput, meshes_len: usize, diff --git a/src/riley/zig/cameraops.zig b/src/riley/zig/cameraops.zig index 3edc2c5e..498268ca 100644 --- a/src/riley/zig/cameraops.zig +++ b/src/riley/zig/cameraops.zig @@ -19,6 +19,39 @@ const rotation = @import("rotation.zig"); const rastcfg = @import("rasterconfig.zig"); const F = buildconfig.F; +// -------------------------------------------------------------------------------------- +// Public Constants & Public Types +// -------------------------------------------------------------------------------------- + +pub const FrameFitMode = enum(u32) { + contain = 0, + cover = 1, + horizontal = 2, + vertical = 3, +}; + +pub const OrbitCam = struct { + pos: vec.Vec3f, + rot: rotation.Rotation, +}; + +pub const StereoPairPosRot = struct { + cam0_pos: vec.Vec3f, + cam0_rot: rotation.Rotation, + cam1_pos: vec.Vec3f, + cam1_rot: rotation.Rotation, +}; + +pub const CameraPlaneMetrics = struct { + sensor_size: [2]F, + focal_px: [2]F, + principal_point_px: [2]F, + roi_plane_dist: F, + roi_plane_size: [2]F, + avg_leng_per_pixel: F, + avg_pixel_per_leng: F, +}; + // -------------------------------------------------------------------------------------- // Public Entry-Point Func // -------------------------------------------------------------------------------------- @@ -66,16 +99,6 @@ pub fn toOpenGLInput(input: cam.CameraInput) cam.CameraInput { return opengl_input; } -const CameraPlaneMetrics = struct { - sensor_size: [2]F, - focal_px: [2]F, - principal_point_px: [2]F, - roi_plane_dist: F, - roi_plane_size: [2]F, - avg_leng_per_pixel: F, - avg_pixel_per_leng: F, -}; - pub fn calcPlaneMetrics(camera_input: cam.CameraInput) CameraPlaneMetrics { const opengl_input = toOpenGLInput(camera_input); const scaling = calcFOVScaling( @@ -101,6 +124,16 @@ pub fn calcPlaneMetrics(camera_input: cam.CameraInput) CameraPlaneMetrics { }; } +pub fn coverageToFovScale(coverage: F) F { + std.debug.assert(coverage > 0.0); + return 1.0 / coverage; +} + +pub fn fovScaleToCoverage(fov_scale: F) F { + std.debug.assert(fov_scale > 0.0); + return 1.0 / fov_scale; +} + pub fn fovFromCamRot( cam_rot: rotation.Rotation, coords_world: *const meshio.Coords, @@ -207,17 +240,18 @@ pub fn lookAtPoint( return rotation.Rotation.init(alpha_z, beta_y, 0.0); } -pub fn imageDistFillFrameFromRot( +pub fn imageDistFrameCoords( coords_world: *const meshio.Coords, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, + fit_mode: FrameFitMode, ) F { var fov_leng = fovFromCamRot(cam_rot, coords_world); - fov_leng[0] = frame_fill * fov_leng[0]; - fov_leng[1] = frame_fill * fov_leng[1]; + fov_leng[0] = fov_scale * fov_leng[0]; + fov_leng[1] = fov_scale * fov_leng[1]; const image_dists = imageDistFromFov( pixels_num, @@ -225,24 +259,126 @@ pub fn imageDistFillFrameFromRot( focal_leng, fov_leng, ); - return @max(image_dists[0], image_dists[1]); + return selectFitDist(image_dists, fit_mode); } -pub fn posFillFrameFromRot( +pub fn imageDistFrameCoordsTarg( coords_world: *const meshio.Coords, + targ_world: vec.Vec3f, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, + fit_mode: FrameFitMode, +) F { + const world_to_cam_mat = matrix.Mat33Ops.inv(F, cam_rot.matrix); + var coord_cam = world_to_cam_mat.mulVec(coords_world.getVec3(0).sub(targ_world)); + var max_abs_x = @abs(coord_cam.get(0)); + var max_abs_y = @abs(coord_cam.get(1)); + + for (1..coords_world.mat.rows_num) |nn| { + coord_cam = world_to_cam_mat.mulVec(coords_world.getVec3(nn).sub(targ_world)); + max_abs_x = @max(max_abs_x, @abs(coord_cam.get(0))); + max_abs_y = @max(max_abs_y, @abs(coord_cam.get(1))); + } + + const fov_leng = [2]F{ + 2.0 * fov_scale * max_abs_x, + 2.0 * fov_scale * max_abs_y, + }; + const image_dists = imageDistFromFov( + pixels_num, + pixels_size, + focal_leng, + fov_leng, + ); + return selectFitDist(image_dists, fit_mode); +} + +pub fn imageDistFrameMeshes( + meshes: []const mo.MeshInput, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, + fit_mode: FrameFitMode, +) F { + var fov_leng = fovFromCamRotOverMeshes(cam_rot, meshes); + fov_leng[0] = fov_scale * fov_leng[0]; + fov_leng[1] = fov_scale * fov_leng[1]; + + const image_dists = imageDistFromFov( + pixels_num, + pixels_size, + focal_leng, + fov_leng, + ); + return selectFitDist(image_dists, fit_mode); +} + +pub fn imageDistFrameMeshesTarg( + meshes: []const mo.MeshInput, + targ_world: vec.Vec3f, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, + fit_mode: FrameFitMode, +) F { + const world_to_cam_mat = matrix.Mat33Ops.inv(F, cam_rot.matrix); + var max_abs_x: F = 0.0; + var max_abs_y: F = 0.0; + var is_first = true; + + for (meshes) |mesh| { + for (0..mesh.coords.mat.rows_num) |nn| { + const coord_cam = world_to_cam_mat.mulVec( + mesh.coords.getVec3(nn).sub(targ_world), + ); + if (is_first) { + max_abs_x = @abs(coord_cam.get(0)); + max_abs_y = @abs(coord_cam.get(1)); + is_first = false; + } else { + max_abs_x = @max(max_abs_x, @abs(coord_cam.get(0))); + max_abs_y = @max(max_abs_y, @abs(coord_cam.get(1))); + } + } + } + + const fov_leng = [2]F{ + 2.0 * fov_scale * max_abs_x, + 2.0 * fov_scale * max_abs_y, + }; + const image_dists = imageDistFromFov( + pixels_num, + pixels_size, + focal_leng, + fov_leng, + ); + return selectFitDist(image_dists, fit_mode); +} + +pub fn posFrameCoords( + coords_world: *const meshio.Coords, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, + fit_mode: FrameFitMode, ) vec.Vec3f { - const image_dist = imageDistFillFrameFromRot( + const image_dist = imageDistFrameCoords( coords_world, pixels_num, pixels_size, focal_leng, cam_rot, - frame_fill, + fov_scale, + fit_mode, ); return calcCamPos( sceneops.boundsCenter(coords_world), @@ -251,46 +387,67 @@ pub fn posFillFrameFromRot( ); } -pub fn posFillFrameFromRotAndTarg( +pub fn posFrameCoordsTarg( coords_world: *const meshio.Coords, targ_world: vec.Vec3f, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, + fit_mode: FrameFitMode, ) vec.Vec3f { - const image_dist = imageDistFillFrameFromRotAndTarg( + const image_dist = imageDistFrameCoordsTarg( coords_world, targ_world, pixels_num, pixels_size, focal_leng, cam_rot, - frame_fill, + fov_scale, + fit_mode, ); return calcCamPos(targ_world, cam_rot, image_dist); } -pub fn posFillFrameFromRotOverMeshes( - meshes: []const mo.MeshInput, +pub fn posFrameMesh( + mesh: mo.MeshInput, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, + fit_mode: FrameFitMode, ) vec.Vec3f { - var fov_leng = fovFromCamRotOverMeshes(cam_rot, meshes); - fov_leng[0] = frame_fill * fov_leng[0]; - fov_leng[1] = frame_fill * fov_leng[1]; + return posFrameCoords( + &mesh.coords, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + fov_scale, + fit_mode, + ); +} - const image_dists = imageDistFromFov( +pub fn posFrameMeshes( + meshes: []const mo.MeshInput, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, + fit_mode: FrameFitMode, +) vec.Vec3f { + const image_dist = imageDistFrameMeshes( + meshes, pixels_num, pixels_size, focal_leng, - fov_leng, + cam_rot, + fov_scale, + fit_mode, ); - const image_dist = @max(image_dists[0], image_dists[1]); return calcCamPos( sceneops.boundsCenterOverMeshes(meshes), cam_rot, @@ -298,103 +455,325 @@ pub fn posFillFrameFromRotOverMeshes( ); } -pub fn posFillFrameFromRotOverMeshesAndTarg( +pub fn posFrameMeshesTarg( meshes: []const mo.MeshInput, targ_world: vec.Vec3f, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, + fit_mode: FrameFitMode, ) vec.Vec3f { - const image_dist = imageDistFillFrameFromRotOverMeshesAndTarg( + const image_dist = imageDistFrameMeshesTarg( meshes, targ_world, pixels_num, pixels_size, focal_leng, cam_rot, - frame_fill, + fov_scale, + fit_mode, ); return calcCamPos(targ_world, cam_rot, image_dist); } +pub fn posOrbitCam( + targ_world: vec.Vec3f, + azimuth_rad: F, + elevation_rad: F, + dist: F, +) OrbitCam { + const cos_elev = @cos(elevation_rad); + const offset = vec.initVec3( + F, + dist * cos_elev * @cos(azimuth_rad), + dist * cos_elev * @sin(azimuth_rad), + dist * @sin(elevation_rad), + ); + const pos = (&targ_world).add(offset); + const rot = lookAtPoint(pos, targ_world); + return .{ + .pos = pos, + .rot = rot, + }; +} + +pub fn posStereoPair( + targ_world: vec.Vec3f, + dist: F, + stereo_angle_rad: F, + baseline_angle_rad: F, +) StereoPairPosRot { + const half_angle = 0.5 * stereo_angle_rad; + const cam0_orbit = posOrbitCam( + targ_world, + baseline_angle_rad - half_angle, + 0.0, + dist, + ); + const cam1_orbit = posOrbitCam( + targ_world, + baseline_angle_rad + half_angle, + 0.0, + dist, + ); + return .{ + .cam0_pos = cam0_orbit.pos, + .cam0_rot = cam0_orbit.rot, + .cam1_pos = cam1_orbit.pos, + .cam1_rot = cam1_orbit.rot, + }; +} + +pub fn calcPixelResolution( + camera_input: cam.CameraInput, + targ_world: vec.Vec3f, +) F { + const opengl_input = toOpenGLInput(camera_input); + const scaling = calcFOVScaling(opengl_input, targ_world); + return 0.5 * (scaling.leng_per_pixel[0] + scaling.leng_per_pixel[1]); +} + // -------------------------------------------------------------------------------------- -// Generic Low-Level Helpers +// Deprecated Backwards-Compatible Aliases // -------------------------------------------------------------------------------------- -fn imageDistFillFrameFromRotAndTarg( +pub fn imageDistFillFrameFromRot( coords_world: *const meshio.Coords, - targ_world: vec.Vec3f, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, + fov_scale: F, ) F { - const world_to_cam_mat = matrix.Mat33Ops.inv(F, cam_rot.matrix); - var coord_cam = world_to_cam_mat.mulVec(coords_world.getVec3(0).sub(targ_world)); - var max_abs_x = @abs(coord_cam.get(0)); - var max_abs_y = @abs(coord_cam.get(1)); + return imageDistFrameCoords( + coords_world, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + fov_scale, + .contain, + ); +} - for (1..coords_world.mat.rows_num) |nn| { - coord_cam = world_to_cam_mat.mulVec(coords_world.getVec3(nn).sub(targ_world)); - max_abs_x = @max(max_abs_x, @abs(coord_cam.get(0))); - max_abs_y = @max(max_abs_y, @abs(coord_cam.get(1))); - } +pub fn posFillFrameFromRot( + coords_world: *const meshio.Coords, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, +) vec.Vec3f { + return posFrameCoords( + coords_world, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + fov_scale, + .contain, + ); +} - const fov_leng = [2]F{ - 2.0 * frame_fill * max_abs_x, - 2.0 * frame_fill * max_abs_y, - }; - const image_dists = imageDistFromFov( +pub fn posFillFrameFromRotAndTarg( + coords_world: *const meshio.Coords, + targ_world: vec.Vec3f, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, +) vec.Vec3f { + return posFrameCoordsTarg( + coords_world, + targ_world, pixels_num, pixels_size, focal_leng, - fov_leng, + cam_rot, + fov_scale, + .contain, ); - return @max(image_dists[0], image_dists[1]); } -fn imageDistFillFrameFromRotOverMeshesAndTarg( +pub fn posFillFrameFromRotOverMeshes( + meshes: []const mo.MeshInput, + pixels_num: [2]u32, + pixels_size: [2]F, + focal_leng: F, + cam_rot: rotation.Rotation, + fov_scale: F, +) vec.Vec3f { + return posFrameMeshes( + meshes, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + fov_scale, + .contain, + ); +} + +pub fn posFillFrameFromRotOverMeshesAndTarg( meshes: []const mo.MeshInput, targ_world: vec.Vec3f, pixels_num: [2]u32, pixels_size: [2]F, focal_leng: F, cam_rot: rotation.Rotation, - frame_fill: F, -) F { - const world_to_cam_mat = matrix.Mat33Ops.inv(F, cam_rot.matrix); - var max_abs_x: F = 0.0; - var max_abs_y: F = 0.0; - var is_first = true; + fov_scale: F, +) vec.Vec3f { + return posFrameMeshesTarg( + meshes, + targ_world, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + fov_scale, + .contain, + ); +} - for (meshes) |mesh| { - for (0..mesh.coords.mat.rows_num) |nn| { - const coord_cam = world_to_cam_mat.mulVec( - mesh.coords.getVec3(nn).sub(targ_world), - ); - if (is_first) { - max_abs_x = @abs(coord_cam.get(0)); - max_abs_y = @abs(coord_cam.get(1)); - is_first = false; - } else { - max_abs_x = @max(max_abs_x, @abs(coord_cam.get(0))); - max_abs_y = @max(max_abs_y, @abs(coord_cam.get(1))); - } - } - } +// -------------------------------------------------------------------------------------- +// Generic Low-Level Helpers +// -------------------------------------------------------------------------------------- - const fov_leng = [2]F{ - 2.0 * frame_fill * max_abs_x, - 2.0 * frame_fill * max_abs_y, +fn selectFitDist(image_dists: [2]F, fit_mode: FrameFitMode) F { + return switch (fit_mode) { + .contain => @max(image_dists[0], image_dists[1]), + .cover => @min(image_dists[0], image_dists[1]), + .horizontal => image_dists[0], + .vertical => image_dists[1], }; - const image_dists = imageDistFromFov( +} + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +test "coverageToFovScale and fovScaleToCoverage roundtrip" { + const coverage: F = 0.8; + const fov_scale = coverageToFovScale(coverage); + try std.testing.expectApproxEqRel(@as(F, 1.25), fov_scale, 1e-6); + const roundtrip = fovScaleToCoverage(fov_scale); + try std.testing.expectApproxEqRel(coverage, roundtrip, 1e-6); +} + +test "FrameFitMode distance selection" { + const dists = [2]F{ 100.0, 50.0 }; + try std.testing.expectEqual(@as(F, 100.0), selectFitDist(dists, .contain)); + try std.testing.expectEqual(@as(F, 50.0), selectFitDist(dists, .cover)); + try std.testing.expectEqual(@as(F, 100.0), selectFitDist(dists, .horizontal)); + try std.testing.expectEqual(@as(F, 50.0), selectFitDist(dists, .vertical)); +} + +test "posOrbitCam placement and rotation" { + const targ = vec.initVec3(F, 0.0, 0.0, 0.0); + const dist: F = 100.0; + const orbit = posOrbitCam(targ, 0.0, 0.0, dist); + try std.testing.expectApproxEqRel(@as(F, 100.0), orbit.pos.get(0), 1e-6); + try std.testing.expectApproxEqRel(@as(F, 0.0), orbit.pos.get(1), 1e-6); + try std.testing.expectApproxEqRel(@as(F, 0.0), orbit.pos.get(2), 1e-6); +} + +test "posStereoPair symmetric separation" { + const targ = vec.initVec3(F, 0.0, 0.0, 0.0); + const dist: F = 100.0; + const stereo_angle: F = std.math.pi / 6.0; + const stereo = posStereoPair(targ, dist, stereo_angle, 0.0); + const diff = (&stereo.cam0_pos).sub(stereo.cam1_pos); + const baseline = diff.vecLen(); + const expected_baseline = 2.0 * dist * @sin(0.5 * stereo_angle); + try std.testing.expectApproxEqRel(expected_baseline, baseline, 1e-5); +} + +test "posFrameCoords framing modes" { + var raw_coords = [_]F{ + -10.0, -5.0, 0.0, + 10.0, -5.0, 0.0, + 10.0, 5.0, 0.0, + -10.0, 5.0, 0.0, + }; + const coords = meshio.Coords.init(&raw_coords, 4); + const pixels_num = [2]u32{ 100, 100 }; + const pixels_size = [2]F{ 0.1, 0.1 }; + const focal_leng: F = 10.0; + const cam_rot = rotation.Rotation.init(0.0, 0.0, 0.0); + + const pos_contain = posFrameCoords( + &coords, pixels_num, pixels_size, focal_leng, - fov_leng, + cam_rot, + 1.0, + .contain, + ); + try std.testing.expectApproxEqRel(@as(F, 0.0), pos_contain.get(0), 1e-5); + try std.testing.expectApproxEqRel(@as(F, 0.0), pos_contain.get(1), 1e-5); + try std.testing.expectApproxEqRel(@as(F, 20.0), pos_contain.get(2), 1e-5); + + const pos_cover = posFrameCoords( + &coords, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + 1.0, + .cover, + ); + try std.testing.expectApproxEqRel(@as(F, 10.0), pos_cover.get(2), 1e-5); + + const pos_horiz = posFrameCoords( + &coords, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + 1.0, + .horizontal, + ); + try std.testing.expectApproxEqRel(@as(F, 20.0), pos_horiz.get(2), 1e-5); + + const pos_vert = posFrameCoords( + &coords, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + 1.0, + .vertical, + ); + try std.testing.expectApproxEqRel(@as(F, 10.0), pos_vert.get(2), 1e-5); +} + +test "posFrameCoordsTarg framing with offset target" { + var raw_coords = [_]F{ + 0.0, 0.0, 0.0, + 20.0, 0.0, 0.0, + }; + const coords = meshio.Coords.init(&raw_coords, 2); + const targ = vec.initVec3(F, 10.0, 0.0, 0.0); + const pixels_num = [2]u32{ 100, 100 }; + const pixels_size = [2]F{ 0.1, 0.1 }; + const focal_leng: F = 10.0; + const cam_rot = rotation.Rotation.init(0.0, 0.0, 0.0); + + const pos = posFrameCoordsTarg( + &coords, + targ, + pixels_num, + pixels_size, + focal_leng, + cam_rot, + 1.0, + .contain, ); - return @max(image_dists[0], image_dists[1]); + try std.testing.expectApproxEqRel(@as(F, 10.0), pos.get(0), 1e-5); + try std.testing.expectApproxEqRel(@as(F, 0.0), pos.get(1), 1e-5); + try std.testing.expectApproxEqRel(@as(F, 20.0), pos.get(2), 1e-5); } diff --git a/src/riley/zig/rasterconfig.zig b/src/riley/zig/rasterconfig.zig index 33c609dc..b9a62694 100644 --- a/src/riley/zig/rasterconfig.zig +++ b/src/riley/zig/rasterconfig.zig @@ -59,6 +59,13 @@ pub const RasterConfig = struct { tile_size_override: ?u16 = null, tile_size_min: u16 = 1, tile_size_max: u16 = 256, + buffer_mode: BufferMode = .tile_local, + global_subpx_tile_size_override: ?u16 = null, + global_subpx_tile_size_min: u16 = 64, + global_subpx_tile_size_max: u16 = 1024, + global_subpx_stripe_size_override: ?u16 = null, + global_subpx_stripe_size_min: u16 = 256, + global_subpx_stripe_size_max: u16 = 4096, // Test/development override for exercising the extended raster domain // without changing the camera PSF or resolve operation. raster_halo_px_override: ?u16 = null, @@ -71,6 +78,12 @@ pub const RasterConfig = struct { save_frame_buff_count: usize = buildconfig.SaveFrameBuffCount, }; +pub const BufferMode = enum { + tile_local, + global_subpx_full, + global_subpx_stripe, +}; + pub const RenderMode = enum { // Preserve timestep order. Geometry/raster work may run in parallel across // cameras, but later timesteps do not advance until the current timestep diff --git a/src/riley/zig/rasterengine_common.zig b/src/riley/zig/rasterengine_common.zig index c4dedbe9..e0c15775 100644 --- a/src/riley/zig/rasterengine_common.zig +++ b/src/riley/zig/rasterengine_common.zig @@ -24,6 +24,7 @@ const rops = @import("rasterops.zig"); const newton = @import("newton.zig"); const pce = @import("parachunkexec.zig"); const scratchresolve = @import("scratchresolve.zig"); +const subpxframe = @import("subpxframe.zig"); const scalingpolicy = @import("scalingpolicy.zig"); const mo = @import("meshpipeline.zig"); const MeshPrepared = mo.MeshPrepared; @@ -225,7 +226,7 @@ pub fn rasterDirectScalComm( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = global_subx, .global_suby = global_suby, }, @@ -295,6 +296,7 @@ pub fn rasterSceneComm( &worker_state.subpx_scratch, tile_rng_ctx.fields_num, tile_rng_ctx.subpx_tile_size, + true, ); } } @@ -358,6 +360,138 @@ pub fn rasterSceneComm( } } +pub fn rasterSceneGlobalComm( + comptime RasterBackend: type, + comptime report_mode: ReportMode, + outer_alloc: std.mem.Allocator, + io: std.Io, + ctx_rast: rops.RasterContext, + ctx_report: report.ReportContext(report_mode), + requested_workers: u16, + tiling: rops.TilingOverlaps, + meshes: []const MeshPrepared, + raster_hulls: []const ?NDArray(F), + target: *subpxframe.SubpxTarget, + image_out_arr: *NDArray(F), +) !usize { + if (tiling.active_tiles.len == 0) return 0; + + const WorkerState = comptime ThreadState(RasterBackend, report_mode); + const GlobalTileRangeCtx = struct { + io: std.Io, + ctx_rast: rops.RasterContext, + shared_log: *report.LogType(report_mode), + tiling: rops.TilingOverlaps, + meshes: []const MeshPrepared, + raster_hulls: []const ?NDArray(F), + target: *subpxframe.SubpxTarget, + image_out_arr: *NDArray(F), + worker_states: []WorkerState, + fields_num: u8, + subpx_tile_size: usize, + }; + const TileRangeWorkerAdapter = struct { + fn run( + ctx_ptr: *anyopaque, + worker_idx: usize, + range_start: usize, + range_end: usize, + ) anyerror!void { + const tile_rng_ctx: *GlobalTileRangeCtx = @ptrCast(@alignCast(ctx_ptr)); + const worker_state = &tile_rng_ctx.worker_states[worker_idx]; + const ctx_report_task = report.ReportContext(report_mode){ + .log = if (comptime report_mode == .full_stats) + tile_rng_ctx.shared_log + else + &worker_state.log, + }; + + for (range_start..range_end) |tile_idx| { + const tile = tile_rng_ctx.tiling.active_tiles[tile_idx]; + RasterBackend.configureTarget( + &worker_state.subpx_scratch, + tile_rng_ctx.target, + tile, + ); + try rasterTileRaw( + RasterBackend, + report_mode, + tile_rng_ctx.io, + tile_rng_ctx.ctx_rast, + ctx_report_task, + tile, + tile_rng_ctx.tiling.overlaps, + tile_rng_ctx.meshes, + tile_rng_ctx.raster_hulls, + tile_rng_ctx.image_out_arr, + &worker_state.subpx_scratch, + tile_rng_ctx.fields_num, + tile_rng_ctx.subpx_tile_size, + ); + worker_state.rasterized_tiles += 1; + } + } + }; + + const workers_num = scalingpolicy.rasterWorkers( + requested_workers, + tiling.active_tiles.len, + ); + var chunk_exec = pce.ParaChunkExecutor.init(io, @intCast(workers_num)); + var arena = std.heap.ArenaAllocator.init(outer_alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + const worker_states = try arena_alloc.alloc(WorkerState, workers_num); + var initialized_num: usize = 0; + errdefer for (worker_states[0..initialized_num]) |*worker_state| { + worker_state.deinit(); + }; + for (worker_states) |*worker_state| { + worker_state.* = try WorkerState.init( + arena_alloc, + ctx_rast, + image_out_arr.dims[0], + tileScratchSubpxSize(ctx_rast), + ); + initialized_num += 1; + } + defer for (worker_states) |*worker_state| worker_state.deinit(); + + var tile_range_ctx = GlobalTileRangeCtx{ + .io = io, + .ctx_rast = ctx_rast, + .shared_log = ctx_report.log, + .tiling = tiling, + .meshes = meshes, + .raster_hulls = raster_hulls, + .target = target, + .image_out_arr = image_out_arr, + .worker_states = worker_states, + .fields_num = @intCast(image_out_arr.dims[0]), + .subpx_tile_size = tileScratchSubpxSize(ctx_rast), + }; + try chunk_exec.runDynRangeWithWorkerErr( + &tile_range_ctx, + TileRangeWorkerAdapter.run, + tiling.active_tiles.len, + scalingpolicy.rasterGrainSize(tiling.active_tiles.len, workers_num), + ); + + if (comptime report_mode == .bench) { + if (report.getBenchLog(report_mode, ctx_report.log)) |bench_log| { + for (worker_states) |*worker_state| { + report.reduceBenchLog(bench_log, &worker_state.log); + } + } + } + var workers_used: usize = 0; + for (worker_states) |worker_state| { + if (worker_state.rasterized_tiles > 0) workers_used += 1; + } + return workers_used; +} + //------------------------------------------------------------------------------------------ // Direct Stepped Tri3 Fixed-Point Helpers //------------------------------------------------------------------------------------------ @@ -612,6 +746,7 @@ fn rasterTileComm( subpx_scratch: *RasterBackend.SubpxScratchBuffs, fields_num: u8, subpx_tile_size: usize, + resolve_tile: bool, ) !void { const tile_scope: ?rasterreport.TileScope = if (comptime report_mode == .full_stats) @@ -1031,7 +1166,9 @@ fn rasterTileComm( else null; - if (ctx_rast.camera.prep_psf.hasFilter()) { + if (!resolve_tile) { + // Global paths resolve frame or stripe storage after all raster tiles complete. + } else if (ctx_rast.camera.prep_psf.hasFilter()) { scratchresolve.resolveTileWithPSF( tile, sub_samp, @@ -1104,6 +1241,39 @@ fn rasterTileComm( ); } +pub fn rasterTileRaw( + comptime RasterBackend: type, + comptime report_mode: ReportMode, + io: std.Io, + ctx_rast: rops.RasterContext, + ctx_report: report.ReportContext(report_mode), + tile: rops.ActiveTile, + overlaps_all: []const rops.OverlapBBox, + meshes: []const MeshPrepared, + raster_hulls: []const ?NDArray(F), + image_out_arr: *NDArray(F), + subpx_scratch: *RasterBackend.SubpxScratchBuffs, + fields_num: u8, + subpx_tile_size: usize, +) !void { + try rasterTileComm( + RasterBackend, + report_mode, + io, + ctx_rast, + ctx_report, + tile, + overlaps_all, + meshes, + raster_hulls, + image_out_arr, + subpx_scratch, + fields_num, + subpx_tile_size, + false, + ); +} + //------------------------------------------------------------------------------------------ // Scene Raster Execution Helpers //------------------------------------------------------------------------------------------ @@ -1128,6 +1298,7 @@ fn ThreadState( arena: std.heap.ArenaAllocator, subpx_scratch: RasterBackend.SubpxScratchBuffs, log: report.LogType(report_mode), + rasterized_tiles: usize = 0, fn init( outer_alloc: std.mem.Allocator, diff --git a/src/riley/zig/rasterengine_scalar.zig b/src/riley/zig/rasterengine_scalar.zig index fddb38c1..747f3ff2 100644 --- a/src/riley/zig/rasterengine_scalar.zig +++ b/src/riley/zig/rasterengine_scalar.zig @@ -42,6 +42,8 @@ const shadekerns = @import("shaderkernels.zig"); // -------------------------------------------------------------------------------------- pub const SubpxScratchBuffs = struct { + pub const exclusive_subpx_target = false; + stride_subpx: usize, inv_z: []F, image: MatSlice(F), @@ -49,6 +51,13 @@ pub const SubpxScratchBuffs = struct { touched_min_x: []usize, touched_max_x: []usize, ideal_pix_cent: []F, + + pub inline fn imageIndex( + _: *const SubpxScratchBuffs, + local_idx: usize, + ) usize { + return local_idx; + } }; // -------------------------------------------------------------------------------------- @@ -143,6 +152,15 @@ pub fn RasterEngine( comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, +) type { + return RasterEngineFor(SubpxScratchBuffs, Geom, ShaderKern, ShaderData); +} + +pub fn RasterEngineFor( + comptime ScratchBuffs: type, + comptime Geom: type, + comptime ShaderKern: type, + comptime ShaderData: type, ) type { return struct { pub fn render( @@ -155,7 +173,7 @@ pub fn RasterEngine( raster_hull: ?*const NDArray(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const sub_samp_u: usize = @intCast(ctx_rast.camera.sub_sample); const sub_samp_f: F = @as(F, @floatFromInt(ctx_rast.camera.sub_sample)); @@ -223,10 +241,11 @@ pub fn RasterEngine( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { if (comptime Geom == geomkerns.Tri3OptKernel()) { return rasterSteppedScal( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -246,6 +265,7 @@ pub fn RasterEngine( } if (comptime Geom.solver_kind != .newton) { return rasterDirectImpl( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -265,6 +285,7 @@ pub fn RasterEngine( } return rasterNewtonImpl( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -295,9 +316,10 @@ pub fn RasterEngine( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { return rasterNewtonImpl( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -319,6 +341,7 @@ pub fn RasterEngine( } fn rasterDirectImpl( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -333,7 +356,7 @@ fn rasterDirectImpl( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { std.debug.assert(subpx_scratch.image.rows_num <= std.math.maxInt(u8)); const fields_num: u8 = @intCast(subpx_scratch.image.rows_num); @@ -343,7 +366,7 @@ fn rasterDirectImpl( ShaderKern, ShaderData, report_mode, - SubpxScratchBuffs, + ScratchBuffs, ctx_rast, ctx_report, tile, @@ -359,6 +382,7 @@ fn rasterDirectImpl( } fn rasterNewtonImpl( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -373,7 +397,7 @@ fn rasterNewtonImpl( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { comptime { if (Geom.solver_kind != .newton) { @@ -620,8 +644,14 @@ fn rasterNewtonImpl( global_subx, global_suby, result.iters, - tile.scratch_x_px_min + scratch_x / sub_samp, - tile.scratch_y_px_min + scratch_y / sub_samp, + @intCast(@max(0, tile.scratch_x_px_min + @as( + i32, + @intCast(scratch_x / sub_samp), + ))), + @intCast(@max(0, tile.scratch_y_px_min + @as( + i32, + @intCast(scratch_y / sub_samp), + ))), ); } @@ -630,7 +660,7 @@ fn rasterNewtonImpl( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = global_subx, .global_suby = global_suby, }; @@ -657,6 +687,7 @@ fn rasterNewtonImpl( } fn rasterSteppedScal( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -671,7 +702,7 @@ fn rasterSteppedScal( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const sub_samp: usize = @intCast(ctx_rast.camera.sub_sample); const tile_subpx_x = @as(isize, tile.scratch_x_px_min) * @@ -694,6 +725,7 @@ fn rasterSteppedScal( max_y_steps, )) |fixed| { return rasterSteppedScalFixP( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -714,6 +746,7 @@ fn rasterSteppedScal( } return rasterSteppedScalFloat( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -733,6 +766,7 @@ fn rasterSteppedScal( } fn rasterSteppedScalFixP( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -747,7 +781,7 @@ fn rasterSteppedScalFixP( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, fixed: comm.Tri3FixedEdges, ) !u64 { const N = Geom.nodes_num; @@ -850,8 +884,14 @@ fn rasterSteppedScalFixP( global_subx, global_suby, 1, - tile.scratch_x_px_min + scratch_x_u / sub_samp, - tile.scratch_y_px_min + scratch_y_u / sub_samp, + @intCast(@max(0, tile.scratch_x_px_min + @as( + i32, + @intCast(scratch_x_u / sub_samp), + ))), + @intCast(@max(0, tile.scratch_y_px_min + @as( + i32, + @intCast(scratch_y_u / sub_samp), + ))), ); } @@ -882,7 +922,7 @@ fn rasterSteppedScalFixP( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = global_subx, .global_suby = global_suby, }; @@ -930,6 +970,7 @@ fn rasterSteppedScalFixP( } fn rasterSteppedScalFloat( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -944,7 +985,7 @@ fn rasterSteppedScalFloat( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const N = Geom.nodes_num; var shaded_px: u64 = 0; @@ -1089,8 +1130,14 @@ fn rasterSteppedScalFloat( global_subx, global_suby, 1, - tile.scratch_x_px_min + scratch_x_u / sub_samp, - tile.scratch_y_px_min + scratch_y_u / sub_samp, + @intCast(@max(0, tile.scratch_x_px_min + @as( + i32, + @intCast(scratch_x_u / sub_samp), + ))), + @intCast(@max(0, tile.scratch_y_px_min + @as( + i32, + @intCast(scratch_y_u / sub_samp), + ))), ); } @@ -1121,7 +1168,7 @@ fn rasterSteppedScalFloat( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = global_subx, .global_suby = global_suby, }; diff --git a/src/riley/zig/rasterengine_simd.zig b/src/riley/zig/rasterengine_simd.zig index a4e34e0a..4e5267ba 100644 --- a/src/riley/zig/rasterengine_simd.zig +++ b/src/riley/zig/rasterengine_simd.zig @@ -53,6 +53,8 @@ const shadekerns = @import("shaderkernels.zig"); // Public Constants & Public Types // -------------------------------------------------------------------------------------- pub const SubpxScratchBuffs = struct { + pub const exclusive_subpx_target = false; + stride_subpx: usize, inv_z: []align(64) F, image: MatSlice(F), @@ -64,9 +66,16 @@ pub const SubpxScratchBuffs = struct { touched_min_x: []usize, touched_max_x: []usize, ideal_pix_cent: []align(64) F, + + pub inline fn imageIndex( + _: *const SubpxScratchBuffs, + local_idx: usize, + ) usize { + return local_idx; + } }; -const SubpxSimdChunk = struct { +pub const SubpxSimdChunk = struct { scratch_x_u: [S]usize, scratch_y_u: [S]usize, px_f: [S]F, @@ -211,6 +220,15 @@ pub fn RasterEngine( comptime Geom: type, // geometrykernels.zig comptime ShaderKern: type, // shaderkernels.zig comptime ShaderData: type, // shaderops_common.zig, ShaderPrepared +) type { + return RasterEngineFor(SubpxScratchBuffs, Geom, ShaderKern, ShaderData); +} + +pub fn RasterEngineFor( + comptime ScratchBuffs: type, + comptime Geom: type, + comptime ShaderKern: type, + comptime ShaderData: type, ) type { return struct { pub fn render( @@ -223,7 +241,7 @@ pub fn RasterEngine( raster_hull: ?*const NDArray(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const sub_samp_u: usize = @intCast(ctx_rast.camera.sub_sample); const sub_samp_f: F = @as(F, @floatFromInt(ctx_rast.camera.sub_sample)); @@ -263,6 +281,7 @@ pub fn RasterEngine( const shaded_px = if (comptime Geom == geomkerns.Tri3OptKernel()) try rasterSteppedSIMD( + ScratchBuffs, Geom, ShaderKern, report_mode, @@ -344,9 +363,10 @@ pub fn RasterEngine( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { return rasterDirectSIMDImpl( + ScratchBuffs, Geom, ShaderKern, report_mode, @@ -379,9 +399,10 @@ pub fn RasterEngine( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { return rasterNewtonSIMDImpl( + ScratchBuffs, Geom, ShaderKern, report_mode, @@ -414,9 +435,10 @@ pub fn RasterEngine( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { return rasterDirectImpl( + ScratchBuffs, Geom, ShaderKern, ShaderData, @@ -438,6 +460,7 @@ pub fn RasterEngine( } fn rasterDirectSIMDImpl( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime report_mode: ReportMode, @@ -451,7 +474,7 @@ fn rasterDirectSIMDImpl( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const N = Geom.nodes_num; var shaded_px: u64 = 0; @@ -569,7 +592,7 @@ fn rasterDirectSIMDImpl( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = comm.globalSubpxForReport( tile.scratch_x_px_min, sub_samp, @@ -581,6 +604,7 @@ fn rasterDirectSIMDImpl( scratch_y_u, ), .v_mask_active = v_depth_mask, + .exclusive_subpx_target = ScratchBuffs.exclusive_subpx_target, }; ShaderKern.shadeSIMD( @@ -603,6 +627,7 @@ fn rasterDirectSIMDImpl( } fn rasterNewtonSIMDImpl( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime report_mode: ReportMode, @@ -617,7 +642,7 @@ fn rasterNewtonSIMDImpl( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const N = Geom.nodes_num; var shaded_px: u64 = 0; @@ -966,7 +991,7 @@ fn rasterNewtonSIMDImpl( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = comm.globalSubpxForReport( tile.scratch_x_px_min, sub_samp, @@ -978,6 +1003,7 @@ fn rasterNewtonSIMDImpl( scratch_y_u, ), .v_mask_active = v_depth_mask, + .exclusive_subpx_target = ScratchBuffs.exclusive_subpx_target, }; ShaderKern.shadeSIMD( @@ -1001,6 +1027,7 @@ fn rasterNewtonSIMDImpl( } fn rasterDirectImpl( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime ShaderData: type, @@ -1015,7 +1042,7 @@ fn rasterDirectImpl( nodes_coords: Vec3Slices(F), shader: *const ShaderData, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { std.debug.assert(subpx_scratch.image.rows_num <= std.math.maxInt(u8)); const fields_num: u8 = @intCast(subpx_scratch.image.rows_num); @@ -1024,7 +1051,7 @@ fn rasterDirectImpl( ShaderKern, ShaderData, report_mode, - SubpxScratchBuffs, + ScratchBuffs, ctx_rast, ctx_report, tile, @@ -1040,6 +1067,7 @@ fn rasterDirectImpl( } fn rasterSteppedSIMD( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime report_mode: ReportMode, @@ -1053,7 +1081,7 @@ fn rasterSteppedSIMD( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { const sub_samp: usize = @intCast(ctx_rast.camera.sub_sample); const tile_subpx_x = @as(isize, tile.scratch_x_px_min) * @@ -1077,6 +1105,7 @@ fn rasterSteppedSIMD( max_y_steps, )) |fixed| { return rasterSteppedSIMDFixP( + ScratchBuffs, Geom, ShaderKern, report_mode, @@ -1096,6 +1125,7 @@ fn rasterSteppedSIMD( } return rasterSteppedSIMDFloat( + ScratchBuffs, Geom, ShaderKern, report_mode, @@ -1114,6 +1144,7 @@ fn rasterSteppedSIMD( } fn rasterSteppedSIMDFixP( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime report_mode: ReportMode, @@ -1127,7 +1158,7 @@ fn rasterSteppedSIMDFixP( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, fixed: comm.Tri3FixedEdges, ) !u64 { var shaded_px: u64 = 0; @@ -1331,7 +1362,7 @@ fn rasterSteppedSIMDFixP( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = comm.globalSubpxForReport( tile.scratch_x_px_min, sub_samp, @@ -1343,6 +1374,7 @@ fn rasterSteppedSIMDFixP( scratch_y_u, ), .v_mask_active = v_depth_mask, + .exclusive_subpx_target = ScratchBuffs.exclusive_subpx_target, }; const v_weights = [3]VecSF{ v_w0, v_w1, v_w2 }; @@ -1368,6 +1400,7 @@ fn rasterSteppedSIMDFixP( } fn rasterSteppedSIMDFloat( + comptime ScratchBuffs: type, comptime Geom: type, comptime ShaderKern: type, comptime report_mode: ReportMode, @@ -1381,7 +1414,7 @@ fn rasterSteppedSIMDFloat( nodes_coords: Vec3Slices(F), shader: anytype, shader_buf: *const shaderops.LocalShaderBuff(Geom.nodes_num), - subpx_scratch: *SubpxScratchBuffs, + subpx_scratch: *ScratchBuffs, ) !u64 { var shaded_px: u64 = 0; const sub_samp: usize = @intCast(ctx_rast.camera.sub_sample); @@ -1600,7 +1633,7 @@ fn rasterSteppedSIMDFloat( .elem_idx = overlap.elem_idx, .fields_num = fields_num, .actual_fields = fields_num, - .scratch_idx = scratch_idx, + .scratch_idx = subpx_scratch.imageIndex(scratch_idx), .global_subx = comm.globalSubpxForReport( tile.scratch_x_px_min, sub_samp, @@ -1612,6 +1645,7 @@ fn rasterSteppedSIMDFloat( scratch_y_u, ), .v_mask_active = v_depth_mask, + .exclusive_subpx_target = ScratchBuffs.exclusive_subpx_target, }; ShaderKern.shadeSIMD( diff --git a/src/riley/zig/rasterengineglobal.zig b/src/riley/zig/rasterengineglobal.zig new file mode 100644 index 00000000..3cff9c20 --- /dev/null +++ b/src/riley/zig/rasterengineglobal.zig @@ -0,0 +1,15 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const cfg = @import("buildconfig.zig").config; +const impl = if (cfg.simd == .on) + @import("rasterengineglobal_simd.zig") +else + @import("rasterengineglobal_scalar.zig"); + +pub const rasterScene = impl.rasterScene; diff --git a/src/riley/zig/rasterengineglobal_common.zig b/src/riley/zig/rasterengineglobal_common.zig new file mode 100644 index 00000000..a0856615 --- /dev/null +++ b/src/riley/zig/rasterengineglobal_common.zig @@ -0,0 +1,51 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const ndarray = @import("ndarray.zig"); +const rops = @import("rasterops.zig"); +const report = @import("report.zig"); +const rasterengine = @import("rasterengine_common.zig"); +const mo = @import("meshpipeline.zig"); +const subpxframe = @import("subpxframe.zig"); + +// -------------------------------------------------------------------------------------- +// Public Entry-Point Func +// -------------------------------------------------------------------------------------- + +pub fn rasterScene( + comptime RasterBackend: type, + comptime report_mode: report.ReportMode, + outer_alloc: std.mem.Allocator, + io: std.Io, + ctx_rast: rops.RasterContext, + ctx_report: report.ReportContext(report_mode), + requested_workers: u16, + tiling: rops.TilingOverlaps, + meshes: []const mo.MeshPrepared, + raster_hulls: []const ?ndarray.NDArray(F), + target: *subpxframe.SubpxTarget, + image_out_arr: *ndarray.NDArray(F), +) !usize { + return rasterengine.rasterSceneGlobalComm( + RasterBackend, + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + requested_workers, + tiling, + meshes, + raster_hulls, + target, + image_out_arr, + ); +} diff --git a/src/riley/zig/rasterengineglobal_scalar.zig b/src/riley/zig/rasterengineglobal_scalar.zig new file mode 100644 index 00000000..44cc603c --- /dev/null +++ b/src/riley/zig/rasterengineglobal_scalar.zig @@ -0,0 +1,140 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const ndarray = @import("ndarray.zig"); +const rops = @import("rasterops.zig"); +const report = @import("report.zig"); +const backend = @import("rasterengine_scalar.zig"); +const common = @import("rasterengineglobal_common.zig"); +const mo = @import("meshpipeline.zig"); +const subpxframe = @import("subpxframe.zig"); + +const GlobalSubpxScratchBuffs = struct { + pub const exclusive_subpx_target = true; + + stride_subpx: usize, + inv_z: []F, + image: @import("matslice.zig").MatSlice(F), + filter_tmp: @import("matslice.zig").MatSlice(F), + touched_min_x: []usize, + touched_max_x: []usize, + ideal_pix_cent: []F, + target_stride_subpx: usize = 0, + target_subx_min: i32 = 0, + target_suby_min: i32 = 0, + tile_subx_min: i32 = 0, + tile_suby_min: i32 = 0, + + pub inline fn imageIndex( + self: *const GlobalSubpxScratchBuffs, + local_idx: usize, + ) usize { + const local_subx = local_idx % self.stride_subpx; + const local_suby = local_idx / self.stride_subpx; + const global_subx = self.tile_subx_min + @as(i32, @intCast(local_subx)); + const global_suby = self.tile_suby_min + @as(i32, @intCast(local_suby)); + const target_subx = global_subx - self.target_subx_min; + const target_suby = global_suby - self.target_suby_min; + return @as(usize, @intCast(target_suby)) * self.target_stride_subpx + + @as(usize, @intCast(target_subx)); + } +}; + +pub const GlobalBackend = struct { + pub const SubpxScratchBuffs = GlobalSubpxScratchBuffs; + + pub fn initSubpxScratch( + arena_alloc: std.mem.Allocator, + fields_num: u8, + subpx_tile_size: usize, + ) !GlobalSubpxScratchBuffs { + const total = subpx_tile_size * subpx_tile_size; + const image_mem = try arena_alloc.alloc(F, 0); + return .{ + .stride_subpx = subpx_tile_size, + .inv_z = try arena_alloc.alloc(F, total), + .image = @import("matslice.zig").MatSlice(F).init(image_mem, fields_num, 0), + .filter_tmp = @import("matslice.zig").MatSlice(F).init( + image_mem, + fields_num, + 0, + ), + .touched_min_x = try arena_alloc.alloc(usize, subpx_tile_size), + .touched_max_x = try arena_alloc.alloc(usize, subpx_tile_size), + .ideal_pix_cent = try arena_alloc.alloc(F, total * 2), + }; + } + + pub fn resetSubpxScratch( + scratch: *GlobalSubpxScratchBuffs, + subpx_tile_size: usize, + _: F, + ) void { + @memset(scratch.inv_z, -std.math.inf(F)); + @memset(scratch.touched_min_x, subpx_tile_size); + @memset(scratch.touched_max_x, 0); + } + + pub fn configureTarget( + scratch: *GlobalSubpxScratchBuffs, + target: *subpxframe.SubpxTarget, + tile: rops.ActiveTile, + ) void { + scratch.image = target.image; + scratch.target_stride_subpx = target.domain.storage_w_subpx; + scratch.target_subx_min = target.global_subx_min; + scratch.target_suby_min = target.global_suby_min; + scratch.tile_subx_min = tile.scratch_subx_min; + scratch.tile_suby_min = tile.scratch_suby_min; + } + + pub fn RasterEngine( + comptime Geom: type, + comptime ShaderKern: type, + comptime ShaderData: type, + ) type { + return backend.RasterEngineFor( + GlobalSubpxScratchBuffs, + Geom, + ShaderKern, + ShaderData, + ); + } +}; + +pub fn rasterScene( + comptime report_mode: report.ReportMode, + outer_alloc: std.mem.Allocator, + io: std.Io, + ctx_rast: rops.RasterContext, + ctx_report: report.ReportContext(report_mode), + requested_workers: u16, + tiling: rops.TilingOverlaps, + meshes: []const mo.MeshPrepared, + raster_hulls: []const ?ndarray.NDArray(F), + target: *subpxframe.SubpxTarget, + image_out_arr: *ndarray.NDArray(F), +) !usize { + return common.rasterScene( + GlobalBackend, + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + requested_workers, + tiling, + meshes, + raster_hulls, + target, + image_out_arr, + ); +} diff --git a/src/riley/zig/rasterengineglobal_simd.zig b/src/riley/zig/rasterengineglobal_simd.zig new file mode 100644 index 00000000..db8d10b3 --- /dev/null +++ b/src/riley/zig/rasterengineglobal_simd.zig @@ -0,0 +1,147 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const ndarray = @import("ndarray.zig"); +const rops = @import("rasterops.zig"); +const report = @import("report.zig"); +const backend = @import("rasterengine_simd.zig"); +const common = @import("rasterengineglobal_common.zig"); +const mo = @import("meshpipeline.zig"); +const subpxframe = @import("subpxframe.zig"); + +const GlobalSubpxScratchBuffs = struct { + pub const exclusive_subpx_target = true; + + stride_subpx: usize, + inv_z: []align(64) F, + image: @import("matslice.zig").MatSlice(F), + filter_tmp: @import("matslice.zig").MatSlice(F), + simd_chunks: []backend.SubpxSimdChunk, + mask: []align(64) bool, + xi: []align(64) F, + eta: []align(64) F, + touched_min_x: []usize, + touched_max_x: []usize, + ideal_pix_cent: []align(64) F, + target_stride_subpx: usize = 0, + target_subx_min: i32 = 0, + target_suby_min: i32 = 0, + tile_subx_min: i32 = 0, + tile_suby_min: i32 = 0, + + pub inline fn imageIndex( + self: *const GlobalSubpxScratchBuffs, + local_idx: usize, + ) usize { + const local_subx = local_idx % self.stride_subpx; + const local_suby = local_idx / self.stride_subpx; + const global_subx = self.tile_subx_min + @as(i32, @intCast(local_subx)); + const global_suby = self.tile_suby_min + @as(i32, @intCast(local_suby)); + const target_subx = global_subx - self.target_subx_min; + const target_suby = global_suby - self.target_suby_min; + return @as(usize, @intCast(target_suby)) * self.target_stride_subpx + + @as(usize, @intCast(target_subx)); + } +}; + +const GlobalBackend = struct { + pub const SubpxScratchBuffs = GlobalSubpxScratchBuffs; + + pub fn initSubpxScratch( + arena_alloc: std.mem.Allocator, + fields_num: u8, + subpx_tile_size: usize, + ) !GlobalSubpxScratchBuffs { + const local = try backend.initSubpxScratch( + arena_alloc, + fields_num, + subpx_tile_size, + ); + return .{ + .stride_subpx = local.stride_subpx, + .inv_z = local.inv_z, + .image = local.image, + .filter_tmp = local.filter_tmp, + .simd_chunks = local.simd_chunks, + .mask = local.mask, + .xi = local.xi, + .eta = local.eta, + .touched_min_x = local.touched_min_x, + .touched_max_x = local.touched_max_x, + .ideal_pix_cent = local.ideal_pix_cent, + }; + } + + pub fn resetSubpxScratch( + scratch: *GlobalSubpxScratchBuffs, + subpx_tile_size: usize, + _: F, + ) void { + @memset(scratch.inv_z, -std.math.inf(F)); + @memset(scratch.touched_min_x, subpx_tile_size); + @memset(scratch.touched_max_x, 0); + } + + pub fn configureTarget( + scratch: *GlobalSubpxScratchBuffs, + target: *subpxframe.SubpxTarget, + tile: rops.ActiveTile, + ) void { + scratch.image = target.image; + scratch.target_stride_subpx = target.domain.storage_w_subpx; + scratch.target_subx_min = target.global_subx_min; + scratch.target_suby_min = target.global_suby_min; + scratch.tile_subx_min = tile.scratch_subx_min; + scratch.tile_suby_min = tile.scratch_suby_min; + } + + pub fn RasterEngine( + comptime Geom: type, + comptime ShaderKern: type, + comptime ShaderData: type, + ) type { + return backend.RasterEngineFor( + GlobalSubpxScratchBuffs, + Geom, + ShaderKern, + ShaderData, + ); + } +}; + +pub fn rasterScene( + comptime report_mode: report.ReportMode, + outer_alloc: std.mem.Allocator, + io: std.Io, + ctx_rast: rops.RasterContext, + ctx_report: report.ReportContext(report_mode), + requested_workers: u16, + tiling: rops.TilingOverlaps, + meshes: []const mo.MeshPrepared, + raster_hulls: []const ?ndarray.NDArray(F), + target: *subpxframe.SubpxTarget, + image_out_arr: *ndarray.NDArray(F), +) !usize { + return common.rasterScene( + GlobalBackend, + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + requested_workers, + tiling, + meshes, + raster_hulls, + target, + image_out_arr, + ); +} diff --git a/src/riley/zig/rasterops.zig b/src/riley/zig/rasterops.zig index ef56b9a5..4d3a2d55 100644 --- a/src/riley/zig/rasterops.zig +++ b/src/riley/zig/rasterops.zig @@ -529,6 +529,16 @@ pub const ActiveTile = struct { scratch_y_px_min: i32, scratch_x_px_max: i32, scratch_y_px_max: i32, + // Global sub-pixel modes populate these exact target ownership bounds. + // Tile-local modes do not use them. + core_subx_min: i32 = 0, + core_suby_min: i32 = 0, + core_subx_max: i32 = 0, + core_suby_max: i32 = 0, + scratch_subx_min: i32 = 0, + scratch_suby_min: i32 = 0, + scratch_subx_max: i32 = 0, + scratch_suby_max: i32 = 0, }; pub const TilingOverlaps = struct { diff --git a/src/riley/zig/report.zig b/src/riley/zig/report.zig index 990aa18e..70e7e772 100644 --- a/src/riley/zig/report.zig +++ b/src/riley/zig/report.zig @@ -25,6 +25,31 @@ pub const ReportMode = rastcfg.ReportMode; pub const OffLog = struct {}; +pub const GlobalSubpxStats = struct { + mode: rastcfg.BufferMode = .tile_local, + output_w_subpx: usize = 0, + output_h_subpx: usize = 0, + outer_halo_subpx: usize = 0, + tile_core_subpx: usize = 0, + tile_scratch_subpx: usize = 0, + tile_grid_count: usize = 0, + active_tile_count: usize = 0, + overlap_refs_total: usize = 0, + overlap_refs_max: usize = 0, + stripe_core_subpx: usize = 0, + stripe_count: usize = 0, + final_stripe_core_subpx: usize = 0, + stripe_storage_w_subpx: usize = 0, + stripe_storage_h_subpx: usize = 0, + stripe_storage_samples_cleared: u64 = 0, +}; + +pub const GlobalSubpxTimes = struct { + buffer_setup: F = 0, + tile_raster: F = 0, + resolve: F = 0, +}; + pub const FrameTimes = struct { setup_frame_buff: F = 0, prepare_frame_context: F = 0, @@ -35,14 +60,30 @@ pub const FrameTimes = struct { geom_remap_inds: F = 0, tile_overlap: F = 0, raster_loop: F = 0, + raster_workers_requested: u16 = 0, + raster_workers_used: u16 = 0, + resolve_workers_requested: u16 = 0, + resolve_workers_used: u16 = 0, cam_invert: F = 0, elem_loop: F = 0, scratch_resolve: F = 0, + global_subpx_times: GlobalSubpxTimes = .{}, + global_subpx_stats: ?GlobalSubpxStats = null, save_frame: F = 0, active_time: F = 0, latency_time: F = 0, }; +pub fn rasterStageTime(frame_times: FrameTimes) F { + if (frame_times.global_subpx_stats != null) { + return frame_times.tile_overlap + + frame_times.global_subpx_times.buffer_setup + + frame_times.global_subpx_times.tile_raster + + frame_times.global_subpx_times.resolve; + } + return frame_times.raster_loop; +} + pub const EndToEndTimes = struct { setup_time: F = 0, setup_other_time: F = 0, @@ -1750,6 +1791,18 @@ pub fn standardReport( nodes_per_elem: F, bench_log: *const BenchLog, ) !void { + if (frame_times.global_subpx_stats) |stats| { + return globalSubpxStandardReport( + io, + camera, + frame_idx, + camera_idx, + frame_times, + stats, + out_dir_path, + bench_log, + ); + } var buff: [4096]u8 = undefined; var stdout_writer = std.Io.File.stdout().writer(io, &buff); const writer = &stdout_writer.interface; @@ -1786,7 +1839,7 @@ pub fn standardReport( const print_break = [_]u8{'='} ** 80; const print_break_inner = [_]u8{'-'} ** 80; - try writer.print("\n{s}\nRaster Frame Times: Frame {d}, Camera {d}\n{s}\n", .{ + try writer.print("{s}\nRILEY TILE LOCAL RASTER: Frame {d}, Camera {d}\n{s}\n", .{ print_break, frame_idx, camera_idx, @@ -1892,17 +1945,163 @@ pub fn standardReport( try writer.print("Raster Throughput = {d:.2} MPx/s\n", .{mpx_sec}); try writer.print("Active Frame Throughput = {d:.2} MPx/s\n", .{frame_mpx_sec}); - try writer.print("{s}\n", .{print_break}); - try writer.print("Frame Output Path =\n", .{}); if (out_dir_path) |path| { - try writer.print(" {s}\n", .{path}); + try writer.print("Output Frame Path = {s}\n", .{path}); } else { - try writer.print(" not written (memory output)\n", .{}); + try writer.print("Output Frame Path = not written (memory output)\n", .{}); } try writer.print("{s}\n", .{print_break}); try writer.flush(); } +fn globalSubpxStandardReport( + io: std.Io, + camera: *const cam.CameraPrepared, + frame_idx: usize, + camera_idx: usize, + frame_times: FrameTimes, + stats: GlobalSubpxStats, + out_dir_path: ?[]const u8, + bench_log: *const BenchLog, +) !void { + var buff: [4096]u8 = undefined; + var stdout_writer = std.Io.File.stdout().writer(io, &buff); + const writer = &stdout_writer.interface; + const stats_break = [_]u8{'='} ** 80; + const section_break = [_]u8{'-'} ** 80; + const conv: F = 1.0 / 1.0e6; + const output_samples = @as(F, @floatFromInt( + stats.output_w_subpx * stats.output_h_subpx, + )); + const output_px = output_samples / @as(F, @floatFromInt( + camera.sub_sample * camera.sub_sample, + )); + const active_tiles = @as(F, @floatFromInt(stats.active_tile_count)); + const mean_refs = if (stats.active_tile_count > 0) + @as(F, @floatFromInt(stats.overlap_refs_total)) / active_tiles + else + 0.0; + const empty_tiles = stats.tile_grid_count -| stats.active_tile_count; + const buffer_planning = frame_times.tile_overlap + + frame_times.global_subpx_times.buffer_setup; + const raster_sec = frame_times.global_subpx_times.tile_raster / 1e9; + const resolve_sec = frame_times.global_subpx_times.resolve / 1e9; + const active_sec = frame_times.active_time / 1e9; + const shaded = @as(F, @floatFromInt(bench_log.total_shaded_px)); + const shade_rate = if (raster_sec > 0) + shaded / (raster_sec * 1e6) + else + 0.0; + const sample_rate = if (raster_sec > 0) + output_samples / (raster_sec * 1e6) + else + 0.0; + const resolve_rate = if (resolve_sec > 0) + output_px / (resolve_sec * 1e6) + else + 0.0; + const active_rate = if (active_sec > 0) + output_px / (active_sec * 1e6) + else + 0.0; + + try writer.print("{s}\nRILEY GLOBAL SUBPIXEL RASTER: Frame {d}, Camera {d}\n{s}\n", .{ + stats_break, + frame_idx, + camera_idx, + stats_break, + }); + try writer.print("Mode = {s}\n", .{@tagName(stats.mode)}); + try writer.print("Output Core = {d} x {d} px | {d} x {d} subpx\n", .{ + camera.pixels_num[0], + camera.pixels_num[1], + stats.output_w_subpx, + stats.output_h_subpx, + }); + try writer.print("Subsample = {d} x {d}\n", .{ + camera.sub_sample, + camera.sub_sample, + }); + try writer.print("PSF / Support = {s} | {d} px\n", .{ + @tagName(camera.psf), + camera.prep_psf.halo_px, + }); + try writer.print("{s}\nTILE DOMAIN\n", .{section_break}); + try writer.print("Tile Core = {d} x {d} subpx\n", .{ + stats.tile_core_subpx, + stats.tile_core_subpx, + }); + try writer.print("Interior Tile Halo = 0 x 0 px\n", .{}); + try writer.print("Outer Frame Halo = {d} x {d} subpx\n", .{ + stats.outer_halo_subpx, + stats.outer_halo_subpx, + }); + try writer.print("Scratch Capacity = {d} x {d} subpx\n", .{ + stats.tile_scratch_subpx, + stats.tile_scratch_subpx, + }); + try writer.print("Raster Workers (Req / Used) = {d} / {d}\n", .{ + frame_times.raster_workers_requested, + frame_times.raster_workers_used, + }); + try writer.print("Resolve Workers (Req / Used) = {d} / {d}\n", .{ + frame_times.resolve_workers_requested, + frame_times.resolve_workers_used, + }); + try writer.print("Tile Grid / Active / Empty = {d} / {d} / {d}\n", .{ + stats.tile_grid_count, + stats.active_tile_count, + empty_tiles, + }); + try writer.print("Overlap Refs / Active Tile = mean {d:.2} | max {d}\n", .{ + mean_refs, + stats.overlap_refs_max, + }); + if (stats.mode == .global_subpx_stripe) { + const repeated_storage = stats.stripe_storage_samples_cleared -| + stats.output_w_subpx * stats.output_h_subpx; + try writer.print("{s}\nSTRIPE DOMAIN\n", .{section_break}); + try writer.print("Core Height = {d} subpx\n", .{stats.stripe_core_subpx}); + try writer.print("Stripe Count / Final Height = {d} / {d} subpx\n", .{ + stats.stripe_count, + stats.final_stripe_core_subpx, + }); + try writer.print("Stripe Edge Halo = x: {d} | y: {d} subpx\n", .{ + stats.outer_halo_subpx, + stats.outer_halo_subpx, + }); + try writer.print("Peak Stripe Storage = {d} x {d} subpx\n", .{ + stats.stripe_storage_w_subpx, + stats.stripe_storage_h_subpx, + }); + try writer.print("Repeated Halo Storage = {d} samples\n", .{repeated_storage}); + } + try writer.print("{s}\nWALL-CLOCK TIMINGS\n", .{section_break}); + try writer.print("Global Buffer + Planning = {d:.3} ms\n", .{buffer_planning * conv}); + try writer.print("Global Tile Raster = {d:.3} ms\n", .{ + frame_times.global_subpx_times.tile_raster * conv, + }); + try writer.print("Global Resolve = {d:.3} ms\n", .{ + frame_times.global_subpx_times.resolve * conv, + }); + try writer.print("Save Frame = {d:.3} ms\n", .{frame_times.save_frame * conv}); + try writer.print("Active Frame = {d:.3} ms\n", .{frame_times.active_time * conv}); + try writer.print("Frame Latency = {d:.3} ms\n", .{frame_times.latency_time * conv}); + try writer.print("{s}\nRATES\n", .{section_break}); + try writer.print("Executed Shade Rate = {d:.3} Msubpx/s\n", .{shade_rate}); + try writer.print("Output Sample Rate = {d:.3} Msubpx/s\n", .{sample_rate}); + try writer.print("Global Resolve Rate = {d:.3} MPx/s\n", .{resolve_rate}); + try writer.print("Active Frame Rate = {d:.3} MPx/s\n", .{active_rate}); + try writer.print("{s}\n", .{section_break}); + if (out_dir_path) |path| { + try writer.print("Output Frame Path = {s}\n", .{path}); + } else { + try writer.print("Output Frame Path = not written (memory output)\n", .{}); + } + try writer.print("{s}\n", .{stats_break}); + try writer.flush(); +} + pub fn printRenderSummary( io: std.Io, cameras: []const cam.CameraPrepared, @@ -1922,15 +2121,21 @@ pub fn printRenderSummary( } total_pixels *= num_time; - const actual_tile_size = scalingpolicy.tileSize( - config.tile_size_override, - config.tile_size_min, - config.tile_size_max, - cameras[0].pixels_num, - cameras[0].sub_sample, - cameras[0].prep_psf.halo_px, - ); - _ = actual_tile_size; + const actual_tile_size = switch (config.buffer_mode) { + .tile_local => scalingpolicy.tileSize( + config.tile_size_override, + config.tile_size_min, + config.tile_size_max, + cameras[0].pixels_num, + cameras[0].sub_sample, + cameras[0].prep_psf.halo_px, + ), + .global_subpx_full, .global_subpx_stripe => @divExact( + config.global_subpx_tile_size_override orelse + config.global_subpx_tile_size_min, + @as(u16, @intCast(cameras[0].sub_sample)), + ), + }; const total_frames = cameras.len * num_time; const total_render_ms = end_to_end_times.total_time / 1e6; @@ -1951,7 +2156,7 @@ pub fn printRenderSummary( var total_raster_ns: F = 0.0; if (bench_capture) |capture| { for (capture) |frame_capture| { - total_raster_ns += frame_capture.bench_log.frame_times.raster_loop; + total_raster_ns += rasterStageTime(frame_capture.bench_log.frame_times); } } const total_raster_sec = total_raster_ns / 1e9; @@ -1965,14 +2170,23 @@ pub fn printRenderSummary( const writer = &stdout_writer.interface; const print_break = [_]u8{'='} ** 80; - try writer.print("\n{s}\nRiley Raster Render Summary\n{s}\n", .{ + try writer.print("{s}\nRILEY RASTER RENDER SUMMARY\n{s}\n", .{ print_break, print_break, }); - // try writer.print("Actual Tile Size = {d}x{d}\n", .{ - // actual_tile_size, - // actual_tile_size, - // }); + try writer.print("Buffer Mode = {s}\n", .{ + @tagName(config.buffer_mode), + }); + if (config.buffer_mode != .tile_local) { + const global_tile_subpx = @as(usize, actual_tile_size) * + @as(usize, cameras[0].sub_sample); + try writer.print("Global Raster Tile = {d} subpx\n", .{global_tile_subpx}); + if (config.buffer_mode == .global_subpx_stripe) { + const stripe_subpx = config.global_subpx_stripe_size_override orelse + config.global_subpx_stripe_size_min; + try writer.print("Global Stripe Height = {d} subpx\n", .{stripe_subpx}); + } + } try writer.print("Setup Time = {d:.3} ms\n", .{setup_ms}); // try writer.print("Setup other = {d:.3} ms\n", .{ // setup_other_ms, diff --git a/src/riley/zig/riley.zig b/src/riley/zig/riley.zig index 2f54c241..eabf09e8 100644 --- a/src/riley/zig/riley.zig +++ b/src/riley/zig/riley.zig @@ -30,9 +30,13 @@ const valinp = @import("validateinput.zig"); const geomkerns = @import("geometrykernels.zig"); const shadekerns = @import("shaderkernels.zig"); const rasterengine = @import("rasterengine.zig"); +const rasterengineglobal = @import("rasterengineglobal.zig"); +const scratchresolveglobal = @import("scratchresolveglobal.zig"); +const subpxframe = @import("subpxframe.zig"); const rastcfg = @import("rasterconfig.zig"); pub const RasterConfig = rastcfg.RasterConfig; +pub const BufferMode = rastcfg.BufferMode; pub const ImageSaveMode = rastcfg.ImageSaveMode; pub const SaveStrategy = rastcfg.SaveStrategy; pub const RenderMode = rastcfg.RenderMode; @@ -1012,25 +1016,145 @@ fn sceneTileOverlapBinning( ); const time_start_overlap = Timestamp.now(io, .awake); - ctx.tiling = try rops.sceneTileElemOverlap( - arena_alloc, - chunk_exec, - scalingpolicy.geometryWorkers(geom_workers), - ctx.actual_tile_size, - tiles_num_x, - tiles_num_y, - @intCast(job.camera.pixels_num[0]), - @intCast(job.camera.pixels_num[1]), - job.config.raster_halo_px_override orelse job.camera.prep_psf.halo_px, - ctx.elems_in_image_by_mesh, - ctx.elem_bboxes_by_mesh, - ); + ctx.tiling = if (job.config.buffer_mode == .tile_local) + try rops.sceneTileElemOverlap( + arena_alloc, + chunk_exec, + scalingpolicy.geometryWorkers(geom_workers), + ctx.actual_tile_size, + tiles_num_x, + tiles_num_y, + @intCast(job.camera.pixels_num[0]), + @intCast(job.camera.pixels_num[1]), + job.config.raster_halo_px_override orelse job.camera.prep_psf.halo_px, + ctx.elems_in_image_by_mesh, + ctx.elem_bboxes_by_mesh, + ) + else + try sceneGlobalTileElemOverlap( + arena_alloc, + ctx.actual_tile_size, + job.camera.sub_sample, + @intCast(job.camera.pixels_num[0]), + @intCast(job.camera.pixels_num[1]), + 0, + @intCast(job.camera.pixels_num[1]), + job.config.raster_halo_px_override orelse job.camera.prep_psf.halo_px, + ctx.elems_in_image_by_mesh, + ctx.elem_bboxes_by_mesh, + ); const time_end_overlap = Timestamp.now(io, .awake); ctx.frame_times.tile_overlap = @floatFromInt( time_start_overlap.durationTo(time_end_overlap).raw.nanoseconds, ); } +fn sceneGlobalTileElemOverlap( + outer_alloc: std.mem.Allocator, + tile_size: u16, + sub_sample: u32, + screen_px_x: u16, + screen_px_y: u16, + core_y_px_min: u16, + core_y_px_max: u16, + halo_px: u16, + elems_in_image_by_mesh: []const usize, + elem_bboxes_by_mesh: []const []rops.ElemBBox, +) !rops.TilingOverlaps { + const tiles_x = try std.math.divCeil(usize, screen_px_x, tile_size); + std.debug.assert(core_y_px_min < core_y_px_max); + std.debug.assert(core_y_px_max <= screen_px_y); + const tiles_y = try std.math.divCeil( + usize, + core_y_px_max - core_y_px_min, + tile_size, + ); + var tiles: std.ArrayList(rops.ActiveTile) = .empty; + defer tiles.deinit(outer_alloc); + var overlaps: std.ArrayList(rops.OverlapBBox) = .empty; + defer overlaps.deinit(outer_alloc); + const sub_samp: i32 = @intCast(sub_sample); + + for (0..tiles_y) |ty| { + const y_min: u16 = @intCast(core_y_px_min + ty * tile_size); + const y_max = @min( + core_y_px_max, + @as(u16, @intCast(core_y_px_min + (ty + 1) * tile_size)), + ); + for (0..tiles_x) |tx| { + const x_min: u16 = @intCast(tx * tile_size); + const x_max = @min(screen_px_x, @as(u16, @intCast((tx + 1) * tile_size))); + const scratch_x_min: i32 = @as(i32, x_min) - if (tx == 0) halo_px else 0; + const scratch_x_max: i32 = @as(i32, x_max) + + if (tx + 1 == tiles_x) halo_px else 0; + const scratch_y_min: i32 = @as(i32, y_min) - if (ty == 0) halo_px else 0; + const scratch_y_max: i32 = @as(i32, y_max) + + if (ty + 1 == tiles_y) halo_px else 0; + const overlap_start = overlaps.items.len; + + for (elem_bboxes_by_mesh, 0..) |elem_bboxes, mesh_idx| { + for (elem_bboxes[0..elems_in_image_by_mesh[mesh_idx]]) |elem_bbox| { + const overlap_x_min = @max(elem_bbox.x_min, scratch_x_min); + const overlap_x_max = @min(elem_bbox.x_max, scratch_x_max); + const overlap_y_min = @max(elem_bbox.y_min, scratch_y_min); + const overlap_y_max = @min(elem_bbox.y_max, scratch_y_max); + if (overlap_x_min >= overlap_x_max or + overlap_y_min >= overlap_y_max) + { + continue; + } + try overlaps.append(outer_alloc, .{ + .mesh_idx = mesh_idx, + .elem_idx = elem_bbox.elem_idx, + .x_min = overlap_x_min, + .x_max = overlap_x_max, + .y_min = overlap_y_min, + .y_max = overlap_y_max, + }); + } + } + if (overlaps.items.len == overlap_start) continue; + try tiles.append(outer_alloc, .{ + .overlap_start = overlap_start, + .overlap_count = overlaps.items.len - overlap_start, + .x_px_min = x_min, + .y_px_min = y_min, + .x_px_max = x_max, + .y_px_max = y_max, + .scratch_x_px_min = scratch_x_min, + .scratch_y_px_min = scratch_y_min, + .scratch_x_px_max = scratch_x_max, + .scratch_y_px_max = scratch_y_max, + .core_subx_min = @as(i32, x_min) * sub_samp, + .core_suby_min = @as(i32, y_min) * sub_samp, + .core_subx_max = @as(i32, x_max) * sub_samp, + .core_suby_max = @as(i32, y_max) * sub_samp, + .scratch_subx_min = scratch_x_min * sub_samp, + .scratch_suby_min = scratch_y_min * sub_samp, + .scratch_subx_max = scratch_x_max * sub_samp, + .scratch_suby_max = scratch_y_max * sub_samp, + }); + } + } + return .{ + .active_tiles = try tiles.toOwnedSlice(outer_alloc), + .overlaps = try overlaps.toOwnedSlice(outer_alloc), + }; +} + +fn recordGlobalTilingStats( + stats: *report.GlobalSubpxStats, + tiling: rops.TilingOverlaps, + tile_grid_count: usize, +) void { + stats.tile_grid_count += tile_grid_count; + stats.active_tile_count += tiling.active_tiles.len; + stats.overlap_refs_total += tiling.overlaps.len; + for (tiling.active_tiles) |tile| { + stats.overlap_refs_max = @max(stats.overlap_refs_max, tile.overlap_count); + } +} + fn runRasterStage( outer_alloc: std.mem.Allocator, io: std.Io, @@ -1315,14 +1439,24 @@ fn prepareFrameContext( input: *const FrameJobDesc, ) !void { const arena_alloc = ctx.arena.allocator(); - ctx.actual_tile_size = scalingpolicy.tileSize( - input.config.tile_size_override, - input.config.tile_size_min, - input.config.tile_size_max, - input.camera.pixels_num, - input.camera.sub_sample, - input.config.raster_halo_px_override orelse input.camera.prep_psf.halo_px, - ); + ctx.actual_tile_size = switch (input.config.buffer_mode) { + .tile_local => scalingpolicy.tileSize( + input.config.tile_size_override, + input.config.tile_size_min, + input.config.tile_size_max, + input.camera.pixels_num, + input.camera.sub_sample, + input.config.raster_halo_px_override orelse input.camera.prep_psf.halo_px, + ), + .global_subpx_full, .global_subpx_stripe => blk: { + const requested_subpx = input.config.global_subpx_tile_size_override orelse + input.config.global_subpx_tile_size_min; + break :blk @divExact( + requested_subpx, + @as(u16, @intCast(input.camera.sub_sample)), + ); + }, + }; ctx.report_storage = try initFrameReportStorage( outer_alloc, @@ -1438,6 +1572,8 @@ fn rasterFrame( const report_ptr = getFrameReportPtr(report_mode, ctx); const ctx_report = report.ReportContext(report_mode){ .log = report_ptr }; const time_start_loop = Timestamp.now(io, .awake); + ctx.frame_times.raster_workers_requested = raster_workers; + ctx.frame_times.resolve_workers_requested = raster_workers; const ctx_rast = rops.RasterContext{ .camera = input.camera, @@ -1445,25 +1581,263 @@ fn rasterFrame( .frame_idx = input.frame_idx, .tile_size = ctx.actual_tile_size, }; + var global_resolve_time_ns: F = 0.0; + if (input.config.buffer_mode != .tile_local) { + const sub_samp: usize = @intCast(input.camera.sub_sample); + const halo_px = input.config.raster_halo_px_override orelse + input.camera.prep_psf.halo_px; + ctx.frame_times.global_subpx_stats = .{ + .mode = input.config.buffer_mode, + .output_w_subpx = @as(usize, input.camera.pixels_num[0]) * sub_samp, + .output_h_subpx = @as(usize, input.camera.pixels_num[1]) * sub_samp, + .outer_halo_subpx = @as(usize, halo_px) * sub_samp, + .tile_core_subpx = @as(usize, ctx.actual_tile_size) * sub_samp, + .tile_scratch_subpx = (@as(usize, ctx.actual_tile_size) + 2 * @as(usize, halo_px)) * + sub_samp, + }; + } - try rasterengine.rasterScene( - report_mode, - outer_alloc, - io, - ctx_rast, - ctx_report, - raster_workers, - ctx.tiling.?, - ctx.prep_meshes, - ctx.raster_hulls, - &ctx.frame_arr, - ); + switch (input.config.buffer_mode) { + .tile_local => try rasterengine.rasterScene( + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + raster_workers, + ctx.tiling.?, + ctx.prep_meshes, + ctx.raster_hulls, + &ctx.frame_arr, + ), + .global_subpx_full => { + const time_start_buffer_setup = Timestamp.now(io, .awake); + const sub_samp: usize = @intCast(input.camera.sub_sample); + const halo_px = input.config.raster_halo_px_override orelse + input.camera.prep_psf.halo_px; + const halo_subpx = @as(usize, halo_px) * sub_samp; + const domain = try subpxframe.SubpxFrameDomain.init( + input.num_fields, + input.camera.pixels_num, + input.camera.sub_sample, + halo_subpx, + ); + var target = try subpxframe.SubpxTarget.init( + outer_alloc, + domain, + -@as(i32, @intCast(halo_subpx)), + -@as(i32, @intCast(halo_subpx)), + input.config.background_value, + ); + defer target.deinit(outer_alloc); + ctx.frame_times.global_subpx_times.buffer_setup += @floatFromInt( + time_start_buffer_setup.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + if (ctx.frame_times.global_subpx_stats) |*stats| { + const tiles_x = std.math.divCeil( + usize, + input.camera.pixels_num[0], + ctx.actual_tile_size, + ) catch unreachable; + const tiles_y = std.math.divCeil( + usize, + input.camera.pixels_num[1], + ctx.actual_tile_size, + ) catch unreachable; + recordGlobalTilingStats(stats, ctx.tiling.?, tiles_x * tiles_y); + } + + const time_start_tile_raster = Timestamp.now(io, .awake); + const workers_used = try rasterengineglobal.rasterScene( + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + raster_workers, + ctx.tiling.?, + ctx.prep_meshes, + ctx.raster_hulls, + &target, + &ctx.frame_arr, + ); + ctx.frame_times.raster_workers_used = @intCast(workers_used); + ctx.frame_times.global_subpx_times.tile_raster += @floatFromInt( + time_start_tile_raster.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + const time_start_resolve = Timestamp.now(io, .awake); + const resolve_workers_used = try scratchresolveglobal.resolve( + outer_alloc, + io, + &target, + input.camera, + input.config.background_value, + &ctx.frame_arr, + raster_workers, + ); + ctx.frame_times.resolve_workers_used = @intCast(resolve_workers_used); + global_resolve_time_ns = @floatFromInt( + time_start_resolve.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + ctx.frame_times.global_subpx_times.resolve += global_resolve_time_ns; + }, + .global_subpx_stripe => { + const sub_samp: usize = @intCast(input.camera.sub_sample); + const halo_px = input.config.raster_halo_px_override orelse + input.camera.prep_psf.halo_px; + const halo_subpx = @as(usize, halo_px) * sub_samp; + const image_w_subpx = @as(usize, input.camera.pixels_num[0]) * sub_samp; + const image_h_subpx = @as(usize, input.camera.pixels_num[1]) * sub_samp; + const requested_stripe_subpx = + input.config.global_subpx_stripe_size_override orelse + input.config.global_subpx_stripe_size_min; + const stripe_subpx: usize = requested_stripe_subpx; + const first_core_suby_max = @min(image_h_subpx, stripe_subpx); + const time_start_buffer_setup = Timestamp.now(io, .awake); + var stripe = try subpxframe.SubpxStripe.init( + outer_alloc, + input.num_fields, + image_w_subpx, + 0, + @intCast(first_core_suby_max), + halo_subpx, + halo_subpx, + input.config.background_value, + ); + defer stripe.deinit(outer_alloc); + ctx.frame_times.global_subpx_times.buffer_setup += @floatFromInt( + time_start_buffer_setup.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + + var core_suby_min: usize = 0; + while (core_suby_min < image_h_subpx) { + const core_suby_max = @min( + image_h_subpx, + core_suby_min + stripe_subpx, + ); + const time_start_buffer_plan = Timestamp.now(io, .awake); + stripe.reset( + @intCast(core_suby_min), + @intCast(core_suby_max), + halo_subpx, + input.config.background_value, + ); + var stripe_tiling: rops.TilingOverlaps = undefined; + var stripe_tiling_owned = false; + if (core_suby_min == 0 and core_suby_max == image_h_subpx) { + stripe_tiling = ctx.tiling.?; + } else { + stripe_tiling = try sceneGlobalTileElemOverlap( + outer_alloc, + ctx.actual_tile_size, + input.camera.sub_sample, + @intCast(input.camera.pixels_num[0]), + @intCast(input.camera.pixels_num[1]), + @intCast(core_suby_min / sub_samp), + @intCast(core_suby_max / sub_samp), + halo_px, + ctx.elems_in_image_by_mesh, + ctx.elem_bboxes_by_mesh, + ); + stripe_tiling_owned = true; + } + ctx.frame_times.global_subpx_times.buffer_setup += @floatFromInt( + time_start_buffer_plan.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + if (ctx.frame_times.global_subpx_stats) |*stats| { + const tiles_x = std.math.divCeil( + usize, + input.camera.pixels_num[0], + ctx.actual_tile_size, + ) catch unreachable; + const stripe_h_px = (core_suby_max - core_suby_min) / sub_samp; + const tiles_y = std.math.divCeil( + usize, + stripe_h_px, + ctx.actual_tile_size, + ) catch unreachable; + recordGlobalTilingStats(stats, stripe_tiling, tiles_x * tiles_y); + stats.stripe_core_subpx = stripe_subpx; + stats.stripe_count += 1; + stats.final_stripe_core_subpx = core_suby_max - core_suby_min; + stats.stripe_storage_w_subpx = stripe.target.domain.storage_w_subpx; + stats.stripe_storage_h_subpx = stripe.target.domain.storage_h_subpx; + stats.stripe_storage_samples_cleared += @intCast( + stripe.target.domain.storage_w_subpx * + stripe.target.domain.storage_h_subpx, + ); + } + errdefer { + if (stripe_tiling_owned) { + outer_alloc.free(stripe_tiling.active_tiles); + outer_alloc.free(stripe_tiling.overlaps); + } + } + + const time_start_tile_raster = Timestamp.now(io, .awake); + const workers_used = try rasterengineglobal.rasterScene( + report_mode, + outer_alloc, + io, + ctx_rast, + ctx_report, + raster_workers, + stripe_tiling, + ctx.prep_meshes, + ctx.raster_hulls, + &stripe.target, + &ctx.frame_arr, + ); + ctx.frame_times.raster_workers_used = @max( + ctx.frame_times.raster_workers_used, + @as(u16, @intCast(workers_used)), + ); + ctx.frame_times.global_subpx_times.tile_raster += @floatFromInt( + time_start_tile_raster.durationTo(Timestamp.now(io, .awake)).raw.nanoseconds, + ); + const time_start_resolve = Timestamp.now(io, .awake); + const resolve_workers_used = try scratchresolveglobal.resolveRows( + outer_alloc, + io, + &stripe.target, + input.camera, + input.config.background_value, + &ctx.frame_arr, + core_suby_min / sub_samp, + core_suby_max / sub_samp, + raster_workers, + ); + ctx.frame_times.resolve_workers_used = @max( + ctx.frame_times.resolve_workers_used, + @as(u16, @intCast(resolve_workers_used)), + ); + const time_end_resolve = Timestamp.now(io, .awake); + global_resolve_time_ns += @floatFromInt( + time_start_resolve.durationTo(time_end_resolve).raw.nanoseconds, + ); + ctx.frame_times.global_subpx_times.resolve += @floatFromInt( + time_start_resolve.durationTo(time_end_resolve).raw.nanoseconds, + ); + if (stripe_tiling_owned) { + outer_alloc.free(stripe_tiling.active_tiles); + outer_alloc.free(stripe_tiling.overlaps); + } + core_suby_min = core_suby_max; + } + }, + } const time_end_loop = Timestamp.now(io, .awake); ctx.frame_times.raster_loop = @floatFromInt( time_start_loop.durationTo(time_end_loop).raw.nanoseconds, ); - if (report.getBenchLog(report_mode, report_ptr)) |bench_log| { + if (input.config.buffer_mode != .tile_local) { + ctx.frame_times.scratch_resolve = global_resolve_time_ns; + if (report.getBenchLog(report_mode, report_ptr)) |bench_log| { + ctx.frame_times.cam_invert = bench_log.cam_time_ns; + ctx.frame_times.elem_loop = bench_log.elem_time_ns; + } + } else if (report.getBenchLog(report_mode, report_ptr)) |bench_log| { ctx.frame_times.cam_invert = bench_log.cam_time_ns; ctx.frame_times.elem_loop = bench_log.elem_time_ns; ctx.frame_times.scratch_resolve = bench_log.resolve_time_ns; @@ -1517,3 +1891,73 @@ fn renderGroupSaveIo(render_group: RenderGroupSpec) std.Io { fn saveOverlapEnabled(config: RasterConfig) bool { return config.save_strategy == .disk and config.disk_save_overlap; } + +test "global sub-pixel tiles own disjoint cores and retain halo only at frame edges" { + var elem_bboxes = [_]rops.ElemBBox{.{ + .elem_idx = 0, + .x_min = -2, + .x_max = 12, + .y_min = -2, + .y_max = 11, + }}; + const elems_in_image = [_]usize{1}; + const elem_bboxes_by_mesh = [_][]rops.ElemBBox{elem_bboxes[0..]}; + const sub_sample: u32 = 2; + const halo_px: u16 = 2; + const screen_w_px: u16 = 10; + const screen_h_px: u16 = 9; + const screen_w_subpx: i32 = screen_w_px * sub_sample; + const screen_h_subpx: i32 = screen_h_px * sub_sample; + const halo_subpx: i32 = halo_px * sub_sample; + + const tiling = try sceneGlobalTileElemOverlap( + std.testing.allocator, + 4, + sub_sample, + screen_w_px, + screen_h_px, + 0, + screen_h_px, + halo_px, + elems_in_image[0..], + elem_bboxes_by_mesh[0..], + ); + defer std.testing.allocator.free(tiling.active_tiles); + defer std.testing.allocator.free(tiling.overlaps); + + try std.testing.expectEqual(@as(usize, 9), tiling.active_tiles.len); + for (tiling.active_tiles) |tile| { + try std.testing.expectEqual( + if (tile.core_subx_min == 0) -halo_subpx else tile.core_subx_min, + tile.scratch_subx_min, + ); + try std.testing.expectEqual( + if (tile.core_subx_max == screen_w_subpx) + screen_w_subpx + halo_subpx + else + tile.core_subx_max, + tile.scratch_subx_max, + ); + try std.testing.expectEqual( + if (tile.core_suby_min == 0) -halo_subpx else tile.core_suby_min, + tile.scratch_suby_min, + ); + try std.testing.expectEqual( + if (tile.core_suby_max == screen_h_subpx) + screen_h_subpx + halo_subpx + else + tile.core_suby_max, + tile.scratch_suby_max, + ); + } + + for (tiling.active_tiles, 0..) |tile_a, aa| { + for (tiling.active_tiles[aa + 1 ..]) |tile_b| { + const overlaps_x = tile_a.scratch_subx_min < tile_b.scratch_subx_max and + tile_b.scratch_subx_min < tile_a.scratch_subx_max; + const overlaps_y = tile_a.scratch_suby_min < tile_b.scratch_suby_max and + tile_b.scratch_suby_min < tile_a.scratch_suby_max; + try std.testing.expect(!(overlaps_x and overlaps_y)); + } + } +} diff --git a/src/riley/zig/scratchresolveglobal.zig b/src/riley/zig/scratchresolveglobal.zig new file mode 100644 index 00000000..8ab6c7ca --- /dev/null +++ b/src/riley/zig/scratchresolveglobal.zig @@ -0,0 +1,63 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const cfg = @import("buildconfig.zig").config; +const std = @import("std"); +const cam = @import("camera.zig"); +const ndarray = @import("ndarray.zig"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const impl = if (cfg.simd == .on) + @import("scratchresolveglobal_simd.zig") +else + @import("scratchresolveglobal_scalar.zig"); +const subpxframe = @import("subpxframe.zig"); + +pub fn resolve( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + requested_workers: u16, +) !usize { + return impl.resolve( + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + requested_workers, + ); +} + +pub fn resolveRows( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + return impl.resolveRows( + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + image_y_min, + image_y_max, + requested_workers, + ); +} diff --git a/src/riley/zig/scratchresolveglobal_common.zig b/src/riley/zig/scratchresolveglobal_common.zig new file mode 100644 index 00000000..9afb34ee --- /dev/null +++ b/src/riley/zig/scratchresolveglobal_common.zig @@ -0,0 +1,499 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const cam = @import("camera.zig"); +const ndarray = @import("ndarray.zig"); +const subpxframe = @import("subpxframe.zig"); +const pce = @import("parachunkexec.zig"); + +const S = buildconfig.SimdWidth; +const VecSF = buildconfig.VecSF; + +// -------------------------------------------------------------------------------------- +// Public Entry-Point Func +// -------------------------------------------------------------------------------------- + +pub fn resolve( + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), +) void { + resolveRows( + target, + camera, + background_value, + image_out_arr, + 0, + target.domain.image_h_subpx / @as(usize, camera.sub_sample), + ); +} + +pub fn resolveRows( + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, +) void { + const sub_samp: usize = @intCast(camera.sub_sample); + const prep_psf = camera.prep_psf; + const fields_num: usize = target.domain.fields_num; + const image_w_px = target.domain.image_w_subpx / sub_samp; + std.debug.assert(image_y_min <= image_y_max); + std.debug.assert(image_out_arr.dims[0] == fields_num); + std.debug.assert(image_out_arr.dims[1] >= image_y_max); + std.debug.assert(image_out_arr.dims[2] >= image_w_px); + + for (image_y_min..image_y_max) |yy| { + const global_suby_start = yy * sub_samp; + for (0..image_w_px) |xx| { + const global_subx_start = xx * sub_samp; + for (0..fields_num) |ff| { + var sum: F = 0.0; + for (0..sub_samp) |ssy| { + for (0..sub_samp) |ssx| { + const global_subx: i32 = @intCast(global_subx_start + ssx); + const global_suby: i32 = @intCast(global_suby_start + ssy); + sum += sampleFiltered( + target, + prep_psf, + background_value, + global_subx, + global_suby, + ff, + ); + } + } + const sub_samp_f: F = @floatFromInt(sub_samp); + image_out_arr.slice[image_out_arr.offset3(ff, yy, xx)] = + sum / (sub_samp_f * sub_samp_f); + } + } + } +} + +/// Resolve a global target using independent output-row bands. The direct +/// implementation remains the scalar reference for identity and non-separable +/// PSFs. Separable PSFs use a compact horizontal intermediate at pixel x +/// resolution, then a vertical pass; this fuses the SSAA box sum into each +/// one-dimensional pass and avoids the former SSAA^2 x Kx x Ky stencil. +pub fn resolveParallel( + comptime use_simd: bool, + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + if (image_y_min == image_y_max) return 0; + return switch (camera.prep_psf.mode) { + .separable => resolveSeparableParallel( + use_simd, + outer_alloc, + io, + target, + camera, + image_out_arr, + image_y_min, + image_y_max, + requested_workers, + ), + .identity_fast, .nonseparable => resolveDirectParallel( + io, + target, + camera, + background_value, + image_out_arr, + image_y_min, + image_y_max, + requested_workers, + ), + }; +} + +fn workersForRows(requested_workers: u16, rows: usize) usize { + return @min( + @max(@as(usize, 1), @as(usize, requested_workers)), + @max(@as(usize, 1), rows), + ); +} + +fn resolveDirectParallel( + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + const workers_num = workersForRows(requested_workers, image_y_max - image_y_min); + const Ctx = struct { + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + }; + const Adapter = struct { + fn run(ctx_ptr: *anyopaque, _: usize, range_start: usize, range_end: usize) void { + const ctx: *Ctx = @ptrCast(@alignCast(ctx_ptr)); + // This is intentionally the unchanged direct scalar oracle. Each + // task owns complete output rows, so no accumulation is shared. + resolveRows( + ctx.target, + ctx.camera, + ctx.background_value, + ctx.image_out_arr, + ctx.image_y_min + range_start, + ctx.image_y_min + range_end, + ); + } + }; + var exec = pce.ParaChunkExecutor.init(io, @intCast(workers_num)); + var ctx = Ctx{ + .target = target, + .camera = camera, + .background_value = background_value, + .image_out_arr = image_out_arr, + .image_y_min = image_y_min, + }; + try exec.runStaticRange(&ctx, Adapter.run, image_y_max - image_y_min, 1); + return workers_num; +} + +fn resolveSeparableParallel( + comptime use_simd: bool, + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + const sub_samp: usize = @intCast(camera.sub_sample); + const image_w_px = target.domain.image_w_subpx / sub_samp; + const storage_h = target.domain.storage_h_subpx; + const fields_num: usize = target.domain.fields_num; + const horizontal_len = try std.math.mul(usize, fields_num, try std.math.mul(usize, storage_h, image_w_px)); + const horizontal = try outer_alloc.alloc(F, horizontal_len); + defer outer_alloc.free(horizontal); + + const HorizontalCtx = struct { + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + horizontal: []F, + image_w_px: usize, + storage_h: usize, + }; + const HorizontalAdapter = struct { + fn run(ctx_ptr: *anyopaque, _: usize, range_start: usize, range_end: usize) void { + const ctx: *HorizontalCtx = @ptrCast(@alignCast(ctx_ptr)); + horizontalRows(use_simd, ctx.*, range_start, range_end); + } + }; + + const horizontal_workers = workersForRows(requested_workers, storage_h); + var horizontal_exec = pce.ParaChunkExecutor.init(io, @intCast(horizontal_workers)); + var horizontal_ctx = HorizontalCtx{ + .target = target, + .camera = camera, + .horizontal = horizontal, + .image_w_px = image_w_px, + .storage_h = storage_h, + }; + try horizontal_exec.runStaticRange(&horizontal_ctx, HorizontalAdapter.run, storage_h, 1); + + const VerticalCtx = struct { + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + horizontal: []const F, + image_out_arr: *ndarray.NDArray(F), + image_w_px: usize, + storage_h: usize, + image_y_min: usize, + }; + const VerticalAdapter = struct { + fn run(ctx_ptr: *anyopaque, _: usize, range_start: usize, range_end: usize) void { + const ctx: *VerticalCtx = @ptrCast(@alignCast(ctx_ptr)); + verticalRows( + use_simd, + ctx.*, + ctx.image_y_min + range_start, + ctx.image_y_min + range_end, + ); + } + }; + + const vertical_workers = workersForRows(requested_workers, image_y_max - image_y_min); + var vertical_exec = pce.ParaChunkExecutor.init(io, @intCast(vertical_workers)); + var vertical_ctx = VerticalCtx{ + .target = target, + .camera = camera, + .horizontal = horizontal, + .image_out_arr = image_out_arr, + .image_w_px = image_w_px, + .storage_h = storage_h, + .image_y_min = image_y_min, + }; + try vertical_exec.runStaticRange( + &vertical_ctx, + VerticalAdapter.run, + image_y_max - image_y_min, + 1, + ); + return @min(horizontal_workers, vertical_workers); +} + +fn horizontalRows( + comptime use_simd: bool, + ctx: anytype, + local_y_start: usize, + local_y_end: usize, +) void { + const sub_samp: usize = @intCast(ctx.camera.sub_sample); + const psf = ctx.camera.prep_psf; + const local_core_x: usize = @intCast(-ctx.target.global_subx_min); + const source_row_stride = ctx.target.domain.storage_w_subpx; + const horizontal_field_stride = ctx.storage_h * ctx.image_w_px; + + for (0..ctx.target.domain.fields_num) |ff| { + const source_field_base = ctx.target.image.rowBase(ff); + const horizontal_field_base = ff * horizontal_field_stride; + for (local_y_start..local_y_end) |local_y| { + const source_row = source_field_base + local_y * source_row_stride; + const horizontal_row = horizontal_field_base + local_y * ctx.image_w_px; + var xx: usize = 0; + if (comptime use_simd) { + while (xx + S <= ctx.image_w_px) : (xx += S) { + var sum = @as(VecSF, @splat(0.0)); + for (0..sub_samp) |sample_x| { + for (psf.weights_x, 0..) |weight, kk| { + var values: [S]F = undefined; + for (0..S) |lane| { + const source_x = local_core_x + + (xx + lane) * sub_samp + sample_x + kk - psf.radius_x_subpx; + values[lane] = ctx.target.image.slice[source_row + source_x]; + } + sum += @as(VecSF, values) * @as(VecSF, @splat(weight)); + } + } + const out_ptr: *[S]F = @ptrCast(&ctx.horizontal[horizontal_row + xx]); + out_ptr.* = @bitCast(sum); + } + } + while (xx < ctx.image_w_px) : (xx += 1) { + var sum: F = 0.0; + for (0..sub_samp) |sample_x| { + for (psf.weights_x, 0..) |weight, kk| { + const source_x = local_core_x + xx * sub_samp + sample_x + kk - psf.radius_x_subpx; + sum += weight * ctx.target.image.slice[source_row + source_x]; + } + } + ctx.horizontal[horizontal_row + xx] = sum; + } + } + } +} + +fn verticalRows( + comptime use_simd: bool, + ctx: anytype, + image_y_start: usize, + image_y_end: usize, +) void { + const sub_samp: usize = @intCast(ctx.camera.sub_sample); + const psf = ctx.camera.prep_psf; + const horizontal_field_stride = ctx.storage_h * ctx.image_w_px; + const inv_sub_samp_sq = 1.0 / @as(F, @floatFromInt(sub_samp * sub_samp)); + + for (0..ctx.target.domain.fields_num) |ff| { + const horizontal_field_base = ff * horizontal_field_stride; + for (image_y_start..image_y_end) |image_y| { + const global_suby: i32 = @intCast(image_y * sub_samp); + const local_core_y: usize = @intCast(global_suby - ctx.target.global_suby_min); + const output_row = ctx.image_out_arr.offset3(ff, image_y, 0); + var xx: usize = 0; + if (comptime use_simd) { + while (xx + S <= ctx.image_w_px) : (xx += S) { + var sum = @as(VecSF, @splat(0.0)); + for (0..sub_samp) |sample_y| { + for (psf.weights_y, 0..) |weight, kk| { + const source_y = local_core_y + sample_y + kk - psf.radius_y_subpx; + const source_ptr: *const [S]F = @ptrCast( + &ctx.horizontal[horizontal_field_base + source_y * ctx.image_w_px + xx], + ); + sum += @as(VecSF, source_ptr.*) * @as(VecSF, @splat(weight)); + } + } + const output_ptr: *[S]F = @ptrCast(&ctx.image_out_arr.slice[output_row + xx]); + output_ptr.* = @bitCast(sum * @as(VecSF, @splat(inv_sub_samp_sq))); + } + } + while (xx < ctx.image_w_px) : (xx += 1) { + var sum: F = 0.0; + for (0..sub_samp) |sample_y| { + for (psf.weights_y, 0..) |weight, kk| { + const source_y = local_core_y + sample_y + kk - psf.radius_y_subpx; + sum += weight * ctx.horizontal[ + horizontal_field_base + source_y * ctx.image_w_px + xx + ]; + } + } + ctx.image_out_arr.slice[output_row + xx] = sum * inv_sub_samp_sq; + } + } + } +} + +// -------------------------------------------------------------------------------------- +// Private Func +// -------------------------------------------------------------------------------------- + +fn sampleFiltered( + target: *const subpxframe.SubpxTarget, + prep_psf: cam.PreparedPSF, + background_value: F, + global_subx: i32, + global_suby: i32, + field_idx: usize, +) F { + return switch (prep_psf.mode) { + .identity_fast => sampleTarget( + target, + background_value, + global_subx, + global_suby, + field_idx, + ), + .separable => sampleSeparable( + target, + prep_psf, + background_value, + global_subx, + global_suby, + field_idx, + ), + .nonseparable => sampleNonSeparable( + target, + prep_psf, + background_value, + global_subx, + global_suby, + field_idx, + ), + }; +} + +fn sampleSeparable( + target: *const subpxframe.SubpxTarget, + prep_psf: cam.PreparedPSF, + background_value: F, + global_subx: i32, + global_suby: i32, + field_idx: usize, +) F { + var sum: F = 0.0; + for (prep_psf.weights_y, 0..) |weight_y, yy| { + const y_off: i32 = @intCast(yy); + for (prep_psf.weights_x, 0..) |weight_x, xx| { + const x_off: i32 = @intCast(xx); + const radius_x: i32 = @intCast(prep_psf.radius_x_subpx); + const radius_y: i32 = @intCast(prep_psf.radius_y_subpx); + sum += weight_x * weight_y * sampleTarget( + target, + background_value, + global_subx + x_off - radius_x, + global_suby + y_off - radius_y, + field_idx, + ); + } + } + return sum; +} + +fn sampleNonSeparable( + target: *const subpxframe.SubpxTarget, + prep_psf: cam.PreparedPSF, + background_value: F, + global_subx: i32, + global_suby: i32, + field_idx: usize, +) F { + const kernel_w = 2 * prep_psf.radius_x_subpx + 1; + var sum: F = 0.0; + for (prep_psf.weights_2d, 0..) |weight, kk| { + const kernel_x = kk % kernel_w; + const kernel_y = kk / kernel_w; + const x_off: i32 = @intCast(kernel_x); + const y_off: i32 = @intCast(kernel_y); + const radius_x: i32 = @intCast(prep_psf.radius_x_subpx); + const radius_y: i32 = @intCast(prep_psf.radius_y_subpx); + sum += weight * sampleTarget( + target, + background_value, + global_subx + x_off - radius_x, + global_suby + y_off - radius_y, + field_idx, + ); + } + return sum; +} + +fn sampleTarget( + target: *const subpxframe.SubpxTarget, + background_value: F, + global_subx: i32, + global_suby: i32, + field_idx: usize, +) F { + const local_subx = global_subx - target.global_subx_min; + const local_suby = global_suby - target.global_suby_min; + if (local_subx < 0 or local_suby < 0 or + local_subx >= @as(i32, @intCast(target.domain.storage_w_subpx)) or + local_suby >= @as(i32, @intCast(target.domain.storage_h_subpx))) + { + return background_value; + } + + const flat_idx = @as(usize, @intCast(local_suby)) * target.domain.storage_w_subpx + + @as(usize, @intCast(local_subx)); + return target.image.slice[target.image.rowBase(field_idx) + flat_idx]; +} + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +test "sampleTarget uses the target global origin" { + const domain = try subpxframe.SubpxFrameDomain.init(1, .{ 2, 2 }, 1, 1); + var target = try subpxframe.SubpxTarget.init( + std.testing.allocator, + domain, + -1, + -1, + 0.0, + ); + defer target.deinit(std.testing.allocator); + + target.image.slice[target.image.rowBase(0) + target.flatIndex(0, 1)] = 7.0; + try std.testing.expectEqual(@as(F, 7.0), sampleTarget(&target, -1.0, 0, 1, 0)); + try std.testing.expectEqual(@as(F, -1.0), sampleTarget(&target, -1.0, 3, 1, 0)); +} diff --git a/src/riley/zig/scratchresolveglobal_scalar.zig b/src/riley/zig/scratchresolveglobal_scalar.zig new file mode 100644 index 00000000..9d5ec447 --- /dev/null +++ b/src/riley/zig/scratchresolveglobal_scalar.zig @@ -0,0 +1,63 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const cam = @import("camera.zig"); +const ndarray = @import("ndarray.zig"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const common = @import("scratchresolveglobal_common.zig"); +const subpxframe = @import("subpxframe.zig"); + +pub fn resolve( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + requested_workers: u16, +) !usize { + return common.resolveParallel( + false, + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + 0, + target.domain.image_h_subpx / @as(usize, camera.sub_sample), + requested_workers, + ); +} + +pub fn resolveRows( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + return common.resolveParallel( + false, + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + image_y_min, + image_y_max, + requested_workers, + ); +} +const std = @import("std"); diff --git a/src/riley/zig/scratchresolveglobal_simd.zig b/src/riley/zig/scratchresolveglobal_simd.zig new file mode 100644 index 00000000..bef9e5a9 --- /dev/null +++ b/src/riley/zig/scratchresolveglobal_simd.zig @@ -0,0 +1,63 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const cam = @import("camera.zig"); +const ndarray = @import("ndarray.zig"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const common = @import("scratchresolveglobal_common.zig"); +const subpxframe = @import("subpxframe.zig"); + +pub fn resolve( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + requested_workers: u16, +) !usize { + return common.resolveParallel( + true, + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + 0, + target.domain.image_h_subpx / @as(usize, camera.sub_sample), + requested_workers, + ); +} + +pub fn resolveRows( + outer_alloc: std.mem.Allocator, + io: std.Io, + target: *const subpxframe.SubpxTarget, + camera: *const cam.CameraPrepared, + background_value: F, + image_out_arr: *ndarray.NDArray(F), + image_y_min: usize, + image_y_max: usize, + requested_workers: u16, +) !usize { + return common.resolveParallel( + true, + outer_alloc, + io, + target, + camera, + background_value, + image_out_arr, + image_y_min, + image_y_max, + requested_workers, + ); +} +const std = @import("std"); diff --git a/src/riley/zig/shaderops_common.zig b/src/riley/zig/shaderops_common.zig index 3d4e7a0a..2eda523b 100644 --- a/src/riley/zig/shaderops_common.zig +++ b/src/riley/zig/shaderops_common.zig @@ -380,6 +380,10 @@ pub const ShadeContext = struct { global_subx: usize, global_suby: usize, v_mask_active: ?buildconfig.VecSB = null, + // Global sub-pixel tiles own disjoint target samples. Their final SIMD + // vector may straddle a tile edge, so inactive lanes must not perform a + // read-modify-write against the neighbouring tile's target samples. + exclusive_subpx_target: bool = false, }; pub fn InterpData(comptime N: usize) type { diff --git a/src/riley/zig/shaderops_simd.zig b/src/riley/zig/shaderops_simd.zig index 2c7c2ef9..89eecf51 100644 --- a/src/riley/zig/shaderops_simd.zig +++ b/src/riley/zig/shaderops_simd.zig @@ -30,6 +30,32 @@ const simdops = @import("simdops.zig"); pub const fillNodalClipScal = scal.fillNodalClipScal; pub const fillNodalPerspScal = scal.fillNodalPerspScal; +inline fn storeShadeSIMD( + subpx_vals: []F, + start_u: usize, + ctx_shade: comm.ShadeContext, + v_mask_active: VecSB, + v_vals: VecSF, +) void { + if (!ctx_shade.exclusive_subpx_target) { + simdops.storeMaskedVecSF( + subpx_vals, + start_u, + v_mask_active, + v_vals, + ); + return; + } + + const mask_arr: [S]bool = v_mask_active; + const vals_arr: [S]F = v_vals; + inline for (0..S) |lane| { + if (mask_arr[lane]) { + subpx_vals[start_u + lane] = vals_arr[lane]; + } + } +} + pub inline fn fillNodalClipSIMD( comptime N: usize, ctx_shade: comm.ShadeContext, @@ -54,9 +80,10 @@ pub inline fn fillNodalClipSIMD( const v_final = v_weighted_sum * v_splat_mul + v_splat_add; const flat_idx = ff * px_stride + ctx_shade.scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, ctx_shade.v_mask_active.?, v_final, ); @@ -90,9 +117,10 @@ pub inline fn fillNodalPerspSIMD( const v_final = (v_weighted_sum * v_subpx_z) * v_splat_mul + v_splat_add; const flat_idx = ff * px_stride + ctx_shade.scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, ctx_shade.v_mask_active.?, v_final, ); @@ -162,9 +190,10 @@ pub inline fn fillTexClipSIMD( const flat_idx = ch * px_stride + ctx_shade.scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); @@ -236,9 +265,10 @@ pub inline fn fillTexPerspSIMD( inline for (0..C) |ch| { const v_final = sampled_vecs[ch] * v_splat_mul + v_splat_add; const flat_idx = ch * px_stride + ctx_shade.scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); @@ -625,9 +655,10 @@ pub inline fn fillFuncClipSIMD( const flat_idx = scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); @@ -646,9 +677,10 @@ pub inline fn fillFuncClipSIMD( const v_final = v_vals[ch] * v_mul + v_add; const flat_idx = ch * px_stride + scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); @@ -717,9 +749,10 @@ pub inline fn fillFuncPerspSIMD( const v_final = v_eval * v_mul + v_add; const flat_idx = scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); @@ -736,9 +769,10 @@ pub inline fn fillFuncPerspSIMD( const v_add = @as(VecSF, @splat(shader.scale_add)); const v_final = v_vals[ch] * v_mul + v_add; const flat_idx = ch * px_stride + scratch_idx; - simdops.storeMaskedVecSF( + storeShadeSIMD( spx_image_scratch.slice, flat_idx, + ctx_shade, v_mask_active, v_final, ); diff --git a/src/riley/zig/subpxframe.zig b/src/riley/zig/subpxframe.zig new file mode 100644 index 00000000..b692b475 --- /dev/null +++ b/src/riley/zig/subpxframe.zig @@ -0,0 +1,278 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const buildconfig = @import("buildconfig.zig"); +const F = buildconfig.F; +const MatSlice = @import("matslice.zig").MatSlice; + +// -------------------------------------------------------------------------------------- +// Public Constants & Public Types +// -------------------------------------------------------------------------------------- + +pub const SubpxFrameDomain = struct { + fields_num: u8, + image_w_subpx: usize, + image_h_subpx: usize, + halo_subpx: usize, + storage_w_subpx: usize, + storage_h_subpx: usize, + + pub fn init( + fields_num: u8, + pixels_num: [2]u32, + sub_samp: u32, + halo_subpx: usize, + ) !SubpxFrameDomain { + const image_w_subpx = try std.math.mul( + usize, + pixels_num[0], + sub_samp, + ); + const image_h_subpx = try std.math.mul( + usize, + pixels_num[1], + sub_samp, + ); + const halo_twice = try std.math.mul(usize, halo_subpx, 2); + + return .{ + .fields_num = fields_num, + .image_w_subpx = image_w_subpx, + .image_h_subpx = image_h_subpx, + .halo_subpx = halo_subpx, + .storage_w_subpx = try std.math.add( + usize, + image_w_subpx, + halo_twice, + ), + .storage_h_subpx = try std.math.add( + usize, + image_h_subpx, + halo_twice, + ), + }; + } +}; + +pub const SubpxTarget = struct { + domain: SubpxFrameDomain, + image: MatSlice(F), + global_subx_min: i32, + global_suby_min: i32, + + pub fn init( + outer_alloc: std.mem.Allocator, + domain: SubpxFrameDomain, + global_subx_min: i32, + global_suby_min: i32, + background_value: F, + ) !SubpxTarget { + const samples_num = try std.math.mul( + usize, + domain.storage_w_subpx, + domain.storage_h_subpx, + ); + const total_num = try std.math.mul( + usize, + samples_num, + domain.fields_num, + ); + const image_mem = try outer_alloc.alloc(F, total_num); + errdefer outer_alloc.free(image_mem); + @memset(image_mem, background_value); + + return .{ + .domain = domain, + .image = MatSlice(F).init( + image_mem, + domain.fields_num, + samples_num, + ), + .global_subx_min = global_subx_min, + .global_suby_min = global_suby_min, + }; + } + + pub fn deinit( + self: *SubpxTarget, + outer_alloc: std.mem.Allocator, + ) void { + outer_alloc.free(self.image.slice); + self.* = undefined; + } + + pub inline fn flatIndex( + self: *const SubpxTarget, + global_subx: i32, + global_suby: i32, + ) usize { + const local_subx = global_subx - self.global_subx_min; + const local_suby = global_suby - self.global_suby_min; + std.debug.assert(local_subx >= 0); + std.debug.assert(local_suby >= 0); + const local_x: usize = @intCast(local_subx); + const local_y: usize = @intCast(local_suby); + std.debug.assert(local_x < self.domain.storage_w_subpx); + std.debug.assert(local_y < self.domain.storage_h_subpx); + return local_y * self.domain.storage_w_subpx + local_x; + } +}; + +pub const SubpxStripe = struct { + target: SubpxTarget, + core_suby_min: i32, + core_suby_max: i32, + + pub fn init( + outer_alloc: std.mem.Allocator, + fields_num: u8, + image_w_subpx: usize, + core_suby_min: i32, + core_suby_max: i32, + halo_subpx_x: usize, + halo_subpx_y: usize, + background_value: F, + ) !SubpxStripe { + std.debug.assert(core_suby_min < core_suby_max); + const core_h_subpx: usize = @intCast(core_suby_max - core_suby_min); + const domain = try SubpxFrameDomain.init( + fields_num, + .{ @intCast(image_w_subpx), @intCast(core_h_subpx) }, + 1, + 0, + ); + const storage_w_subpx = try std.math.add( + usize, + domain.image_w_subpx, + try std.math.mul(usize, halo_subpx_x, 2), + ); + const storage_h_subpx = try std.math.add( + usize, + domain.image_h_subpx, + try std.math.mul(usize, halo_subpx_y, 2), + ); + const stripe_domain = SubpxFrameDomain{ + .fields_num = fields_num, + .image_w_subpx = domain.image_w_subpx, + .image_h_subpx = domain.image_h_subpx, + .halo_subpx = 0, + .storage_w_subpx = storage_w_subpx, + .storage_h_subpx = storage_h_subpx, + }; + + return .{ + .target = try SubpxTarget.init( + outer_alloc, + stripe_domain, + -@as(i32, @intCast(halo_subpx_x)), + core_suby_min - @as(i32, @intCast(halo_subpx_y)), + background_value, + ), + .core_suby_min = core_suby_min, + .core_suby_max = core_suby_max, + }; + } + + pub fn deinit( + self: *SubpxStripe, + outer_alloc: std.mem.Allocator, + ) void { + self.target.deinit(outer_alloc); + self.* = undefined; + } + + pub fn reset( + self: *SubpxStripe, + core_suby_min: i32, + core_suby_max: i32, + halo_subpx_y: usize, + background_value: F, + ) void { + const core_h_subpx: usize = @intCast(core_suby_max - core_suby_min); + std.debug.assert(core_suby_min < core_suby_max); + std.debug.assert( + core_h_subpx + 2 * halo_subpx_y <= + self.target.domain.storage_h_subpx, + ); + + @memset(self.target.image.slice, background_value); + self.target.global_suby_min = core_suby_min - @as( + i32, + @intCast(halo_subpx_y), + ); + self.core_suby_min = core_suby_min; + self.core_suby_max = core_suby_max; + } +}; + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +test "SubpxTarget maps global coordinates through outer halo" { + const domain = try SubpxFrameDomain.init(1, .{ 3, 2 }, 4, 2); + var target = try SubpxTarget.init( + std.testing.allocator, + domain, + -2, + -2, + 0.0, + ); + defer target.deinit(std.testing.allocator); + + try std.testing.expectEqual(@as(usize, 0), target.flatIndex(-2, -2)); + try std.testing.expectEqual(@as(usize, 2), target.flatIndex(0, -2)); + try std.testing.expectEqual( + domain.storage_w_subpx + 2, + target.flatIndex(0, -1), + ); +} + +test "SubpxStripe covers its core plus only stripe-edge halos" { + var stripe = try SubpxStripe.init( + std.testing.allocator, + 1, + 24, + 8, + 16, + 2, + 3, + 0.0, + ); + defer stripe.deinit(std.testing.allocator); + + try std.testing.expectEqual(@as(i32, -2), stripe.target.global_subx_min); + try std.testing.expectEqual(@as(i32, 5), stripe.target.global_suby_min); + try std.testing.expectEqual(@as(usize, 28), stripe.target.domain.storage_w_subpx); + try std.testing.expectEqual(@as(usize, 14), stripe.target.domain.storage_h_subpx); + try std.testing.expectEqual(@as(i32, 8), stripe.core_suby_min); + try std.testing.expectEqual(@as(i32, 16), stripe.core_suby_max); +} + +test "SubpxStripe reset reuses storage for a shorter final stripe" { + var stripe = try SubpxStripe.init( + std.testing.allocator, + 1, + 12, + 0, + 8, + 1, + 2, + 0.0, + ); + defer stripe.deinit(std.testing.allocator); + + stripe.target.image.slice[0] = 3.0; + stripe.reset(8, 12, 2, -1.0); + + try std.testing.expectEqual(@as(i32, 6), stripe.target.global_suby_min); + try std.testing.expectEqual(@as(i32, 8), stripe.core_suby_min); + try std.testing.expectEqual(@as(i32, 12), stripe.core_suby_max); + try std.testing.expectEqual(@as(F, -1.0), stripe.target.image.slice[0]); +} diff --git a/src/riley/zig/subpxtileops.zig b/src/riley/zig/subpxtileops.zig new file mode 100644 index 00000000..3fde333e --- /dev/null +++ b/src/riley/zig/subpxtileops.zig @@ -0,0 +1,233 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +const std = @import("std"); +const rops = @import("rasterops.zig"); + +// -------------------------------------------------------------------------------------- +// Public Constants & Public Types +// -------------------------------------------------------------------------------------- + +pub const SubpxTile = struct { + subx_min: i32, + subx_max: i32, + suby_min: i32, + suby_max: i32, + overlap_start: usize = 0, + overlap_count: usize = 0, +}; + +pub const SubpxOverlap = struct { + mesh_idx: usize, + elem_idx: usize, + subx_min: i32, + subx_max: i32, + suby_min: i32, + suby_max: i32, +}; + +pub const SubpxTilingOverlaps = struct { + tiles: []SubpxTile, + overlaps: []SubpxOverlap, + + pub fn deinit( + self: *SubpxTilingOverlaps, + outer_alloc: std.mem.Allocator, + ) void { + outer_alloc.free(self.tiles); + outer_alloc.free(self.overlaps); + self.* = undefined; + } +}; + +// -------------------------------------------------------------------------------------- +// Public Entry-Point Func +// -------------------------------------------------------------------------------------- + +pub fn calcTiles( + outer_alloc: std.mem.Allocator, + subx_min: i32, + subx_max: i32, + suby_min: i32, + suby_max: i32, + tile_size_subpx: u16, +) ![]SubpxTile { + std.debug.assert(subx_min < subx_max); + std.debug.assert(suby_min < suby_max); + std.debug.assert(tile_size_subpx > 0); + + const tile_size: i32 = tile_size_subpx; + const tiles_x = try std.math.divCeil( + usize, + @intCast(subx_max - subx_min), + tile_size_subpx, + ); + const tiles_y = try std.math.divCeil( + usize, + @intCast(suby_max - suby_min), + tile_size_subpx, + ); + const tiles_num = try std.math.mul(usize, tiles_x, tiles_y); + const tiles = try outer_alloc.alloc(SubpxTile, tiles_num); + + for (0..tiles_y) |yy| { + const tile_suby_min = suby_min + @as(i32, @intCast(yy)) * tile_size; + const tile_suby_max = @min(tile_suby_min + tile_size, suby_max); + for (0..tiles_x) |xx| { + const tile_subx_min = subx_min + @as(i32, @intCast(xx)) * tile_size; + const tile_subx_max = @min(tile_subx_min + tile_size, subx_max); + tiles[yy * tiles_x + xx] = .{ + .subx_min = tile_subx_min, + .subx_max = tile_subx_max, + .suby_min = tile_suby_min, + .suby_max = tile_suby_max, + }; + } + } + + return tiles; +} + +pub fn elemBBoxToSubpx( + elem_bbox: rops.ElemBBox, + sub_samp: u32, +) !SubpxOverlap { + const sub_samp_i: i32 = @intCast(sub_samp); + return .{ + .mesh_idx = 0, + .elem_idx = elem_bbox.elem_idx, + .subx_min = try std.math.mul(i32, elem_bbox.x_min, sub_samp_i), + .subx_max = try std.math.mul(i32, elem_bbox.x_max, sub_samp_i), + .suby_min = try std.math.mul(i32, elem_bbox.y_min, sub_samp_i), + .suby_max = try std.math.mul(i32, elem_bbox.y_max, sub_samp_i), + }; +} + +pub fn binElemOverlaps( + outer_alloc: std.mem.Allocator, + tiles_inp: []const SubpxTile, + elem_bboxes_by_mesh: []const []const rops.ElemBBox, + sub_samp: u32, +) !SubpxTilingOverlaps { + std.debug.assert(sub_samp > 0); + const tiles = try outer_alloc.dupe(SubpxTile, tiles_inp); + errdefer outer_alloc.free(tiles); + + var overlaps_num: usize = 0; + for (tiles) |*tile| { + var tile_overlaps: usize = 0; + for (elem_bboxes_by_mesh) |elem_bboxes| { + for (elem_bboxes) |elem_bbox| { + const overlap = try elemBBoxToSubpx(elem_bbox, sub_samp); + if (rangesIntersect(tile, overlap)) { + tile_overlaps += 1; + } + } + } + tile.overlap_start = overlaps_num; + tile.overlap_count = tile_overlaps; + overlaps_num = try std.math.add(usize, overlaps_num, tile_overlaps); + } + + const overlaps = try outer_alloc.alloc(SubpxOverlap, overlaps_num); + errdefer outer_alloc.free(overlaps); + for (tiles) |tile| { + var overlap_idx = tile.overlap_start; + for (elem_bboxes_by_mesh, 0..) |elem_bboxes, mesh_idx| { + for (elem_bboxes) |elem_bbox| { + var overlap = try elemBBoxToSubpx(elem_bbox, sub_samp); + if (!rangesIntersect(&tile, overlap)) continue; + + overlap.mesh_idx = mesh_idx; + overlap.subx_min = @max(overlap.subx_min, tile.subx_min); + overlap.subx_max = @min(overlap.subx_max, tile.subx_max); + overlap.suby_min = @max(overlap.suby_min, tile.suby_min); + overlap.suby_max = @min(overlap.suby_max, tile.suby_max); + overlaps[overlap_idx] = overlap; + overlap_idx += 1; + } + } + std.debug.assert(overlap_idx == tile.overlap_start + tile.overlap_count); + } + + return .{ .tiles = tiles, .overlaps = overlaps }; +} + +fn rangesIntersect( + tile: *const SubpxTile, + overlap: SubpxOverlap, +) bool { + return overlap.subx_min < tile.subx_max and + overlap.subx_max > tile.subx_min and + overlap.suby_min < tile.suby_max and + overlap.suby_max > tile.suby_min; +} + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +test "calcTiles partitions a sub-pixel rectangle without halo" { + const tiles = try calcTiles(std.testing.allocator, -2, 7, -1, 5, 4); + defer std.testing.allocator.free(tiles); + + try std.testing.expectEqual(@as(usize, 6), tiles.len); + try std.testing.expectEqual(SubpxTile{ + .subx_min = -2, + .subx_max = 2, + .suby_min = -1, + .suby_max = 3, + }, tiles[0]); + try std.testing.expectEqual(SubpxTile{ + .subx_min = 6, + .subx_max = 7, + .suby_min = 3, + .suby_max = 5, + }, tiles[5]); +} + +test "elemBBoxToSubpx scales conservative pixel bounds" { + const overlap = try elemBBoxToSubpx(.{ + .elem_idx = 3, + .x_min = -2, + .x_max = 7, + .y_min = 1, + .y_max = 4, + }, 8); + + try std.testing.expectEqual(@as(i32, -16), overlap.subx_min); + try std.testing.expectEqual(@as(i32, 56), overlap.subx_max); + try std.testing.expectEqual(@as(i32, 8), overlap.suby_min); + try std.testing.expectEqual(@as(i32, 32), overlap.suby_max); +} + +test "binElemOverlaps gives each halo-free tile clipped overlaps" { + const tiles = try calcTiles(std.testing.allocator, 0, 8, 0, 4, 4); + defer std.testing.allocator.free(tiles); + const mesh0 = [_]rops.ElemBBox{ + .{ .elem_idx = 0, .x_min = 1, .x_max = 3, .y_min = 0, .y_max = 2 }, + .{ .elem_idx = 1, .x_min = 3, .x_max = 5, .y_min = 1, .y_max = 3 }, + }; + const mesh1 = [_]rops.ElemBBox{ + .{ .elem_idx = 0, .x_min = 6, .x_max = 8, .y_min = 0, .y_max = 4 }, + }; + const meshes = [_][]const rops.ElemBBox{ &mesh0, &mesh1 }; + var tiling = try binElemOverlaps( + std.testing.allocator, + tiles, + &meshes, + 1, + ); + defer tiling.deinit(std.testing.allocator); + + try std.testing.expectEqual(@as(usize, 4), tiling.overlaps.len); + try std.testing.expectEqual(@as(usize, 2), tiling.tiles[0].overlap_count); + try std.testing.expectEqual(@as(usize, 2), tiling.tiles[1].overlap_count); + try std.testing.expectEqual(@as(i32, 4), tiling.overlaps[1].subx_max); + try std.testing.expectEqual(@as(usize, 1), tiling.overlaps[3].mesh_idx); +} diff --git a/src/riley/zig/validateinput.zig b/src/riley/zig/validateinput.zig index 75a13ebd..55357316 100644 --- a/src/riley/zig/validateinput.zig +++ b/src/riley/zig/validateinput.zig @@ -87,6 +87,40 @@ pub fn checkRenderInpsErr( return error.InvalidTileSizeOverride; } } + if (config.global_subpx_tile_size_min == 0) { + return error.InvalidGlobalSubpxTileSizeMin; + } + if (config.global_subpx_tile_size_max == 0) { + return error.InvalidGlobalSubpxTileSizeMax; + } + if (config.global_subpx_tile_size_min > config.global_subpx_tile_size_max) { + return error.InvalidGlobalSubpxTileSizeRange; + } + if (config.global_subpx_tile_size_override) |tile_size_override| { + if (tile_size_override < config.global_subpx_tile_size_min or + tile_size_override > config.global_subpx_tile_size_max) + { + return error.InvalidGlobalSubpxTileSizeOverride; + } + } + if (config.global_subpx_stripe_size_min == 0) { + return error.InvalidGlobalSubpxStripeSizeMin; + } + if (config.global_subpx_stripe_size_max == 0) { + return error.InvalidGlobalSubpxStripeSizeMax; + } + if (config.global_subpx_stripe_size_min > + config.global_subpx_stripe_size_max) + { + return error.InvalidGlobalSubpxStripeSizeRange; + } + if (config.global_subpx_stripe_size_override) |stripe_size_override| { + if (stripe_size_override < config.global_subpx_stripe_size_min or + stripe_size_override > config.global_subpx_stripe_size_max) + { + return error.InvalidGlobalSubpxStripeSizeOverride; + } + } if (!std.math.isFinite(config.background_value)) { return error.InvalidBackgroundValue; } @@ -101,6 +135,7 @@ pub fn checkRenderInpsErr( for (cam_inps) |cam_inp| { try checkCamInpErr(cam_inp); + try checkGlobalSubpxAlignment(config, cam_inp.sub_sample); } const num_time = mo.countFrames(meshes); @@ -177,6 +212,29 @@ pub fn checkRenderInpsAssert( std.debug.assert(tile_size_override >= config.tile_size_min); std.debug.assert(tile_size_override <= config.tile_size_max); } + std.debug.assert(config.global_subpx_tile_size_min > 0); + std.debug.assert(config.global_subpx_tile_size_max > 0); + std.debug.assert( + config.global_subpx_tile_size_min <= config.global_subpx_tile_size_max, + ); + if (config.global_subpx_tile_size_override) |tile_size_override| { + std.debug.assert(tile_size_override >= config.global_subpx_tile_size_min); + std.debug.assert(tile_size_override <= config.global_subpx_tile_size_max); + } + std.debug.assert(config.global_subpx_stripe_size_min > 0); + std.debug.assert(config.global_subpx_stripe_size_max > 0); + std.debug.assert( + config.global_subpx_stripe_size_min <= + config.global_subpx_stripe_size_max, + ); + if (config.global_subpx_stripe_size_override) |stripe_size_override| { + std.debug.assert( + stripe_size_override >= config.global_subpx_stripe_size_min, + ); + std.debug.assert( + stripe_size_override <= config.global_subpx_stripe_size_max, + ); + } std.debug.assert(std.math.isFinite(config.background_value)); if (config.save_strategy == .disk or config.save_strategy == .both) { std.debug.assert(config.image_save_opts.len > 0); @@ -187,6 +245,7 @@ pub fn checkRenderInpsAssert( for (cam_inps) |cam_inp| { checkCamInpAssert(cam_inp); + checkGlobalSubpxAlignment(config, cam_inp.sub_sample) catch unreachable; } const num_time = mo.countFrames(meshes); @@ -216,6 +275,26 @@ pub fn checkRenderInpsAssert( }; } +fn checkGlobalSubpxAlignment( + config: rastcfg.RasterConfig, + sub_samp: u32, +) !void { + if (config.buffer_mode == .tile_local) return; + + const tile_size = config.global_subpx_tile_size_override orelse + config.global_subpx_tile_size_min; + if (@mod(tile_size, sub_samp) != 0) { + return error.GlobalSubpxTileSizeNotAligned; + } + if (config.buffer_mode == .global_subpx_stripe) { + const stripe_size = config.global_subpx_stripe_size_override orelse + config.global_subpx_stripe_size_min; + if (@mod(stripe_size, sub_samp) != 0) { + return error.GlobalSubpxStripeSizeNotAligned; + } + } +} + // -------------------------------------------------------------------------------------- // Generic Low-Level Helpers // -------------------------------------------------------------------------------------- diff --git a/src/run_all_demos.zig b/src/run_all_demos.zig index 37503694..8d90523d 100644 --- a/src/run_all_demos.zig +++ b/src/run_all_demos.zig @@ -1,6 +1,7 @@ const std = @import("std"); const demo_sphere200 = @import("demo_sphere200.zig"); +const demo_psf = @import("demo_psf.zig"); const demo_rabbits = @import("demo_rabbits.zig"); const demo_rabbits_fields = @import("demo_rabbits_fields.zig"); const demo_rabbits_rgb = @import("demo_rabbits_rgb.zig"); @@ -9,6 +10,7 @@ const demo_stereocal = @import("demo_stereocal.zig"); pub fn main(init: std.process.Init) !void { try demo_sphere200.main(init); + try demo_psf.main(init); try demo_rabbits.main(init); try demo_rabbits_rgb.main(init); try demo_rabbits_fields.main(init); diff --git a/src/test_bench.zig b/src/test_bench.zig index 6b508c2b..61f3b817 100644 --- a/src/test_bench.zig +++ b/src/test_bench.zig @@ -180,7 +180,7 @@ test "Unified Benchmark Tests" { config; if (common.shouldRun(run_config, mt, st, sc, data_dir)) { - var r_config = tcfg.getRasterConfig(.bench); + var r_config = tcfg.getRasterConfig(.testing); // Bench tests compare the returned in-memory image directly // against gold, so avoid the disk-save path here. r_config.save_strategy = .memory; diff --git a/src/test_demoframes.zig b/src/test_demoframes.zig new file mode 100644 index 00000000..cd29dc8f --- /dev/null +++ b/src/test_demoframes.zig @@ -0,0 +1,11 @@ +// -------------------------------------------------------------------------------------- +// Riley: A High Performance Rasteriser for DIC UQ +// +// Copyright (c) 2025-2026 scepticalrabbit (Lloyd Fletcher) +// Licensed under the MIT License (see LICENSE file for details) +// +// Authors: scepticalrabbit (Lloyd Fletcher) +// -------------------------------------------------------------------------------------- +test { + _ = @import("dev_support/demoframes.zig"); +} diff --git a/src/test_min.zig b/src/test_min.zig index cae66375..dccecc13 100644 --- a/src/test_min.zig +++ b/src/test_min.zig @@ -18,6 +18,7 @@ const gk = @import("riley/zig/geometrykernels.zig"); const iio = @import("riley/zig/imageio.zig"); const texops = @import("riley/zig/textureops.zig"); const Rotation = @import("riley/zig/rotation.zig").Rotation; +const BufferMode = @import("riley/zig/rasterconfig.zig").BufferMode; const simd_on = buildconfig.config.simd == .on; @@ -84,232 +85,249 @@ test "MIN Suite: sphere200 and multimesh" { .{ .sample = .quintic_bspline, .mode = .direct }, .{ .sample = .quintic_bspline, .mode = .lut_lerp }, }; + const buffer_modes = [_]BufferMode{ + .tile_local, + .global_subpx_full, + .global_subpx_stripe, + }; var total_fails: usize = 0; if (simd_on) { - std.debug.print("\nRunning MIN Suite sphere200/base tests...\n", .{}); - for (mesh_types) |mt| { - for (shader_types) |st| { - for (samp_cfgs) |sc| { - const folder_name = policy.meshName( - .benchmark_data, - mt, - ); - const data_dir = try std.fmt.allocPrint( - allocator, - "data/min/{s}_sphere200", - .{folder_name}, - ); - defer allocator.free(data_dir); - - const is_rgb = (st == .tex8_rgb or st == .nodal_rgb); - const is_allowed_rgb = (st == .nodal_rgb) or - (st == .tex8_rgb and - sc.sample == .cubic_catmull_rom and - sc.mode == .lut_lerp); - - if (is_rgb and !is_allowed_rgb) continue; - - if (common.shouldRun( - .{ .run = .all, .skip_quad4ibi_sphere = true }, - mt, - st, - sc, - data_dir, - )) { - var r_config = tcfg.getRasterConfig(.bench); - r_config.save_strategy = .memory; - r_config.image_save_opts = &[_]iio.ImageSaveOpts{}; - - const case_name = try minsuite.calcMinCaseName( - allocator, + for (buffer_modes) |buffer_mode| { + std.debug.print( + "Running MIN Suite sphere200/base tests ({s})...\n", + .{@tagName(buffer_mode)}, + ); + for (mesh_types) |mt| { + for (shader_types) |st| { + for (samp_cfgs) |sc| { + const folder_name = policy.meshName( + .benchmark_data, mt, - st, - sc, ); - defer allocator.free(case_name); - var result = try common.runBenchmarkQuiet( - u8, + const data_dir = try std.fmt.allocPrint( allocator, - io, + "data/min/{s}_sphere200", + .{folder_name}, + ); + defer allocator.free(data_dir); + + const is_rgb = (st == .tex8_rgb or st == .nodal_rgb); + const is_allowed_rgb = (st == .nodal_rgb) or + (st == .tex8_rgb and + sc.sample == .cubic_catmull_rom and + sc.mode == .lut_lerp); + + if (is_rgb and !is_allowed_rgb) continue; + + if (common.shouldRun( + .{ .run = .all, .skip_quad4ibi_sphere = true }, mt, st, sc, - null, data_dir, - render_defaults_sphere, - texture_grey, - texture_rgb, - r_config, - "", - ); - defer result.deinit(allocator); + )) { + var r_config = tcfg.getRasterConfig(.testing); + r_config.buffer_mode = buffer_mode; + r_config.save_strategy = .memory; + r_config.image_save_opts = &[_]iio.ImageSaveOpts{}; - const gold_case_dir = try std.fs.path.join( - allocator, - &[_][]const u8{ - gold_dir, - "sphere200", - "base", - case_name, - }, - ); - defer allocator.free(gold_case_dir); - const gold_fname = try tests.findGoldPath( - allocator, - io, - gold_case_dir, - 0, - 0, - 0, - is_rgb, - ); - defer allocator.free(gold_fname); + const case_name = try minsuite.calcMinCaseName( + allocator, + mt, + st, + sc, + ); + defer allocator.free(case_name); + var result = try common.runBenchmarkQuiet( + u8, + allocator, + io, + mt, + st, + sc, + null, + data_dir, + render_defaults_sphere, + texture_grey, + texture_rgb, + r_config, + "", + ); + defer result.deinit(allocator); - const channels: usize = if (is_rgb) 3 else 1; - tests.compareNDArrayToGold( - allocator, - io, - &result.image.?, - 0, - 0, - 0, - channels, - gold_fname, - tcfg.REL_TOL, - tcfg.ABS_TOL, - ) catch |err| { - try tests.saveComparisonArtifactsFromResult( + const gold_case_dir = try std.fs.path.join( + allocator, + &[_][]const u8{ + gold_dir, + "sphere200", + "base", + case_name, + }, + ); + defer allocator.free(gold_case_dir); + const gold_fname = try tests.findGoldPath( + allocator, + io, + gold_case_dir, + 0, + 0, + 0, + is_rgb, + ); + defer allocator.free(gold_fname); + + const channels: usize = if (is_rgb) 3 else 1; + tests.compareNDArrayToGold( allocator, io, - "fails", - case_name, &result.image.?, 0, 0, 0, - gold_fname, channels, - ); - if (err == error.PixelMismatch) { - total_fails += 1; - continue; - } - return err; - }; + gold_fname, + tcfg.REL_TOL, + tcfg.ABS_TOL, + ) catch |err| { + try tests.saveComparisonArtifactsFromResult( + allocator, + io, + "fails", + case_name, + &result.image.?, + 0, + 0, + 0, + gold_fname, + channels, + ); + if (err == error.PixelMismatch) { + total_fails += 1; + continue; + } + return err; + }; + } } } } } - std.debug.print("Running MIN Suite sphere200multicull tests...\n", .{}); - for (mesh_types) |mt| { - for (shader_types) |st| { - for (samp_cfgs) |sc| { - const folder_name = policy.meshName( - .benchmark_data, - mt, - ); - const data_dir = try std.fmt.allocPrint( - allocator, - "data/min/{s}_sphere200", - .{folder_name}, - ); - defer allocator.free(data_dir); - - const is_rgb = (st == .tex8_rgb or st == .nodal_rgb); - const is_allowed_rgb = (st == .nodal_rgb) or - (st == .tex8_rgb and - sc.sample == .cubic_catmull_rom and - sc.mode == .lut_lerp); - - if (is_rgb and !is_allowed_rgb) continue; - - if (common.shouldRun( - .{ .run = .all, .skip_quad4ibi_sphere = true }, - mt, - st, - sc, - data_dir, - )) { - var r_config = tcfg.getRasterConfig(.bench); - r_config.save_strategy = .memory; - r_config.image_save_opts = &[_]iio.ImageSaveOpts{}; - - const case_name = try minsuite.calcMinCaseName( - allocator, + for (buffer_modes) |buffer_mode| { + std.debug.print( + "Running MIN Suite sphere200multicull tests ({s})...\n", + .{@tagName(buffer_mode)}, + ); + for (mesh_types) |mt| { + for (shader_types) |st| { + for (samp_cfgs) |sc| { + const folder_name = policy.meshName( + .benchmark_data, mt, - st, - sc, ); - defer allocator.free(case_name); - var result = try minsuite.runSphere200MultiCullQuiet( + const data_dir = try std.fmt.allocPrint( allocator, - io, + "data/min/{s}_sphere200", + .{folder_name}, + ); + defer allocator.free(data_dir); + + const is_rgb = (st == .tex8_rgb or st == .nodal_rgb); + const is_allowed_rgb = (st == .nodal_rgb) or + (st == .tex8_rgb and + sc.sample == .cubic_catmull_rom and + sc.mode == .lut_lerp); + + if (is_rgb and !is_allowed_rgb) continue; + + if (common.shouldRun( + .{ .run = .all, .skip_quad4ibi_sphere = true }, mt, st, sc, data_dir, - pixel_num_sphere, - texture_grey, - texture_rgb, - r_config, - "", - 0.75, - ); - defer result.deinit(allocator); + )) { + var r_config = tcfg.getRasterConfig(.testing); + r_config.buffer_mode = buffer_mode; + r_config.save_strategy = .memory; + r_config.image_save_opts = &[_]iio.ImageSaveOpts{}; - const gold_case_dir = try std.fs.path.join( - allocator, - &[_][]const u8{ - gold_dir, - "sphere200multicull", - case_name, - }, - ); - defer allocator.free(gold_case_dir); - const gold_fname = try tests.findGoldPath( - allocator, - io, - gold_case_dir, - 0, - 0, - 0, - is_rgb, - ); - defer allocator.free(gold_fname); + const case_name = try minsuite.calcMinCaseName( + allocator, + mt, + st, + sc, + ); + defer allocator.free(case_name); + var result = try minsuite.runSphere200MultiCullQuiet( + allocator, + io, + mt, + st, + sc, + data_dir, + pixel_num_sphere, + texture_grey, + texture_rgb, + r_config, + "", + 0.75, + ); + defer result.deinit(allocator); - const channels: usize = if (is_rgb) 3 else 1; - tests.compareNDArrayToGold( - allocator, - io, - &result.image.?, - 0, - 0, - 0, - channels, - gold_fname, - tcfg.REL_TOL, - tcfg.ABS_TOL, - ) catch |err| { - try tests.saveComparisonArtifactsFromResult( + const gold_case_dir = try std.fs.path.join( + allocator, + &[_][]const u8{ + gold_dir, + "sphere200multicull", + case_name, + }, + ); + defer allocator.free(gold_case_dir); + const gold_fname = try tests.findGoldPath( + allocator, + io, + gold_case_dir, + 0, + 0, + 0, + is_rgb, + ); + defer allocator.free(gold_fname); + + const channels: usize = if (is_rgb) 3 else 1; + tests.compareNDArrayToGold( allocator, io, - "fails", - case_name, &result.image.?, 0, 0, 0, - gold_fname, channels, - ); - if (err == error.PixelMismatch) { - total_fails += 1; - continue; - } - return err; - }; + gold_fname, + tcfg.REL_TOL, + tcfg.ABS_TOL, + ) catch |err| { + try tests.saveComparisonArtifactsFromResult( + allocator, + io, + "fails", + case_name, + &result.image.?, + 0, + 0, + 0, + gold_fname, + channels, + ); + if (err == error.PixelMismatch) { + total_fails += 1; + continue; + } + return err; + }; + } } } } @@ -330,7 +348,7 @@ test "MIN Suite: sphere200 and multimesh" { "data/min/quad9_twoelems/", }; - { + for (buffer_modes) |buffer_mode| { tests.runMultimeshTestExt( allocator, io, @@ -339,6 +357,7 @@ test "MIN Suite: sphere200 and multimesh" { pixel_num_multi, tcfg.REL_TOL, tcfg.ABS_TOL, + buffer_mode, ) catch |err| { total_fails += 1; if (err != error.PixelMismatch) { @@ -347,7 +366,7 @@ test "MIN Suite: sphere200 and multimesh" { }; } - { + for (buffer_modes) |buffer_mode| { tests.runMultimeshMixedTestExt( allocator, io, @@ -356,6 +375,7 @@ test "MIN Suite: sphere200 and multimesh" { pixel_num_multi, tcfg.REL_TOL, tcfg.ABS_TOL, + buffer_mode, ) catch |err| { total_fails += 1; if (err != error.PixelMismatch) { @@ -364,7 +384,7 @@ test "MIN Suite: sphere200 and multimesh" { }; } - { + for (buffer_modes) |buffer_mode| { tests.runMultimeshMixedRGBTestExt( allocator, io, @@ -373,6 +393,7 @@ test "MIN Suite: sphere200 and multimesh" { pixel_num_multi, tcfg.REL_TOL, tcfg.ABS_TOL, + buffer_mode, ) catch |err| { total_fails += 1; if (err != error.PixelMismatch) { diff --git a/src/test_sin_approx.zig b/src/test_sin_approx.zig index 054ba2ee..afefa023 100644 --- a/src/test_sin_approx.zig +++ b/src/test_sin_approx.zig @@ -153,7 +153,7 @@ fn renderSinImage( .distortion = camera.distortion, }; - var config = tcfg.getRasterConfig(.bench); + var config = tcfg.getRasterConfig(.testing); config.save_strategy = .memory; config.image_save_opts = &[_]iio.ImageSaveOpts{}; diff --git a/src/tests/test_gold_psf.zig b/src/tests/test_gold_psf.zig index 9fb957da..396bd65c 100644 --- a/src/tests/test_gold_psf.zig +++ b/src/tests/test_gold_psf.zig @@ -9,6 +9,7 @@ const std = @import("std"); const buildconfig = @import("../riley/zig/buildconfig.zig"); const F = buildconfig.F; +const riley = @import("../riley/zig/riley.zig"); const Timestamp = std.Io.Clock.Timestamp; const common = @import("../dev_support/tests.zig"); const suite = @import("../dev_support/psfsuite.zig"); @@ -34,6 +35,8 @@ test "Gold PSF Suite" { std.debug.print("Running Gold PSF Tests...\n", .{}); const suite_start = Timestamp.now(io, .awake); + const tile_local_start = Timestamp.now(io, .awake); + var tile_local_cases: usize = 0; for (suite.distortion_cases) |distortion_case| { for (distortion_case.mesh_types) |mesh_type| { for (suite.shader_cases) |shader_case| { @@ -45,71 +48,98 @@ test "Gold PSF Suite" { .shader_case = shader_case, .psf_case = psf_case, }; - const case_dir_name = try suite.caseDirName(aa, render_case); - const gold_dir = try std.fmt.allocPrint( + try testCaseAgainstGold( + allocator, + io, aa, - "{s}/{s}", - .{ suite.gold_root, case_dir_name }, + render_case, + .tile_local, ); - const result = try suite.renderCase(allocator, io, render_case, null); - defer { - allocator.free(result.slice); - var result_mut = result; - result_mut.deinit(allocator); - } - - const frames_num = if (result.dims.len == 5) result.dims[1] else result.dims[0]; - var first_err: ?anyerror = null; - for (0..frames_num) |frame_idx| { - const gold_path = try common.findGoldPath( - aa, - io, - gold_dir, - 0, - frame_idx, - 0, - false, - ); - - common.compareNDArrayToGold( - allocator, - io, - &result, - 0, - frame_idx, - 0, - 1, - gold_path, - tcfg.REL_TOL, - tcfg.ABS_TOL, - ) catch |err| { - if (first_err == null) { - first_err = err; - } - const fail_dir_name = try std.fmt.allocPrint( - aa, - "psf_{s}", - .{case_dir_name}, - ); - try common.saveComparisonArtifactsFromResult( - aa, - io, - common.default_fails_root, - fail_dir_name, - &result, - 0, - frame_idx, - 0, - gold_path, - 1, - ); - }; - } - if (first_err) |err| return err; + tile_local_cases += 1; } } } } + printModeComplete( + io, + "tile_local", + tile_local_cases, + tile_local_start, + ); + + const global_cases = [_]suite.RenderCase{ + .{ + .distortion_case_name = "distort_bulge", + .mesh_type = .tri6, + .shader_case = suite.shader_cases[0], + .psf_case = suite.psf_cases[0], + }, + .{ + .distortion_case_name = "distort_bulge", + .mesh_type = .tri6, + .shader_case = suite.shader_cases[0], + .psf_case = suite.psf_cases[1], + }, + .{ + .distortion_case_name = "distort_bulge", + .mesh_type = .tri6, + .shader_case = suite.shader_cases[0], + .psf_case = suite.psf_cases[2], + }, + .{ + .distortion_case_name = "distort_bulge", + .mesh_type = .tri6, + .shader_case = suite.shader_cases[0], + .psf_case = suite.psf_cases[3], + }, + .{ + .distortion_case_name = "distort_shear", + .mesh_type = .quad8, + .shader_case = suite.shader_cases[1], + .psf_case = suite.psf_cases[0], + }, + .{ + .distortion_case_name = "distort_shear", + .mesh_type = .quad8, + .shader_case = suite.shader_cases[1], + .psf_case = suite.psf_cases[1], + }, + .{ + .distortion_case_name = "distort_shear", + .mesh_type = .quad8, + .shader_case = suite.shader_cases[1], + .psf_case = suite.psf_cases[2], + }, + .{ + .distortion_case_name = "distort_shear", + .mesh_type = .quad8, + .shader_case = suite.shader_cases[1], + .psf_case = suite.psf_cases[3], + }, + }; + const global_modes = [_]riley.BufferMode{ + .global_subpx_full, + .global_subpx_stripe, + }; + for (global_modes) |buffer_mode| { + _ = arena.reset(.retain_capacity); + const mode_start = Timestamp.now(io, .awake); + for (global_cases) |render_case| { + try testCaseAgainstGold( + allocator, + io, + aa, + render_case, + buffer_mode, + ); + } + printModeComplete( + io, + @tagName(buffer_mode), + global_cases.len, + mode_start, + ); + } const suite_end = Timestamp.now(io, .awake); const suite_ms = @as( @@ -119,6 +149,96 @@ test "Gold PSF Suite" { std.debug.print("Gold PSF Test Suite took {d:.3} ms\n", .{suite_ms}); } +fn testCaseAgainstGold( + outer_alloc: std.mem.Allocator, + io: std.Io, + alloc: std.mem.Allocator, + render_case: suite.RenderCase, + buffer_mode: riley.BufferMode, +) !void { + const case_dir_name = try suite.caseDirName(alloc, render_case); + const gold_dir = try std.fmt.allocPrint( + alloc, + "{s}/{s}", + .{ suite.gold_root, case_dir_name }, + ); + const result = try suite.renderCaseWithBufferMode( + outer_alloc, + io, + render_case, + null, + buffer_mode, + ); + defer { + outer_alloc.free(result.slice); + var result_mut = result; + result_mut.deinit(outer_alloc); + } + + const frames_num = if (result.dims.len == 5) result.dims[1] else result.dims[0]; + var first_err: ?anyerror = null; + for (0..frames_num) |frame_idx| { + const gold_path = try common.findGoldPath( + alloc, + io, + gold_dir, + 0, + frame_idx, + 0, + false, + ); + common.compareNDArrayToGold( + outer_alloc, + io, + &result, + 0, + frame_idx, + 0, + 1, + gold_path, + tcfg.REL_TOL, + tcfg.ABS_TOL, + ) catch |err| { + if (first_err == null) first_err = err; + const fail_dir_name = try std.fmt.allocPrint( + alloc, + "psf_{s}_{s}", + .{ case_dir_name, @tagName(buffer_mode) }, + ); + try common.saveComparisonArtifactsFromResult( + alloc, + io, + common.default_fails_root, + fail_dir_name, + &result, + 0, + frame_idx, + 0, + gold_path, + 1, + ); + }; + } + if (first_err) |err| return err; +} + +fn printModeComplete( + io: std.Io, + mode_name: []const u8, + cases_num: usize, + time_start: Timestamp, +) void { + const time_end = Timestamp.now(io, .awake); + const elapsed_ms = @as( + F, + @floatFromInt(time_start.durationTo(time_end).raw.nanoseconds), + ) / 1e6; + std.debug.print( + "PSF {s} complete: {d} cases ({d:.3} ms)\n", + .{ mode_name, cases_num, elapsed_ms }, + ); +} + test "PSF isotropic gaussian separable and non-separable agree" { const allocator = std.testing.allocator; const io = std.testing.io; @@ -170,6 +290,66 @@ test "PSF isotropic gaussian separable and non-separable agree" { ); } +test "PSF global buffer modes match tile-local output" { + const allocator = std.testing.allocator; + const io = std.testing.io; + const render_case = suite.RenderCase{ + .distortion_case_name = "distort_shear", + .mesh_type = .quad8, + .shader_case = suite.shader_cases[0], + .psf_case = suite.psf_cases[1], + }; + const tile_local = try suite.renderCaseWithBufferMode( + allocator, + io, + render_case, + suite.tile_size_small, + .tile_local, + ); + defer { + allocator.free(tile_local.slice); + var tile_local_mut = tile_local; + tile_local_mut.deinit(allocator); + } + const global_full = try suite.renderCaseWithBufferMode( + allocator, + io, + render_case, + suite.tile_size_small, + .global_subpx_full, + ); + defer { + allocator.free(global_full.slice); + var global_full_mut = global_full; + global_full_mut.deinit(allocator); + } + const global_stripe = try suite.renderCaseWithBufferMode( + allocator, + io, + render_case, + suite.tile_size_small, + .global_subpx_stripe, + ); + defer { + allocator.free(global_stripe.slice); + var global_stripe_mut = global_stripe; + global_stripe_mut.deinit(allocator); + } + + try suite.expectResultsApproxEq( + &tile_local, + &global_full, + PSF_REL_TOL, + PSF_ABS_TOL, + ); + try suite.expectResultsApproxEq( + &tile_local, + &global_stripe, + PSF_REL_TOL, + PSF_ABS_TOL, + ); +} + test "PSF gaussian checker render is invariant to tile size" { const allocator = std.testing.allocator; const io = std.testing.io; diff --git a/src/tests/test_gold_sphere.zig b/src/tests/test_gold_sphere.zig index f9d231a8..075905ed 100644 --- a/src/tests/test_gold_sphere.zig +++ b/src/tests/test_gold_sphere.zig @@ -146,7 +146,7 @@ test "Sphere Gold Tests" { defer allocator.free(gold_case_name); // 1. Run benchmark - var r_config = tcfg.getRasterConfig(.bench); + var r_config = tcfg.getRasterConfig(.testing); r_config.save_strategy = if (c.out.len > 0) .both else .memory; const test_dir_case = try std.fs.path.join( diff --git a/src/tests/test_visibility.zig b/src/tests/test_visibility.zig index 781eeab3..6e1d5764 100644 --- a/src/tests/test_visibility.zig +++ b/src/tests/test_visibility.zig @@ -35,7 +35,7 @@ test "MIN multi-cull render is unchanged by a halo-only bypass" { ); defer texture_rgb.deinit(allocator); - var config_base = tcfg.getRasterConfig(.bench); + var config_base = tcfg.getRasterConfig(.testing); config_base.save_strategy = .memory; config_base.image_save_opts = &[_]iio.ImageSaveOpts{}; var config_halo = config_base;