Added a new script utililty called "change_gds_cell" that can be
used to swap one cell for another in a GDS file.  It is very
simple and does not consider any subcells that might be inside
the swapped cell;  it is designed for quick ECOs for metal fixes.
diff --git a/common/change_gds_cell.py b/common/change_gds_cell.py
new file mode 100755
index 0000000..7e2c22a
--- /dev/null
+++ b/common/change_gds_cell.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Script to read a GDS file, and replace a single cell structure with a cell of
+# the same name from a different GDS file.  If a checksum is provided, then
+# the cell contents will be checked against the checksum before allowing the
+# replacement.  The checksum is just the sum of the length of all GDS records in
+# the cell.  A checksum can be determined by running this routine once without
+# supplying a checksum;  the checksum will be calculated and printed.
+# 
+# There are no checks to ensure that the replacement cell is in any way compatible
+# with the existing cell.  Validation must be done independently.  This script is
+# only a simple GDS data compositor.
+
+import os
+import sys
+
+def usage():
+    print('change_gds_cell.py <cell_name> <path_to_cell_gds> <path_to_gds_in> [<path_to_gds_out>] [-checksum=<checksum>]')
+
+if __name__ == '__main__':
+    debug = False
+
+    if len(sys.argv) == 1:
+        print("No options given to change_gds_cell.py.")
+        usage()
+        sys.exit(0)
+
+    optionlist = []
+    arguments = []
+
+    for option in sys.argv[1:]:
+        if option.find('-', 0) == 0:
+            optionlist.append(option)
+        else:
+            arguments.append(option)
+
+    if len(arguments) < 3 or len(arguments) > 4:
+        print("Wrong number of arguments given to change_gds_cell.py.")
+        usage()
+        sys.exit(0)
+
+    checksum = '0'
+    for option in optionlist:
+        if option == '-debug':
+            debug = True
+        elif option.split('=')[0] == '-checksum':
+            checksum = option.split('=')[1]
+
+    try:
+        checksum = int(checksum)
+    except:
+        print('Checksum must evaluate to an integer.')
+        sys.exit(1)
+
+    cellname = arguments[0]
+    cellsource = arguments[1]
+    source = arguments[2]
+
+    # If only three arguments are provided, then overwrite the source file.
+    if len(arguments) == 4:
+        dest = arguments[3]
+    else:
+        dest = arguments[2]
+
+    cellsrcdir = os.path.split(cellsource)[0]
+    cellinfile = os.path.split(cellsource)[1]
+    sourcedir = os.path.split(source)[0]
+    gdsinfile = os.path.split(source)[1]
+
+    destdir = os.path.split(dest)[0]
+    gdsoutfile = os.path.split(dest)[1]
+
+    print('Reading GDS file for alternate cell ' + cellname)
+    with open(cellsource, 'rb') as ifile:
+        celldata = ifile.read()
+
+    #----------------------------------------------------------------------
+    # Assume that celldata contains the cell in question.
+    # Find the extend of the data from 'beginstr' to 'endstr'
+    #----------------------------------------------------------------------
+    
+    datalen = len(celldata)
+    dataptr = 0
+    incell = False
+    datastart = dataend = -1
+    while dataptr < datalen:
+        # Read stream records up to 'beginstr', then save data through 'endstr'
+        bheader = celldata[dataptr:dataptr + 2]
+        reclen = int.from_bytes(bheader, 'big')
+        if reclen == 0:
+            print('Error: found zero-length record at position ' + str(dataptr))
+            break
+
+        rectype = celldata[dataptr + 2]
+        datatype = celldata[dataptr + 3]
+
+        brectype = rectype.to_bytes(1, byteorder='big')
+        bdatatype = datatype.to_bytes(1, byteorder='big')
+
+        if rectype == 5:            # beginstr
+            saveptr = dataptr
+
+        elif rectype == 6:            # strname
+            if datatype != 6:
+                print('Error: Structure name record is not a string!')
+                sys.exit(1)
+
+            bstring = celldata[dataptr + 4: dataptr + reclen]
+            strname = bstring.decode('ascii')
+            if strname == cellname:
+                print('Cell ' + cellname + ' found at position ' + str(saveptr))
+                datastart = saveptr
+                incell = True
+            elif debug:
+                print('Cell ' + strname + ' position ' + str(dataptr) + ' (ignored)')
+
+        elif rectype == 7:		# endstr
+            if incell:
+                incell = False
+                dataend = dataptr + reclen
+                print('Cell ' + cellname + ' ends at position ' + str(dataend))
+
+        # Advance the pointer past the data
+        dataptr += reclen
+
+    if datastart == -1 or dataend == -1:
+        print('Failed to find the cell data for ' + cellname)
+        sys.exit(1)
+
+    #-----------------------------------------------------------------
+    # Now do the same thing for the source GDS file.
+    #-----------------------------------------------------------------
+
+    print('Reading GDS file for original source ' + source)
+    with open(source, 'rb') as ifile:
+        gdsdata = ifile.read()
+
+    datalen = len(gdsdata)
+    dataptr = 0
+    incell = False
+    cellchecksum = 0
+    oldstart = oldend = -1
+    while dataptr < datalen:
+        # Read stream records up to any structure, then check for structure name
+        bheader = gdsdata[dataptr:dataptr + 2]
+        reclen = int.from_bytes(bheader, 'big')
+        if reclen == 0:
+            print('Error: found zero-length record at position ' + str(dataptr))
+            break
+
+        rectype = gdsdata[dataptr + 2]
+        datatype = gdsdata[dataptr + 3]
+
+        brectype = rectype.to_bytes(1, byteorder='big')
+        bdatatype = datatype.to_bytes(1, byteorder='big')
+
+        if rectype == 5:            # beginstr
+            saveptr = dataptr
+
+        elif rectype == 6:            # strname
+            if datatype != 6:
+                print('Error: Structure name record is not a string!')
+                sys.exit(1)
+
+            bstring = gdsdata[dataptr + 4: dataptr + reclen]
+            strname = bstring.decode('ascii')
+            if strname == cellname:
+                print('Cell ' + cellname + ' found at position ' + str(saveptr))
+                oldstart = saveptr
+                incell = True
+            elif debug:
+                print('Cell ' + strname + ' position ' + str(dataptr) + ' (copied)')
+
+        elif rectype == 7:		# endstr
+            if incell:
+                incell = False
+                cellchecksum = cellchecksum + reclen
+                oldend = dataptr + reclen
+                print('Cell ' + cellname + ' ends at position ' + str(oldend))
+                print('Cell ' + cellname + ' checksum is ' + str(cellchecksum))
+
+        # Find checksum (sum of length of all records in the cell of interest)
+        if incell:
+            cellchecksum = cellchecksum + reclen
+
+        # Advance the pointer past the data
+        dataptr += reclen
+
+    if oldstart == -1 or oldend == -1:
+        print('Failed to find the cell data for ' + cellname)
+        sys.exit(1)
+
+    if checksum != 0:
+        if cellchecksum == checksum:
+            print('Info:  Structure ' + cellname + ' matches checksum ' + str(checksum))
+        else:
+            print('Info:  Structure ' + cellname + ' at ' + str(oldstart) + ' to ' +
+		str(oldend) + ' has checksum ' + str(cellchecksum) +
+		' != ' + str(checksum) + ' (checksum failure)')
+            sys.exit(1)
+    else:
+        print('Info:  Structure ' + cellname + ' checksum is ' + str(cellchecksum))
+
+    print('Info:  Structure ' + cellname + ' at ' + str(oldstart) + ' to ' +
+		str(oldend) + ' will be replaced by alternate data.')
+
+    before = gdsdata[0:oldstart]
+    after = gdsdata[oldend:]
+
+    cellstrdata = celldata[datastart:dataend]
+
+    # Reassemble the GDS data around the new cell
+    gdsdata = before + cellstrdata + after
+
+    with open(dest, 'wb') as ofile:
+        ofile.write(gdsdata)
+
+    exit(0)