Split exports under nets into individual files by purpose: API unchanged.
diff --git a/util/common/nets/__init__.py b/util/common/nets/__init__.py
index eed7478..845adcb 100644
--- a/util/common/nets/__init__.py
+++ b/util/common/nets/__init__.py
@@ -1,321 +1,3 @@
-import re
-import tiles
-
-# REGEXs for global/clock signals
-
-# Globals including spine inputs, TAP_DRIVE inputs and TAP_DRIVE outputs
-global_spine_tap_re = re.compile(r'R\d+C\d+_[HV]P[TLBR]X(\d){2}00')
-# CMUX outputs
-global_cmux_out_re = re.compile(r'R\d+C\d+_[UL][LR]PCLK\d+')
-# CMUX inputs
-global_cmux_in_re = re.compile(r'R\d+C\d+_[HV]PF[NESW](\d){2}00')
-# Clock pins
-clock_pin_re = re.compile(r'R\d+C\d+_J?PCLK[TBLR]\d+')
-# PLL global outputs
-pll_out_re = re.compile(r'R\d+C\d+_J?[UL][LR][QC]PLL\dCLKO[PS]\d?')
-
-# CIB clock inputs
-cib_clk_re = re.compile(r'R\d+C\d+_J?[ULTB][LR][QCM]PCLKCIB\d+')
-# Oscillator output
-osc_clk_re = re.compile(r'R\d+C\d+_J?OSC')
-# Clock dividers
-cdivx_clk_re = re.compile(r'R\d+C\d+_J?[UL]CDIVX\d+')
-# SED clock output
-sed_clk_re = re.compile(r'R\d+C\d+_J?SEDCLKOUT')
-
-# SERDES reference clocks
-pcs_clk_re = re.compile(r'R\d+C\d+_J?PCS[AB][TR]XCLK\d')
-
-
-# DDRDEL delay signals
-ddr_delay_re = re.compile(r'R\d+C\d+_[UL][LR]DDRDEL')
-
-# DCC signals
-dcc_clk_re = re.compile(r'R\d+C\d+_J?(CLK[IO]|CE)_[BLTR]?DCC(\d+|[BT][LR])')
-# DCC inputs
-dcc_clki_re = re.compile(r'R\d+C\d+_[BLTR]?DCC(\d+|[BT][LR])CLKI')
-# DCS signals
-dcs_sig_re = re.compile(r'R\d+C\d+_J?(CLK\d|SEL\d|DCSOUT|MODESEL)_DCS\d')
-# DCS clocks
-dcs_clk_re = re.compile(r'R\d+C\d+_DCS\d(CLK\d)?')
-# Misc. center clocks
-center_clk_re = re.compile(r'R\d+C\d+_J?(LE|RE)CLK\d')
-
-# Shared DQS signals
-dqs_ssig_re = re.compile(r'R\d+C\d+_(DQS[RW]\d*|(RD|WR)PNTR\d)$')
-
-# Bank edge clocks
-bnk_eclk_re = re.compile('R\d+C\d+_BANK\d+(ECLK\d+)')
-# CIB ECLK inputs
-cib_eclk_re = re.compile(r'R\d+C\d+_J?[ULTB][LR][QCM]ECLKCIB\d+')
-
-brg_eclk_re = re.compile(r'R\d+C(\d+)_JBRGECLK\d+')
-
-
-def is_global_brgeclk(wire):
-    m = brg_eclk_re.match(wire)
-    if not m:
-        return False
-    if m:
-        x = int(m.group(1))
-        return x > 5 and x < 67
-
-def is_global(wire):
-    """Return true if a wire is part of the global clock network"""
-    return bool(global_spine_tap_re.match(wire) or
-                global_cmux_out_re.match(wire) or
-                global_cmux_in_re.match(wire) or
-                clock_pin_re.match(wire) or
-                pll_out_re.match(wire) or
-                cib_clk_re.match(wire) or
-                osc_clk_re.match(wire) or
-                cdivx_clk_re.match(wire) or
-                sed_clk_re.match(wire) or
-                ddr_delay_re.match(wire) or
-                dcc_clk_re.match(wire) or
-                dcc_clki_re.match(wire) or
-                dcs_sig_re.match(wire) or
-                dcs_clk_re.match(wire) or
-                pcs_clk_re.match(wire) or
-                center_clk_re.match(wire) or
-                cib_eclk_re.match(wire) or
-                is_global_brgeclk(wire))
-
-
-# General inter-tile routing
-general_routing_re = re.compile('R\d+C\d+_[VH]\d{2}[NESWTLBR]\d{4}')
-# CIB signals
-cib_signal_re = re.compile('R\d+C\d+_J?[ABCDFMQ]\d')
-# CIB clock/control signals
-cib_control_re = re.compile('R\d+C\d+_J?(CLK|LSR|CEN|CE)\d')
-# CIB bounce signals
-cib_bounce_re = re.compile('R\d+C\d+_[NESW]BOUNCE')
-
-
-def is_cib(wire):
-    """Return true if a wire is considered part of the CIB (rather than
-       a special function - EBR, DSP, etc)"""
-    return bool(general_routing_re.match(wire) or
-                cib_signal_re.match(wire) or
-                cib_control_re.match(wire) or
-                cib_bounce_re.match(wire))
-
-
-h_wire_regex = re.compile(r'H(\d{2})([EW])(\d{2})(\d{2})')
-v_wire_regex = re.compile(r'V(\d{2})([NS])(\d{2})(\d{2})')
-
-
-def handle_edge_name(chip_size, tile_pos, wire_pos, netname):
-    """
-    At the edges of the device, canonical wire names do not follow normal naming conventions, as they
-    would mean the nominal position of the wire would be outside the bounds of the chip. Before we add routing to the
-    database, we must however normalise these names to what they would be if not near the edges, otherwise there is a
-    risk of database conflicts, having multiple names for the same wire.
-
-    chip_size: chip size as tuple (max_row, max_col)
-    tile_pos: tile position as tuple (r, c)
-    wire_pos: wire nominal position as tuple (r, c)
-    netname: wire name without position prefix
-
-    Returns a tuple (netname, wire_pos)
-    """
-    hm = h_wire_regex.match(netname)
-    vm = v_wire_regex.match(netname)
-    if hm:
-        if hm.group(1) == "01":
-            if tile_pos[1] == chip_size[1] - 1:
-                # H01xyy00 --> x+1, H01xyy01
-                assert hm.group(4) == "00"
-                return "H01{}{}01".format(hm.group(2), hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
-        elif hm.group(1) == "02":
-            if tile_pos[1] == 1:
-                # H02E0002 --> x-1, H02E0001
-                # H02W0000 --> x-1, H02W00001
-                if hm.group(2) == "E" and wire_pos[1] == 1 and hm.group(4) == "02":
-                    return "H02E{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] - 1)
-                elif hm.group(2) == "W" and wire_pos[1] == 1 and hm.group(4) == "00":
-                    return "H02W{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] - 1)
-            elif tile_pos[1] == (chip_size[1] - 1):
-                # H02E0000 --> x+1, H02E0001
-                # H02W0002 --> x+1, H02W00001
-                if hm.group(2) == "E" and wire_pos[1] == (chip_size[1] - 1) and hm.group(4) == "00":
-                    return "H02E{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
-                elif hm.group(2) == "W" and wire_pos[1] == (chip_size[1] - 1) and hm.group(4) == "02":
-                    return "H02W{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
-        elif hm.group(1) == "06":
-            if tile_pos[1] <= 5:
-                # x-2, H06W0302 --> x-3, H06W0303
-                # x-2, H06E0004 --> x-3, H06E0003
-                # x-1, H06W0301 --> x-3, H06W0303
-                # x-1, H06E0305 --> x-3, H06E0303
-                if hm.group(2) == "W":
-                    return "H06W{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] - (3 - int(hm.group(4))))
-                elif hm.group(2) == "E":
-                    return "H06E{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] - (int(hm.group(4)) - 3))
-            if tile_pos[1] >= (chip_size[1] - 5):
-                # x+2, H06W0304 --> x+3, H06W0303
-                # x+2, H06E0302 --> x+3, H06E0303
-                if hm.group(2) == "W":
-                    return "H06W{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] + (int(hm.group(4)) - 3))
-                elif hm.group(2) == "E":
-                    return "H06E{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] + (3 - int(hm.group(4))))
-        else:
-            assert False
-    if vm:
-        if vm.group(1) == "01":
-            if tile_pos[0] == 1:
-                # V01N000 --> y-1, V01N0001
-                if wire_pos[0] == 1 and vm.group(2) == "N" and vm.group(4) == "00":
-                    return "V01{}{}01".format(vm.group(2), vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
-                if wire_pos[0] == 1 and vm.group(2) == "S" and vm.group(4) == "01":
-                    return "V01{}{}00".format(vm.group(2), vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
-        elif vm.group(1) == "02":
-            if tile_pos[0] == 1:
-                # V02S0002 --> y-1, V02S0001
-                # V02N0000 --> y-1, V02N0001
-                if vm.group(2) == "S" and wire_pos[0] == 1 and vm.group(4) == "02":
-                    return "V02S{}01".format(vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
-                elif vm.group(2) == "N" and wire_pos[0] == 1 and vm.group(4) == "00":
-                    return "V02N{}01".format(vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
-            elif tile_pos[0] == (chip_size[0] - 1):
-                # V02S0000 --> y+1, V02S0001
-                # V02N0002 --> y+1, V02N00001
-                if vm.group(2) == "S" and wire_pos[0] == (chip_size[0] - 1) and vm.group(4) == "00":
-                    return "V02S{}01".format(vm.group(3)), (wire_pos[0] + 1, wire_pos[1])
-                elif vm.group(2) == "N" and wire_pos[0] == (chip_size[0] - 1) and vm.group(4) == "02":
-                    return "V02N{}01".format(vm.group(3)), (wire_pos[0] + 1, wire_pos[1])
-        elif vm.group(1) == "06":
-            if tile_pos[0] <= 5:
-                # y-2, V06N0302 --> y-3, H06W0303
-                # y-2, V06S0004 --> y-3, V06S0003
-                # y-1, V06N0301 --> y-3, V06N0303
-                # y-1, V06S0005 --> y-3, V06S0003
-                if vm.group(2) == "N":
-                    return "V06N{}03".format(vm.group(3)), (wire_pos[0] - (3 - int(vm.group(4))), wire_pos[1])
-                elif vm.group(2) == "S":
-                    return "V06S{}03".format(vm.group(3)), (wire_pos[0] - (int(vm.group(4)) - 3), wire_pos[1])
-            if tile_pos[0] >= (chip_size[0] - 5):
-                # y+2, V06N0304 --> y+3, V06N0303
-                # y+2, V06S0302 --> x+3, V06S0303
-                if vm.group(2) == "N":
-                    return "V06N{}03".format(vm.group(3)), (wire_pos[0] + (int(vm.group(4)) - 3), wire_pos[1])
-                elif vm.group(2) == "S":
-                    return "V06S{}03".format(vm.group(3)), (wire_pos[0] + (3 - int(vm.group(4))), wire_pos[1])
-        else:
-            assert False
-    return netname, wire_pos
-
-
-def normalise_name(chip_size, tile, wire, bias):
-    """
-    Wire name normalisation for tile wires and fuzzing
-    All net names that we have access too are canonical, global names
-    These are thus no good for building up a database that is the same for all tiles
-    of a given type, as the names will be different in each location.
-
-    Lattice names are of the form R{r}C{c}_{NETNAME}
-
-    Hence, we normalise names in the following way:
-     - Global wires have the prefix "G_" added
-     - Wires where (r, c) correspond to the current tile have their prefix removed
-     - Wires to the left (in TAP_DRIVEs) are given the prefix L, and wires to the right
-       are given the prefix R
-     - Wires within a DQS group are given the prefix DQSG_
-     - Wires within a bank are given the prefix BNK_
-     - Other wires are given a relative position prefix using the syntax
-       ([NS]\d+)?([EW]\d+)?_
-       so a wire whose nominal location is 6 tiles up would be given a prefix N6_
-       a wire whose nominal location is 2 tiles down and 1 tile right would be given a prefix
-       S2E1_
-
-    TODO: this is more complicated at the edges of the device, where irregular names are used to keep the row and column
-    of the nominal position in bounds. Extra logic will be needed to catch and regularise these cases.
-
-    chip_size: chip size as tuple (max_row, max_col)
-    tile: name of the relevant tile
-    wire: full Lattice name of the wire
-    bias: Use 1-based column indexing
-
-    Returns the normalised netname
-    """
-    upos = wire.index("_")
-    prefix = wire[:upos]
-    prefix_pos = tiles.pos_from_name(prefix, chip_size, bias)
-    tile_pos = tiles.pos_from_name(tile, chip_size, bias)
-    netname = wire[upos+1:]
-    if tile.startswith("TAP") and netname.startswith("H"):
-        if prefix_pos[1] < tile_pos[1]:
-            return "L_" + netname
-        elif prefix_pos[1] > tile_pos[1]:
-            return "R_" + netname
-        else:
-            assert False, "bad TAP_DRIVE netname"
-    elif is_global(wire):
-        return "G_" + netname
-    elif dqs_ssig_re.match(wire):
-        return "DQSG_" + netname
-    elif bnk_eclk_re.match(wire):
-        if "ECLK" in tile:
-            return "G_" + netname
-        else:
-            return "BNK_" + bnk_eclk_re.match(wire).group(1)
-    elif netname in ("INRD", "LVDS"):
-        return "BNK_" + netname
-    netname, prefix_pos = handle_edge_name(chip_size, tile_pos, prefix_pos, netname)
-    if tile_pos == prefix_pos:
-        return netname
-    else:
-        prefix = ""
-        if prefix_pos[0] < tile_pos[0]:
-            prefix += "N{}".format(tile_pos[0] - prefix_pos[0])
-        elif prefix_pos[0] > tile_pos[0]:
-            prefix += "S{}".format(prefix_pos[0] - tile_pos[0])
-        if prefix_pos[1] > tile_pos[1]:
-            prefix += "E{}".format(prefix_pos[1] - tile_pos[1])
-        elif prefix_pos[1] < tile_pos[1]:
-            prefix += "W{}".format(tile_pos[1] - prefix_pos[1])
-        return prefix + "_" + netname
-
-
-rel_netname_re = re.compile(r'^([NS]\d+)?([EW]\d+)?_.*')
-
-def canonicalise_name(chip_size, tile, wire, bias):
-    """
-    Convert a normalised name in a given tile back to a canonical global name
-    :param chip_size: chip size as tuple (max_row, max_col)
-    :param tile: tilename
-    :param wire: normalised netname
-    :return: global canonical netname
-    """
-    if wire.startswith("G_"):
-        return wire
-    m = rel_netname_re.match(wire)
-    tile_pos = tiles.pos_from_name(tile, chip_size, bias)
-    wire_pos = tile_pos
-    if m:
-        assert len(m.groups()) >= 1
-        for g in m.groups():
-            if g is not None:
-                delta = int(g[1:])
-                if g[0] == "N":
-                    wire_pos = (wire_pos[0] - delta, wire_pos[1])
-                elif g[0] == "S":
-                    wire_pos = (wire_pos[0] + delta, wire_pos[1])
-                elif g[0] == "W":
-                    wire_pos = (wire_pos[0], wire_pos[1] - delta)
-                elif g[0] == "E":
-                    wire_pos = (wire_pos[0], wire_pos[1] + delta)
-        wire = wire.split("_", 1)[1]
-    if wire_pos[0] < 0 or wire_pos[0] > chip_size[0] or wire_pos[1] < 0 or wire_pos[1] > chip_size[1]:
-        return None #TODO: edge normalisation
-    return "R{}C{}_{}".format(wire_pos[0], wire_pos[1], wire)
-
-
-# Useful functions for constructing nets.
-def char_range(c1, c2):
-    """Generates the characters from `c1` to `c2`, exclusive."""
-    for c in range(ord(c1), ord(c2)):
-        yield chr(c)
-
-def net_product(net_list, range_iter):
-    return [n.format(i) for i in range_iter for n in net_list]
+from .general import *
+from .ecp5 import *
+from .util import *
diff --git a/util/common/nets/ecp5.py b/util/common/nets/ecp5.py
new file mode 100644
index 0000000..e96ac92
--- /dev/null
+++ b/util/common/nets/ecp5.py
@@ -0,0 +1,82 @@
+import re
+import tiles
+
+# REGEXs for global/clock signals
+
+# Globals including spine inputs, TAP_DRIVE inputs and TAP_DRIVE outputs
+global_spine_tap_re = re.compile(r'R\d+C\d+_[HV]P[TLBR]X(\d){2}00')
+# CMUX outputs
+global_cmux_out_re = re.compile(r'R\d+C\d+_[UL][LR]PCLK\d+')
+# CMUX inputs
+global_cmux_in_re = re.compile(r'R\d+C\d+_[HV]PF[NESW](\d){2}00')
+# Clock pins
+clock_pin_re = re.compile(r'R\d+C\d+_J?PCLK[TBLR]\d+')
+# PLL global outputs
+pll_out_re = re.compile(r'R\d+C\d+_J?[UL][LR][QC]PLL\dCLKO[PS]\d?')
+
+# CIB clock inputs
+cib_clk_re = re.compile(r'R\d+C\d+_J?[ULTB][LR][QCM]PCLKCIB\d+')
+# Oscillator output
+osc_clk_re = re.compile(r'R\d+C\d+_J?OSC')
+# Clock dividers
+cdivx_clk_re = re.compile(r'R\d+C\d+_J?[UL]CDIVX\d+')
+# SED clock output
+sed_clk_re = re.compile(r'R\d+C\d+_J?SEDCLKOUT')
+
+# SERDES reference clocks
+pcs_clk_re = re.compile(r'R\d+C\d+_J?PCS[AB][TR]XCLK\d')
+
+
+# DDRDEL delay signals
+ddr_delay_re = re.compile(r'R\d+C\d+_[UL][LR]DDRDEL')
+
+# DCC signals
+dcc_clk_re = re.compile(r'R\d+C\d+_J?(CLK[IO]|CE)_[BLTR]?DCC(\d+|[BT][LR])')
+# DCC inputs
+dcc_clki_re = re.compile(r'R\d+C\d+_[BLTR]?DCC(\d+|[BT][LR])CLKI')
+# DCS signals
+dcs_sig_re = re.compile(r'R\d+C\d+_J?(CLK\d|SEL\d|DCSOUT|MODESEL)_DCS\d')
+# DCS clocks
+dcs_clk_re = re.compile(r'R\d+C\d+_DCS\d(CLK\d)?')
+# Misc. center clocks
+center_clk_re = re.compile(r'R\d+C\d+_J?(LE|RE)CLK\d')
+
+# Shared DQS signals
+dqs_ssig_re = re.compile(r'R\d+C\d+_(DQS[RW]\d*|(RD|WR)PNTR\d)$')
+
+# Bank edge clocks
+bnk_eclk_re = re.compile('R\d+C\d+_BANK\d+(ECLK\d+)')
+# CIB ECLK inputs
+cib_eclk_re = re.compile(r'R\d+C\d+_J?[ULTB][LR][QCM]ECLKCIB\d+')
+
+brg_eclk_re = re.compile(r'R\d+C(\d+)_JBRGECLK\d+')
+
+
+def is_global_brgeclk(wire):
+    m = brg_eclk_re.match(wire)
+    if not m:
+        return False
+    if m:
+        x = int(m.group(1))
+        return x > 5 and x < 67
+
+def is_global(wire):
+    """Return true if a wire is part of the global clock network"""
+    return bool(global_spine_tap_re.match(wire) or
+                global_cmux_out_re.match(wire) or
+                global_cmux_in_re.match(wire) or
+                clock_pin_re.match(wire) or
+                pll_out_re.match(wire) or
+                cib_clk_re.match(wire) or
+                osc_clk_re.match(wire) or
+                cdivx_clk_re.match(wire) or
+                sed_clk_re.match(wire) or
+                ddr_delay_re.match(wire) or
+                dcc_clk_re.match(wire) or
+                dcc_clki_re.match(wire) or
+                dcs_sig_re.match(wire) or
+                dcs_clk_re.match(wire) or
+                pcs_clk_re.match(wire) or
+                center_clk_re.match(wire) or
+                cib_eclk_re.match(wire) or
+                is_global_brgeclk(wire))
diff --git a/util/common/nets/general.py b/util/common/nets/general.py
new file mode 100644
index 0000000..1639919
--- /dev/null
+++ b/util/common/nets/general.py
@@ -0,0 +1,233 @@
+import re
+import tiles
+
+from .ecp5 import *
+
+
+# General inter-tile routing
+general_routing_re = re.compile('R\d+C\d+_[VH]\d{2}[NESWTLBR]\d{4}')
+# CIB signals
+cib_signal_re = re.compile('R\d+C\d+_J?[ABCDFMQ]\d')
+# CIB clock/control signals
+cib_control_re = re.compile('R\d+C\d+_J?(CLK|LSR|CEN|CE)\d')
+# CIB bounce signals
+cib_bounce_re = re.compile('R\d+C\d+_[NESW]BOUNCE')
+
+
+def is_cib(wire):
+    """Return true if a wire is considered part of the CIB (rather than
+       a special function - EBR, DSP, etc)"""
+    return bool(general_routing_re.match(wire) or
+                cib_signal_re.match(wire) or
+                cib_control_re.match(wire) or
+                cib_bounce_re.match(wire))
+
+
+h_wire_regex = re.compile(r'H(\d{2})([EW])(\d{2})(\d{2})')
+v_wire_regex = re.compile(r'V(\d{2})([NS])(\d{2})(\d{2})')
+
+
+def handle_edge_name(chip_size, tile_pos, wire_pos, netname):
+    """
+    At the edges of the device, canonical wire names do not follow normal naming conventions, as they
+    would mean the nominal position of the wire would be outside the bounds of the chip. Before we add routing to the
+    database, we must however normalise these names to what they would be if not near the edges, otherwise there is a
+    risk of database conflicts, having multiple names for the same wire.
+
+    chip_size: chip size as tuple (max_row, max_col)
+    tile_pos: tile position as tuple (r, c)
+    wire_pos: wire nominal position as tuple (r, c)
+    netname: wire name without position prefix
+
+    Returns a tuple (netname, wire_pos)
+    """
+    hm = h_wire_regex.match(netname)
+    vm = v_wire_regex.match(netname)
+    if hm:
+        if hm.group(1) == "01":
+            if tile_pos[1] == chip_size[1] - 1:
+                # H01xyy00 --> x+1, H01xyy01
+                assert hm.group(4) == "00"
+                return "H01{}{}01".format(hm.group(2), hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
+        elif hm.group(1) == "02":
+            if tile_pos[1] == 1:
+                # H02E0002 --> x-1, H02E0001
+                # H02W0000 --> x-1, H02W00001
+                if hm.group(2) == "E" and wire_pos[1] == 1 and hm.group(4) == "02":
+                    return "H02E{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] - 1)
+                elif hm.group(2) == "W" and wire_pos[1] == 1 and hm.group(4) == "00":
+                    return "H02W{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] - 1)
+            elif tile_pos[1] == (chip_size[1] - 1):
+                # H02E0000 --> x+1, H02E0001
+                # H02W0002 --> x+1, H02W00001
+                if hm.group(2) == "E" and wire_pos[1] == (chip_size[1] - 1) and hm.group(4) == "00":
+                    return "H02E{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
+                elif hm.group(2) == "W" and wire_pos[1] == (chip_size[1] - 1) and hm.group(4) == "02":
+                    return "H02W{}01".format(hm.group(3)), (wire_pos[0], wire_pos[1] + 1)
+        elif hm.group(1) == "06":
+            if tile_pos[1] <= 5:
+                # x-2, H06W0302 --> x-3, H06W0303
+                # x-2, H06E0004 --> x-3, H06E0003
+                # x-1, H06W0301 --> x-3, H06W0303
+                # x-1, H06E0305 --> x-3, H06E0303
+                if hm.group(2) == "W":
+                    return "H06W{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] - (3 - int(hm.group(4))))
+                elif hm.group(2) == "E":
+                    return "H06E{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] - (int(hm.group(4)) - 3))
+            if tile_pos[1] >= (chip_size[1] - 5):
+                # x+2, H06W0304 --> x+3, H06W0303
+                # x+2, H06E0302 --> x+3, H06E0303
+                if hm.group(2) == "W":
+                    return "H06W{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] + (int(hm.group(4)) - 3))
+                elif hm.group(2) == "E":
+                    return "H06E{}03".format(hm.group(3)), (wire_pos[0], wire_pos[1] + (3 - int(hm.group(4))))
+        else:
+            assert False
+    if vm:
+        if vm.group(1) == "01":
+            if tile_pos[0] == 1:
+                # V01N000 --> y-1, V01N0001
+                if wire_pos[0] == 1 and vm.group(2) == "N" and vm.group(4) == "00":
+                    return "V01{}{}01".format(vm.group(2), vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
+                if wire_pos[0] == 1 and vm.group(2) == "S" and vm.group(4) == "01":
+                    return "V01{}{}00".format(vm.group(2), vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
+        elif vm.group(1) == "02":
+            if tile_pos[0] == 1:
+                # V02S0002 --> y-1, V02S0001
+                # V02N0000 --> y-1, V02N0001
+                if vm.group(2) == "S" and wire_pos[0] == 1 and vm.group(4) == "02":
+                    return "V02S{}01".format(vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
+                elif vm.group(2) == "N" and wire_pos[0] == 1 and vm.group(4) == "00":
+                    return "V02N{}01".format(vm.group(3)), (wire_pos[0] - 1, wire_pos[1])
+            elif tile_pos[0] == (chip_size[0] - 1):
+                # V02S0000 --> y+1, V02S0001
+                # V02N0002 --> y+1, V02N00001
+                if vm.group(2) == "S" and wire_pos[0] == (chip_size[0] - 1) and vm.group(4) == "00":
+                    return "V02S{}01".format(vm.group(3)), (wire_pos[0] + 1, wire_pos[1])
+                elif vm.group(2) == "N" and wire_pos[0] == (chip_size[0] - 1) and vm.group(4) == "02":
+                    return "V02N{}01".format(vm.group(3)), (wire_pos[0] + 1, wire_pos[1])
+        elif vm.group(1) == "06":
+            if tile_pos[0] <= 5:
+                # y-2, V06N0302 --> y-3, H06W0303
+                # y-2, V06S0004 --> y-3, V06S0003
+                # y-1, V06N0301 --> y-3, V06N0303
+                # y-1, V06S0005 --> y-3, V06S0003
+                if vm.group(2) == "N":
+                    return "V06N{}03".format(vm.group(3)), (wire_pos[0] - (3 - int(vm.group(4))), wire_pos[1])
+                elif vm.group(2) == "S":
+                    return "V06S{}03".format(vm.group(3)), (wire_pos[0] - (int(vm.group(4)) - 3), wire_pos[1])
+            if tile_pos[0] >= (chip_size[0] - 5):
+                # y+2, V06N0304 --> y+3, V06N0303
+                # y+2, V06S0302 --> x+3, V06S0303
+                if vm.group(2) == "N":
+                    return "V06N{}03".format(vm.group(3)), (wire_pos[0] + (int(vm.group(4)) - 3), wire_pos[1])
+                elif vm.group(2) == "S":
+                    return "V06S{}03".format(vm.group(3)), (wire_pos[0] + (3 - int(vm.group(4))), wire_pos[1])
+        else:
+            assert False
+    return netname, wire_pos
+
+
+def normalise_name(chip_size, tile, wire, bias):
+    """
+    Wire name normalisation for tile wires and fuzzing
+    All net names that we have access too are canonical, global names
+    These are thus no good for building up a database that is the same for all tiles
+    of a given type, as the names will be different in each location.
+
+    Lattice names are of the form R{r}C{c}_{NETNAME}
+
+    Hence, we normalise names in the following way:
+     - Global wires have the prefix "G_" added
+     - Wires where (r, c) correspond to the current tile have their prefix removed
+     - Wires to the left (in TAP_DRIVEs) are given the prefix L, and wires to the right
+       are given the prefix R
+     - Wires within a DQS group are given the prefix DQSG_
+     - Wires within a bank are given the prefix BNK_
+     - Other wires are given a relative position prefix using the syntax
+       ([NS]\d+)?([EW]\d+)?_
+       so a wire whose nominal location is 6 tiles up would be given a prefix N6_
+       a wire whose nominal location is 2 tiles down and 1 tile right would be given a prefix
+       S2E1_
+
+    TODO: this is more complicated at the edges of the device, where irregular names are used to keep the row and column
+    of the nominal position in bounds. Extra logic will be needed to catch and regularise these cases.
+
+    chip_size: chip size as tuple (max_row, max_col)
+    tile: name of the relevant tile
+    wire: full Lattice name of the wire
+    bias: Use 1-based column indexing
+
+    Returns the normalised netname
+    """
+    upos = wire.index("_")
+    prefix = wire[:upos]
+    prefix_pos = tiles.pos_from_name(prefix, chip_size, bias)
+    tile_pos = tiles.pos_from_name(tile, chip_size, bias)
+    netname = wire[upos+1:]
+    if tile.startswith("TAP") and netname.startswith("H"):
+        if prefix_pos[1] < tile_pos[1]:
+            return "L_" + netname
+        elif prefix_pos[1] > tile_pos[1]:
+            return "R_" + netname
+        else:
+            assert False, "bad TAP_DRIVE netname"
+    elif is_global(wire):
+        return "G_" + netname
+    elif dqs_ssig_re.match(wire):
+        return "DQSG_" + netname
+    elif bnk_eclk_re.match(wire):
+        if "ECLK" in tile:
+            return "G_" + netname
+        else:
+            return "BNK_" + bnk_eclk_re.match(wire).group(1)
+    elif netname in ("INRD", "LVDS"):
+        return "BNK_" + netname
+    netname, prefix_pos = handle_edge_name(chip_size, tile_pos, prefix_pos, netname)
+    if tile_pos == prefix_pos:
+        return netname
+    else:
+        prefix = ""
+        if prefix_pos[0] < tile_pos[0]:
+            prefix += "N{}".format(tile_pos[0] - prefix_pos[0])
+        elif prefix_pos[0] > tile_pos[0]:
+            prefix += "S{}".format(prefix_pos[0] - tile_pos[0])
+        if prefix_pos[1] > tile_pos[1]:
+            prefix += "E{}".format(prefix_pos[1] - tile_pos[1])
+        elif prefix_pos[1] < tile_pos[1]:
+            prefix += "W{}".format(tile_pos[1] - prefix_pos[1])
+        return prefix + "_" + netname
+
+
+rel_netname_re = re.compile(r'^([NS]\d+)?([EW]\d+)?_.*')
+
+def canonicalise_name(chip_size, tile, wire, bias):
+    """
+    Convert a normalised name in a given tile back to a canonical global name
+    :param chip_size: chip size as tuple (max_row, max_col)
+    :param tile: tilename
+    :param wire: normalised netname
+    :return: global canonical netname
+    """
+    if wire.startswith("G_"):
+        return wire
+    m = rel_netname_re.match(wire)
+    tile_pos = tiles.pos_from_name(tile, chip_size, bias)
+    wire_pos = tile_pos
+    if m:
+        assert len(m.groups()) >= 1
+        for g in m.groups():
+            if g is not None:
+                delta = int(g[1:])
+                if g[0] == "N":
+                    wire_pos = (wire_pos[0] - delta, wire_pos[1])
+                elif g[0] == "S":
+                    wire_pos = (wire_pos[0] + delta, wire_pos[1])
+                elif g[0] == "W":
+                    wire_pos = (wire_pos[0], wire_pos[1] - delta)
+                elif g[0] == "E":
+                    wire_pos = (wire_pos[0], wire_pos[1] + delta)
+        wire = wire.split("_", 1)[1]
+    if wire_pos[0] < 0 or wire_pos[0] > chip_size[0] or wire_pos[1] < 0 or wire_pos[1] > chip_size[1]:
+        return None #TODO: edge normalisation
+    return "R{}C{}_{}".format(wire_pos[0], wire_pos[1], wire)
diff --git a/util/common/nets/util.py b/util/common/nets/util.py
new file mode 100644
index 0000000..e13413f
--- /dev/null
+++ b/util/common/nets/util.py
@@ -0,0 +1,8 @@
+# Useful functions for constructing nets.
+def char_range(c1, c2):
+    """Generates the characters from `c1` to `c2`, exclusive."""
+    for c in range(ord(c1), ord(c2)):
+        yield chr(c)
+
+def net_product(net_list, range_iter):
+    return [n.format(i) for i in range_iter for n in net_list]