pyfmaps 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. pyfmaps-1.3.0/.github/workflows/documentation.yml +63 -0
  2. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/.github/workflows/test_and_deploy.yml +1 -1
  3. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/.gitignore +9 -0
  4. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/PKG-INFO +49 -27
  5. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/README.md +43 -8
  6. pyfmaps-1.3.0/docs/source/_static/pyfm.css +24 -0
  7. pyfmaps-1.3.0/docs/source/_templates/autosummary/base.rst +8 -0
  8. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/_templates/autosummary/class.rst +3 -0
  9. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/api.rst +12 -0
  10. pyfmaps-1.3.0/docs/source/conf.py +137 -0
  11. pyfmaps-1.3.0/docs/source/getting_started.md +105 -0
  12. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/index.rst +3 -2
  13. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/install.md +11 -1
  14. pyfmaps-1.3.0/examples/data/cat.obj +21618 -0
  15. pyfmaps-1.3.0/examples/data/tr_reg_000.off +20668 -0
  16. pyfmaps-1.3.0/examples/data/tr_reg_040.off +20668 -0
  17. pyfmaps-1.3.0/examples/data/tr_reg_047.off +20668 -0
  18. pyfmaps-1.3.0/examples/gallery/GALLERY_HEADER.rst +11 -0
  19. pyfmaps-1.3.0/examples/gallery/plot_01_load_mesh.py +103 -0
  20. pyfmaps-1.3.0/examples/gallery/plot_02_mesh_geometry.py +269 -0
  21. pyfmaps-1.3.0/examples/gallery/plot_03_spectrum.py +351 -0
  22. pyfmaps-1.3.0/examples/gallery/plot_04_maps_from_correspondence.py +358 -0
  23. pyfmaps-1.3.0/examples/gallery/plot_05_maps_without_correspondence.py +290 -0
  24. pyfmaps-1.3.0/examples/gallery/plot_06_functional_map_network.py +340 -0
  25. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/FMN/FMN.py +96 -49
  26. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/_version.py +3 -3
  27. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/functional.py +299 -194
  28. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/file_utils.py +8 -5
  29. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/geometry.py +7 -16
  30. pyfmaps-1.3.0/pyFM/mesh/trimesh.py +1741 -0
  31. pyfmaps-1.3.0/pyFM/optimize/__init__.py +2 -0
  32. pyfmaps-1.3.0/pyFM/optimize/weights.py +203 -0
  33. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/refine/icp.py +19 -31
  34. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/refine/zoomout.py +22 -16
  35. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/signatures/HKS_functions.py +6 -4
  36. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/signatures/WKS_functions.py +6 -4
  37. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/spectral/__init__.py +1 -1
  38. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/spectral/convert.py +18 -14
  39. pyfmaps-1.3.0/pyFM/spectral/nearest_neighbor/__init__.py +26 -0
  40. pyfmaps-1.3.0/pyFM/spectral/nearest_neighbor/backends.py +152 -0
  41. pyfmaps-1.3.0/pyFM/spectral/nearest_neighbor/config.py +69 -0
  42. pyfmaps-1.3.0/pyFM/spectral/nearest_neighbor/dispatch.py +99 -0
  43. pyfmaps-1.3.0/pyFM/spectral/nn_utils.py +13 -0
  44. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/spectral/projection_utils.py +9 -7
  45. pyfmaps-1.3.0/pyFM/viz/__init__.py +20 -0
  46. pyfmaps-1.3.0/pyFM/viz/_convert.py +73 -0
  47. pyfmaps-1.3.0/pyFM/viz/plot.py +351 -0
  48. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyfmaps.egg-info/PKG-INFO +49 -27
  49. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyfmaps.egg-info/SOURCES.txt +26 -5
  50. pyfmaps-1.3.0/pyfmaps.egg-info/requires.txt +10 -0
  51. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyfmaps.egg-info/scm_file_list.json +95 -74
  52. pyfmaps-1.3.0/pyfmaps.egg-info/scm_version.json +8 -0
  53. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyproject.toml +34 -10
  54. pyfmaps-1.3.0/requirements_docs.txt +12 -0
  55. pyfmaps-1.3.0/tests/conftest.py +40 -0
  56. pyfmaps-1.3.0/tests/test_data.py +8 -0
  57. pyfmaps-1.3.0/tests/test_functional.py +100 -0
  58. pyfmaps-1.3.0/tests/test_trimesh.py +817 -0
  59. pyfmaps-1.3.0/uv.lock +5104 -0
  60. pyfmaps-1.2.0/.github/workflows/documentation.yml +0 -51
  61. pyfmaps-1.2.0/docs/source/conf.py +0 -76
  62. pyfmaps-1.2.0/docs/source/examples.md +0 -15
  63. pyfmaps-1.2.0/docs/source/getting_started.md +0 -89
  64. pyfmaps-1.2.0/examples/functional_map_network/FMN_base.ipynb +0 -332
  65. pyfmaps-1.2.0/examples/mesh_and_matching/basic_functions.ipynb +0 -447
  66. pyfmaps-1.2.0/examples/mesh_and_matching/matching_with_zoomout.ipynb +0 -187
  67. pyfmaps-1.2.0/pyFM/mesh/trimesh.py +0 -1420
  68. pyfmaps-1.2.0/pyFM/optimize/__init__.py +0 -1
  69. pyfmaps-1.2.0/pyFM/spectral/nn_utils.py +0 -40
  70. pyfmaps-1.2.0/pyfmaps.egg-info/requires.txt +0 -23
  71. pyfmaps-1.2.0/pyfmaps.egg-info/scm_version.json +0 -8
  72. pyfmaps-1.2.0/requirements_docs.txt +0 -13
  73. pyfmaps-1.2.0/tests/test_data.py +0 -8
  74. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/.github/workflows/lint.yml +0 -0
  75. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/.pre-commit-config.yaml +0 -0
  76. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/LICENSE +0 -0
  77. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/MANIFEST.in +0 -0
  78. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/doc/zoomout.gif +0 -0
  79. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/_static/.gitkeep +0 -0
  80. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/docs/source/_templates/autosummary/module.rst +0 -0
  81. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-01.off +0 -0
  82. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-02.off +0 -0
  83. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-03.off +0 -0
  84. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-04.off +0 -0
  85. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-05.off +0 -0
  86. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-06.off +0 -0
  87. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-07.off +0 -0
  88. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-08.off +0 -0
  89. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-09.off +0 -0
  90. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/camel-gallop-10.off +0 -0
  91. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/10_to_2 +0 -0
  92. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/10_to_8 +0 -0
  93. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/1_to_3 +0 -0
  94. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/1_to_5 +0 -0
  95. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/1_to_7 +0 -0
  96. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/2_to_10 +0 -0
  97. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/2_to_3 +0 -0
  98. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/2_to_4 +0 -0
  99. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/2_to_5 +0 -0
  100. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/3_to_1 +0 -0
  101. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/3_to_2 +0 -0
  102. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/3_to_5 +0 -0
  103. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/3_to_6 +0 -0
  104. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/3_to_9 +0 -0
  105. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/4_to_2 +0 -0
  106. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/4_to_8 +0 -0
  107. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/5_to_1 +0 -0
  108. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/5_to_2 +0 -0
  109. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/5_to_3 +0 -0
  110. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/5_to_7 +0 -0
  111. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/6_to_3 +0 -0
  112. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/6_to_7 +0 -0
  113. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/7_to_1 +0 -0
  114. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/7_to_5 +0 -0
  115. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/7_to_6 +0 -0
  116. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/8_to_10 +0 -0
  117. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/8_to_4 +0 -0
  118. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/8_to_9 +0 -0
  119. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/9_to_3 +0 -0
  120. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/camel_gallop/maps/9_to_8 +0 -0
  121. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/cat-00.off +0 -0
  122. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/landmarks.txt +0 -0
  123. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/lion-00.off +0 -0
  124. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/examples/data/lion2cat +0 -0
  125. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/FMN/__init__.py +0 -0
  126. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/__init__.py +0 -0
  127. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/eval/__init__.py +0 -0
  128. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/eval/evaluate.py +0 -0
  129. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/__init__.py +0 -0
  130. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/data/texture_1.jpg +0 -0
  131. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/data/texture_2.jpg +0 -0
  132. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/mesh/laplacian.py +0 -0
  133. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/optimize/base_functions.py +0 -0
  134. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/refine/__init__.py +0 -0
  135. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/signatures/__init__.py +0 -0
  136. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyFM/spectral/shape_difference.py +0 -0
  137. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyfmaps.egg-info/dependency_links.txt +0 -0
  138. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/pyfmaps.egg-info/top_level.txt +0 -0
  139. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/requirements.txt +0 -0
  140. {pyfmaps-1.2.0 → pyfmaps-1.3.0}/setup.cfg +0 -0
