1# -*- coding: utf-8 -*-
2
3from mutatorMath.objects.error import MutatorError
4from mutatorMath.objects.location import Location, sortLocations, biasFromLocations
5
6import sys, warnings
7from operator import itemgetter
8
9
10__all__ = ['Mutator', 'buildMutator']
11
12_EPSILON = sys.float_info.epsilon
13
14
15def noBend(loc): return loc
16
17
18def buildMutator(items, axes=None, bias=None):
19    """
20        Build a mutator with the (location, obj) pairs in items.
21        Determine the bias based on the given locations.
22    """
23    from mutatorMath.objects.bender import Bender
24    m = Mutator()
25    if axes is not None:
26        bender = Bender(axes)
27        m.setBender(bender)
28    else:
29        bender = noBend
30    # the order itself does not matter, but we should always build in the same order.
31    items = sorted(items)
32    if not bias:
33        bias = biasFromLocations([bender(Location(loc)) for loc, obj in items], True)
34    else:
35        # note: this means that the actual bias might be different from the initial value.
36        bias = bender(bias)
37    m.setBias(bias)
38    n = None
39    ofx = []
40    onx = []
41    for loc, obj in items:
42        loc = bender(Location(loc))
43        if (loc-bias).isOrigin():
44            m.setNeutral(obj)
45            break
46    if m.getNeutral() is None:
47        raise MutatorError("Did not find a neutral for this system", m)
48    for loc, obj in items:
49        locbent = bender(Location(loc))
50        #lb = loc-bias
51        lb = locbent-bias
52        if lb.isOrigin(): continue
53        if lb.isOnAxis():
54            onx.append((lb, obj-m.getNeutral()))
55        else:
56            ofx.append((lb, obj-m.getNeutral()))
57    for loc, obj in onx:
58        m.addDelta(loc, obj, punch=False,  axisOnly=True)
59    for loc, obj in ofx:
60        m.addDelta(loc, obj, punch=True,  axisOnly=True)
61    return bias, m
62
63
64class Mutator(dict):
65
66    """
67        Calculator for multi dimensional interpolations.
68
69    ::
70
71        # The mutator needs one neutral object.
72        m = Mutator(myNeutralMathObject)
73
74        # The mutator needs one or more deltas.
75        m.addDelta(Location(pop=1), myMasterMathObject-myNeutralMathObject)
76
77        # The mutator calculates instances at other locations. Remember to inflate.
78        m.getInstance(Location(pop=0.5)) + myNeutralMathObject
79
80    """
81
82    def __init__(self, neutral=None):
83        self._axes = {}
84        self._tags = {}
85        self._bender = noBend
86        self._neutral = neutral
87        self._bias = Location()
88
89    def setBender(self, bender):
90        self._bender = bender
91
92    def setBias(self, bias):
93        self._bias = bias
94
95    def getBias(self):
96        return self._bias
97
98    def setNeutral(self, aMathObject, deltaName="origin"):
99        """Set the neutral object."""
100        self._neutral = aMathObject
101        self.addDelta(Location(), aMathObject-aMathObject, deltaName, punch=False, axisOnly=True)
102
103    def getNeutral(self):
104        """Get the neutral object."""
105        return self._neutral
106
107    def addDelta(self, location, aMathObject, deltaName = None, punch=False,  axisOnly=True):
108        """ Add a delta at this location.
109            *   location:   a Location object
110            *   mathObject: a math-sensitive object
111            *   deltaName: optional string/token
112            *   punch:
113                *   True: add the difference with the instance value at that location and the delta
114                *   False: just add the delta.
115        """
116        #location = self._bender(location)
117        if punch:
118            r = self.getInstance(location, axisOnly=axisOnly)
119            if r is not None:
120                self[location.asTuple()] = aMathObject-r, deltaName
121            else:
122                raise MutatorError("Could not get instance.")
123        else:
124            self[location.asTuple()] = aMathObject, deltaName
125
126    #
127    # info
128    #
129
130    def getAxisNames(self):
131        """
132            Collect a set of axis names from all deltas.
133        """
134        s = {}
135        for l, x in self.items():
136            s.update(dict.fromkeys([k for k, v in l], None))
137        return set(s.keys())
138
139    def _collectAxisPoints(self):
140        """
141            Return a dictionary with all on-axis locations.
142        """
143        for l, (value, deltaName) in self.items():
144            location = Location(l)
145            name = location.isOnAxis()
146            if name is not None and name is not False:
147                if name not in self._axes:
148                    self._axes[name] = []
149                if l not in self._axes[name]:
150                    self._axes[name].append(l)
151        return self._axes
152
153    def _collectOffAxisPoints(self):
154        """
155            Return a dictionary with all off-axis locations.
156        """
157        offAxis = {}
158        for l, (value, deltaName) in self.items():
159            location = Location(l)
160            name = location.isOnAxis()
161            if name is None or name is False:
162                offAxis[l] = 1
163        return list(offAxis.keys())
164
165
166    def collectLocations(self):
167        """
168            Return a dictionary with all objects.
169        """
170        pts = []
171        for l, (value, deltaName) in self.items():
172            pts.append(Location(l))
173        return pts
174
175    def _allLocations(self):
176        """
177            Return a list of all locations of all objects.
178        """
179        l = []
180        for locationTuple in self.keys():
181            l.append(Location(locationTuple))
182        return l
183
184    #
185    #   get instances
186    #
187
188    def getInstance(self, aLocation, axisOnly=False, getFactors=False):
189
190        """ Calculate the delta at aLocation.
191            *   aLocation:  a Location object, expected to be in bent space
192            *   axisOnly:
193                *   True: calculate an instance only with the on-axis masters.
194                *   False: calculate an instance with on-axis and off-axis masters.
195            *   getFactors:
196                *   True: return a list of the calculated factors.
197        """
198        self._collectAxisPoints()
199        factors = self.getFactors(aLocation, axisOnly)
200        total = None
201        for f, item, name in factors:
202            if total is None:
203                total = f * item
204                continue
205            total += f * item
206        if total is None:
207            total = 0 * self._neutral
208        if getFactors:
209            return total, factors
210        return total
211
212    def makeInstance(self, aLocation, bend=True):
213        """
214            Calculate an instance with the right bias and add the neutral.
215            aLocation: expected to be in input space
216        """
217        if bend:
218            aLocation = self._bender(Location(aLocation))
219        if not aLocation.isAmbivalent():
220            instanceObject = self.getInstance(aLocation-self._bias)
221        else:
222            locX, locY = aLocation.split()
223            instanceObject = self.getInstance(locX-self._bias)*(1,0)+self.getInstance(locY-self._bias)*(0,1)
224        return instanceObject+self._neutral
225
226    def getFactors(self, aLocation, axisOnly=False, allFactors=False):
227        """
228            Return a list of all factors and math items at aLocation.
229            factor, mathItem, deltaName
230            all = True: include factors that are zero or near-zero
231        """
232        deltas = []
233        aLocation.expand(self.getAxisNames())
234        limits = getLimits(self._allLocations(), aLocation)
235        for deltaLocationTuple, (mathItem, deltaName) in sorted(self.items()):
236            deltaLocation = Location(deltaLocationTuple)
237            deltaLocation.expand( self.getAxisNames())
238            factor = self._accumulateFactors(aLocation, deltaLocation, limits, axisOnly)
239            if not (factor-_EPSILON < 0 < factor+_EPSILON) or allFactors:
240                # only add non-zero deltas.
241                deltas.append((factor, mathItem, deltaName))
242        deltas = sorted(deltas, key=itemgetter(0), reverse=True)
243        return deltas
244
245    #
246    #   calculate
247    #
248
249    def _accumulateFactors(self, aLocation, deltaLocation, limits, axisOnly):
250        """
251            Calculate the factors of deltaLocation towards aLocation,
252        """
253        relative = []
254        deltaAxis = deltaLocation.isOnAxis()
255        if deltaAxis is None:
256            relative.append(1)
257        elif deltaAxis:
258            deltasOnSameAxis = self._axes.get(deltaAxis, [])
259            d = ((deltaAxis, 0),)
260            if d not in deltasOnSameAxis:
261                deltasOnSameAxis.append(d)
262            if len(deltasOnSameAxis) == 1:
263                relative.append(aLocation[deltaAxis] * deltaLocation[deltaAxis])
264            else:
265                factor =  self._calcOnAxisFactor(aLocation, deltaAxis, deltasOnSameAxis, deltaLocation)
266                relative.append(factor)
267        elif not axisOnly:
268            factor = self._calcOffAxisFactor(aLocation, deltaLocation, limits)
269            relative.append(factor)
270        if not relative:
271            return 0
272        f = None
273        for v in relative:
274            if f is None: f = v
275            else:
276                f *= v
277        return f
278
279    def _calcOnAxisFactor(self, aLocation, deltaAxis, deltasOnSameAxis, deltaLocation):
280        """
281            Calculate the on-axis factors.
282        """
283        if deltaAxis == "origin":
284            f = 0
285            v = 0
286        else:
287            f = aLocation[deltaAxis]
288            v = deltaLocation[deltaAxis]
289        i = []
290        iv = {}
291        for value in deltasOnSameAxis:
292            iv[Location(value)[deltaAxis]]=1
293        i = sorted(iv.keys())
294        r = 0
295        B, M, A = [], [], []
296        mA, mB, mM = None, None, None
297        for value in i:
298            if value < f: B.append(value)
299            elif value > f: A.append(value)
300            else: M.append(value)
301        if len(B) > 0:
302            mB = max(B)
303            B.sort()
304        if len(A) > 0:
305            mA = min(A)
306            A.sort()
307        if len(M) > 0:
308            mM = min(M)
309            M.sort()
310        if mM is not None:
311            if ((f-_EPSILON <  v) and (f+_EPSILON > v)) or f==v: r = 1
312            else: r = 0
313        elif mB is not None and mA is not None:
314            if v < mB or v > mA: r = 0
315            else:
316                if v == mA:
317                    r = float(f-mB)/(mA-mB)
318                else:
319                    r = float(f-mA)/(mB-mA)
320        elif mB is None and mA is not None:
321            if v==A[1]:
322                r = float(f-A[0])/(A[1]-A[0])
323            elif v == A[0]:
324                r = float(f-A[1])/(A[0]-A[1])
325            else:
326                r = 0
327        elif  mB is not None and mA is None:
328            if v == B[-2]:
329                r = float(f-B[-1])/(B[-2]-B[-1])
330            elif v == mB:
331                r = float(f-B[-2])/(B[-1]-B[-2])
332            else:
333                r = 0
334        return r
335
336    def _calcOffAxisFactor(self, aLocation, deltaLocation, limits):
337        """
338            Calculate the off-axis factors.
339        """
340        relative = []
341        for dim in limits.keys():
342            f = aLocation[dim]
343            v = deltaLocation[dim]
344            mB, M, mA = limits[dim]
345            r = 0
346            if mA is not None and v > mA:
347                relative.append(0)
348                continue
349            elif mB is not None and v < mB:
350                relative.append(0)
351                continue
352            if f < v-_EPSILON:
353                if mB is None:
354                    if M is not None and mA is not None:
355                        if v == M:
356                            r = (float(max(f,mA)-min(f, mA))/float(max(M,mA)-min(M, mA)))
357                        else:
358                            r = -(float(max(f,mA)-min(f, mA))/float(max(M,mA)-min(M, mA)) -1)
359                    else: r = 0
360                elif mA is None: r = 0
361                else: r = float(f-mB)/(mA-mB)
362            elif f > v+_EPSILON:
363                if mB is None: r = 0
364                elif mA is None:
365                    if M is not None and mB is not None:
366                        if v == M:
367                            r = (float(max(f,mB)-min(f, mB))/(max(mB, M)-min(mB, M)))
368                        else:
369                            r = -(float(max(f,mB)-min(f, mB))/(max(mB, M)-min(mB, M)) - 1)
370                    else: r = 0
371                else: r = float(mA-f)/(mA-mB)
372            else: r = 1
373            relative.append(r)
374        f = 1
375        for i in relative:
376            f *= i
377        return f
378
379
380def getLimits(locations, current, sortResults=True, verbose=False):
381    """
382        Find the projections for each delta in the list of locations, relative to the current location.
383        Return only the dimensions that are relevant for current.
384    """
385    limit = {}
386    for l in locations:
387        a, b = current.common(l)
388        if a is None:
389            continue
390        for name, value in b.items():
391            f = a[name]
392            if name not in limit:
393                limit[name] = {}
394                limit[name]['<'] = {}
395                limit[name]['='] = {}
396                limit[name]['>'] = {}
397                if f > 0:
398                    limit[name]['>'] = {0: [Location()]}
399                elif f<0:
400                    limit[name]['<'] = {0: [Location()]}
401                else:
402                    limit[name]['='] = {0: [Location()]}
403            if current[name] < value - _EPSILON:
404                if value not in limit[name]["<"]:
405                    limit[name]["<"][value] = []
406                limit[name]["<"][value].append(l)
407            elif current[name] > value + _EPSILON:
408                if value not in limit[name][">"]:
409                    limit[name][">"][value] = []
410                limit[name][">"][value].append(l)
411            else:
412                if value not in limit[name]["="]:
413                    limit[name]["="][value] = []
414                limit[name]["="][value].append(l)
415    if not sortResults:
416        return limit
417    # now we have all the data, let's sort to the relevant values
418    l = {}
419    for name, lims in  limit.items():
420        less = []
421        more = []
422        if lims[">"].keys():
423            less = sorted(lims[">"].keys())
424            lim_min = less[-1]
425        else:
426            lim_min = None
427        if lims["<"].keys():
428            more = sorted(lims["<"].keys())
429            lim_max = more[0]
430        else:
431            lim_max = None
432        if lim_min is None and lim_max is not None:
433            # extrapolation < min
434            if len(limit[name]['='])>0:
435                l[name] = (None, list(limit[name]['='].keys())[0], None)
436            elif len(more) > 1 and len(limit[name]['='])==0:
437                # extrapolation
438                l[name] = (None,  more[0], more[1])
439        elif lim_min is not None and lim_max is None:
440            # extrapolation < max
441            if len(limit[name]['='])>0:
442                # less > 0, M > 0, more = None
443                # -> end of a limit
444                l[name] = (None, limit[name]['='], None)
445            elif len(less) > 1 and len(limit[name]['='])==0:
446                # less > 0, M = None, more = None
447                # extrapolation
448                l[name] = (less[-2], less[-1], None)
449        else:
450            if len(limit[name]['=']) > 0:
451                l[name] = (None, list(limit[name]['='].keys())[0], None)
452            else:
453                l[name] = (lim_min,  None, lim_max)
454    return l
455
456
457if __name__ == "__main__":
458    import doctest
459    sys.exit(doctest.testmod().failed)
460