Example #1
0
def decode(document: DocT, obj: dict, emit_signals: bool = True):
    """Parses a dictionary (obj) created from reading a json file and uses it
    to populate the given document with model data.

    Args:
        document:
        obj:
        emit_signals: whether to signal views

    Raises:
        IOError, AssertionError, TypeError
    """
    num_bases = len(obj['vstrands'][0]['scaf'])
    if num_bases % 32 == 0:
        lattice_type = LatticeEnum.SQUARE
        grid_type = GridEnum.SQUARE
    elif num_bases % 21 == 0:
        lattice_type = LatticeEnum.HONEYCOMB
        grid_type = GridEnum.HONEYCOMB
    else:
        raise IOError("error decoding number of bases")

    part = None
    # DETERMINE MAX ROW,COL
    max_row_json = max_col_json = 0
    for helix in obj['vstrands']:
        max_row_json = max(max_row_json, int(helix['row'])+1)
        max_col_json = max(max_col_json, int(helix['col'])+1)

    # CREATE PART ACCORDING TO LATTICE TYPE
    if lattice_type == LatticeEnum.HONEYCOMB:
        doLattice = HoneycombDnaPart.legacyLatticeCoordToPositionXY
        isEven = HoneycombDnaPart.isEvenParity
    elif lattice_type == LatticeEnum.SQUARE:
        doLattice = SquareDnaPart.legacyLatticeCoordToPositionXY
        isEven = SquareDnaPart.isEvenParity
    else:
        raise TypeError("Lattice type not recognized")
    part = document.createNucleicAcidPart(grid_type=grid_type, use_undostack=False)
    part.setActive(True)
    document.setGridType(grid_type)
    document.setSliceOrGridViewVisible(OrthoViewEnum.SLICE)
    setBatch(True)
    # POPULATE VIRTUAL HELICES
    ordered_id_list = []
    vh_num_to_coord = {}
    min_row, max_row = 10000, -10000
    min_col, max_col = 10000, -10000

    # find row, column limits
    for helix in obj['vstrands']:
        row = helix['row']
        if row < min_row:
            min_row = row
        if row > max_row:
            max_row = row
        col = helix['col']
        if col < min_col:
            min_col = col
        if col > max_col:
            max_col = col
    # end for

    delta_row = (max_row + min_row) // 2
#    # 2 LINES COMMENTED OUT BY NC, doesn't appear to be necessary for honeycomb
#    if delta_row & 0:
    if delta_row & 1:
        delta_row += 1
    delta_column = (max_col + min_col) // 2
