]> SALOME platform Git repositories - modules/adao.git/blob - src/daComposant/daNumerics/pst4mod/modelica_libraries/automatic_simulation.py
Salome HOME
Documentation and models update, pst4mod
[modules/adao.git] / src / daComposant / daNumerics / pst4mod / modelica_libraries / automatic_simulation.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2016-2024 EDF R&D
4 #
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation; either
8 # version 2.1 of the License.
9 #
10 # This library is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this library; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19 ###############################################################################
20 #                            LIBRARIES IMPORT                                 #
21 ###############################################################################
22 import sys
23 import logging
24 from os import path,remove,getcwd,chdir,pardir,mkdir
25 from shutil import copyfile,move
26 from buildingspy.io.outputfile import Reader
27
28 if sys.version_info[0]<3:
29     import subprocess32 as subprocess
30 else:
31     import subprocess
32
33 from .functions import get_cur_time, tol_equality
34
35 #The library to write in dsin.txt is imported directly in method single_simulation
36 #It can be either modelicares or write_in_dsin
37
38 ###############################################################################
39 #                     CLASS Automatic_Simulation                              #
40 ###############################################################################
41
42 class DiscardedInput(Exception):
43     pass
44
45 ###############################################################################
46 #                     CLASS Automatic_Simulation                              #
47 ###############################################################################
48
49 class Automatic_Simulation(object):
50     """
51     Aim: making it easier to run automatic Dymola simulations.
52
53     Notes:
54     - Commentaries are written directly in the code
55     - Examples are provided with the code to make it easier to use
56     - There is a slight difference between the Python2 and Python3 implementation
57     of method singleSimulation
58
59     Required libraries that are not in the standard distribution:
60     - modelicares
61     - subprocess32 (only for Python2)
62     - subprocess (only for Python3)
63
64     New features introduced 02/2019:
65     - possibility of performing simulations with dsint.txt and dymosim.exe files
66     not nessarily in the same folder
67     - possibility of creating a results' folder in another folder
68     - possibility of using an equivalent file to dsin.txt but with a different name
69     - possibility of modifying the results file name
70     - the previous use of this module should remain unchanged compared to the previous version
71     - Not yet checked for Linux but should work, Python within a Linux environment is not yet implemented.
72
73     Update 06/2019:
74     - correction of set_success_code function in order to consider the directory in which the results mat file is created (and not the default one)
75
76     Update 03/2021:
77     - better handling of linux simulations (definition dymosim_path)
78
79     Update 09/2022:
80     - bug correction in copy_dsres_to and check_inputs
81     """
82
83     def __init__(self,simu_dir,dict_inputs,
84                  dymosim_path = None, dsin_name = 'dsin.txt', dsres_path = None,
85                  logfile=None,timeout=60,copy_dsres_to=None,
86                  copy_initial_dsin=None,without_modelicares=False,linux=False,check_inputs=[]):
87         self.__simu_dir = path.abspath(simu_dir)
88         self.__dsin_name = path.join(path.abspath(simu_dir),dsin_name)
89         if dsres_path is None:
90             self.__dsres_path = path.join(path.abspath(simu_dir),'dsres.mat')
91         elif path.basename(dsres_path)[-4:] =='.mat':
92             self.__dsres_path = dsres_path
93         else:
94             self.__dsres_path = path.join(path.abspath(dsres_path),'dsres.mat')
95         self.__logfile = logfile
96         self.__timeout = timeout
97         self.__copy_dsres_to = copy_dsres_to
98         self.__copy_initial_dsin = copy_initial_dsin
99         self.__without_modelicares = without_modelicares
100         self.__linux = linux
101
102         if dymosim_path is None:
103             if self.linux:
104                 self.__dymosim_path = path.join(path.abspath(simu_dir),'dymosim')
105             else:
106                 self.__dymosim_path = path.join(path.abspath(simu_dir),'dymosim.exe')
107         elif path.isdir(dymosim_path):
108             if self.linux:
109                 self.__dymosim_path = path.join(path.abspath(dymosim_path),'dymosim')
110             else:
111                 self.__dymosim_path = path.join(path.abspath(dymosim_path),'dymosim.exe')
112         elif path.isfile(dymosim_path):
113             self.__dymosim_path = dymosim_path
114         else:
115             raise ValueError('Model file or directory not found using %s'%str(dymosim_path))
116
117         self.__success_code = 99
118
119         #Potential separation of standard inputs parameters from time varying inputs (dsu.txt)
120         if 'Time' in dict_inputs.keys() : #Reserved keyword for time variation
121             logging.info("Reserved variable Time found -> dsu.txt creation")
122             dsu_keys = []
123             for key in dict_inputs :
124                 #if hasattr(dict_inputs[key],'__iter__') and not isinstance(dict_inputs[key],str) : #Identifying iterables not string
125                 if isinstance(dict_inputs[key],(list,tuple)) : #Identifying list or tuples
126                     dsu_keys.append(key)
127             self.dsu_inputs = {key:dict_inputs[key] for key in dsu_keys} #Dictionary restricted to time varying inputs
128             for key in dsu_keys : del dict_inputs[key] #Removing the variable from the standard dictionary
129         else :
130             self.dsu_inputs = None
131         self.__dict_inputs = dict_inputs
132
133         if check_inputs is True:
134             self.__check_inputs = dict_inputs.keys()
135         else:
136             self.__check_inputs = check_inputs
137
138 ###############################################################################
139 #                                  GETTERS                                    #
140 ###############################################################################
141     @property
142     def simu_dir(self):
143         """
144         Path of the directory in which the simulation will be done (directory which
145         contains file dsin.txt or equivalent)
146         """
147         return self.__simu_dir
148
149     @property
150     def check_inputs(self):
151         """
152         Variable used to select inputs that have to be checked: set value vs. used
153         Possible values:
154          - True : all variables in dict_inputs are checked
155          - List of variables name to be checked
156         """
157         return self.__check_inputs
158
159     @property
160     def dict_inputs(self):
161         """
162         Dictionary associating to each variable whose value has to be given to
163         dsin.txt (inputs + iteration variables if necessary) its value.
164         """
165         return self.__dict_inputs
166
167     @property
168     def logfile(self):
169         """
170         log file
171         """
172         return self.__logfile
173
174     @property
175     def timeout(self):
176         """
177         If a simulation has not converged after timeout, the siumlation is stopped.
178         """
179         return self.__timeout
180
181     @property
182     def copy_dsres_to(self):
183         """
184         Path of the file where dsres.mat should be copied if simulation converges
185         """
186         return self.__copy_dsres_to
187
188     @property
189     def success_code(self):
190         """
191         Type: Integer
192         If a simulation converges, this attribute is setted to 0
193         If a simulation fails to initialize, this attribute is setted to 1
194         If a simulation fails after the initialization, this attribute is setted to 2
195         If a simulation does not fail but does not converge either, this attribute is setted to 3
196         Default value: 99
197         """
198         return self.__success_code
199
200     @property
201     def copy_initial_dsin_to(self):
202         """
203         Path of the file where dsin.txt should be copied before the first simulation is run.
204         """
205         return self.__copy_initial_dsin
206
207     @property
208     def without_modelicares(self):
209         """
210         If True, modelicares is not imported.
211         Instead, the script named "writer" is imported. A method of class Writer
212         is used to write in dsin.txt.
213         """
214         return self.__without_modelicares
215
216     @property
217     def linux(self):
218         """
219         If the script has to be run on Linux, this attribute should be setted to True.
220         On Linux, the exe file is indeed named "dymosim" instead of "dymosim.exe".
221         """
222         return self.__linux
223
224
225     def dymosim_path(self):
226         """
227         Path of the directory that contains the dymosim.exe (or equivalent) file,
228         can be different of simu_dir
229         """
230         return self.__dymosim_path
231
232     def dsin_name(self):
233         """
234         Name of the dsint.txt (or equivalent) file, must be inclueded in simu_dir
235         """
236         return self.__dsin_name
237
238     def dsres_path(self):
239         """
240         Path of the file in which the results mat file will be created
241         """
242         return self.__dsres_path
243 ###############################################################################
244 #                                  SETTERS                                    #
245 ###############################################################################
246     def set_success_code(self):
247         """
248         This function sets the values of the following attributes:
249             "success":
250                 True if simulation has converged (a file named success exists (success. on Linux))
251                 False if simulation has failed (a file named failure exists (failure. on Linux))
252                 None if simulation has not converged (For instance if the simulation was not given the time to converge)
253             "initsuccess":
254                 True if the simulation succeeded to initialise (a dsres.mat file is found)
255                 False if the initialization failed (no dsres.mat file is found)
256         """
257         if path.exists(self.__dsres_path):
258             if self.linux:
259                 if path.exists(path.join(path.dirname(self.__dsres_path),'success.')):
260                     self.__success_code = 0
261                 elif path.exists(path.join(path.dirname(self.__dsres_path),'failure.')):
262                     self.__success_code = 2
263                 else:
264                     self.__success_code = 3
265             else:
266                 if path.exists(path.join(path.dirname(self.__dsres_path),'success')):
267                     self.__success_code = 0
268                 elif path.exists(path.join(path.dirname(self.__dsres_path),'failure')):
269                     self.__success_code = 2
270                 else:
271                     self.__success_code = 3
272         else:
273             self.__success_code  = 1 #Suggestion: We should probably also consider timeout happens durng the initialization phase (neither 'dsres.mat' nor 'failure')
274
275 ###############################################################################
276 #                                 RESETTERS                                   #
277 ###############################################################################
278     def reset_success_code(self):
279         """
280         Reset value of attribute success
281         """
282         self.__success_code = 99
283 ###############################################################################
284 #                               MAIN METHODS                                  #
285 ###############################################################################
286
287     def cleaning(self):
288         """
289         Remove the files that are automatically created during a simulation
290         """
291         if path.exists(path.join(self.simu_dir,"success")):
292             remove(path.join(self.simu_dir,"success"))
293         if path.exists(path.join(self.simu_dir,"failure")):
294             remove(path.join(self.simu_dir,"failure"))
295         if path.exists(path.join(self.simu_dir,"null")):
296             remove(path.join(self.simu_dir,"null"))
297         if path.exists(path.join(self.simu_dir,"dsfinal.txt")):
298             remove(path.join(self.simu_dir,"dsfinal.txt"))
299         if path.exists(path.join(self.simu_dir,"status")):
300             remove(path.join(self.simu_dir,"status"))
301         if path.exists(path.join(self.simu_dir,"dslog.txt")):
302             remove(path.join(self.simu_dir,"dslog.txt"))
303         if path.exists(self.__dsres_path):
304             remove(self.__dsres_path)
305
306     def single_simulation(self):
307         """
308         Run an simulation using dymosim.exe after inserting the values that are
309         contained in self.dict_inputs in dsin.txt
310         """
311         #Import a library to write in dsin.txt
312         if self.without_modelicares == True:
313             from .write_in_dsin import Write_in_dsin
314         else:
315             import modelicares
316
317         #Save the value of the current working directory in CUR_DIR
318         CUR_DIR = getcwd()
319
320         #It is necessary to change the working directory before running dymosim.exe
321         chdir(self.simu_dir)
322         if self.logfile:
323             logging.info('Time = '+get_cur_time()+' \t'+'Script working in directory '+self.simu_dir)
324
325         #Cleaning the working directory (mandatory to check the intialisation success)
326         self.cleaning()
327
328         #File dsin.txt is copied before being changed if it has been given a value
329         if self.copy_initial_dsin_to:
330             dir_copy_dsin = path.abspath(path.join(self.copy_initial_dsin_to,pardir))
331             if not(path.exists(dir_copy_dsin)):
332                 mkdir(dir_copy_dsin)
333             copyfile(src=path.basename(self.__dsin_name),dst=self.copy_initial_dsin_to)
334
335         # Change file dsin.txt
336         if self.logfile:
337             #print('Changing dsin.txt')
338             logging.info('Time = '+get_cur_time()+' \t'+'Changing dsin.txt')
339         if self.without_modelicares:
340             writer = Write_in_dsin(dict_inputs = self.dict_inputs,filedir = self.simu_dir, dsin_name = path.basename(self.__dsin_name), new_file_name = path.basename(self.__dsin_name))
341             writer.write_in_dsin()
342         else:
343             modelicares.exps.write_params(self.dict_inputs,path.basename(self.__dsin_name))
344
345         #A folder for results file is created if it does not exist (usually for dsres.mat)
346         if self.dsres_path:
347             dir_dsres_path = path.abspath(path.join(self.__dsres_path,pardir))
348             if not(path.exists(dir_dsres_path)):
349                 mkdir(dir_dsres_path)
350
351         # Simulation launching : execution of dymosim.exe
352         if self.logfile:
353             #print('Launching simulation')
354             logging.info('Time = '+get_cur_time()+' \t'+"Launching simulation")
355 ###############################################################################
356 #      WARNING: DIFFERENT SOLUTIONS FOR PYTHON 2 AND PYTHON 3                 #
357 ###############################################################################
358 #Python2 solution - working solution
359         if sys.version_info[0]<3:
360             try:
361                 if self.linux:
362                     #print('Running dymosim')
363                     subprocess.call(str(self.__dymosim_path + ' ' + self.__dsin_name + ' ' + self.__dsres_path),timeout=self.timeout)           #NOT TESTED
364                 else:
365                     #print('Running dymosim.exe')
366                     subprocess.call(str(self.__dymosim_path + ' ' + self.__dsin_name + ' ' + self.__dsres_path),timeout=self.timeout)       #NOT TESTED
367
368             except subprocess.TimeoutExpired:
369                 if self.logfile:
370                     logging.info('Time = '+get_cur_time()+' \t'+'Time limit reached. Simulation is stopped.')
371                     #Add information about the simulation time when the simulation is stopped
372                     try:
373                         with open('status','r') as lines:
374                             for line in lines:
375                                 words = line.split()
376                                 if len(words) > 0:
377                                     stop_time = line.split()[-1]
378                         logging.info('Simulation time when simulation is stopped: '+str(stop_time))
379                     except:
380                         pass
381
382 #Python3 solution - best solution. Cannot be easily implemented on Python2 because
383 #the TimeoutExpired exception creates an error (apparently because of subprocess32)
384         else:
385             try:
386                 #print('Running dymosim.exe')
387                 proc = subprocess.Popen([str(arg) for arg in [self.__dymosim_path,self.__dsin_name,self.__dsres_path]],
388                                         stderr=subprocess.STDOUT,stdout=subprocess.PIPE,universal_newlines=True)
389                 dymout, _ = proc.communicate(timeout=self.timeout)
390                 if self.logfile:
391                     logging.debug(dymout)
392
393             except subprocess.TimeoutExpired:
394                 proc.kill()
395                 dymout, _ = proc.communicate() #Finalize communication (python guidelines)
396                 if self.logfile:
397                     logging.debug(dymout)
398                     logging.info('Time = '+get_cur_time()+' \t'+'Time limit reached. Simulation is stopped.')
399                     try:
400                         with open('status','r') as lines:
401                             for line in lines:
402                                 words = line.split()
403                                 if len(words) > 0:
404                                     stop_time = line.split()[-1]
405                         logging.info('Simulation time when simulation is stopped: '+str(stop_time))
406                     except:
407                         pass
408 ###############################################################################
409 #                       END OF WARNING                                        #
410 ###############################################################################
411
412         #Set the value of attribute success
413         self.set_success_code()
414
415         #Copy dsres.mat if attribute copy_dsres_to is not None
416         if self.success_code == 0:
417             logging.info('Time = '+get_cur_time()+' \t'+'Simulation has converged')
418             if self.copy_dsres_to:
419                 move(self.__dsres_path,self.copy_dsres_to) #Test
420
421         elif self.success_code == 1:
422             logging.info('Time = '+get_cur_time()+' \t'+'Simulation has failed - Error during initialization')
423
424         elif self.success_code == 2:
425             logging.info('Time = '+get_cur_time()+' \t'+'Simulation has failed - Error after the initialization')
426
427         elif self.success_code == 3:
428             #The message is written in the TimeoutExpired Exception
429             pass
430         # Check whether the inputs have been taken into account
431         if self.success_code != 1 :
432             for key in self.check_inputs :
433                 if self.copy_dsres_to:
434                     outfile = Reader(self.copy_dsres_to,'dymola')
435                 else:
436                     outfile = Reader(self.__dsres_path,'dymola')
437                 used_val = outfile.values(key)[1][0]
438                 if self.logfile:
439                     logging.debug('Checking %s : %s (set) vs. %s (used)' % (key,self.dict_inputs[key],used_val))
440                 if not tol_equality((self.dict_inputs[key],outfile.values(key)[1][0])):
441                     raise DiscardedInput('Parameter %s: %s set but %s used' % (key,self.dict_inputs[key],used_val))
442
443         #Go back to CUR_DIR
444         chdir(CUR_DIR)
445
446 if __name__ == '__main__':
447     print('\n  AUTODIAGNOSTIC\n  ==============\n')