diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..28cb03b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,42 @@ +name: Documentation +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v5 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # setuptools_scm needs the tags + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install package and docs dependencies + run: pip install .[docs] + # Notebooks are converted with their saved outputs, not executed + - name: Convert example notebooks + run: jupyter nbconvert --to markdown docs/examples/*/*.ipynb + - name: Build site + run: zensical build --clean + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.gitignore b/.gitignore index 23c4cbd..0615338 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,11 @@ __pycache__/ .vscode/ interpcore.egg-info -_version.py \ No newline at end of file +_version.py + +# documentation build +site/ +.cache/ +# generated from the notebooks by nbconvert at build time +docs/examples/*/*.md +docs/examples/*/*_files/ \ No newline at end of file diff --git a/README.md b/README.md index 8c201f4..4c3c34b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ # InterpCore -A Python library for interpolating physical field data (electromagnetic forces, heat flux, etc.) between different mesh representations and exporting to ANSYS APDL format. +[![Documentation](https://img.shields.io/badge/docs-online-blue)](https://fusion4energy.github.io/InterpCore/) + +A Python library for interpolating physical field data (electromagnetic forces, heat +flux, heat generation, convection boundary conditions) between different mesh +representations and exporting the result to ANSYS APDL format. + +**Full documentation: ** ## Features -- **Multiple interpolation kernels**: Distance-weighted, FEM-based, K-nearest neighbors, closest point -- **Flexible query methods**: K-nearest neighbors or radius-based search -- **Support for multiple load types**: - - EM forces (3-component vector fields) - - Heat flux (scalar fields) - - Heat generation (volumetric) - - Heat Transfer Coefficient + bulk fluid temperature (convection BCs) -- **Analysis tools**: - - Scalar field integration for computing total heat generation, flux, etc. - - EM force resultant computation for validating force/moment conservation -- **Export to ANSYS APDL**: Direct export of interpolated results in APDL format -- **Visualization**: Built-in VTK export for ParaView or PyVista visualization -- **Efficient**: KDTree-based spatial queries for fast neighbor searches +- Multiple interpolation kernels: distance-weighted, FEM-based, average, weighted + average, closest point +- K-nearest or radius-based neighbour search (KDTree) +- EM forces, heat flux, heat generation and HTC + bulk temperature +- Force/moment resultants and scalar integrals to validate the interpolation +- Direct export to ANSYS APDL and VTK output for ParaView/PyVista ## Installation @@ -24,6 +23,8 @@ A Python library for interpolating physical field data (electromagnetic forces, pip install interpcore ``` +Requires Python ≥ 3.11. + ## Quick Start ```python @@ -33,204 +34,33 @@ from interpcore.config import ( ColumnsConfig, QUERY_TYPE, INTERPOLATED_LOAD_TYPE, + INTERPOLATION_KERNEL, ) -from interpcore.kernels import INTERPOLATION_KERNEL -# Configure interpolation config = InterpolationConfig( - method=QUERY_TYPE.K, # type of neighbour search - param=5, # parameter relative to the neighbour search (K or radius) - max_distance=2.0, # filter by a max radius of search (in case of K is used) - coincidence_tolerance=0.01, # tolerance to consider two nodes coincident - kernel=INTERPOLATION_KERNEL.DISTANCE_WEIGHTED, # How to interpolate - multithread=False, # use or not multithread - interpolated_load=INTERPOLATED_LOAD_TYPE.EM_FORCE, # type of load that is being interpolated + method=QUERY_TYPE.K, + param=5, + max_distance=2.0, + coincidence_tolerance=0.01, + kernel=INTERPOLATION_KERNEL.DISTANCE_WEIGHTED, + multithread=False, + interpolated_load=INTERPOLATED_LOAD_TYPE.EM_FORCE, ) - -# Define column indices for the input files columns = ColumnsConfig(id=0, dest_xyz=1, source_xyz=1, value=4) -# Create interpolator and run interpolator = Interpolator( path_to_src_folder="source_data", path_to_dest_mesh="destination_mesh.txt", config=config, columns=columns, ) - -# Interpolate all source files interpolator.interpolate_all() - -# Export to ANSYS format interpolator.export_to_ansys("output_directory") - -# Optional: Build VTK for visualization. If outdir=None they are not exported -interpolator.build_vtk_output(outdir="vtk_output") ``` -## Interpolation Configuration - -Two dataclass objects fully control how the interpolation is performed. - -### `InterpolationConfig` - -Controls the spatial search strategy, the interpolation kernel, and the target load type. - -| Parameter | Type | Description | -|---|---|---| -| `method` | `QUERY_TYPE` | Neighbour search strategy: `QUERY_TYPE.K` (K-nearest) or `QUERY_TYPE.RADIUS` (radius-based). | -| `param` | `int \| float` | Parameter for the chosen method: number of neighbours for `K`, radius (same unit as coordinates) for `RADIUS`. | -| `max_distance` | `float` | Maximum distance (same unit as coordinates) beyond which a destination node is not considered a neighbour, even when using `QUERY_TYPE.K`. Acts as a hard cutoff. | -| `coincidence_tolerance` | `float` | Distance below which two nodes are treated as coincident and the source value is copied directly without interpolation. | -| `kernel` | `INTERPOLATION_KERNEL` | Algorithm used to assign a value from the neighbourhood. See [Interpolation Kernels](#interpolation-kernels). | -| `multithread` | `bool` | Whether to parallelise the interpolation loop using threads. | -| `interpolated_load` | `INTERPOLATED_LOAD_TYPE` | Physical quantity being interpolated. Determines the number of components and the APDL export format. | -| `accept_no_neighbor` | `bool` | If `False` (default), raise an error when a destination node has no neighbour within `max_distance`. If `True`, assign zero to those nodes silently. | - -> **Note**: not every kernel is compatible with every load type. See [Kernel–Load Compatibility](#kernel-load-compatibility). - -### `ColumnsConfig` - -Describes the zero-based column indices in both the source data files and the destination mesh file. All three coordinate components (x, y, z) are expected in consecutive columns starting at the given index. - -| Parameter | Type | Description | -|---|---|---| -| `source_xyz` | `int` | Index of the **x** column in the source file; y and z must follow immediately. | -| `value` | `int` | Index of the column containing the scalar (or first component) value to interpolate from the source file. | -| `dest_xyz` | `int` | Index of the **x** column in the destination mesh file; y and z must follow immediately. | -| `id` | `int` | Index of the node-ID column in the destination mesh file. | -| `volume_area` | `int \| None` | Index of the volume/area column in the destination mesh file. Required for scalar integrals and for EM force density inputs. | - -## Interpolation checks and outputs - -## Analysis Methods - -After interpolation, InterpCore provides methods to analyze and validate results: - -### Scalar Integrals - -For scalar fields (heat flux, heat generation), you can compute the total integral over the destination mesh: - -```python -# Requires volume or area data in the destination mesh -columns = ColumnsConfig( - id=0, dest_xyz=1, source_xyz=1, value=4, volume_area=4 -) # volume_area points to the vol/area column - -interpolator = Interpolator( - path_to_src_folder="source_data", - path_to_dest_mesh="destination_mesh.txt", - config=config, - columns=columns, -) - -interpolator.interpolate_all() - -# Compute integrals (e.g., total heat generation in W) -integrals = interpolator.compute_scalar_integrals() -# Returns: {"data_001": array([total_value])} -``` - -**Note**: The destination mesh must include volume (for 3D elements) or area (for 2D elements) data. Use the APDL scripts in [`apdl-scripts/`](apdl-scripts/) to export element centroids with volumes and areas. - -### EM Force Resultants - -For EM force fields, you can compute force and moment resultants to verify conservation: - -```python -# After interpolation with EM_FORCE load type -resultants = interpolator.compute_EM_resultants(pole=np.array([0.0, 0.0, 0.0])) - -# Returns for each source file: -# { -# "force_001": { -# "R_F_EM": [Fx, Fy, Fz], # Total force from source data -# "R_F_Mech": [Fx, Fy, Fz], # Total force from interpolated data -# "R_M_EM": [Mx, My, Mz], # Total moment from source data -# "R_M_Mech": [Mx, My, Mz], # Total moment from interpolated data -# "f_err_comp": [ex, ey, ez], # Relative force error by component -# "m_err_comp": [ex, ey, ez], # Relative moment error by component -# "Unmapped_EM_Force": float # Norm of unmapped forces -# } -# } -``` - -This is useful for validating that the interpolation preserves global force and moment equilibrium. Small errors indicate good interpolation quality. - -## Examples - -Complete working examples with sample data are available in the [`doc/`](doc/) folder: - -- **[Heat Flux Example](doc/heat_flux/heat_flux.ipynb)**: Scalar field interpolation using the AVERAGE kernel -- **[Heat Generation Example](doc/heat_gen/heat_gen.ipynb)**: Volumetric heat generation using the CLOSEST kernel -- **[EM Force Example](doc/em_force/em_force.ipynb)**: Vector field interpolation with glyph visualization -- **[HTC Example](doc/htc/htc.ipynb)**: Convection boundary condition interpolation (HTC + bulk fluid temperature) - -Each example includes: -- Sample mesh files -- Sample data files -- Jupyter notebook with full workflow -- Visualization with PyVista - -## Configuration Options - -### Query Methods -- `QUERY_TYPE.K`: K-nearest neighbors (param = number of neighbors) -- `QUERY_TYPE.RADIUS`: Radius-based search (param = radius in same unit as coordinates) - -### Interpolation Kernels - -#### Source-to-target - -Each source point is distributed to destination neighbours: - -- `DISTANCE_WEIGHTED`: Weight by inverse distance -- `FEM`: FEM-based interpolation - - -#### Target-to-source -A value is assigned to each destination point based on source neighbours - -- `CLOSEST`: Use closest source point value -- `AVERAGE`: Simple average of neighbors -- `AVERAGE_WEIGHTED`: Average the neighbours values but weighting them by distance (the closer the more important). - -### Load Types -- `EM_FORCE`: 3-component vector fields (Fx, Fy, Fz). If "vol" column is provided the forces are - interpreted as force densities and will be multiplied by the volume. -- `HEAT_FLUX`: Scalar fields for surface heat flux -- `HEAT_GEN`: Scalar fields for volumetric heat generation -- `HTC`: 2-component convection boundary condition — Heat Transfer Coefficient and bulk fluid (reference) temperature. Exported as `SFE,,CONV,1` and `SFE,,CONV,2` in APDL. - -### Kernel-Load Compatibility - -Not every kernel can be used with every load type. The kernels are divided into two families based on the direction of the mapping: - -| Kernel | Family | Compatible loads | -|---|---|---| -| `DISTANCE_WEIGHTED` | Source-to-target | `EM_FORCE` only | -| `FEM` | Source-to-target | `EM_FORCE` only | -| `AVERAGE` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | -| `AVERAGE_WEIGHTED` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | -| `CLOSEST` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | - -Source-to-target kernels distribute each source point's contribution onto its destination neighbours, which is suited to force conservation in EM applications. Target-to-source kernels assign a value to each destination node by aggregating its source neighbours, which is better suited to scalar field interpolation. Using an incompatible combination raises a `ConfigurationError` at construction time. - -## File Format - -The file format is pretty free, header, no header, commas, tabs.... -The important part is that the correct index columns are specified when creating the interpolator. - -Destination mesh input files can be created using the apdl scripts included in this repository -[here](/apdl-scripts/). - -## Requirements - -- Python ≥ 3.10 -- scikit-learn -- pandas -- tqdm -- pyvista +See the [documentation](https://fusion4energy.github.io/InterpCore/) for the +configuration options, the kernels and load types, and the +[examples](docs/examples/) for complete Jupyter notebook workflows. ## License diff --git a/docs/api/config.md b/docs/api/config.md new file mode 100644 index 0000000..166702a --- /dev/null +++ b/docs/api/config.md @@ -0,0 +1,3 @@ +# Configuration + +::: interpcore.config diff --git a/docs/api/dest_tree.md b/docs/api/dest_tree.md new file mode 100644 index 0000000..77a238c --- /dev/null +++ b/docs/api/dest_tree.md @@ -0,0 +1,3 @@ +# Destination tree + +::: interpcore.dest_tree diff --git a/docs/api/errors.md b/docs/api/errors.md new file mode 100644 index 0000000..85df953 --- /dev/null +++ b/docs/api/errors.md @@ -0,0 +1,3 @@ +# Errors + +::: interpcore.errors diff --git a/docs/api/interpolator.md b/docs/api/interpolator.md new file mode 100644 index 0000000..a904c51 --- /dev/null +++ b/docs/api/interpolator.md @@ -0,0 +1,3 @@ +# Interpolator + +::: interpcore.interpolator diff --git a/docs/api/kernels.md b/docs/api/kernels.md new file mode 100644 index 0000000..6fa896e --- /dev/null +++ b/docs/api/kernels.md @@ -0,0 +1,8 @@ +# Kernels + +The kernel maths is described in the [user guide](../user-guide/kernels.md). + +::: interpcore.kernels + options: + members: + - interpolate_block diff --git a/docs/api/parsers.md b/docs/api/parsers.md new file mode 100644 index 0000000..ec89622 --- /dev/null +++ b/docs/api/parsers.md @@ -0,0 +1,3 @@ +# Parsers + +::: interpcore.parsers diff --git a/docs/assets/img/fem_kernel.svg b/docs/assets/img/fem_kernel.svg new file mode 100644 index 0000000..8e393aa --- /dev/null +++ b/docs/assets/img/fem_kernel.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + F₁ = K₁u + + F₂ = K₂u + + F₃ = K₃u + + + + F + + + kᵢ ∝ 1/dᵢ + Source point loaded by F, springs to fixed destination nodes + diff --git a/docs/assets/img/philosophies.svg b/docs/assets/img/philosophies.svg new file mode 100644 index 0000000..70ae34d --- /dev/null +++ b/docs/assets/img/philosophies.svg @@ -0,0 +1,56 @@ + + + + + + + + + + Source-to-target + one source value is distributed + + + + + + + + + + + + + F + + w₁F + w₂F + w₃F + w₄F + + Σ wᵢ = 1 → total force conserved + + + + Target-to-source + one destination value is assigned + + + + + + + + + + + q₁ + q₂ + q₃ + q₄ + + + q = f(q₁…q₄) + + average, weighted average or closest + diff --git a/docs/assets/img/query_methods.svg b/docs/assets/img/query_methods.svg new file mode 100644 index 0000000..8aed1a3 --- /dev/null +++ b/docs/assets/img/query_methods.svg @@ -0,0 +1,72 @@ + + + + QUERY_TYPE.K (K = 4) + + + max_distance + + + + + + + + + + + + + + + + + + + + + + + + + 4 nearest found; the 4th is discarded + because it is beyond max_distance + + + + QUERY_TYPE.RADIUS + + + + r = param + + + + + + + + + + + + + + + + + + + all points inside the sphere are neighbours + (their number varies, may be zero) + + + + + query point + + neighbour + + coincidence_tolerance + + diff --git a/doc/em_force/destination_mesh.txt b/docs/examples/em_force/destination_mesh.txt similarity index 100% rename from doc/em_force/destination_mesh.txt rename to docs/examples/em_force/destination_mesh.txt diff --git a/doc/em_force/em_force.ipynb b/docs/examples/em_force/em_force.ipynb similarity index 100% rename from doc/em_force/em_force.ipynb rename to docs/examples/em_force/em_force.ipynb diff --git a/doc/em_force/source_data/em_force_data.txt b/docs/examples/em_force/source_data/em_force_data.txt similarity index 100% rename from doc/em_force/source_data/em_force_data.txt rename to docs/examples/em_force/source_data/em_force_data.txt diff --git a/doc/heat_flux/destination_mesh.txt b/docs/examples/heat_flux/destination_mesh.txt similarity index 100% rename from doc/heat_flux/destination_mesh.txt rename to docs/examples/heat_flux/destination_mesh.txt diff --git a/doc/heat_flux/heat_flux.ipynb b/docs/examples/heat_flux/heat_flux.ipynb similarity index 100% rename from doc/heat_flux/heat_flux.ipynb rename to docs/examples/heat_flux/heat_flux.ipynb diff --git a/doc/heat_flux/source_data/heat_flux_data.txt b/docs/examples/heat_flux/source_data/heat_flux_data.txt similarity index 100% rename from doc/heat_flux/source_data/heat_flux_data.txt rename to docs/examples/heat_flux/source_data/heat_flux_data.txt diff --git a/doc/heat_flux/source_mesh.txt b/docs/examples/heat_flux/source_mesh.txt similarity index 100% rename from doc/heat_flux/source_mesh.txt rename to docs/examples/heat_flux/source_mesh.txt diff --git a/doc/heat_gen/destination_mesh.txt b/docs/examples/heat_gen/destination_mesh.txt similarity index 100% rename from doc/heat_gen/destination_mesh.txt rename to docs/examples/heat_gen/destination_mesh.txt diff --git a/doc/heat_gen/heat_gen.ipynb b/docs/examples/heat_gen/heat_gen.ipynb similarity index 100% rename from doc/heat_gen/heat_gen.ipynb rename to docs/examples/heat_gen/heat_gen.ipynb diff --git a/doc/heat_gen/source_data/heat_gen_data.txt b/docs/examples/heat_gen/source_data/heat_gen_data.txt similarity index 100% rename from doc/heat_gen/source_data/heat_gen_data.txt rename to docs/examples/heat_gen/source_data/heat_gen_data.txt diff --git a/doc/htc/destination_mesh.txt b/docs/examples/htc/destination_mesh.txt similarity index 100% rename from doc/htc/destination_mesh.txt rename to docs/examples/htc/destination_mesh.txt diff --git a/doc/htc/htc.ipynb b/docs/examples/htc/htc.ipynb similarity index 100% rename from doc/htc/htc.ipynb rename to docs/examples/htc/htc.ipynb diff --git a/doc/htc/source_data/htc_data.txt b/docs/examples/htc/source_data/htc_data.txt similarity index 100% rename from doc/htc/source_data/htc_data.txt rename to docs/examples/htc/source_data/htc_data.txt diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..9e17572 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,52 @@ +# Examples + +Complete workflows with sample data. Each example folder contains a source data +folder, a destination mesh and a Jupyter notebook; the pages below are rendered from +the notebooks. + +
+ +- **Heat flux** + + --- + + Scalar surface load interpolated with the `AVERAGE` kernel. + + [:octicons-arrow-right-24: Example](heat_flux/heat_flux.md) · + [:octicons-download-24: Notebook](heat_flux/heat_flux.ipynb) + +- **Heat generation** + + --- + + Volumetric heat generation interpolated with the `CLOSEST` kernel, with total + power check. + + [:octicons-arrow-right-24: Example](heat_gen/heat_gen.md) · + [:octicons-download-24: Notebook](heat_gen/heat_gen.ipynb) + +- **EM force** + + --- + + Vector field interpolation with force/moment resultants and glyph + visualisation. + + [:octicons-arrow-right-24: Example](em_force/em_force.md) · + [:octicons-download-24: Notebook](em_force/em_force.ipynb) + +- **HTC** + + --- + + Convection boundary condition: Heat Transfer Coefficient and bulk fluid + temperature. + + [:octicons-arrow-right-24: Example](htc/htc.md) · + [:octicons-download-24: Notebook](htc/htc.ipynb) + +
+ +To run the examples locally, clone the repository and open the notebooks from the +[`docs/examples/`](https://github.com/Fusion4Energy/InterpCore/tree/main/docs/examples) +folder, so that the relative paths to the data files are correct. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..8f7d257 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,58 @@ +# Getting started + +A complete interpolation consists of four steps: + +1. describe **how** to interpolate with an [`InterpolationConfig`](user-guide/configuration.md#interpolationconfig); +2. describe **where** the data are in the input files with a [`ColumnsConfig`](user-guide/configuration.md#columnsconfig); +3. create an [`Interpolator`](api/interpolator.md) and run it; +4. export the results to APDL and, optionally, to VTK. + +```python +from interpcore.interpolator import Interpolator +from interpcore.config import ( + InterpolationConfig, + ColumnsConfig, + QUERY_TYPE, + INTERPOLATED_LOAD_TYPE, + INTERPOLATION_KERNEL, +) + +# 1. How to interpolate +config = InterpolationConfig( + method=QUERY_TYPE.K, # type of neighbour search + param=5, # K for QUERY_TYPE.K, radius for QUERY_TYPE.RADIUS + max_distance=2.0, # hard cut-off on neighbour distance + coincidence_tolerance=0.01, # below this distance nodes are coincident + kernel=INTERPOLATION_KERNEL.DISTANCE_WEIGHTED, + multithread=False, + interpolated_load=INTERPOLATED_LOAD_TYPE.EM_FORCE, +) + +# 2. Where the data are (0-based column indices) +columns = ColumnsConfig(id=0, dest_xyz=1, source_xyz=1, value=4) + +# 3. Interpolate every file found in the source folder +interpolator = Interpolator( + path_to_src_folder="source_data", + path_to_dest_mesh="destination_mesh.txt", + config=config, + columns=columns, +) +interpolator.interpolate_all() + +# 4. Export +interpolator.export_to_ansys("output_directory") +interpolator.build_vtk_output(outdir="vtk_output") # outdir=None: build only, no files +``` + +!!! tip "Multiple load cases" + `path_to_src_folder` can be a folder containing many source files (e.g. one per + time step or load case) sharing the same coordinates. The KDTree query is computed + only once and re-used for every file. A single file path is also accepted. + +## Next steps + +- Understand the [interpolation workflow](user-guide/how-it-works.md). +- Pick the right [kernel](user-guide/kernels.md) for your [load type](user-guide/load-types.md). +- [Validate the results](user-guide/analysis.md) with integrals and resultants. +- Browse the [examples](examples/index.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ae55805 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,73 @@ +# InterpCore + +**InterpCore** is a Python library for interpolating physical field data +(electromagnetic forces, heat flux, heat generation, convection boundary conditions) +from a *source* point cloud onto a *destination* mechanical mesh, and for exporting +the result directly as ANSYS APDL load commands. + +```mermaid +flowchart LR + S[("Source data
(x, y, z, value)")] --> P[Parsers] + D[("Destination mesh
(id, x, y, z, [vol/area])")] --> P + P --> T["KDTree
neighbour query
(K or radius)"] + T --> K["Interpolation kernel"] + K --> A["APDL export
F / SFE / BFE"] + K --> V["VTK / PyVista
visualisation"] + K --> C["Checks
integrals, resultants"] +``` + +
+ +- :lucide-rocket: **Getting started** + + --- + + Install the package and run your first interpolation in a few lines. + + [:octicons-arrow-right-24: Quick start](getting-started.md) + +- :lucide-git-compare-arrows: **Interpolation kernels** + + --- + + Source-to-target and target-to-source kernels, with the maths behind them. + + [:octicons-arrow-right-24: Kernels](user-guide/kernels.md) + +- :lucide-zap: **Load types** + + --- + + EM forces, heat flux, heat generation and HTC with their APDL output format. + + [:octicons-arrow-right-24: Load types](user-guide/load-types.md) + +- :lucide-notebook-pen: **Examples** + + --- + + Complete Jupyter notebook workflows with sample data and PyVista plots. + + [:octicons-arrow-right-24: Examples](examples/index.md) + +
+ +## Features + +- **Multiple interpolation kernels**: distance-weighted, FEM-based, average, + weighted average, closest point. +- **Flexible neighbour search**: K-nearest neighbours or radius-based, with a hard + `max_distance` cut-off and coincident-node detection. +- **Multiple load types**: EM forces (3-component vectors), heat flux, heat generation + and Heat Transfer Coefficient + bulk temperature. +- **Analysis tools**: scalar field integration and EM force/moment resultants to + validate conservation. +- **Export to ANSYS APDL**: one ready-to-read command file per source file. +- **Visualisation**: VTK output for ParaView or PyVista. +- **Efficient**: KDTree-based spatial queries and optional multithreading. + +## License + +Licensed under the +[European Union Public Licence (EUPL) 1.2](https://github.com/Fusion4Energy/InterpCore/blob/main/LICENSE). +Developed by the F4E mechanical team. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..1f486dd --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,38 @@ +# Installation + +InterpCore requires **Python ≥ 3.11**. + +=== "pip" + + ```bash + pip install interpcore + ``` + +=== "From source" + + ```bash + git clone https://github.com/Fusion4Energy/InterpCore.git + cd InterpCore + pip install -e .[dev] + ``` + +## Dependencies + +Installed automatically: + +- [scikit-learn](https://scikit-learn.org/) (KDTree and distance computations) +- [pandas](https://pandas.pydata.org/) (file parsing) +- [tqdm](https://tqdm.github.io/) (progress bars) +- [PyVista](https://docs.pyvista.org/) (VTK output and visualisation) + +## Building this documentation + +The documentation is built with [Zensical](https://zensical.org/). The example pages +are generated from the Jupyter notebooks with `nbconvert`, using the outputs saved in +the notebooks (they are not re-executed). + +```bash +pip install -e .[docs] +jupyter nbconvert --to markdown docs/examples/*/*.ipynb +zensical serve +``` diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js new file mode 100644 index 0000000..0946ce0 --- /dev/null +++ b/docs/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true }, + ], + }); +}); diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..5802eef --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,9 @@ +/* Diagrams: keep SVGs readable in both light and dark schemes */ +.md-typeset img.diagram { + display: block; + margin: 1em auto; + max-width: 100%; + background: #ffffff; + border-radius: 0.4rem; + padding: 0.6rem; +} diff --git a/docs/user-guide/analysis.md b/docs/user-guide/analysis.md new file mode 100644 index 0000000..77dceed --- /dev/null +++ b/docs/user-guide/analysis.md @@ -0,0 +1,104 @@ +# Analysis and checks + +After `interpolate_all()`, the [`Interpolator`](../api/interpolator.md) offers methods +to check the quality of the interpolation. + +## Scalar integrals + +For scalar loads (`HEAT_FLUX`, `HEAT_GEN`), `compute_scalar_integrals()` integrates +the interpolated field over the destination mesh: + +$$ +Q = \sum_{e} q_e\, V_e +$$ + +where $V_e$ is the volume (or area) of element $e$. For heat generation in W/m³ and +volumes in m³, the result is the total power in W. Compare it with the same quantity +computed in the source model to check the interpolation. + +The `volume_area` column must be set in the +[`ColumnsConfig`](configuration.md#columnsconfig): + +```python +columns = ColumnsConfig(id=0, dest_xyz=1, source_xyz=1, value=4, volume_area=4) + +interpolator = Interpolator( + path_to_src_folder="source_data", + path_to_dest_mesh="destination_mesh.txt", + config=config, + columns=columns, +) +interpolator.interpolate_all() + +integrals = interpolator.compute_scalar_integrals() +# {"data_001": array([total_value])} +``` + +An `IncompatibleResultsError` is raised for multi-component loads or when no volume or +area is available. + +!!! tip + Element centroids with areas and volumes can be exported with the + [`export_centroids.apdl`](file-formats.md#apdl-export-scripts) script. + +## EM force resultants + +For `EM_FORCE`, `compute_EM_resultants(pole)` compares the total force and moment of +the source data with those of the interpolated nodal forces: + +$$ +\mathbf{R}_F = \sum_i \mathbf{F}_i, +\qquad +\mathbf{R}_M = \sum_i (\mathbf{x}_i - \mathbf{p}) \times \mathbf{F}_i +$$ + +where $\mathbf{p}$ is the pole (origin by default). + +```python +import numpy as np + +resultants = interpolator.compute_EM_resultants(pole=np.array([0.0, 0.0, 0.0])) +``` + +For each source file, the returned dictionary contains: + +| Key | Description | +|---|---| +| `R_F_EM` | Total force of the source data $[F_x, F_y, F_z]$ | +| `R_F_Mech` | Total force of the interpolated data | +| `R_M_EM` | Total moment of the source data about the pole $[M_x, M_y, M_z]$ | +| `R_M_Mech` | Total moment of the interpolated data about the pole | +| `f_err_comp` | Relative force error per component, $(R_{F,EM} - R_{F,Mech}) / R_{F,EM}$ | +| `m_err_comp` | Relative moment error per component | +| `Unmapped_EM_Force` | Norm of the sum of the source forces that found no destination neighbour | + +Both source-to-target kernels conserve the total force by construction, so a force +error usually means that part of the force was unmapped. The moment is not conserved +exactly, since forces are moved from the source points to the nodes: its error +depends on the distance between the two point sets and is a good indicator of the +interpolation quality. + +## Visualisation + +`build_vtk_output(outdir)` builds PyVista `PolyData` point clouds of both the source +and the interpolated data, stored in the `src_vtk` and `dest_vtk` dictionaries. If +`outdir` is given, they are also saved as `_src.vtk` and +`_interpolated.vtk` for ParaView. + +```python +import pyvista as pv + +interpolator.build_vtk_output() +name = "data_001" + +pl = pv.Plotter(shape=(1, 2)) +pl.subplot(0, 0) +pl.add_mesh(interpolator.src_vtk[name], scalars="Value") +pl.subplot(0, 1) +pl.add_mesh(interpolator.dest_vtk[name], scalars="Value") +pl.link_views() +pl.show() +``` + +For multi-component loads, each component is also stored as a separate +`Component_` array. diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md new file mode 100644 index 0000000..feb3b96 --- /dev/null +++ b/docs/user-guide/configuration.md @@ -0,0 +1,66 @@ +# Configuration + +Two dataclasses fully control how the interpolation is performed. Both are defined in +[`interpcore.config`](../api/config.md). + +## `InterpolationConfig` + +Controls the neighbour search strategy, the interpolation kernel and the target load +type. + +| Parameter | Type | Description | +|---|---|---| +| `method` | `QUERY_TYPE` | Neighbour search strategy: `QUERY_TYPE.K` (K-nearest) or `QUERY_TYPE.RADIUS` (radius-based). See [Query methods](query-methods.md). | +| `param` | `int \| float` | Parameter of the chosen method: number of neighbours for `K`, radius (same unit as the coordinates) for `RADIUS`. | +| `max_distance` | `float` | Neighbours farther than this distance are discarded, whatever the query method. Acts as a hard cut-off. | +| `coincidence_tolerance` | `float` | Below this distance two points are considered coincident and the value is copied directly, without interpolation. | +| `kernel` | `INTERPOLATION_KERNEL` | Algorithm used to compute the value from the neighbourhood. See [Kernels](kernels.md). | +| `multithread` | `bool` | Split the interpolation loop into blocks processed in parallel threads (one per CPU). | +| `interpolated_load` | `INTERPOLATED_LOAD_TYPE` | Physical quantity being interpolated. Sets the number of components and the APDL export format. See [Load types](load-types.md). | +| `accept_no_neighbor` | `bool` | Only for target-to-source kernels. If `False` (default), raise an `InterpolationError` when a destination point has no neighbour within `max_distance`. If `True`, assign zero to it. | + +The configuration is validated on creation; a `ConfigurationError` is raised if: + +- `method` is `QUERY_TYPE.K` and `param` cannot be converted to an integer; +- the `kernel` is not compatible with the `interpolated_load` + (see [kernel–load compatibility](kernels.md#kernel-load-compatibility)). + +## `ColumnsConfig` + +Describes the **0-based** column indices of the source data files and of the +destination mesh file. The three coordinates (x, y, z) must be in consecutive columns, +starting at the given index. + +| Parameter | Type | File | Description | +|---|---|---|---| +| `source_xyz` | `int` | source | Index of the **x** column; y and z must follow. | +| `value` | `int` | source | Index of the (first) value column. Multi-component loads read the following columns too (3 for `EM_FORCE`, 2 for `HTC`). | +| `dest_xyz` | `int` | destination | Index of the **x** column; y and z must follow. | +| `id` | `int` | destination | Index of the node/element ID column. | +| `volume_area` | `int \| None` | both | Index of the volume or area column. Required for [scalar integrals](analysis.md#scalar-integrals). For `EM_FORCE`, if set, the source values are interpreted as force **densities** and multiplied by the volume read at this index in the source file. | + +### Example + +Given a source file and a destination mesh like + +=== "Source file" + + ```text + x, y, z, Fx, Fy, Fz + 0.10, 0.20, 0.30, 1.5, -0.2, 3.1 + ... + ``` + +=== "Destination mesh" + + ```text + Node_ID, X, Y, Z + 1, 0.11, 0.19, 0.31 + ... + ``` + +the matching configuration is: + +```python +columns = ColumnsConfig(source_xyz=0, value=3, dest_xyz=1, id=0) +``` diff --git a/docs/user-guide/file-formats.md b/docs/user-guide/file-formats.md new file mode 100644 index 0000000..954ee21 --- /dev/null +++ b/docs/user-guide/file-formats.md @@ -0,0 +1,79 @@ +# File formats + +## Input files + +The input format is flexible. The parser: + +- skips the header lines automatically (lines containing more letters than digits); +- detects the delimiter from the first line: `;`, `,`, `:` or, if none is found, + any whitespace (spaces or tabs). + +Only the column indices must be given, through the +[`ColumnsConfig`](configuration.md#columnsconfig). + +### Source files + +One file per load case, with at least the three coordinates and the value columns. +All source files of a folder must share the same coordinates, as the neighbour query +is computed only once. + +```text +X [m] Y [m] Z [m] Q [W/m3] +0.100 0.200 0.300 3.4e6 +... +``` + +### Destination mesh + +One file with the IDs, the coordinates and, optionally, the volume or area of the +destination points: + +- **nodes** for `EM_FORCE`; +- **element centroids** for `HEAT_FLUX`, `HEAT_GEN` and `HTC`. + +## APDL export scripts + +The [`apdl-scripts/`](https://github.com/Fusion4Energy/InterpCore/tree/main/apdl-scripts) +folder contains two macros to export the destination mesh from an ANSYS `.cdb` file. +Edit the variables in the `USER INPUTS` section, then run the macro in ANSYS +Mechanical APDL. + +=== "export_nodes.apdl" + + Exports the nodes of a component to a CSV file, for `EM_FORCE`. + + ```text + Node_ID,X,Y,Z + ``` + + ```python + columns = ColumnsConfig(id=0, dest_xyz=1, source_xyz=..., value=...) + ``` + +=== "export_centroids.apdl" + + Exports the element centroids of a component, with their area and volume, to a + CSV file, for `HEAT_FLUX`, `HEAT_GEN` and `HTC`. + + ```text + Element_ID,X,Y,Z,Area,Volume + ``` + + ```python + # volume_area=4 for areas (surface loads), 5 for volumes + columns = ColumnsConfig(id=0, dest_xyz=1, source_xyz=..., value=..., volume_area=5) + ``` + +Both macros use the same user inputs: + +```text +cdb_file = 'CDB_name' ! CDB file name without extension +named_selection = 'component_name' ! Name of the component/named selection +output_file = 'nodes_output' ! Output CSV file name without extension +``` + +## Output files + +`export_to_ansys(outdir)` writes one `interpolated_.txt` file per source file, +containing APDL commands that can be read with `/INPUT`. See +[Load types](load-types.md) for the commands written for each load. diff --git a/docs/user-guide/how-it-works.md b/docs/user-guide/how-it-works.md new file mode 100644 index 0000000..bcfdacc --- /dev/null +++ b/docs/user-guide/how-it-works.md @@ -0,0 +1,61 @@ +# How it works + +## Overview + +InterpCore maps values known on a **source** point cloud (e.g. the integration points +of an electromagnetic or CFD model) onto a **destination** point set (the nodes or the +element centroids of a mechanical FE model). +The neighbour query is computed only once, in the constructor, since all source files +are assumed to share the same coordinates. + +## Two mapping philosophies + +Kernels fall into two families, depending on which point set the KDTree is built on +and on which point set the loop runs. + +![Source-to-target vs target-to-source](../assets/img/philosophies.svg){ .diagram } + +Orange circles are source points, blue squares are destination points. + +| | Source-to-target | Target-to-source | +|---|---|---| +| KDTree built on | destination points | source points | +| Loop over | source points | destination points | +| Each iteration | **distributes** one source value to its destination neighbours | **assigns** one destination value from its source neighbours | +| Contributions | accumulate (`+=`) | assigned once (`=`) | +| Conserves | the total (sum) of the field | the local intensity of the field | +| Kernels | `DISTANCE_WEIGHTED`, `FEM` | `AVERAGE`, `AVERAGE_WEIGHTED`, `CLOSEST` | +| Loads | `EM_FORCE` | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | + +Forces are *extensive* quantities: the sum of the nodal forces must equal the total +force of the source, so each source force is split among the destination nodes. +Heat flux, heat generation and HTC are *intensive* quantities (densities): each +destination point must receive a representative local value, regardless of how many +source points are nearby. + +## Per-point decision flow + +For every point of the loop, the following logic is applied +(see [`interpolate_block`](../api/kernels.md)): + +```mermaid +flowchart TD + A[Point of the loop] --> B{Neighbours found
by the query?} + B -- no --> N + B -- yes --> C[Discard neighbours farther
than max_distance] + C --> D{Any neighbour left?} + D -- no --> N{Philosophy} + N -- source-to-target --> U[Add value to
unmapped total] + N -- target-to-source --> E{accept_no_neighbor?} + E -- yes --> Z[Assign 0] + E -- no --> R[[Raise InterpolationError]] + D -- yes --> F{"Closest neighbour within
coincidence_tolerance?"} + F -- yes --> G[Copy the value directly] + F -- no --> H[Apply the kernel] +``` + +!!! warning "Unmapped values" + With source-to-target kernels, a source point with no destination neighbour within + `max_distance` is not lost silently: its value is added to an `unmapped` total, + reported by [`compute_EM_resultants`](analysis.md#em-force-resultants) as + `Unmapped_EM_Force`. diff --git a/docs/user-guide/kernels.md b/docs/user-guide/kernels.md new file mode 100644 index 0000000..6b7090d --- /dev/null +++ b/docs/user-guide/kernels.md @@ -0,0 +1,105 @@ +# Interpolation kernels + +The kernel defines how the value of a point is computed from its neighbourhood, once +the neighbours have been found by the [query](query-methods.md) and filtered by +`max_distance`. Kernels belong to one of two +[families](how-it-works.md#two-mapping-philosophies). + +In the following, $d_i$ is the distance between the current point and its $i$-th +neighbour, and $N$ is the number of neighbours. + +## Inverse distance weights + +`DISTANCE_WEIGHTED` and `AVERAGE_WEIGHTED` use normalised inverse distance weights: + +$$ +w_i = \frac{1/d_i}{\sum_{j=1}^{N} 1/d_j}, \qquad \sum_{i=1}^{N} w_i = 1 +$$ + +The closer the neighbour, the larger its weight. + +## Source-to-target kernels + +Each source point distributes its value $\mathbf{F}$ to its destination neighbours. +The contributions coming from different source points are summed on each destination +node. + +### `DISTANCE_WEIGHTED` + +$$ +\mathbf{F}_i \mathrel{+}= w_i\,\mathbf{F} +$$ + +Since the weights sum to one, the total force is conserved exactly. Each component is +split independently, so the direction of every nodal contribution is the same as the +direction of $\mathbf{F}$. + +### `FEM` + +The source point is connected to each of its destination neighbours by an axial +spring with stiffness inversely proportional to its length, with the destination +nodes fixed. The force $\mathbf{F}$ is applied to the source point and the reaction +forces of the springs are the nodal forces. + +![FEM kernel spring analogy](../assets/img/fem_kernel.svg){ .diagram } + +With $\mathbf{e}_i$ the unit vector from the source point to neighbour $i$: + +$$ +\mathbf{K}_i = \frac{1}{d_i}\,\mathbf{e}_i\,\mathbf{e}_i^{\mathsf T}, +\qquad +\Big(\sum_{i=1}^{N}\mathbf{K}_i\Big)\,\mathbf{u} = \mathbf{F}, +\qquad +\mathbf{F}_i = \mathbf{K}_i\,\mathbf{u} +$$ + +The nodal forces sum to $\mathbf{F}$, so the total force is conserved. Unlike +`DISTANCE_WEIGHTED`, each nodal force is aligned with its spring, so the distribution +depends on the geometry of the neighbourhood. + +!!! warning + The $3\times3$ stiffness matrix is singular when all neighbours are aligned or + coplanar with the source point (e.g. very few neighbours or a 2D-like + neighbourhood). In that case no force is distributed for that source point. + Use enough neighbours (K ≥ 4 is a reasonable minimum) to avoid it, and check the + [resultants](analysis.md#em-force-resultants). + +## Target-to-source kernels + +Each destination point receives a value $q$ computed from the values $q_j$ of its +source neighbours. + +### `AVERAGE` + +$$ +q = \frac{1}{N}\sum_{j=1}^{N} q_j +$$ + +### `AVERAGE_WEIGHTED` + +$$ +q = \sum_{j=1}^{N} w_j\, q_j +$$ + +### `CLOSEST` + +$$ +q = q_{k}, \qquad k = \arg\min_j d_j +$$ + +The value of the closest source neighbour is copied. This is useful when source and +destination points are close to each other, or when sharp gradients should not be +smoothed. + +## Kernel–load compatibility { #kernel-load-compatibility } + +| Kernel | Family | Compatible loads | +|---|---|---| +| `DISTANCE_WEIGHTED` | Source-to-target | `EM_FORCE` | +| `FEM` | Source-to-target | `EM_FORCE` | +| `AVERAGE` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | +| `AVERAGE_WEIGHTED` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | +| `CLOSEST` | Target-to-source | `HEAT_FLUX`, `HEAT_GEN`, `HTC` | + +Using an incompatible combination raises a `ConfigurationError` when the +`InterpolationConfig` is created. diff --git a/docs/user-guide/load-types.md b/docs/user-guide/load-types.md new file mode 100644 index 0000000..9aeabd6 --- /dev/null +++ b/docs/user-guide/load-types.md @@ -0,0 +1,67 @@ +# Load types + +The `interpolated_load` parameter of the +[`InterpolationConfig`](configuration.md#interpolationconfig) sets the number of +value columns read from the source files and the APDL commands written by +[`export_to_ansys`](../api/interpolator.md). + +| Load type | Components | Destination points | Kernels | APDL command | +|---|---|---|---|---| +| `EM_FORCE` | 3 ($F_x, F_y, F_z$) | nodes | source-to-target | `F` | +| `HEAT_FLUX` | 1 | element centroids | target-to-source | `SFE,,HFLUX` | +| `HEAT_GEN` | 1 | element centroids | target-to-source | `BFE,,HGEN` | +| `HTC` | 2 (HTC, $T_{bulk}$) | element centroids | target-to-source | `SFE,,CONV` | + +The ID column of the destination mesh is written as the node/element number of the +APDL command, so it must contain node IDs for `EM_FORCE` and element IDs for the +other loads. The [APDL scripts](file-formats.md#apdl-export-scripts) export +destination meshes in the right format. + +## `EM_FORCE` + +Nodal force vectors. The value columns are $F_x, F_y, F_z$, starting at +`ColumnsConfig.value`. + +If `ColumnsConfig.volume_area` is set, the source values are interpreted as **force +densities** and multiplied by the volume column of the source file before +interpolation. + +```text +F, 1234, Fx, 1.523000e+01 +... +F, 1234, Fy, -2.100000e-01 +... +F, 1234, Fz, 3.100000e+00 +``` + +## `HEAT_FLUX` + +Surface heat flux applied to element faces. + +```text +SFE, 5678,, HFLUX,, 1.250000e+05 +``` + +## `HEAT_GEN` + +Volumetric heat generation applied to elements. + +```text +BFE, 5678, HGEN,, 3.400000e+06 +``` + +## `HTC` + +Convection boundary condition. The first value column is the Heat Transfer +Coefficient, the second one (next column) is the bulk fluid temperature. + +```text +SFE, 5678,, CONV, 1, 2.000000e+04 +... +SFE, 5678,, CONV, 2, 1.500000e+02 +``` + +!!! note + The output file contains first all the lines of the first component, then all + those of the second component, and so on. The number formatting shown above is + illustrative. diff --git a/docs/user-guide/query-methods.md b/docs/user-guide/query-methods.md new file mode 100644 index 0000000..1c2142f --- /dev/null +++ b/docs/user-guide/query-methods.md @@ -0,0 +1,35 @@ +# Query methods + +Before interpolating, the neighbours of each point are searched with a +[KDTree](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KDTree.html). +Which point set the tree is built on depends on the +[kernel family](how-it-works.md#two-mapping-philosophies). + +![Query methods](../assets/img/query_methods.svg){ .diagram } + +## `QUERY_TYPE.K` + +Returns the `param` nearest neighbours (`param` must be an integer). It always returns +exactly K points, even if some are very far: use `max_distance` to discard them. + +## `QUERY_TYPE.RADIUS` + +Returns all neighbours within a sphere of radius `param`. The number of neighbours +varies from point to point and can be zero. + +## Filters applied after the query + +`max_distance` +: Neighbours farther than `max_distance` are discarded. With `QUERY_TYPE.RADIUS` + it is effective only when smaller than the radius. + +`coincidence_tolerance` +: If the neighbour is closer than this tolerance, the two points are considered + coincident and the value is copied directly, skipping the kernel. + +!!! tip "Choosing the parameters" + - `max_distance` should be a few times the typical spacing of the coarser of the + two point sets: big enough to always find a neighbour, small enough not to pick + points from a different part of the geometry. + - `coincidence_tolerance` should be much smaller than the mesh size, e.g. a + fraction of the smallest element. diff --git a/pyproject.toml b/pyproject.toml index ee8715e..ea19a59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,11 +27,11 @@ dependencies = [ ] [project.optional-dependencies] dev = ["pytest", "pytest-cov", "ruff"] +docs = ["zensical", "mkdocstrings-python", "nbconvert"] [project.urls] -# Homepage = "https://github.com/Fusion4Energy" -# Repository = "https://github.com/Fusion4Energy/EM-interpolator" -# Documentation = "https://github.com/Fusion4Energy/EM-interpolator" +Repository = "https://github.com/Fusion4Energy/InterpCore" +Documentation = "https://fusion4energy.github.io/InterpCore/" [tool.setuptools_scm] write_to = "src/_version.py" diff --git a/src/interpcore/dest_tree.py b/src/interpcore/dest_tree.py index 99c3b93..b2300b0 100644 --- a/src/interpcore/dest_tree.py +++ b/src/interpcore/dest_tree.py @@ -92,7 +92,10 @@ def _run_query( logging.info("Extracting nodes from KDTree...") tic = time.perf_counter() if method == QUERY_TYPE.RADIUS: - idx = dest_tree.query_radius(src_coordinates, r=param) + # sorted so that the first neighbour is the closest (coincidence check) + idx = dest_tree.query_radius( + src_coordinates, r=param, return_distance=True, sort_results=True + )[0] elif method == QUERY_TYPE.K: # check for int already done in config idx = dest_tree.query(src_coordinates, k=int(param))[1] diff --git a/src/interpcore/interpolator.py b/src/interpcore/interpolator.py index 4eaede9..2b86822 100644 --- a/src/interpcore/interpolator.py +++ b/src/interpcore/interpolator.py @@ -234,7 +234,7 @@ def build_vtk_output(self, outdir: Path | None = None): Parameters ---------- - oiutdir : Path | None, optional + outdir : Path | None, optional output directory for the vtk files, if None, files are not saved, by default None """ diff --git a/tests/test_kernels.py b/tests/test_kernels.py index 77f9694..a2a09c3 100644 --- a/tests/test_kernels.py +++ b/tests/test_kernels.py @@ -326,6 +326,32 @@ def test_interpolate_block_coincident_node(self): # Nothing should be unmapped assert np.all(unmapped == 0) + def test_radius_query_coincident_node_not_first_in_tree(self): + """Radius query must detect a coincident node even if it is not the first found""" + src_coords = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]) + dest_coords = np.array([[1.0001, 0.0, 0.0]]) + src_values = np.array([[10.0], [20.0], [90.0]]) + + config = InterpolationConfig( + kernel=INTERPOLATION_KERNEL.AVERAGE, + max_distance=5.0, + coincidence_tolerance=0.01, + method=QUERY_TYPE.RADIUS, + param=1.5, + multithread=False, + interpolated_load=INTERPOLATED_LOAD_TYPE.HEAT_FLUX, + ) + tree = DestinationTree( + dest_coordinates=dest_coords, + src_coordinates=src_coords, + dest_ids=np.array([1]), + config=config, + ) + interpolated, _ = tree.interpolate(src_values) + + # Coincident with src[1]: value copied, not averaged with the other neighbours + np.testing.assert_almost_equal(interpolated[0, 0], 20.0) + def test_destination_to_source_multithread_handles_empty_blocks(self, monkeypatch): """Dest-to-source multithread should handle blocks larger than points without shape errors""" dest_coords = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..7e65813 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,148 @@ +[project] +site_url = "https://fusion4energy.github.io/InterpCore/" +site_name = "InterpCore" +site_description = "Interpolation of physical field data between meshes with export to ANSYS APDL" +site_author = "F4E mechanical team" +copyright = "Licensed under the EUPL 1.2 — F4E mechanical team" + +repo_url = "https://github.com/Fusion4Energy/InterpCore" +repo_name = "Fusion4Energy/InterpCore" +edit_uri = "edit/main/docs/" + +docs_dir = "docs" +site_dir = "site" + +nav = [ + { "Home" = "index.md" }, + { "Getting started" = [ + "installation.md", + "getting-started.md", + ] }, + { "User guide" = [ + "user-guide/how-it-works.md", + "user-guide/configuration.md", + "user-guide/query-methods.md", + "user-guide/kernels.md", + "user-guide/load-types.md", + "user-guide/analysis.md", + "user-guide/file-formats.md", + ] }, + { "Examples" = [ + "examples/index.md", + { "Heat flux" = "examples/heat_flux/heat_flux.md" }, + { "Heat generation" = "examples/heat_gen/heat_gen.md" }, + { "EM force" = "examples/em_force/em_force.md" }, + { "HTC" = "examples/htc/htc.md" }, + ] }, + { "API reference" = [ + "api/interpolator.md", + "api/config.md", + "api/dest_tree.md", + "api/kernels.md", + "api/parsers.md", + "api/errors.md", + ] }, +] + +extra_css = [ + "stylesheets/extra.css", + "https://unpkg.com/katex@0/dist/katex.min.css", +] +extra_javascript = [ + "javascripts/katex.js", + "https://unpkg.com/katex@0/dist/katex.min.js", + "https://unpkg.com/katex@0/dist/contrib/auto-render.min.js", +] + +[project.theme] +language = "en" +features = [ + "content.action.edit", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.path", + "navigation.sections", + "navigation.tabs", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "indigo" +accent = "deep orange" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "indigo" +accent = "deep orange" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/Fusion4Energy/InterpCore" + +[project.markdown_extensions] +abbr = {} +admonition = {} +attr_list = {} +def_list = {} +footnotes = {} +md_in_html = {} +tables = {} +toc.permalink = true +pymdownx.arithmatex.generic = true +pymdownx.betterem = {} +pymdownx.caret = {} +pymdownx.details = {} +pymdownx.emoji.emoji_generator = "zensical.extensions.emoji.to_svg" +pymdownx.emoji.emoji_index = "zensical.extensions.emoji.twemoji" +pymdownx.highlight.anchor_linenums = true +pymdownx.highlight.line_spans = "__span" +pymdownx.highlight.pygments_lang_class = true +pymdownx.inlinehilite = {} +pymdownx.keys = {} +pymdownx.magiclink = {} +pymdownx.mark = {} +pymdownx.smartsymbols = {} +pymdownx.superfences.custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] +pymdownx.tabbed.alternate_style = true +pymdownx.tabbed.combine_header_slug = true +pymdownx.tasklist.custom_checkbox = true +pymdownx.tilde = {} + +[project.plugins.mkdocstrings.handlers.python] +paths = ["src"] + +[project.plugins.mkdocstrings.handlers.python.options] +docstring_style = "numpy" +show_source = true +show_root_heading = true +show_root_full_path = false +show_symbol_type_heading = true +show_symbol_type_toc = true +members_order = "source" +merge_init_into_class = true +separate_signature = true +show_signature_annotations = true +signature_crossrefs = true +filters = ["!^_"]