@@ -0,0 +1,63 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ workflow_dispatch:
9
+ release:
10
+
11
+ jobs:
12
+ build_documentation:
13
+ name: Build Documentation
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 30
16
+
17
+ permissions:
18
+ contents: write
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0 # setuptools_scm needs tags to compute the version
24
+
25
+ # VTK's rendering libraries link against libGL at import time, and the
26
+ # gallery renders PyVista scenes off-screen. Since VTK 9.5 the stock wheel
27
+ # does headless rendering through EGL, so libegl1 is enough and no Xvfb is
28
+ # needed. NB: libgl1-mesa-glx does not exist on ubuntu-24.04 (what
29
+ # ubuntu-latest now resolves to) -- it is libgl1 + libglx-mesa0.
30
+ - name: Install VTK runtime libraries
31
+ run: |
32
+ sudo apt-get update
33
+ sudo apt-get install -y --no-install-recommends \
34
+ libgl1 libegl1 libglx-mesa0 libxrender1
35
+
36
+ - uses: astral-sh/setup-uv@v5
37
+ with:
38
+ enable-cache: true
39
+
40
+ # dev + docs come from [tool.uv] default-groups; viz via pyfmaps[viz].
41
+ - name: Install dependencies from the lockfile
42
+ run: uv sync --locked
43
+
44
+ # -d keeps Sphinx's doctree cache out of the published directory. It defaults
45
+ # to <outdir>/.doctrees, which means it gets deployed to the website: 12 MB of
46
+ # build artifacts (61% of the push) plus an environment.pickle containing
47
+ # absolute build paths.
48
+ - name: Build HTML
49
+ run: uv run sphinx-build -d docs/_build/doctrees -b html docs/source/ docs/_build/html
50
+
51
+ - name: upload artifact
52
+ uses: actions/upload-artifact@v4
53
+ with:
54
+ path:
55
+ docs/_build/html/
56
+ - name: Deploy to GitHub Pages
57
+ uses: peaceiris/actions-gh-pages@v4
58
+ if: github.ref == 'refs/heads/master'
59
+ with:
60
+ publish_branch: gh-pages
61
+ github_token: ${{ secrets.GITHUB_TOKEN }}
62
+ publish_dir: docs/_build/html
63
+ force_orphan: true
@@ -20,7 +20,7 @@ jobs:
20
20
  strategy:
