| | | 1 | | //======================================================================= |
| | | 2 | | // ContactQuadratic.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2024-present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using System; |
| | | 9 | | using static FixedMathSharp.Geometry.CylinderContactAlgebra; |
| | | 10 | | |
| | | 11 | | namespace FixedMathSharp.Geometry; |
| | | 12 | | |
| | | 13 | | /// <summary>Borrowed exact quadratic-field values and final nearest-even ratios.</summary> |
| | | 14 | | /// <content>Owns quadratic-field arithmetic; ratio comparison and materialization share this borrowed layout.</content> |
| | | 15 | | internal readonly ref partial struct ContactQuadratic |
| | | 16 | | { |
| | | 17 | | internal readonly Span<ulong> Values; |
| | | 18 | | internal readonly Span<int> Signs; |
| | 21598904 | 19 | | internal int FieldWords => Values.Length / 2; |
| | 11902562 | 20 | | internal Span<ulong> Rational => Values[..FieldWords]; |
| | 8831109 | 21 | | internal Span<ulong> Radical => Values[FieldWords..]; |
| | 10540980 | 22 | | internal ContactQuadratic(Span<ulong> values, Span<int> signs) { Values = values; Signs = signs; } |
| | 1265910 | 23 | | internal void Clear() { Values.Clear(); Signs.Clear(); } |
| | | 24 | | internal void Set(Signed576 value) |
| | | 25 | | { |
| | 1334928 | 26 | | Import(value, Rational); Radical.Clear(); Signs[0] = value.Sign; Signs[1] = 0; |
| | 333732 | 27 | | } |
| | | 28 | | internal void Set(in Signed832 value) |
| | | 29 | | { |
| | 239293 | 30 | | if (FieldWords >= 13) |
| | | 31 | | { |
| | | 32 | | // Plane-ray banks already hold the full scalar width. Import it |
| | | 33 | | // directly rather than prepare and rescan a second magnitude bank. |
| | 233845 | 34 | | WideArithmetic.GetMagnitude(value, Rational); |
| | 701535 | 35 | | Radical.Clear(); Signs[0] = value.Sign; Signs[1] = 0; |
| | 233845 | 36 | | return; |
| | | 37 | | } |
| | 5448 | 38 | | Span<ulong> magnitude = stackalloc ulong[13]; |
| | 5448 | 39 | | WideArithmetic.GetMagnitude(value, magnitude); |
| | 5448 | 40 | | int active = WideArithmetic.GetActiveMagnitudeLength(magnitude); |
| | 5448 | 41 | | magnitude[..active].CopyTo(Rational); |
| | 10896 | 42 | | Rational[active..].Clear(); Radical.Clear(); |
| | 10896 | 43 | | Signs[0] = value.Sign; Signs[1] = 0; |
| | 5448 | 44 | | } |
| | | 45 | | internal void CopyTo(ContactQuadratic destination, int multiplier = 1) |
| | | 46 | | { |
| | | 47 | | // Signs are authoritative: a zero field can retain stale scratch bytes. |
| | | 48 | | // Both complete active fields must fit the destination's borrowed banks. |
| | 838485 | 49 | | int rationalWords = Signs[0] == 0 ? 0 : WideArithmetic.GetActiveMagnitudeLength(Rational); |
| | 838485 | 50 | | int radicalWords = Signs[1] == 0 ? 0 : WideArithmetic.GetActiveMagnitudeLength(Radical); |
| | | 51 | | // A nonzero retained sign on an empty magnitude is still exact zero. |
| | | 52 | | // Reuse the active lengths above to canonicalize it without a rescan. |
| | 838485 | 53 | | int rationalSign = rationalWords == 0 ? 0 : Signs[0] * multiplier; |
| | 838485 | 54 | | int radicalSign = radicalWords == 0 ? 0 : Signs[1] * multiplier; |
| | 1676970 | 55 | | Rational[..rationalWords].CopyTo(destination.Rational); Radical[..radicalWords].CopyTo(destination.Radical); |
| | 1676970 | 56 | | destination.Rational[rationalWords..].Clear(); destination.Radical[radicalWords..].Clear(); |
| | 1676970 | 57 | | destination.Signs[0] = rationalSign; destination.Signs[1] = radicalSign; |
| | 838485 | 58 | | } |
| | 441345 | 59 | | internal void MultiplySign(int sign) { Signs[0] *= sign; Signs[1] *= sign; } |
| | | 60 | | internal void Add(ContactQuadratic value, int multiplier = 1) |
| | | 61 | | { |
| | 2564598 | 62 | | int rationalSign = Signs[0], radicalSign = Signs[1]; |
| | 1282299 | 63 | | CylinderContactAlgebra.Add(value.Rational, value.Signs[0] * multiplier, Rational, ref rationalSign); |
| | 1282299 | 64 | | CylinderContactAlgebra.Add(value.Radical, value.Signs[1] * multiplier, Radical, ref radicalSign); |
| | 2564598 | 65 | | Signs[0] = rationalSign; Signs[1] = radicalSign; |
| | 1282299 | 66 | | } |
| | | 67 | | internal int Sign(ReadOnlySpan<ulong> root) => |
| | 465797 | 68 | | WideConvexPrismRelations.GetConvexContactCandidateQuadraticSign(Rational, Signs[0], Radical, Signs[1], root); |
| | | 69 | | |
| | | 70 | | internal static ContactQuadratic At(Span<ulong> values, Span<int> signs, int index, int words = Words) => |
| | 3504584 | 71 | | new(values.Slice(index * 2 * words, 2 * words), signs.Slice(index * 2, 2)); |
| | | 72 | | |
| | | 73 | | /// <summary>Retains one branch of a*u²+2*b*u+c=0 without rounding.</summary> |
| | | 74 | | /// <remarks> |
| | | 75 | | /// Inputs are rational integer coefficients; the caller proves the complete discriminant fits the supplied root sto |
| | | 76 | | /// Outputs borrow caller storage. A zero polynomial denotes a family and |
| | | 77 | | /// returns false; callers must retain its interval/endpoints separately. |
| | | 78 | | /// Branches are -1 and +1; the linear and repeated cases use +1 only. |
| | | 79 | | /// </remarks> |
| | | 80 | | internal static bool TryGetParameter(ContactQuadratic a, ContactQuadratic b, ContactQuadratic c, |
| | | 81 | | int branch, Span<ulong> root, ContactQuadratic numerator, ContactQuadratic denominator) |
| | | 82 | | { |
| | 145908 | 83 | | root.Clear(); numerator.Clear(); denominator.Clear(); |
| | 48636 | 84 | | if (a.Signs[0] == 0) |
| | | 85 | | { |
| | 3456 | 86 | | if (b.Signs[0] == 0 || branch < 0) |
| | 2462 | 87 | | return false; |
| | 994 | 88 | | c.CopyTo(numerator, -b.Signs[0]); |
| | 1988 | 89 | | b.CopyTo(denominator, b.Signs[0]); denominator.Add(denominator); |
| | 994 | 90 | | return true; |
| | | 91 | | } |
| | | 92 | | // A coefficient bank need not fit its square. Preserve both complete |
| | | 93 | | // active products and subtraction carry before copying the discriminant. |
| | 45180 | 94 | | int words = Math.Max(2 * ActiveWords(b), ActiveWords(a) + ActiveWords(c)) + 1; |
| | 45180 | 95 | | Span<ulong> work = stackalloc ulong[4 * words]; |
| | 45180 | 96 | | Span<int> signs = stackalloc int[4]; |
| | 45180 | 97 | | ContactQuadratic discriminant = ContactQuadratic.At(work, signs, 0, words); |
| | 45180 | 98 | | ContactQuadratic product = ContactQuadratic.At(work, signs, 1, words); |
| | 45180 | 99 | | ContactQuadratic.Multiply(b, b, root, discriminant); |
| | 45180 | 100 | | ContactQuadratic.Multiply(a, c, root, product); |
| | 45180 | 101 | | discriminant.Add(product, -1); |
| | 45180 | 102 | | if (discriminant.Signs[0] < 0 || discriminant.Signs[0] == 0 && branch < 0) |
| | 24036 | 103 | | return false; |
| | 21144 | 104 | | int active = WideArithmetic.GetActiveMagnitudeLength(discriminant.Rational); |
| | | 105 | | System.Diagnostics.Debug.Assert(active <= root.Length); |
| | 21144 | 106 | | discriminant.Rational[..active].CopyTo(root); |
| | 21144 | 107 | | int orientation = a.Signs[0]; |
| | 21144 | 108 | | b.CopyTo(numerator, -orientation); |
| | 21144 | 109 | | numerator.Radical[0] = 1; |
| | 21144 | 110 | | numerator.Signs[1] = branch; |
| | 21144 | 111 | | a.CopyTo(denominator, orientation); |
| | 21144 | 112 | | return true; |
| | | 113 | | } |
| | | 114 | | |
| | | 115 | | internal static Fixed64 RoundRatio(ContactQuadratic numerator, ContactQuadratic denominator, ReadOnlySpan<ulong> roo |
| | | 116 | | long low = long.MinValue, long high = long.MaxValue, long parityOffset = 0) |
| | | 117 | | { |
| | 22179 | 118 | | if (numerator.Signs[1] == 0 && denominator.Signs[1] == 0 && (parityOffset & 1) == 0) |
| | | 119 | | { |
| | 12191 | 120 | | bool represented = Fixed64.TryGetSignedRawRatio(numerator.Rational, denominator.Rational, |
| | 12191 | 121 | | numerator.Signs[0] < 0, out Fixed64 result); |
| | | 122 | | System.Diagnostics.Debug.Assert(represented); |
| | 12191 | 123 | | return result; |
| | | 124 | | } |
| | 9988 | 125 | | RefineRoundRatioBounds(numerator, denominator, root, ref low, ref high); |
| | 9988 | 126 | | int words = Math.Max(ActiveWords(numerator), ActiveWords(denominator)) + 2; |
| | 9988 | 127 | | Span<ulong> values = stackalloc ulong[4 * words]; |
| | 9988 | 128 | | Span<int> signs = stackalloc int[4]; |
| | 19976 | 129 | | ContactQuadratic doubled = At(values, signs, 0, words), query = At(values, signs, 1, words); |
| | | 130 | | // Retained fields are padded to their worst-case proof width. Scaling |
| | | 131 | | // uses only complete active products; two extra limbs cover doubling |
| | | 132 | | // and every signed 65-bit threshold without copying padded tails. |
| | 9988 | 133 | | Scale(numerator, Signed576.ExtendValue(Signed320.ExtendValue(Signed192.Signed(2))), doubled); |
| | 22897 | 134 | | while (low < high) |
| | | 135 | | { |
| | 12909 | 136 | | ulong span = unchecked((ulong)high - (ulong)low); |
| | 12909 | 137 | | long midpoint = unchecked((long)((ulong)low + (span >> 1) + (span & 1))); |
| | 12909 | 138 | | Signed192 threshold = WideArithmetic.AddSigned192(Signed192.Signed(midpoint), Signed192.Signed(midpoint)); |
| | 12909 | 139 | | Scale(denominator, Signed576.ExtendValue(Signed320.ExtendValue(threshold)), query); |
| | 25818 | 140 | | query.MultiplySign(-1); query.Add(doubled); |
| | 20710 | 141 | | if (query.Sign(root) >= 0) low = midpoint; |
| | 5108 | 142 | | else high = midpoint - 1; |
| | | 143 | | } |
| | 9988 | 144 | | Signed192 half = WideArithmetic.AddSigned192(WideArithmetic.AddSigned192(Signed192.Signed(low), Signed192.Signed |
| | 9988 | 145 | | Scale(denominator, Signed576.ExtendValue(Signed320.ExtendValue(half)), query); |
| | 19976 | 146 | | query.MultiplySign(-1); query.Add(doubled); |
| | 9988 | 147 | | int comparison = query.Sign(root); |
| | 9988 | 148 | | return Fixed64.FromRaw(low + (comparison > 0 || comparison == 0 && ((low ^ parityOffset) & 1) != 0 ? 1 : 0)); |
| | | 149 | | } |
| | | 150 | | |
| | | 151 | | internal static void RefineRoundRatioBounds(ContactQuadratic numerator, ContactQuadratic denominator, |
| | | 152 | | ReadOnlySpan<ulong> root, ref long low, ref long high, bool squaredDepth = false) |
| | | 153 | | { |
| | 11309 | 154 | | WideArithmetic.GetMagnitudeSquareRootBounds(root, |
| | 11309 | 155 | | out Signed192 lowerRoot, out Signed192 upperRoot, out int shift); |
| | 11309 | 156 | | Span<ulong> roots = stackalloc ulong[4] |
| | 11309 | 157 | | { lowerRoot.Low, lowerRoot.Middle, upperRoot.Low, upperRoot.Middle }; |
| | 11309 | 158 | | int words = Math.Max(2, Math.Max(Math.Max(ActiveWords(numerator), ActiveWords(denominator)), |
| | 11309 | 159 | | Math.Max(WideArithmetic.GetActiveMagnitudeLength(numerator.Radical), |
| | 11309 | 160 | | WideArithmetic.GetActiveMagnitudeLength(denominator.Radical)) + 2 + ((shift + 63) >> 6))) + 1; |
| | 11309 | 161 | | Span<ulong> bounds = stackalloc ulong[4 * words]; |
| | 22618 | 162 | | Span<ulong> nMin = bounds[..words], nMax = bounds.Slice(words, words); |
| | 22618 | 163 | | Span<ulong> dMin = bounds.Slice(2 * words, words), dMax = bounds[(3 * words)..]; |
| | 11309 | 164 | | WideConvexPrismRelations.GetConvexContactCandidateQuadraticBounds(numerator.Rational, numerator.Signs[0], |
| | 11309 | 165 | | numerator.Radical, numerator.Signs[1], roots, shift, nMin, nMax, out int nMinSign, out int nMaxSign); |
| | 11309 | 166 | | WideConvexPrismRelations.GetConvexContactCandidateQuadraticBounds(denominator.Rational, denominator.Signs[0], |
| | 11309 | 167 | | denominator.Radical, denominator.Signs[1], roots, shift, dMin, dMax, out int dMinSign, out _); |
| | 11309 | 168 | | if (squaredDepth) |
| | | 169 | | { |
| | 1085 | 170 | | if (dMinSign <= 0) return; |
| | | 171 | | // N/D is four times depth squared. Positive interval endpoints |
| | | 172 | | // bound its floor; exact range and half-raw signs still own rounding. |
| | 1081 | 173 | | long floorLower = nMinSign <= 0 ? 0 : (long)(WideArithmetic.GetRatioFloorSquareRoot(nMin, dMax, ulong.MaxVal |
| | 1081 | 174 | | long upper = (long)(WideArithmetic.GetRatioFloorSquareRoot(nMax, dMin, ulong.MaxValue) >> 1); |
| | 1081 | 175 | | low = Math.Min(high, Math.Max(low, floorLower)); |
| | 1081 | 176 | | high = Math.Max(low, Math.Min(high, upper)); |
| | 1081 | 177 | | return; |
| | | 178 | | } |
| | | 179 | | // Division by a strictly positive interval is monotone in N; the |
| | | 180 | | // denominator endpoint reverses when N is negative. Cancellation can |
| | | 181 | | // make the enclosure unusable, so exact full-range search remains valid. |
| | 10226 | 182 | | if (dMinSign <= 0 |
| | 10226 | 183 | | || !Fixed64.TryGetSignedRawRatio(nMin, nMinSign < 0 ? dMin : dMax, nMinSign < 0, out Fixed64 roundedMin) |
| | 10316 | 184 | | || !Fixed64.TryGetSignedRawRatio(nMax, nMaxSign < 0 ? dMax : dMin, nMaxSign < 0, out Fixed64 roundedMax)) re |
| | | 185 | | // floor(x) lies in [roundEven(lower)-1, roundEven(upper)]. Preserve |
| | | 186 | | // the caller's clipped floor domain, including values outside it; the |
| | | 187 | | // original exact integer and half-raw signs still decide every output. |
| | 10136 | 188 | | long lower = roundedMin.m_rawValue == long.MinValue ? long.MinValue : roundedMin.m_rawValue - 1; |
| | 10136 | 189 | | low = Math.Min(high, Math.Max(low, lower)); |
| | 10136 | 190 | | high = Math.Max(low, Math.Min(high, roundedMax.m_rawValue)); |
| | 10136 | 191 | | } |
| | | 192 | | |
| | | 193 | | internal static void Scale(ContactQuadratic value, Signed576 scalar, ContactQuadratic result) |
| | | 194 | | { |
| | 795244 | 195 | | Span<ulong> magnitude = stackalloc ulong[9]; |
| | 795244 | 196 | | Import(scalar, magnitude); |
| | 795244 | 197 | | Scale(value, magnitude, scalar.Sign, result); |
| | 795244 | 198 | | } |
| | | 199 | | internal static void Scale(ContactQuadratic value, Signed832 scalar, ContactQuadratic result) |
| | | 200 | | { |
| | 559270 | 201 | | Span<ulong> magnitude = stackalloc ulong[13]; |
| | 559270 | 202 | | WideArithmetic.GetMagnitude(scalar, magnitude); |
| | 559270 | 203 | | Scale(value, magnitude, scalar.Sign, result); |
| | 559270 | 204 | | } |
| | | 205 | | |
| | | 206 | | // The caller proves that result has room for both complete products and |
| | | 207 | | // does not alias the source coefficient or scalar storage. |
| | | 208 | | internal static void Scale(ContactQuadratic value, ReadOnlySpan<ulong> magnitude, |
| | | 209 | | int scalarSign, ContactQuadratic result) |
| | | 210 | | { |
| | | 211 | | // Primitive plane covectors and reduced homogeneous scales often |
| | | 212 | | // supply integer +/-1. Copy the same exact fields without multiplying |
| | | 213 | | // padded banks; CopyTo also clears stale bytes under zero field signs. |
| | 1369765 | 214 | | if (scalarSign != 0 && WideArithmetic.GetActiveMagnitudeLength(magnitude) == 1 && magnitude[0] == 1UL) |
| | | 215 | | { |
| | 156685 | 216 | | value.CopyTo(result, scalarSign); |
| | 156685 | 217 | | return; |
| | | 218 | | } |
| | 2426160 | 219 | | int rationalSign = value.Signs[0] * scalarSign, radicalSign = value.Signs[1] * scalarSign; |
| | | 220 | | // Retained sign zero is authoritative even when borrowed bank bytes |
| | | 221 | | // are stale. Clear output rather than scan/multiply a coefficient |
| | | 222 | | // that has no mathematical contribution. |
| | 1213080 | 223 | | if (rationalSign == 0) |
| | | 224 | | { |
| | 911862 | 225 | | result.Rational.Clear(); result.Signs[0] = 0; |
| | | 226 | | } |
| | | 227 | | else |
| | | 228 | | { |
| | 757149 | 229 | | WideArithmetic.MultiplyMagnitudes(value.Rational, magnitude, result.Rational); |
| | 757149 | 230 | | result.Signs[0] = IsZero(result.Rational) ? 0 : rationalSign; |
| | | 231 | | } |
| | 1213080 | 232 | | if (radicalSign == 0) |
| | | 233 | | { |
| | 2239406 | 234 | | result.Radical.Clear(); result.Signs[1] = 0; |
| | | 235 | | } |
| | | 236 | | else |
| | | 237 | | { |
| | 93377 | 238 | | WideArithmetic.MultiplyMagnitudes(value.Radical, magnitude, result.Radical); |
| | 93377 | 239 | | result.Signs[1] = IsZero(result.Radical) ? 0 : radicalSign; |
| | | 240 | | } |
| | 93377 | 241 | | } |
| | | 242 | | |
| | | 243 | | internal static void Multiply(ContactQuadratic first, ContactQuadratic second, ReadOnlySpan<ulong> root, ContactQuad |
| | | 244 | | { |
| | 521348 | 245 | | int words = result.FieldWords; |
| | 521348 | 246 | | if (first.Signs[1] == 0 && second.Signs[1] == 0 || IsZero(root)) |
| | | 247 | | { |
| | | 248 | | // A zero radicand also removes both radical contributions. Rational |
| | | 249 | | // events need one complete product. Keep separate scratch |
| | | 250 | | // because result may alias either borrowed input; clear the unused |
| | | 251 | | // radical bank so later reuse cannot retain an old field. |
| | 480139 | 252 | | Span<ulong> rationalProduct = stackalloc ulong[words]; |
| | 480139 | 253 | | int sign = first.Signs[0] * second.Signs[0]; |
| | 480139 | 254 | | WideArithmetic.MultiplyMagnitudes(first.Rational, second.Rational, rationalProduct); |
| | 960278 | 255 | | rationalProduct.CopyTo(result.Rational); result.Radical.Clear(); |
| | 960278 | 256 | | result.Signs[0] = IsZero(rationalProduct) ? 0 : sign; result.Signs[1] = 0; |
| | 480139 | 257 | | return; |
| | | 258 | | } |
| | 41209 | 259 | | Span<ulong> scratch = stackalloc ulong[3 * words]; |
| | 123627 | 260 | | Span<ulong> rational = scratch[..words], radical = scratch.Slice(words, words), product = scratch[(2 * words)..] |
| | 41209 | 261 | | WideArithmetic.MultiplyMagnitudes(first.Rational, second.Rational, rational); |
| | 41209 | 262 | | int rationalSign = IsZero(rational) ? 0 : first.Signs[0] * second.Signs[0]; |
| | 41209 | 263 | | WideArithmetic.MultiplyMagnitudes(first.Radical, second.Radical, product); |
| | 41209 | 264 | | WideArithmetic.MultiplyMagnitudes(product, root, radical); |
| | 41209 | 265 | | CylinderContactAlgebra.Add(radical, first.Signs[1] * second.Signs[1], rational, ref rationalSign); |
| | 41209 | 266 | | WideArithmetic.MultiplyMagnitudes(first.Rational, second.Radical, radical); |
| | 41209 | 267 | | int radicalSign = IsZero(radical) ? 0 : first.Signs[0] * second.Signs[1]; |
| | 41209 | 268 | | WideArithmetic.MultiplyMagnitudes(first.Radical, second.Rational, product); |
| | 41209 | 269 | | CylinderContactAlgebra.Add(product, first.Signs[1] * second.Signs[0], radical, ref radicalSign); |
| | 82418 | 270 | | rational.CopyTo(result.Rational); radical.CopyTo(result.Radical); |
| | 82418 | 271 | | result.Signs[0] = rationalSign; result.Signs[1] = radicalSign; |
| | 41209 | 272 | | } |
| | | 273 | | |
| | | 274 | | internal static void MultiplyRoot(ContactQuadratic value, ReadOnlySpan<ulong> root, ContactQuadratic result) |
| | | 275 | | { |
| | 7364 | 276 | | WideArithmetic.MultiplyMagnitudes(value.Radical, root, result.Rational); |
| | 7364 | 277 | | value.Rational.CopyTo(result.Radical); |
| | 7364 | 278 | | result.Radical[value.FieldWords..].Clear(); |
| | 7364 | 279 | | result.Signs[0] = IsZero(result.Rational) ? 0 : value.Signs[1]; |
| | 7364 | 280 | | result.Signs[1] = value.Signs[0]; |
| | 7364 | 281 | | } |
| | | 282 | | |
| | | 283 | | } |