Files
2026-09-27 19:21:03 +08:00

538 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"
"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"
"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)