XYShape.js (15013B)
1 import sortBy from "lodash/sortBy.js"; 2 import { epsilon_float } from "./magicNumbers.js"; 3 import * as E_A from "./utility/E_A.js"; 4 import * as E_A_Floats from "./utility/E_A_Floats.js"; 5 import * as E_A_Sorted from "./utility/E_A_Sorted.js"; 6 import * as Result from "./utility/result.js"; 7 export const XYShapeError = { 8 mapErrorArrayToError(errors) { 9 if (errors.length === 0) { 10 return undefined; 11 } 12 else if (errors.length === 1) { 13 return errors[0]; 14 } 15 else { 16 return { 17 tag: "MultipleErrors", 18 errors, 19 }; 20 } 21 }, 22 toString(t) { 23 switch (t.tag) { 24 case "NotSorted": 25 return `${t.property} is not sorted`; 26 case "IsEmpty": 27 return `${t.property} is empty`; 28 case "NotFinite": 29 return `${t.property} is not finite. Example value: ${t.value}`; 30 case "DifferentLengths": 31 return `${t.p1Name} and ${t.p2Name} have different lengths. ${t.p1Name} has length ${t.p1Length} and ${t.p2Name} has length ${t.p2Length}`; 32 case "MultipleErrors": 33 return `Multiple Errors: ${t.errors 34 .map(XYShapeError.toString) 35 .map((r) => `[${r}]`) 36 .join(", ")}`; 37 } 38 }, 39 }; 40 const interpolate = (xMin, xMax, yMin, yMax, xIntended) => { 41 const minProportion = (xMax - xIntended) / (xMax - xMin); 42 const maxProportion = (xIntended - xMin) / (xMax - xMin); 43 return yMin * minProportion + yMax * maxProportion; 44 }; 45 const extImp = (value) => { 46 if (value === undefined) { 47 throw new Error("Tried to perform an operation on an empty XYShape."); 48 } 49 return value; 50 }; 51 export const T = { 52 length(t) { 53 return t.xs.length; 54 }, 55 empty: { xs: [], ys: [] }, 56 isEmpty(t) { 57 return T.length(t) === 0; 58 }, 59 minX(t) { 60 return extImp(t.xs[0]); 61 }, 62 maxX(t) { 63 return extImp(t.xs[t.xs.length - 1]); 64 }, 65 firstY(t) { 66 return extImp(t.ys[0]); 67 }, 68 lastY(t) { 69 return extImp(t.ys[t.ys.length - 1]); 70 }, 71 xTotalRange(t) { 72 return T.maxX(t) - T.minX(t); 73 }, 74 mapX(t, fn) { 75 return { xs: t.xs.map(fn), ys: t.ys }; 76 }, 77 mapY(t, fn) { 78 return { xs: t.xs, ys: t.ys.map(fn) }; 79 }, 80 mapYResult(t, fn) { 81 const mappedYs = []; 82 for (const y of t.ys) { 83 const mappedY = fn(y); 84 if (!mappedY.ok) { 85 return mappedY; 86 } 87 mappedYs.push(mappedY.value); 88 } 89 return Result.Ok({ 90 xs: t.xs, 91 ys: mappedYs, 92 }); 93 }, 94 square(t) { 95 return T.mapX(t, (x) => x ** 2); 96 }, 97 zip({ xs, ys }) { 98 return E_A.zip(xs, ys); 99 }, 100 fromArray([xs, ys]) { 101 return { xs, ys }; 102 }, 103 fromArrays(xs, ys) { 104 return { xs, ys }; 105 }, 106 accumulateYs(p, fn) { 107 return T.fromArray([p.xs, E_A.accumulate(p.ys, fn)]); 108 }, 109 concat(t1, t2) { 110 const cxs = [...t1.xs, ...t2.xs]; 111 const cys = [...t1.ys, ...t2.ys]; 112 return { xs: cxs, ys: cys }; 113 }, 114 isEqual(t1, t2) { 115 return E_A.isEqual(t1.xs, t2.xs) && E_A.isEqual(t1.ys, t2.ys); 116 }, 117 fromZippedArray(pairs) { 118 return T.fromArray(E_A.unzip(pairs)); 119 }, 120 equallyDividedXs(t, newLength) { 121 return E_A_Floats.range(T.minX(t), T.maxX(t), newLength); 122 }, 123 Validator: { 124 notSortedError(p) { 125 return { 126 tag: "NotSorted", 127 property: p, 128 }; 129 }, 130 notFiniteError(p, exampleValue) { 131 return { 132 tag: "NotFinite", 133 property: p, 134 value: exampleValue, 135 }; 136 }, 137 isEmptyError(propertyName) { 138 return { tag: "IsEmpty", property: propertyName }; 139 }, 140 differentLengthsError(t) { 141 return { 142 tag: "DifferentLengths", 143 p1Name: "Xs", 144 p2Name: "Ys", 145 p1Length: t.xs.length, 146 p2Length: t.ys.length, 147 }; 148 }, 149 areXsSorted(t) { 150 return E_A_Floats.isSorted(t.xs); 151 }, 152 areXsEmpty(t) { 153 return t.xs.length === 0; 154 }, 155 getNonFiniteXs(t) { 156 return t.xs.find((v) => !Number.isFinite(v)); 157 }, 158 getNonFiniteYs(t) { 159 return t.ys.find((v) => !Number.isFinite(v)); 160 }, 161 validate(t) { 162 const errors = []; 163 if (!T.Validator.areXsSorted(t)) { 164 errors.push(T.Validator.notSortedError("Xs")); 165 } 166 if (T.Validator.areXsEmpty(t)) { 167 errors.push(T.Validator.isEmptyError("Xs")); 168 } 169 if (t.xs.length !== t.ys.length) { 170 errors.push(T.Validator.differentLengthsError(t)); 171 } 172 const nonFiniteX = T.Validator.getNonFiniteXs(t); 173 if (nonFiniteX !== undefined) { 174 errors.push(T.Validator.notFiniteError("Xs", nonFiniteX)); 175 } 176 const nonFiniteY = T.Validator.getNonFiniteYs(t); 177 if (nonFiniteY !== undefined) { 178 errors.push(T.Validator.notFiniteError("Ys", nonFiniteY)); 179 } 180 return XYShapeError.mapErrorArrayToError(errors); 181 }, 182 }, 183 make(xs, ys) { 184 const attempt = { xs, ys }; 185 const maybeError = T.Validator.validate(attempt); 186 if (maybeError) { 187 return Result.Err(maybeError); 188 } 189 else { 190 return Result.Ok(attempt); 191 } 192 }, 193 makeFromZipped(values) { 194 const [xs, ys] = E_A.unzip(values); 195 return T.make(xs, ys); 196 }, 197 }; 198 const Pairs = { 199 first(t) { 200 return [T.minX(t), T.firstY(t)]; 201 }, 202 last(t) { 203 return [T.maxX(t), T.lastY(t)]; 204 }, 205 getBy(t, fn) { 206 return T.zip(t).find(fn); 207 }, 208 firstAtOrBeforeXValue(t, xValue) { 209 const firstGreaterIndex = E_A_Sorted.firstGreaterIndex(t.xs, xValue); 210 if (firstGreaterIndex === 0) { 211 return; 212 } 213 return [t.xs[firstGreaterIndex - 1], t.ys[firstGreaterIndex - 1]]; 214 }, 215 }; 216 export const YtoX = { 217 linear(t, y) { 218 const firstHigherIndex = E_A_Sorted.firstGreaterIndex(t.ys, y); 219 if (firstHigherIndex === t.ys.length) { 220 return T.maxX(t); 221 } 222 else if (firstHigherIndex === 0) { 223 return T.minX(t); 224 } 225 else { 226 const lowerOrEqualIndex = firstHigherIndex - 1; 227 if (t.ys[lowerOrEqualIndex] === y) { 228 return t.xs[lowerOrEqualIndex]; 229 } 230 else { 231 return interpolate(t.ys[lowerOrEqualIndex], t.ys[firstHigherIndex], t.xs[lowerOrEqualIndex], t.xs[firstHigherIndex], y); 232 } 233 } 234 }, 235 }; 236 export const XtoY = { 237 stepwiseIncremental(t, x) { 238 return Pairs.firstAtOrBeforeXValue(t, x)?.[1]; 239 }, 240 stepwiseIfAtX(t, f) { 241 return Pairs.getBy(t, ([x]) => x === f)?.[1]; 242 }, 243 linear(t, x) { 244 const firstHigherIndex = E_A_Sorted.firstGreaterIndex(t.xs, x); 245 if (firstHigherIndex === t.xs.length) { 246 return T.lastY(t); 247 } 248 else if (firstHigherIndex === 0) { 249 return T.firstY(t); 250 } 251 else { 252 const lowerOrEqualIndex = firstHigherIndex - 1; 253 if (t.xs[lowerOrEqualIndex] === x) { 254 return t.ys[lowerOrEqualIndex]; 255 } 256 else { 257 return interpolate(t.xs[lowerOrEqualIndex], t.xs[firstHigherIndex], t.ys[lowerOrEqualIndex], t.ys[firstHigherIndex], x); 258 } 259 } 260 }, 261 continuousInterpolator(interpolation, extrapolation) { 262 if (interpolation === "Linear" && extrapolation === "UseZero") { 263 return (t, leftIndex, x) => { 264 if (leftIndex < 0) { 265 return 0; 266 } 267 else if (leftIndex >= T.length(t) - 1) { 268 return 0; 269 } 270 else { 271 const x1 = t.xs[leftIndex]; 272 const x2 = t.xs[leftIndex + 1]; 273 const y1 = t.ys[leftIndex]; 274 const y2 = t.ys[leftIndex + 1]; 275 const fraction = (x - x1) / (x2 - x1); 276 return y1 * (1 - fraction) + y2 * fraction; 277 } 278 }; 279 } 280 else if (interpolation === "Linear" && 281 extrapolation === "UseOutermostPoints") { 282 return (t, leftIndex, x) => { 283 if (leftIndex < 0) { 284 return t.ys[0]; 285 } 286 else if (leftIndex >= T.length(t) - 1) { 287 return t.ys[T.length(t) - 1]; 288 } 289 else { 290 const x1 = t.xs[leftIndex]; 291 const x2 = t.xs[leftIndex + 1]; 292 const y1 = t.ys[leftIndex]; 293 const y2 = t.ys[leftIndex + 1]; 294 const fraction = (x - x1) / (x2 - x1); 295 return y1 * (1 - fraction) + y2 * fraction; 296 } 297 }; 298 } 299 else if (interpolation === "Stepwise" && extrapolation === "UseZero") { 300 return (t, leftIndex, _x) => { 301 if (leftIndex < 0) { 302 return 0; 303 } 304 else if (leftIndex >= T.length(t) - 1) { 305 return 0; 306 } 307 else { 308 return t.ys[leftIndex]; 309 } 310 }; 311 } 312 else if (interpolation === "Stepwise" && 313 extrapolation === "UseOutermostPoints") { 314 return (t, leftIndex, _x) => { 315 if (leftIndex < 0) { 316 return t.ys[0]; 317 } 318 else if (leftIndex >= T.length(t) - 1) { 319 return t.ys[T.length(t) - 1]; 320 } 321 else { 322 return t.ys[leftIndex]; 323 } 324 }; 325 } 326 else { 327 throw new Error("Implementation error: invalid interpolation/extrapolation strategy combination"); 328 } 329 }, 330 discreteInterpolator: (() => 0), 331 }; 332 export const XsConversion = { 333 _replaceWithXs(newXs, t) { 334 const newYs = newXs.map((x) => XtoY.linear(t, x)); 335 return { xs: newXs, ys: newYs }; 336 }, 337 equallyDivideXByMass(integral, newLength) { 338 return E_A_Floats.range(0, 1, newLength).map((y) => YtoX.linear(integral, y)); 339 }, 340 proportionEquallyOverX(t, newLength) { 341 return XsConversion._replaceWithXs(T.equallyDividedXs(t, newLength), t); 342 }, 343 proportionByProbabilityMass(t, newLength, integral) { 344 return XsConversion._replaceWithXs(XsConversion.equallyDivideXByMass(integral, newLength), t); 345 }, 346 }; 347 export const Zipped = { 348 sortByY(t) { 349 return sortBy(t, [([x, y]) => y]); 350 }, 351 sortByX(t) { 352 return sortBy(t, [([x, y]) => x]); 353 }, 354 filterByX(t, testFn) { 355 return t.filter(([x]) => testFn(x)); 356 }, 357 }; 358 export const PointwiseCombination = { 359 combine(interpolator, fn, t1, t2) { 360 const t1n = t1.xs.length; 361 const t2n = t2.xs.length; 362 const outX = []; 363 const outY = []; 364 let i1 = 0, i2 = 0; 365 while (i1 < t1n || i2 < t2n) { 366 let x, y1, y2; 367 if (i1 > 0 && i1 === t1n && i2 === 0) { 368 x = t1.xs[i1 - 1] + Number.EPSILON * t1.xs[i1 - 1]; 369 y1 = interpolator(t1, t1n, x); 370 y2 = interpolator(t2, -1, x); 371 i1++; 372 } 373 else if (i1 === t1n + 1 && i2 === 0) { 374 x = t2.xs[0] - Number.EPSILON * t2.xs[0]; 375 y1 = interpolator(t1, t1n, x); 376 y2 = interpolator(t2, -1, x); 377 i1++; 378 } 379 else if (i2 > 0 && i2 === t2n && i1 === 0) { 380 x = t2.xs[i2 - 1] + Number.EPSILON * t2.xs[i2 - 1]; 381 y1 = interpolator(t1, -1, x); 382 y2 = interpolator(t2, t2n, x); 383 i2++; 384 } 385 else if (i2 === t2n + 1 && i1 === 0) { 386 x = t1.xs[0] - Number.EPSILON * t1.xs[0]; 387 y1 = interpolator(t1, -1, x); 388 y2 = interpolator(t2, t2n, x); 389 i2++; 390 } 391 else if (i2 >= t2n || (i1 < t1n && t1.xs[i1] < t2.xs[i2])) { 392 x = t1.xs[i1]; 393 y1 = t1.ys[i1]; 394 y2 = interpolator(t2, i2 - 1, x); 395 i1++; 396 } 397 else if (i1 >= t1n || (i2 < t2n && t1.xs[i1] > t2.xs[i2])) { 398 x = t2.xs[i2]; 399 y1 = interpolator(t1, i1 - 1, x); 400 y2 = t2.ys[i2]; 401 i2++; 402 } 403 else if (i1 < t1n && i2 < t2n && t1.xs[i1] === t2.xs[i2]) { 404 x = t1.xs[i1]; 405 y1 = t1.ys[i1]; 406 y2 = t2.ys[i2]; 407 i1++; 408 i2++; 409 } 410 else { 411 throw new Error(`PointwiseCombination error: ${i1}, ${i2}`); 412 } 413 outX.push(x); 414 const newY = fn(y1, y2); 415 if (!newY.ok) { 416 return newY; 417 } 418 outY.push(newY.value); 419 } 420 return Result.Ok({ xs: outX, ys: outY }); 421 }, 422 addCombine(interpolator, t1, t2) { 423 const result = PointwiseCombination.combine(interpolator, (a, b) => Result.Ok(a + b), t1, t2); 424 if (!result.ok) { 425 throw new Error("Add operation should never fail"); 426 } 427 return result.value; 428 }, 429 }; 430 export const Range = { 431 integrateWithTriangles({ xs, ys }) { 432 const length = xs.length; 433 const cumulativeY = new Array(length).fill(0); 434 for (let x = 0; x <= length - 2; x++) { 435 cumulativeY[x + 1] = 436 (xs[x + 1] - xs[x]) * ((ys[x] + ys[x + 1]) / 2) + cumulativeY[x]; 437 } 438 return { xs, ys: cumulativeY }; 439 }, 440 stepwiseToLinear({ xs, ys }) { 441 const length = xs.length; 442 const newXs = new Array(2 * length); 443 const newYs = new Array(2 * length); 444 newXs[0] = xs[0] - epsilon_float; 445 newYs[0] = 0; 446 newXs[1] = xs[0]; 447 newYs[1] = ys[0]; 448 for (let i = 1; i <= length - 1; i++) { 449 newXs[i * 2] = xs[i] - epsilon_float; 450 newYs[i * 2] = ys[i - 1]; 451 newXs[i * 2 + 1] = xs[i]; 452 newYs[i * 2 + 1] = ys[i]; 453 } 454 return { xs: newXs, ys: newYs }; 455 }, 456 }; 457 export const Analysis = { 458 getVarianceDangerously(t, mean, getMeanOfSquares) { 459 const meanSquared = mean(t) ** 2; 460 const meanOfSquares = getMeanOfSquares(t); 461 return meanOfSquares - meanSquared; 462 }, 463 }; 464 //# sourceMappingURL=XYShape.js.map