1 """
2 Framework for performing a Proper Orthogonal Decomposition (POD).
3
4 Useful references:
5 - http://en.wikipedia.org/wiki/Homogeneous_coordinates
6 - http://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations
7
8 Usage example:
9
10 >>> import pod
11 >>> refs = [ [-4.0, -1.0],
12 >>> [-2.0, -1.0],
13 >>> [3.0, 4.0] ]
14 >>> target = [-2.0, 1.5]
15 >>> decomposition = pod.decompose(target, refs, epsilon=1E-6, max_iter=90)
16 >>> print decomposition.get_decomposition()
17 [-1.9999991745134178, 1.4999993808850638]
18 >>> print decomposition.get_reference_weights()
19 [0.96153806466991254, 0.0, 0.61538436138874408]
20
21 The example above shows the reconstruction of the target using 3 reference
22 signals, from which only reference 1 and reference 3 are useful (reference 2
23 is assigned a weight of 0).
24
25 @author: Christophe Alexandre <ch.alexandre at bluewin dot ch>
26
27 @license:
28 Copyright(C) 2010 Christophe Alexandre
29
30 This program is free software: you can redistribute it and/or modify
31 it under the terms of the GNU Lesser General Public License as published by
32 the Free Software Foundation, either version 3 of the License, or
33 (at your option) any later version.
34
35 This program is distributed in the hope that it will be useful,
36 but WITHOUT ANY WARRANTY; without even the implied warranty of
37 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38 GNU General Public License for more details.
39
40 You should have received a copy of the GNU Lesser General Public License
41 along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>.
42 """
43 __all__ = ['decompose', 'combined_distance', 'DecompositionBasic']
44
45 import os
46 import logging
47
48 import vecspace
49 from util import NullHandler
50
51 _h = NullHandler()
52 _logger = logging.getLogger('pod')
53 _logger.addHandler(_h)
54
56 """
57 Distance function used for ordering the projections.
58
59 A weight of 0.0 defines the distance to the projected point, while a weight
60 of 1.0 defines the distance relative to the point generating the line.
61
62 At each iteration step the current point is projected onto the closest of
63 all lines.
64
65 @param generator_weight: how much weight is assigned to the generator point
66 @type generator_weight: float usually in range [0.0, 1.0]
67 """
68
69 def func(start_point, projected_point,
70 generator_point, w=generator_weight):
71 d1 = start_point.sub(generator_point).norm()
72 d2 = start_point.sub(projected_point).norm()
73 return w * d1 + (1.0 - w) * d2
74
75 return func
76
77 -def decompose(source, references,
78 epsilon=1E-10, max_iter=20,
79 max_factors=None, max_weight=None,
80 distance=combined_distance(0.0)
81 ):
82 """
83 Decomposing the source using the proposed reference points.
84
85 @param source: input point
86 @type source: list
87 @param references: list of reference points
88 @type references: list
89 @param epsilon: limit of the error sequence for stopping iteration
90 @type epsilon: float
91 @param max_iter: safeguard for stopping iteration
92 @type max_iter: int
93 @param max_factors: limit for the number of reference points, None allowing to use all of them
94 @type max_factors: int
95 @param distance: function used for finding the closest line to project on
96 @type distance: a function of start point, projected point, generator point
97 @return: decomposition details
98 @rtype: IterativeDecomposition
99 """
100 r = BaseDecomposition(references, epsilon, max_iter, max_factors, max_weight)
101 r.resolve(source)
102 return r
103
105 """
106 Decomposition interface definition.
107 """
108
109 - def __init__(self, references, epsilon=1E-10, max_iter=20, max_factors=None):
110 assert len(references) > 0, 'at least one reference is required'
111 dim = len(references[0])
112 for r in references:
113 assert len(r) == dim
114
115 self._vector_space = vecspace.VectorSpace(dim)
116 self._reference_points = []
117 self._ignores = []
118 for count, r in enumerate(references):
119 ref = self._vector_space.define_point(*r)
120 if ref in self._reference_points:
121 logging.warning('filtered out redundant reference %d' % count)
122 self._ignores.append(ref)
123
124 elif ref.norm() == 0.0:
125 logging.warning('filtered out reference at origin %d' % count)
126 self._ignores.append(ref)
127
128 self._reference_points.append(ref)
129
130 self._weights = dict()
131 for p in self._reference_points:
132 self._weights[p] = 0.0
133
134 self._epsilon = epsilon
135 self._start = None
136 self._max_iter = max_iter
137 if max_factors is None:
138 self._max_factors = len(self._reference_points)
139
140 else:
141 self._max_factors = max_factors
142
143 self._error_norm = None
144
146 """
147 Computes current decomposition result on the fly.
148
149 @return: the current relicate
150 @rtype: linalg.Point
151 """
152 decomposition = self._vector_space.origin
153 for d in self._weights.keys():
154 w_d = self._weights[d]
155 decomposition = decomposition.add(d.scale(w_d))
156 return decomposition
157
159 """
160 Iterates decomposition until convergence or max iteration is reached.
161
162 @param point: coordinates of the point to be decompositiond
163 @type point: list
164 @return: coordinates of decompositiond point
165 @rtype: list
166 """
167 pass
168
170 """
171 Returns the weight assigned to the reference provided in the constructor
172 at the indicated position.
173
174 @param position: position of the reference in the list provided in the constructor
175 @type position: int
176 """
177 ref = self._reference_points[position]
178 return self._weights[ref]
179
181 """
182 Returns the weights assigned to the references in order to construct the
183 proposed input.
184 """
185 return [self._weights[self._reference_points[i]]
186 for i in range(len(self._reference_points))]
187
189 """
190 Returns the result of the decomposition process.
191
192 @return: decomposition result
193 @rtype: list
194 """
195 return self._compute_decomposition().get_data()
196
198 """
199 Returns a measure of the decomposition error.
200
201 @return: length of the difference between the result and the initial point
202 @rtype: float
203 """
204 return self._compute_decomposition().sub(self._start).norm()
205
207 """
208 Returns the rank-th reference influencing the input variable
209 (main component: rank = 0), multiplied by its assigned weight.
210
211 @param rank: the rank of the reference (0 means principal component)
212 @return: a reference vector
213 """
214 ref_weights = self.get_reference_weights()
215 sorted_weights = [(pos - 1, weight)
216 for pos, weight in enumerate(ref_weights)]
217 sorted_weights.sort(lambda w1, w2: cmp(abs(w2[1]), abs(w1[1])))
218 max_abs_weight_pos = sorted_weights[rank][0]
219 weight = sorted_weights[rank][1]
220 main_component = self._reference_points[max_abs_weight_pos].scale(weight)
221 return main_component.get_data()
222
224 """
225 Returns the position in the initial reference list of the rank-th
226 reference influencing the input variable (main component: rank = 0).
227
228 @param rank: the rank of the reference (0 means principal component)
229 @return: position in the initial reference list
230 """
231 ref_weights = self.get_reference_weights()
232 sorted_weights = [(pos - 1, weight)
233 for pos, weight in enumerate(ref_weights)]
234 sorted_weights.sort(lambda w1, w2: cmp(abs(w2[1]), abs(w1[1])))
235 max_abs_weight_pos = sorted_weights[rank][0]
236 return max_abs_weight_pos
237
239 out = 'reference points:' + os.linesep
240 for p in self._reference_points:
241 out += str(p) + os.linesep
242 out += 'weightings:' + os.linesep
243 out += str(self._weights)
244 return out
245
247 """
248 Decomposition on a set of reference points.
249 """
250
251 - def __init__(self, references,
252 epsilon=1E-10,
253 max_iter=20,
254 max_factors=None,
255 max_weight=None,
256 distance=combined_distance(0.0)):
257 """
258 @param distance: function of start point, projected point and generator point
259 """
260 IterativeDecomposition.__init__(self, references, epsilon, max_iter, max_factors)
261 self._max_weight = max_weight
262 self._distance = distance
263
265 """ Projects onto the closest of the straight lines defined by
266 the reference points.
267 """
268
269 origin = self._vector_space.origin
270 if point.sub(origin).norm() <= self._epsilon:
271
272 return point
273
274
275 projections = dict()
276 distances = dict()
277 for ref in reference_points:
278
279 line = self._vector_space.define_line(ref, origin)
280
281 ref_proj = line.project(point)
282 projections[ref] = ref_proj.projected
283 distances[ref] = self._distance(point, ref_proj.projected, ref)
284 _logger.debug('distance to reference %.3f' % distances[ref])
285
286 ref_points = []
287 for p in reference_points:
288 if self._max_weight is None:
289 ref_points.append(p)
290
291 else:
292 additional_weight = projections[p].units(p)
293 suggested_weight = self._weights[p] + additional_weight
294 if abs(suggested_weight) < self._max_weight:
295 ref_points.append(p)
296
297 if len(ref_points) == 0:
298
299 return point
300
301
302 def by_dist(ref1, ref2, d=distances):
303 return cmp(d[ref1], d[ref2])
304
305 ref_points.sort(by_dist)
306
307 closest = ref_points[0]
308 additional_weight = projections[closest].units(closest)
309 self._weights[closest] += additional_weight
310 _logger.debug('closest driver: %s, weight=%f' % (str(closest), self._weights[closest]))
311
312 return point.sub(projections[closest])
313
315 """
316 Iterates decomposition until convergence or max iteration is reached.
317
318 @param point: coordinates of the point to be decompositiond
319 @type point: list
320 @return: coordinates of decompositiond point
321 @rtype: list
322 """
323 IterativeDecomposition.resolve(self, point)
324 _logger.debug(' ------------- STARTING PROCESS -------------')
325 target = self._vector_space.define_point(*point)
326 self._start = target
327 reference_points = [ref for ref in self._reference_points if ref not in self._ignores]
328 projector = self._project_point(target, reference_points)
329 diff = None
330 _logger.debug('distance to projection: %f' % projector.norm())
331
332 i = 0
333 while (diff is None) or (diff > self._epsilon and i < self._max_iter):
334 i = i + 1
335 previous = self._compute_decomposition()
336 _logger.debug(' ------------- ITERATION %d -------------' % i)
337 projector = self._project_point(projector, reference_points)
338 enabled_drivers = [p for p in self._weights.keys() if abs(self._weights[p]) > 0.0]
339 drivers_count = len(enabled_drivers)
340 _logger.debug('number of drivers: %d' % drivers_count)
341 if drivers_count >= self._max_factors:
342
343 _logger.debug('count limit reached for drivers: %d' % self._max_factors)
344 reference_points = enabled_drivers
345
346 decomposition = self._compute_decomposition()
347 diff = decomposition.sub(previous).norm()
348 _logger.debug('improvement: %f' % diff)
349 _logger.debug('distance to projection: %f' % projector.norm())
350
351 _logger.debug('decomposition: ' + str(decomposition))
352
353 _logger.debug('start:' + str(target))
354 _logger.debug('diff:' + str(decomposition.sub(target)))
355 return decomposition.get_data()
356
358
359 - def __init__(self, ref1, ref2, max_iter):
360 BaseDecomposition.__init__(self, [ref1, ref2], max_iter=max_iter)
361