finesse.py 21.5 KB
Newer Older
Daniel Brown's avatar
Daniel Brown committed
1
2
3
4
# -*- coding: utf-8 -*-
"""
Created on Sun Jan 27 09:56:53 2013

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PyKat - Python interface and wrapper for FINESSE
Copyright (C) 2013 Daniel David Brown

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Contact at ddb@star.sr.bham.ac.uk

@author: Daniel Brown
Daniel Brown's avatar
Daniel Brown committed
25
"""
26
import sys
Daniel Brown's avatar
Daniel Brown committed
27
28
29
30
import os
import subprocess
import tempfile
import numpy as np
31
32
import datetime
import pickle
33
import pykat
34
import warnings
35
import re
Daniel Brown's avatar
Daniel Brown committed
36

37
from pykat.exceptions import *
38

Daniel Brown's avatar
Daniel Brown committed
39
40
41
from pykat.node_network import NodeNetwork
from pykat.detectors import Detector
from pykat.components import Component
42
from pykat.commands import Command, xaxis
Daniel Brown's avatar
Daniel Brown committed
43
44
45
from pykat.gui.gui import pyKatGUI

NO_GUI = False
46
47
NO_BLOCK = "NO_BLOCK"

48
class katRun(object):
Daniel Brown's avatar
Daniel Brown committed
49
    def __init__(self):
50
51
52
53
54
55
56
57
        self.runDateTime = datetime.datetime.now()
        self.x = None
        self.y = None
        self.xlabel = None
        self.ylabels = None
        self.katScript = None
        self.katVersion = None
        
58
    def saveKatRun(self, filename):
59
        with open(filename,'w') as outfile:
60
            pickle.dump(self, outfile)
Daniel Brown's avatar
Daniel Brown committed
61
    
62
63
64
65
66
67
    @staticmethod
    def loadKatRun(filename):
        with open(filename,'r') as infile:
            return pickle.load(infile)
        
        
68
69
70
class Block:
    def __init__(self, name):
        self.__name = name
71
        self.contents = [] # List of objects and strings of finesse code
72
73
74
75
76
        self.enabled = True 
        
    @property
    def name(self): return self.__name
    
77
78
class kat(object):                    
        
79
    def __init__(self, kat_file=None, kat_code=None, katdir="", katname="", tempdir=None, tempname=None):
80
        
81
        self.scene = None # scene object for GUI
Andreas Freise's avatar
Andreas Freise committed
82
        self.verbose = True
83
        self.__blocks = {} # dictionary of blocks that are used
Daniel Brown's avatar
Daniel Brown committed
84
85
86
87
        self.__components = {}  # dictionary of optical components      
        self.__detectors = {}   # dictionary of detectors
        self.__commands = {}    # dictionary of commands
        self.__extra_lines = [] # an array of strings which are just normal finesse code to include when running
Daniel Brown's avatar
Daniel Brown committed
88
89
        self.__gui = None
        self.nodes = NodeNetwork(self)  
90
91
        self.__katdir = katdir
        self.__katname = katname
92
93
        self.__tempdir = tempdir
        self.__tempname = tempname
Daniel Brown's avatar
Daniel Brown committed
94
        self.pykatgui = None
95
96
97
        
        # Various options for running finesse, typicaly the commands with just 1 input
        # and have no name attached to them.
98
99
100
        self.__phase = None
        self.__maxtem = None
        self.__noxaxis = None
101
        self.__time_code = None
Daniel Brown's avatar
Daniel Brown committed
102
        
103
        if kat_code != None and kat_file != None:
104
            raise BasePyKatException("Specify either a Kat file or some Kat code, not both.")
105
106
107
108
109
110
        
        if kat_code != None:
            self.parseCommands(kat_code)
        
        if kat_file != None:
            self.loadKatFile(kat_file)
111
112
113
114
        
        cls = type(self)
        self.__class__ = type(cls.__name__, (cls,), {})
        
115
116
117
118
119
120
121
122
123
124
    @property
    def maxtem(self): return self.__maxtem
    @maxtem.setter
    def maxtem(self,value): self.__maxtem = int(value)
    
    @property
    def phase(self): return self.__phase
    @phase.setter
    def phase(self,value): self.__phase = int(value)
    
125
126
127
128
129
    @property
    def getPerformanceData(self): return self.__time_code
    @getPerformanceData.setter
    def getPerformanceData(self,value): self.__time_code = bool(value)
    
