github.com/jingcheng-WU/gonum@v0.9.1-0.20210323123734-f1a2a11a8f7b/interp/cubic_test.go (about) 1 // Copyright ©2020 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package interp 6 7 import ( 8 "math" 9 "testing" 10 11 "github.com/jingcheng-WU/gonum/floats" 12 "github.com/jingcheng-WU/gonum/floats/scalar" 13 "github.com/jingcheng-WU/gonum/mat" 14 ) 15 16 func TestPiecewiseCubic(t *testing.T) { 17 t.Parallel() 18 const ( 19 h = 1e-8 20 valueTol = 1e-13 21 derivTol = 1e-6 22 nPts = 100 23 ) 24 for i, test := range []struct { 25 xs []float64 26 f func(float64) float64 27 df func(float64) float64 28 }{ 29 { 30 xs: []float64{-1.001, 0.2, 2}, 31 f: func(x float64) float64 { return x * x }, 32 df: func(x float64) float64 { return 2 * x }, 33 }, 34 { 35 xs: []float64{-1.2, -1.001, 0, 0.2, 2.01, 2.1}, 36 f: func(x float64) float64 { return 4*math.Pow(x, 3) - 2*x*x + 10*x - 7 }, 37 df: func(x float64) float64 { return 12*x*x - 4*x + 10 }, 38 }, 39 { 40 xs: []float64{-1.001, 0.2, 10}, 41 f: func(x float64) float64 { return 1.5*x - 1 }, 42 df: func(x float64) float64 { return 1.5 }, 43 }, 44 { 45 xs: []float64{-1.001, 0.2, 10}, 46 f: func(x float64) float64 { return -1 }, 47 df: func(x float64) float64 { return 0 }, 48 }, 49 } { 50 ys := applyFunc(test.xs, test.f) 51 dydxs := applyFunc(test.xs, test.df) 52 var pc PiecewiseCubic 53 pc.FitWithDerivatives(test.xs, ys, dydxs) 54 n := len(test.xs) 55 m := n - 1 56 x0 := test.xs[0] 57 x1 := test.xs[m] 58 x := x0 - 0.1 59 got := pc.Predict(x) 60 want := ys[0] 61 if got != want { 62 t.Errorf("Mismatch in value extrapolated to the left for test case %d: got %v, want %g", i, got, want) 63 } 64 got = pc.PredictDerivative(x) 65 want = dydxs[0] 66 if got != want { 67 t.Errorf("Mismatch in derivative extrapolated to the left for test case %d: got %v, want %g", i, got, want) 68 } 69 x = x1 + 0.1 70 got = pc.Predict(x) 71 want = ys[m] 72 if got != want { 73 t.Errorf("Mismatch in value extrapolated to the right for test case %d: got %v, want %g", i, got, want) 74 } 75 got = pc.PredictDerivative(x) 76 want = dydxs[m] 77 if got != want { 78 t.Errorf("Mismatch in derivative extrapolated to the right for test case %d: got %v, want %g", i, got, want) 79 } 80 for j := 0; j < n; j++ { 81 x := test.xs[j] 82 got := pc.Predict(x) 83 want := test.f(x) 84 if math.Abs(got-want) > valueTol { 85 t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want) 86 } 87 if j < m { 88 got = pc.coeffs.At(j, 0) 89 if math.Abs(got-want) > valueTol { 90 t.Errorf("Mismatch in 0-th order interpolation coefficient in %d-th node for test case %d: got %v, want %g", j, i, got, want) 91 } 92 dx := (test.xs[j+1] - x) / nPts 93 for k := 1; k < nPts; k++ { 94 xk := x + float64(k)*dx 95 got := pc.Predict(xk) 96 want := test.f(xk) 97 if math.Abs(got-want) > valueTol { 98 t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want) 99 } 100 got = pc.PredictDerivative(xk) 101 want = discrDerivPredict(&pc, x0, x1, xk, h) 102 if math.Abs(got-want) > derivTol { 103 t.Errorf("Mismatch in interpolated derivative at x == %g for test case %d: got %v, want %g", x, i, got, want) 104 } 105 } 106 } else { 107 got = pc.lastY 108 if math.Abs(got-want) > valueTol { 109 t.Errorf("Mismatch in lastY for test case %d: got %v, want %g", i, got, want) 110 } 111 } 112 113 if j > 0 { 114 dx := test.xs[j] - test.xs[j-1] 115 got = ((pc.coeffs.At(j-1, 3)*dx+pc.coeffs.At(j-1, 2))*dx+pc.coeffs.At(j-1, 1))*dx + pc.coeffs.At(j-1, 0) 116 if math.Abs(got-want) > valueTol { 117 t.Errorf("Interpolation coefficients in %d-th node produce mismatch in interpolated value at %g for test case %d: got %v, want %g", j-1, x, i, got, want) 118 } 119 } 120 got = discrDerivPredict(&pc, x0, x1, x, h) 121 want = test.df(x) 122 if math.Abs(got-want) > derivTol { 123 t.Errorf("Mismatch in numerical derivative of interpolated function at x == %g for test case %d: got %v, want %g", x, i, got, want) 124 } 125 got = pc.PredictDerivative(x) 126 if math.Abs(got-want) > valueTol { 127 t.Errorf("Mismatch in interpolated derivative value at x == %g for test case %d: got %v, want %g", x, i, got, want) 128 } 129 } 130 } 131 } 132 133 func TestPiecewiseCubicFitWithDerivatives(t *testing.T) { 134 t.Parallel() 135 xs := []float64{-1, 0, 1} 136 ys := make([]float64, 3) 137 dydxs := make([]float64, 3) 138 leftPoly := func(x float64) float64 { 139 return x*x - x + 1 140 } 141 leftPolyDerivative := func(x float64) float64 { 142 return 2*x - 1 143 } 144 rightPoly := func(x float64) float64 { 145 return x*x*x - x + 1 146 } 147 rightPolyDerivative := func(x float64) float64 { 148 return 3*x*x - 1 149 } 150 ys[0] = leftPoly(xs[0]) 151 ys[1] = leftPoly(xs[1]) 152 ys[2] = rightPoly(xs[2]) 153 dydxs[0] = leftPolyDerivative(xs[0]) 154 dydxs[1] = leftPolyDerivative(xs[1]) 155 dydxs[2] = rightPolyDerivative(xs[2]) 156 var pc PiecewiseCubic 157 pc.FitWithDerivatives(xs, ys, dydxs) 158 lastY := rightPoly(xs[2]) 159 if pc.lastY != lastY { 160 t.Errorf("Mismatch in lastY: got %v, want %g", pc.lastY, lastY) 161 } 162 lastDyDx := rightPolyDerivative(xs[2]) 163 if pc.lastDyDx != lastDyDx { 164 t.Errorf("Mismatch in lastDxDy: got %v, want %g", pc.lastDyDx, lastDyDx) 165 } 166 if !floats.Equal(pc.xs, xs) { 167 t.Errorf("Mismatch in xs: got %v, want %v", pc.xs, xs) 168 } 169 coeffs := mat.NewDense(2, 4, []float64{3, -3, 1, 0, 1, -1, 0, 1}) 170 if !mat.EqualApprox(&pc.coeffs, coeffs, 1e-14) { 171 t.Errorf("Mismatch in coeffs: got %v, want %v", pc.coeffs, coeffs) 172 } 173 } 174 175 func TestPiecewiseCubicFitWithDerivativesErrors(t *testing.T) { 176 t.Parallel() 177 for _, test := range []struct { 178 xs, ys, dydxs []float64 179 }{ 180 { 181 xs: []float64{0, 1, 2}, 182 ys: []float64{10, 20}, 183 dydxs: []float64{0, 0, 0}, 184 }, 185 { 186 xs: []float64{0, 1, 1}, 187 ys: []float64{10, 20, 30}, 188 dydxs: []float64{0, 0, 0, 0}, 189 }, 190 { 191 xs: []float64{0}, 192 ys: []float64{0}, 193 dydxs: []float64{0}, 194 }, 195 { 196 xs: []float64{0, 1, 1}, 197 ys: []float64{10, 20, 10}, 198 dydxs: []float64{0, 0, 0}, 199 }, 200 } { 201 var pc PiecewiseCubic 202 if !panics(func() { pc.FitWithDerivatives(test.xs, test.ys, test.dydxs) }) { 203 t.Errorf("expected panic for xs: %v, ys: %v and dydxs: %v", test.xs, test.ys, test.dydxs) 204 } 205 } 206 } 207 208 func TestAkimaSpline(t *testing.T) { 209 t.Parallel() 210 const ( 211 derivAbsTol = 1e-8 212 derivRelTol = 1e-7 213 h = 1e-8 214 nPts = 100 215 tol = 1e-14 216 ) 217 for i, test := range []struct { 218 xs []float64 219 f func(float64) float64 220 }{ 221 { 222 xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3}, 223 f: func(x float64) float64 { return x * x }, 224 }, 225 { 226 xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3}, 227 f: func(x float64) float64 { return math.Pow(x, 3.) - x*x + 2 }, 228 }, 229 { 230 xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3}, 231 f: func(x float64) float64 { return -10 * x }, 232 }, 233 { 234 xs: []float64{-5, -3, -2, -1.5, -1, 0.5, 1.5, 2.5, 3}, 235 f: math.Sin, 236 }, 237 { 238 xs: []float64{0, 1}, 239 f: math.Exp, 240 }, 241 { 242 xs: []float64{-1, 0.5}, 243 f: math.Cos, 244 }, 245 } { 246 var as AkimaSpline 247 n := len(test.xs) 248 m := n - 1 249 x0 := test.xs[0] 250 x1 := test.xs[m] 251 ys := applyFunc(test.xs, test.f) 252 err := as.Fit(test.xs, ys) 253 if err != nil { 254 t.Errorf("Error when fitting AkimaSpline in test case %d: %v", i, err) 255 } 256 for j := 0; j < n; j++ { 257 x := test.xs[j] 258 got := as.Predict(x) 259 want := test.f(x) 260 if math.Abs(got-want) > tol { 261 t.Errorf("Mismatch in interpolated value at x == %g for test case %d: got %v, want %g", x, i, got, want) 262 } 263 if j < m { 264 dx := (test.xs[j+1] - x) / nPts 265 for k := 1; k < nPts; k++ { 266 xk := x + float64(k)*dx 267 got = as.PredictDerivative(xk) 268 want = discrDerivPredict(&as, x0, x1, xk, h) 269 if math.Abs(got-want) > derivRelTol*math.Abs(want)+derivAbsTol { 270 t.Errorf("Mismatch in interpolated derivative at x == %g for test case %d: got %v, want %g", x, i, got, want) 271 } 272 } 273 } 274 } 275 if n == 2 { 276 got := as.cubic.coeffs.At(0, 1) 277 want := (ys[1] - ys[0]) / (test.xs[1] - test.xs[0]) 278 if math.Abs(got-want) > tol { 279 t.Errorf("Mismatch in approximated slope for length-2 test case %d: got %v, want %g", i, got, want) 280 } 281 for j := 2; i < 4; j++ { 282 got := as.cubic.coeffs.At(0, j) 283 if got != 0 { 284 t.Errorf("Non-zero order-%d coefficient for length-2 test case %d: got %v", j, i, got) 285 } 286 } 287 } 288 } 289 } 290 291 func TestAkimaSplineFitErrors(t *testing.T) { 292 t.Parallel() 293 for _, test := range []struct { 294 xs, ys []float64 295 }{ 296 { 297 xs: []float64{0, 1, 2}, 298 ys: []float64{10, 20}, 299 }, 300 { 301 xs: []float64{0, 1}, 302 ys: []float64{10, 20, 30}, 303 }, 304 { 305 xs: []float64{0}, 306 ys: []float64{0}, 307 }, 308 { 309 xs: []float64{0, 1, 1}, 310 ys: []float64{10, 20, 10}, 311 }, 312 { 313 xs: []float64{0, 2, 1}, 314 ys: []float64{10, 20, 10}, 315 }, 316 { 317 xs: []float64{0, 0}, 318 ys: []float64{-1, 2}, 319 }, 320 { 321 xs: []float64{0, -1}, 322 ys: []float64{-1, 2}, 323 }, 324 } { 325 var as AkimaSpline 326 if !panics(func() { _ = as.Fit(test.xs, test.ys) }) { 327 t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys) 328 } 329 } 330 } 331 332 func TestAkimaWeightedAverage(t *testing.T) { 333 t.Parallel() 334 for i, test := range []struct { 335 v1, v2, w1, w2, want float64 336 // "want" values calculated by hand. 337 }{ 338 { 339 v1: -1, 340 v2: 1, 341 w1: 0, 342 w2: 0, 343 want: 0, 344 }, 345 { 346 v1: -1, 347 v2: 1, 348 w1: 1e6, 349 w2: 1e6, 350 want: 0, 351 }, 352 { 353 v1: -1, 354 v2: 1, 355 w1: 1e-10, 356 w2: 0, 357 want: -1, 358 }, 359 { 360 v1: -1, 361 v2: 1, 362 w1: 0, 363 w2: 1e-10, 364 want: 1, 365 }, 366 { 367 v1: 0, 368 v2: 1000, 369 w1: 1e-13, 370 w2: 3e-13, 371 want: 750, 372 }, 373 { 374 v1: 0, 375 v2: 1000, 376 w1: 3e-13, 377 w2: 1e-13, 378 want: 250, 379 }, 380 } { 381 got := akimaWeightedAverage(test.v1, test.v2, test.w1, test.w2) 382 if !scalar.EqualWithinAbsOrRel(got, test.want, 1e-14, 1e-14) { 383 t.Errorf("Mismatch in test case %d: got %v, want %g", i, got, test.want) 384 } 385 } 386 } 387 388 func TestAkimaSlopes(t *testing.T) { 389 t.Parallel() 390 for i, test := range []struct { 391 xs, ys, want []float64 392 // "want" values calculated by hand. 393 }{ 394 { 395 xs: []float64{-2, 0, 1}, 396 ys: []float64{2, 0, 1.5}, 397 want: []float64{-6, -3.5, -1, 1.5, 4, 6.5}, 398 }, 399 { 400 xs: []float64{-2, -0.5, 1}, 401 ys: []float64{-2, -0.5, 1}, 402 want: []float64{1, 1, 1, 1, 1, 1}, 403 }, 404 { 405 xs: []float64{-2, -0.5, 1}, 406 ys: []float64{1, 1, 1}, 407 want: []float64{0, 0, 0, 0, 0, 0}, 408 }, 409 { 410 xs: []float64{0, 1.5, 2, 4, 4.5, 5, 6, 7.5, 8}, 411 ys: []float64{-5, -4, -3.5, -3.25, -3.25, -2.5, -1.5, -1, 2}, 412 want: []float64{0, 1. / 3, 2. / 3, 1, 0.125, 0, 1.5, 1, 1. / 3, 6, 12 - 1./3, 18 - 2./3}, 413 }, 414 } { 415 got := akimaSlopes(test.xs, test.ys) 416 if !floats.EqualApprox(got, test.want, 1e-14) { 417 t.Errorf("Mismatch in test case %d: got %v, want %v", i, got, test.want) 418 } 419 } 420 } 421 422 func TestAkimaSlopesErrors(t *testing.T) { 423 t.Parallel() 424 for _, test := range []struct { 425 xs, ys []float64 426 }{ 427 { 428 xs: []float64{0, 1, 2}, 429 ys: []float64{10, 20}, 430 }, 431 { 432 xs: []float64{0, 1}, 433 ys: []float64{10, 20, 30}, 434 }, 435 { 436 xs: []float64{0, 2}, 437 ys: []float64{0, 1}, 438 }, 439 { 440 xs: []float64{0, 1, 1}, 441 ys: []float64{10, 20, 10}, 442 }, 443 { 444 xs: []float64{0, 2, 1}, 445 ys: []float64{10, 20, 10}, 446 }, 447 { 448 xs: []float64{0, 0}, 449 ys: []float64{-1, 2}, 450 }, 451 { 452 xs: []float64{0, -1}, 453 ys: []float64{-1, 2}, 454 }, 455 } { 456 if !panics(func() { akimaSlopes(test.xs, test.ys) }) { 457 t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys) 458 } 459 } 460 } 461 462 func TestAkimaWeights(t *testing.T) { 463 t.Parallel() 464 const tol = 1e-14 465 slopes := []float64{-2, -1, -0.1, 0.2, 1.2, 2.5} 466 // "want" values calculated by hand. 467 want := [][]float64{ 468 {0.3, 1}, 469 {1, 0.9}, 470 {1.3, 0.3}, 471 } 472 for i := 0; i < len(want); i++ { 473 gotLeft, gotRight := akimaWeights(slopes, i) 474 if math.Abs(gotLeft-want[i][0]) > tol { 475 t.Errorf("Mismatch in left weight for node %d: got %v, want %g", i, gotLeft, want[i][0]) 476 } 477 if math.Abs(gotRight-want[i][1]) > tol { 478 t.Errorf("Mismatch in left weight for node %d: got %v, want %g", i, gotRight, want[i][1]) 479 } 480 } 481 } 482 483 func TestFritschButland(t *testing.T) { 484 t.Parallel() 485 const ( 486 tol = 1e-14 487 nPts = 100 488 ) 489 for k, test := range []struct { 490 xs, ys []float64 491 }{ 492 { 493 xs: []float64{0, 2}, 494 ys: []float64{0, 0.5}, 495 }, 496 { 497 xs: []float64{0, 2}, 498 ys: []float64{0, -0.5}, 499 }, 500 { 501 xs: []float64{0, 2}, 502 ys: []float64{0, 0}, 503 }, 504 { 505 xs: []float64{0, 2, 3, 4}, 506 ys: []float64{0, 1, 2, 2.5}, 507 }, 508 { 509 xs: []float64{0, 2, 3, 4}, 510 ys: []float64{0, 1.5, 1.5, 2.5}, 511 }, 512 { 513 xs: []float64{0, 2, 3, 4}, 514 ys: []float64{0, 1.5, 1.5, 1}, 515 }, 516 { 517 xs: []float64{0, 2, 3, 4}, 518 ys: []float64{0, 2.5, 1.5, 1}, 519 }, 520 { 521 xs: []float64{0, 2, 3, 4}, 522 ys: []float64{0, 2.5, 1.5, 2}, 523 }, 524 { 525 xs: []float64{0, 2, 3, 4}, 526 ys: []float64{4, 3, 2, 1}, 527 }, 528 { 529 xs: []float64{0, 2, 3, 4}, 530 ys: []float64{4, 3, 2, 2}, 531 }, 532 { 533 xs: []float64{0, 2, 3, 4}, 534 ys: []float64{4, 3, 2, 5}, 535 }, 536 { 537 xs: []float64{0, 2, 3, 4, 5, 6}, 538 ys: []float64{0, 1, 0.5, 0.5, 1.5, 1.5}, 539 }, 540 { 541 xs: []float64{0, 2, 3, 4, 5, 6}, 542 ys: []float64{0, 1, 1.5, 2.5, 1.5, 1}, 543 }, 544 { 545 xs: []float64{0, 2, 3, 4, 5, 6}, 546 ys: []float64{0, -1, -1.5, -2.5, -1.5, -1}, 547 }, 548 { 549 xs: []float64{0, 2, 3, 4, 5, 6}, 550 ys: []float64{0, 1, 0.5, 1.5, 1, 2}, 551 }, 552 { 553 xs: []float64{0, 2, 3, 4, 5, 6}, 554 ys: []float64{0, 1, 1.5, 2.5, 3, 4}, 555 }, 556 { 557 xs: []float64{0, 2, 3, 4, 5, 6}, 558 ys: []float64{0, 0.0001, -1.5, -2.5, -0.0001, 0}, 559 }, 560 } { 561 var fb FritschButland 562 err := fb.Fit(test.xs, test.ys) 563 if err != nil { 564 t.Errorf("Error when fitting FritschButland in test case %d: %v", k, err) 565 } 566 n := len(test.xs) 567 for i := 0; i < n; i++ { 568 got := fb.Predict(test.xs[i]) 569 want := test.ys[i] 570 if got != want { 571 t.Errorf("Mismatch in interpolated value for node %d in test case %d: got %v, want %g", i, k, got, want) 572 } 573 } 574 if n == 2 { 575 h := test.xs[1] - test.xs[0] 576 want := (test.ys[1] - test.ys[0]) / h 577 for i := 0; i < 2; i++ { 578 got := fb.PredictDerivative(test.xs[i]) 579 if !scalar.EqualWithinAbs(got, want, tol) { 580 t.Errorf("Mismatch in approximated derivative for node %d in 2-node test case %d: got %v, want %g", i, k, got, want) 581 } 582 } 583 dx := h / (nPts + 1) 584 for i := 1; i < nPts; i++ { 585 x := test.xs[0] + float64(i)*dx 586 got := fb.PredictDerivative(x) 587 if !scalar.EqualWithinAbs(got, want, tol) { 588 t.Errorf("Mismatch in interpolated derivative for x == %g in 2-node test case %d: got %v, want %g", x, k, got, want) 589 } 590 } 591 } else { 592 m := n - 1 593 for i := 1; i < m; i++ { 594 got := fb.PredictDerivative(test.xs[i]) 595 slope := (test.ys[i+1] - test.ys[i]) / (test.xs[i+1] - test.xs[i]) 596 prevSlope := (test.ys[i] - test.ys[i-1]) / (test.xs[i] - test.xs[i-1]) 597 if slope*prevSlope > 0 { 598 if got == 0 { 599 t.Errorf("Approximated derivative is zero for node %d in test case %d: %g", i, k, got) 600 } else if math.Signbit(slope) != math.Signbit(got) { 601 t.Errorf("Approximated derivative has wrong sign for node %d in test case %d: got %g, want %g", i, k, math.Copysign(1, got), math.Copysign(1, slope)) 602 } 603 } else { 604 if got != 0 { 605 t.Errorf("Approximated derivative is not zero for node %d in test case %d: %g", i, k, got) 606 } 607 } 608 } 609 for i := 0; i < m; i++ { 610 yL := test.ys[i] 611 yR := test.ys[i+1] 612 xL := test.xs[i] 613 dx := (test.xs[i+1] - xL) / (nPts + 1) 614 if yL == yR { 615 for j := 1; j < nPts; j++ { 616 x := xL + float64(j)*dx 617 got := fb.Predict(x) 618 if got != yL { 619 t.Errorf("Mismatch in interpolated value for x == %g in test case %d: got %v, want %g", x, k, got, yL) 620 } 621 got = fb.PredictDerivative(x) 622 if got != 0 { 623 t.Errorf("Interpolated derivative not zero for x == %g in test case %d: got %v", x, k, got) 624 } 625 } 626 } else { 627 minY := math.Min(yL, yR) 628 maxY := math.Max(yL, yR) 629 for j := 1; j < nPts; j++ { 630 x := xL + float64(j)*dx 631 got := fb.Predict(x) 632 if got < minY || got > maxY { 633 t.Errorf("Interpolated value out of [%g, %g] bounds for x == %g in test case %d: got %v", minY, maxY, x, k, got) 634 } 635 got = fb.PredictDerivative(x) 636 dy := yR - yL 637 if got*dy < 0 { 638 t.Errorf("Interpolated derivative has wrong sign for x == %g in test case %d: want %g, got %g", x, k, math.Copysign(1, dy), math.Copysign(1, got)) 639 } 640 } 641 } 642 } 643 } 644 } 645 } 646 647 func TestFritschButlandErrors(t *testing.T) { 648 t.Parallel() 649 for _, test := range []struct { 650 xs, ys []float64 651 }{ 652 { 653 xs: []float64{0}, 654 ys: []float64{0}, 655 }, 656 { 657 xs: []float64{0, 1, 2}, 658 ys: []float64{0, 1}}, 659 { 660 xs: []float64{0, 0, 1}, 661 ys: []float64{0, 0, 0}, 662 }, 663 { 664 xs: []float64{0, 1, 0}, 665 ys: []float64{0, 0, 0}, 666 }, 667 } { 668 var fb FritschButland 669 if !panics(func() { _ = fb.Fit(test.xs, test.ys) }) { 670 t.Errorf("expected panic for xs: %v and ys: %v", test.xs, test.ys) 671 } 672 } 673 }