gonum / matrix

Matrix packages for the Go language [DEPRECATED]

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

matrix/mat64: Ability to get SymDense from Dense

ChristopherRabotin opened this issue · comments

commented

I propose to add the following signature to dense.go, and am seeking feedback prior to implementation: func (m *Dense) AsSymDense() (*SymDense, error) . I'm not sure if it's preferable to return a Symmetric instead, i.e. the interface instead of a pointer to the concrete implementation. If so, I think the signature should also be changed to AsSymmetric.

This function would return nil and an error if the matrix is not symmetric, or a pointer to a SymDense with no error (nil).

The algorithm would be trivial:

  1. if the matrix isn't square, return an error
  2. if m.T().At(i,j) != m.At(i,j) for (i,j) in m.Dims(), return an error
  3. return NewSymDense(...)

Thanks for your input! (And many thanks for this library.)

P.S.: I've identified a need for this in a Kalman filter library that I'm writing where I'm doing matrix multiplications and the result of a given multiplication always returns a symmetric matrix. I push this result on a chan (mat64.Symmetric).

I'm not sure I like this. Here is my reasoning, feel free to disagree

  1. I agree with your use case, that there are certain operations that mathematically yield a symmetric matrix, and that should be able to be guaranteed. For example, A * A^T is symmetric, and so we provide SymDense.OuterK. If there are other versions that are sufficiently common, we could add those operations instead. We've been thinking about Schur compliment for example.

  2. As you suggest, it would seem we have to do strict equality (modified in NaN). I think this would be really hard to do in practice. Parallel computation can make it be that A^T * A is not strictly symmetric. It should be "almost" symmetric, but I still imagine that there would be a lot of errors from floating point noise. We could add a tolerance, but it could get tricky and difficult to use.

  3. There is a workaround to 2, assuming you know the matrix is symmetric. You can use RawMatrix. Normally this is discouraged, but if there's a special circumstance that can't be covered by 1, maybe that's the best option.

commented
commented

Here is the implementation which I am currently using in gokalman.

// AsSymDense attempts return a SymDense from the provided Dense.
func AsSymDense(m *mat64.Dense) (*mat64.SymDense, error) {
	r, c := m.Dims()
	if r != c {
		return nil, errors.New("matrix must be square")
	}
	mT := m.T()
	vals := make([]float64, r*c)
	idx := 0
	for i := 0; i < r; i++ {
		for j := 0; j < c; j++ {
			if mT.At(i, j) != m.At(i, j) {
				return nil, errors.New("matrix is not symmetric")
			}
			vals[idx] = m.At(i, j)
			idx++
		}
	}

	return mat64.NewSymDense(r, vals), nil
}

And its associated test:

func TestAsSymDense(t *testing.T) {
	d := mat64.NewDense(3, 3, []float64{1, 0, 0, 0, 1, 0, 0, 0, 1})
	dsym, err := AsSymDense(d)
	if err != nil {
		t.Fatal("AsSymDense failed on i33")
	}
	r, c := d.Dims()
	for i := 0; i < r; i++ {
		for j := 0; j < c; j++ {
			if dsym.At(i, j) != d.At(i, j) {
				t.Fatalf("returned symmetric matrix invalid: %+v %+v", dsym, d)
			}
		}
	}
	_, err = AsSymDense(mat64.NewDense(3, 3, []float64{1, 0, 3, 0, 1, 0, 1, 2, 1}))
	if err == nil {
		t.Fatal("non symmetric matrix did not fail")
	}

	_, err = AsSymDense(mat64.NewDense(2, 3, []float64{1, 0, 1, 1, 2, 3}))
	if err == nil {
		t.Fatal("non square matrix did not fail")
	}
}

Edited to fixed the size bug and update the test.

No horse in this fight, but vals := make([]float64, r+c) seems wrong for 4x4 and larger matrices.

Also, there are alternative formulations of the algorithm which will always result in symmetric P matrices. I don't have Dan Simon's book Optimal State Estimation available until later but there are a few formulations there, a google search indicates that Introduction to Random Signals and Applied Kalman Filtering with Matlab Exercises has a formulation with guaranteed symmetry as well.

Personally I would not create a library with channels and would instead implement it synchronously, and if someone desired they could create an asynchronous implementation from its components. Usually having a chan type in an exposed interface is an antipattern.

Also your Q and R matrices should be symmetric.

commented

@jonlawlor I'll look again at this implementation of the AsSymDense.

Thanks for your input concerning the gokalman library. You're right concerning the Q and R matrices. I'm actually using Introduction to Random Signals and Applied Kalman Filtering with Matlab Exercises by Brown and Hwang as part of my Intro to KF filter graduate class. I'll be applying this statistical orbit determination as a final project for this class. So, concerning the antipattern of having a chan type, how would you recommend I handle time-step KF filtering (not batch filtering) where the measurement comes in asynchronously?

You could block on measurement:

kf := gokalman.NewVanilla( ... stuff ...)
for {
    m, err := performMeasurement()
    ... handle err, including no more measurement? ...
    err = kf.Update(m)
    ... handle err ...
    s := kf.State()
    do_something_with(s)
}

You could receive from a measurement channel:

n := some positive int
kf := gokalman.NewVanilla( ... stuff ...)
meas := make(chan *mat64.Dense, n)
for m := range meas {
    err := kf.Update(m)
    ... handle err ...
    s := kf.State()
    do_something_with(s)
}

You could also do things like have multiple measurement channels that operate on different frequencies and a method to either choose between them or to combine them, you could have a timer so that the state is updated on a regular interval (and get more and more uncertain in the absence of new data...). You could have one kalman filter to estimate the current state, and then a different one which goes backwards and smooths the history. It would all depend on application.

commented

Thanks for your input. I'll most likely choose one of those solutions and try to organize the library to be more idiomatic.

commented

@jonlawlor sorry to bother, just wanted to know whether the following looked more idiomatic: ChristopherRabotin/gokalman@c8a0c54#diff-55c93d25bf851b298d1e5606e8743d49R6 . (Note the code is very far from complete in terms of KF implementation).

Thanks

That looks a lot better. I haven't looked through the math.

One thing I would change is to make the Vanilla Update method return a VanillaEstimate struct instead of an Estimate interface. The usual guideline is "accept interfaces, return structures." In this case there is no mat64.vector interface, so that part can be ignored, but you could return a struct that implements the Estimate interface.

Also you could change the Vanilla Q and R fields to be mat64.Symmetric, and some comments need updating. Otherwise looks good from a high level.

@ChristopherRabotin there are a lot of steps, so it's hard to tell, but I think you can have your matrix be symmetric using functions that are already available (or should be available). For example, if kf.F, and kf.prevEst were defined as symmetric, I think you can have FPFt be symmetric, and so with similar transformations Pkp1Minus also.