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