Rotation Matrices? Why not, might be better. Let’s spike. Warning, this is gonna get a tiny bit mathematical.

We’ll take some things as given, because they’re pretty much provided as given in most of the math courses on the subject, though I think I could have derived these facts back when my math chops were fresh.

In particular, When it comes to transformations, rotations in particular, a vector is represented vertically:

x
y

And a rotation matrix is like this:

cos A sin A
-sin A cos A

Suppose we have a matrix M

a b
c d

And a vector V:

x
y

M*v equals:

ax + by
cx + dy

So we should be able to implement that easily enough, beginning with a test:

Matrices

        _:test("Multiply matrix times vector", function()
            local m = Matrix({1, 2, 
                             3, 4})
            local v = vec2(1,10)
            local w = m*v
            _:expect(w.x).is(21)
            _:expect(w.y).is(43)
        end)

Test should fail looking for Matrix:

1: Multiply matrix times vector -- TestMatrices:16: attempt to call a nil value (global 'Matrix')

Let’s code a bit:

Matrix = class()

function Matrix:init(array)
    self.m = array
    assert(#array==4, "must be 4 elements")
end

Should fail on *:

1: Multiply matrix times vector -- TestMatrices:19: bad argument #1 to 'mul' (`vec2' expected)

We can implement multiply this way:

function Matrix:__mul(v)
    local a,b,c,d = table.unpack(self.m)
    local x = v.x*a + v.y*b
    local y = v.x*c + v.y*d
    return vec2(x,y)
end

I expect the test to run. It does. So that’s nice, I can multiply a vector times a matrix and get a vector back. If the matrix is a rotation matrix, I’ll get the rotated vector back. Let’s do matrix multiply as well, with a new test.

        _:test("multiply two matrices", function()
            local m1 = Matrix{1,2,
                              3,4}
            local m2 = Matrix{3,5,
                              7,11}
            local m = m1*m2
            a,b,c,d = table.unpack(m.m)
            _:expect(a).is(17)
            _:expect(b).is(27)
            _:expect(c).is(37)
            _:expect(d).is(59)
        end)

My raw implementation for that is this:

function Matrix:__mul(v)
    if type(v)=="table" then
        local a,b,c,d = table.unpack(self.m)
        local w,x,y,z = table.unpack(v.m)
        local r1 = a*w+b*y
        local r2 = a*x+b*z
        local r3 = c*w+d*y
        local r4 = c*x+d*z
        return Matrix{r1,r2,r3,r4}
    end
    local a,b,c,d = table.unpack(self.m)
    local x = v.x*a + v.y*b
    local y = v.x*c + v.y*d
    return vec2(x,y)
end

We can clean this up a bit:

function Matrix:__mul(v)
    if self:isMatrix(v) then
        local a,b,c,d = table.unpack(self.m)
        local w,x,y,z = table.unpack(v.m)
        local r1 = a*w+b*y
        local r2 = a*x+b*z
        local r3 = c*w+d*y
        local r4 = c*x+d*z
        return Matrix{r1,r2,r3,r4}
    else -- must be vector
        local a,b,c,d = table.unpack(self.m)
        local x = v.x*a + v.y*b
        local y = v.x*c + v.y*d
        return vec2(x,y)
    end
end

function Matrix:isMatrix(m)
    if type(m) ~= "table" then return false end
    if m.is_a and m:is_a(Matrix) then return true end
    return false
end

That’s a bit more clear and the check for matrix is stronger. It’s difficult but not impossible to check whether it’s a vec2. It would require checking the metatable of the userdata item that is a vec2. We’ll skip that.

Oh. It comes to me how I might have expressed things in today’s earlier article. If the robot turns right, that’s the same as rotating all the data left. That’s why I had to use different swapping than I had expected.

Let’s go ahead here in matrix and do some rotations.

We’ll need two rotation matrices, which I’ll call rot90 and rot270. And we’ll access them this way:

        _:test("Rotate vectors", function()
            local v = vec2(2,3)
            local tr = Matrix:turnRight()
            local w = tr*v
            _:expect(w.x).is(-3)
            _:expect(w.y).is(2)
        end)

And implement turnRight … (I wonder if I’ll want to say turn("right")) … we’ll see. For now:

3: Rotate vectors -- TestMatrices:39: attempt to call a nil value (method 'turnRight')

And …

function Matrix:turnRight()
    return Matrix{0,-1,1,0}
end
0 -1
1 0

Is the rotation matrix for 90 degrees, which is counterclockwise, which is just exactly the opposite of turning right. So the test should run. And it does.

Extend the test:

        _:test("Rotate vectors", function()
            local v = vec2(2,3)
            local tr = Matrix:turnRight()
            local w = tr*v
            _:expect(w.x).is(-3)
            _:expect(w.y).is(2)
            local tl = Matrix:turnLeft()
            w = tl*v
            _:expect(w.x).is(3)
            _:expect(w.y).is(-2)
        end)

And implement:

function Matrix:turnLeft()
    return Matrix{0,1,-1,0}
end

Tests all run. Houston, we have a matrix.

Let’s add one more matrix definition:

function Matrix:unit()
    return Matrix{1,0,0,1}
end

Now I think we can refactor our RotationLens to use Matrix. Let’s do.

Refactoring RotationLens

Is this a refactoring, or a rewrite? Well, it isn’t going to change behavior. But it is certainly replacing the algorithm. I don’t care what you call it. The tests will make sure that we don’t break anything.

RotatingLens works like this:

function RotatingLens:init(knowledge)
    self.knowledge = knowledge
    self.turns = 0
end

function RotatingLens:adjust(x,y)
    local xa,ya = x,y
    local turn = self.turns
    while turn > 0 do
        xa,ya = ya,-xa
        turn = turn - 1
    end
    return xa,ya
ens

function RotatingLens:turn(direction)
    if direction=="right" then
        self.turns = self.turns + 1
    elseif direction=="left" then
        self.turns = self.turns + 3
    end
    self.turns = self.turns%4
end

Let’s change it thus:

function RotatingLens:init(knowledge)
    self.knowledge = knowledge
    self.matrix = Matrix:unit()
end

function RotatingLens:adjust(x,y)
    local v = self.matrix*vec2(x,y)
    return v.x,v.y
end

function RotatingLens:turn(direction)
    if direction=="right" then
        self.matrix = self.matrix*Matrix:turnRight()
    elseif direction=="left" then
        self.matrix = self.matrix*Matrix:turnLeft()
    end
end

Test. That totally doesn’t work. I think I’ve got left and right wrong again, so I do this:

function RotatingLens:turn(direction)
    if direction=="right" then
        self.matrix = self.matrix*Matrix:turnLeft()
    elseif direction=="left" then
        self.matrix = self.matrix*Matrix:turnRight()
    end
end

If and when this works, I’ll sort it out. P.S. I really wish I had committed the Matrix. Let me do that now. Commit: Matrix operations.

Now the tests for rotation lens again.

Just one fails:

1: Right turn moves dead ahead to left turns -- 
Actual: nil, 
Expected: 1

That’s checking the turns variable, which no longer exists:

        _:test("Right turn moves dead ahead to left", function()
            local k = Knowledge()
            k:addFactAt("DA",0,1) -- dead ahead
            local lens = RotatingLens(k)
            _:expect(lens:factAt(0,1),"0 degrees").is("DA")
            lens:turn("right")
            _:expect(lens.turns,"turns").is(1)
            xa,ya = lens:adjust(-1,0)
            _:expect(xa,"xa").is(0)
            _:expect(ya,"ya").is(1)
            _:expect(lens:factAt(-1,0), "90").is("DA") -- off to left
        end)

Just remove that line and the tests should run. They do. Now I just hate that left turn right turn stuff. The big is that the Matrix methods should be called rotateRight and rotateLeft.

Let’s fix that:

function Matrix:rotateLeft()
    return Matrix{0,1,-1,0}
end

function Matrix:rotateRight()
    return Matrix{0,-1,1,0}
end

Now it makes sense to say this:

function RotatingLens:turn(direction)
    -- when robot turns right, world rotates left and vice versa
    if direction=="right" then
        self.matrix = self.matrix*Matrix:rotateLeft()
    elseif direction=="left" then
        self.matrix = self.matrix*Matrix:rotateRight()
    end
end

I think I still have a problem here. I think that if we want to do rotation R1 and then R2, the matrix should be R2*R1.

Do we even have a test for that, and could we tell the difference? I’ll begin by changing the code:

function RotatingLens:turn(direction)
    -- when robot turns right, world rotates left and vice versa
    if direction=="right" then
        self.matrix = Matrix:rotateLeft()*self.matrix
    elseif direction=="left" then
        self.matrix = Matrix:rotateRight()*self.matrix
    end
end

Curiously, I think we can never tell the difference, because it doesn’t matter which order we apply when the turns are all 90 degrees. Be that as it may, I think this is the formally correct version.

Let’s sum up here before I make more mistakes.

Summary

My tests are confirming my math and my intuition. When we turn left, the world turns right, and vice versa. And I hand-draw pictures and turned them around to get the values. For the matrix multiply, I typed in the formulas one way and did the numeric calculations another, and as you can see, with those numbers, there’s no real chance to get them wrong by choosing the wrong values to use.

sticky notes on the desktop

Because the matrix only has ones and zeros in it, there is no chance of accumulated rounding errors in my matrix multiply: integers times and plus integers are integers. So we can just let the matrix accumulate. If trig didn’t work the way it does we might have to deal with drift, but we shouldn’t have that issue here.

Looking at where things stand, we have some gaps in our design. We’re not accumulating scan knowledge into our private version. We’re not really doing anything sensible about moving and turning together: when we turn, our motion should go, say, 1,0 instead of the usual 0,1. We can fix this by rotating the vector of motion … if we’re careful.

All this is too ad hoc. This is not inherently bad: we have been working to get a viable demo, and we are evolving the program and the design, indeed evolving our understanding of the problem and how to solve it. We should expect that our design will be rough … and we should be sure to improve it every chance we get.

What do I mean by chance? I mean every time we change the code we should improve it. Every time we read the code, if it’s not good enough, we should improve it. We need to keep the design quite close to our understanding. All our ideas should be in the code, not just in our heads.

Or so it seems to me. I said “should” a lot up there. I would do better to say something like “things go better for me when I”, and “I choose to focus on”, and such. You get to decide what works for you.

Anyway, now we have a Matrix, and we have a RotationLens that uses it. I think in the future, we’ll build the rotation directly into the regular Lens, but we’ll see about that.

Tomorrow is the last day before the walking skeleton will be shown to our stakeholders (me). I’d like to get moving and turning integrated, and I’d like to get our robot knowledge to accumulate everything it learns from all the scans, not just the most recent one.

But that’s for tomorrow. Today, I’ve got other matters to attend to.

See you next time!