#    if delta_column & 0:
    if delta_column & 1:
        delta_column += 1

    print("Found cadnano version 2 file")
    # print("\trows(%d, %d): avg: %d" % (min_row, max_row, delta_row))
    # print("\tcolumns(%d, %d): avg: %d" % (min_col, max_col, delta_column))

    for helix in obj['vstrands']:
        vh_num = helix['num']
        row = helix['row']
        col = helix['col']
        scaf = helix['scaf']
        # align row and columns to the center 0, 0
        if HoneycombDnaPart.isEvenParity(row, col):
            coord = (row - delta_row, col - delta_column)
        else:
            coord = (row - delta_row, col - delta_column)
        vh_num_to_coord[vh_num] = coord
        ordered_id_list.append(vh_num)
    # end for

    # make sure we retain the original order
    radius = DEFAULT_RADIUS
    for vh_num in sorted(vh_num_to_coord.keys()):
        row, col = vh_num_to_coord[vh_num]
        x, y = doLattice(radius, row, col)
        part.createVirtualHelix(x, y, 0., num_bases,
                                id_num=vh_num, use_undostack=False)
    # zoom to fit
    if emit_signals:
        part.partZDimensionsChangedSignal.emit(part, *part.zBoundsIds(), True)
    if not getReopen():
        setBatch(False)
    part.setImportedVHelixOrder(ordered_id_list)
    setReopen(False)
    setBatch(False)

    # INSTALL STRANDS AND COLLECT XOVER LOCATIONS
    scaf_seg = defaultdict(list)
    scaf_xo = defaultdict(list)
    stap_seg = defaultdict(list)
    stap_xo = defaultdict(list)
    try:
        for helix in obj['vstrands']:
            vh_num = helix['num']
            row, col = vh_num_to_coord[vh_num]
            scaf = helix['scaf']
            stap = helix['stap']
            insertions = helix['loop']
            skips = helix['skip']

            if isEven(row, col):
                scaf_strand_set, stap_strand_set = part.getStrandSets(vh_num)
            else:
                stap_strand_set, scaf_strand_set = part.getStrandSets(vh_num)

            # validate file serialization of lists
            assert(len(scaf) == len(stap) and
                   len(scaf) == len(insertions) and
                   len(insertions) == len(skips))

            # read scaffold segments and xovers
            for i in range(len(scaf)):
                five_vh, five_idx, three_vh, three_idx = scaf[i]
                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandEnum.SCAFFOLD, vh_num, i, five_vh,
                                       five_idx, three_vh, three_idx):
                    scaf_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    scaf_seg[vh_num].append(i)  # end segment on a double crossover
                if is3primeXover(StrandEnum.SCAFFOLD, vh_num, i, three_vh, three_idx):
                    scaf_xo[vh_num].append((i, three_vh, three_idx))
            assert (len(scaf_seg[vh_num]) % 2 == 0)

            # install scaffold segments
            for i in range(0, len(scaf_seg[vh_num]), 2):
                low_idx = scaf_seg[vh_num][i]
                high_idx = scaf_seg[vh_num][i + 1]
                scaf_strand_set.createStrand(low_idx, high_idx, use_undostack=False)

            # read staple segments and xovers
            for i in range(len(stap)):
                five_vh, five_idx, three_vh, three_idx = stap[i]
                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandEnum.STAPLE, vh_num, i, five_vh,
                                       five_idx, three_vh, three_idx):
                    stap_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    stap_seg[vh_num].append(i)  # end segment on a double crossover
                if is3primeXover(StrandEnum.STAPLE, vh_num, i, three_vh, three_idx):
                    stap_xo[vh_num].append((i, three_vh, three_idx))
            assert (len(stap_seg[vh_num]) % 2 == 0)

            # install staple segments
            for i in range(0, len(stap_seg[vh_num]), 2):
                low_idx = stap_seg[vh_num][i]
                high_idx = stap_seg[vh_num][i + 1]
                stap_strand_set.createStrand(low_idx, high_idx, use_undostack=False)
            part.refreshSegments(vh_num)
        # end for
    except AssertionError:
        print("Unrecognized file format.")
        raise

    # INSTALL XOVERS
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row, col = vh_num_to_coord[vh_num]

        if isEven(row, col):
            scaf_strand_set, stap_strand_set = part.getStrandSets(vh_num)
        else:
            stap_strand_set, scaf_strand_set = part.getStrandSets(vh_num)

        # install scaffold xovers
        for (idx5p, to_vh_num, idx3p) in scaf_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            try:
                strand5p = scaf_strand_set.getStrand(idx5p)
            except Exception:
                print(vh_num, idx5p)
                print(scaf_strand_set.strand_heap)
                print(stap_strand_set.strand_heap)
                raise
            coord = vh_num_to_coord[to_vh_num]
            if isEven(*coord):
                to_scaf_strand_set, to_stap_strand_set = part.getStrandSets(to_vh_num)
            else:
                to_stap_strand_set, to_scaf_strand_set = part.getStrandSets(to_vh_num)
            strand3p = to_scaf_strand_set.getStrand(idx3p)
            part.createXover(strand5p, idx5p,
                             strand3p, idx3p,
                             allow_reordering=True,
                             update_oligo=False,
                             use_undostack=False)

        # install staple xovers
        for (idx5p, to_vh_num, idx3p) in stap_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            strand5p = stap_strand_set.getStrand(idx5p)
            coord = vh_num_to_coord[to_vh_num]
            if isEven(*coord):
                to_scaf_strand_set, to_stap_strand_set = part.getStrandSets(to_vh_num)
            else:
                to_stap_strand_set, to_scaf_strand_set = part.getStrandSets(to_vh_num)
            strand3p = to_stap_strand_set.getStrand(idx3p)
            part.createXover(strand5p, idx5p,
                             strand3p, idx3p,
                             allow_reordering=True,
                             update_oligo=False,
                             use_undostack=False)

    # need to heal all oligo connections into a continuous
    # oligo for the next steps
    RefreshOligosCommand(part).redo()

    # COLORS, INSERTIONS, SKIPS
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row, col = vh_num_to_coord[vh_num]
        scaf = helix['scaf']
        stap = helix['stap']
        insertions = helix['loop']
        skips = helix['skip']

        if isEven(row, col):
            scaf_strand_set, stap_strand_set = part.getStrandSets(vh_num)
        else:
            stap_strand_set, scaf_strand_set = part.getStrandSets(vh_num)

        # install insertions and skips
        for base_idx in range(len(stap)):
            sum_of_insert_skip = insertions[base_idx] + skips[base_idx]
            if sum_of_insert_skip != 0:
                strand = scaf_strand_set.getStrand(base_idx)
                strand.addInsertion(base_idx,
                                    sum_of_insert_skip,
                                    use_undostack=False)
        # end for
        # populate colors
        for base_idx, color_number in helix['stap_colors']:
            color = intToColorHex(color_number)
            strand = stap_strand_set.getStrand(base_idx)
            strand.oligo().applyColor(color, use_undostack=False)

    if 'oligos' in obj:
        for oligo in obj['oligos']:
            vh_num = oligo['vh_num']
            idx = oligo['idx']
            seq = str(oligo['seq']) if oligo['seq'] is not None else ''
            if seq != '':
                coord = vh_num_to_coord[vh_num]
                if isEven(*coord):
                    scaf_ss, stap_ss = part.getStrandSets(vh_num)
                else:
                    stap_ss, scaf_ss = part.getStrandSets(vh_num)
                strand = scaf_ss.getStrand(idx)
                # print "sequence", seq, vh, idx,  strand.oligo()._strand5p
                strand.oligo().applySequence(seq, use_undostack=False)
    if 'modifications' in obj:
        for mod_id, item in obj['modifications'].items():
            if mod_id != 'int_instances' and mod_id != 'ext_instances':
                part.createMod(item, mod_id)
        for key, mid in obj['modifications']['ext_instances'].items():
            # strand, idx, coord, isstaple = part.getModStrandIdx(key)
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(document, mid, idx, use_undostack=False)
            except Exception:
                print(strand, idx)
                raise
        for key, mid in obj['modifications']['int_instances'].items():
            # strand, idx, coord, isstaple  = part.getModStrandIdx(key)
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(document, mid, idx, use_undostack=False)
            except Exception:
                print(strand, idx)
                raise
