-
Notifications
You must be signed in to change notification settings - Fork 6
/
lmmin.py
3175 lines (2508 loc) · 93 KB
/
lmmin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 1997-2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, Craig Markwardt
# Copyright 2003 Mark Rivers
# Copyright 2006, 2009-2011 (inclusive) Nadia Dencheva
# Copyright 2011-2017 (inclusive) Peter Williams
#
# This software is provided as is without any warranty whatsoever. Permission
# to use, copy, modify, and distribute modified or unmodified copies is
# granted, provided this copyright and disclaimer are included unchanged.
### lmmin is a Levenberg-Marquardt least-squares minimizer derived
### (circuitously) from the classic MINPACK implementation. Usage information
### is given in the docstring farther below. Various important pieces of
### information that are out of the scope of the docstring follow immediately
### below.
# == Provenance ==
#
# This implementation of the Levenberg-Marquardt technique has its origins in
# MINPACK-1 (the lmdif and lmdir subroutines), by Jorge Moré, Burt Garbow, and
# Ken Hillstrom, implemented around 1980.
#
# In 1997-1998, Craig Markwardt ported the FORTRAN code (with permission) to
# IDL, resulting in the MPFIT procedure.
#
# Around 2003, Mark Rivers ported the mpfit.pro file to Python and the Numeric
# module, creating mpfit.py. (It would be helpful to be able to identify the
# precise version that was ported, so that bugfixes to mpfit.pro could be
# forward-ported. The bug corrected on "21 Nov 2003" in mpfit.pro was
# originally present in this version, so the Python port likely happened
# before then.)
#
# Around 2006, mpfit.py was ported to the Numpy module to create nmpfit.py.
# Based on STSCI version control logs it appears that this was done by Nadia
# Dencheva.
#
# In 2011-2012, Peter Williams began fixing bugs in the port and significantly
# reworking the API, creating this file, lmmin.py. Previous authors deserve
# all of the credit for anything that works and none of the blame for anything
# that doesn't.
#
# (There exists a C-based Levenberg-Marquardt minimizer named lmmin by Joachim
# Wuttke [http://joachimwuttke.de/lmfit/]. This implementation is not directly
# related to that one, although lmmin also appears to stem from the original
# MINPACK implementation.)
#
#
# == Transposition ==
#
# This version of the MINPACK implementation differs from the others of which
# I am aware in that it transposes the matrices used in intermediate
# calculations. While in both Fortran and Python, an n-by-m matrix is
# visualized as having n rows and m columns, in Fortran the columns are
# directly adjacent in memory, and hence the preferred inner axis for
# iteration, while in Python the rows are the preferred inner axis. By
# transposing the matrices we match the algorithms to the memory layout as
# intended in the original Fortran. I have no idea how much of a performance
# boost this gives, and of course we're using Python so you're deluding
# yourself if you're trying to wring out every cycle, but I suppose it helps,
# and it makes some of the code constructs nicer and feels a lot cleaner
# conceptually to me.
#
# The main operation of interest is the Q R factorization, which in the
# Fortran version involves matrices A, P, Q and R such that
#
# A P = Q R or, in Python,
# a[:,pmut] == np.dot (q, r)
#
# where A is an arbitrary m-by-n matrix, P is a permutation matrix, Q is an
# orthogonal m-by-m matrix (Q Q^T = Ident), and R is an m-by-n upper
# triangular matrix. In the transposed version,
#
# A P = R Q
#
# where A is n-by-m and R is n-by-m and lower triangular. We refer to this as
# the "transposed Q R factorization." I've tried to update the documentation
# to reflect this change, but I can't claim that I completely understand the
# mapping of the matrix algebra into code, so there are probably confusing
# mistakes in the comments and docstrings.
#
#
# == Web Links ==
#
# MINPACK-1: http://www.netlib.org/minpack/
#
# Markwardt's IDL software MPFIT.PRO: http://purl.com/net/mpfit
#
# Rivers' Python software mpfit.py: http://cars.uchicago.edu/software/python/mpfit.html
#
# nmpfit.py is part of stsci_python:
# http://www.stsci.edu/institute/software_hardware/pyraf/stsci_python
#
#
# == Academic References ==
#
# Levenberg, K. 1944, "A method for the solution of certain nonlinear
# problems in least squares," Quart. Appl. Math., vol. 2,
# pp. 164-168.
#
# Marquardt, DW. 1963, "An algorithm for least squares estimation of
# nonlinear parameters," SIAM J. Appl. Math., vol. 11, pp. 431-441.
# (DOI: 10.1137/0111030 )
#
# For MINPACK-1:
#
# Moré, J. 1978, "The Levenberg-Marquardt Algorithm: Implementation
# and Theory," in Numerical Analysis, vol. 630, ed. G. A. Watson
# (Springer-Verlag: Berlin), p. 105 (DOI: 10.1007/BFb0067700 )
#
# Moré, J and Wright, S. 1987, "Optimization Software Guide," SIAM,
# Frontiers in Applied Mathematics, no. 14. (ISBN:
# 978-0-898713-22-0)
#
# For Markwardt's IDL software MPFIT.PRO:
#
# Markwardt, C. B. 2008, "Non-Linear Least Squares Fitting in IDL with
# MPFIT," in Proc. Astronomical Data Analysis Software and Systems
# XVIII, Quebec, Canada, ASP Conference Series, Vol. XXX, eds.
# D. Bohlender, P. Dowler & D. Durand (Astronomical Society of the
# Pacific: San Francisco), pp. 251-254 (ISBN: 978-1-58381-702-5;
# arxiv:0902.2850; bibcode: 2009ASPC..411..251M)
"""pwkit.lmmin - Pythonic, Numpy-based Levenberg-Marquardt least-squares minimizer
Basic usage::
from pwkit.lmmin import Problem, ResidualProblem
def yfunc(params, vals):
vals[:] = {stuff with params}
def jfunc(params, jac):
jac[i,j] = {deriv of val[j] w.r.t. params[i]}
# i.e. jac[i] = {deriv of val wrt params[i]}
p = Problem(npar, nout, yfunc, jfunc=None)
solution = p.solve(guess)
p2 = Problem()
p2.set_npar(npar) # enables configuration of parameter meta-info
p2.set_func(nout, yfunc, jfunc)
Main Solution properties:
prob - The Problem.
status - Set of strings; presence of 'ftol', 'gtol', or 'xtol' suggests success.
params - Final parameter values.
perror - 1σ uncertainties on params.
covar - Covariance matrix of parameters.
fnorm - Final norm of function output.
fvec - Final vector of function outputs.
fjac - Final Jacobian matrix of d(fvec)/d(params).
Automatic least-squares model-fitting (subtracts "observed" Y values and
multiplies by inverse errors):
def yrfunc(params, modelyvalues):
modelyvalues[:] = {stuff with params}
def yjfunc(params, modelyjac):
jac[i,j] = {deriv of modelyvalue[j] w.r.t. params[i]}
p.set_residual_func(yobs, errinv, yrfunc, jrfunc, reckless=False)
p = ResidualProblem(npar, yobs, errinv, yrfunc, jrfunc=None, reckless=False)
Parameter meta-information:
p.p_value(paramindex, value, fixed=False)
p.p_limit(paramindex, lower=-inf, upper=+inf)
p.p_step(paramindex, stepsize, maxstep=info, isrel=False)
p.p_side(paramindex, sidedness) # one of 'auto', 'pos', 'neg', 'two'
p.p_tie(paramindex, tiefunc) # pval = tiefunc(params)
solve() status codes:
Solution.status is a set of strings. The presence of a string in the
set means that the specified condition was active when the iteration
terminated. Multiple conditions may contribute to ending the
iteration. The algorithm likely did not converge correctly if none of
'ftol', 'xtol', or 'gtol' are in status upon termination.
'ftol' (MINPACK/MPFIT equiv: 1, 3)
"Termination occurs when both the actual and predicted relative
reductions in the sum of squares are at most FTOL. Therefore, FTOL
measures the relative error desired in the sum of squares."
'xtol' (MINPACK/MPFIT equiv: 2, 3)
"Termination occurs when the relative error between two consecutive
iterates is at most XTOL. Therefore, XTOL measures the relative
error desired in the approximate solution."
'gtol' (MINPACK/MPFIT equiv: 4)
"Termination occurs when the cosine of the angle between fvec and
any column of the jacobian is at most GTOL in absolute
value. Therefore, GTOL measures the orthogonality desired between
the function vector and the columns of the jacobian."
'maxiter' (MINPACK/MPFIT equiv: 5)
Number of iterations exceeds maxiter.
'feps' (MINPACK/MPFIT equiv: 6)
"ftol is too small. no further reduction in the sum of squares is
possible."
'xeps' (MINPACK/MPFIT equiv: 7)
"xtol is too small. no further improvement in the approximate
solution x is possible."
'geps' (MINPACK/MPFIT equiv: 8)
"gtol is too small. fvec is orthogonal to the columns of the jacobian
to machine precision."
(This docstring contains only usage information. For important
information regarding provenance, license, and academic references,
see comments in the module source code.)
"""
from __future__ import absolute_import, division, print_function, unicode_literals
__all__ = """enorm_fast enorm_mpfit_careful enorm_minpack Problem Solution
ResidualProblem check_derivative""".split()
import numpy as np
# Quickie testing infrastructure
_testfuncs = []
def test(f): # a decorator
_testfuncs.append(f)
return f
def _runtests(namefilt=None):
for f in _testfuncs:
if namefilt is not None and f.__name__ != namefilt:
continue
n = f.__name__
if n[0] == "_":
n = n[1:]
print(n, "...")
f()
from numpy.testing import assert_array_almost_equal as Taaae
from numpy.testing import assert_almost_equal as Taae
def _timer_helper(n=100):
for i in range(n):
for f in _testfuncs:
f()
# Parameter Info attributes that can be specified
#
# Each parameter can be described by five floats:
PI_F_VALUE = 0 # specified initial value
PI_F_LLIMIT = 1 # lower bound on param value (can be -inf)
PI_F_ULIMIT = 2 # upper bound (can be +inf)
PI_F_STEP = 3 # fixed parameter step size to use (abs or rel), 0. for unspecified
PI_F_MAXSTEP = 4 # maximum step to take
PI_NUM_F = 5
# Four bits of data
PI_M_SIDE = 0x3 # sidedness of derivative - two bits
PI_M_FIXED = 0x4 # fixed value
PI_M_RELSTEP = 0x8 # whether the specified stepsize is relative
# And one object
PI_O_TIEFUNC = 0 # fixed to be a function of other parameters
PI_NUM_O = 1
# Codes for the automatic derivative sidedness
DSIDE_AUTO = 0x0
DSIDE_POS = 0x1
DSIDE_NEG = 0x2
DSIDE_TWO = 0x3
_dside_names = {
"auto": DSIDE_AUTO,
"pos": DSIDE_POS,
"neg": DSIDE_NEG,
"two": DSIDE_TWO,
}
anynotfinite = lambda x: not np.all(np.isfinite(x))
# Euclidean norm-calculating functions. The naive implementation is
# fast but can be sensitive to under/overflows. The "mpfit_careful"
# version is slower but tries to be more robust. The "minpack"
# version, which does indeed emulate the MINPACK implementation, also
# tries to be careful. I've used this last implementation a little
# bit but haven't compared it to the others thoroughly.
enorm_fast = lambda v, finfo: np.sqrt(np.dot(v, v))
def enorm_mpfit_careful(v, finfo):
# "This is hopefully a compromise between speed and robustness.
# Need to do this because of the possibility of over- or under-
# flow."
mx = max(abs(v.max()), abs(v.min()))
if mx == 0:
return v[0] * 0.0 # preserve type (?)
if not np.isfinite(mx):
raise ValueError("tried to compute norm of a vector with nonfinite values")
if mx > finfo.max / v.size or mx < finfo.tiny * v.size:
return mx * np.sqrt(np.dot(v / mx, v / mx))
return np.sqrt(np.dot(v, v))
def enorm_minpack(v, finfo):
rdwarf = 3.834e-20
rgiant = 1.304e19
agiant = rgiant / v.size
s1 = s2 = s3 = x1max = x3max = 0.0
for i in range(v.size):
xabs = abs(v[i])
if xabs > rdwarf and xabs < agiant:
s2 += xabs**2
elif xabs <= rdwarf:
if xabs <= x3max:
if xabs != 0.0:
s3 += (xabs / x3max) ** 2
else:
s3 = 1 + s3 * (x3max / xabs) ** 2
x3max = xabs
else:
if xabs <= x1max:
s1 += (xabs / x1max) ** 2
else:
s1 = 1.0 + s1 * (x1max / xabs) ** 2
x1max = xabs
if s1 != 0.0:
return x1max * np.sqrt(s1 + (s2 / x1max) / x1max)
if s2 == 0.0:
return x3max * np.sqrt(s3)
if s2 >= x3max:
return np.sqrt(s2 * (1 + (x3max / s2) * (x3max * s3)))
return np.sqrt(x3max * ((s2 / x3max) + (x3max * s3)))
# Q-R factorization.
def _qr_factor_packed(a, enorm, finfo):
"""Compute the packed pivoting Q-R factorization of a matrix.
Parameters:
a - An n-by-m matrix, m >= n. This will be *overwritten*
by this function as described below!
enorm - A Euclidian-norm-computing function.
finfo - A Numpy finfo object.
Returns:
pmut - An n-element permutation vector
rdiag - An n-element vector of the diagonal of R
acnorm - An n-element vector of the norms of the rows
of the input matrix 'a'.
Computes the transposed Q-R factorization of the matrix 'a', with
pivoting, in a packed form, in-place. The packed information can be
used to construct matrices Q and R such that
A P = R Q or, in Python,
np.dot(r, q) = a[pmut]
where q is m-by-m and q q^T = ident and r is n-by-m and is lower
triangular. The function _qr_factor_full can compute these
matrices. The packed form of output is all that is used by the main LM
fitting algorithm.
"Pivoting" refers to permuting the rows of 'a' to have their norms in
nonincreasing order. The return value 'pmut' maps the unpermuted rows
of 'a' to permuted rows. That is, the norms of the rows of a[pmut] are
in nonincreasing order.
The parameter 'a' is overwritten by this function. Its new value
should still be interpreted as an n-by-m array. It comes in two
parts. Its strict lower triangular part contains the struct lower
triangular part of R. (The diagonal of R is returned in 'rdiag' and
the strict upper trapezoidal part of R is zero.) The upper trapezoidal
part of 'a' contains Q as factorized into a series of Householder
transformation vectors. Q can be reconstructed as the matrix product
of n Householder matrices, where the i'th Householder matrix is
defined by
H_i = I - 2 (v^T v) / (v v^T)
where 'v' is the pmut[i]'th row of 'a' with its strict lower
triangular part set to zero. See _qr_factor_full for more information.
'rdiag' contains the diagonal part of the R matrix, taking into
account the permutation of 'a'. The strict lower triangular part of R
is stored in 'a' *with permutation*, so that the i'th row of R has
rdiag[i] as its diagonal and a[pmut[i],:i] as its upper part. See
_qr_factor_full for more information.
'acnorm' contains the norms of the rows of the original input
matrix 'a' without permutation.
The form of this transformation and the method of pivoting first
appeared in Linpack."""
machep = finfo.eps
n, m = a.shape
if m < n:
raise ValueError('"a" must be at least as tall as it is wide')
acnorm = np.empty(n, finfo.dtype)
for j in range(n):
acnorm[j] = enorm(a[j], finfo)
rdiag = acnorm.copy()
wa = acnorm.copy()
pmut = np.arange(n)
for i in range(n):
# Find the row of a with the i'th largest norm, and note it in
# the pivot vector.
kmax = rdiag[i:].argmax() + i
if kmax != i:
temp = pmut[i]
pmut[i] = pmut[kmax]
pmut[kmax] = temp
rdiag[kmax] = rdiag[i]
wa[kmax] = wa[i]
temp = a[i].copy()
a[i] = a[kmax]
a[kmax] = temp
# Compute the Householder transformation to reduce the i'th
# row of A to a multiple of the i'th unit vector.
ainorm = enorm(a[i, i:], finfo)
if ainorm == 0:
rdiag[i] = 0
continue
if a[i, i] < 0:
# Doing this apparently improves FP precision somehow.
ainorm = -ainorm
a[i, i:] /= ainorm
a[i, i] += 1
# Apply the transformation to the remaining rows and update
# the norms.
for j in range(i + 1, n):
a[j, i:] -= a[i, i:] * np.dot(a[i, i:], a[j, i:]) / a[i, i]
if rdiag[j] != 0:
rdiag[j] *= np.sqrt(max(1 - (a[j, i] / rdiag[j]) ** 2, 0))
if 0.05 * (rdiag[j] / wa[j]) ** 2 <= machep:
# What does this do???
wa[j] = rdiag[j] = enorm(a[j, i + 1 :], finfo)
rdiag[i] = -ainorm
return pmut, rdiag, acnorm
def _manual_qr_factor_packed(a, dtype=float):
# This testing function gives sensible defaults to _qr_factor_packed
# and makes a copy of its input to make comparisons easier.
a = np.array(a, dtype)
pmut, rdiag, acnorm = _qr_factor_packed(a, enorm_mpfit_careful, np.finfo(dtype))
return a, pmut, rdiag, acnorm
def _qr_factor_full(a, dtype=float):
"""Compute the QR factorization of a matrix, with pivoting.
Parameters:
a - An n-by-m arraylike, m >= n.
dtype - (optional) The data type to use for computations.
Default is float.
Returns:
q - An m-by-m orthogonal matrix (q q^T = ident)
r - An n-by-m upper triangular matrix
pmut - An n-element permutation vector
The returned values will satisfy the equation
np.dot(r, q) == a[:,pmut]
The outputs are computed indirectly via the function
_qr_factor_packed. If you need to compute q and r matrices in
production code, there are faster ways to do it. This function is for
testing _qr_factor_packed.
The permutation vector pmut is a vector of the integers 0 through
n-1. It sorts the rows of 'a' by their norms, so that the
pmut[i]'th row of 'a' has the i'th biggest norm."""
n, m = a.shape
# Compute the packed Q and R matrix information.
packed, pmut, rdiag, acnorm = _manual_qr_factor_packed(a, dtype)
# Now we unpack. Start with the R matrix, which is easy: we just
# have to piece it together from the strict lower triangle of 'a'
# and the diagonal in 'rdiag'.
r = np.zeros((n, m))
for i in range(n):
r[i, :i] = packed[i, :i]
r[i, i] = rdiag[i]
# Now the Q matrix. It is the concatenation of n Householder
# transformations, each of which is defined by a row in the upper
# trapezoidal portion of 'a'. We extract the appropriate vector,
# construct the matrix for the Householder transform, and build up
# the Q matrix.
q = np.eye(m)
v = np.empty(m)
for i in range(n):
v[:] = packed[i]
v[:i] = 0
hhm = np.eye(m) - 2 * np.outer(v, v) / np.dot(v, v)
q = np.dot(hhm, q)
return q, r, pmut
@test
def _qr_examples():
# This is the sample given in the comments of Craig Markwardt's
# IDL MPFIT implementation.
a = np.asarray([[9.0, 2, 6], [4, 8, 7]])
packed, pmut, rdiag, acnorm = _manual_qr_factor_packed(a)
Taaae(
packed,
[[1.35218036, 0.70436073, 0.61631563], [-8.27623852, 1.96596229, 0.25868293]],
)
assert pmut[0] == 1
assert pmut[1] == 0
Taaae(rdiag, [-11.35781669, 7.24595584])
Taaae(acnorm, [11.0, 11.35781669])
q, r, pmut = _qr_factor_full(a)
Taaae(np.dot(r, q), a[pmut])
# This is the sample given in Wikipedia. I know, shameful!
a = np.asarray([[12.0, 6, -4], [-51, 167, 24], [4, -68, -41]])
packed, pmut, rdiag, acnorm = _manual_qr_factor_packed(a)
Taaae(
packed,
[
[1.28935268, -0.94748818, -0.13616597],
[-71.16941178, 1.36009392, 0.93291606],
[1.66803309, -2.18085468, 2.0],
],
)
assert pmut[0] == 1
assert pmut[1] == 2
assert pmut[2] == 0
Taaae(rdiag, [176.25549637, 35.43888862, 13.72812946])
Taaae(acnorm, [14.0, 176.25549637, 79.50471684])
q, r, pmut = _qr_factor_full(a)
Taaae(np.dot(r, q), a[pmut])
# A sample I constructed myself analytically. I made the Q
# from rotation matrices and chose R pretty dumbly to get a
# nice-ish matrix following the columnar norm constraint.
r3 = np.sqrt(3)
a = np.asarray([[-3 * r3, 7, -2], [3 * r3, 9, -6]])
q, r, pmut = _qr_factor_full(a)
r *= np.sign(q[0, 0])
for i in range(3):
# Normalize signs.
q[i] *= (-1) ** i * np.sign(q[i, 0])
assert pmut[0] == 1
assert pmut[1] == 0
Taaae(q, 0.25 * np.asarray([[r3, 3, -2], [-2 * r3, 2, 0], [1, r3, 2 * r3]]))
Taaae(r, np.asarray([[12, 0, 0], [4, 8, 0]]))
Taaae(np.dot(r, q), a[pmut])
# QR solution.
def _qrd_solve(r, pmut, ddiag, bqt, sdiag):
"""Solve an equation given a QR factored matrix and a diagonal.
Parameters:
r - **input-output** n-by-n array. The full lower triangle contains
the full lower triangle of R. On output, the strict upper
triangle contains the transpose of the strict lower triangle of
S.
pmut - n-vector describing the permutation matrix P.
ddiag - n-vector containing the diagonal of the matrix D in the base
problem (see below).
bqt - n-vector containing the first n elements of B Q^T.
sdiag - output n-vector. It is filled with the diagonal of S. Should
be preallocated by the caller -- can result in somewhat greater
efficiency if the vector is reused from one call to the next.
Returns:
x - n-vector solving the equation.
Compute the n-vector x such that
A^T x = B, D x = 0
where A is an n-by-m matrix, B is an m-vector, and D is an n-by-n
diagonal matrix. We are given information about pivoted QR
factorization of A with permutation, such that
A P = R Q
where P is a permutation matrix, Q has orthogonal rows, and R is lower
triangular with nonincreasing diagonal elements. Q is m-by-m, R is
n-by-m, and P is n-by-n. If x = P z, then we need to solve
R z = B Q^T,
P^T D P z = 0 (why the P^T? and do these need to be updated for the transposition?)
If the system is rank-deficient, these equations are solved as well as
possible in a least-squares sense. For the purposes of the LM
algorithm we also compute the lower triangular n-by-n matrix S such
that
P^T (A^T A + D D) P = S^T S. (transpose?)"""
n, m = r.shape
# "Copy r and bqt to preserve input and initialize s. In
# particular, save the diagonal elements of r in x." Recall that
# on input only the full lower triangle of R is meaningful, so we
# can mirror that into the upper triangle without issues.
for i in range(n):
r[i, i:] = r[i:, i]
x = r.diagonal().copy()
zwork = bqt.copy()
# "Eliminate the diagonal matrix d using a Givens rotation."
for i in range(n):
# "Prepare the row of D to be eliminated, locating the
# diagonal element using P from the QR factorization."
li = pmut[i]
if ddiag[li] == 0:
sdiag[i] = r[i, i]
r[i, i] = x[i]
continue
sdiag[i:] = 0
sdiag[i] = ddiag[li]
# "The transformations to eliminate the row of d modify only a
# single element of (q transpose)*b beyond the first n, which
# is initially zero."
bqtpi = 0.0
for j in range(i, n):
# "Determine a Givens rotation which eliminates the
# appropriate element in the current row of D."
if sdiag[j] == 0:
continue
if abs(r[j, j]) < abs(sdiag[j]):
cot = r[j, j] / sdiag[j]
sin = 0.5 / np.sqrt(0.25 + 0.25 * cot**2)
cos = sin * cot
else:
tan = sdiag[j] / r[j, j]
cos = 0.5 / np.sqrt(0.25 + 0.25 * tan**2)
sin = cos * tan
# "Compute the modified diagonal element of r and the
# modified element of ((q transpose)*b,0)."
r[j, j] = cos * r[j, j] + sin * sdiag[j]
temp = cos * zwork[j] + sin * bqtpi
bqtpi = -sin * zwork[j] + cos * bqtpi
zwork[j] = temp
# "Accumulate the transformation in the row of s."
if j + 1 < n:
temp = cos * r[j, j + 1 :] + sin * sdiag[j + 1 :]
sdiag[j + 1 :] = -sin * r[j, j + 1 :] + cos * sdiag[j + 1 :]
r[j, j + 1 :] = temp
# Save the diagonal of S and restore the diagonal of R
# from its saved location in x.
sdiag[i] = r[i, i]
r[i, i] = x[i]
# "Solve the triangular system for z. If the system is singular
# then obtain a least squares solution."
nsing = n
for i in range(n):
if sdiag[i] == 0.0:
nsing = i
zwork[i:] = 0
break
if nsing > 0:
zwork[nsing - 1] /= sdiag[nsing - 1] # Degenerate case
# "Reverse loop"
for i in range(nsing - 2, -1, -1):
s = np.dot(zwork[i + 1 : nsing], r[i, i + 1 : nsing])
zwork[i] = (zwork[i] - s) / sdiag[i]
# "Permute the components of z back to components of x."
x[pmut] = zwork
return x
def _manual_qrd_solve(r, pmut, ddiag, bqt, dtype=float, build_s=False):
r = np.asarray(r, dtype)
pmut = np.asarray(pmut, int)
ddiag = np.asarray(ddiag, dtype)
bqt = np.asarray(bqt, dtype)
swork = r.copy()
sdiag = np.empty(r.shape[1], r.dtype)
x = _qrd_solve(swork, pmut, ddiag, bqt, sdiag)
if not build_s:
return x, swork, sdiag
# Rebuild s.
swork = swork.T
for i in range(r.shape[1]):
swork[i, i:] = 0
swork[i, i] = sdiag[i]
return x, swork
def _qrd_solve_full(a, b, ddiag, dtype=float):
"""Solve the equation A^T x = B, D x = 0.
Parameters:
a - an n-by-m array, m >= n
b - an m-vector
ddiag - an n-vector giving the diagonal of D. (The rest of D is 0.)
Returns:
x - n-vector solving the equation.
s - the n-by-n supplementary matrix s.
pmut - n-element permutation vector defining the permutation matrix P.
The equations are solved in a least-squares sense if the system is
rank-deficient. D is a diagonal matrix and hence only its diagonal is
in fact supplied as an argument. The matrix s is full lower triangular
and solves the equation
P^T (A A^T + D D) P = S^T S (needs transposition?)
where P is the permutation matrix defined by the vector pmut; it puts
the rows of 'a' in order of nonincreasing rank, so that a[pmut]
has its rows sorted that way."""
a = np.asarray(a, dtype)
b = np.asarray(b, dtype)
ddiag = np.asarray(ddiag, dtype)
n, m = a.shape
assert m >= n
assert b.shape == (m,)
assert ddiag.shape == (n,)
# The computation is straightforward.
q, r, pmut = _qr_factor_full(a)
bqt = np.dot(b, q.T)
x, s = _manual_qrd_solve(r[:, :n], pmut, ddiag, bqt, dtype=dtype, build_s=True)
return x, s, pmut
@test
def _qrd_solve_alone():
# Testing out just the QR solution function without
# also the QR factorization bits.
# The very simplest case.
r = np.eye(2)
pmut = np.asarray([0, 1])
diag = np.asarray([0.0, 0])
bqt = np.asarray([3.0, 5])
x, s = _manual_qrd_solve(r, pmut, diag, bqt, build_s=True)
Taaae(x, [3.0, 5])
Taaae(s, np.eye(2))
# Now throw in a diagonal matrix ...
diag = np.asarray([2.0, 3.0])
x, s = _manual_qrd_solve(r, pmut, diag, bqt, build_s=True)
Taaae(x, [0.6, 0.5])
Taaae(s, np.sqrt(np.diag([5, 10])))
# And a permutation. We permute A but maintain
# B, effectively saying x1 = 5, x2 = 3, so
# we need to permute diag as well to scale them
# by the amounts that yield nice X values.
pmut = np.asarray([1, 0])
diag = np.asarray([3.0, 2.0])
x, s = _manual_qrd_solve(r, pmut, diag, bqt, build_s=True)
Taaae(x, [0.5, 0.6])
Taaae(s, np.sqrt(np.diag([5, 10])))
# Calculation of the Levenberg-Marquardt parameter
def _lm_solve(r, pmut, ddiag, bqt, delta, par0, enorm, finfo):
"""Compute the Levenberg-Marquardt parameter and solution vector.
Parameters:
r - IN/OUT n-by-m matrix, m >= n. On input, the full lower triangle is
the full lower triangle of R and the strict upper triangle is
ignored. On output, the strict upper triangle has been
obliterated. The value of 'm' here is not relevant so long as it
is at least n.
pmut - n-vector, defines permutation of R
ddiag - n-vector, diagonal elements of D
bqt - n-vector, first elements of B Q^T
delta - positive scalar, specifies scale of enorm(Dx)
par0 - positive scalar, initial estimate of the LM parameter
enorm - norm-computing function
finfo - info about chosen floating-point representation
Returns:
par - positive scalar, final estimate of LM parameter
x - n-vector, least-squares solution of LM equation (see below)
This routine computes the Levenberg-Marquardt parameter 'par' and a LM
solution vector 'x'. Given an n-by-n matrix A, an n-by-n nonsingular
diagonal matrix D, an m-vector B, and a positive number delta, the
problem is to determine values such that 'x' is the least-squares
solution to
A x = B
sqrt(par) * D x = 0
and either
(1) par = 0, dxnorm - delta <= 0.1 delta or
(2) par > 0 and |dxnorm - delta| <= 0.1 delta
where dxnorm = enorm(D x).
This routine is not given A, B, or D directly. If we define the
column-pivoted transposed QR factorization of A such that
A P = R Q
where P is a permutation matrix, Q has orthogonal rows, and R is a
lower triangular matrix with diagonal elements of nonincreasing
magnitude, this routine is given the full lower triangle of R, a
vector defining P ('pmut'), and the first n components of B Q^T
('bqt'). These values are essentially passed verbatim to _qrd_solve().
This routine iterates to estimate par. Usually only a few iterations
are needed, but no more than 10 are performed."""
dwarf = finfo.tiny
n, m = r.shape
x = np.empty_like(bqt)
sdiag = np.empty_like(bqt)
# "Compute and store x in the Gauss-Newton direction. If the
# Jacobian is rank-deficient, obtain a least-squares solution."
nnonsingular = n
wa1 = bqt.copy()
for i in range(n):
if r[i, i] == 0:
nnonsingular = i
wa1[i:] = 0
break
for j in range(nnonsingular - 1, -1, -1):
wa1[j] /= r[j, j]
wa1[:j] -= r[j, :j] * wa1[j]
x[pmut] = wa1
# Initial function evaluation. Check if the Gauss-Newton direction
# was good enough.
wa2 = ddiag * x
dxnorm = enorm(wa2, finfo)
normdiff = dxnorm - delta
if normdiff <= 0.1 * delta:
return 0, x
# If the Jacobian is not rank deficient, the Newton step provides
# a lower bound for the zero of the function.
par_lower = 0.0
if nnonsingular == n:
wa1 = ddiag[pmut] * wa2[pmut] / dxnorm
wa1[0] /= r[0, 0] # "Degenerate case"
for j in range(1, n):
wa1[j] = (wa1[j] - np.dot(wa1[:j], r[j, :j])) / r[j, j]
temp = enorm(wa1, finfo)
par_lower = normdiff / delta / temp**2
# We can always find an upper bound.
for j in range(n):
wa1[j] = np.dot(bqt[: j + 1], r[j, : j + 1]) / ddiag[pmut[j]]
gnorm = enorm(wa1, finfo)
par_upper = gnorm / delta
if par_upper == 0:
par_upper = dwarf / min(delta, 0.1)
# Now iterate our way to victory.
par = np.clip(par0, par_lower, par_upper)
if par == 0:
par = gnorm / dxnorm
itercount = 0
while True:
itercount += 1
if par == 0:
par = max(dwarf, par_upper * 0.001)
temp = np.sqrt(par)
wa1 = temp * ddiag
x = _qrd_solve(r[:, :n], pmut, wa1, bqt, sdiag) # sdiag is an output arg here
wa2 = ddiag * x
dxnorm = enorm(wa2, finfo)
olddiff = normdiff
normdiff = dxnorm - delta
if abs(normdiff) < 0.1 * delta:
break # converged
if par_lower == 0 and normdiff <= olddiff and olddiff < 0:
break # overshot, I guess?
if itercount == 10:
break # this is taking too long
# Compute and apply the Newton correction
wa1 = ddiag[pmut] * wa2[pmut] / dxnorm
for j in range(n - 1):
wa1[j] /= sdiag[j]
wa1[j + 1 : n] -= r[j, j + 1 : n] * wa1[j]
wa1[n - 1] /= sdiag[n - 1] # degenerate case
par_delta = normdiff / delta / enorm(wa1, finfo) ** 2