130
131
132
133
    @property
    def noxaxis(self): return self.__noxaxis
    @noxaxis.setter
    def noxaxis(self,value): self.__noxaxis = bool(value)
Daniel Brown's avatar
Daniel Brown committed
134
       
135
    def loadKatFile(self, katfile):
136
137
        commands=open(katfile).read()
        self.parseCommands(commands)
138
    
139
140
    def parseKatCode(self, code):
        #commands = code.split("\n")
141
        self.parseCommands(code)
142
        
143
    def parseCommands(self, commands):
144
        blockComment = False
Andreas Freise's avatar
Andreas Freise committed
145

146
147
148
149
        self.__currentTag= NO_BLOCK
        
        if not (NO_BLOCK in self.__blocks):
            self.__blocks[NO_BLOCK] = Block(NO_BLOCK)
150
        
151
        commands=self.remove_comments(commands)
Andreas Freise's avatar
Andreas Freise committed
152
        
153
154
155
        after_process = [] # list of commands that should be processed after 
                           # objects have been set and created
        
156
        for line in commands.split("\n"):
157
            #for line in commands:
158
159
            if len(line.strip()) >= 2:
                line = line.strip()
160
161

                # Looking for block start or end
162
163
                values = line.split(" ")
                if values[0] == "%%%":
164
165
                    if values[1] == "FTblock":
                        newTag = values[2]
Daniel Brown's avatar
Daniel Brown committed
166
167
                        
                        if self.__currentTag != None and newTag != self.__currentTag: 
168
                            warnings.warn("found block {0} before block {1} ended".format(newTag, self.__currentTag))    
Daniel Brown's avatar
Daniel Brown committed
169
170
171
172
                            
                        if newTag in self.__blocks:
                            raise pkex.BasePyKatException("Block `{0}` has already been read")
                            
173
                        self.__blocks[newTag] = Block(newTag) # create new list to store all references to components in block
174
                        self.__currentTag = newTag                            
Daniel Brown's avatar
Daniel Brown committed
175
                        
176
                    if values[1] == "FTend":
177
                        self.__currentTag = NO_BLOCK
Daniel Brown's avatar
Daniel Brown committed
178
                        
179
                    continue
180
181
                #warnings.warn("current tag {0}".format(self.__currentTag))    

182
183
184
185
186
187
188
189
190
191
192
193
                # don't read comment lines
                if line[0] == "#" or line[0] == "%":
                    continue
                
                # check if block comment is being used
                if not blockComment and line[0:2] == "/*":
                    blockComment = True
                    continue
                elif blockComment and line[0:2] == "*/":
                    blockComment = False
                    continue
                
194
                first = line.split(" ",1)[0]
195
                obj = None
196
197
                
                if(first == "m"):
198
                    obj = pykat.components.mirror.parseFinesseText(line)
199
                elif(first == "s"):
200
                    obj = pykat.components.space.parseFinesseText(line)
201
                elif(first == "l"):
202
                    obj = pykat.components.laser.parseFinesseText(line)
Andreas Freise's avatar
Andreas Freise committed
203
204
205
206
                elif(first[0:2] == "pd"):
                    obj = pykat.detectors.photodiode.parseFinesseText(line)
                elif(first == "xaxis" or first == "x2axis" or first == "xaxis*" or first == "x2axis*"):
                    obj = pykat.commands.xaxis.parseFinesseText(line)
207
208
                elif(first == "gauss" or first == "gauss*" or first == "gauss**"):
                    after_process.append(line)
209
                else:
Andreas Freise's avatar
Andreas Freise committed
210
211
                    if self.verbose:
                        print "Parsing `{0}` into pykat object not implemented yet, added as extra line.".format(line)
212
                    obj = line
213
214
                    # manually add the line to the block contents
                    self.__blocks[self.__currentTag].contents.append(line) 
215
                
216
                if obj != None and not isinstance(obj, str):
217
                    self.add(obj)
Daniel Brown's avatar
Daniel Brown committed
218
                    
219
220
221
222
223
224
225
226
        # now process all the varous gauss/attr etc. commands which require
        # components to exist first before they can be processed
        for line in after_process:
            first = line.split(" ",1)[0]
            
            if first == "gauss" or first == "gauss*" or first == "gauss**":
                pykat.commands.gauss.parseFinesseText(line)
            
227
        self.__currentTag = NO_BLOCK 
228
            
229
230
231
232
233
234
    def run(self, printout=1, printerr=1, save_output=False, save_kat=False,kat_name=None) :
        """ 
        Runs the current simulation setup that has been built thus far.
        It returns a katRun object which is populated with the various
        data from the simulation run.
        """