Example #2
0
def decode(document, obj):
    """
    Parses a dictionary (obj) created from reading a json file and uses it
    to populate the given document with model data.
    """
    num_bases = len(obj['vstrands'][0]['scaf'])
    if num_bases % 32 == 0:
        lattice_type = LatticeType.SQUARE
    elif num_bases % 21 == 0:
        lattice_type = LatticeType.HONEYCOMB
    else:
        raise IOError("error decoding number of bases")

    # DETERMINE MAX ROW,COL
    max_row_json = max_col_json = 0
    for helix in obj['vstrands']:
        max_row_json = max(max_row_json, int(helix['row']) + 1)
        max_col_json = max(max_col_json, int(helix['col']) + 1)

    # CREATE PART ACCORDING TO LATTICE TYPE
    if lattice_type == LatticeType.HONEYCOMB:
        steps = num_bases // 21
        # num_rows = max(30, max_row_json, cadnano.app().prefs.honeycombRows)
        # num_cols = max(32, max_col_json, cadnano.app().prefs.honeycombCols)
        num_rows = max(30, max_row_json, prefs.HONEYCOMB_PART_MAXROWS)
        num_cols = max(32, max_col_json, prefs.HONEYCOMB_PART_MAXCOLS)
        part = document.addHoneycombPart(num_rows, num_cols, steps)
    elif lattice_type == LatticeType.SQUARE:
        is_SQ_100 = True  # check for custom SQ100 format
        for helix in obj['vstrands']:
            if helix['col'] != 0:
                is_SQ_100 = False
                break
        if is_SQ_100:
            # num_rows, num_cols = 100, 1
            num_rows, num_cols = 40, 30
        else:
            num_rows, num_cols = 40, 30
        steps = num_bases // 32
        # num_rows = max(30, max_row_json, cadnano.app().prefs.squareRows)
        # num_cols = max(32, max_col_json, cadnano.app().prefs.squareCols)
        num_rows = max(30, max_row_json, prefs.SQUARE_PART_MAXROWS)
        num_cols = max(32, max_col_json, prefs.SQUARE_PART_MAXCOLS)
        part = document.addSquarePart(num_rows, num_cols, steps)
        # part = SquarePart(document=document, max_row=num_rows, max_col=num_cols, max_steps=steps)
    else:
        raise TypeError("Lattice type not recognized")
    # document._addPart(part, use_undostack=False)
    setBatch(True)
    # POPULATE VIRTUAL HELICES
    ordered_coord_list = []
    vh_num_to_coord = {}
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row = helix['row']
        col = helix['col']
        scaf = helix['scaf']
        coord = (row, col)
        vh_num_to_coord[vh_num] = coord
        ordered_coord_list.append(coord)
    # make sure we retain the original order
    for vh_num in sorted(vh_num_to_coord.keys()):
        row, col = vh_num_to_coord[vh_num]
        part.createVirtualHelix(row, col, use_undostack=False)
    if not getReopen():
        setBatch(False)
    part.setImportedVHelixOrder(ordered_coord_list)
    setReopen(False)
    setBatch(False)

    # INSTALL STRANDS AND COLLECT XOVER LOCATIONS
    num_helices = len(obj['vstrands']) - 1
    scaf_seg = defaultdict(list)
    scaf_xo = defaultdict(list)
    stap_seg = defaultdict(list)
    stap_xo = defaultdict(list)
    try:
        for helix in obj['vstrands']:
            vh_num = helix['num']
            row = helix['row']
            col = helix['col']
            scaf = helix['scaf']
            stap = helix['stap']
            insertions = helix['loop']
            skips = helix['skip']
            vh = part.virtualHelixAtCoord((row, col))
            scaf_strand_set = vh.scaffoldStrandSet()
            stap_strand_set = vh.stapleStrandSet()
            assert(len(scaf) == len(stap) and len(stap) == part.maxBaseIdx() + 1 and\
                   len(scaf) == len(insertions) and len(insertions) == len(skips))
            # read scaffold segments and xovers
            for i in range(len(scaf)):
                five_vh, five_idx, three_vh, three_idx = scaf[i]
                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandType.SCAFFOLD, vh_num, i, five_vh,\
                                       five_idx, three_vh, three_idx):
                    scaf_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    scaf_seg[vh_num].append(
                        i)  # end segment on a double crossover
                if is3primeXover(StrandType.SCAFFOLD, vh_num, i, three_vh,
                                 three_idx):
                    scaf_xo[vh_num].append((i, three_vh, three_idx))
            assert (len(scaf_seg[vh_num]) % 2 == 0)
            # install scaffold segments
            for i in range(0, len(scaf_seg[vh_num]), 2):
                low_idx = scaf_seg[vh_num][i]
                high_idx = scaf_seg[vh_num][i + 1]
                scaf_strand_set.createStrand(low_idx,
                                             high_idx,
                                             use_undostack=False)
            # read staple segments and xovers
            for i in range(len(stap)):
                five_vh, five_idx, three_vh, three_idx = stap[i]
                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandType.STAPLE, vh_num, i, five_vh,\
                                       five_idx, three_vh, three_idx):
                    stap_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    stap_seg[vh_num].append(
                        i)  # end segment on a double crossover
                if is3primeXover(StrandType.STAPLE, vh_num, i, three_vh,
                                 three_idx):
                    stap_xo[vh_num].append((i, three_vh, three_idx))
            assert (len(stap_seg[vh_num]) % 2 == 0)
            # install staple segments
            for i in range(0, len(stap_seg[vh_num]), 2):
                low_idx = stap_seg[vh_num][i]
                high_idx = stap_seg[vh_num][i + 1]
                stap_strand_set.createStrand(low_idx,
                                             high_idx,
                                             use_undostack=False)
    except AssertionError:
        print("Unrecognized file format.")
        raise

    # INSTALL XOVERS
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row = helix['row']
        col = helix['col']
        scaf = helix['scaf']
        stap = helix['stap']
        insertions = helix['loop']
        skips = helix['skip']
        from_vh = part.virtualHelixAtCoord((row, col))
        scaf_strand_set = from_vh.scaffoldStrandSet()
        stap_strand_set = from_vh.stapleStrandSet()
        # install scaffold xovers
        for (idx5p, to_vh_num, idx3p) in scaf_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            strand5p = scaf_strand_set.getStrand(idx5p)
            to_vh = part.virtualHelixAtCoord(vh_num_to_coord[to_vh_num])
            strand3p = to_vh.scaffoldStrandSet().getStrand(idx3p)
            part.createXover(strand5p,
                             idx5p,
                             strand3p,
                             idx3p,
                             update_oligo=False,
                             use_undostack=False)
        # install staple xovers
        for (idx5p, to_vh_num, idx3p) in stap_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            strand5p = stap_strand_set.getStrand(idx5p)
            to_vh = part.virtualHelixAtCoord(vh_num_to_coord[to_vh_num])
            strand3p = to_vh.stapleStrandSet().getStrand(idx3p)
            part.createXover(strand5p,
                             idx5p,
                             strand3p,
                             idx3p,
                             update_oligo=False,
                             use_undostack=False)

    # need to heal all oligo connections into a continuous
    # oligo for the next steps
    RefreshOligosCommand(part,
                         include_scaffold=True,
                         colors=(prefs.DEFAULT_SCAF_COLOR,
                                 prefs.DEFAULT_STAP_COLOR)).redo()

    # SET DEFAULT COLOR
    # for oligo in part.oligos():
    #     if oligo.isStaple():
    #         default_color = prefs.DEFAULT_STAP_COLOR
    #     else:
    #         default_color = prefs.DEFAULT_SCAF_COLOR
    #     oligo.applyColor(default_color, use_undostack=False)

    # COLORS, INSERTIONS, SKIPS
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row = helix['row']
        col = helix['col']
        scaf = helix['scaf']
        stap = helix['stap']
        insertions = helix['loop']
        skips = helix['skip']
        vh = part.virtualHelixAtCoord((row, col))
        scaf_strand_set = vh.scaffoldStrandSet()
        stap_strand_set = vh.stapleStrandSet()
        # install insertions and skips
        for base_idx in range(len(stap)):
            sum_of_insert_skip = insertions[base_idx] + skips[base_idx]
            if sum_of_insert_skip != 0:
                strand = scaf_strand_set.getStrand(base_idx)
                strand.addInsertion(base_idx,
                                    sum_of_insert_skip,
                                    use_undostack=False)
        # end for
        # populate colors
        for base_idx, color_number in helix['stap_colors']:
            color = Color((color_number >> 16) & 0xFF,
                          (color_number >> 8) & 0xFF,
                          color_number & 0xFF).name()
            strand = stap_strand_set.getStrand(base_idx)
            strand.oligo().applyColor(color, use_undostack=False)

    if 'oligos' in obj:
        for oligo in obj['oligos']:
            vh_num = oligo['vh_num']
            idx = oligo['idx']
            seq = str(oligo['seq']) if oligo['seq'] is not None else ''
            if seq != '':
                coord = vh_num_to_coord[vhNum]
                vh = part.virtualHelixAtCoord(coord)
                scaf_ss = vh.scaffoldStrandSet()
                # stapStrandSet = vh.stapleStrandSet()
                strand = scaf_ss.getStrand(idx)
                # print "sequence", seq, vh, idx,  strand.oligo()._strand5p
                strand.oligo().applySequence(seq, use_undostack=False)
    if 'modifications' in obj:
        # print("AD", cadnano.app().activeDocument)
        # win = cadnano.app().activeDocument.win
        # modstool = win.pathToolManager.modsTool
        # modstool.connectSignals(part)
        for mod_id, item in obj['modifications'].items():
            if mod_id != 'int_instances' and mod_id != 'ext_instances':
                part.createMod(item, mod_id)
        for key, mid in obj['modifications']['ext_instances'].items():
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(mid, idx, use_undostack=False)
            except:
                print(strand, idx)
                raise
        for key in obj['modifications']['int_instances'].items():
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(mid, idx, use_undostack=False)
            except:
                print(strand, idx)
                raise
