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