21
21
  matrix:
22
22
  platform: [windows-latest, ubuntu-latest, macos-latest]
23
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
23
+ python-version: ['3.10', '3.11', '3.12', '3.13']
24
24
 
25
25
  steps:
26
26
  - uses: actions/checkout@v4
@@ -3,6 +3,11 @@ docs/source/generated/*
3
3
  docs/make.bat
4
4
  docs/Makefile
5
5
 
6
+ # sphinx-gallery output (regenerated on every build)
7
+ docs/source/auto_examples/
8
+ docs/source/gen_modules/
9
+ docs/source/sg_execution_times.rst
10
+
6
11
  # Byte-compiled / optimized / DLL files
7
12
  __pycache__/
8
13
  *.py[cod]
@@ -82,6 +87,7 @@ target/
82
87
  # Pycharm and VSCode
83
88
  .idea/
84
89
  venv/
90
+ .venv/
85
91
  .vscode/
86
92
 
87
93
  # IPython Notebook
@@ -102,3 +108,6 @@ small_test.ipynb
102
108
  # Dask
103
109
  dask-worker-space/
104
110
 
111
+
112
+ # research scratch, not part of the package
113
+ studies/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfmaps
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Python bindings for functional maps
5
5
  Author: Robin Magnet
6
6
  License: MIT License
@@ -35,13 +35,12 @@ Classifier: Operating System :: OS Independent
35
35
  Classifier: Programming Language :: Python
36
36
  Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Programming Language :: Python :: 3 :: Only
38
- Classifier: Programming Language :: Python :: 3.8
39
- Classifier: Programming Language :: Python :: 3.9
40
38
  Classifier: Programming Language :: Python :: 3.10
41
39
  Classifier: Programming Language :: Python :: 3.11
42
40
  Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
43
42
  Classifier: Topic :: Software Development :: Testing
44
- Requires-Python: >=3.8
43
+ Requires-Python: >=3.10
45
44
  Description-Content-Type: text/markdown
46
45
  License-File: LICENSE
47
46
  Requires-Dist: numpy
@@ -50,21 +49,9 @@ Requires-Dist: potpourri3d>=1.3
50
49
  Requires-Dist: robust_laplacian
51
50
  Requires-Dist: tqdm
52
51
  Requires-Dist: scikit-learn
53
- Provides-Extra: dev
54
- Requires-Dist: ruff; extra == "dev"
55
- Requires-Dist: pre-commit; extra == "dev"
56
- Requires-Dist: pytest; extra == "dev"
57
- Requires-Dist: pytest-cov; extra == "dev"
58
- Requires-Dist: build; extra == "dev"
59
- Provides-Extra: docs
60
- Requires-Dist: furo; extra == "docs"
61
- Requires-Dist: myst_parser>=0.13; extra == "docs"
62
- Requires-Dist: sphinx>=4.0; extra == "docs"
63
- Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
64
- Requires-Dist: sphinx-copybutton; extra == "docs"
65
- Requires-Dist: sphinx-design; extra == "docs"
66
- Requires-Dist: sphinx-gallery; extra == "docs"
67
- Requires-Dist: sphinx-math-dollar; extra == "docs"
52
+ Provides-Extra: viz
53
+ Requires-Dist: pyvista[jupyter]>=0.44; extra == "viz"
54
+ Requires-Dist: matplotlib; extra == "viz"
68
55
  Dynamic: license-file
69
56
 
70
57
  # pyFM - Python Functional Maps Library
@@ -73,11 +60,15 @@ Dynamic: license-file
73
60
  <img src="doc/zoomout.gif" alt="animated" />
74
61
  </p>
75
62
 
76
- [![](https://github.com/RobinMagnet/pyFM/actions/workflows/documentation.yml/badge.svg)](https://robinmagnet.github.io/pyFM/)
63
+ [![PyPI version](https://img.shields.io/pypi/v/pyfmaps.svg)](https://pypi.org/project/pyfmaps/)
64
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyfmaps.svg)](https://pypi.org/project/pyfmaps/)
65
+ [![License: MIT](https://img.shields.io/pypi/l/pyfmaps.svg)](https://github.com/RobinMagnet/pyFM/blob/master/LICENSE)
66
+ [![Tests](https://github.com/RobinMagnet/pyFM/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/RobinMagnet/pyFM/actions/workflows/test_and_deploy.yml)
67
+ [![Documentation](https://github.com/RobinMagnet/pyFM/actions/workflows/documentation.yml/badge.svg)](https://robinmagnet.github.io/pyFM/)
77
68
 
78
- **NEW** API Documentation is available [here](https://robinmagnet.github.io/pyFM/)
69
+ This package contains a comprehensive Python implementation for shape correspondence using functional maps, featuring code from [multiple papers](#implemented-papers).
79
70
 
80
- This package contains a comprehensive Python implementation for shape correspondence using functional maps, featuring code from [multiple papers](#implemented-papers)
71
+ Full [API documentation](https://robinmagnet.github.io/pyFM/) is available online.
81
72
 
82
73
  ## Core Features
83
74
  - Complete TriMesh class with geometric measures (geodesics, normals, LBO projections, differential operators)
@@ -90,13 +81,44 @@ This package contains a comprehensive Python implementation for shape correspond
90
81
  ## Installation
91
82
  ```bash
92
83
  pip install pyfmaps
93
- ```
94
84
 
95
- Please check the PyPi page to ensure the latest version is uploaded.
85
+ # with the optional PyVista-based plotting helpers used in the gallery
86
+ pip install 'pyfmaps[viz]'
87
+ ```
96
88
 
97
89
  ### Key Dependencies
98
- - Required: numpy, scipy, tqdm, scikit-learn
99
- - Optional: [`potpourri3d`](https://github.com/nmwsharp/potpourri3d) (geodesics), [`robust_laplacian`](https://github.com/nmwsharp/robust-laplacians-py) (Delaunay/tufted Laplacian)
90
+ Requires Python 3.10+ and: numpy, scipy, tqdm, scikit-learn,
91
+ [`potpourri3d`](https://github.com/nmwsharp/potpourri3d) (geodesic distances via the heat
92
+ and fast-marching methods), and
93
+ [`robust_laplacian`](https://github.com/nmwsharp/robust-laplacians-py) (Delaunay/tufted
94
+ Laplacian). All are installed automatically.
95
+
96
+ ## Quickstart
97
+ ```python
98
+ from pyFM.mesh import TriMesh
99
+ from pyFM.functional import FunctionalMapping
100
+
101
+ # Load a source and a target mesh
102
+ mesh1 = TriMesh.load("cat.off", area_normalize=True, center=True)
103
+ mesh2 = TriMesh.load("lion.off", area_normalize=True, center=True)
104
+
105
+ # Compute the LBO spectrum + descriptors, then fit a functional map.
106
+ # Keep w_orient above 0: descriptors alone cannot tell left from right,
107
+ # and w_orient=0 (the default) usually returns a symmetry-flipped map.
108
+ model = FunctionalMapping(mesh1, mesh2)
109
+ model.preprocess(descr_type="WKS", subsample_step=2, verbose=True)
110
+ model.fit(K=30, w_descr=1e0, w_lap=1e-1, w_dcomm=1e0, w_orient=1e0, verbose=True)
111
+
112
+ # Convert the functional map to a point-to-point map (mesh2 -> mesh1)
113
+ p2p_21 = model.get_p2p()
114
+
115
+ # Refine the map -- this is where most of the accuracy comes from
116
+ FM_zo = model.zoomout_refine(nit=16, step=5)
117
+ p2p_21_zo = model.get_p2p(FM_zo)
118
+ ```
119
+
120
+ See the [example gallery](https://robinmagnet.github.io/pyFM/auto_examples/index.html) for
121
+ complete, runnable workflows with interactive 3D figures.
100
122
 
101
123
  ## Design Philosophy
102
124
  This codebase prioritizes readability and adaptability over rigid modularity.
@@ -125,7 +147,7 @@ I selected this approach from my personal experience with research codebases and
125
147
 
126
148
  ## Documentation & Examples
127
149
  - [API Documentation](https://robinmagnet.github.io/pyFM/)
128
- - [Example Notebooks](https://github.com/RobinMagnet/pyFM/tree/master/examples)
150
+ - [Example Gallery](https://robinmagnet.github.io/pyFM/auto_examples/index.html)
129
151
 
130
152
  ## Implemented Papers
131
153
  This library implements methods from several key papers in shape correspondence, including:
@@ -4,11 +4,15 @@
4
4
  <img src="doc/zoomout.gif" alt="animated" />
5
5
  </p>
6
6
 
7
- [![](https://github.com/RobinMagnet/pyFM/actions/workflows/documentation.yml/badge.svg)](https://robinmagnet.github.io/pyFM/)
7
+ [![PyPI version](https://img.shields.io/pypi/v/pyfmaps.svg)](https://pypi.org/project/pyfmaps/)
8
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyfmaps.svg)](https://pypi.org/project/pyfmaps/)
9
+ [![License: MIT](https://img.shields.io/pypi/l/pyfmaps.svg)](https://github.com/RobinMagnet/pyFM/blob/master/LICENSE)
10
+ [![Tests](https://github.com/RobinMagnet/pyFM/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/RobinMagnet/pyFM/actions/workflows/test_and_deploy.yml)
11
+ [![Documentation](https://github.com/RobinMagnet/pyFM/actions/workflows/documentation.yml/badge.svg)](https://robinmagnet.github.io/pyFM/)
8
12
 
9
- **NEW** API Documentation is available [here](https://robinmagnet.github.io/pyFM/)
13
+ This package contains a comprehensive Python implementation for shape correspondence using functional maps, featuring code from [multiple papers](#implemented-papers).
10
14
 
11
- This package contains a comprehensive Python implementation for shape correspondence using functional maps, featuring code from [multiple papers](#implemented-papers)
15
+ Full [API documentation](https://robinmagnet.github.io/pyFM/) is available online.
12
16
 
13
17
  ## Core Features
14
18
  - Complete TriMesh class with geometric measures (geodesics, normals, LBO projections, differential operators)
@@ -21,13 +25,44 @@ This package contains a comprehensive Python implementation for shape correspond
21
25
  ## Installation
22
26
  ```bash
23
27
  pip install pyfmaps
24
- ```
25
28
 
26
- Please check the PyPi page to ensure the latest version is uploaded.
29
+ # with the optional PyVista-based plotting helpers used in the gallery
30
+ pip install 'pyfmaps[viz]'
31
+ ```
27
32
 
28
33
  ### Key Dependencies
29
- - Required: numpy, scipy, tqdm, scikit-learn
30
- - Optional: [`potpourri3d`](https://github.com/nmwsharp/potpourri3d) (geodesics), [`robust_laplacian`](https://github.com/nmwsharp/robust-laplacians-py) (Delaunay/tufted Laplacian)
34
+ Requires Python 3.10+ and: numpy, scipy, tqdm, scikit-learn,
35
+ [`potpourri3d`](https://github.com/nmwsharp/potpourri3d) (geodesic distances via the heat
36
+ and fast-marching methods), and
37
+ [`robust_laplacian`](https://github.com/nmwsharp/robust-laplacians-py) (Delaunay/tufted
38
+ Laplacian). All are installed automatically.
39
+
40
+ ## Quickstart
41
+ ```python
42
+ from pyFM.mesh import TriMesh
43
+ from pyFM.functional import FunctionalMapping
44
+
45
+ # Load a source and a target mesh
46
+ mesh1 = TriMesh.load("cat.off", area_normalize=True, center=True)
47
+ mesh2 = TriMesh.load("lion.off", area_normalize=True, center=True)
48
+
49
+ # Compute the LBO spectrum + descriptors, then fit a functional map.
50
+ # Keep w_orient above 0: descriptors alone cannot tell left from right,
51
+ # and w_orient=0 (the default) usually returns a symmetry-flipped map.
52
+ model = FunctionalMapping(mesh1, mesh2)
53
+ model.preprocess(descr_type="WKS", subsample_step=2, verbose=True)
54
+ model.fit(K=30, w_descr=1e0, w_lap=1e-1, w_dcomm=1e0, w_orient=1e0, verbose=True)
55
+
56
+ # Convert the functional map to a point-to-point map (mesh2 -> mesh1)
57
+ p2p_21 = model.get_p2p()
58
+
59
+ # Refine the map -- this is where most of the accuracy comes from
60
+ FM_zo = model.zoomout_refine(nit=16, step=5)
61
+ p2p_21_zo = model.get_p2p(FM_zo)
62
+ ```
63
+
64
+ See the [example gallery](https://robinmagnet.github.io/pyFM/auto_examples/index.html) for
65
+ complete, runnable workflows with interactive 3D figures.
31
66
 
32
67
  ## Design Philosophy
33
68
  This codebase prioritizes readability and adaptability over rigid modularity.
@@ -56,7 +91,7 @@ I selected this approach from my personal experience with research codebases and
56
91
 
57
92
  ## Documentation & Examples
58
93
  - [API Documentation](https://robinmagnet.github.io/pyFM/)
59
- - [Example Notebooks](https://github.com/RobinMagnet/pyFM/tree/master/examples)
94
+ - [Example Gallery](https://robinmagnet.github.io/pyFM/auto_examples/index.html)
60
95
 
61
96
  ## Implemented Papers
62
97
  This library implements methods from several key papers in shape correspondence, including:
@@ -0,0 +1,24 @@
1
+ /* PyVista interactive scenes (sphinx-gallery + pyvista.ext.viewer_directive).
2
+ *
3
+ * The viewer directive hardcodes width='100%%' (a literal double percent, see
4
+ * pyvista/ext/viewer_directive.py). Browsers reject that as an invalid length
5
+ * and fall back to the default iframe width, so the scene renders in a narrow
6
+ * box. Force a sane size here rather than patching PyVista.
7
+ */
8
+ iframe[src*="static_viewer.html"] {
9
+ width: 100%;
10
+ min-height: 400px;
11
+ border: 1px solid var(--color-background-border);
12
+ border-radius: 4px;
13
+ background: #ffffff;
14
+ }
15
+
16
+ /* Keep the Static/Interactive tab panes from collapsing around the iframe. */
17
+ .sd-tab-content:has(> iframe[src*="static_viewer.html"]) {
18
+ padding: 0;
19
+ }
20
+
21
+ /* Wide outputs (arrays, timing tables) scroll instead of pushing the page. */
22
+ .sphx-glr-script-out pre {
23
+ overflow-x: auto;
24
+ }
@@ -0,0 +1,8 @@
1
+ {{ fullname | escape | underline }}
2
+
3
+ .. currentmodule:: {{ module }}
4
+
5
+ .. auto{{ objtype }}:: {{ objname }}
6
+
7
+ .. minigallery:: {{ module }}.{{ objname }}
8
+ :add-heading: Examples using ``{{ objname }}``
@@ -6,3 +6,6 @@
6
6
  :members:
7
7
  :show-inheritance:
8
8
  :member-order: bysource
9
+
10
+ .. minigallery:: {{ module }}.{{ objname }}
11
+ :add-heading: Examples using ``{{ objname }}``
@@ -91,6 +91,18 @@ Laplacians, and mesh file readers/writers.
91
91
  pyFM.mesh.laplacian
92
92
  pyFM.mesh.file_utils
93
93
 
94
+ Visualization
95
+ -------------
96
+
97
+ PyVista-based plotting helpers for meshes, point clouds and vector fields.
98
+ Requires the ``viz`` extra (``pip install 'pyfmaps[viz]'``).
99
+
100
+ .. autosummary::
101
+ :toctree: generated
102
+ :recursive:
103
+
104
+ pyFM.viz
105
+
94
106
  Optimization internals
95
107
  ----------------------
96
108
 
@@ -0,0 +1,137 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # For the full list of built-in configuration values, see the documentation:
4
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
+
6
+
7
+ import importlib.metadata
8
+ import os
9
+ import pathlib
10
+ import re
11
+ import sys
12
+
13
+ sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix())
14
+
15
+ # -- Project information -----------------------------------------------------
16
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
17
+
18
+ project = "pyfmaps"
19
+ copyright = "2024, Robin Magnet"
20
+ author = "Robin Magnet"
21
+ try:
22
+ release = importlib.metadata.version("pyfmaps")
23
+ except importlib.metadata.PackageNotFoundError:
24
+ release = "0.0.0"
25
+
26
+
27
+ # -- General configuration ---------------------------------------------------
28
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
29
+
30
+ extensions = [
31
+ "sphinx.ext.duration",
32
+ "sphinx.ext.doctest",
33
+ "sphinx.ext.autodoc",
34
+ "sphinx.ext.autosummary",
35
+ "sphinx.ext.napoleon",
36
+ "sphinx.ext.intersphinx",
37
+ "sphinx_autodoc_typehints", # merge type hints into descriptions (after napoleon)
38
+ "sphinx_math_dollar",
39
+ "sphinx.ext.mathjax",
40
+ "sphinx_copybutton", # copy button on code blocks
41
+ "myst_parser",
42
+ "sphinx_design",
43
+ "sphinx_gallery.gen_gallery",
44
+ "pyvista.ext.viewer_directive", # renders the .vtksz scenes the scraper emits
45
+ ]
46
+
47
+
48
+ # -- Example gallery (sphinx-gallery + PyVista) ------------------------------
49
+ # The gallery executes examples/gallery/plot_*.py at build time and scrapes the
50
+ # scenes their pl.show() calls produce.
51
+
52
+ import pyvista as pv # noqa: E402
53
+ from pyvista.plotting.utilities.sphinx_gallery import DynamicScraper # noqa: E402
54
+
55
+ pv.BUILDING_GALLERY = True
56
+ pv.OFF_SCREEN = True
57
+ pv.set_plot_theme("document")
58
+ pv.global_theme.window_size = [1024, 768]
59
+
60
+
61
+ def reset_pyvista(gallery_conf, fname):
62
+ """Keep one example's theme/plotter state from leaking into the next."""
63
+ pv.close_all()
64
+ pv.set_plot_theme("document")
65
+ pv.global_theme.window_size = [1024, 768]
66
+
67
+
68
+ sphinx_gallery_conf = {
69
+ "examples_dirs": "../../examples/gallery",
70
+ "gallery_dirs": "auto_examples",
71
+ # Only plot_*.py are executed. os.sep keeps this working on Windows.
72
+ "filename_pattern": re.escape(os.sep) + r"plot_",
73
+ # The default ("NumberOfCodeLinesSortKey") orders the gallery by script length.
74
+ # Sorting by file name lets the file names encode the reading order instead.
75
+ "within_subsection_order": "FileNameSortKey",
76
+ "image_scrapers": (DynamicScraper(), "matplotlib"),
77
+ "doc_module": ("pyFM",),
78
+ # Rendered by the ``minigallery`` directive in _templates/autosummary/.
79
+ "backreferences_dir": "gen_modules/backreferences",
80
+ # The default is ("matplotlib", "seaborn") -- keep matplotlib's reset, since
81
+ # setting this key replaces the list rather than extending it.
82
+ "reset_modules": ("matplotlib", reset_pyvista),
83
+ "reset_modules_order": "both",
84
+ "remove_config_comments": True,
85
+ }
86
+
87
+ # sphinx_gallery_conf holds a scraper instance and a reset callback, so Sphinx
88
+ # cannot pickle it into the config cache. Harmless, but noisy on every build.
89
+ suppress_warnings = ["config.cache"]
90
+
91
+ # Resolve :class:/:meth: roles pointing at our dependencies. A short timeout keeps
92
+ # an unreachable inventory to a warning rather than a stalled build.
93
+ intersphinx_mapping = {
94
+ "python": ("https://docs.python.org/3", None),
95
+ "numpy": ("https://numpy.org/doc/stable", None),
96
+ "scipy": ("https://docs.scipy.org/doc/scipy", None),
97
+ "matplotlib": ("https://matplotlib.org/stable", None),
98
+ "pyvista": ("https://docs.pyvista.org", None),
99
+ }
100
+ intersphinx_timeout = 10
101
+
102
+ # Napoleon (numpydoc parsing)
103
+ napoleon_google_docstring = False
104
+ napoleon_numpy_docstring = True
105
+ napoleon_use_param = True
106
+ napoleon_use_rtype = True
107
+
108
+ # Members are documented on their own per-object pages (numpy-style), so module
109
+ # and class pages only render the module/class docstring + summary tables.
110
+ autodoc_default_options = {"member-order": "bysource"}
111
+ autosummary_generate = True
112
+ # Honour each module's __all__ so only intended public names are documented
113
+ # (default True *ignores* __all__, which would leak imported numpy/tqdm/sklearn names).
114
+ autosummary_ignore_module_all = False
115
+
116
+ templates_path = ["_templates"]
117
+ exclude_patterns = []
118
+
119
+ source_suffix = [".rst", ".md"]
120
+
121
+
122
+ # -- Options for HTML output -------------------------------------------------
123
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
124
+
125
+ html_theme = "furo"
126
+ html_static_path = ["_static"]
127
+ html_css_files = ["pyfm.css"]
128
+
129
+
130
+ from sphinx.ext.autodoc import between
131
+
132
+
133
+ def setup(app):
134
+ # Register a sphinx.ext.autodoc.between listener to ignore everything
135
+ # between lines that contain the word IGNORE
136
+ app.connect("autodoc-process-docstring", between("^.*IGNORE.*$", exclude=True))
137
+ return {"parallel_read_safe": True}
@@ -0,0 +1,105 @@
1
+ Getting Started
2
+ ===============
3
+
4
+ This short tutorial computes a functional map between two meshes and converts it
5
+ to a point-to-point correspondence. It is the condensed version of
6
+ [Computing a map without a correspondence](auto_examples/plot_05_maps_without_correspondence),
7
+ which explains every step and shows what each one buys.
8
+
9
+ Loading meshes
10
+ --------------
11
+
12
+ A `TriMesh` is read from a file with `TriMesh.load`, or built directly from raw vertex/face arrays:
13
+
14
+ ```python
15
+ from pyFM.mesh import TriMesh
16
+
17
+ mesh1 = TriMesh.load("cat-00.off", area_normalize=True, center=True)
18
+ mesh2 = TriMesh.load("lion-00.off", area_normalize=True, center=True)
19
+ ```
20
+
21
+ Computing a functional map
22
+ --------------------------
23
+
24
+ `FunctionalMapping` drives the standard workflow: compute the Laplace-Beltrami
25
+ spectrum and descriptors with `preprocess`, then optimize the map with `fit`.
26
+
27
+ ```python
28
+ from pyFM.functional import FunctionalMapping
29
+
30
+ model = FunctionalMapping(mesh1, mesh2)
31
+
32
+ # Spectrum + descriptors
33
+ model.preprocess(
34
+ descr_type="WKS", # "WKS" or "HKS"
35
+ subsample_step=2, # keep every 2nd descriptor
36
+ verbose=True,
37
+ )
38
+
39
+ # Optimize the functional map
40
+ model.fit(
41
+ K=30, # eigenvectors kept on each shape
42
+ w_descr=1e0, # descriptor preservation
43
+ w_lap=1e-1, # Laplacian commutativity
44
+ w_dcomm=1e0, # descriptor commutativity
45
+ w_orient=1e0, # orientation term
46
+ verbose=True,
47
+ )
48
+ ```
49
+
50
+ Keep `w_orient` above zero. Descriptors alone cannot tell the left side of a shape
51
+ from its right, so with `w_orient=0` the optimization typically returns a
52
+ symmetry-flipped map that refinement will not fix.
53
+
54
+ Getting a point-to-point map
55
+ ----------------------------
56
+
57
+ `get_p2p` converts the functional map `model.FM_12` into a vertex-to-vertex map
58
+ `p2p_21` (for each vertex of mesh2, the index of its match on mesh1):
59
+
60
+ ```python
61
+ p2p_21 = model.get_p2p()
62
+ ```
63
+
64
+ Refinement
65
+ ----------
66
+
67
+ The output of `fit` is a starting point, not an answer — refinement is where most
68
+ of the accuracy comes from. ICP and ZoomOut both return a new functional map
69
+ rather than overwriting `model.FM_12`:
70
+
71
+ ```python
72
+ FM_icp = model.icp_refine(verbose=True)
73
+ p2p_21_icp = model.get_p2p(FM_icp)
74
+
75
+ FM_zo = model.zoomout_refine(nit=16, step=5, verbose=True)
76
+ p2p_21_zo = model.get_p2p(FM_zo)
77
+ ```
78
+
79
+ ZoomOut grows the map by `step` at each of its `nit` iterations, so `K = 30` ends
80
+ at `30 + 16 * 5 = 110`.
81
+
82
+ Evaluating accuracy
83
+ -------------------
84
+
85
+ Given an (approximate) ground-truth map and a geodesic distance matrix on the
86
+ source mesh, `pyFM.eval.accuracy` reports the mean geodesic error:
87
+
88
+ ```python
89
+ import numpy as np
90
+ import pyFM.eval
91
+
92
+ A_geod = mesh1.geodesic_matrix()
93
+ gt_p2p = np.loadtxt("lion2cat", dtype=int)
94
+
95
+ for name, p2p in [("fit", p2p_21), ("ICP", p2p_21_icp), ("ZoomOut", p2p_21_zo)]:
96
+ acc = pyFM.eval.accuracy(p2p, gt_p2p, A_geod, sqrt_area=mesh1.sqrt_area)
97
+ print(f"{name:8s}: {1e2 * acc:.2f}")
98
+ ```
99
+
100
+ On this pair that prints roughly `23.85`, `7.39` and `8.53`, in percent of
101
+ $\sqrt{\text{area}}$: the raw fit is coarse, and refinement is what makes it usable.
102
+
103
+ A few matched landmarks, passed to `preprocess` as `landmarks=`, cut the error
104
+ further still — see
105
+ [Computing a map without a correspondence](auto_examples/plot_05_maps_without_correspondence).
@@ -43,7 +43,8 @@ Whether you are an academic researcher exploring functional map theory or an ind
43
43
  What’s Next?