Example #3
0
def decode(document, obj, emit_signals=True):
    """
    Parses a dictionary (obj) created from reading a json file and uses it
    to populate the given document with model data.
    """
    num_bases = len(obj['vstrands'][0]['fwd_ss'])

    lattice_type = LatticeType.HONEYCOMB

    part = None
    # DETERMINE MAX ROW,COL
    max_row_json = max_col_json = 0
    for helix in obj['vstrands']:
        max_row_json = max(max_row_json, int(helix['row']) + 1)
        max_col_json = max(max_col_json, int(helix['col']) + 1)

    # CREATE PART ACCORDING TO LATTICE TYPE
    if lattice_type == LatticeType.HONEYCOMB:
        doLattice = HoneycombDnaPart.latticeCoordToPositionXY
        isEven = HoneycombDnaPart.isEvenParity
    elif lattice_type == LatticeType.SQUARE:
        doLattice = SquareDnaPart.latticeCoordToPositionXY
        isEven = SquareDnaPart.isEvenParity
    else:
        raise TypeError("Lattice type not recognized")
    part = document.createNucleicAcidPart(use_undostack=False)
    part.setActive(True)
    setBatch(True)
    # POPULATE VIRTUAL HELICES
    ordered_id_list = []
    property_dict = {}
    vh_num_to_coord = {}
    min_row, max_row = 10000, -10000
    min_col, max_col = 10000, -10000

    # find row, column limits
    for helix in obj['vstrands']:
        row = helix['row']
        if row < min_row:
            min_row = row
        if row > max_row:
            max_row = row
        col = helix['col']
        if col < min_col:
            min_col = col
        if col > max_col:
            max_col = col
    # end for

    delta_row = (max_row + min_row) // 2
    if delta_row & 1:
        delta_row += 1
    delta_column = (max_col + min_col) // 2
    if delta_column & 1:
        delta_column += 1

    # print("Found cadnano version 2.5 file")
    # print("\trows(%d, %d): avg: %d" % (min_row, max_row, delta_row))
    # print("\tcolumns(%d, %d): avg: %d" % (min_col, max_col, delta_column))

    encoded_keys = [
        'eulerZ', 'repeats', 'bases_per_repeat', 'turns_per_repeat', 'z'
    ]
    model_keys = [
        'eulerZ', 'repeat_hint', 'bases_per_repeat', 'turns_per_repeat', 'z'
    ]
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row = helix['row']
        col = helix['col']
        # align row and columns to the center 0, 0
        coord = (row - delta_row, col - delta_column)
        vh_num_to_coord[vh_num] = coord
        ordered_id_list.append(vh_num)
        property_dict[vh_num] = [helix[key] for key in encoded_keys]
    # end for

    radius = DEFAULT_RADIUS
    for vh_num in sorted(vh_num_to_coord.keys()):
        row, col = vh_num_to_coord[vh_num]
        x, y = doLattice(radius, row, col)
        props = property_dict[vh_num]
        z = convertToModelZ(props[-1])
        props[-1] = z
        # print("%d:" % vh_num, x, y, z)
        # print({k:v for k, v in zip(encoded_keys, props)})
        part.createVirtualHelix(x,
                                y,
                                z,
                                num_bases,
                                id_num=vh_num,
                                properties=(model_keys, props),
                                use_undostack=False)
    if not getReopen():
        setBatch(False)
    part.setImportedVHelixOrder(ordered_id_list)
    # zoom to fit
    if emit_signals:
        part.partZDimensionsChangedSignal.emit(part, *part.zBoundsIds(), True)
    setReopen(False)
    setBatch(False)

    # INSTALL STRANDS AND COLLECT XOVER LOCATIONS
    fwd_ss_seg = defaultdict(list)
    fwd_ss_xo = defaultdict(list)
    rev_ss_seg = defaultdict(list)
    rev_ss_xo = defaultdict(list)
    try:
        for helix in obj['vstrands']:
            vh_num = helix['num']
            row, col = vh_num_to_coord[vh_num]
            insertions = helix['insertions']
            deletions = helix['deletions']

            if isEven(row, col):
                # parity matters still despite the fwd and rev naming of
                # this format
                fwd_strandset, rev_strandset = part.getStrandSets(vh_num)
                fwd_ss = helix['fwd_ss']
                rev_ss = helix['rev_ss']
            else:
                rev_strandset, fwd_strandset = part.getStrandSets(vh_num)
                rev_ss = helix['fwd_ss']
                fwd_ss = helix['rev_ss']

            # validate file serialization of lists
            assert (len(fwd_ss) == len(rev_ss)
                    and len(fwd_ss) == len(insertions)
                    and len(insertions) == len(deletions))

            # read fwd_strandset segments and xovers
            for i in range(len(fwd_ss)):
                five_vh, five_strand, five_idx, three_vh, three_strand, three_idx = fwd_ss[
                    i]

                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandType.SCAFFOLD, vh_num, i, five_vh,
                                       five_idx, three_vh, three_idx):
                    fwd_ss_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    fwd_ss_seg[vh_num].append(
                        i)  # end segment on a double crossover
                if is3primeXover(StrandType.SCAFFOLD, vh_num, i, three_vh,
                                 three_idx):
                    fwd_ss_xo[vh_num].append(
                        (i, three_vh, three_strand, three_idx))
            assert (len(fwd_ss_seg[vh_num]) % 2 == 0)

            # install fwd_strandset segments
            for i in range(0, len(fwd_ss_seg[vh_num]), 2):
                low_idx = fwd_ss_seg[vh_num][i]
                high_idx = fwd_ss_seg[vh_num][i + 1]
                fwd_strandset.createStrand(low_idx,
                                           high_idx,
                                           use_undostack=False)

            # read rev_strandset segments and xovers
            for i in range(len(rev_ss)):
                five_vh, five_strand, five_idx, three_vh, three_strand, three_idx = rev_ss[
                    i]
                if five_vh == -1 and three_vh == -1:
                    continue  # null base
                if isSegmentStartOrEnd(StrandType.STAPLE, vh_num, i, five_vh,
                                       five_idx, three_vh, three_idx):
                    rev_ss_seg[vh_num].append(i)
                if five_vh != vh_num and three_vh != vh_num:  # special case
                    rev_ss_seg[vh_num].append(
                        i)  # end segment on a double crossover
                if is3primeXover(StrandType.STAPLE, vh_num, i, three_vh,
                                 three_idx):
                    rev_ss_xo[vh_num].append(
                        (i, three_vh, three_strand, three_idx))
            assert (len(rev_ss_seg[vh_num]) % 2 == 0)

            # install rev_strandset segments
            for i in range(0, len(rev_ss_seg[vh_num]), 2):
                low_idx = rev_ss_seg[vh_num][i]
                high_idx = rev_ss_seg[vh_num][i + 1]
                rev_strandset.createStrand(low_idx,
                                           high_idx,
                                           use_undostack=False)
            part.refreshSegments(vh_num)
        # end for
    except AssertionError:
        print("Unrecognized file format.")
        raise
    """ INSTALL XOVERS
    parity matters for the from idx but is already encoded in
    the `to_strand3p` parameter of the tuple in `fwd_ss_xo` and `rev_ss_xo`
    """
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row, col = vh_num_to_coord[vh_num]
        if isEven(row, col):
            fwd_strandset, rev_strandset = part.getStrandSets(vh_num)
        else:
            rev_strandset, fwd_strandset = part.getStrandSets(vh_num)

        # install fwd_strandset xovers
        for (idx5p, to_vh_num, to_strand3p, idx3p) in fwd_ss_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            strand5p = fwd_strandset.getStrand(idx5p)
            to_fwd_strandset, to_rev_strandset = part.getStrandSets(to_vh_num)

            if to_strand3p == 0:
                strand3p = to_fwd_strandset.getStrand(idx3p)
            else:
                strand3p = to_rev_strandset.getStrand(idx3p)
            part.createXover(strand5p,
                             idx5p,
                             strand3p,
                             idx3p,
                             allow_reordering=True,
                             update_oligo=False,
                             use_undostack=False)

        # install rev_strandset xovers
        for (idx5p, to_vh_num, to_strand3p, idx3p) in rev_ss_xo[vh_num]:
            # idx3p is 3' end of strand5p, idx5p is 5' end of strand3p
            strand5p = rev_strandset.getStrand(idx5p)
            to_fwd_strandset, to_rev_strandset = part.getStrandSets(to_vh_num)
            coord = vh_num_to_coord[to_vh_num]

            if to_strand3p == 0:
                strand3p = to_fwd_strandset.getStrand(idx3p)
            else:
                strand3p = to_rev_strandset.getStrand(idx3p)
            part.createXover(strand5p,
                             idx5p,
                             strand3p,
                             idx3p,
                             allow_reordering=True,
                             update_oligo=False,
                             use_undostack=False)

    # need to heal all oligo connections into a continuous
    # oligo for the next steps
    RefreshOligosCommand(part).redo()

    # COLORS, INSERTIONS, deletions
    for helix in obj['vstrands']:
        vh_num = helix['num']
        row, col = vh_num_to_coord[vh_num]
        fwd_ss = helix['fwd_ss']
        rev_ss = helix['rev_ss']
        insertions = helix['insertions']
        deletions = helix['deletions']

        fwd_strandset, rev_strandset = part.getStrandSets(vh_num)

        # install insertions and deletions
        for base_idx in range(len(rev_ss)):
            sum_of_insert_deletion = insertions[base_idx] + deletions[base_idx]
            if sum_of_insert_deletion != 0:
                strand = fwd_strandset.getStrand(base_idx)
                strand.addInsertion(base_idx,
                                    sum_of_insert_deletion,
                                    use_undostack=False)
        # end for

        # populate colors
        for strand_type, base_idx, color in helix['colors']:
            strandset = fwd_strandset if strand_type == 0 else rev_strandset
            strand = strandset.getStrand(base_idx)
            strand.oligo().applyColor(color, use_undostack=False)

    if 'oligos' in obj:
        for oligo in obj['oligos']:
            vh_num = oligo['vh_num']
            idx = oligo['idx']
            seq = str(oligo['seq']) if oligo['seq'] is not None else ''
            if seq != '':
                coord = vh_num_to_coord[vh_num]
                fwd_strandset, rev_strandset = part.getStrandSets(vh_num)
                strand = fwd_strandset.getStrand(idx)
                strand.oligo().applySequence(seq, use_undostack=False)

    if 'modifications' in obj:
        for mod_id, item in obj['modifications'].items():
            if mod_id != 'int_instances' and mod_id != 'ext_instances':
                part.createMod(item, mod_id)
        for key, mid in obj['modifications']['ext_instances'].items():
            # strand, idx, coord, isstaple = part.getModStrandIdx(key)
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(document, mid, idx, use_undostack=False)
            except Exception:
                print(strand, idx)
                raise
        for key in obj['modifications']['int_instances'].items():
            # strand, idx, coord, isstaple  = part.getModStrandIdx(key)
            strand, idx = part.getModStrandIdx(key)
            try:
                strand.addMods(document, mid, idx, use_undostack=False)
            except Exception:
                print(strand, idx)
                raise
