"""
Application class that implements pyFoamFunkyDoCalcFiles.py
"""
from optparse import OptionGroup
from .PyFoamApplication import PyFoamApplication
from PyFoam.Basics.FoamOptionParser import Subcommand
from PyFoam.RunDictionary.ParsedParameterFile import ParsedParameterFile
from PyFoam.Basics.FoamFileGenerator import FoamFileGenerator as gen
from PyFoam.Error import error
from os import path
from collections import OrderedDict
from pprint import pprint,pformat
from PyFoam.ThirdParty.six import print_,iteritems
smallEps=1e-15
[docs]class FunkyDoCalcData:
def __init__(self,d):
self.data=d
def __repr__(self):
return pformat(self.data)
def __binop(self,other,op):
def doOp(a,b):
result={}
for k,va in iteritems(a):
try:
vb=b[k]
try:
result[k]=op(va,vb)
except TypeError:
if hasattr(va,"keys"):
result[k]=doOp(va,vb)
else:
result[k]=[r if abs(r)>smallEps else 0.
for r in [op(aa,bb) for aa,bb in zip(va,vb)]]
except KeyError:
pass
return result
return FunkyDoCalcData(doOp(self.data,other.data))
def __getitem__(self,k):
return self.data[k]
def __sub__(self,other):
return self.__binop(other,lambda x,y:x-y)
def __div__(self,other):
def secureDiv(a,b):
if abs(b)<smallEps:
b=0
try:
return float(a)/b
except ZeroDivisionError:
return float("NaN")
return self.__binop(other,secureDiv)
[docs]class FunkyDoCalcFile:
"""The actual file"""
def __init__(self,fName):
self.__content=ParsedParameterFile(fName,
doMacroExpansion=True,
noHeader=True)
self.fName=fName
rawData=[]
for k,v in iteritems(self.__content["data"]):
rawData.append((v["time"],v))
rawData.sort(key=lambda x:float(x[0]))
self.__data=OrderedDict()
self.entries=None
for k,v in rawData:
self.__data[k]=v
if self.entries is None:
self.entries=OrderedDict()
for k2,v2 in iteritems(v):
if k2=="time":
continue
e=OrderedDict()
for k3,v3 in iteritems(v2):
try:
e[k3]=len(list(v3))
except TypeError:
e[k3]=1
self.entries[k2]=e
else:
for k2,v2 in iteritems(v):
if k2=="time":
continue
val=0
for k3,v3 in iteritems(v2):
try:
val=len(list(v3))
except TypeError:
val=1
if val!=self.entries[k2][k3]:
error("Inconsistency in data for time",k,
": expected",self.entries[k2][k3],"values for",
k2,"/",k3,". Got",val)
[docs] def compare(self,other,digits=5,times=None,additionalDigits=0):
problems=[]
checks=0
if "tolerances" not in self.__content:
tolerances=self.calcTolerances(digits)["tolerances"]
problems.append("No 'tolerances' specified in file {}".format(self.fName))
factor=1
else:
tolerances=self.__content["tolerances"]
from math import pow
factor=pow(10.,-additionalDigits)
if times is None:
times=self.times
usedTimes=set()
span=self.span
amax=self.absmax
for t in times:
aVal=self[t]
if t not in self.times:
problems.append("Specified time {} not in {}. "
"Using {} instead (shift {})".format(t,
self.fName,
aVal["time"],
aVal["time"]-t))
t=aVal["time"]
if t in usedTimes:
problems.append("Time {} was aready used (Duplicate). Skipping".format(t))
continue
usedTimes.add(t)
bVal=other[t]
diffVal=aVal-bVal
if t not in other.times:
problems.append("No exact match for time {} in {}. "
"Using {} instead (difference {})".format(t,
other.fName,
bVal["time"],
diffVal["time"]))
for key,tol in iteritems(tolerances):
for acc,spec in iteritems(tol):
try:
a=aVal[key][acc]
except KeyError:
problems.append("Missin/A: Item {}/{} missing for t={}".format(
key,acc,t))
continue
try:
b=bVal[key][acc]
except KeyError:
problems.append("Missin/B: Item {}/{} missing for t={}".format(
key,acc,t))
continue
d=diffVal[key][acc]
sp=span[key][acc]
am=amax[key][acc]
if len(spec)==1:
a=[a]
b=[b]
d=[d]
sp=[sp]
am=[am]
for i in range(len(spec)):
s=spec[i]
specString="t={} Key: {} Accumulation: {} Component: {}" \
.format(t,key,acc,i)
eps=smallEps
if "smallEps" in s and s["smallEps"] is not None:
eps=float(s["smallEps"])
allowZero=False
if "allowZero" in s and s["allowZero"] is not None:
allowZero=bool(s["allowZero"])
if "abstol" in s and s["abstol"] is not None:
try:
checks+=1
if abs(d[i])>float(s["abstol"])*factor:
problems.append("Abstol {}"
" Difference |{}|>{} (Orig: {} Real: {})"
.format(specString,
d[i],s["abstol"]*factor,
a[i],b[i]))
except ValueError:
problems.append("SpecError: {} - 'abstol' "
"{} is of type {}".format(specString,
s["abstol"],
type(s["abstol"])))
if "reltol" in s and s["reltol"] is not None:
for typ in ["value","amax","span"]:
typName=typ+"Rel"
if typName in s and s[typName]==True:
checks+=1
if typ=="value":
val=0.5*(abs(a[i])+abs(b[i]))
elif typ=="amax":
val=am[i]
elif typ=="span":
val=sp[i]
try:
if abs(val)<eps:
if not allowZero:
problems.append("AlmostZero/{} {}:"
" Reference value for {} is almost zero (eps={}): {}"
.format(typ,specString,typ,eps,val))
continue
relError=d[i]/val
if abs(relError)>float(s["reltol"])*factor:
problems.append("Reltol/{} {}"
" Relative error |{}|>{} (Orig: {} Real: {} Ref: {})"
.format(typ,specString,
relError,s["reltol"]*factor,
a[i],b[i],val))
except ZeroDivisionError:
problems.append("Zero/{}: {} - Value is zero. No relative error".format(typ,specString))
except ValueError:
problems.append("SpecError/{}: {} - 'reltol' "
"{} is of type {}".format(typ,specString,
s["reltol"],
type(s["reltol"])))
return checks,problems
[docs] def calcTolerances(self,digits):
from math import log10,floor,pow
def calcDigits(w):
try:
return int(floor(log10(w)))
except ValueError:
return -digits
tol=OrderedDict()
span=self.span
amax=self.absmax
def tolDict(span,amax):
try:
relspan=float(span)/amax
except ZeroDivisionError:
relspan=None
if span>amax: # Don't calc relative tolerance if min and max have different signs
relspan=None
return { "abstol" : max(pow(10,calcDigits(span)-digits),
pow(10,calcDigits(amax)-digits)),
"reltol" : None if relspan is None else min(pow(10,calcDigits(relspan)),
pow(10,-digits)),
"valueRel" : True,
"spanRel" : True,
"amaxRel" : True,
"span" : span,
"amax" : amax ,
"smallEps" : 1e-15,
"allowZero" : False }
for k,v in iteritems(self.entries):
t=OrderedDict()
for n,nr in iteritems(v):
s=span[k][n]
a=amax[k][n]
if nr==1:
s=[s]
a=[a]
t[n]=[tolDict(*par) for par in zip(s,a)]
tol[k]=t
return {'tolerances':tol}
def __accumulate(self,func):
ranges={}
init=self[self.times[0]]
for k1,v1 in iteritems(init.data):
if k1=="time":
continue
ranges[k1]={}
for k2,v2 in iteritems(v1):
ranges[k1][k2]=v2
for t in self.times[1:]:
val=self[t]
for k1,v1 in iteritems(val.data):
if k1=="time":
continue
for k2,v2 in iteritems(v1):
old=ranges[k1][k2]
if self.entries[k1][k2]==1:
ranges[k1][k2]=func(old,v2)
else:
r=[]
for i in range(self.entries[k1][k2]):
r.append(func(old[i],v2[i]))
ranges[k1][k2]=r
return FunkyDoCalcData(ranges)
@property
def min(self):
return self.__accumulate(min)
@property
def max(self):
return self.__accumulate(max)
@property
def absmax(self):
return self.__accumulate(lambda x,y:max(abs(x),abs(y)))
@property
def span(self):
return self.max-self.min
@property
def spec(self):
return self.__content["spec"]
@property
def expressions(self):
return self.__content["expressions"]
@property
def times(self):
return self.__data.keys()
def __getitem__(self,tm):
nearest=None
for t in self.times:
if nearest is None:
nearest=t
continue
if abs(float(nearest)-float(tm))>abs(float(t)-float(tm)):
nearest=t
return FunkyDoCalcData(self.__data[nearest])
[docs]class FunkyDoCalcFiles(PyFoamApplication):
def __init__(self,
args=None,
**kwargs):
description="""\
This utility helps handling the dictionary files produced by the funkyDoCalc-utility from the swak4Foam-package
"""
PyFoamApplication.__init__(self,
args=args,
description=description,
usage="%prog COMMAND <dict-file>",
changeVersion=False,
examples="""To use this command you must first create a result file with
funkyDoCalc from the swak4Foam-packages. Assume that the result file is called 'checkValues'. The command
%prog info checkValues
gives an overview of the values in that dict that can be compared (including the time-steps). Calls like
%prog max checkValues
%prog span checkValues
print statistics about the data. The data from one timestep can be printed with
%prog get checkValues 0.5
if there is no time 0.5 the nearest time will be used. Similarily the command
%prog difftime checkValues 0.1 0.5
compares the data from two timesteps.
Based on the data the command
%prog defaults checkValues 3
prints an example dictionary with tolerances that can be copy/pasted
into 'checkValues' and edited (change the calues and remove unwanted comparisons).
The tolerances are calculated under the assumption
that you want a precision of 3 digits
The command
%prog diff checkValues ../otherCase/checkValues
compares the results assuming that they were generated with the same
funkyDoCalc-dictionary. If there is a 'tolerances' dictionary in the
first file then this is used. Otherwise the values that the 'defaults' command
genenerates will be used. If results differ by more than the specifies tolerances
an output is done. Otherwise only the number of comparisons is printed
The entries of a tolerances-specification are
abstol: the absolute tolerance for this value. If missing no absolute difference is compared
reltol: the relative tolerance. If missing no relative tolerance is calculated
There are 3 different ways to calculate the relative tolerance depending
on which value the error is calculated relative to: the average of the absolute values,
the maximum of the absolute value over all times or the span over all times. These
can be switched on with the entries valueRel, amaxRel and spanRel respectively""",
subcommands=True,
**kwargs)
[docs] def addOptions(self):
infoCmd=Subcommand(name='info',
help="Information about the dictionary",
aliases=('print',),
nr=1,
exactNr=True)
self.parser.addSubcommand(infoCmd)
minCmd=Subcommand(name='min',
help="Minimum over all the time-steps",
aliases=('minimum',),
nr=1,
exactNr=True)
self.parser.addSubcommand(minCmd)
maxCmd=Subcommand(name='max',
help="Maximum over all the time-steps",
aliases=('maximum',),
nr=1,
exactNr=True)
self.parser.addSubcommand(maxCmd)
maxCmd=Subcommand(name='absmax',
help="Maximum of the absolute values over all the time-steps",
nr=1,
exactNr=True)
self.parser.addSubcommand(maxCmd)
spanCmd=Subcommand(name='span',
help="Span of the values",
aliases=('width',),
nr=1,
exactNr=True)
self.parser.addSubcommand(spanCmd)
rspanCmd=Subcommand(name='relativespan',
help="Relative span of the values (compared to the absolute values)",
aliases=('rspan',),
nr=1,
exactNr=True)
self.parser.addSubcommand(rspanCmd)
getCmd=Subcommand(name='get',
help="Get data for a specific time",
nr=2,
exactNr=True)
self.parser.addSubcommand(getCmd,
usage="%prog COMMAND <dict-file> <time>")
difftimeCmd=Subcommand(name='difftimes',
help="Compare two times",
nr=3,
exactNr=True)
self.parser.addSubcommand(difftimeCmd,
usage="%prog COMMAND <dict-file> <time1> <time2>")
defaultCmd=Subcommand(name='defaults',
help="Calculate a 'tolerances' dictionary to insert into the data. Specify the number of digits you want to be relevant",
nr=2,
exactNr=True)
self.parser.addSubcommand(defaultCmd,
usage="%prog COMMAND <dict-file> <digits>")
diffCmd=Subcommand(name='diff',
help="Compare two dictionaries assuming that they were made with the same specification",
aliases=("compare","diffcases"),
nr=2,
exactNr=True)
self.parser.addSubcommand(diffCmd,
usage="%prog COMMAND <dict-file1> <dict-file2>")
diffCmd.parser.add_option("--digits",
action="store",
dest="nrDigits",
type="int",
default=3,
help="Number of digits that are relevant if no 'tolerances' dictionary is found. Default: 3")
diffCmd.parser.add_option("--additional-digits",
action="store",
dest="addDigits",
type="int",
default=0,
help="Number of digits to add if a 'tolerances' dictionary is found. Default: 0")
for cmd in [diffCmd]:
timeGrp=OptionGroup(cmd.parser,
"Time selection",
"Select time-steps to operate on. These options can be used at the same time and only add time-steps")
timeGrp.add_option("--time",
action="append",
dest="times",
type="float",
default=[],
help="Select a time. Can be used more than once. If unset all times are used")
timeGrp.add_option("--after-time",
dest="afterTime",
default=None,
type="float",
help="Select all times after this time")
timeGrp.add_option("--before-time",
dest="beforeTime",
default=None,
type="float",
help="Select all times before this time")
cmd.parser.add_option_group(timeGrp)
[docs] def run(self):
def selectTimes(data):
if len(self.opts.times)==0 and self.opts.afterTime is None and self.opts.beforeTime is None:
return None
times=self.opts.times[:]
availTimes=data.times
if self.opts.afterTime is not None:
times+=[t for t in availTimes if float(t)>=self.opts.afterTime]
if self.opts.beforeTime is not None:
times+=[t for t in availTimes if float(t)<=self.opts.beforeTime]
times=list(set(times))
times.sort()
return times
args=self.parser.getArgs()
if self.cmdname=="info":
f=FunkyDoCalcFile(args[0])
times=f.times
if len(times)==0:
print_("No times")
else:
if len(times)==1:
print_("Single time:",times[0])
else:
print_(len(times),"times from",times[0],"to",times[-1])
print_("\nValues:")
maxlen=max(len(k) for k in f.entries.keys())
form="%%%ds : " % (maxlen+1)
for k,v in iteritems(f.entries):
print_(form % k,", ".join("%s (%d values)"%i for i in iteritems(v)))
elif self.cmdname=="min":
f=FunkyDoCalcFile(args[0])
pprint(f.min)
elif self.cmdname=="absmax":
f=FunkyDoCalcFile(args[0])
pprint(f.absmax)
elif self.cmdname=="max":
f=FunkyDoCalcFile(args[0])
pprint(f.max)
elif self.cmdname=="span":
f=FunkyDoCalcFile(args[0])
pprint(f.span)
elif self.cmdname=="relativespan":
f=FunkyDoCalcFile(args[0])
pprint(f.span/f.absmax)
elif self.cmdname=="get":
f=FunkyDoCalcFile(args[0])
t=float(args[1])
pprint(f[t])
elif self.cmdname=="defaults":
f=FunkyDoCalcFile(args[0])
d=int(args[1])
print_(gen(f.calcTolerances(d)))
elif self.cmdname=="difftimes":
f=FunkyDoCalcFile(args[0])
t1=float(args[1])
t2=float(args[2])
pprint(f[t1]-f[t2])
# print_(gen(f[t]))
elif self.cmdname=="diff":
f1=FunkyDoCalcFile(args[0])
f2=FunkyDoCalcFile(args[1])
times=selectTimes(f1)
checks,problems=f1.compare(f2,
digits=self.opts.nrDigits,
additionalDigits=self.opts.addDigits,
times=times)
print_("\n".join(problems))
if len(problems)>0:
print_("\n\n {} problems".format(len(problems)))
print_(checks,"Checks done")
else:
self.error("Subcommand",self.cmdname,"not implemeted")