Files
orisys-sensor-design/blender/scripts/test_probe_falloff.py
T
ykcheungab c377c14474 1. added blender deformation probe for marker size picking and optimization
2. added unwarp map export in current cli, don't need to generate by precompute in sensor running
2026-09-25 15:44:44 +08:00

539 lines
28 KiB
Python

"""Tests for the bpy-free probe falloff and basis helpers.
The Xynova regression fixture below is the measured set of nearest-probe-vertex
distances (mm) for the 2502 non-probe vertices of ``sensing_surface`` in
``sensors/finger-xynova/r01/source/geometry.blend`` (sha256
``1C8A050B8490B6832D3A9F269DFEF3DAFBF94DBFE3E943B3956BE1104A50D9C9``). It was
dumped once from Blender and is embedded as base64 float32 because pytest cannot
import bpy: the measured distribution has to travel with the test. The values
are sorted ascending, so the first entries are the ones the radius can reach.
"""
from __future__ import annotations
import base64
import numpy as np
import pytest
from probe_falloff import (
FALLOFF_MODES,
affected_bands,
edge_lengths_mm,
falloff_weight,
falloff_weights,
flatness_mm,
linear_weight,
orthonormal_basis,
orthonormality_error,
percentile,
sharp_weight,
smooth_weight,
)
#: Sorted nearest-probe distances, mm, for the 2502 non-probe vertices.
_NONPROBE_DISTANCES_F32_B64 = (
"SRaSPkIO1T5RDtU+nmLVPrFi1T44xks/DpdUPxiXVD8n71Q/pu9UPzvEYj8SxmI/h6BoP4egaD+vtWg/cAh7P40Iez8uv4E/"
"Yb+BP0dMmD90TJg/RHKYP2Hjnj+a454/lSefP8wnnz8YDaI/Yg2iPzz8vD+i/Lw/T+HLP9Hhyz/g59I/T+jSPyNF0z+XRdM/"
"FlnZP0tP3z9sUN8/PRrkPzSD5j/ag+Y/yC/nP4ai5z8Yxec/+uTrP3Oj7z+oo+8/GJfxP0AY+j/4GPo/R3b6P82K+z99JAJA"
"oiQCQPgiA0BSIwNAAmADQF5gA0D4kwNAfB0HQHQtCUDJLQlA0pAJQFMwEUCLMBFA7lYSQAobE0C7rxRAG7AUQIchFUDjIRVA"
"4+cbQCR2HECkdhxAKMUcQH7FHEDdoB9AWKEfQCN/I0BZfyNAxJcjQIZtJEDkbSRAqQcmQJwZKEBCGihAHX8pQCqILUDEli1A"
"PpctQF0BL0DMAS9AWDw0QPFnNUCQaDVADas1QH2rNUBnzjVA4s41QFZJNkDwSTZA4t43QCLfN0AO6zdAXOs3QGraOUCirztA"
"hrM7QKTMO0D11TtAMHo9QJhXPkBUPD9AkAJAQJc5QECyO0FARjxBQP28QUCVvUFAOTlDQPKOQ0Aej0NAW71EQD+zRkDzx0ZA"
"RO9HQL3vR0BtUEhApz9KQH+FTEAQhkxAmf1MQDP+TECNVE1AF+hNQBOuTkDReVBARclQQCMhUUDdZ1JAKGhSQBeKUkA+glVA"
"6ZJXQH3KV0AUy1dAr1NYQEZUWEDbSVpA+OlaQInqWkCQIltA6D5bQLlSW0DTbVtAEY5cQDS7XECjul1AbNtdQPz/XUA7jF5A"
"o75eQBHdXkBgDV9A2ThfQBp7YECne2BAvy5iQBlPYkARuGJAqeFiQAkIY0DACGNAqaNjQEGkY0AmmWRA1ZlkQPBjZUC2hmVA"
"6O9lQGbwZUDWaGZARX1mQMSwZkDZgGdArahnQKysZ0CTHGhAoCloQL8AaUAG82tAqRNtQC9XbUBEfW1A38ZtQCPmbUDi521A"
"j2FuQCpibkBatG5AeLRuQHXrbkAs7G5Ab3lvQHpRckA/hXJA4wl0QMoqdEAYT3RA0E51QKTtdkB3DXdAomN3QDbHd0AkPnhA"
"zT54QMkkeUAKiXlApLx5QCMqekD4KnpAmOp7QFwNfED2fnxAj398QOnyfEDWo31AIMt9QIxrf0B/YoBAbm6AQN9xgECcuoBA"
"4LqAQOLFgECZ34BA8wiBQMIVgUAcFoFA6TuBQPipgUDkuoFAucGBQALSgUDp4IFA5+GBQKP0gUADHoJARDaCQBg5gkBxQoJA"
"BFOCQAdTgkBpaYJA3a+CQD2wgkAQF4NAqi+DQDh0g0AthINA4omDQLeRhECuqoRAjdiEQMHYhECp2YRAdxGFQFwehUANI4VA"
"aSOFQFckhUAqM4VA7zyFQIluhUB+gIVA0hOGQEAUhkDSWIZAC4aGQCmfhkAd9IZALQSHQKZPh0CnY4dA9X6HQB6IiEAL64hA"
"TQSJQFFXiUCgaYlAXnGJQJyCiUAZz4lAbOiJQM4FikD3GIpAwxmKQF4Yi0DOGItA/zSLQCV1i0CGt4tAxNCLQIcCjEASEoxA"
"jSyMQMssjEC+54xAjf2MQBo9jUBZPY1Au0+NQG/jjUC+8I1A//ONQI0CjkCJOo5Aik+OQGF0jkCkdI5AQLmOQAbIjkBK0o5A"
"JeWOQEj+jkAgaY9AnAqQQAkSkEA/JpBAfvKQQCMEkUAZCZFAkhuRQGRZkUCbQZJA1kGSQKtukkAXcJJAHoKSQBeJkkByvJJA"
"tcCSQK/HkkB1zJJAaFWTQHMflUB3MJVArjGVQCsylUDnT5VAf2KVQJF0lUCchpVAtvSVQOv+lUATCJZAdAuWQOwVlkCBHpZA"
"gDmWQO5BlkCUT5ZANlOWQINplkAJbJZA/oCWQLSJlkB2n5ZASQmXQIRDl0BhW5dAI4iXQIuIl0COnZdAq++XQED/l0Dzi5hA"
"DqSYQH20mEDmuJhAYMaYQH0ImUCyHJlAwGGZQHy8mUDRzJlACwGaQEkBmkDXK5pAaz2aQHc/mkDLP5pAyFeaQHKHmkAKoJpA"
"pwybQCUgm0BmHpxApR6cQD1XnECxb5xAOJicQGmpnEB41pxAYR+dQHIxnUALl51AwqmdQEy9nkD1y55AIc2eQHLknkDgAJ9A"
"NBmfQB4gn0BpMZ9AcXWfQPinn0AyqJ9AuUCgQFJKoEAQTKBA91+gQECpoEA4u6BAIfSgQC35oECfBKFAEg6hQGaWoUDHraFA"
"066hQGy/oUBywqFAcSSiQJ44okC/rKJAj7GjQJXJo0D10aNAb+WjQOvwo0BEEqRAxiGkQNlApEBhUaRAJVKkQH9ipEAbsqRA"
"aMqkQNPppEB5vaVAj82lQI0GpkBDGaZA3IGmQJWDpkAqk6ZAcIKnQHe5p0DJuadAyxyoQPg0qEBeWKhA72ioQPm/qEBKxqhA"
"OdioQKgCqUCnSalALlupQAKzqUBMs6lA3dGpQAQXqkBFHqpAZyqqQJ0sqkCiM6pAQ0CqQHBXqkCcbapARIGqQPeVqkA0napA"
"n7OqQOLlqkBo9apAYVurQHhvq0Cwb6tAdYarQIabq0CHrKtANtOrQCHrq0ClEKxA3yGsQDScrEBLs6xASLWsQJ7IrEBz3axA"
"COesQB/3rEDxAq1AKQOtQAXHrUAG/K1ATA2uQKEhrkDvOK5AL5auQNGorkAZh69AX9CvQFTir0AB6K9A7vKvQBP9r0CBFLBA"
"rVuwQNJqsEDVnrBA0vuwQLIEsUCtDLFAnBaxQKXfsUA577FA+iiyQHhAskDI/bJAzg6zQAEBtECHArRAtgq0QMATtEAPFrRA"
"wRy0QA0itEBEMbRArTS0QM9EtECen7RAG6q0QCi3tED+vbRAqMe0QPNStUAyU7VATom1QGmatUCTrLVAAbS1QMq4tUCEubVA"
"RMe1QHastkA3vbZAvy23QPctt0CPMbdA1EC3QFpEt0DYVrdAzVu3QChzt0CAjrdAJJ+3QExPuEAfYLhAlna4QJKNuEDh+7hA"
"gQu5QNxjuUDUdblAMaK5QOZLukAVTLpAXFq6QIdxukCAc7pASIS6QDuku0BjtLtA5Ba8QBsovEDyzbxA/dy8QHRGvUCwmr1A"
"sbG9QEzJvUCd3b1Az++9QMP5vUCfBL5Acwq+QLIOvkB0Ir5AySe+QMcovkAROL5AyDy+QF12vkCai75AY62+QBTNvkAg2r5A"
"NuC+QJDvvkABUr9AWV6/QK5nv0AKb79AIre/QNnHv0CfEMBAICPAQKY5wEBNScBA7XnAQN+PwEApG8FA7zHBQLV2wUBt5MFA"
"wufBQFL2wUA8+sFA7v3BQPsKwkDRysJAEcvCQA44w0AzSMNAAWPDQG2aw0BsrcNAv7DDQGy8w0AST8RASmDEQNiKxEAii8RA"
"49jEQFLvxEAzQcVAfFDFQMOPxUAxpsVAYzTGQPfHxkBLMcdA1UHHQOhTx0COZMdAcH3HQKV9x0DHhMdAWYfHQM+Yx0DamsdA"
"icPHQOTZx0AQ6MdAiPzHQAz/x0D6EchA4szIQAfjyEAV+shAdQzJQMCKyUCXmclApDHKQPJHykAkrspAKr3KQMHyykD3AstA"
"CZDLQC+Qy0AN08xAKdbMQJPjzEAN5cxAS+zMQHL6zEBSJc9ADzXPQAKiz0DDr89AzcXPQPYF0EBUHtBAlR7QQHxq0ECmf9BA"
"n47QQOQR0UA6JdFA8UfRQL9M0UC2WtFAYWDRQOR30UDYxtFACcfRQFDn0UBu+9FALP/RQGYR0kDrU9JATGjSQHK/0kAn09JA"
"NtXSQL7n0kC+RtNAgVjTQGhj00AIeNNARK3TQErh00Cm8NNAVQPUQBUY1EDnktRAIJPUQMax1ECzxtRAlSnVQLU61UCtbdVA"
"rYLVQD8F1kDUGtZA62HWQDly1kCdC9dAwCDXQIqv10AmwNdADATYQKFy2EA23NhAe/HYQLwb2UDkKtlAfYDZQMmV2UBHNdpA"
"tkraQCqp2kAZvNpAbdzaQDXe2kCf7NpAb/PaQAIc20BYL9tAuEXbQIjo20BN/NtAM/7bQHwO3ECHDt1AtiPdQCcr3UDgLN1A"
"WkDdQOJB3UBWSt1AnErdQCKt3kDtvN5Ar9DeQO3d3kAZ3t5As9/eQLxq30DKf99ACFPgQPln4EBqqOBAfb3gQKrW4EBC3uBA"
"5+rgQImH4UCuh+FA97PhQFPw4UBSBeJA9xnjQGkp40CbQuNAf1fjQCWt40CNvuNA0cHjQE7Y40CtCORA7xrkQCpG5ECsRuRA"
"/VfkQFtZ5ECYn+RAb7TkQILs5ECM/+RAwxHlQBcj5UBFOeVAaDnlQBHm5UBm+eVAugfmQGoc5kAqeuZA+3zmQAyL5kBtkOZA"
"siLnQEU250CRNudA4UrnQDl750DJj+dAfdXnQIrb50Ao6edALxnoQE0o6EALi+hA4pPoQH6b6ECup+hAWvroQMsO6UDKXOlA"
"o3DpQEfb6UABL+pA6ULqQM9L6kAITOpAO4XqQIeZ6kB8repANMHqQJRL60CIW+tAknXrQIiF60B+zOtAsszrQDvt60A/AexA"
"iKTtQFqz7UDIvu1ATs7tQOPS7UBi4u1ADVfuQDZX7kDXuu5AfLvuQA/L7kBzze5AstHvQLbc70DB5e9Alu/vQLPf8EC38/BA"
"SibxQBY68UB1qPFAKt/xQEzf8UDD9fFAwgnyQHrV8kC+5PJAvBPzQKQn80B43vNAXzn0QExN9EBqbfRAlG30QLmA9EB/m/RA"
"Ra/0QKNm9UCFevVARCD2QKqb9kBkr/ZAFr72QEvP9kBp6PZABPr2QB4N90DyHfdA3x/3QBIg90DbLvdAmof3QIaZ90ABqvdA"
"frX3QP/I90Cy2PdAXOz3QN4Z+EBeKvhAVIX4QHOP+ECnj/hAhZf4QGIe+UD9MflAucD5QBzL+UC60PlAjd35QKpQ+kArX/pA"
"S236QMOA+kAdg/pAu5X6QGf8+kC5/fpA5/36QKsP+0CiRvtAO1n7QOrF+0BN2ftAeBP8QGUc/EAqJvxAGCz8QGvn/EBX6PxA"
"Fvb8QCL7/ECpKP1A8Tv9QA3E/UDl1v1AHRf+QLUp/kDTX/5A+F/+QOuV/kB/pf5AF6n+QFu4/kCZL/9A+T7/QLU7AEEoRQBB"
"0cgAQUTSAEH1FQFBPy8BQbY4AUGVewFBFYMBQR6tAUGRtgFBLOQBQUXkAUGZLQJBETcCQb9cAkERZgJBG5QCQTSUAkHEsAJB"
"NLoCQUDrAkGgNgNBC0ADQQNJA0E5vgNBTr4DQSm/A0GUyANBO9UDQUHeA0F/OgRBykMEQXBKBEHIUwRBSWAEQRGmBEEKqwRB"
"3awEQUCtBEFaswRBBbUEQWjYBEG14QRBvusEQTT0BEFd+wRBXAMFQbkNBUG+XQVBz10FQUFjBUE1aQVB02sFQYFyBUFCnwVB"
"GKcFQdupBUEGswVBMP0FQVEFBkFwBgZBAA4GQVNDBkGESgZBrJQGQdudBkHKngZBcqYGQdTFBkGbzgZBhi0HQQgwB0FmNgdB"
"LjkHQbZkB0ERmwdB7aMHQaDPB0G42AdBzQoIQbETCEHCJAhB3CQIQcBzCEHTfAhB8HwIQdmFCEFxzQhBi80IQbscCUG7JQlB"
"XikJQR1mCUEJbwlB8rcJQUq/CUGL6wlBo+sJQZJTCkF5XApBTH4KQTmHCkECzApB6NQKQcZFC0GwTgtBvXoLQdJ6C0ENewtB"
"KqkLQfTAC0HRyQtB2+4LQXX3C0F/PQxBZ0YMQYNIDEF8TwxBvIoMQZe7DEFzxAxBRzsNQSVEDUFjSA1B7lANQbG8DUGExQ1B"
"XdwNQTnkDUH/8Q1Bu/oNQYEBDkHNAQ5BeQkOQYUJDkGJOw5B5D8OQZZCDkGtSA5BLlMOQUhTDkEcZg5BF2wOQTJuDkFRdQ5B"
"BMUOQcHNDkEg9Q5BM/UOQZH9DkHIBQ9BgEYPQTNMD0EmTw9B3lAPQedUD0FGWA9BALsPQUnDD0Hd1Q9BjN4PQfcHEEEICBBB"
"cGIQQRlrEEGVkRBB7JkQQW7yEEED+xBBkP0QQTN3EUGXfxFBMIYRQeWHEUHzhxFBvY4RQb4ZEkEgHhJB4SASQaUmEkGyYxJB"
"HmwSQZ66EkEVwxJB8lETQV9XE0FWWhNBqsgTQQrRE0HP6BNBwAIUQSMLFEEjDBRB8hIUQSg/FEGVRxRBJnAUQT1wFEGltRRB"
"DL4UQb4LFUHTCxVBWiwVQbg0FUGcUBVBulgVQVajFUG0qxVBkcAVQQcUFkEZFBZBvRoWQRojFkGIVxZBZF4WQXuSFkHCkhZB"
"hpoWQQ+bFkHHpxZBkwsXQeITF0GKFxdBoxkXQREfF0EXIRdBRl0XQexkF0FbhRdBj4UXQaSFF0GejRdB3KwXQRDfF0HN5hdB"
"QQAYQX0IGEFqHRhBkCQYQdpbGEFxfBhBp4QYQcSPGEGHlxhBLPoYQVwCGUHNYRlBp2kZQe15GUEOghlBLPwZQUcEGkEISBpB"
"508aQYOBGkGTiRpBpJoaQZKhGkFdChtBYhIbQRw4G0EJQBtBMEYbQSuXG0EnnxtB4O0bQXj0G0FkKBxB+ykcQVowHEHmMRxB"
"7aEcQeWpHEGrGB1BmCAdQbZZHUGYYR1B9Y0dQeCVHUH8AR5B5AkeQf90HkHkfB5BMIkeQc+QHkF0kh5BGZkeQUnnHkEp7x5B"
"CFkfQdtgH0FDth9B0r0fQWTKH0E50h9BKwUgQbY7IEGoPSBBg0MgQcVEIEG0WSBB4mAgQaRxIEGneCBBpXkgQbx5IEGIrCBB"
"NK0gQf20IEGBuiBBzcEgQRz5IEGM/iBBagUhQZ4JIUGzCSFBHh8hQd8mIUG7UyFBE1shQaaRIUFlmSFB4qEhQXP+IUGO/iFB"
"HAUiQc0MIkHLFyJBMR8iQbZ5IkFhgSJBA/AiQaz3IkHQ+CJBSQAjQTE1I0HSOyNBB0sjQW1VI0F9VSNBnGgjQTZwI0EL5CNB"
"U+kjQVDqI0Gi6yNBsvAjQcrwI0HgYiRBamokQQXVJEHM1SRBAd8kQY/lJEF35iRBE+0kQZNsJUEOdCVB29ElQVHZJUHESCZB"
"PlAmQYSJJkHpkCZBcL0mQefEJkHg5yZBPu4mQcAvJ0ExNydBSJ4nQe2fJ0F4pSdBX6cnQVQOKEHAFShBK1AoQUN7KEGfgihB"
"57goQQLAKEH35ihBXO4oQcZRKUEiWSlBkGopQVtxKUFAgilB7ogpQRqgKUH3pilB+bspQVHDKUGJ8ClBI/cpQQgXKkHsHSpB"
"9SUqQUItKkH9jypBQpcqQZzCKkGTySpBTfoqQZYBK0FEZStBfGwrQdtsK0H5lCtBAZwrQS7RK0Fl2CtBj+UrQfPrK0F6/itB"
"ngQsQQ0iLEGfPixBzUUsQa1ZLEEagCxBKYcsQTauLEFctSxBXs4sQXDOLEH41ixBiCAtQaonLUFUdi1BYX0tQSmWLUFCnS1B"
"hG0uQZR0LkFxjS5BeZQuQdrmLkHi7S5BP/AuQVHwLkGFVC9BplovQd1dL0HiZC9BAZgvQfaeL0Ef0i9BIdkvQYTzL0FUQzBB"
"WkowQduIMEEjlTBB6pswQW+xMEFruDBBiLwwQbYcMUGzIzFBsj0xQc5DMUGQhTFBiYwxQT2fMUHvpTFBaOwxQVjzMUGNUTJB"
"elgyQXSdMkEunjJB1qMyQaekMkFWtTJBQLwyQeroMkFv7zJBkvAyQdr2MkEuGDNBER8zQYVxM0EVeDNBhnozQWGBM0FTszNB"
"rdwzQYDjM0HuKjRBiTE0QfI+NEHHRTRBtaE0QXmoNEHSqDRB5q40QUkFNUFDBzVBDww1Qd4NNUFZajVBF3E1QX7RNUEz2DVB"
"TPg1Qez+NUGZqDZBQK82QW3wNkET9zZBD483QaiVN0HVvzdBxcI3QaXVN0GA2zdBr+U3QVDsN0G2XDhBjGI4QZhiOEFTYzhB"
"EIk4QZqPOEGR0DhBL9c4QfVAOUGKRzlBkE45QaNTOUFWcTlBqnc5QaCtOUExtDlBhRY6QRYdOkGuXTpBul06QQx8OkGagjpB"
"rt46QTXlOkHcPjtBYkU7QfqcO0F6oztBz8E7QdfHO0GT1ztBrd07QXL5O0G1/DtB4f87QbICPEHkBDxBDzA8QdgzPEELOjxB"
"uVQ8QSxbPEEVZDxBvGk8QVGvPEG8tTxBXso8QZHQPEGVCT1BAhA9QehjPUFIaj1BjHw9QVyCPUHijT1BGZQ9QZ++PUEAxT1B"
"MRo+QYggPkFscD5BonY+QUJ3PkGWfT5B1Ik+QW7WPkG33D5B7GM/QSJqP0HGnT9B/6M/QdFaQEEKYUBB5mhAQYJuQEHZdEBB"
"CntAQWRLQUGdUUFBdFJBQeRXQUHoX0FBDGZBQem+QUEdxUFBmSpCQY4uQkG8NEJBnTVCQZc7QkERmkJBRaBCQVUBQ0F7B0NB"
"7D1DQU9kQ0FxakNBeMNDQaPJQ0EJ0kNBFdJDQVwfREF6JURBd3hEQSR8REGMfkRBP89EQVfVREFv7kRBnfJEQSb0REG1E0VB"
"ZxZFQVsZRUEtHEVBLCRFQUIqRUHMd0VB2H1FQUyBRUEgh0VB9KhFQfyoRUGhykVBodBFQQb2RUEUHUZBoSJGQRYjRkF1KEZB"
"115GQVlkRkGPb0ZBkHVGQXvCRkFwyEZBJu1GQf3yRkFSFkdBSBxHQZNXR0FYw0dBO8lHQfzSR0Ha2EdB5XtIQcKBSEEuxkhB"
"CsxIQVYMSUGqEUlBy0RJQZlKSUFZuUlBN79JQQwfSkGyJEpB8C9KQck1SkEzo0pBBqlKQW7nSkEA7UpBRRJLQRwYS0GKVktB"
"6XxLQbeCS0HX4ktBpOhLQQxETEHUSUxBl6BMQVumTEH2+ExBuv5MQcoOTUHnE01BzE1NQYRTTUGWn01BSaVNQeLuTUGX9E1B"
"wgdOQboiTkGsJU5BGyhOQV80TkG1OU5BMTxOQeJBTkGgWk5BCmBOQSaITkHQjU5BtdFOQUnTTkEo105B59hOQREeT0GtI09B"
"BidPQRMnT0EITk9BOFNPQehoT0F9bk9BZ3tPQeeAT0GpAFBBMQZQQYZKUEEEUFBBJ59QQa2kUEHL21BB19tQQV8xUUHaNlFB"
"4klRQV1PUUEiv1FBkAVSQQQLUkEdIlJBmidSQQsuUkGOM1JBx4xSQRrDUkGXyFJBc89SQbbUUkHGD1NBQRVTQY5SU0EEWFNB"
"j2tTQYiCU0H+h1NBtYxTQe6RU0Gp4FNBFuZTQUbxU0G69lNBSVtUQbxgVEFKwFRBu8VUQbTsVEExIFVBmiVVQe96VUFagFVB"
"tdBVQRrWVUEVIlZBbydWQZtvVkH4dFZB+7lWQVS/VkHIAVdBGQdXQYpHV0HbTFdB2V1XQSVeV0HVYldBKmNXQaiBV0Hli1dB"
"LpFXQSakV0FAqVdBd89XQbfUV0GHJVhBoCpYQaNIWEGKTVhBKlZYQWhbWEGu1VhB2NpYQYLfWEG15FhBP3BZQWZ1WUGWp1lB"
"vaxZQRDrWUHjDVpBBhNaQdppWkHraVpB/nxaQa6BWkG2jVpB25JaQT69WkFTwlpBiXpbQad/W0FD/ltBUf5bQeQqXEHLL1xB"
"VGFcQW5mXEGmz1xBv9RcQXA5XUGDPl1B7J1dQf2iXUEJ811BjfddQfL8XUEEAl5BglZeQZFbXkGYkV5Bj6peQZ6vXkFq+V5B"
"cP5eQWM2X0GkQ19BqkhfQemJX0Hjjl9B5MxfQeDRX0Ey1V9BxddfQT0NYEEzEmBBiUtgQYFQYEF3iGBBZY1gQY2PYEE1lGBB"
"bKBgQSalYEGvxGBBmclgQSTzYEHf92BB/TxhQdxBYUFSfWFBEn5hQWp/YUEWgmFBerhhQVO9YUGRMmJBTjdiQSM8YkHvQGJB"
"Ys1iQSTSYkHeBWNBpwpjQQNxY0HFdWNBAupjQcXuY0GTxmRBJ8tkQcTRZEGM1mRBJixlQTChZUE9oWVBzLBlQY+1ZUE462VB"
"XRpmQR0fZkHVfmZBj4NmQZXdZkFR4mZB1RZnQeAWZ0F1NmdBLDtnQX2JZ0E1jmdBo8lnQdTWZ0F922dBrx5oQVQjaEHHYWhB"
"b2ZoQc2gaEFspWhBfNxoQRrhaEGHFWlBHhppQUkiaUGJTGlBIFFpQS+CaUHHhmlBPbdpQcu7aUFRyWlBqc1pQabpaUEB7mlB"
"giFqQQsmakHYR2pBPkxqQclcakEFYWpBqY9qQS+UakHF2WpBJd5qQZ8Ga0EZC2tB44trQVaQa0H2kmtBWJdrQRkdbEErIWxB"
"NCRsQZMkbEGdKGxBp2ZsQRZrbEHuR21BWkxtQVZibUGRZm1B5iluQU8ubkES7m5BoABvQQUFb0G7B29BqQtvQTZlb0GgaW9B"
"wmxvQUPEb0GdyG9BIh1wQXchcEHBb3BBHnRwQUa8cEGewHBBk9JwQaHScEHP8XBB0wJxQSUHcUG5Q3FBFUhxQcB/cUEMhHFB"
"nLdxQem7cUEV7HFBV/BxQab9cUHiHXJBIyJyQeQqckHvKnJBsk1yQfNRckEsqnJBba5yQacGc0HmCnNB/UZzQY5Hc0GYZ3NB"
"wmtzQZiic0GopnNB+tFzQR3Wc0FfO3RBaD90QU1LdEFqT3RBHoV0QTHYdEFE3HRBtfd0Qcr7dEEjy3VBOM91Qdb+dUG3AnZB"
"/qh2QQ2tdkHaT3dBlnZ3QbWEd0HCiHdBA+53QRLyd0HVUnhB3lZ4QVqyeEFntnhB6At5QesPeUHjXnlB6WJ5QVKreUFPr3lB"
"WPF5QVv1eUFfGXpBMDF6QS81ekFCa3pBQW96QU6gekFGpHpBE9F6QQvVekFu/npBYgJ7QRspe0ELLXtBzVF7QbhVe0Eon3tB"
"NqB7QRSke0Hz7ntBzvJ7QQECfEEQAnxBmUJ8QW5GfEG0U3xBXFd8QaeQfEFUlHxBu5V8QUaZfEE+oHxBEqR8QZcDfUFHB31B"
"aA19QTURfUFVPn1BWT59QZ6OfUFkkn1BoqJ9QVmmfUF45X1B7+h9QbdhfkFtZX5Bg+1+QZ/wfkFpNH9BIjh/QTcHgEETCYBB"
"1FOAQdpxgEGyc4BBeKSAQVKmgEGT1IBBataAQc8BgUGlA4FB2CuBQa4tgUFsUoFBQFSBQZB1gUFod4FBZpWBQTqXgUHKrIFB"
"BrKBQdezgUGxxYFBs8uBQYTNgUHK4oFBmOSBQbT3gUGB+YFB4QqCQbEMgkG1HIJBfx6CQZAtgkFXL4JBxU2CQZBPgkFOboJB"
"EHCCQXuRgkE8k4JB6LmCQaa7gkFL6oJBCeyCQeUkg0GeJoNBZ1SDQe2Yg0HxmINBrp+DQVChg0G5KYRBuymEQb40hEEINoRB"
"kbyEQUO+hEH4I4VBVSWFQZYlhUGjJYVBilSFQThWhUFhgoVBDoSFQSKthUHNroVBgNSFQSnWhUE5+IVB4/mFQVwYhkEIGoZB"
"ETWGQbc2hkF7ToZBIlCGQdxkhkGCZoZBl3iGQT16hkEfioZBwIuGQd2ZhkGAm4ZBPqGGQaG1hkE+t4ZBJL6GQf7OhkGV0IZB"
"P3aHQa93h0GGfIdB/X2HQaKoh0EdqodBw8GHQR3Dh0HM64dBS+2HQRL4h0FEQYhBxUKIQSmjiEGrpIhBygiJQY4LiUERDYlB"
"ATKJQQMyiUGvdIlBMXaJQb6niUFBqYlBDbaJQRC2iUHp2IlBatqJQYoHikEHCYpBLTOKQao0ikGIW4pBBV2KQUuAikHKgYpB"
"RKGKQcCiikGBvopB/L+KQSzYikGq2YpBeu6KQfDvikGnAYtBHAOLQR8Si0GWE4tBVCCLQcohi0G9LItBMC6LQbtBi0EuQ4tB"
"S1SLQbdVi0FGZ4tBtGiLQS59i0GZfotBMYSLQaOYi0EOmotBV7yLQcO9i0F16otB2OuLQRlLjEFmTIxBpMqMQVSMjUHlo45B"
"j6+OQRzNjkEezY5BDEWPQQxFj0EVxI9BX8WPQW7Sj0G2049Bt5aQQRyYkEFd25BBfdyQQcMUkUHjFZFBb2GRQZRikUHQvJFB"
"9b2RQTghkkFcIpJB/YiSQSSKkkHE7pJB6O+SQTL9kkFBH5NBZyCTQVNNk0F2TpNBX3iTQYN5k0ECoJNBJ6GTQfvDk0EbxZNB"
"/uOTQSDlk0HZ/5NB+gCUQa4XlEHSGJRBtSuUQdYslEEgPJRBQD2UQSdJlEFDSZRBYkqUQY9TlEGtVJRBbFuUQY9clEFcYZRB"
"fWKUQSpplEFIapRB92qUQflqlEFLbpRBZG+UQcVzlEHedJRBLnyUQUZ9lEE8ipRBUIuUQY+glEGhoZRBOMGUQUfClEEax5RB"
"Z9eUQWfXlEFrBZVBaAaVQR6JlUFs25VBBkWaQc1FmkGtkJpBdJGaQbXWmkHW5ppBoOeaQewJm0GnLZtBdEibQTlJm0EIsJtB"
"0rCbQQHMm0EtGJxB9xicQdd7nEGgfJxBnoycQW2qnEE3q5xBHdacQeXWnEFW/pxBHv+cQbsinUGCI51BDUOdQdZDnUEOX51B"
"1l+dQZF2nUFXd51BuomdQYOKnUFfi51B55WdQZSWnUGllp1BVZedQcuYnUGSmZ1BiJydQSOdnUFGnZ1B3Z2dQQGknUHKpJ1B"
"/qSdQcKlnUG1q51BeaydQUOtnUEHrp1BW66dQRivnUFTsJ1BFrGdQS+ynUFQsp1B9rKdQRaznUHH0Z1BcNKdQWgbnkG0lZ9B"
"7bCfQe6wn0HsBaBBHwagQT0IoEE/CKBBP4agQVGGoEGobaFBZW+kQQGepEH90aRBbeGkQd3hpEEB/aRBKk2lQZhNpUHntqVB"
"UrelQYK8pUHVGaZBPRqmQU1HpkG8R6ZBbXGmQdpxpkGpl6ZBE5imQbCwpkEEsaZBh7KmQd+ypkGLs6ZB8LOmQa25pkEZuqZB"
"G72mQYS9pkG+zaZBJc6mQT7XpkGu16ZBseKmQRrjpkEm8KZBkPCmQTL5pkGb+aZBNgSnQaAEp0FuDqdB1w6nQZcTp0EDFKdB"
"jx6nQf0ep0E6H6dBqB+nQeMkp0FNJadBZiWnQdMlp0EeKKdBcyinQYgop0HaKKdBhMOnQfUEqUGCrqlBh9upQfDnqUFa7qlB"
"0xaqQaUkqkHNZapB0WWqQZV3qkFZ1KpBd9uqQT/lqkHPQ6tBvk2rQeC2q0FXuqtBWB2sQcQerEHPfaxBtoOsQUDarEEL4qxB"
"f/KsQeAgrUEYKq1BOTCtQTY0rUGMWa1Bn1mtQRZnrUE7cq1BJHetQah4rUERfK1BdJytQYycrUGXpa1BlK6tQXK2rUGJuK1B"
"TdStQcjWrUFd2K1B6eWtQd7mrUEz+a1B4v2tQWYErkEFBa5BchGuQQwUrkGwFq5B6xauQSkXrkGWG65B3xuuQfkbrkFcHK5B"
)
#: Signed offsets of the 6 measured probe vertices from their centroid along the
#: basis z axis (mm). Peak-to-valley is the contract's flatness_mm = 0.007611.
_MEASURED_PROBE_OFFSETS_Z = (
-0.0006562,
0.0042672,
-0.0033438,
0.0036863,
-0.0033205,
-0.0006329,
)
def _measured_nonprobe_distances() -> np.ndarray:
raw = base64.b64decode(_NONPROBE_DISTANCES_F32_B64)
return np.frombuffer(raw, dtype="<f4").astype(np.float64)
def _measured_probe_cloud() -> tuple[np.ndarray, np.ndarray]:
"""Return (coords, member_indices) reproducing the measured radii exactly.
The 6 probe members sit at the origin; each non-probe vertex sits on the +x
axis at its measured nearest-probe distance, so the minimum distance to a
member equals the measured value.
"""
distances = _measured_nonprobe_distances()
members = np.zeros((6, 3), dtype=np.float64)
others = np.stack(
[distances, np.zeros_like(distances), np.zeros_like(distances)], axis=1
)
coords = np.vstack([members, others])
return coords, np.arange(6, dtype=np.int64)
# ------------------------------------------------------------- curves
def test_curve_endpoints() -> None:
assert smooth_weight(0.0) == 1.0
assert smooth_weight(1.0) == 0.0
assert linear_weight(0.0) == 1.0
assert linear_weight(1.0) == 0.0
assert sharp_weight(0.0) == 1.0
assert sharp_weight(1.0) == 0.0
assert smooth_weight(0.25) == pytest.approx(1.0 - (3 * 0.25**2 - 2 * 0.25**3))
def test_curve_monotonic_decreasing() -> None:
samples = np.linspace(0.0, 1.0, 101)
smooth = np.array([smooth_weight(t) for t in samples])
linear = np.array([linear_weight(t) for t in samples])
sharp = np.array([sharp_weight(t) for t in samples])
for curve in (smooth, linear, sharp):
assert np.all(np.diff(curve) <= 0.0)
assert curve.min() == pytest.approx(0.0)
assert curve.max() == pytest.approx(1.0)
def test_falloff_weight_thresholds_and_dispatch() -> None:
assert falloff_weight(0.0, "smooth") == 1.0
assert falloff_weight(-5.0, "sharp") == 1.0
assert falloff_weight(1.0, "constant") == 0.0
assert falloff_weight(1.5, "smooth") == 0.0
assert falloff_weight(0.4, "linear") == pytest.approx(0.6)
assert falloff_weight(0.4, "sharp") == pytest.approx(0.36)
assert falloff_weight(0.4, "constant") == 1.0
with pytest.raises(ValueError):
falloff_weight(0.5, "bounce")
def test_falloff_modes_list_matches_the_contract() -> None:
assert FALLOFF_MODES == ("smooth", "linear", "sharp", "constant")
# -------------------------------------------------------------- basis
def _mixed_normals() -> np.ndarray:
"""Six unit normals whose mean is tilted away from +z (mixed, not symmetric)."""
angle_a = np.radians(2.5533)
angle_b = np.radians(0.5)
tilted_a = np.array([np.sin(angle_a), 0.0, np.cos(angle_a)])
tilted_b = np.array([-np.sin(angle_b), 0.0, np.cos(angle_b)])
return np.array([tilted_a] * 3 + [tilted_b] * 3, dtype=np.float64)
def _tilted_normals(angle_deg: float) -> np.ndarray:
"""Six unit normals with mean exactly +z and max deviation angle_deg."""
angle = np.radians(angle_deg)
return np.array(
[[np.sin(angle), 0.0, np.cos(angle)]] * 3
+ [[-np.sin(angle), 0.0, np.cos(angle)]] * 3,
dtype=np.float64,
)
def test_orthonormal_basis_rows_are_x_y_z_and_right_handed() -> None:
normal = np.array([0.2, -0.3, 0.9])
normals = np.array([normal / np.linalg.norm(normal)] * 6)
origin, basis, report = orthonormal_basis(
normals, origin=[74.76961, 16.2155, 120.80202]
)
x, y, z = basis
assert report["member_count"] == 6
assert report["origin_given"] is True
assert origin == pytest.approx([74.76961, 16.2155, 120.80202])
assert z == pytest.approx(normals[0])
assert x == pytest.approx(
np.cross([0.0, 0.0, 1.0], normals[0])
/ np.linalg.norm(np.cross([0.0, 0.0, 1.0], normals[0]))
)
assert y == pytest.approx(np.cross(z, x))
assert np.allclose(np.cross(z, x), y, atol=1e-12)
assert orthonormality_error(basis) < 1e-12
assert report["orthonormal"] is True
assert report["x_fallback"] is False
assert report["normal_angle_max_deg"] == pytest.approx(0.0, abs=1e-9)
def test_orthonormal_basis_reports_max_normal_angle() -> None:
normals = _mixed_normals()
_, basis, report = orthonormal_basis(normals, origin=[0.0, 0.0, 0.0])
unit = normals / np.linalg.norm(normals, axis=1)[:, None]
expected = float(
np.degrees(np.arccos(np.clip(unit @ basis[2], -1.0, 1.0))).max()
)
assert report["normal_angle_max_deg"] == pytest.approx(expected, abs=1e-9)
assert 1.5 < report["normal_angle_max_deg"] < 1.6
assert orthonormality_error(basis) < 1e-12
def test_orthonormal_basis_origin_is_optional() -> None:
origin, basis, report = orthonormal_basis(_mixed_normals())
assert origin is None
assert report["origin_given"] is False
assert basis.shape == (3, 3)
def test_orthonormal_basis_degenerate_x_falls_back_to_plus_x() -> None:
origin, basis, report = orthonormal_basis(
np.array([[0.0, 0.0, 1.0]] * 4), origin=[0.0, 0.0, 0.0]
)
assert report["x_fallback"] is True
assert basis[0] == pytest.approx([1.0, 0.0, 0.0])
assert basis[1] == pytest.approx([0.0, 1.0, 0.0])
assert basis[2] == pytest.approx([0.0, 0.0, 1.0])
assert report["orthonormal"] is True
def test_orthonormal_basis_rejects_degenerate_input() -> None:
with pytest.raises(ValueError):
orthonormal_basis(np.zeros((0, 3)))
with pytest.raises(ValueError):
orthonormal_basis(np.zeros((2, 2)))
with pytest.raises(ValueError):
orthonormal_basis(np.array([[0.0, 0.0, 0.0]]))
with pytest.raises(ValueError):
# perfectly cancelling normals leave no surface normal
orthonormal_basis(
np.array([[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]])
)
def test_orthonormality_error_detects_a_bad_basis() -> None:
assert orthonormality_error(np.eye(3)) == 0.0
assert orthonormality_error(np.diag([2.0, 1.0, 1.0])) == pytest.approx(3.0)
with pytest.raises(ValueError):
orthonormality_error(np.eye(2))
# ----------------------------------------------------- falloff_weights
def test_falloff_weights_are_one_for_members_and_zero_at_radius() -> None:
coords = np.array(
[
[0.0, 0.0, 0.0], # member
[0.5, 0.0, 0.0], # member
[0.25, 0.0, 0.0], # inside (d = 0.25 from the first member)
[1.0, 0.0, 0.0], # exactly radius from the 0.5 member -> d = 0.5
[0.0, 0.9, 0.0], # outside
]
)
weights = falloff_weights(coords, [0, 1], 0.5, "smooth")
assert weights.dtype == np.float64
assert weights.shape == (5,)
assert weights[0] == 1.0
assert weights[1] == 1.0
assert weights[2] == pytest.approx(smooth_weight(0.5))
assert weights[3] == 0.0
assert weights[4] == 0.0
def test_falloff_weights_match_the_scalar_curve() -> None:
rng = np.random.default_rng(7)
coords = rng.uniform(-1.0, 1.0, size=(64, 3))
members = np.array([0, 1, 2])
for mode in FALLOFF_MODES:
weights = falloff_weights(coords, members, 0.75, mode)
for index in range(coords.shape[0]):
distance = float(
np.linalg.norm(coords[index] - coords[members], axis=1).min()
)
assert weights[index] == pytest.approx(
falloff_weight(distance / 0.75, mode), abs=1e-15
)
def test_falloff_weights_are_symmetric_in_offset_direction() -> None:
coords = np.array(
[
[0.0, 0.0, 0.0],
[0.3, 0.0, 0.0],
[-0.3, 0.0, 0.0],
[0.0, 0.3, 0.0],
[0.0, 0.0, 0.3],
[0.3 / np.sqrt(3)] * 3,
]
)
weights = falloff_weights(coords, [0], 0.5, "smooth")
for index in (2, 3, 4, 5):
assert weights[index] == pytest.approx(weights[1], abs=1e-15)
assert weights[1] == pytest.approx(smooth_weight(0.6))
def test_falloff_weights_constant_mode() -> None:
coords = np.array([[0.0, 0.0, 0.0], [0.4, 0.0, 0.0], [0.6, 0.0, 0.0]])
weights = falloff_weights(coords, [0], 0.5, "constant")
assert list(weights) == [1.0, 1.0, 0.0]
@pytest.mark.parametrize(
"kwargs",
[
{"radius_mm": 0.0},
{"radius_mm": -1.0},
{"mode": "bounce"},
],
)
def test_falloff_weights_reject_bad_settings(kwargs: dict) -> None:
coords = np.array([[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]])
settings = {"radius_mm": 0.5}
settings.update(kwargs)
with pytest.raises(ValueError):
falloff_weights(coords, [0], **settings)
def test_falloff_weights_reject_bad_geometry() -> None:
with pytest.raises(ValueError):
falloff_weights(np.zeros((3, 2)), [0], 0.5)
with pytest.raises(ValueError):
falloff_weights(np.zeros((3, 3)), [], 0.5)
with pytest.raises(ValueError):
falloff_weights(np.zeros((3, 3)), [5], 0.5)
# -------------------------------------------------------- affected_bands
def test_affected_bands_omits_empty_bands() -> None:
weights = np.array([1.0, 1.0, 0.75, 0.4, 0.2, 0.07, 0.01, 0.0])
assert affected_bands(weights) == {
"1.0": 2,
"0.5-1.0": 1,
"0.3-0.5": 1,
"0.1-0.3": 1,
"0.05-0.1": 1,
"0.0-0.05": 1,
}
assert affected_bands(np.array([0.0, 0.0, 1.0])) == {"1.0": 1}
# ------------------------------------------- measured Xynova regression
def test_measured_xynova_radius_half_mm_reaches_exactly_five_vertices() -> None:
coords, members = _measured_probe_cloud()
assert coords.shape == (2508, 3)
assert len(members) == 6
weights = falloff_weights(coords, members, 0.5, "smooth")
inside = weights[(weights > 0.0) & (weights < 1.0)]
assert int((weights == 0.0).sum()) == 2497
assert int((weights >= 1.0).sum()) == 6
assert inside.size == 5, "radius 0.5 mm must touch exactly 5 non-probe vertices"
distances = _measured_nonprobe_distances()
assert float(distances[0]) == pytest.approx(0.2853262745311897, abs=1e-6)
assert float(distances[1]) == pytest.approx(0.41612439347389385, abs=1e-6)
assert float(distances[4]) == pytest.approx(0.41676858076285744, abs=1e-6)
assert float(distances[5]) == pytest.approx(0.795993, abs=1e-6)
# The gap between the 5th and 6th neighbour is what makes the count 5, not 11.
assert int((distances < 0.5).sum()) == 5
assert int((distances < 0.6).sum()) == 5
assert int((distances < 0.79).sum()) == 5
def test_measured_xynova_weight_bands_match_the_contract_report() -> None:
coords, members = _measured_probe_cloud()
weights = falloff_weights(coords, members, 0.5, "smooth")
inside = np.sort(weights[(weights > 0.0) & (weights < 1.0)])
assert affected_bands(weights) == {"1.0": 6, "0.3-0.5": 1, "0.05-0.1": 4}
assert inside[4] == pytest.approx(0.3947265, abs=1e-6) # d = 0.28533
assert inside[:4] == pytest.approx(
[0.0739043, 0.0739052, 0.0749794, 0.0749802], abs=1e-6
)
# One vertex in 0.3-0.5, four in 0.05-0.1, none anywhere else.
assert int(((inside >= 0.3) & (inside < 0.5)).sum()) == 1
assert int(((inside >= 0.05) & (inside < 0.1)).sum()) == 4
def test_measured_xynova_percentile_utility() -> None:
distances = _measured_nonprobe_distances()
assert percentile(distances, 0.0) == pytest.approx(float(distances.min()))
assert percentile(distances, 50.0) == pytest.approx(
float(np.percentile(distances, 50)), rel=1e-6
)
assert percentile(distances, 100.0) == pytest.approx(float(distances.max()))
# ----------------------------------------------------------- statistics
def test_edge_lengths_mm() -> None:
coords = np.array(
[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [3.0, 4.0, 0.0], [0.0, 0.0, 2.0]]
)
edges = np.array([[0, 1], [1, 2], [0, 3]])
assert edge_lengths_mm(coords, edges) == pytest.approx([3.0, 4.0, 2.0])
assert edge_lengths_mm(coords, np.zeros((0, 2), dtype=np.int64)).size == 0
def test_edge_percentiles_match_the_measured_contract_row() -> None:
# Contract: mesh edges min 0.0504, p50 0.6285, p90 0.991, max 1.816 mm.
edges = np.array([0.0504, 0.628501, 0.991, 1.816])
assert percentile(edges, 0.0) == pytest.approx(0.0504)
assert percentile(edges, 100.0) == pytest.approx(1.816)
assert percentile(edges, 50.0) == pytest.approx(np.percentile(edges, 50))
def test_flatness_mm_is_peak_to_valley_along_the_normal() -> None:
offsets = np.array(_MEASURED_PROBE_OFFSETS_Z)
coords = np.stack([np.zeros_like(offsets), np.zeros_like(offsets), offsets], axis=1)
assert flatness_mm(coords, [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]) == pytest.approx(
0.007611, abs=1e-6
)
assert flatness_mm(coords, [0.0, 0.0, 0.0], [0.0, 0.0, 2.0]) == pytest.approx(
0.007611, abs=1e-6
) # non-unit normals are normalised
with pytest.raises(ValueError):
flatness_mm(coords, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0])
def test_percentile_edges() -> None:
assert percentile([3.0, 1.0, 2.0], 0.0) == 1.0
assert percentile([3.0, 1.0, 2.0], 100.0) == 3.0
assert percentile([3.0, 1.0, 2.0], 50.0) == 2.0
with pytest.raises(ValueError):
percentile([], 50.0)
with pytest.raises(ValueError):
percentile([1.0], 101.0)