Skip to content

Commit ff7366e

Browse files
Merge pull request #286 from jordanmontt/least-squares
Added Least squares implementation
2 parents 7c07cd5 + 51a2aff commit ff7366e

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"
2+
SVD-based implementation of a minimum-norm solution to the overdetermined least squares problem.
3+
"
4+
Class {
5+
#name : #PMLeastSquares,
6+
#superclass : #Object,
7+
#category : #'Math-Numerical-Math-LinearAlgebra'
8+
}
9+
10+
{ #category : #accessing }
11+
PMLeastSquares >> pseudoinverseOfDiagonal: aMatrix [
12+
13+
"To get pseudoinverse of a diagonal rectangular matrix, we take reciprocal of any no-zero
14+
element of the main diagonal, leaving all zeros in place. Then we transpose the matrix."
15+
16+
| pseudoinverse diagonalSize |
17+
"Rows become columns and columns become rows because we transpose"
18+
pseudoinverse := PMMatrix zerosRows: aMatrix numberOfColumns cols: aMatrix numberOfRows.
19+
20+
"The size of the main diagonal of a rectangular matrix is its smallest dimension"
21+
diagonalSize := aMatrix numberOfRows min: aMatrix numberOfColumns.
22+
23+
"Inverting the elements on the main diaginal"
24+
1 to: diagonalSize do: [ :i |
25+
pseudoinverse at: i at: i put: ((aMatrix at: i at: i) = 0 ifTrue: [ 0 ]
26+
ifFalse: [ 1 / (aMatrix at: i at: i) ]) ].
27+
28+
^ pseudoinverse
29+
]
30+
31+
{ #category : #api }
32+
PMLeastSquares >> solveMatrixA: aMatrix matrixB: aMatrixOrVector [
33+
34+
"If b is a vector:
35+
x' = minimize || b - Ax ||
36+
37+
If B is a matrix:
38+
X' = minimize || B - AX ||"
39+
40+
| svd u s v pseudoinverse |
41+
svd := PMSingularValueDecomposition decompose: aMatrix.
42+
43+
u := svd leftSingularMatrix.
44+
s := svd diagonalSingularValueMatrix.
45+
v := svd rightSingularMatrix.
46+
47+
pseudoinverse := self pseudoinverseOfDiagonal: s.
48+
^ v * pseudoinverse * u transpose * aMatrixOrVector
49+
]
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"
2+
A PMLeastSquaresTest is a test class for testing the behavior of PMLeastSquares
3+
"
4+
Class {
5+
#name : #PMLeastSquaresTest,
6+
#superclass : #TestCase,
7+
#instVars : [
8+
'leastSquares'
9+
],
10+
#category : #'Math-Tests-Numerical'
11+
}
12+
13+
{ #category : #running }
14+
PMLeastSquaresTest >> setUp [
15+
16+
super setUp.
17+
leastSquares := PMLeastSquares new
18+
]
19+
20+
{ #category : #tests }
21+
PMLeastSquaresTest >> testPseudoinverseOfDiagonalSquareMatrix [
22+
23+
| matrix expectedInverse inverse |
24+
25+
matrix := PMMatrix rows: #(
26+
(2 0 0 0)
27+
(0 1 0 0)
28+
(0 0 -3 0)
29+
(0 0 0 -1) ).
30+
31+
expectedInverse := PMMatrix rows: {
32+
{ 1/2 . 0 . 0 . 0 } .
33+
{ 0 . 1 . 0 . 0 } .
34+
{ 0 . 0 . -1/3 . 0} .
35+
{ 0 . 0 . 0 . -1 } }.
36+
37+
inverse := leastSquares pseudoinverseOfDiagonal: matrix.
38+
self assert: inverse closeTo: expectedInverse.
39+
]
40+
41+
{ #category : #tests }
42+
PMLeastSquaresTest >> testPseudoinverseOfDiagonalSquareMatrixWithZeros [
43+
44+
| matrix expectedInverse inverse |
45+
46+
matrix := PMMatrix rows: #(
47+
(2 0 0 0)
48+
(0 1 0 0)
49+
(0 0 0 0)
50+
(0 0 0 0) ).
51+
52+
expectedInverse := PMMatrix rows: #(
53+
(0.5 0 0 0)
54+
( 0 1 0 0)
55+
( 0 0 0 0)
56+
( 0 0 0 0) ).
57+
58+
inverse := leastSquares pseudoinverseOfDiagonal: matrix.
59+
self assert: inverse closeTo: expectedInverse.
60+
]
61+
62+
{ #category : #tests }
63+
PMLeastSquaresTest >> testPseudoinverseOfDiagonalTallMatrix [
64+
65+
| matrix expectedInverse inverse |
66+
67+
matrix := PMMatrix rows: #(
68+
(2 0 0 0)
69+
(0 1 0 0)
70+
(0 0 -3 0)
71+
(0 0 0 -1)
72+
(0 0 0 0)
73+
(0 0 0 0) ).
74+
75+
expectedInverse := PMMatrix rows: {
76+
{ 1/2 . 0 . 0 . 0 . 0 . 0 } .
77+
{ 0 . 1 . 0 . 0 . 0 . 0 } .
78+
{ 0 . 0 . -1/3 . 0 . 0 . 0 } .
79+
{ 0 . 0 . 0 . -1 . 0 . 0 }}.
80+
81+
inverse := leastSquares pseudoinverseOfDiagonal: matrix.
82+
self assert: inverse closeTo: expectedInverse.
83+
]
84+
85+
{ #category : #tests }
86+
PMLeastSquaresTest >> testPseudoinverseOfDiagonalWideMatrix [
87+
88+
| matrix expectedInverse inverse |
89+
90+
matrix := PMMatrix rows: #(
91+
(2 0 0 0 0 0)
92+
(0 1 0 0 0 0)
93+
(0 0 -3 0 0 0)
94+
(0 0 0 -1 0 0)).
95+
96+
expectedInverse := PMMatrix rows: {
97+
{ 1/2 . 0 . 0 . 0 } .
98+
{ 0 . 1 . 0 . 0 } .
99+
{0 . 0 . -1/3 . 0} .
100+
{0 . 0 . 0 . -1 } .
101+
{0 . 0 . 0 . 0 } .
102+
{0 . 0 . 0 . 0 }}.
103+
104+
inverse := leastSquares pseudoinverseOfDiagonal: matrix.
105+
self assert: inverse closeTo: expectedInverse.
106+
]
107+
108+
{ #category : #tests }
109+
PMLeastSquaresTest >> testSolveSmallOneSolution [
110+
"Small example of least squares system (AX = B) with one solution taken from here: https://textbooks.math.gatech.edu/ila/least-squares.html"
111+
| matrixA vectorB expectedSolution solution |
112+
113+
matrixA := PMMatrix rows: #(
114+
(0 1.1)
115+
(1 0 )
116+
(0 -0.2) ).
117+
118+
vectorB := #(1.1 -1.1 -0.2) asPMVector.
119+
expectedSolution := #(-1.1 1) asPMVector.
120+
121+
solution := leastSquares
122+
solveMatrixA: matrixA
123+
matrixB: vectorB.
124+
125+
self assert: solution closeTo: expectedSolution.
126+
]

0 commit comments

Comments
 (0)