Skip to content

Commit 228fe62

Browse files
QR Decomposition Method Object Refactoring (#288)
* Added an example discovered in wikipedia that we can use to get a deeper understanding. * Added a test to see how QR Decomposition works and then began migrating logic to the new method object. * Clarified the classification of a method. * Copied the logic to a more sensible place. * Replaced logic with a method that does the same thing. * Made the code more intention-revealing. * Extracted a computation to an intention revealing variable. * Broke up the computation of the indices. The computation i appears to be a matrix of minor. * Extracted a computation to a more sensible place in an intention-revealing method. * Added a test that computes the minor of a square matrix. * Added another assertion to make logic of method clear. * Added another assertion to raise confidence. * Another another interesting test where we compute the minor of a non-square matrix. * Improved code formatting, replaced duplicate code with a message send. * Inlined some local variables, made the decomposeWithPivot method look more similar to decompose. * Used a more sensible name, derived from the wikipedia entry. * Improved the name of the temporary variable. * Added a test that gets to the route of the problem. * Promoted the Q and R to state variables as they are duplicated in the decomposition methods. * Extracted a pivot temporary variable. * Inproved the names of some temporary variables. * Improved the name of the temporary variables. * Corrected the name of the temporary variable. * Use an already existing intention-revealing message. * Moved code to a more sensible place. * Inproved the names of some variables. * Improved the name of a variable. * Improved the name of a temporary variable. * Extracted logic to a more sensible place, enhancing encapsulation. * Made similar code even more similar. * Made similar code even more similar. * Made similar code even more similar. * Made the name of the local variable the same.
1 parent bb393e9 commit 228fe62

File tree

5 files changed

+210
-15
lines changed

5 files changed

+210
-15
lines changed

src/Math-Core/PMVector.class.st

+5
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ PMVector >> isReal [
220220
^ self allSatisfy: [ :each | each isRealNumber ].
221221
]
222222

223+
{ #category : #testing }
224+
PMVector >> isZero [
225+
^ self allSatisfy: [ :element | element = 0 ]
226+
]
227+
223228
{ #category : #operation }
224229
PMVector >> log [
225230
"Apply log function to every element of a vector"

src/Math-Matrix/PMMatrix.class.st

+14
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,14 @@ PMMatrix >> lupInverse [
660660
ifNotNil: [ :i | ^ self class rows: i ]
661661
]
662662

663+
{ #category : #operation }
664+
PMMatrix >> minor: rowIndex and: columnIndex [
665+
666+
^ PMMatrix rows:
667+
((self rows allButFirst: columnIndex) collect: [ :aRow |
668+
aRow allButFirst: rowIndex ])
669+
]
670+
663671
{ #category : #'as yet unclassified' }
664672
PMMatrix >> mpInverse [
665673
"Moore Penrose Inverse. "
@@ -883,6 +891,12 @@ PMMatrix >> rowsDo: aBlock [
883891
^ rows do: aBlock
884892
]
885893

894+
{ #category : #iterators }
895+
PMMatrix >> rowsWithIndexDo: aBlock [
896+
897+
^ rows withIndexDo: aBlock
898+
]
899+
886900
{ #category : #transformation }
887901
PMMatrix >> scaleBy: aNumber [
888902

src/Math-Matrix/PMQRDecomposition.class.st

+84-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ Class {
66
#superclass : #Object,
77
#instVars : [
88
'matrixToDecompose',
9-
'colSize'
9+
'colSize',
10+
'r',
11+
'q'
1012
],
1113
#category : #'Math-Matrix'
1214
}
@@ -18,7 +20,7 @@ PMQRDecomposition class >> of: matrix [
1820
^ self new of: matrix
1921
]
2022

21-
{ #category : #private }
23+
{ #category : #arithmetic }
2224
PMQRDecomposition >> decompose [
2325

2426
"
@@ -27,25 +29,23 @@ that describes the mechanics:
2729
https://en.wikipedia.org/wiki/QR_decomposition#Using_Householder_reflections
2830
"
2931

30-
| identityMatrix q r householderVector i |
31-
r := self initialRMatrix.
32-
q := self initialQMatrix.
32+
| identityMatrix householderVector i matrixOfMinor |
3333
identityMatrix := PMSymmetricMatrix identity: colSize.
3434
1 to: self numberOfColumns do: [ :col |
35-
| columnVectorFromRMatrix householderMatrix |
35+
| columnVectorFromRMatrix householderMatrix v |
3636
columnVectorFromRMatrix := r columnVectorAt: col size: colSize.
3737
householderVector := columnVectorFromRMatrix householder.
38-
i := (PMVector zeros: col - 1) , (householderVector at: 2).
38+
v := (PMVector zeros: col - 1) , (householderVector at: 2).
3939
householderMatrix := identityMatrix
4040
-
41-
((householderVector at: 1) * i tensorProduct: i).
41+
((householderVector at: 1) * v tensorProduct: v).
4242
q := q * householderMatrix.
43-
i := PMMatrix rows:
44-
((r rows allButFirst: col - 1) collect: [ :aRow |
45-
aRow allButFirst: col - 1 ]).
46-
i := i - ((householderVector at: 2) tensorProduct:
47-
(householderVector at: 1) * (householderVector at: 2) * i).
48-
i rows withIndexDo: [ :aRow :index |
43+
matrixOfMinor := r minor: col - 1 and: col - 1.
44+
matrixOfMinor := matrixOfMinor
45+
- ((householderVector at: 2) tensorProduct:
46+
(householderVector at: 1)
47+
* (householderVector at: 2) * matrixOfMinor).
48+
matrixOfMinor rowsWithIndexDo: [ :aRow :index |
4949
aRow withIndexDo: [ :n :c |
5050
r
5151
rowAt: col + index - 1
@@ -54,7 +54,7 @@ https://en.wikipedia.org/wiki/QR_decomposition#Using_Householder_reflections
5454
ifTrue: [ 0 ]
5555
ifFalse: [ n ]) ] ] ].
5656
i := 0.
57-
[ (r rowAt: colSize) allSatisfy: [ :n | n = 0 ] ] whileTrue: [
57+
[ (r rowAt: colSize) isZero ] whileTrue: [
5858
i := i + 1.
5959
colSize := colSize - 1 ].
6060
i > 0 ifTrue: [
@@ -65,6 +65,73 @@ https://en.wikipedia.org/wiki/QR_decomposition#Using_Householder_reflections
6565
^ Array with: q with: r
6666
]
6767

68+
{ #category : #arithmetic }
69+
PMQRDecomposition >> decomposeWithPivot [
70+
71+
| identityMatrix householderVector i v vectorOfNormSquareds rank mx pivot matrixOfMinor |
72+
vectorOfNormSquareds := matrixToDecompose columnsCollect: [
73+
:columnVector | columnVector * columnVector ].
74+
mx := vectorOfNormSquareds indexOf: vectorOfNormSquareds max.
75+
pivot := Array new: vectorOfNormSquareds size.
76+
rank := 0.
77+
identityMatrix := PMSymmetricMatrix identity: colSize.
78+
[
79+
| householderMatrix |
80+
rank := rank + 1.
81+
pivot at: rank put: mx.
82+
r swapColumn: rank withColumn: mx.
83+
vectorOfNormSquareds swap: rank with: mx.
84+
householderVector := (r columnVectorAt: rank size: colSize)
85+
householder.
86+
v := (PMVector zeros: rank - 1) , (householderVector at: 2).
87+
householderMatrix := identityMatrix
88+
-
89+
((householderVector at: 1) * v tensorProduct: v).
90+
q := q * householderMatrix.
91+
matrixOfMinor := r minor: rank - 1 and: rank - 1.
92+
matrixOfMinor := matrixOfMinor
93+
- ((householderVector at: 2) tensorProduct:
94+
(householderVector at: 1)
95+
* (householderVector at: 2) * matrixOfMinor).
96+
matrixOfMinor rowsWithIndexDo: [ :aRow :index |
97+
aRow withIndexDo: [ :element :column |
98+
r
99+
rowAt: rank + index - 1
100+
columnAt: rank + column - 1
101+
put: ((element closeTo: 0)
102+
ifTrue: [ 0 ]
103+
ifFalse: [ element ]) ] ].
104+
rank + 1 to: vectorOfNormSquareds size do: [ :ind |
105+
vectorOfNormSquareds
106+
at: ind
107+
put:
108+
(vectorOfNormSquareds at: ind)
109+
- (r rowAt: rank columnAt: ind) squared ].
110+
rank < vectorOfNormSquareds size
111+
ifTrue: [
112+
mx := (vectorOfNormSquareds
113+
copyFrom: rank + 1
114+
to: vectorOfNormSquareds size) max.
115+
(mx closeTo: 0) ifTrue: [ mx := 0 ].
116+
mx := mx > 0
117+
ifTrue: [
118+
vectorOfNormSquareds indexOf: mx startingAt: rank + 1 ]
119+
ifFalse: [ 0 ] ]
120+
ifFalse: [ mx := 0 ].
121+
mx > 0 ] whileTrue.
122+
i := 0.
123+
[ (r rowAt: colSize) isZero ] whileTrue: [
124+
i := i + 1.
125+
colSize := colSize - 1 ].
126+
i > 0 ifTrue: [
127+
r := PMMatrix rows: (r rows copyFrom: 1 to: colSize).
128+
i := q numberOfColumns - i.
129+
pivot := pivot copyFrom: 1 to: i.
130+
q := PMMatrix rows:
131+
(q rows collect: [ :row | row copyFrom: 1 to: i ]) ].
132+
^ Array with: q with: r with: pivot
133+
]
134+
68135
{ #category : #private }
69136
PMQRDecomposition >> initialQMatrix [
70137

@@ -88,4 +155,6 @@ PMQRDecomposition >> of: matrix [
88155

89156
matrixToDecompose := matrix.
90157
colSize := matrixToDecompose numberOfRows.
158+
r := self initialRMatrix.
159+
q := self initialQMatrix.
91160
]

src/Math-Tests-Matrix/PMMatrixTest.class.st

+35
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,41 @@ PMMatrixTest >> testMatrixTrace [
652652
self assert: a tr equals: 15
653653
]
654654

655+
{ #category : #'linear algebra' }
656+
PMMatrixTest >> testMinorsOfNonSquareMatrix [
657+
658+
| matrix expected |
659+
matrix := PMMatrix rows: {
660+
{ 1. 2 }.
661+
{ 6. 7 }.
662+
{ 5. 4 } }.
663+
664+
expected := PMMatrix rows: {
665+
{ 7 }.
666+
{ 4 } }.
667+
self assert: (matrix minor: 0 and: 0) equals: matrix.
668+
self assert: (matrix minor: 1 and: 1) equals: expected
669+
]
670+
671+
{ #category : #'linear algebra' }
672+
PMMatrixTest >> testMinorsOfSquareMatrix [
673+
674+
| matrix expected |
675+
matrix := PMMatrix rows: {
676+
{ 1. 2. 3 }.
677+
{ 6. 7. 8 }.
678+
{ 5. 4. 1 } }.
679+
680+
expected := PMMatrix rows: {
681+
{ 7. 8 }.
682+
{ 4. 1 } }.
683+
self assert: (matrix minor: 0 and: 0) equals: matrix.
684+
self assert: (matrix minor: 1 and: 1) equals: expected.
685+
self
686+
assert: (matrix minor: 2 and: 2)
687+
equals: (PMMatrix rows: { { 1 } })
688+
]
689+
655690
{ #category : #tests }
656691
PMMatrixTest >> testOnesMatrix [
657692
| a |

src/Math-Tests-Matrix/PMQRTest.class.st

+72
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,39 @@ PMQRTest >> assert: inverse isMoorePenroseInverseOf: aMatrix [
2727
closeTo: aMatrix mpInverse transpose.
2828
]
2929

30+
{ #category : #tests }
31+
PMQRTest >> testDecompositionOfMatrixCausingErraticFailure [
32+
33+
| a qrDecomposition matricesAndPivot q r expectedMatrix pivot |
34+
a := PMSymmetricMatrix rows:
35+
#( #( 0.41929313699681925 0.05975350554089691
36+
0.2771676258543356 0.35628773381760703 )
37+
#( 0.05975350554089691 0.12794227252152854
38+
0.3257742693302102 0.28814463284245906 )
39+
#( 0.2771676258543356 0.3257742693302102 0.8468441832097453
40+
0.9101872061892353 )
41+
#( 0.35628773381760703 0.28814463284245906
42+
0.9101872061892353 0.5163744224777326 ) ).
43+
44+
qrDecomposition := PMQRDecomposition of: a.
45+
matricesAndPivot := qrDecomposition decomposeWithPivot.
46+
47+
expectedMatrix := PMMatrix rows:
48+
#( #( 0.2771676258543356 0.35628773381760703
49+
0.41929313699681925 0.05975350554089691 )
50+
#( 0.3257742693302102 0.28814463284245906
51+
0.05975350554089691 0.12794227252152854 )
52+
#( 0.8468441832097453 0.9101872061892353
53+
0.2771676258543356 0.3257742693302102 )
54+
#( 0.9101872061892353 0.5163744224777326
55+
0.35628773381760703 0.28814463284245906 ) ).
56+
q := matricesAndPivot at: 1.
57+
r := matricesAndPivot at: 2.
58+
pivot := matricesAndPivot at: 3.
59+
self assert: q * r closeTo: expectedMatrix.
60+
self assert: pivot equals: #( 3 4 3 nil )
61+
]
62+
3063
{ #category : #tests }
3164
PMQRTest >> testHorizontalRectangularMatrixCannotBeDecomposed [
3265

@@ -169,6 +202,45 @@ PMQRTest >> testRank [
169202
self assert: matrix transpose rank equals: 3 ] ].
170203
]
171204

205+
{ #category : #tests }
206+
PMQRTest >> testSimpleQRDecomposition [
207+
208+
| a qrDecomposition decomposition |
209+
a := PMMatrix rows: {
210+
{ 12. -51. 4 }.
211+
{ 6. 167. -68 }.
212+
{ -4. 24. -41 } }.
213+
214+
qrDecomposition := PMQRDecomposition of: a.
215+
216+
decomposition := qrDecomposition decompose.
217+
decomposition first * decomposition second.
218+
self assert: decomposition first * decomposition second equals: a
219+
]
220+
221+
{ #category : #tests }
222+
PMQRTest >> testSimpleQRDecompositionWithPivot [
223+
224+
| a qrDecomposition decomposition expected |
225+
a := PMMatrix rows: {
226+
{ 12. -51. 4 }.
227+
{ 6. 167. -68 }.
228+
{ -4. 24. -41 } }.
229+
230+
qrDecomposition := PMQRDecomposition of: a.
231+
232+
decomposition := qrDecomposition decomposeWithPivot.
233+
decomposition first * decomposition second.
234+
235+
expected := PMMatrix rows: {
236+
{ -51. 4. 12 }.
237+
{ 167. -68. 6 }.
238+
{ 24. -41. -4 } }.
239+
self
240+
assert: decomposition first * decomposition second
241+
closeTo: expected
242+
]
243+
172244
{ #category : #tests }
173245
PMQRTest >> testVectorHouseholder [
174246

0 commit comments

Comments
 (0)