235
236
237
        try:
            r = katRun()
            r.katScript = "".join(self.generateKatScript())       
238
            
239
            if len(self.__katdir) == 0:
240
241
242
                # Get the environment variable for where Finesse is stored
                self.__finesse_dir = os.environ.get('FINESSE_DIR')
                
243
                if self.__finesse_dir == None :
244
                    raise MissingFinesseEnvVar()
245
246
247
248
            else:
                self.__finesse_dir = self.__katdir
                
            if len(self.__katname) == 0:
249
250
251
252
                katexe = "kat"
                
                if os.sys.platform == "win32":
                    katexe += ".exe"
253
            else:
254
255
256
                katexe = self.__katname
            
            kat_exec = os.path.join(self.__finesse_dir, katexe) 
257
258
259
            
            # check if kat file exists and it is executable by user        
            if not (os.path.isfile(kat_exec) and os.access(kat_exec, os.X_OK)):
260
                raise MissingFinesse()
261
262
            
            # create a kat file which we will write the script into
263
264
265
266
267
268
            if self.__tempname == None:
                katfile = tempfile.NamedTemporaryFile(suffix=".kat", dir=self.__tempdir)
            else:
                filepath =os.path.join(self.__tempdir, self.__tempname+".kat" )
                katfile = open( filepath, 'w' ) 
                
269
270
            katfile.writelines(r.katScript)
            katfile.flush()
271
            
272
            cmd=[kat_exec, '--perl1']
273
            if self.__time_code:
274
275
276
277
                cmd.append('--perf-timing')
                cmd.append('--no-backspace')

            cmd.append(katfile.name)
Andreas Freise's avatar
Andreas Freise committed
278
279
            if self.verbose:
                print cmd
280
            p=subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
281
            err = ""
282
            
283
284
285
            for line in iter(p.stderr.readline, ""):
                err += line
                vals = line.split("-")
286
                
287
288
289
290
291
292
293
294
295
                if len(vals) == 2:
                    action = vals[0].strip()
                    prc = vals[1].strip()[:-1]
                    
                    #sys.stdout.write("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b")
                    sys.stdout.write("\r{0} {1}%".format(action, prc))
                    sys.stdout.flush()
                
            [out,errpipe] = p.communicate()
Daniel Brown's avatar
Daniel Brown committed
296
            
297
298
299
300
            # get the version number
            ix = out.find('build ') + 6
            ix2 = out.find(')',ix)
            r.katVersion = out[ix:ix2]
Daniel Brown's avatar
Daniel Brown committed
301
            
302
            r.runDateTime = datetime.datetime.now()
Daniel Brown's avatar
Daniel Brown committed
303
            
304
            if p.returncode != 0:
305
                raise FinesseRunError(err, katfile.name)
306
            
307
308
309
310
311
312
313
314
            if printout == 1: print out
            if printerr == 1: print err

            root = os.path.splitext(katfile.name)
            base = os.path.basename(root[0])            
            outfile = root[0] + ".out"
                    
            [r.x,r.y,hdr] = self.readOutFile(outfile)
Daniel Brown's avatar
Daniel Brown committed
315
            
316
317
            r.xlabel = hdr[0]
            r.ylabels = hdr[1:]
Daniel Brown's avatar
Daniel Brown committed
318
            
319
320
321
322
323
324
325
326
327
328
            if save_output:        
                newoutfile = "{0}.out".format(base)
                
                cwd = os.path.os.getcwd()
                newoutfile = os.path.join(cwd,newoutfile)
                
                if os.path.isfile(newoutfile):
                    os.remove(newoutfile)
                    
                os.rename(outfile, newoutfile)
Daniel Brown's avatar
Daniel Brown committed
329

330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
                print "Output data saved to '{0}'".format(newoutfile)
                
            if save_kat:
                if kat_name == None:
                    kat_name = "pykat_output"                
                
                cwd = os.path.os.getcwd()
                newkatfile = os.path.join(cwd, kat_name + ".kat")
                
                if os.path.isfile(newkatfile):
                    os.remove(newkatfile)
                  
                os.rename(katfile.name, newkatfile)         
                
                print "Kat file saved to '{0}'".format(newkatfile)
                

            katfile.close()
            perfData = []
349
            