Example #4
0
def importToPart(part_instance: ObjectInstance,
                 copy_dict: dict,
                 offset: Tuple[float, float] = None,
                 use_undostack: bool = True) -> Set[int]:
    """Use this to duplicate virtual_helices within a ``Part``.  duplicate
    ``id_num``s will start numbering ``part.getMaxIdNum() + 1`` rather than the
    lowest available ``id_num``.
    Args:
        part_instance:
        copy_dict:
        offset:
        use_undostack: default is ``True``

    Returns:
        set of new virtual helix IDs
    """
    assert isinstance(offset, (tuple, list)) or offset is None
    assert isinstance(use_undostack, bool)

    part = part_instance.reference()
    if use_undostack:
        undostack = part.undoStack()
        undostack.beginMacro("Import to Part")
    id_num_offset = part.getMaxIdNum() + 1
    if id_num_offset % 2 == 1:
        id_num_offset += 1
    vh_id_list = copy_dict['vh_list']
    origins = copy_dict['origins']
    vh_props = copy_dict['virtual_helices']
    name_suffix = ".%d"

    xoffset = offset[0] if offset else 0
    yoffset = offset[1] if offset else 0

    keys = list(vh_props.keys())
    name_index = keys.index('name')
    new_vh_id_set = set()
    copied_vh_index_set = set()
    if offset is None:
        offx, offy = 0, 0
    else:
        offx, offy = offset

    for i, pair in enumerate(vh_id_list):
        id_num, size = pair
        x, y = origins[i]

        if offset is not None:
            x += offx
            y += offy
        try:
            # Don't use id_num since is compacted
            z = vh_props['z'][i]
        except:
            print(vh_props)
            raise
        vals = [vh_props[k][i] for k in keys]
        new_id_num = i + id_num_offset
        # print("creating", new_id_num)
        vals[name_index] += (name_suffix % new_id_num)

        # NOTE GOT RID OF 'if' BY NC SINCE 'neighbors' SHOULD JUST BE
        # RECALCULATED ON THE FLY? TODO LOOK INTO THIS
        try:
            ignore_index = keys.index('neighbors')
            fixed_keys = keys[:ignore_index] + keys[ignore_index + 1:]
            fixed_vals = vals[:ignore_index] + vals[ignore_index + 1:]
        except ValueError:
            fixed_keys = keys
            fixed_vals = vals
        # end if

        did_create = part.createVirtualHelix(x,
                                             y,
                                             z,
                                             size,
                                             id_num=new_id_num,
                                             properties=(fixed_keys,
                                                         fixed_vals),
                                             safe=use_undostack,
                                             use_undostack=use_undostack)
        if did_create:
            copied_vh_index_set.add(i)
            new_vh_id_set.add(new_id_num)
    # end for
    strands = copy_dict['strands']
    strand_index_list = strands['indices']
    color_list = strands['properties']
    for i, idx_set in enumerate(strand_index_list):
        if i not in copied_vh_index_set:
            continue
        if idx_set is not None:
            fwd_strand_set, rev_strand_set = part.getStrandSets(i +
                                                                id_num_offset)
            fwd_idxs, rev_idxs = idx_set
            fwd_colors, rev_colors = color_list[i]
            for idxs, color in zip(fwd_idxs, fwd_colors):
                low_idx, high_idx = idxs
                fwd_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)

            for idxs, color in zip(rev_idxs, rev_colors):
                low_idx, high_idx = idxs
                rev_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)
    # end def

    xovers = copy_dict['xovers']
    for from_i, from_is_fwd, from_idx, to_i, to_is_fwd, to_idx in xovers:
        from_strand = part.getStrand(from_is_fwd, from_i + id_num_offset,
                                     from_idx)
        to_strand = part.getStrand(to_is_fwd, to_i + id_num_offset, to_idx)
        part.createXover(from_strand,
                         from_idx,
                         to_strand,
                         to_idx,
                         update_oligo=use_undostack,
                         use_undostack=use_undostack)
    if not use_undostack:
        RefreshOligosCommand(part).redo()

    # INSERTIONS, SKIPS
    for i, idx, length in copy_dict['insertions']:
        fwd_strand = part.getStrand(True, i + id_num_offset, idx)
        rev_strand = part.getStrand(False, i + id_num_offset, idx)
        if fwd_strand:
            fwd_strand.addInsertion(idx, length, use_undostack=use_undostack)
        elif rev_strand:
            rev_strand.addInsertion(idx, length, use_undostack=use_undostack)
        else:
            ins = 'Insertion' if length > 0 else 'Skip'
            err = "Cannot find strand for {} at {}[{}]"
            print(err.format(ins, i + id_num_offset, idx))
    """
    TODO: figure out copy_dict['view_properties'] handling here
    """
    if use_undostack:
        undostack.endMacro()
    return new_vh_id_set
