-
-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathPMGeneralFunctionFit.class.st
More file actions
312 lines (275 loc) · 10.5 KB
/
Copy pathPMGeneralFunctionFit.class.st
File metadata and controls
312 lines (275 loc) · 10.5 KB
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
"
GeneralFunctionFit fits a function to some data. the data has to be a collection of points x@f(x). the fitting minimizes by default the squared error, but it can also do more robust regressions. GeneralFunctionFit can also deal with local minima. if you have a function with many parameters you might want to enlarge the populationSize and/or the maximumIterations. if the result is not exact enough, it is faster and often sufficient to rerun #evaluate.
f:=[:x :a :b |x squared sin squared -b/ (a+x squared)].
col:=(-2.5 to: 2.5 by:0.25)collect: [:i|i@(f cull: i cull: 2 cull: 3 )].
fit:= GeneralFunctionFit function: f data: col minimumValues: #(0 0) maximumValues: #(10 10) .
fit evaluate .
fit result . --> #(2.0000000000000413 3.000000000000014)
"
Class {
#name : #PMGeneralFunctionFit,
#superclass : #Object,
#instVars : [
'go',
'manager',
'errorFunction',
'result',
'firstResult',
'verbose',
'data',
'dataTruncated'
],
#category : 'Math-FunctionFit'
}
{ #category : #creation }
PMGeneralFunctionFit class >> function: aBlock data: aCollectionOfPoints minimumValues: anArray maximumValues: anotherArray [
(aBlock isBlock and:[aBlock numArgs > 1]) ifFalse:
[^self error: 'aBlock must be a Block with one independent variable and at least one parameter' ].
^self new
function: aBlock;
data: (aCollectionOfPoints as: PMDataHolder);
minValues: anArray maxValues: anotherArray;
yourself
]
{ #category : #util }
PMGeneralFunctionFit class >> range: anArray size: anInteger type: string [
^anArray isNumber
ifTrue: [Array new: anInteger withAll: anArray ]
ifFalse: [anArray isCollection
ifTrue: [anArray size=anInteger
ifTrue: [anArray]
ifFalse: [self error: 'minimum or maximum values dont have correct sizes'] ]
ifFalse: [self error: (string, ' values is no collection or number')]].
]
{ #category : #accessing }
PMGeneralFunctionFit >> data: aCollectionOfPoints [
"the data, a collection of x@f(x), used to estimate the parameters"
self resetResult .
data :=aCollectionOfPoints.
dataTruncated:=false.
^errorFunction data: aCollectionOfPoints .
]
{ #category : #accessing }
PMGeneralFunctionFit >> dataTruncated [
^dataTruncated
]
{ #category : #accessing }
PMGeneralFunctionFit >> error [
"returns the sqrt of the mean of the sum of squared errors, or the mean abs error, or the quartile error, or the insensitive error"
result ifNil: [^nil].
^self error: result
]
{ #category : #accessing }
PMGeneralFunctionFit >> error: parameters [
"returns the error for any parameter array; for example if you want to compare the error of the result with the error of the 'real' parameters"
^errorFunction realValue: parameters .
]
{ #category : #accessing }
PMGeneralFunctionFit >> errorCollection [
^ result
ifNil: [ Error signal: 'run "self evaluate" first' ]
ifNotNil: [ :r | self optimizer functionBlock errorCollection: r ]
]
{ #category : #accessing }
PMGeneralFunctionFit >> errorType [
^errorFunction errorType
]
{ #category : #accessing }
PMGeneralFunctionFit >> errorType: aSymbol [
"defines what kind of fitting should be used. valid errorTypes are: #squared, #abs, #insensitive, #quartile, #median. Default is #squared"
self resetResult.
^errorFunction errorType: aSymbol
]
{ #category : #operation }
PMGeneralFunctionFit >> evaluate [
|ff|
ff :=PMErrorMinimizer function: errorFunction.
firstResult :=[ff evaluate .ff parameters]
ifError: [:err :rcvr | verbose ifTrue: [self inform: 'ErrorMinimizer was not successful']. nil].
firstResult ifNotNil: [go addPointAt: firstResult] .
firstResult := go evaluate .
self errorType =#squared
ifTrue: [ff :=PMFunctionFit function: errorFunction function data: errorFunction data ].
ff parameters: firstResult.
ff desiredPrecision: PMFloatingPointMachine new machinePrecision.
ff maximumIterations: 1000 .
result:=[ff evaluate .ff parameters]
ifError: [:err :rcvr |
verbose ifTrue: [self inform: 'last FunctionFit was not successful'].
ff result parameters].
((errorFunction value: result) > (errorFunction value: firstResult)) ifTrue:[
ff:=result.
result :=firstResult.
firstResult :=ff.
verbose ifTrue: [self inform: 'first result was better than final result' ] ].
^result
]
{ #category : #operation }
PMGeneralFunctionFit >> findQuartile [
| errorCol d2 max v n|
(#(#quartile #insensitive) includes: self errorType ) ifFalse:
[^ Error signal: 'errorType has to be set to #quartile or #insensitive' ].
dataTruncated ifTrue: [errorFunction data: data. dataTruncated :=false]. "necessary"
(data size - errorFunction parameterSize)<7 ifTrue: [^ Error signal: 'datasize to small'].
max:= 0.999.
v:=verbose .
verbose:=false.
n:=0.
[ self quartile: max.
self evaluate.
errorCol:= self errorCollection sort.
d2:=(errorFunction parameterSize to: (errorCol size -4) ) collect: [:i| {i+2.
(2*(errorCol at: i)) - (errorCol at: i+1) - (2*(errorCol at: i+2))- (errorCol at: i+3)+(2*(errorCol at: i+4))} ].
[max :=#(5 0).
d2 do: [:d|((d at: 2) >= (max at:2)) ifTrue: [max :=d]].
(max first = (d2 last first))ifTrue: [
(d2 size =1)ifTrue: [^ Error signal: 'findQuartile unsolvable'].
d2 := (d2 copyFrom: 1 to:(d2 size -1)).
true ] ifFalse:[false] .] whileTrue.
max :=(max first / errorCol size) .
n:=n+1.
((max <self quartile ) ifTrue: [true] ifFalse: [(max =self quartile)ifTrue: [false] ifFalse: [n<5] ])
ifTrue: [v ifTrue: [self inform: 'found quartile: ' , max asString ].true] ifFalse:[false] .
] whileTrue.
verbose :=v.
^self evaluate
]
{ #category : #accessing }
PMGeneralFunctionFit >> function [
"does usually not return the originally entered block, but a SimpleParameterFunction with the parameters set to the result"
|f|
f :=errorFunction function .
^result ifNil: [f] ifNotNil: [PMSimpleParameterFunction function: f parameters: result]
]
{ #category : #accessing }
PMGeneralFunctionFit >> function: aBlock [
"sets the function to be fitted. the first value has to be the independent variable, further values are the parameters to be fitted. only one independent variable is allowed (if you have several independent vars or eg a function of a vector, that returns another vector, have a look at the 'spiral' example)."
self resetResult.
errorFunction function: aBlock .
]
{ #category : #initialization }
PMGeneralFunctionFit >> initialize [
super initialize .
errorFunction :=PMErrorOfParameterFunction new.
manager :=PMAnotherChromosomeManager new.
manager populationSize: 50.
go :=PMAnotherGeneticOptimizer minimizingFunction: errorFunction .
go chromosomeManager: manager.
go maximumIterations: 170.
self errorType: #squared.
verbose:=false.
dataTruncated :=false.
]
{ #category : #accessing }
PMGeneralFunctionFit >> manager [
"if you want to change parameters of AnotherChromosomeManager, that are not directly available in GeneralFunctionFit"
^manager
]
{ #category : #accessing }
PMGeneralFunctionFit >> maximumIterations: anInteger [
"default is 170."
go maximumIterations: anInteger.
^anInteger
]
{ #category : #accessing }
PMGeneralFunctionFit >> minValues: anArray maxValues: anotherArray [
"instead of an array one can also use a number, an array will then be filled with this number. eg one can use 0 instead of the tiresome #(0 0 0 0). minValues and maxValues need not be absolutely correct. also results outside of this range, but nearby, will often be found."
|array1 array2 aSize|
aSize := errorFunction function numArgs-1.
array1 := self class range: anArray size: aSize type: 'minimum'.
array2 :=self class range: anotherArray size: aSize type: 'maximum'.
(array2 - array1 allSatisfy: [:each| each >0]) ifFalse:
[^DomainError signal: 'maximum values should be greater than minimum values'].
self resetResult.
manager origin: array1.
manager range: (array2 - array1).
]
{ #category : #accessing }
PMGeneralFunctionFit >> optimizer [
"if you want to change parameters of AnotherGeneticOptimizer not directly available in GeneralFunctionFit"
^go
]
{ #category : #accessing }
PMGeneralFunctionFit >> populationSize: anInteger [
" if you have a lot of parameters (essentially beginning with 3 parameters), you may want to enlarge the populationsize. default is only 50. eg for 3 parameters, you might want set this to 300"
self resetResult.
manager populationSize: anInteger .
^anInteger
]
{ #category : #accessing }
PMGeneralFunctionFit >> precision [
"not really useful to look at, you might better look at #error"
^go precision
]
{ #category : #printing }
PMGeneralFunctionFit >> printOn: aStream [
aStream
nextPutAll: 'a ';
nextPutAll: self class name;
nextPut: $(;
print: go;
nextPutAll: ' with data of size: ';
print: data size .
dataTruncated ifTrue:
[aStream nextPutAll: ' truncated to: '; print: self optimizer functionBlock data size] .
aStream nextPut: $).
]
{ #category : #accessing }
PMGeneralFunctionFit >> quartile [
^errorFunction quartile.
]
{ #category : #accessing }
PMGeneralFunctionFit >> quartile: aFloat [
"quartile: is used by errortypes #quartile and #insensitive. if you set quartile to 1, it does not make much sense to use #insensitive, since #insensitive is much slower than #quartile and calculating the tube radius is not quite correct for #insensitive in this special case (it will leave out one datapoint )."
self resetResult.
^errorFunction quartile: aFloat.
]
{ #category : #accessing }
PMGeneralFunctionFit >> relativeError: aBoolean [
"by default false"
self resetResult .
^errorFunction relativeError: aBoolean .
]
{ #category : #operation }
PMGeneralFunctionFit >> resetBestPoints [
"for repeated evaluating with different starting populations"
self resetResult .
]
{ #category : #operation }
PMGeneralFunctionFit >> resetData [
self data: data.
]
{ #category : #private }
PMGeneralFunctionFit >> resetResult [
result :=nil.
firstResult :=nil.
go resetBestPoints .
]
{ #category : #accessing }
PMGeneralFunctionFit >> result [
"the fitted parameters"
result ifNil: [self evaluate ].
^result
]
{ #category : #accessing }
PMGeneralFunctionFit >> secondaryResult [
"not really necessary"
^firstResult
]
{ #category : #operation }
PMGeneralFunctionFit >> truncateData [
|ec ei |
dataTruncated ifTrue: [^Error signal:'data is already truncated'].
ec:=self errorCollection .
ei:=(ec withIndexCollect: [:e :i|{e.i}]).
ei:=(ei sort:[:a :b|a first<b first]) collect: [:e|e second ].
ei :=ei copyFrom: 1 to: (self quartile * ec size) truncated.
self resetResult.
dataTruncated:=true.
^self optimizer functionBlock data: (ei collect: [:i|self optimizer functionBlock data at: i]).
]
{ #category : #accessing }
PMGeneralFunctionFit >> verbose: aBoolean [
" by default false. if set it to true, you will get some tiresome messages"
verbose :=aBoolean
]