44
44
  ------------
45
45
 
46
- To get started with `pyFM`, check out the example notebook for a quick overview of the package’s capabilities.
46
+ To get started with `pyFM`, work through :doc:`getting_started`, then browse the :ref:`gallery` for runnable
47
+ examples with interactive 3D figures.
47
48
 
48
49
  .. To get started with `pyFM`, check out the [Installation Guide](#installation) and the [Quickstart Tutorial](#getting-started) for a hands-on introduction. For a deeper dive, explore the [API Reference](#detailed-api-reference) and [Tutorials](#tutorials) to learn more about advanced features and workflows.
49
50
 
@@ -57,7 +58,7 @@ Table of Contents
57
58
 
58
59
  install.md
59
60
  getting_started.md
60
- examples.md
61
+ auto_examples/index
61
62
 
62
63
  .. toctree::
63
64
  :maxdepth: 2
@@ -2,7 +2,7 @@ Installation
2
2
  ============
3
3
 
4
4
  This package is pretty minimal and should run on all machines. It supports
5
- Python 3.8+.
5
+ Python 3.10+.
6
6
 
7
7
  With pip
8
8
  --------
@@ -11,6 +11,16 @@ With pip
11
11
  pip install pyfmaps
12
12
  ```
13
13
 
14
+ Plotting
15
+ --------
16
+
17
+ The plotting helpers in {mod}`pyFM.viz`, used throughout the
18
+ [example gallery](auto_examples/index), are optional and pull in PyVista:
19
+
20
+ ```bash
21
+ pip install 'pyfmaps[viz]'
22
+ ```
23
+
14
24
  From source
15
25
  -----------
16
26