Example #5
0
def decodePart(document: DocT,
               part_dict: dict,
               grid_type: EnumType,
               emit_signals: bool = False):
    """Decode a a deserialized Part dictionary

    Args:
        document:
        part_dict: deserialized dictionary describing the Part
        grid_type:
        emit_signals:
    """
    if (part_dict.get('point_type') == PointEnum.ARBITRARY
            or not part_dict.get('is_lattice', True)):
        is_lattice = False
    else:
        is_lattice = True

    part = document.createNucleicAcidPart(use_undostack=False,
                                          is_lattice=is_lattice,
                                          grid_type=grid_type)
    part.setActive(True)

    vh_id_list = part_dict.get('vh_list')
    vh_props = part_dict.get('virtual_helices')
    origins = part_dict.get('origins')
    keys = list(vh_props.keys())

    if not is_lattice:
        # TODO add code to deserialize parts
        pass
    else:
        for id_num, size in vh_id_list:
            x, y = origins[id_num]
            z = vh_props['z'][id_num]
            vh_props['eulerZ'][id_num] = 0.5 * (360. / 10.5)
            vals = [vh_props[k][id_num] for k in keys]
            part.createVirtualHelix(x,
                                    y,
                                    z,
                                    size,
                                    id_num=id_num,
                                    properties=(keys, vals),
                                    safe=False,
                                    use_undostack=False)
        # end for
        # zoom to fit
        if emit_signals:
            part.partZDimensionsChangedSignal.emit(part, *part.zBoundsIds(),
                                                   True)

    strands = part_dict['strands']
    strand_index_list = strands['indices']
    color_list = strands['properties']
    for i in range(len(vh_id_list)):
        id_num = vh_id_list[i][0]
        idx_set = strand_index_list[i]
        if idx_set is not None:
            fwd_strand_set, rev_strand_set = part.getStrandSets(id_num)
            fwd_idxs, rev_idxs = idx_set
            fwd_colors, rev_colors = color_list[i]
            for idxs, color in zip(fwd_idxs, fwd_colors):
                low_idx, high_idx = idxs
                fwd_strand_set.createDeserializedStrand(low_idx,
                                                        high_idx,
                                                        color,
                                                        use_undostack=False)
            for idxs, color in zip(rev_idxs, rev_colors):
                low_idx, high_idx = idxs
                rev_strand_set.createDeserializedStrand(low_idx,
                                                        high_idx,
                                                        color,
                                                        use_undostack=False)
            part.refreshSegments(id_num)  # update segments
    # end for

    xovers = part_dict['xovers']
    for from_id, from_is_fwd, from_idx, to_id, to_is_fwd, to_idx in xovers:
        from_strand = part.getStrand(from_is_fwd, from_id, from_idx)
        to_strand = part.getStrand(to_is_fwd, to_id, to_idx)
        part.createXover(from_strand,
                         from_idx,
                         to_strand,
                         to_idx,
                         update_oligo=False,
                         use_undostack=False)
    # end for

    RefreshOligosCommand(part).redo()
    for oligo in part_dict['oligos']:
        id_num = oligo['id_num']
        idx = oligo['idx5p']
        is_fwd = oligo['is_5p_fwd']
        color = oligo['color']
        sequence = oligo['sequence']
        strand5p = part.getStrand(is_fwd, id_num, idx)
        this_oligo = strand5p.oligo()
        # this_oligo.applyColor(color, use_undostack=False)
        if sequence is not None:
            this_oligo.applySequence(sequence, use_undostack=False)
    # end for

    # INSERTIONS, SKIPS
    for id_num, idx, length in part_dict['insertions']:
        fwd_strand = part.getStrand(True, id_num, idx)
        rev_strand = part.getStrand(False, id_num, idx)
        if fwd_strand:
            fwd_strand.addInsertion(idx, length, use_undostack=False)
        elif rev_strand:
            rev_strand.addInsertion(idx, length, use_undostack=False)
        else:
            ins = 'Insertion' if length > 0 else 'Skip'
            print("Cannot find strand for {} at {}[{}]".format(
                ins, id_num, idx))
    # end for

    # TODO fix this to set position
    # instance_props = part_dict['instance_properties']    # list

    vh_order = part_dict['virtual_helix_order']
    if vh_order:
        # print("import order", vh_order)
        part.setImportedVHelixOrder(vh_order)

    # Restore additional Part properties
    for key in ('name', 'color', 'crossover_span_angle', 'max_vhelix_length'):
        value = part_dict.get(key)
        if value is not None:
            part.setProperty(key, value, use_undostack=False)
            part.partPropertyChangedSignal.emit(part, key, value)
