pynoddy.history — pynoddy documentation

# Source code for pynoddy.history

    '''Noddy history file wrapper
    Created on 24/03/2014

    @author: Florian Wellmann
    '''

    import time # for header in model generation
    import numpy as np
    # import numpy as np
    # import matplotlib.pyplot as plt

    import events

    class NoddyHistory(object):
        """Class container for Noddy history files"""

        def __init__(self, history=None, **kwds):
            """Methods to analyse and change Noddy history files

            **Arguments**:
                - *history* = string : Name of Noddy history file

            **Optional Keywords**:
                - *url* = url : link to history file on web (e.g. to download
                and open directly from Atlas of Structural Geophysics,
                http://virtualexplorer.com.au/special/noddyatlas/index.html

                - *verbose* = True if this function should print output to the printstream. Default is False.

            Note: if both a (local) history is given and a URL, the local
            file is opened!
            """

            vb = kwds.get('verbose',False)

            if history is None:
                if kwds.has_key("url"):
                    self.load_history_from_url(kwds['url'])
                    self.determine_events(verbose = vb)
                else:
                    # generate a new history
                    self.create_new_history()
            else:
                # load existing history
                self.load_history(history)
                self.determine_events(verbose = vb)

        def __repr__(self):
            """Print out model information"""
            return self.get_info_string()

        def info(self, **kwds):
            """Print out model information

            **Optional keywords**:
                - *events_only* = bool : only information on events
            """
            print self.get_info_string(**kwds)

        def get_info_string(self, **kwds):
            """Get model information as string

            **Optional keywords**:
                - *events_only* = bool : only information on events
            """
            events_only = kwds.get("events_only", False)

            local_os = ""

            if not events_only:
                # First: check if all information available
                if not hasattr(self, 'extent_x'): self.get_extent()
                if not hasattr(self, 'origin_x'): self.get_origin()
                if not hasattr(self, 'cube_size'): self.get_cube_size()
                if not hasattr(self, 'filename'): self.get_filename()
                if not hasattr(self, 'date_saved'): self.get_date_saved()
                local_os +=(60 * "*" + "\n\t\t\tModel Information\n" + 60 * "*")
                local_os +=("\n\n")
            if self.n_events == 0:
                local_os +=("The model does not yet contain any events\n")
            else:
                local_os +=("This model consists of %d events:\n" % self.n_events)
                for k,ev in self.events.items():
                    local_os +=("\t(%d) - %s\n" % (k,ev.event_type))
            if not events_only:
                local_os +=("The model extent is:\n")
                local_os +=("\tx - %.1f m\n" % self.extent_x)
                local_os +=("\ty - %.1f m\n" % self.extent_y)
                local_os +=("\tz - %.1f m\n" % self.extent_z)

                local_os +=("Number of cells in each direction:\n")
                local_os +=("\tnx = %d\n" % (self.extent_x / self.cube_size))
                local_os +=("\tny = %d\n" % (self.extent_y / self.cube_size))
                local_os +=("\tnz = %d\n" % (self.extent_z / self.cube_size))

                local_os +=("The model origin is located at: \n\t(%.1f, %.1f, %.1f)\n" % (self.origin_x,
                                                                              self.origin_y,
                                                                              self.origin_z))

                local_os +=("The cubesize for model export is: \n\t%d m\n" % self.cube_size)
                # and now some metadata
                local_os +=("\n\n")
                local_os +=(60 * "*" + "\n\t\t\tMeta Data\n" + 60 * "*")
                local_os +=("\n\n")
                local_os +=("The filename of the model is:\n\t%s\n" % self.filename)
                local_os +=("It was last saved (if origin was a history file!) at:\n\t%s\n" % self.date_saved)

            return local_os

        def get_origin(self):
            """Get coordinates of model origin and return and store in local variables

            **Returns**: (origin_x, origin_y, origin_z)
            """
            # check if footer_lines exist (e.g. read in from file)
            # if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

            for i,line in enumerate(self.footer_lines):
                if "Origin X" in line:
                    self.origin_x = float(self.footer_lines[i].split("=")[1])
                    self.origin_y = float(self.footer_lines[i+1].split("=")[1])
                    self.origin_z = float(self.footer_lines[i+2].split("=")[1])
                    break

            return(self.origin_x, self.origin_y, self.origin_z)

        def set_origin(self, origin_x, origin_y, origin_z):
            """Set coordinates of model origin and update local variables

            **Arguments**:
                - *origin_x* = float : x-location of model origin
                - *origin_y* = float : y-location of model origin
                - *origin_z* = float : z-location of model origin
            """
            # check if footer_lines exist (e.g. read in from file)
            # if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

            self.origin_x = origin_x
            self.origin_y = origin_y
            self.origin_z = origin_z
            origin_x_line = "    Origin X    =   %.2f\n" % origin_x
            origin_y_line = "    Origin Y    =   %.2f\n" % origin_y
            origin_z_line = "    Origin Z    =   %.2f\n" % origin_z

            for i,line in enumerate(self.footer_lines):
                if "Origin X" in line:
                    self.footer_lines[i] = origin_x_line
                    self.footer_lines[i+1] = origin_y_line
                    self.footer_lines[i+2] = origin_z_line
                    break

        def get_extent(self):
            """Get model extent and return and store in local variables

            **Returns**: (extent_x, extent_y, extent_z)
            """
            # check if footer_lines exist (e.g. read in from file)
            # if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

            for i,line in enumerate(self.footer_lines):
                if "Length X" in line:
                    self.extent_x = float(self.footer_lines[i].split("=")[1])
                    self.extent_y = float(self.footer_lines[i+1].split("=")[1])
                    self.extent_z = float(self.footer_lines[i+2].split("=")[1])
                    break

            return(self.extent_x, self.extent_y, self.extent_z)

        def set_extent(self, extent_x, extent_y, extent_z):
            """Set model extent and update local variables

            **Arguments**:
                - *extent_x* = float : extent in x-direction
                - *extent_y* = float : extent in y-direction
                - *extent_z* = float : extent in z-direction
            """
            # check if footer_lines exist (e.g. read in from file)
            # if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

            self.extent_x = extent_x
            self.extent_y = extent_y
            self.extent_z = extent_z
            extent_x_line = "    Length X    =   %.2f\n" % extent_x
            extent_y_line = "    Length Y    =   %.2f\n" % extent_y
            extent_z_line = "    Length Z    =   %.2f\n" % extent_z

            for i,line in enumerate(self.footer_lines):
                if "Length X" in line:
                    self.footer_lines[i] = extent_x_line
                    self.footer_lines[i+1] = extent_y_line
                    self.footer_lines[i+2] = extent_z_line
                    break

        def get_drillhole_data(self, x, y, **kwds):
            """Get geology values along 1-D profile at position x,y with a 1 m resolution

            The following steps are performed:
            1. creates a copy of the entire object,
            2. sets values of origin, extent and geology cube size,
            3. saves model to a temporary file,
            4. runs Noddy on that file
            5. opens and analyses output
            6. deletes temporary files

            Note: this method only works if write access to current directory
            is enabled and noddy can be executed!

            **Arguments**:
                - *x* = float: x-position of drillhole
                - *y* = float: y-position of drillhole

            **Optional Arguments**:
                - *z_min* = float : minimum depth of drillhole (default: model range)
                - *z_max* = float : maximum depth of drillhole (default: model range)
                - *resolution* = float : resolution along profile (default: 1 m)
            """
            # resolve keywords
            resolution = kwds.get("resolution", 1)
            self.get_extent()
            self.get_origin()
            z_min = kwds.get("z_min", self.origin_z)
            z_max = kwds.get("z_max", self.extent_z)
            # 1. create copy
            import copy
            tmp_his = copy.deepcopy(self)
            tmp_his.write_history("test.his")
            # 2. set values
            tmp_his.set_origin(x, y, z_min)
            tmp_his.set_extent(resolution, resolution, z_max)
            tmp_his.change_cube_size(resolution)
            # 3. save temporary file
            tmp_his_file = "tmp_1D_drillhole.his"
            tmp_his.write_history(tmp_his_file)
            tmp_out_file = "tmp_1d_out"
            # 4. run noddy
            import pynoddy
            import pynoddy.output

            pynoddy.compute_model(tmp_his_file, tmp_out_file)
            # 5. open output
            tmp_out = pynoddy.output.NoddyOutput(tmp_out_file)
            # 6.
            return tmp_out.block[0,0,:]

        def load_history(self, history):
            """Load Noddy history

            **Arguments**:
                - *history* = string : Name of Noddy history file
            """
            self.history_lines = open(history, 'r').readlines()
            # set flag for model loaded from file
            self._from_file = True
            # get footer lines
            self.get_footer_lines()

        def load_history_from_url(self, url):
            """Directly load a Noddy history from a URL

            This method is useful to load a model from the Structural Geophysics
            Atlas on the pages of the Virtual Explorer.
            See: http://tectonique.net/asg

            **Arguments**:
                - *url* : url of history file
            """
            import urllib2
            response = urllib2.urlopen(url)
            tmp_lines = response.read().split("\n")
            self.history_lines = []
            for line in tmp_lines:
                # append EOL again for consistency
                self.history_lines.append(line + "\n")
            # set flag for model loaded from URL
            self._from_url = True
            # get footer lines
            self.get_footer_lines()

        def determine_model_stratigraphy(self):
            """Determine stratigraphy of entire model from all events"""
            self.model_stratigraphy = []
            for e in np.sort(self.events.keys()):
                if self.events[e].event_type == 'STRATIGRAPHY':
                    self.model_stratigraphy += self.events[e].layer_names
                if self.events[e].event_type == 'UNCONFORMITY':
                    self.model_stratigraphy += self.events[e].layer_names
                if self.events[e].event_type == 'DYKE':
                    self.model_stratigraphy += self.events[e].name
                if self.events[e].event_type == 'PLUG':
                    self.model_stratigraphy += self.events[e].name

        def determine_events(self, **kwds):
            """Determine events and save line numbers

            .. note:: Parsing of the history file is based on a fixed Noddy output order.
                      If this is, for some reason (e.g. in a changed version of Noddy) not the case, then
                      this parsing might fail!

              **Optional Keywords**:
               - verbose = True if this function is should write to the print bufffer, otherwise False. Default is False.
            """

            vb = kwds.get('verbose',False)

            self._raw_events = []
            for i,line in enumerate(self.history_lines):
                if "No of Events" in line:
                    self.n_events = int(line.split("=")[1])
                elif "Event #" in line:
                    event = {'type': line.split('=')[1].rstrip(), 'num': int(line[7:9]), 'line_start': i}
                    self._raw_events.append(event)
                # finally: if the definition for BlockOptions starts, the event definition is over
                elif "BlockOptions" in line:
                    last_event_stop = i-2
            # now: find the line ends for the single event blocks
            for i,event in enumerate(self._raw_events[1:]):
                self._raw_events[i]['line_end'] = event['line_start']-1
            # now adjust for last event
            self._raw_events[-1]['line_end'] = last_event_stop

            self.events = {} # idea: create events as dictionary so that it is easier
            # to swap order later!
            # now create proper event objects for these events
            if vb:
                print "Loaded model with the following events:"

            for e in self._raw_events:
                event_lines = self.history_lines[e['line_start']:e['line_end']+1]

                if vb:
                    print e['type']

                if 'FAULT' in e['type']:
                    ev = events.Fault(lines = event_lines)
                elif 'SHEAR_ZONE' in e['type']:
                    ev = events.Shear(lines = event_lines)
                elif 'FOLD' in e['type']:
                    ev = events.Fold(lines = event_lines)
                elif 'UNCONFORMITY' in e['type']:
                    ev = events.Unconformity(lines = event_lines)
                elif 'STRATIGRAPHY' in e['type']:
                    ev = events.Stratigraphy(lines = event_lines)
                elif 'TILT' in e['type']: # AK
                    ev = events.Tilt(lines = event_lines)
                elif 'DYKE' in e['type']:
                    ev = events.Dyke(lines = event_lines)
                elif 'PLUG' in e['type']:
                    ev = events.Plug(lines = event_lines)
                elif 'STRAIN' in e['type']:
                    ev = events.Strain(lines = event_lines)
                else:
                    print "Warning: event of type %s has not been implemented in PyNoddy yet" % e['type']
                    continue
                # now set shared attributes (those defined in superclass Event)
                order = e['num'] #retrieve event number
                self.events[order] = ev #store events sequentially

            # determine overall begin and end of the history events
            self.all_events_begin = self._raw_events[0]['line_start']
            self.all_events_end = self._raw_events[-1]['line_end']

        def copy_events(self):
            """Create a copy of the current event state"""
            import copy
            return copy.deepcopy(self.events)

        def get_cube_size(self, **kwds):
            """Determine cube size for model export
               **Optional Args**
                -type: choose geology or geophysics cube size to return. Should be either 'Geology' (default) or 'Geophysics'
            """

            #get args
            sim_type = kwds.get("type", 'Geophysics') #everything seems to use this
            cube_string = 'Geophysics Cube Size' #get geology cube size by default
            if ('Geology' in sim_type):
                cube_string = 'Geology Cube Size' #instead get geology cube size
                print "Warning: pynoddy uses the geophysics cube size for all calculations... changing the geology cube size will have no effect internally."

           # check if footer exists, if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

            for line in self.footer_lines:
                if cube_string in line:
                    self.cube_size = float(line.split('=')[1].rstrip())
                    return self.cube_size

        def get_filename(self):
            """Determine model filename from history file/ header"""
            self.filename = self.history_lines[0].split('=')[1].rstrip()

        def get_date_saved(self):
            """Determine the last savepoint of the file"""
            self.date_saved = self.history_lines[1].split('=')[1].rstrip()

        def change_cube_size(self, cube_size, **kwds):
            """Change the model cube size (isotropic)

            **Arguments**:
                - *cube_size* = float : new model cube size
            """

            # check if footer_lines exist (e.g. read in from file)
            # if not: create from template
            if not hasattr(self, "footer_lines"):
                self.create_footer_from_template()

    #        lines_new = self.history_lines[:]
            for i,line in enumerate(self.footer_lines):
                if "Geophysics Cube Size" in line:  #correct line, make change
                    l = line.split('=')
                    l_new = '%7.2f\r\n' % cube_size
                    line_new = l[0] + "=" + l_new
                    self.footer_lines[i] = line_new

                if "Geology Cube Size" in line: #change geology cube size also
                    l = line.split('=')
                    l_new = '%7.2f\r\n' % cube_size
                    line_new = l[0] + "=" + l_new
                    self.footer_lines[i] = line_new
            # assign changed lines back to object
    #        self.history_lines = lines_new[:]

        def swap_events(self, event_num_1, event_num_2):
            """Swap two geological events in the timeline

            **Arguments**:
                - *event_num_1/2* = int : number of events to be swapped ("order")
            """
            # events have to be copied, otherwise only a reference is passed!
            event_tmp = self.events[event_num_1]
            self.events[event_num_1] = self.events[event_num_2]
            self.events[event_num_2] = event_tmp
            self.update_event_numbers()

        def reorder_events(self, reorder_dict):
            """Reorder events accoring to assignment in reorder_dict

            **Arguments**:
                - *reorder_dict* = dict : for example {1 : 2, 2 : 3, 3 : 1}
            """
            tmp_events = self.events.copy()
            for key, value in reorder_dict.items():
                try:
                    tmp_events[value] = self.events[key]
                except KeyError:
                    print("Event with id %d is not defined, please check!" % value)
            self.events = tmp_events.copy()
            self.update_event_numbers()

        def update_event_numbers(self):
            """Update event numbers in 'Event #' line in noddy history file"""
            for key, event in self.events.items():
                event.set_event_number(key)

        def update_all_event_properties(self):
            """Update properties of all events - in case changes were made"""
            for event in self.events.values():
                event.update_properties()

    #
    #class NewHistory():
    #    """Methods to create a Noddy model"""
    #
        def create_new_history(self):
            """Methods to create a Noddy model

            """
            # set event counter
            self.event_counter = 0
            self.all_events_begin = 7 # default after header
            self.all_events_end = 7
            # initialise history lines
            self.history_lines = []
            self.events = {}

        def get_ev_counter(self):
            """Event counter for implicit and continuous definition of events"""
            self.event_counter += 1
            return self.event_counter

        def add_event(self, event_type, event_options, **kwds):
            """Add an event type to history

            **Arguments**:
                - *event_type* = string : type of event, legal options to date are:
                'stratigraphy', 'fault', 'fold', 'unconformity'
                - *event_options* = list : required options to create event (event dependent)
            **Optional keywords**:
                - *event_num* = int : event number (default: implicitly defined with increasing counter)
            """

            event_num = kwds.get("event_num", self.get_ev_counter())

            if event_type == 'stratigraphy':
                ev = self._create_stratigraphy(event_options)
                ev.event_type = 'STRATIGRAPHY'

            elif event_type == 'fault':
                ev = self._create_fault(event_options)
                ev.event_type = 'FAULT'

            elif event_type == 'tilt': # AK
                ev = self._create_tilt(event_options)
                ev.event_type = 'TILT'

            elif event_type == 'unconformity': # AK
                ev = self._create_unconformity(event_options)
                ev.event_type = 'UNCONFORMITY'

            else:
                raise NameError('Event type %s not (yet) implemented' % event_type)

            ev.set_event_number(event_num)
            self.events[event_num] = ev

            # update beginning and ending of events in history
            self.all_events_end = self.all_events_end + len(ev.event_lines)

            # add event to history lines, as well (for consistency with other methods)
            self.history_lines[:self.all_events_begin] + \
            ev.event_lines + \
            self.history_lines[self.all_events_end:]

        def _create_header(self):
            """Create model header, include actual date"""
            t = time.localtime() # get current time
            time_string = "%d/%d/%d %d:%d:%d" % (t.tm_mday,
                                                 t.tm_mon,
                                                 t.tm_year,
                                                 t.tm_hour,
                                                 t.tm_min,
                                                 t.tm_sec)
            self.header_lines = """#Filename = """ + self.filename + """
    #Date Saved = """ + time_string + """
    FileType = 111
    Version = 7.11

    """

        @staticmethod
        def _create_stratigraphy(event_options):
            """Create a stratigraphy event

            **Arguments**:
                - *event_options* = list : list of required and optional settings for event
                Options are:
                'num_layers' = int : number of layers (required)
                'layer_names' = list of strings : names for layers (default names otherwise)
                'layer_thickness' = list of floats : thicknesses for all layers
            """
            ev = events.Stratigraphy()
            tmp_lines = [""]
            tmp_lines.append("\tNum Layers\t= %d" % event_options['num_layers'])
            for i in range(event_options['num_layers']):
                """Add stratigraphy layers"""
                layer_name = event_options['layer_names'][i]
                cum_thickness = np.cumsum(event_options['layer_thickness'])
                layer_lines = _Templates().strati_layer
                # now replace required variables
                layer_lines = layer_lines.replace("$NAME$", layer_name)
                layer_lines = layer_lines.replace("$HEIGHT$", "%.1f" % cum_thickness[i])
                layer_lines = layer_lines.replace("    ", "\t")
                # split lines and add to event lines list:
                for layer_line in layer_lines.split("\n"):
                    tmp_lines.append(layer_line)

            # append event name
            tmp_lines.append("""\tName\t= Strat
    """)

            # event lines are defined in list:
            tmp_lines_list = []
            for line in tmp_lines:
                tmp_lines_list.append(line + "\n")
            ev.set_event_lines(tmp_lines_list)
            ev.num_layers = event_options['num_layers']
            return ev

        def _create_fault(self, event_options):
            """Create a fault event

            **Arguments**:
                - *event_options* = list : list of required and optional settings for event;
                Options are:
                'name' = string : name of fault event
                'pos' = (x,y,z) : position of reference point (floats)
                    .. note::     for convenience, it is possible to assign 'top' to z
                                  for position at "surface"
                'dip_dir' = [0,360] : dip direction of fault
                'dip' = [0,90] : dip angle of fault
                'slip' = float : slip along fault
            """
            ev = events.Fault()
            tmp_lines = [""]
            fault_lines = _Templates.fault
            # substitute text with according values
            fault_lines = fault_lines.replace("$NAME$", event_options['name'])
            fault_lines = fault_lines.replace("$POS_X$", "%.1f" % event_options['pos'][0])
            fault_lines = fault_lines.replace("$POS_Y$", "%.1f" % event_options['pos'][1])
            if event_options['pos'] == 'top':
                # recalculate z-value to be at top of model
                z = self.zmax
                fault_lines = fault_lines.replace("$POS_Z$", "%.1f" % z)
            else:
                fault_lines = fault_lines.replace("$POS_Z$", "%.1f" % event_options['pos'][2])
            fault_lines = fault_lines.replace("$DIP_DIR$", "%.1f" % event_options['dip_dir'])
            fault_lines = fault_lines.replace("$DIP$", "%.1f" % event_options['dip'])
            fault_lines = fault_lines.replace("$SLIP$", "%.1f" % event_options['slip'])

            # now split lines and add as list entries to event lines
            # event lines are defined in list:

            # split lines and add to event lines list:
            for layer_line in fault_lines.split("\n"):
                tmp_lines.append(layer_line)

            tmp_lines_list = []
            for line in tmp_lines:
                tmp_lines_list.append(line + "\n")
            ev.set_event_lines(tmp_lines_list)
            return ev

        # AK 2014-10
        def _create_tilt(self, event_options):
            """Create a tilt event

            **Arguments**:
                - *event_options* = list : list of required and optional settings for event;
                Options are:
                'name' = string : name of fault event
                'pos' = (x,y,z) : position of reference point (floats)
                    .. note::     for convenience, it is possible to assign 'top' to z
                                  for position at "surface"
                'rotation' = [0,360] : dip?
                'plunge_direction' = [0,360] : strike of plunge, measured from x axis
                'plunge' = float : ?
            """
            ev = events.Tilt()
            tmp_lines = [""]
            tilt_lines = _Templates.tilt
            # substitute text with according values
            tilt_lines = tilt_lines.replace("$NAME$", event_options['name'])
            tilt_lines = tilt_lines.replace("$POS_X$", "%.1f" % event_options['pos'][0])
            tilt_lines = tilt_lines.replace("$POS_Y$", "%.1f" % event_options['pos'][1])
            if event_options['pos'] == 'top':
                # recalculate z-value to be at top of model
                z = self.zmax
                tilt_lines = tilt_lines.replace("$POS_Z$", "%.1f" % z)
            else:
                tilt_lines = tilt_lines.replace("$POS_Z$", "%.1f" % event_options['pos'][2])
            tilt_lines = tilt_lines.replace("$ROTATION$", "%.1f" % event_options['rotation'])
            tilt_lines = tilt_lines.replace("$PLUNGE_DIRECTION$", "%.1f" % event_options['plunge_direction'])
            tilt_lines = tilt_lines.replace("$PLUNGE$", "%.1f" % event_options['plunge'])

            # now split lines and add as list entries to event lines
            # event lines are defined in list:

            # split lines and add to event lines list:
            for tilt_line in tilt_lines.split("\n"):
                tmp_lines.append(tilt_line)

            tmp_lines_list = []
            for line in tmp_lines:
                tmp_lines_list.append(line + "\n")
            ev.set_event_lines(tmp_lines_list)
            return ev

        # AK 2014-10
        def _create_unconformity(self, event_options):
            """Create a unconformity event

            **Arguments**:
                - *event_options* = list : list of required and optional settings for event;
                Options are:
                'name' = string : name of unconformity event
                'pos' = (x,y,z) : position of reference point (floats)
                    .. note::     for convenience, it is possible to assign 'top' to z
                                  for position at "surface"
                'rotation' = [0,360] : dip?
                'plunge_direction' = [0,360] : strike of plunge, measured from x axis
                'plunge' = float : ?
            """
            ev = events.Unconformity()
            tmp_lines = [""]
            unconformity_lines = _Templates.unconformity
            # substitute text with according values
            unconformity_lines = unconformity_lines.replace("$NAME$", event_options['name'])
            unconformity_lines = unconformity_lines.replace("$POS_X$", "%.1f" % event_options['pos'][0])
            unconformity_lines = unconformity_lines.replace("$POS_Y$", "%.1f" % event_options['pos'][1])
            if event_options['pos'] == 'top':
                # recalculate z-value to be at top of model
                z = self.zmax
                unconformity_lines = unconformity_lines.replace("$POS_Z$", "%.1f" % z)
            else:
                unconformity_lines = unconformity_lines.replace("$POS_Z$", "%.1f" % event_options['pos'][2])
            unconformity_lines = unconformity_lines.replace("$DIP_DIRECTION$", "%.1f" % event_options['dip_direction'])
            unconformity_lines = unconformity_lines.replace("$DIP$", "%.1f" % event_options['dip'])

            # split lines and add to event lines list:
            for unconformity_line in unconformity_lines.split("\n"):
                tmp_lines.append(unconformity_line)

            # unconformity has a stratigraphy block
            tmp_lines.append("\tNum Layers\t= %d" % event_options['num_layers'])
            for i in range(event_options['num_layers']):
                """Add stratigraphy layers"""
                layer_name = event_options['layer_names'][i]
                cum_thickness = np.cumsum(event_options['layer_thickness'])
                layer_lines = _Templates().strati_layer
                # now replace required variables
                layer_lines = layer_lines.replace("$NAME$", layer_name)
                layer_lines = layer_lines.replace("$HEIGHT$", "%.1f" % cum_thickness[i])
                layer_lines = layer_lines.replace("    ", "\t")
                # split lines and add to event lines list:
                for layer_line in layer_lines.split("\n"):
                    tmp_lines.append(layer_line)

            # append event name
            tmp_lines.append("""\tName\t= %s""" % event_options.get('name','Unconf'))

            tmp_lines_list = []
            for line in tmp_lines:
                tmp_lines_list.append(line + "\n")
            ev.set_event_lines(tmp_lines_list)
            return ev

        def set_event_params(self, params_dict):
            """set multiple event parameters according to settings in params_dict

            **Arguments**:
                - *params_dict* = dictionary : entries to set (multiple) parameters

            """
            for key,sub_dict in params_dict.items():
                for sub_key, val in sub_dict.items():
                    self.events[key].properties[sub_key] = val

        def change_event_params(self, changes_dict):
            """Change multiple event parameters according to settings in changes_dict

            **Arguments**:
                - *changes_dict* = dictionary : entries define relative changes for (multiple) parameters

            Per default, the values in the dictionary are added to the event parameters.
            """
            #print changes_dict
            for key,sub_dict in changes_dict.items(): #loop through events (key)
                for sub_key, val in sub_dict.items(): #loop through parameters being changed (sub_key)
                    self.events[key].properties[sub_key] += val

        def get_event_params( self, event_number ):
            '''
            Returns the parameter dictionary for a given event.

            **Arguments**:
             - *event_number* = the event to get a parameter for (integer)

             **Returns**
              - Returns the parameter dictionary for the requested event
            '''
            return self.events[event_number].properties

        def get_event_param( self, event_number, name ):
            '''
            Returns the value of a given parameter for a given event.

            **Arguments**:
             - *event_number* = the event to get a parameter for (integer)
             - *name* = the name of the parameter to retreive (string)

             **Returns**
              - Returns the value of the request parameter, or None if it does not
                exists.
            '''
            try:
                ev = self.events[event_number].properties
                return ev[name]
            except KeyError:
                return None #property does not exist

        def write_history(self, filename):
            """Write history to new file

            **Arguments**:
                - *filename* = string : filename of new history file

            .. hint:: Just love it how easy it is to 'write history' with Noddy ;-)

            """
            # before saving: update all event properties (in case changes were made)
            self.update_all_event_properties()

            # first: create header
            if not hasattr(self, "filename"):
                self.filename = filename
            self._create_header()

            # initialise history lines
            history_lines = []

            # add header
            for line in self.header_lines.split("\n"):
                history_lines.append(line + "\n")

            # add number of events
            history_lines.append("No of Events\t= %d\n" % len(self.events))
            # add events
            for event_id in sorted(self.events.keys()):
                for line in self.events[event_id].event_lines:
                    history_lines.append(line)

            # add footer: from original footer or from template (if new file):
            if not hasattr(self,"footer_lines"):
                self.create_footer_from_template()

            # add footer
            for line in self.footer_lines:
                history_lines.append(line)

            f = open(filename, 'w')
            for i,line in enumerate(history_lines):
                # add empty line before "BlockOptions", if not there:
                if ('BlockOptions' in line) and (history_lines[i-1] != "\n"):
                    f.write("\n")

                #write line
                f.write(line)

            f.close()

    #===============================================================================
    # End of NoddyHistory
    #===============================================================================

    #===============================================================================
    # Templates for Noddy history file
    #===============================================================================

    class _Templates():

        header = """#Filename = simple_two_faults.his
    #Date Saved = 24/3/2014 14:21:0
    FileType = 111
    Version = 7.11"""

        strati_layer = """    Unit Name    = $NAME$
        Height    = $HEIGHT$
        Apply Alterations    = ON
        Density    = 4.00e+000
        Anisotropic Field    = 0
        MagSusX    = 1.60e-003
        MagSusY    = 1.60e-003
        MagSusZ    = 1.60e-003
        MagSus Dip    = 9.00e+001
        MagSus DipDir    = 9.00e+001
        MagSus Pitch    = 0.00e+000
        Remanent Magnetization    = 0
        Inclination    =  30.00
        Angle with the Magn. North    =  30.00
        Strength    = 1.60e-003
        Color Name    = Color 92
        Red    = 0
        Green    = 153
        Blue    = 48 """

        fault = """    Geometry    = Translation
        Movement    = Hanging Wall
        X    = $POS_X$
        Y    = $POS_Y$
        Z    =   $POS_Z$
        Dip Direction    =  $DIP_DIR$
        Dip    =  $DIP$
        Pitch    =  90.00
        Slip    = $SLIP$
        Rotation    =  30.00
        Amplitude    = 2000.00
        Radius    = 1000.00
        XAxis    = 2000.00
        YAxis    = 2000.00
        ZAxis    = 2000.00
        Cyl Index    =   0.00
        Profile Pitch    =  90.00
        Color Name    = Custom Colour 8
        Red    = 0
        Green    = 0
        Blue    = 254
        Fourier Series
            Term A 0    =   0.00
            Term B 0    =   0.00
            Term A 1    =   0.00
            Term B 1    =   1.00
            Term A 2    =   0.00
            Term B 2    =   0.00
            Term A 3    =   0.00
            Term B 3    =   0.00
            Term A 4    =   0.00
            Term B 4    =   0.00
            Term A 5    =   0.00
            Term B 5    =   0.00
            Term A 6    =   0.00
            Term B 6    =   0.00
            Term A 7    =   0.00
            Term B 7    =   0.00
            Term A 8    =   0.00
            Term B 8    =   0.00
            Term A 9    =   0.00
            Term B 9    =   0.00
            Term A 10    =   0.00
            Term B 10    =   0.00
        Name    = Fault Plane
        Type    = 1
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 6.280000
        Min Y Scale    = -1.000000
        Max Y Scale    = 1.000000
        Scale Origin    = 0.000000
        Min Y Replace    = -1.000000
        Max Y Replace    = 1.000000
        Num Points    = 21
            Point X    = 0
            Point Y    = 0
            Point X    = 31
            Point Y    = 30
            Point X    = 62
            Point Y    = 58
            Point X    = 94
            Point Y    = 80
            Point X    = 125
            Point Y    = 94
            Point X    = 157
            Point Y    = 99
            Point X    = 188
            Point Y    = 95
            Point X    = 219
            Point Y    = 81
            Point X    = 251
            Point Y    = 58
            Point X    = 282
            Point Y    = 31
            Point X    = 314
            Point Y    = 0
            Point X    = 345
            Point Y    = -31
            Point X    = 376
            Point Y    = -59
            Point X    = 408
            Point Y    = -81
            Point X    = 439
            Point Y    = -95
            Point X    = 471
            Point Y    = -100
            Point X    = 502
            Point Y    = -96
            Point X    = 533
            Point Y    = -82
            Point X    = 565
            Point Y    = -59
            Point X    = 596
            Point Y    = -32
            Point X    = 628
            Point Y    = -1
        Alteration Type     = NONE
        Num Profiles    = 12
        Name    = Density
        Type    = 2
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = 0.000000
        Max Y Scale    = 4.000000
        Scale Origin    = 1.000000
        Min Y Replace    = 0.000000
        Max Y Replace    = 10.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = -50
            Point X    = 628
            Point Y    = -50
        Name    = Anisotropy
        Type    = 3
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -10.000000
        Max Y Scale    = 10.000000
        Scale Origin    = 0.000000
        Min Y Replace    = -10.000000
        Max Y Replace    = 10.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - X Axis (Sus)
        Type    = 4
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -5.000000
        Max Y Scale    = 5.000000
        Scale Origin    = 0.000000
        Min Y Replace    = 2.000000
        Max Y Replace    = 8.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Y Axis (Sus)
        Type    = 5
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -5.000000
        Max Y Scale    = 5.000000
        Scale Origin    = 0.000000
        Min Y Replace    = 2.000000
        Max Y Replace    = 8.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Z Axis (Sus)
        Type    = 6
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -5.000000
        Max Y Scale    = 5.000000
        Scale Origin    = 0.000000
        Min Y Replace    = 2.000000
        Max Y Replace    = 8.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Dip (Sus)
        Type    = 7
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -180.000000
        Max Y Scale    = 180.000000
        Scale Origin    = 1.000000
        Min Y Replace    = -180.000000
        Max Y Replace    = 180.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 1
            Point X    = 628
            Point Y    = 1
        Name    = - Dip Dir (Sus)
        Type    = 8
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -360.000000
        Max Y Scale    = 360.000000
        Scale Origin    = 1.000000
        Min Y Replace    = -360.000000
        Max Y Replace    = 360.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Pitch (Sus)
        Type    = 9
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -360.000000
        Max Y Scale    = 360.000000
        Scale Origin    = 1.000000
        Min Y Replace    = -360.000000
        Max Y Replace    = 360.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = Remanence
        Type    = 10
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -10.000000
        Max Y Scale    = 10.000000
        Scale Origin    = 0.000000
        Min Y Replace    = -10.000000
        Max Y Replace    = 10.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Declination (Rem)
        Type    = 11
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -360.000000
        Max Y Scale    = 360.000000
        Scale Origin    = 1.000000
        Min Y Replace    = -360.000000
        Max Y Replace    = 360.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Inclination (Rem)
        Type    = 12
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -360.000000
        Max Y Scale    = 360.000000
        Scale Origin    = 1.000000
        Min Y Replace    = -360.000000
        Max Y Replace    = 360.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Name    = - Intensity (Rem)
        Type    = 13
        Join Type     = LINES
        Graph Length    = 200.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = -5.000000
        Max Y Scale    = 5.000000
        Scale Origin    = 0.000000
        Min Y Replace    = -5.000000
        Max Y Replace    = 5.000000
        Num Points    = 2
            Point X    = 0
            Point Y    = 0
            Point X    = 628
            Point Y    = 0
        Surface Type    = FLAT_SURFACE
        Surface Filename    =
        Surface Directory    = \\psf\Home
        Surface XDim    = 0.000000
        Surface YDim    = 0.000000
        Surface ZDim    = 0.000000
        Name    = $NAME$"""

        # AK 2014-10
        tilt = """X    =   $POS_X$
        Y    =   $POS_Y$
        Z    =   $POS_Z$
        Rotation     =  $ROTATION$
        Plunge Direction     = $PLUNGE_DIRECTION$
        Plunge     =   $PLUNGE$
        Name    = $NAME$"""

        unconformity = """X    =   $POS_X$
        Y    =   $POS_Y$
        Z    = $POS_Z$
        Dip Direction    =  $DIP_DIRECTION$
        Dip    =   $DIP$
        Alteration Type     = NONE
        Num Profiles    = 1
        Name    =
        Type    = 0
        Join Type     = LINES
        Graph Length    = 0.000000
        Min X    = 0.000000
        Max X    = 0.000000
        Min Y Scale    = 0.000000
        Max Y Scale    = 0.000000
        Scale Origin    = 0.000000
        Min Y Replace    = 0.000000
        Max Y Replace    = 0.000000
        Num Points    = 0
        Surface Type    = FLAT_SURFACE
        Surface Filename    =
        Surface Directory    = /tmp_mnt/sci6/users/mark/Atlas/case
        Surface XDim    = 0.000000
        Surface YDim    = 0.000000
        Surface ZDim    = 0.000000"""
        temp = """
        Num Layers    = 5
        Unit Name    = UC Base
        Height    = -32000
        Apply Alterations    = ON
        Density    = 3.50e+00
        Anisotropic Field    = 0
        MagSusX    = 1.50e-06
        MagSusY    = 1.60e-03
        MagSusZ    = 1.60e-03
        MagSus Dip    = 9.00e+01
        MagSus DipDir    = 9.00e+01
        MagSus Pitch    = 0.00e+00
        Remanent Magnetization    = 0
        Inclination    =  30.00
        Angle with the Magn. North    =  30.00
        Strength    = 1.60e-03
        Color Name    = Color 98
        Red    = 84
        Green    = 153
        Blue    = 0
        Unit Name    = UC Layer 1
        Height    = 5650
        Apply Alterations    = ON
        Density    = 3.50e+00
        Anisotropic Field    = 0
        MagSusX    = 1.50e-06
        MagSusY    = 1.60e-03
        MagSusZ    = 1.60e-03
        MagSus Dip    = 9.00e+01
        MagSus DipDir    = 9.00e+01
        MagSus Pitch    = 0.00e+00
        Remanent Magnetization    = 0
        Inclination    =  30.00
        Angle with the Magn. North    =  30.00
        Strength    = 1.60e-03
        Color Name    = Color 68
        Red    = 204
        Green    = 117
        Blue    = 0
        Name    = $NAME$"""

        # everything below events
        footer = """
    #BlockOptions
        Number of Views    = 1
        Current View    = 0
        NAME    = Default
        Origin X    =   0.00
        Origin Y    =   0.00
        Origin Z    = 5000.00
        Length X    = 10000.00
        Length Y    = 7000.00
        Length Z    = 5000.00
        Geology Cube Size    =  50.00
        Geophysics Cube Size    = 50.00

    #GeologyOptions
        Scale    =  10.00
        SectionDec    =  90.00
        WellDepth    = 5000.00
        WellAngleZ    =   0.00
        BoreholeX    =   0.00
        BoreholeX    =   0.00
        BoreholeX    = 5000.00
        BoreholeDecl    =  90.00
        BoreholeDip    =   0.00
        BoreholeLength    = 5000.00
        SectionX    =   0.00
        SectionY    =   0.00
        SectionZ    = 5000.00
        SectionDecl    =  90.00
        SectionLength    = 10000.00
        SectionHeight    = 5000.00
        topofile    = FALSE
        Topo Filename    =
        Topo Directory    = .
        Topo Scale    =   1.00
        Topo Offset    =   0.00
        Topo First Contour    = 100.00
        Topo Contour Interval    = 100.00
        Chair Diagram    = FALSE
        Chair_X    = 5000.00
        Chair_Y    = 3500.00
        Chair_Z    = 2500.00

    #GeophysicsOptions
        GPSRange     = 0
        Declination    =   0.00
        Inclination    = -67.00
        Intensity    = 63000.00
        Field Type    = FIXED
        Field xPos    =   0.00
        Field yPos    =   0.00
        Field zPos    = 5000.00
        Inclination Ori    =   0.00
        Inclination Change    =   0.00
        Intensity Ori    =  90.00
        Intensity Change    =   0.00
        Declination Ori    =   0.00
        Declination Change    =   0.00
        Altitude    =  80.00
        Airborne=     FALSE
        Calculation Method    = SPATIAL
        Spectral Padding Type    = RECLECTION_PADDING
        Spectral Fence    = 100
        Spectral Percent    = 100
        Constant Boxing Depth    =   0.00
        Clever Boxing Ratio    =   1.00
        Deformable Remanence=     FALSE
        Deformable Anisotropy=     TRUE
        Vector Components=     FALSE
        Project Vectors=     TRUE
        Pad With Real Geology=     FALSE
        Draped Survey=     FALSE

    #3DOptions
        Declination    = 150.000000
        Elevation    = 30.000000
        Scale    = 1.000000
        Offset X    = 1.000000
        Offset Y    = 1.000000
        Offset Z    = 1.000000
        Fill Type    = 2

    #ProjectOptions
        Susceptibility Units    = CGS
        Geophysical Calculation    = 2
        Calculation Type    = LOCAL_JOB
        Length Scale    = 0
        Printing Scale    = 1.000000
        Image Scale    = 10.000000
        New Windows    = FALSE
        Background Red Component    = 254
        Background Green Component    = 254
        Background Blue Component    = 254
        Internet Address    = 255.255.255.255
        Account Name    =
        Noddy Path    = ./noddy
        Help Path    = iexplore %h
        Movie Frames Per Event    = 3
        Movie Play Speed    =  10.00
        Movie Type    = 0
        Gravity Clipping Type    = RELATIVE_CLIPPING
        Gravity Image Display Clip Min    = 0.000000
        Gravity Image Display Clip Max    = 100.000000
        Gravity Image Display Type    = GREY
        Gravity Image Display Num Contour    = 25
        Magnetics Clipping Type    = RELATIVE_CLIPPING
        Magnetics Image Display Clip Min    = 0.000000
        Magnetics Image Display Clip Max    = 100.000000
        Magnetics Image Display Type    = GREY
        Magnetics Image Display Num Contour    = 25
        False Easting    = 0.000000
        False Northing    = 0.000000

    #Window Positions
        Num Windows    = 16
        Name    = Block Diagram
        X    = 60
        Y    = 60
        Width    = 500
        Height    = 300
        Name    = Movie
        X    = 60
        Y    = 60
        Width    = -1
        Height    = -1
        Name    = Well Log
        X    = 60
        Y    = 60
        Width    = 400
        Height    = 430
        Name    = Section
        X    = 14
        Y    = 16
        Width    = 490
        Height    = -1
        Name    = Topography Map
        X    = 60
        Y    = 60
        Width    = 490
        Height    = 375
        Name    = 3D Topography Map
        X    = 60
        Y    = 60
        Width    = 490
        Height    = 375
        Name    = 3D Stratigraphy
        X    = 60
        Y    = 60
        Width    = 490
        Height    = 375
        Name    = Line Map
        X    = 60
        Y    = 60
        Width    = 490
        Height    = -1
        Name    = Profile - From Image
        X    = 60
        Y    = 60
        Width    = 490
        Height    = 600
        Name    = Sterographic Projections
        X    = 60
        Y    = 60
        Width    = 430
        Height    = 430
        Name    = Stratigraphic Column
        X    = 60
        Y    = 60
        Width    = 230
        Height    = 400
        Name    = Image
        X    = 30
        Y    = 30
        Width    = -1
        Height    = -1
        Name    = Contour
        X    = 30
        Y    = 30
        Width    = -1
        Height    = -1
        Name    = Toolbar
        X    = 10
        Y    = 0
        Width    = -1
        Height    = -1
        Name    = History
        X    = 229
        Y    = 160
        Width    = 762
        Height    = 898
        Name    = History
        X    = 229
        Y    = 160
        Width    = 762
        Height    = 898

    #Icon Positions
        Num Icons    = 3
        Row    = 1
        Column    = 1
        X Position    = 1
        Y Position    = 1
        Row    = 1
        Column    = 2
        X Position    = 4
        Y Position    = 1
        Row    = 1
        Column    = 3
        X Position    = 7
        Y Position    = 1
        Floating Menu Rows    = 1
        Floating Menu Cols    = 24
    End of Status Report"""

    if __name__ == '__main__':
        # some testing and debugging:
        import os
        os.chdir(r'C:\Users\Sam\OneDrive\Documents\Masters\Models\Mt Painter')
        H1 = NoddyHistory("mt_pa_simplified.his")
        H1.swap_events(2, 3)
        H1.write_history("test")
        H2 = NoddyHistory("test")
        H2.events[10].properties['Radius'] = 2000
        H2.write_history("test2")