350
351
352
353
354
355
356
357
358
359
            if self.__time_code:
                perffile = open(root[0] + ".perf",'r')
                
                for l in perffile.readlines():
                    vals = l.strip().split(' ')
                    perfData.append((vals[0], float(vals[1]), float(vals[2]), float(vals[3])))
                    
                return [r, perfData]
            else:
                return r
360
                
361
        except FinesseRunError as fe:
362
363
            print fe
            
Daniel Brown's avatar
Daniel Brown committed
364
        
365
    def add(self, obj):
366
        try:
367
            obj.tag = self.__currentTag
368
            self.__blocks[self.__currentTag].contents.append(obj)
Daniel Brown's avatar
Daniel Brown committed
369
            
370
371
372
            if isinstance(obj, Component):
                
                if obj.name in self.__components :
373
                    raise BasePyKatException("A component with name '{0}' has already been added".format([obj.name]))            
374
375
376
377
378
379
380
                            
                self.__components[obj.name] = obj
                self.__add_component(obj)
                
            elif isinstance(obj, Detector):
                
                if obj.name in self.__detectors :
381
                        raise BasePyKatException("A detector '{0}' has already been added".format(obj.name))
Daniel Brown's avatar
Daniel Brown committed
382
                        
383
384
385
386
387
388
389
390
391
                self.__detectors[obj.name] = obj
                self.__add_detector(obj)
                
            elif isinstance(obj, Command):
                
                self.__commands[obj.__class__.__name__] = obj
                self.__add_command(obj)
                
            else :
392
                raise BasePyKatException("Object {0} could not be added".format(obj))
393
394
                
            obj._on_kat_add(self)
Daniel Brown's avatar
Daniel Brown committed
395
            
396
        except BasePyKatException as ex:
397
            print ex
398

Daniel Brown's avatar
Daniel Brown committed
399
400
401
402
403
404
405
406
407
408
409
    def readOutFile(self, filename):
        
        outfile = open(filename,'r')
        
        # read first to lines to get to header line
        outfile.readline()
        outfile.readline()
        
        hdr = outfile.readline().replace('%','').replace('\n','').split(',')

        data = np.loadtxt(filename,comments='%')
410
        shape_len = len(data.shape)
Daniel Brown's avatar
Daniel Brown committed
411
        
412
413
414
415
416
417
418
419
420
421
        if shape_len > 1:
            rows,cols = data.shape
            x = data[:,0]
            y = data[:,1:cols].squeeze()
        else:
            rows = 1
            cols = data.shape[0]
            
            x = data[0]
            y = data[1:cols].squeeze()
Daniel Brown's avatar
Daniel Brown committed
422
        
423
424
425
        return [x, y, hdr]
            
    def generateKatScript(self) :
Daniel Brown's avatar
Daniel Brown committed
426
        """ Generates the kat file which can then be run """
Daniel Brown's avatar
Daniel Brown committed
427
        
Daniel Brown's avatar
Daniel Brown committed
428
429
        out = []    
        
430
        for key in self.__blocks:
431
            objs = self.__blocks[key].contents
432
433
            
            out.append("%%% FTblock " + key + "\n")
434
            
435
436
            for obj in objs:
                if isinstance(obj, str):
Daniel Brown's avatar
Daniel Brown committed
437
                    out.append(obj + '\n')
438
439
440
441
442
443
444
445
446
447
                    
                elif isinstance(obj, Component) or isinstance(obj, Detector) or isinstance(obj, Command):
                    txt = obj.getFinesseText() 
                    
                    if txt != None:
                        if isinstance(txt,list):
                            for t in txt: out.append(t + "\n")
                        else:
                            out.append(txt + "\n")
                            
448
            out.append("%%% FTend " + key + "\n")
Daniel Brown's avatar
Daniel Brown committed
449
450
451
452
        
        if self.noxaxis != None and self.noxaxis == True:
            out.append("noxaxis\n")
            
453
454
455
456
457
458
459
460
461
462
        # now loop through all the nodes and get any gauss commands
        for key in self.nodes.getNodes():
            txt = self.nodes.getNodes()[key].getFinesseText()
            
            if txt != None:
                if isinstance(txt,list):
                    for t in txt: out.append(t+ "\n")
                else:
                    out.append(txt + "\n")
        
Daniel Brown's avatar
Daniel Brown committed
463
464
        if self.phase != None: out.append("phase {0}\n".format(self.phase))
        if self.maxtem != None: out.append("maxtem {0}\n".format(self.maxtem))            
465

Daniel Brown's avatar
Daniel Brown committed
466
467
        # ensure we don't do any plotting. That should be handled
        # by user themselves
