gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/diff/fd/jacobian.go (about)

     1  // Copyright ©2016 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 fd
     6  
     7  import (
     8  	"sync"
     9  
    10  	"gonum.org/v1/gonum/floats"
    11  	"gonum.org/v1/gonum/mat"
    12  )
    13  
    14  type JacobianSettings struct {
    15  	Formula     Formula
    16  	OriginValue []float64
    17  	Step        float64
    18  	Concurrent  bool
    19  }
    20  
    21  // Jacobian approximates the Jacobian matrix of a vector-valued function f at
    22  // the location x and stores the result in-place into dst.
    23  //
    24  // Finite difference formula and other options are specified by settings. If
    25  // settings is nil, the Jacobian will be estimated using the Forward formula and
    26  // a default step size.
    27  //
    28  // The Jacobian matrix J is the matrix of all first-order partial derivatives of f.
    29  // If f maps an n-dimensional vector x to an m-dimensional vector y = f(x), J is
    30  // an m×n matrix whose elements are given as
    31  //
    32  //	J_{i,j} = ∂f_i/∂x_j,
    33  //
    34  // or expanded out
    35  //
    36  //	    [ ∂f_1/∂x_1 ... ∂f_1/∂x_n ]
    37  //	    [     .  .          .     ]
    38  //	J = [     .      .      .     ]
    39  //	    [     .          .  .     ]
    40  //	    [ ∂f_m/∂x_1 ... ∂f_m/∂x_n ]
    41  //
    42  // dst must be non-nil, the number of its columns must equal the length of x, and
    43  // the derivative order of the formula must be 1, otherwise Jacobian will panic.
    44  func Jacobian(dst *mat.Dense, f func(y, x []float64), x []float64, settings *JacobianSettings) {
    45  	n := len(x)
    46  	if n == 0 {
    47  		panic("jacobian: x has zero length")
    48  	}
    49  	m, c := dst.Dims()
    50  	if c != n {
    51  		panic("jacobian: mismatched matrix size")
    52  	}
    53  
    54  	// Default settings.
    55  	formula := Forward
    56  	step := formula.Step
    57  	var originValue []float64
    58  	var concurrent bool
    59  
    60  	// Use user settings if provided.
    61  	if settings != nil {
    62  		if !settings.Formula.isZero() {
    63  			formula = settings.Formula
    64  			step = formula.Step
    65  			checkFormula(formula)
    66  			if formula.Derivative != 1 {
    67  				panic(badDerivOrder)
    68  			}
    69  		}
    70  		if settings.Step != 0 {
    71  			step = settings.Step
    72  		}
    73  		originValue = settings.OriginValue
    74  		if originValue != nil && len(originValue) != m {
    75  			panic("jacobian: mismatched OriginValue slice length")
    76  		}
    77  		concurrent = settings.Concurrent
    78  	}
    79  
    80  	evals := n * len(formula.Stencil)
    81  	for _, pt := range formula.Stencil {
    82  		if pt.Loc == 0 {
    83  			evals -= n - 1
    84  			break
    85  		}
    86  	}
    87  
    88  	nWorkers := computeWorkers(concurrent, evals)
    89  	if nWorkers == 1 {
    90  		jacobianSerial(dst, f, x, originValue, formula, step)
    91  		return
    92  	}
    93  	jacobianConcurrent(dst, f, x, originValue, formula, step, nWorkers)
    94  }
    95  
    96  func jacobianSerial(dst *mat.Dense, f func([]float64, []float64), x, origin []float64, formula Formula, step float64) {
    97  	m, n := dst.Dims()
    98  	xcopy := make([]float64, n)
    99  	y := make([]float64, m)
   100  	col := make([]float64, m)
   101  	for j := 0; j < n; j++ {
   102  		for i := range col {
   103  			col[i] = 0
   104  		}
   105  		for _, pt := range formula.Stencil {
   106  			if pt.Loc == 0 {
   107  				if origin == nil {
   108  					origin = make([]float64, m)
   109  					copy(xcopy, x)
   110  					f(origin, xcopy)
   111  				}
   112  				floats.AddScaled(col, pt.Coeff, origin)
   113  			} else {
   114  				copy(xcopy, x)
   115  				xcopy[j] += pt.Loc * step
   116  				f(y, xcopy)
   117  				floats.AddScaled(col, pt.Coeff, y)
   118  			}
   119  		}
   120  		dst.SetCol(j, col)
   121  	}
   122  	dst.Scale(1/step, dst)
   123  }
   124  
   125  func jacobianConcurrent(dst *mat.Dense, f func([]float64, []float64), x, origin []float64, formula Formula, step float64, nWorkers int) {
   126  	m, n := dst.Dims()
   127  	for i := 0; i < m; i++ {
   128  		for j := 0; j < n; j++ {
   129  			dst.Set(i, j, 0)
   130  		}
   131  	}
   132  
   133  	var (
   134  		wg sync.WaitGroup
   135  		mu = make([]sync.Mutex, n) // Guard access to individual columns.
   136  	)
   137  	worker := func(jobs <-chan jacJob) {
   138  		defer wg.Done()
   139  		xcopy := make([]float64, n)
   140  		y := make([]float64, m)
   141  		yVec := mat.NewVecDense(m, y)
   142  		var col mat.VecDense
   143  		for job := range jobs {
   144  			copy(xcopy, x)
   145  			xcopy[job.j] += job.pt.Loc * step
   146  			f(y, xcopy)
   147  			col.ColViewOf(dst, job.j)
   148  			mu[job.j].Lock()
   149  			col.AddScaledVec(&col, job.pt.Coeff, yVec)
   150  			mu[job.j].Unlock()
   151  		}
   152  	}
   153  	jobs := make(chan jacJob, nWorkers)
   154  	for i := 0; i < nWorkers; i++ {
   155  		wg.Add(1)
   156  		go worker(jobs)
   157  	}
   158  	var hasOrigin bool
   159  	for _, pt := range formula.Stencil {
   160  		if pt.Loc == 0 {
   161  			hasOrigin = true
   162  			continue
   163  		}
   164  		for j := 0; j < n; j++ {
   165  			jobs <- jacJob{j, pt}
   166  		}
   167  	}
   168  	close(jobs)
   169  	if hasOrigin && origin == nil {
   170  		wg.Add(1)
   171  		go func() {
   172  			defer wg.Done()
   173  			origin = make([]float64, m)
   174  			xcopy := make([]float64, n)
   175  			copy(xcopy, x)
   176  			f(origin, xcopy)
   177  		}()
   178  	}
   179  	wg.Wait()
   180  
   181  	if hasOrigin {
   182  		// The formula evaluated at x, we need to add scaled origin to
   183  		// all columns of dst. Iterate again over all Formula points
   184  		// because we don't forbid repeated locations.
   185  
   186  		originVec := mat.NewVecDense(m, origin)
   187  		for _, pt := range formula.Stencil {
   188  			if pt.Loc != 0 {
   189  				continue
   190  			}
   191  			var col mat.VecDense
   192  			for j := 0; j < n; j++ {
   193  				col.ColViewOf(dst, j)
   194  				col.AddScaledVec(&col, pt.Coeff, originVec)
   195  			}
   196  		}
   197  	}
   198  
   199  	dst.Scale(1/step, dst)
   200  }
   201  
   202  type jacJob struct {
   203  	j  int
   204  	pt Point
   205  }