Example #6
0
def decodePart(document, part_dict, emit_signals=False):
    """ Decode a a deserialized Part dictionary

    Args:
        document (Document):
        part_dict (dict): deserialized dictionary describing the Part
    """
    name = part_dict['name']
    dc = document._controller
    part = document.createNucleicAcidPart(use_undostack=False)
    part.setActive(True)

    vh_id_list = part_dict['vh_list']
    vh_props = part_dict['virtual_helices']
    origins = part_dict['origins']
    keys = list(vh_props.keys())

    if part_dict.get('point_type') == PointType.ARBITRARY:
        # TODO add code to deserialize parts
        pass
    else:
        for id_num, size in vh_id_list:
            x, y = origins[id_num]
            z = vh_props['z'][id_num]
            vh_props['eulerZ'][id_num] = 0.5 * (360. / 10.5)
            vals = [vh_props[k][id_num] for k in keys]
            part.createVirtualHelix(x,
                                    y,
                                    z,
                                    size,
                                    id_num=id_num,
                                    properties=(keys, vals),
                                    safe=False,
                                    use_undostack=False)
        # end for
        # zoom to fit
        if emit_signals:
            part.partZDimensionsChangedSignal.emit(part, *part.zBoundsIds(),
                                                   True)

    strands = part_dict['strands']
    strand_index_list = strands['indices']
    color_list = strands['properties']
    for id_num, idx_set in enumerate(strand_index_list):
        if idx_set is not None:
            fwd_strand_set, rev_strand_set = part.getStrandSets(id_num)
            fwd_idxs, rev_idxs = idx_set
            fwd_colors, rev_colors = color_list[id_num]
            for idxs, color in zip(fwd_idxs, fwd_colors):
                low_idx, high_idx = idxs
                fwd_strand_set.createDeserializedStrand(low_idx,
                                                        high_idx,
                                                        color,
                                                        use_undostack=False)
            for idxs, color in zip(rev_idxs, rev_colors):
                low_idx, high_idx = idxs
                rev_strand_set.createDeserializedStrand(low_idx,
                                                        high_idx,
                                                        color,
                                                        use_undostack=False)
            part.refreshSegments(id_num)  # update segments
    # end def

    xovers = part_dict['xovers']
    for from_id, from_is_fwd, from_idx, to_id, to_is_fwd, to_idx in xovers:
        from_strand = part.getStrand(from_is_fwd, from_id, from_idx)
        to_strand = part.getStrand(to_is_fwd, to_id, to_idx)
        part.createXover(from_strand,
                         from_idx,
                         to_strand,
                         to_idx,
                         update_oligo=False,
                         use_undostack=False)

    RefreshOligosCommand(part).redo()
    for oligo in part_dict['oligos']:
        id_num = oligo['id_num']
        idx = oligo['idx5p']
        is_fwd = oligo['is_5p_fwd']
        color = oligo['color']
        sequence = oligo['sequence']
        strand5p = part.getStrand(is_fwd, id_num, idx)
        this_oligo = strand5p.oligo()
        # this_oligo.applyColor(color, use_undostack=False)
        if sequence is not None:
            this_oligo.applySequence(sequence, use_undostack=False)

    # INSERTIONS, SKIPS
    for id_num, idx, length in part_dict['insertions']:
        strand = part.getStrand(True, id_num, idx)
        strand.addInsertion(idx, length, use_undostack=False)

    # TODO fix this to set position
    instance_props = part_dict['instance_properties']  # list

    vh_order = part_dict['virtual_helix_order']
    if vh_order:
        # print("import order", vh_order)
        part.setImportedVHelixOrder(vh_order)