Daniel Brown's avatar
Daniel Brown committed
468
469
470
471
472
473
        out.append("gnuterm no\n")
        out.append("pyterm no\n")
        
        return out
        
    def openGUI(self):
Daniel Brown's avatar
Daniel Brown committed
474
475
476
477
478
479
480
481
482
        if NO_GUI:
            print  "No PyQt4 module was installed so cannot open a GUI"
        else:
            if self.pykatgui == None:
                #self.app = QtGui.QApplication([""])
                self.pykatgui = pyKatGUI(self)
                self.pykatgui.main()
            else:
                self.pykatgui.show()
Daniel Brown's avatar
Daniel Brown committed
483
484
485
486
    
    def getComponents(self):
        return self.__components.values()
    
487
488
489
    def hasComponent(self, name):
        return (name in self.__components)
    
490
    def _newName(self, container, prefix):
491
492
493
        n = 1
        name = "{0}{1}".format(prefix, n)
        
494
        while name in container:
495
496
497
498
499
            n += 1
            name = "{0}{1}".format(prefix,n)
        
        return name
    
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
    def getNewComponentName(self,prefix):
        '''
        Returns a name for a component which hasn't already been added.
        Returns [prefix] + number, where number is greater than 1. e.g.
        if m1 exists getNewName('m') will return 'm2'
        '''
        return self._newName(self.__components, prefix)
    
    def getNewDetectorName(self,prefix):
        '''
        Returns a name for a component which hasn't already been added.
        Returns [prefix] + number, where number is greater than 1. e.g.
        if m1 exists getNewName('m') will return 'm2'
        '''
        return self._newName(self.__detectors, prefix)
        
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
    def getNewNodeNames(self,prefix,N=1):
        '''
        Returns a list of names for N number of nodes which haven't already been added.
        Returns [prefix] + number, where number is greater than 1. e.g.
        if m1 exists getNewName('m') will return 'm2'
        '''
        rtn = []
        n = 1
        
        for M in range(1,N+1):
            name = "{0}{1}".format(prefix, n)
            
            while name in self.nodes.getNodes() or (name in rtn):
                n += 1
                name = "{0}{1}".format(prefix,n)
        
            rtn.append(name)
            
        return rtn
        
Daniel Brown's avatar
Daniel Brown committed
536
537
538
539
    
    def __add_detector(self, det):

        if not isinstance(det, Detector):
540
            raise exceptions.ValueError("Argument is not of type Detector")
Daniel Brown's avatar
Daniel Brown committed
541
542
        
        name = det.name
543
        fget = lambda self: self.__get_detector(name)
Daniel Brown's avatar
Daniel Brown committed
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
        
        setattr(self.__class__, name, property(fget))
        setattr(self, '__det_' + name, det)                   

    def __get_detector(self, name):
        return getattr(self, '__det_' + name) 
        
        
    def __add_command(self, com):

        if not isinstance(com, Command):
            raise exceptions.ValueError("Argument is not of type Command")
        
        name = com.__class__.__name__
        fget = lambda self: self.__get_command(name)
        
        setattr(self.__class__, name, property(fget))
        setattr(self, '__com_' + name, com)                   

    def __get_command(self, name):
        return getattr(self, '__com_' + name)            
    
    def __add_component(self, comp):

        if not isinstance(comp, Component):
            raise exceptions.ValueError("Argument is not of type Component")
            
        fget = lambda self: self.__get_component(comp.name)
        
        setattr(self.__class__, comp.name, property(fget))
        setattr(self, '__comp_' + comp.name, comp)                   

    def __get_component(self, name):
577
        return getattr(self, '__comp_' + name)        
578
579
580
581
582
583
584
585
586
587
588
589
590
591

    def remove_comments(self, string):
        pattern = r"(\".*?\"|\'.*?\'|%{3}[^\r\n]*$)|(/\*.*?\*/|%[^\r\n]*$|#[^\r\n]*$|//[^\r\n]*$)"
        # first group captures quoted strings (double or single)
        # second group captures comments (//single-line or /* multi-line */)
        regex = re.compile(pattern, re.MULTILINE|re.DOTALL)
        def _replacer(match):
            # if the 2nd group (capturing comments) is not None,
            # it means we have captured a non-quoted (real) comment string.
            if match.group(2) is not None:
                return "" # so we will return empty to remove the comment
            else: # otherwise, we will return the 1st group
                return match.group(1) # captured quoted-string
        return regex.sub(_replacer, string)