1 |
|
# |
2 |
# $Id$ |
# $Id$ |
3 |
|
# |
4 |
|
####################################################### |
5 |
|
# |
6 |
|
# Copyright 2003-2007 by ACceSS MNRF |
7 |
|
# Copyright 2007 by University of Queensland |
8 |
|
# |
9 |
|
# http://esscc.uq.edu.au |
10 |
|
# Primary Business: Queensland, Australia |
11 |
|
# Licensed under the Open Software License version 3.0 |
12 |
|
# http://www.opensource.org/licenses/osl-3.0.php |
13 |
|
# |
14 |
|
####################################################### |
15 |
|
# |
16 |
|
|
17 |
""" |
""" |
18 |
Environment for implementing models in escript |
Environment for implementing models in escript |
41 |
import numarray |
import numarray |
42 |
import operator |
import operator |
43 |
import itertools |
import itertools |
44 |
|
import time |
45 |
|
import os |
46 |
|
|
47 |
# import the 'set' module if it's not defined (python2.3/2.4 difference) |
# import the 'set' module if it's not defined (python2.3/2.4 difference) |
48 |
try: |
try: |
87 |
self.__dom = minidom.parseString(xml) |
self.__dom = minidom.parseString(xml) |
88 |
self.__linkable_object_registry= {} |
self.__linkable_object_registry= {} |
89 |
self.__link_registry= [] |
self.__link_registry= [] |
90 |
self.__esys=self.__dom.firstChild |
self.__esys=self.__dom.getElementsByTagName('ESys')[0] |
91 |
self.debug=debug |
self.debug=debug |
92 |
|
|
93 |
def getClassPath(self, node): |
def getClassPath(self, node): |
230 |
self.attribute = None |
self.attribute = None |
231 |
self.setAttributeName(attribute) |
self.setAttributeName(attribute) |
232 |
|
|
233 |
|
def getTarget(self): |
234 |
|
""" |
235 |
|
returns the target |
236 |
|
""" |
237 |
|
return self.target |
238 |
def getAttributeName(self): |
def getAttributeName(self): |
239 |
""" |
""" |
240 |
returns the name of the attribute the link is pointing to |
returns the name of the attribute the link is pointing to |
483 |
setattr(self,prm,value) |
setattr(self,prm,value) |
484 |
self.parameters.add(prm) |
self.parameters.add(prm) |
485 |
|
|
|
self.trace("parameter %s has been declared."%prm) |
|
|
|
|
486 |
def releaseParameters(self,name): |
def releaseParameters(self,name): |
487 |
""" |
""" |
488 |
Removes parameter name from the paramameters. |
Removes parameter name from the paramameters. |
490 |
if self.isParameter(name): |
if self.isParameter(name): |
491 |
self.parameters.remove(name) |
self.parameters.remove(name) |
492 |
self.trace("parameter %s has been removed."%name) |
self.trace("parameter %s has been removed."%name) |
493 |
|
|
494 |
|
def checkLinkTargets(self, models, hash): |
495 |
|
""" |
496 |
|
returns a set of tuples ("<self>(<name>)", <target model>) if the parameter <name> is linked to model <target model> |
497 |
|
but <target model> is not in the list models. If the a parameter is linked to another parameter set which is not in the hash list |
498 |
|
the parameter set is checked for its models. hash gives the call history. |
499 |
|
""" |
500 |
|
out=set() |
501 |
|
for name, value in self: |
502 |
|
if isinstance(value, Link): |
503 |
|
m=value.getTarget() |
504 |
|
if isinstance(m, Model): |
505 |
|
if not m in models: out.add( (str(self)+"("+name+")",m) ) |
506 |
|
elif isinstance(m, ParameterSet) and not m in hash: |
507 |
|
out|=set( [ (str(self)+"("+name+")."+f[0],f[1]) for f in m.checkLinkTargets(models, hash+[ self ] ) ] ) |
508 |
|
return out |
509 |
|
|
510 |
def __iter__(self): |
def __iter__(self): |
511 |
""" |
""" |
557 |
for i in value: |
for i in value: |
558 |
if isinstance(i, bool): |
if isinstance(i, bool): |
559 |
elem_type = max(elem_type,0) |
elem_type = max(elem_type,0) |
560 |
if isinstance(i, int) and not isinstance(i, bool): |
elif isinstance(i, int): |
561 |
elem_type = max(elem_type,1) |
elem_type = max(elem_type,1) |
562 |
if isinstance(i, float): |
elif isinstance(i, float): |
563 |
elem_type = max(elem_type,2) |
elem_type = max(elem_type,2) |
564 |
if elem_type == 0: value = numarray.array(value,numarray.Bool) |
if elem_type == 0: value = numarray.array(value,numarray.Bool) |
565 |
if elem_type == 1: value = numarray.array(value,numarray.Int) |
if elem_type == 1: value = numarray.array(value,numarray.Int) |
617 |
dic.appendChild(i) |
dic.appendChild(i) |
618 |
param.appendChild(dic) |
param.appendChild(dic) |
619 |
else: |
else: |
|
print value |
|
620 |
raise ValueError("cannot serialize %s type to XML."%str(value.__class__)) |
raise ValueError("cannot serialize %s type to XML."%str(value.__class__)) |
621 |
|
|
622 |
node.appendChild(param) |
node.appendChild(param) |
705 |
parameters[pname] = pvalue |
parameters[pname] = pvalue |
706 |
|
|
707 |
# Create the instance of ParameterSet |
# Create the instance of ParameterSet |
708 |
o = cls(debug=esysxml.debug) |
try: |
709 |
|
o = cls(debug=esysxml.debug) |
710 |
|
except TypeError, inst: |
711 |
|
print inst.args[0] |
712 |
|
if inst.args[0]=="__init__() got an unexpected keyword argument 'debug'": |
713 |
|
raise TypeError("The Model class %s __init__ needs to have argument 'debug'.") |
714 |
|
else: |
715 |
|
raise inst |
716 |
o.declareParameters(parameters) |
o.declareParameters(parameters) |
717 |
esysxml.registerLinkableObject(o, node) |
esysxml.registerLinkableObject(o, node) |
718 |
return o |
return o |
765 |
return "<%s %d>"%(self.__class__.__name__,id(self)) |
return "<%s %d>"%(self.__class__.__name__,id(self)) |
766 |
|
|
767 |
|
|
768 |
|
def setUp(self): |
769 |
|
""" |
770 |
|
Sets up the model. |
771 |
|
|
772 |
|
This function may be overwritten. |
773 |
|
""" |
774 |
|
pass |
775 |
|
|
776 |
def doInitialization(self): |
def doInitialization(self): |
777 |
""" |
""" |
778 |
Initializes the time stepping scheme. |
Initializes the time stepping scheme. This method is not called in case of a restart. |
779 |
|
|
780 |
This function may be overwritten. |
This function may be overwritten. |
781 |
""" |
""" |
782 |
pass |
pass |
783 |
def doInitialStep(self): |
def doInitialStep(self): |
784 |
""" |
""" |
785 |
performs an iteration step in the initialization phase |
performs an iteration step in the initialization phase. This method is not called in case of a restart. |
786 |
|
|
787 |
This function may be overwritten. |
This function may be overwritten. |
788 |
""" |
""" |
790 |
|
|
791 |
def terminateInitialIteration(self): |
def terminateInitialIteration(self): |
792 |
""" |
""" |
793 |
Returns True if iteration at the inital phase is terminated. |
Returns True if iteration at the inital phase is terminated. |
794 |
""" |
""" |
795 |
return True |
return True |
796 |
|
|
797 |
def doInitialPostprocessing(self): |
def doInitialPostprocessing(self): |
798 |
""" |
""" |
799 |
finalises the initialization iteration process |
finalises the initialization iteration process. This method is not called in case of a restart. |
800 |
|
|
801 |
This function may be overwritten. |
This function may be overwritten. |
802 |
""" |
""" |
893 |
Initiates a simulation from a list of models. |
Initiates a simulation from a list of models. |
894 |
""" |
""" |
895 |
super(Simulation, self).__init__(**kwargs) |
super(Simulation, self).__init__(**kwargs) |
896 |
|
self.declareParameter(time=0., |
897 |
|
time_step=0, |
898 |
|
dt = self.UNDEF_DT) |
899 |
|
for m in models: |
900 |
|
if not isinstance(m, Model): |
901 |
|
raise TypeError("%s is not a subclass of Model."%m) |
902 |
self.__models=[] |
self.__models=[] |
|
|
|
903 |
for i in range(len(models)): |
for i in range(len(models)): |
904 |
self[i] = models[i] |
self[i] = models[i] |
905 |
|
|
944 |
""" |
""" |
945 |
return len(self.__models) |
return len(self.__models) |
946 |
|
|
947 |
|
def getAllModels(self): |
948 |
|
""" |
949 |
|
returns a list of all models used in the Simulation including subsimulations |
950 |
|
""" |
951 |
|
out=[] |
952 |
|
for m in self.iterModels(): |
953 |
|
if isinstance(m, Simulation): |
954 |
|
out+=m.getAllModels() |
955 |
|
else: |
956 |
|
out.append(m) |
957 |
|
return list(set(out)) |
958 |
|
|
959 |
|
def checkModels(self, models, hash): |
960 |
|
""" |
961 |
|
returns a list of (model,parameter, target model ) if the the parameter of model |
962 |
|
is linking to the target_model which is not in list of models. |
963 |
|
""" |
964 |
|
out=self.checkLinkTargets(models, hash + [self]) |
965 |
|
for m in self.iterModels(): |
966 |
|
if isinstance(m, Simulation): |
967 |
|
out|=m.checkModels(models, hash) |
968 |
|
else: |
969 |
|
out|=m.checkLinkTargets(models, hash + [self]) |
970 |
|
return set( [ (str(self)+"."+f[0],f[1]) for f in out ] ) |
971 |
|
|
972 |
|
|
973 |
def getSafeTimeStepSize(self,dt): |
def getSafeTimeStepSize(self,dt): |
974 |
""" |
""" |
978 |
""" |
""" |
979 |
out=min([o.getSafeTimeStepSize(dt) for o in self.iterModels()]) |
out=min([o.getSafeTimeStepSize(dt) for o in self.iterModels()]) |
980 |
return out |
return out |
981 |
|
|
982 |
|
def setUp(self): |
983 |
|
""" |
984 |
|
performs the setup for all models |
985 |
|
""" |
986 |
|
for o in self.iterModels(): |
987 |
|
o.setUp() |
988 |
|
|
989 |
def doInitialization(self): |
def doInitialization(self): |
990 |
""" |
""" |
991 |
Initializes all models. |
Initializes all models. |
992 |
""" |
""" |
|
self.n=0 |
|
|
self.tn=0. |
|
993 |
for o in self.iterModels(): |
for o in self.iterModels(): |
994 |
o.doInitialization() |
o.doInitialization() |
995 |
def doInitialStep(self): |
def doInitialStep(self): |
1053 |
""" |
""" |
1054 |
for o in self.iterModels(): |
for o in self.iterModels(): |
1055 |
o.doStepPostprocessing(dt) |
o.doStepPostprocessing(dt) |
1056 |
self.n+=1 |
self.time_step+=1 |
1057 |
self.tn+=dt |
self.time+=dt |
1058 |
|
self.dt=dt |
1059 |
|
|
1060 |
def doStep(self,dt): |
def doStep(self,dt): |
1061 |
""" |
""" |
1069 |
""" |
""" |
1070 |
self.iter=0 |
self.iter=0 |
1071 |
while not self.terminateIteration(): |
while not self.terminateIteration(): |
1072 |
if self.iter==0: self.trace("iteration at %d-th time step %e starts"%(self.n+1,self.tn+dt)) |
if self.iter==0: self.trace("iteration at %d-th time step %e starts"%(self.time_step+1,self.time+dt)) |
1073 |
self.iter+=1 |
self.iter+=1 |
1074 |
self.trace("iteration step %d"%(self.iter)) |
self.trace("iteration step %d"%(self.iter)) |
1075 |
for o in self.iterModels(): |
for o in self.iterModels(): |
1076 |
o.doStep(dt) |
o.doStep(dt) |
1077 |
if self.iter>0: self.trace("iteration at %d-th time step %e finalized."%(self.n+1,self.tn+dt)) |
if self.iter>0: self.trace("iteration at %d-th time step %e finalized."%(self.time_step+1,self.time+dt)) |
1078 |
|
|
1079 |
def run(self,check_point=None): |
def run(self,check_pointing=None): |
1080 |
""" |
""" |
1081 |
Run the simulation by performing essentially:: |
Run the simulation by performing essentially:: |
1082 |
|
|
1083 |
self.doInitialization() |
self.setUp() |
1084 |
while not self.terminateInitialIteration(): self.doInitialStep() |
if not restart: |
1085 |
self.doInitialPostprocessing() |
self.doInitialization() |
1086 |
|
while not self.terminateInitialIteration(): self.doInitialStep() |
1087 |
|
self.doInitialPostprocessing() |
1088 |
while not self.finalize(): |
while not self.finalize(): |
1089 |
dt=self.getSafeTimeStepSize() |
dt=self.getSafeTimeStepSize() |
1090 |
self.doStep(dt) |
self.doStepPreprocessing(dt_new) |
1091 |
if n%check_point==0: |
self.doStep(dt_new) |
1092 |
self.writeXML() |
self.doStepPostprocessing(dt_new) |
1093 |
self.doFinalization() |
self.doFinalization() |
1094 |
|
|
1095 |
If one of the models in throws a C{FailedTimeStepError} exception a |
If one of the models in throws a C{FailedTimeStepError} exception a |
1102 |
In both cases the time integration is given up after |
In both cases the time integration is given up after |
1103 |
C{Simulation.FAILED_TIME_STEPS_MAX} attempts. |
C{Simulation.FAILED_TIME_STEPS_MAX} attempts. |
1104 |
""" |
""" |
1105 |
self.doInitialization() |
# check the completness of the models: |
1106 |
self.doInitialStep() |
# first a list of all the models involved in the simulation including subsimulations: |
1107 |
self.doInitialPostprocessing() |
# |
1108 |
dt=self.UNDEF_DT |
missing=self.checkModels(self.getAllModels(), []) |
1109 |
|
if len(missing)>0: |
1110 |
|
msg="" |
1111 |
|
for l in missing: |
1112 |
|
msg+="\n\t"+str(l[1])+" at "+l[0] |
1113 |
|
raise MissingLink("link targets missing in the Simulation: %s"%msg) |
1114 |
|
#============================== |
1115 |
|
self.setUp() |
1116 |
|
if self.time_step < 1: |
1117 |
|
self.doInitialization() |
1118 |
|
self.doInitialStep() |
1119 |
|
self.doInitialPostprocessing() |
1120 |
while not self.finalize(): |
while not self.finalize(): |
1121 |
step_fail_counter=0 |
step_fail_counter=0 |
1122 |
iteration_fail_counter=0 |
iteration_fail_counter=0 |
1123 |
if self.n==0: |
if self.time_step==0: |
1124 |
dt_new=self.getSafeTimeStepSize(dt) |
dt_new=self.getSafeTimeStepSize(self.dt) |
1125 |
else: |
else: |
1126 |
dt_new=min(max(self.getSafeTimeStepSize(dt),dt/self.MAX_CHANGE_OF_DT),dt*self.MAX_CHANGE_OF_DT) |
dt_new=min(max(self.getSafeTimeStepSize(self.dt),self.dt/self.MAX_CHANGE_OF_DT),self.dt*self.MAX_CHANGE_OF_DT) |
1127 |
self.trace("%d. time step %e (step size %e.)" % (self.n+1,self.tn+dt_new,dt_new)) |
self.trace("%d. time step %e (step size %e.)" % (self.time_step+1,self.time+dt_new,dt_new)) |
1128 |
end_of_step=False |
end_of_step=False |
1129 |
while not end_of_step: |
while not end_of_step: |
1130 |
end_of_step=True |
end_of_step=True |
1131 |
if not dt_new>0: |
if not dt_new>0: |
1132 |
raise NonPositiveStepSizeError("non-positive step size in step %d"%(self.n+1)) |
raise NonPositiveStepSizeError("non-positive step size in step %d"%(self.time_step+1)) |
1133 |
try: |
try: |
1134 |
self.doStepPreprocessing(dt_new) |
self.doStepPreprocessing(dt_new) |
1135 |
self.doStep(dt_new) |
self.doStep(dt_new) |
1142 |
raise SimulationBreakDownError("reduction of time step to achieve convergence failed after %s steps."%self.FAILED_TIME_STEPS_MAX) |
raise SimulationBreakDownError("reduction of time step to achieve convergence failed after %s steps."%self.FAILED_TIME_STEPS_MAX) |
1143 |
self.trace("Iteration failed. Time step is repeated with new step size %s."%dt_new) |
self.trace("Iteration failed. Time step is repeated with new step size %s."%dt_new) |
1144 |
except FailedTimeStepError: |
except FailedTimeStepError: |
1145 |
dt_new=self.getSafeTimeStepSize(dt) |
dt_new=self.getSafeTimeStepSize(self.dt) |
1146 |
end_of_step=False |
end_of_step=False |
1147 |
step_fail_counter+=1 |
step_fail_counter+=1 |
1148 |
self.trace("Time step is repeated with new time step size %s."%dt_new) |
self.trace("Time step is repeated with new time step size %s."%dt_new) |
1149 |
if step_fail_counter>self.FAILED_TIME_STEPS_MAX: |
if step_fail_counter>self.FAILED_TIME_STEPS_MAX: |
1150 |
raise SimulationBreakDownError("Time integration is given up after %d attempts."%step_fail_counter) |
raise SimulationBreakDownError("Time integration is given up after %d attempts."%step_fail_counter) |
1151 |
dt=dt_new |
if not check_pointing==None: |
1152 |
if not check_point==None: |
if check_pointing.doDump(): |
|
if n%check_point==0: |
|
1153 |
self.trace("check point is created.") |
self.trace("check point is created.") |
1154 |
self.writeXML() |
self.writeXML() |
1155 |
self.doFinalization() |
self.doFinalization() |
1175 |
if isinstance(n, minidom.Text): |
if isinstance(n, minidom.Text): |
1176 |
continue |
continue |
1177 |
sims.append(esysxml.getComponent(n)) |
sims.append(esysxml.getComponent(n)) |
1178 |
sims.sort(cmp=_comp) |
sims.sort(_comp) |
1179 |
sim=cls([s[1] for s in sims], debug=esysxml.debug) |
sim=cls([s[1] for s in sims], debug=esysxml.debug) |
1180 |
esysxml.registerLinkableObject(sim, node) |
esysxml.registerLinkableObject(sim, node) |
1181 |
return sim |
return sim |
1219 |
""" |
""" |
1220 |
pass |
pass |
1221 |
|
|
1222 |
|
class MissingLink(Exception): |
1223 |
|
""" |
1224 |
|
Exception thrown when a link is missing |
1225 |
|
""" |
1226 |
|
pass |
1227 |
|
|
1228 |
class DataSource(object): |
class DataSource(object): |
1229 |
""" |
""" |
1230 |
Class for handling data sources, including local and remote files. This class is under development. |
Class for handling data sources, including local and remote files. This class is under development. |
1254 |
return self.uri |
return self.uri |
1255 |
|
|
1256 |
fromDom = classmethod(fromDom) |
fromDom = classmethod(fromDom) |
1257 |
|
|
1258 |
|
class RestartManager(object): |
1259 |
|
""" |
1260 |
|
A restart manager which does two things: it decides when restart files have created (when doDump returns true) and |
1261 |
|
manages directories for restart files. The method getNewDumper creates a new directory and returns its name. |
1262 |
|
|
1263 |
|
This restart manager will decide to dump restart files if every dump_step calls of doDump or |
1264 |
|
if more than dump_time since the last dump has elapsed. The restart manager controls two directories for dumping restart data, namely |
1265 |
|
for the current and previous dump. This way the previous dump can be used for restart in the case the current dump failed. |
1266 |
|
|
1267 |
|
@cvar SEC: unit of seconds, for instance for 5*RestartManager.SEC to define 5 seconds. |
1268 |
|
@cvar MIN: unit of minutes, for instance for 5*RestartManager.MIN to define 5 minutes. |
1269 |
|
@cvar H: unit of hours, for instance for 5*RestartManager.H to define 5 hours. |
1270 |
|
@cvar D: unit of days, for instance for 5*RestartManager.D to define 5 days. |
1271 |
|
""" |
1272 |
|
SEC=1. |
1273 |
|
MIN=60. |
1274 |
|
H=360. |
1275 |
|
D=8640. |
1276 |
|
def __init__(self,dump_time=1080., dump_step=None, dumper=None): |
1277 |
|
""" |
1278 |
|
initializes the RestartManager. |
1279 |
|
|
1280 |
|
@param dump_time: defines the minimum time interval in SEC between to dumps. If None, time is not used as criterion. |
1281 |
|
@param dump_step: defines the number of calls of doDump between to dump events. If None, the call counter is not used as criterion. |
1282 |
|
@param dumper: defines the directory for dumping restart files. Additionally the directories dumper+"_bkp" and dumper+"_bkp2" are used. |
1283 |
|
if the directory does not exist it is created. If dumper is not present a unique directory within the current |
1284 |
|
working directory is used. |
1285 |
|
""" |
1286 |
|
self.__dump_step=dump_time |
1287 |
|
self.__dump_time=dump_step |
1288 |
|
self.__counter=0 |
1289 |
|
self.__saveMarker() |
1290 |
|
if dumper == None: |
1291 |
|
self.__dumper="restart"+str(os.getpid()) |
1292 |
|
else: |
1293 |
|
self.__dumper=dumper |
1294 |
|
self.__dumper_bkp=self.__dumper+"_bkp" |
1295 |
|
self.__dumper_bkp2=self.__dumper+"_bkp2" |
1296 |
|
self.__current_dumper=None |
1297 |
|
def __saveMarker(self): |
1298 |
|
self.__last_restart_time=time.time() |
1299 |
|
self.__last_restart_counter=self.__counter |
1300 |
|
def getCurrentDumper(self): |
1301 |
|
""" |
1302 |
|
returns the name of the currently used dumper |
1303 |
|
""" |
1304 |
|
return self.__current_dumper |
1305 |
|
def doDump(self): |
1306 |
|
""" |
1307 |
|
returns true the restart should be dumped. use C{getNewDumper} to get the directory name to be used. |
1308 |
|
""" |
1309 |
|
if self.__dump_step == None: |
1310 |
|
if self.__dump_step == None: |
1311 |
|
out = False |
1312 |
|
else: |
1313 |
|
out = (self.__dump_step + self.__last_restart_counter) <= self.__counter |
1314 |
|
else: |
1315 |
|
if dump_step == None: |
1316 |
|
out = (self.__last_restart_time + self.__dump_time) <= time.time() |
1317 |
|
else: |
1318 |
|
out = ( (self.__dump_step + self.__last_restart_counter) <= self.__counter) \ |
1319 |
|
or ( (self.__last_restart_time + self.__dump_time) <= time.time() ) |
1320 |
|
if out: self.__saveMarker() |
1321 |
|
self__counter+=1 |
1322 |
|
def getNewDumper(self): |
1323 |
|
""" |
1324 |
|
creates a new directory to be used for dumping and returns its name. |
1325 |
|
""" |
1326 |
|
if os.access(self.__dumper_bkp,os.F_OK): |
1327 |
|
if os.access(self.__dumper_bkp2, os.F_OK): |
1328 |
|
raise RunTimeError("please remove %s."%self.__dumper_bkp2) |
1329 |
|
try: |
1330 |
|
os.rename(self.__dumper_bkp, self.__dumper_bkp2) |
1331 |
|
except: |
1332 |
|
self.__current_dumper=self.__dumper |
1333 |
|
raise RunTimeError("renaming back-up directory %s failed. Use %s for restart."%(self.__dumper_bkp,self.__dumper)) |
1334 |
|
if os.access(self.__dumper,os.F_OK): |
1335 |
|
if os.access(self.__dumper_bkp, os.F_OK): |
1336 |
|
raise RunTimeError("please remove %s."%self.__dumper_bkp) |
1337 |
|
try: |
1338 |
|
os.rename(self.__dumper, self.__dumper_bkp) |
1339 |
|
except: |
1340 |
|
self.__current_dumper=self.__dumper_bkp2 |
1341 |
|
raise RunTimeError("moving directory %s to back-up failed. Use %s for restart."%(self.__dumper,self.__dumper_bkp2)) |
1342 |
|
try: |
1343 |
|
os.mkdir(self.__dumper) |
1344 |
|
except: |
1345 |
|
self.__current_dumper=self.__dumper_bkp |
1346 |
|
raise RunTimeError("creating a new restart directory %s failed. Use %s for restart."%(self.__dumper,self.__dumper_bkp)) |
1347 |
|
if os.access(self.__dumper_bkp2, os.F_OK): os.rmdir(self.__dumper_bkp2) |
1348 |
|
return self.getCurrentDumper() |
1349 |
|
|
1350 |
|
|
1351 |
# vim: expandtab shiftwidth=4: |
# vim: expandtab shiftwidth=4: |