Example #7
0
def importToPart(part_instance, copy_dict, use_undostack=True):
    """Use this to duplicate virtual_helices within a Part.  duplicate id_nums
    will start numbering `part.getIdNumMax()` rather than the lowest available
    id_num.  TODO should this numbering change?

    Args:
        part_instance (ObjectInstance):
        copy_dict (dict):
    """
    part = part_instance.reference()
    id_num_offset = part.getIdNumMax() + 1
    print("Starting from", id_num_offset)
    vh_id_list = copy_dict['vh_list']
    origins = copy_dict['origins']
    vh_props = copy_dict['virtual_helices']
    name_suffix = ".%d"

    keys = list(vh_props.keys())
    name_index = keys.index('name')
    new_vh_id_set = set()
    for i, pair in enumerate(vh_id_list):
        id_num, size = pair
        x, y = origins[i]
        z = vh_props['z'][id_num]
        vals = [vh_props[k][i] for k in keys]
        new_id_num = i + id_num_offset
        vals[name_index] += (name_suffix % new_id_num)
        part.createVirtualHelix(x,
                                y,
                                z,
                                size,
                                id_num=new_id_num,
                                properties=(keys, vals),
                                safe=use_undostack,
                                use_undostack=use_undostack)
        new_vh_id_set.add(new_id_num)
    # end for
    strands = copy_dict['strands']
    strand_index_list = strands['indices']
    color_list = strands['properties']
    for id_num, idx_set in enumerate(strand_index_list):
        if idx_set is not None:
            fwd_strand_set, rev_strand_set = part.getStrandSets(id_num +
                                                                id_num_offset)
            fwd_idxs, rev_idxs = idx_set
            fwd_colors, rev_colors = color_list[id_num]
            for idxs, color in zip(fwd_idxs, fwd_colors):
                low_idx, high_idx = idxs
                fwd_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)

            for idxs, color in zip(rev_idxs, rev_colors):
                low_idx, high_idx = idxs
                rev_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)
    # end def

    xovers = copy_dict['xovers']
    for from_id, from_is_fwd, from_idx, to_id, to_is_fwd, to_idx in xovers:
        from_strand = part.getStrand(from_is_fwd, from_id + id_num_offset,
                                     from_idx)
        to_strand = part.getStrand(to_is_fwd, to_id + id_num_offset, to_idx)
        part.createXover(from_strand,
                         from_idx,
                         to_strand,
                         to_idx,
                         update_oligo=use_undostack,
                         use_undostack=use_undostack)
    if not use_undostack:
        RefreshOligosCommand(part).redo()

    # INSERTIONS, SKIPS
    for id_num, idx, length in copy_dict['insertions']:
        strand = part.getStrand(True, id_num + id_num_offset, idx)
        strand.addInsertion(idx, length, use_undostack=use_undostack)
    """
    TODO: figure out copy_dict['view_properties'] handling here
    """

    return new_vh_id_set
Example #8
0
def importToPart(part_instance,
                 copy_dict,
                 offset=None,
                 use_undostack=True,
                 ignore_neighbors=False):
    """
    Use this to duplicate virtual_helices within a Part. Duplicate id_nums
    will start numbering `part.getMaxIdNum()` rather than the lowest available
    id_num.  TODO should this numbering change?

    Args:
        part_instance (ObjectInstance):
        copy_dict (dict):
    """
    assert isinstance(offset, (tuple, list)) or offset is None
    assert isinstance(use_undostack, bool)

    print('Importing to part where use_undostack is %s' % use_undostack)

    part = part_instance.reference()
    id_num_offset = part.getMaxIdNum() + 1
    if id_num_offset % 2 == 1:
        id_num_offset += 1
    vh_id_list = copy_dict['vh_list']
    origins = copy_dict['origins']
    vh_props = copy_dict['virtual_helices']
    # name_suffix = ".%d"

    xoffset = offset[0] if offset else 0
    yoffset = offset[1] if offset else 0

    keys = list(vh_props.keys())
    name_index = keys.index('name')
    new_vh_id_set = set()
    for i, pair in enumerate(vh_id_list):
        id_num, size = pair
        x, y = origins[i]

        z = vh_props['z'][i]
        vals = [vh_props[k][i] for k in keys]
        new_id_num = i + id_num_offset
        vals[name_index] = 'vh%s' % new_id_num
        if ignore_neighbors:
            try:
                ignore_index = keys.index('neighbors')
                fixed_keys = keys[:ignore_index] + keys[ignore_index + 1:]
                fixed_vals = vals[:ignore_index] + vals[ignore_index + 1:]
            except ValueError:
                fixed_keys = keys
                fixed_vals = vals
        part.createVirtualHelix(x + xoffset,
                                y + yoffset,
                                z,
                                size,
                                id_num=new_id_num,
                                properties=(fixed_keys, fixed_vals),
                                safe=use_undostack,
                                use_undostack=use_undostack)
        new_vh_id_set.add(new_id_num)
    # end for
    strands = copy_dict['strands']
    strand_index_list = strands['indices']
    color_list = strands['properties']
    for id_num, idx_set in enumerate(strand_index_list):
        if idx_set is not None:
            fwd_strand_set, rev_strand_set = part.getStrandSets(id_num +
                                                                id_num_offset)
            fwd_idxs, rev_idxs = idx_set
            fwd_colors, rev_colors = color_list[id_num]
            for idxs, color in zip(fwd_idxs, fwd_colors):
                low_idx, high_idx = idxs
                fwd_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)

            for idxs, color in zip(rev_idxs, rev_colors):
                low_idx, high_idx = idxs
                rev_strand_set.createDeserializedStrand(
                    low_idx, high_idx, color, use_undostack=use_undostack)
    # end def

    xovers = copy_dict['xovers']
    for from_id, from_is_fwd, from_idx, to_id, to_is_fwd, to_idx in xovers:
        from_strand = part.getStrand(from_is_fwd, from_id + id_num_offset,
                                     from_idx)
        to_strand = part.getStrand(to_is_fwd, to_id + id_num_offset, to_idx)
        part.createXover(from_strand,
                         from_idx,
                         to_strand,
                         to_idx,
                         update_oligo=use_undostack,
                         use_undostack=use_undostack)
    if not use_undostack:
        RefreshOligosCommand(part).redo()

    # INSERTIONS, SKIPS
    for id_num, idx, length in copy_dict['insertions']:
        fwd_strand = part.getStrand(True, id_num + id_num_offset, idx)
        rev_strand = part.getStrand(False, id_num + id_num_offset, idx)
        if fwd_strand:
            fwd_strand.addInsertion(idx, length, use_undostack=use_undostack)
        elif rev_strand:
            rev_strand.addInsertion(idx, length, use_undostack=use_undostack)
        else:
            ins = 'Insertion' if length > 0 else 'Skip'
            print("Cannot find strand for {} at {}[{}]".format(
                ins, id_num, idx))
    """
    TODO: figure out copy_dict['view_properties'] handling here
    """
    return new_vh_id_set