Repository: cogent3/cogent3
Pull request #1894 (2024-06-08 01:44:41)

Seq collections refactor
- add `SeqsData.to_alphabet`
- add `SequenceCollection.to_moltype`
- add `SequenceCollection.dotplot`

coveralls: ## Pull Request Test Coverage Report for [Build 9425225812](https://coveralls.io/builds/67968264)


### Details

* **51** of **59**   **(86.44%)**  changed or added relevant lines in **3** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.2%**) to **90.621%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67968264/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L853) | 49 | 57 | 85.96%
<!-- | **Total:** | **51** | **59** | **86.44%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [src/cogent3/maths/scipy_optimize.py](https://coveralls.io/builds/67968264/source?filename=src%2Fcogent3%2Fmaths%2Fscipy_optimize.py#L542) | 2 | 84.92% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67968264/badge)](https://coveralls.io/builds/67968264) |
| :-- | --: |
| Change from base [Build 9415176482](https://coveralls.io/builds/67953291): |  0.2% |
| Covered Lines: | 31923 |
| Relevant Lines: | 35227 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index fce2b696e..5a06a94b4 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -1057,7 +1057,7 @@ class _SequenceCollectionBase:
 
     def get_translation(
         self, gc=None, incomplete_ok=False, include_stop=False, trim_stop=True, **kwargs
-    ):  # will not port
+    ):  # ported
         """translate from nucleic acid to protein
 
         Parameters
@@ -1121,7 +1121,7 @@ class _SequenceCollectionBase:
 
         return align_dict
 
-    def get_ambiguous_positions(self):
+    def get_ambiguous_positions(self):  # ported
         """Returns dict of seq:{position:char} for ambiguous chars.
 
         Used in likelihood calculations.
@@ -1194,7 +1194,7 @@ class _SequenceCollectionBase:
 
     def trim_stop_codons(
         self, gc: Any = None, strict: bool = False, **kwargs
-    ):  # will not port
+    ):  # ported
         """Removes any terminal stop codons from the sequences
 
         Parameters
@@ -1760,7 +1760,7 @@ class _SequenceCollectionBase:
         allow_gap=False,
         exclude_unobserved=False,
         warn=False,
-    ):
+    ):  # ported
         """return MotifFreqsArray per sequence"""
 
         counts = self.counts_per_seq(
@@ -1779,7 +1779,7 @@ class _SequenceCollectionBase:
         allow_gap=False,
         exclude_unobserved=True,
         warn=False,
-    ):
+    ):  # ported
         """Returns the Shannon entropy per sequence.
 
         Parameters
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 7434642b3..d6bf15096 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -226,6 +226,7 @@ class SeqsData(SeqsDataABC):
     ):
         self._alphabet = alphabet
         self._make_seq = make_seq
+        # todo: kath, make underlying data unchangable with `arrl.flags.writeable = False`
         self._data: dict[str, numpy.ndarray] = {
             str(name): self._alphabet.to_indices(seq) for name, seq in data.items()
         }
@@ -295,8 +296,32 @@ class SeqsData(SeqsDataABC):
     def to_alphabet(
         self, alphabet: new_alpha.CharAlphabet, check_valid=True
     ) -> SeqsData:
-        # todo: kath
-        ...
+        # refactor: design -- map directly between arrays?
+        # refactor: design -- better way to check if alphabets are DNA and RNA?
+        if len(alphabet) == len(self.alphabet):
+            return self.__class__(data=self._data, alphabet=alphabet)
+
+        new_data = {}
+        old = self.alphabet.as_bytes()
+        new = alphabet.as_bytes()
+        for seqid in self.names:
+            seq_data = self.get_seq_array(seqid=seqid)
+
+            convert_old_to_bytes = new_alpha.array_to_bytes(old)
+            convert_bytes_to_new = new_alpha.bytes_to_array(
+                new, dtype=new_alpha.get_array_type(len(new))
+            )
+
+            as_new_alpha = convert_bytes_to_new(convert_old_to_bytes(seq_data))
+
+            if check_valid and not alphabet.is_valid(as_new_alpha):
+                raise ValueError(
+                    f"Changing from old alphabet={self.alphabet} to new {alphabet=} is not valid for this data"
+                )
+
+            new_data[seqid] = as_new_alpha
+
+        return self.__class__(data=new_data, alphabet=alphabet)
 
     def __len__(self):
         return len(self.names)
@@ -430,6 +455,9 @@ class SequenceCollection:
         # todo: kath, note gavins comment that this method could operate on only the names
         # list and not the seqs_data object
 
+        if isinstance(names, str):
+            names = [names]
+
         if negate:
             names = [name for name in self.names if name not in names]
         else:
@@ -471,6 +499,13 @@ class SequenceCollection:
     ):
         """Returns new collection containing seqs where f(seq) is True.
 
+        Parameters
+        ----------
+        f
+            function that takes a sequence object and returns True or False
+        negate
+            select all sequences EXCEPT those where f(seq) is True
+
         Notes
         -----
         The seqs in the new collection are the same objects as the
@@ -480,12 +515,28 @@ class SequenceCollection:
         return self.take_seqs(self.get_seq_names_if(f, negate), **kwargs)
 
     def get_seq(self, seqname: str, copy_annotations: bool = False):
-        """Return a sequence object for the specified seqname."""
+        """Return a sequence object for the specified seqname.
+
+        Parameters
+        ----------
+        seqname
+            name of the sequence to return
+        copy_annotations
+            if True, only annotations from the selected seq are copied to the
+            annotation_db of the new collection, the same annotation db is used.
+
+        """
+        # refactor: design
+        # This method provides a mechanism for binding the annotation db to the sequence instance,
+        # which self.seqs[seq_name] does not. This is a difference to the original implementation,
+        # so it needs more thought.
         seq = self.seqs[seqname]
 
         if copy_annotations:
             seq.annotation_db = type(self.annotation_db)()
             seq.annotation_db.update(annot_db=self.annotation_db, seqids=seqname)
+        else:
+            seq.annotation_db = self.annotation_db
 
         return seq
 
@@ -517,6 +568,42 @@ class SequenceCollection:
         seqs = {name: self.seqs[name].degap() for name in self.names}
         return make_unaligned_seqs(seqs, moltype=self.moltype, info=self.info, **kwargs)
 
+    def to_moltype(self, moltype: str) -> SequenceCollection:
+        """returns copy of self with changed moltype
+
+        Parameters
+        ----------
+        moltype
+            name of the new moltype, e.g, 'dna', 'rna'.
+
+        Notes
+        -----
+        Cannot convert from nucleic acids to proteins. Use get_translation() for that.
+
+        """
+        mtype = new_moltype.get_moltype(moltype)
+        if mtype is self.moltype:
+            return self  # nothing to be done
+
+        alpha = (
+            mtype.degen_gapped_alphabet
+            if mtype.degen_gapped_alphabet is not None
+            else mtype.gapped_alphabet
+        )
+        try:
+            new_seqs_data = self.seqs.to_alphabet(alpha)
+        except ValueError as e:
+            raise ValueError(
+                f"Failed to convert moltype from {self.moltype.label} to {moltype}"
+            ) from e
+
+        return self.__class__(
+            seqs_data=new_seqs_data,
+            moltype=mtype,
+            info=self.info,
+            annotation_db=self.annotation_db,
+        )
+
     def get_lengths(
         self, include_ambiguity: bool = False, allow_gap: bool = False
     ) -> dict[str, int]:
@@ -713,11 +800,121 @@ class SequenceCollection:
         -----
         raises exception if invalid sequences do not all have the same length
         """
-        if len(set(self.seqs.seq_lengths().values())) > 1:
+        if self.is_ragged():
             raise ValueError("not all seqs same length, cannot convert to phylip")
 
         return alignment_to_phylip(self.to_dict())
 
+    def dotplot(
+        self,
+        name1: Optional[str] = None,
+        name2: Optional[str] = None,
+        window: int = 20,
+        threshold: Optional[int] = None,
+        k: Optional[int] = None,
+        min_gap: int = 0,
+        width: int = 500,
+        title: Optional[str] = None,
+        rc: bool = False,
+        show_progress: bool = False,
+    ):
+        """make a dotplot between specified sequences. Random sequences
+        chosen if names not provided.
+
+        Parameters
+        ----------
+        name1, name2
+            names of sequences -- if not provided, a random choice is made
+        window
+            segment size for comparison between sequences
+        threshold
+            windows where the sequences are identical >= threshold are a match
+        k
+            size of k-mer to break sequences into. Larger values increase
+            speed but reduce resolution. If not specified, and
+            window == threshold, then k is set to window. Otherwise, it is
+            computed as the maximum of {threshold // (window - threshold), 5}.
+        min_gap
+            permitted gap for joining adjacent line segments, default is no gap
+            joining
+        width
+            figure width. Figure height is computed based on the ratio of
+            len(seq1) / len(seq2)
+        title
+            title for the plot
+        rc
+            include dotplot of reverse compliment also. Only applies to Nucleic
+            acids moltypes
+
+        Returns
+        -------
+        a Drawable or AnnotatedDrawable
+        """
+        from cogent3.draw.dotplot import Dotplot
+        from cogent3.draw.drawable import AnnotatedDrawable
+
+        if k is not None:
+            assert 0 < k < window, "k must be smaller than window size"
+
+        if len(self.names) == 1:
+            name1 = name2 = self.names[0]
+        elif name1 is None and name2 is None:
+            name1, name2 = list(numpy.random.choice(self.names, size=2, replace=False))
+        elif not (name1 and name2):
+            names = list(set(self.names + [None]) ^ {name1, name2})
+            name = list(numpy.random.choice(names, size=1))[0]
+            name1 = name1 or name
+            name2 = name2 or name
+
+        if not {name1, name2} <= set(self.names):
+            msg = f"{name1}, {name2} missing"
+            raise ValueError(msg)
+
+        seq1 = self.get_seq(seqname=name1, copy_annotations=False)
+        seq2 = self.get_seq(seqname=name2, copy_annotations=False)
+
+        if seq1.is_annotated() or seq2.is_annotated():
+            annotated = True
+            data = getattr(seq1, "data", seq1)
+            bottom = data.get_drawable()
+            data = getattr(seq2, "data", seq2)
+            left = data.get_drawable(vertical=True)
+        else:
+            annotated = False
+
+        dotplot = Dotplot(
+            seq1,
+            seq2,
+            False,
+            window=window,
+            threshold=threshold,
+            k=k,
+            min_gap=min_gap,
+            xtitle=None if annotated else seq1.name,
+            ytitle=None if annotated else seq2.name,
+            title=title,
+            moltype=self.moltype,
+            rc=rc,
+            show_progress=show_progress,
+            width=width,
+        )
+
+        if annotated:
+            dotplot = AnnotatedDrawable(
+                dotplot,
+                left_track=left,
+                bottom_track=bottom,
+                xtitle=seq1.name,
+                ytitle=seq2.name,
+                title=title,
+                xrange=[0, len(seq1)],
+                yrange=[0, len(seq2)],
+            )
+        return dotplot
+
+    def is_ragged(self) -> bool:
+        return len(set(self.seqs.seq_lengths().values())) > 1
+
     def has_terminal_stop(self, gc: typing.Any = None, strict: bool = False) -> bool:
         """Returns True if any sequence has a terminal stop codon.
 
@@ -1237,23 +1434,30 @@ def make_unaligned_seqs(
     source
         origins of this data, defaults to 'unknown'. Converted to a string
         and added to info["source"].
+    annotation_db
+        annotation database to attach to the collection
+
+    Notes
+    -----
+    - If no annotation_db is provided, but the sequences are annotated, an
+    annotation_db is created by merging any annotation db's found in the sequences.
+    - If the sequences are annotated AND an annotation_db is provided, only the
+    annotation_db is used.
+
     """
     if len(data) == 0:
         raise ValueError("data must be at least one sequence.")
 
-    # create a db from sequences if they're annotated
-    seqs_anno_db = merged_db_collection(data)
-    # if db from seqs and provided db, merge
-    # todo: kath, should we default to not merging unless specified?
-    if annotation_db and seqs_anno_db:
-        annotation_db.update(seqs_anno_db)
-    else:
-        annotation_db = annotation_db or seqs_anno_db
+    annotation_db = annotation_db or merged_db_collection(data)
 
     seqs_data = coerce_to_seqs_data_dict(data, label_to_name=label_to_name)
 
     moltype = new_moltype.get_moltype(moltype)
-    seqs_data = SeqsData(data=seqs_data, alphabet=moltype.degen_gapped_alphabet)
+    alphabet = (
+        moltype.degen_gapped_alphabet or moltype.gapped_alphabet or moltype.alphabet
+    )
+
+    seqs_data = SeqsData(data=seqs_data, alphabet=alphabet)
     return make_unaligned_seqs(
         seqs_data,
         moltype=moltype,
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 0e257e793..6407fcaf2 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -24,7 +24,6 @@ from random import shuffle
 from numpy import array, floating, integer, issubdtype
 
 from cogent3._version import __version__
-from cogent3.core.alphabet import AlphabetError
 from cogent3.core.annotation import Feature
 from cogent3.core.annotation_db import (
     BasicAnnotationDb,
@@ -36,6 +35,7 @@ from cogent3.core.annotation_db import (
 from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import FeatureMap, IndelMap, LostSpan
+from cogent3.core.new_alphabet import AlphabetError
 from cogent3.format.fasta import alignment_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
diff --git a/src/cogent3/draw/dotplot.py b/src/cogent3/draw/dotplot.py
index 967f6d769..3fe2f7911 100644
--- a/src/cogent3/draw/dotplot.py
+++ b/src/cogent3/draw/dotplot.py
@@ -57,7 +57,7 @@ def _convert_input(seq, moltype):
         return gap_map, seq
 
     seq = (
-        moltype.make_seq(str(seq), seq.name)
+        moltype.make_seq(seq=str(seq), name=seq.name)
         if hasattr(seq, "moltype")
         else moltype.make_seq(seq)
     )


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 384fc3721..ad7c8a04b 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -201,7 +201,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(self.identical.seq_len, 4)
         self.assertEqual(self.gaps.seq_len, 7)
 
-    def test_Seqs(self):
+    def test_Seqs(self):  # ported
         """SequenceCollection seqs property should return seqs in correct order."""
         first = self.ordered1
         sec = self.ordered2
@@ -454,7 +454,7 @@ class SequenceCollectionBaseTests(object):
         aln = self.Class({"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"})
         self.assertEqual(aln.num_seqs, 3)
 
-    def test_add_info(self):
+    def test_add_info(self):  # will not port
         """__add__ should preserve info attribute"""
         align1 = self.Class(
             {"a": "AAAA", "b": "TTTT", "c": "CCCC"}, info={"key": "foo"}
@@ -465,7 +465,7 @@ class SequenceCollectionBaseTests(object):
         align = align1 + align2
         self.assertEqual(align.info["key"], "foo")
 
-    def test_add_seqs_info(self):
+    def test_add_seqs_info(self):  # ported
         """add_seqs should preserve info attribute"""
         data = [("name1", "AAA"), ("name2", "AAA"), ("name3", "AAA"), ("name4", "AAA")]
         data2 = [("name5", "BBB"), ("name6", "CCC")]
@@ -474,7 +474,7 @@ class SequenceCollectionBaseTests(object):
         out_aln = aln.add_seqs(aln2)
         self.assertEqual(out_aln.info["key"], "foo")
 
-    def test_write(self):
+    def test_write(self):  # ported
         """SequenceCollection.write should write in correct format"""
         aln = self.Class([("a", "AAAA"), ("b", "TTTT"), ("c", "CCCC")])
         fn = mktemp(suffix=".fasta")
@@ -502,7 +502,7 @@ class SequenceCollectionBaseTests(object):
         for i in list(aln.to_dict().values()):
             assert isinstance(i, str)
 
-    def test_get_ambiguous_positions(self):
+    def test_get_ambiguous_positions(self):  # ported
         """SequenceCollection.get_ambiguous_positions should return pos"""
         aln = self.Class({"s1": "ATGRY?", "s2": "T-AG??"}, moltype=DNA)
         self.assertEqual(
@@ -651,7 +651,7 @@ class SequenceCollectionBaseTests(object):
         # assertRaises error when pad_length is less than max seq length
         self.assertRaises(ValueError, self.ragged_padded.pad_seqs, 5)
 
-    def test_to_moltype_info(self):
+    def test_to_moltype_info(self):  # ported
         """correctly convert to specified moltype"""
         data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
         seqs = self.Class(data=data, info={"key": "value"})
@@ -1340,7 +1340,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         result = a.replace_seqs(seqs)  # default behaviour
         self.assertEqual(result.info["key"], "value")
 
-    def test_counts(self):
+    def test_counts(self):  # ported
         """SequenceCollection.counts handles motif length, allow_gaps etc.."""
         data = {"a": "AAAA??????", "b": "CCCGGG--NN"}
         coll = self.Class(data=data, moltype=DNA)
@@ -1408,7 +1408,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         c = aln.counts_per_pos(include_ambiguity=False, allow_gap=True)
         assert_equal(set(c.motifs), set("ACGT-"))
 
-    def test_counts_per_seq_default_moltype(self):  # will not port
+    def test_counts_per_seq_default_moltype(self):
         """produce correct counts per seq with default moltypes"""
         data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
         coll = self.Class(data=data)
@@ -1420,7 +1420,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         got = coll.counts_per_seq(include_ambiguity=True, allow_gap=True)
         self.assertEqual(got.col_sum()["-"], 2)
 
-    def test_counts_per_pos_default_moltype(self):
+    def test_counts_per_pos_default_moltype(self):  # will not port
         """produce correct counts per pos with default moltypes"""
         data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
         coll = self.Class(data=data)
@@ -2445,7 +2445,7 @@ def test_to_rich_dict_alignment():
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_dotplot_annotated(cls):
+def test_dotplot_annotated(cls):  # ported for SequenceCollection
     """exercising dotplot method with annotated sequences"""
     db = GffAnnotationDb()
     db.add_feature(seqid="Human", biotype="exon", name="fred", spans=[(10, 15)])
@@ -2529,7 +2529,7 @@ def test_get_translation_with_stop(cls):
 
 
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment, SequenceCollection))
-def test_get_translation_trim_stop(cls):
+def test_get_translation_trim_stop(cls):  # ported for SequenceCollection
     seqs = {"seq1": "GATTCCTAG", "seq2": "GATTCCTCC"}
     alignment = cls(data=seqs, moltype=DNA)
     expect = {"seq1": "DS", "seq2": "DSS"}
@@ -2694,7 +2694,7 @@ def test_has_terminal_stop_strict(cls):
         (2, ("GATTTT", "TCCAGG")),
     ),
 )
-def test_trim_stops_true(cls, gc, seqs):
+def test_trim_stops_true(cls, gc, seqs):  # ported for SequenceCollection
     gc = get_code(gc)
     data = {f"s{i}": s for i, s in enumerate(seqs)}
 
@@ -2717,7 +2717,7 @@ def test_trim_stops_true(cls, gc, seqs):
     "gc,seqs",
     ((1, ("T-CTGC", "GATAA?")), (2, ("GATTTT", "TCCCGG")), (1, ("CCTGC", "GATAA"))),
 )
-def test_trim_terminal_stops_nostop(cls, gc, seqs):
+def test_trim_terminal_stops_nostop(cls, gc, seqs):  # ported for SequenceCollection
     gc = get_code(gc)
     data = {f"s{i}": s for i, s in enumerate(seqs)}
     seqs = cls(data=data, moltype="dna")
@@ -2727,7 +2727,7 @@ def test_trim_terminal_stops_nostop(cls, gc, seqs):
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
 @pytest.mark.parametrize("seqs", (("CCTCA", "ATTTT"), ("CCTCA-", "ATTTTA")))
-def test_trim_terminal_stops_strict(cls, seqs):
+def test_trim_terminal_stops_strict(cls, seqs):  # ported for SequenceCollection
     gc = get_code(1)
     data = {f"s{i}": s for i, s in enumerate(seqs)}
     seqs = cls(data=data, moltype="dna")
@@ -2736,7 +2736,7 @@ def test_trim_terminal_stops_strict(cls, seqs):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
-def test_trim_stop_codons_info(cls):
+def test_trim_stop_codons_info(cls):  # ported for SequenceCollection
     """trim_stop_codons should preserve info attribute"""
     coll = cls(
         data={"seq1": "ACGTAA", "seq2": "ACGACG", "seq3": "ACGCGT"},
@@ -3235,7 +3235,7 @@ def brca1_data():
 
 
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment, SequenceCollection))
-def test_dotplot(cls, brca1_data):
+def test_dotplot(cls, brca1_data):  # ported for SequenceCollection
     """exercising dotplot method"""
     seqs = cls(data=brca1_data, moltype=DNA)
     _ = seqs.dotplot()
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index dbe620cc4..05f5b82de 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -300,7 +300,7 @@ def test_seqs_data_getitem_raises(dna_sd, make_seq):
         _ = dna_sd[invalid_index]
 
 
-def tests_seqs_data_subset(dna_sd):
+def test_seqs_data_subset(dna_sd):
     got = dna_sd.subset("seq1")
     assert isinstance(got, new_aln.SeqsData)
     assert got.names == ["seq1"]
@@ -332,6 +332,29 @@ def test_seqs_data_subset_raises(dna_sd):
         _ = dna_sd.subset(["seq4"])
 
 
+def test_seqs_data_to_alphabet():
+    ASCII = new_moltype.ASCII.alphabet
+    DNA = new_moltype.DNA.degen_gapped_alphabet
+    RNA = new_moltype.RNA.degen_gapped_alphabet
+    seqs = new_aln.SeqsData(data={"a": "AAA", "b": "TTT", "c": "CCC"}, alphabet=ASCII)
+    dna_seqs = seqs.to_alphabet(DNA)
+
+    assert dna_seqs.get_seq_str(seqid="b") == "TTT"
+    assert dna_seqs.alphabet == DNA
+
+    rna_seqs = dna_seqs.to_alphabet(RNA)
+    assert rna_seqs.get_seq_str(seqid="b") == "UUU"
+    assert rna_seqs.alphabet == RNA
+
+
+def test_seqs_data_to_alphabet_invalid():
+    ASCII = new_moltype.ASCII.alphabet
+    DNA = new_moltype.DNA.degen_gapped_alphabet
+    seqs = new_aln.SeqsData(data={"a": "AAA", "b": "TTT", "c": "LLL"}, alphabet=ASCII)
+    with pytest.raises(ValueError):
+        _ = seqs.to_alphabet(DNA)
+
+
 @pytest.mark.parametrize(
     "index",
     [
@@ -906,6 +929,12 @@ def test_sequence_collection_to_fasta():
     assert seqs.to_fasta(block_size=3) == ">seq_0\nGCA\nTGC\nAT\n>seq_1\nTCA\nGAC\nGT\n"
 
 
+def test_is_ragged(ragged, ragged_padded):
+    """SequenceCollection is_ragged should return true if ragged alignment"""
+    assert ragged.is_ragged()
+    assert not ragged_padded.is_ragged()
+
+
 def test_sequence_collection_to_phylip():
     """SequenceCollection should return PHYLIP string format correctly"""
     align_norm = new_aln.make_unaligned_seqs(
@@ -1044,12 +1073,13 @@ def test_sequence_collection_get_seq_annotated():
         {"seq1": "GATTTT", "seq2": "GATC??"}, moltype="dna"
     )
     seqs.add_feature(seqid="seq1", biotype="xyz", name="abc", spans=[(1, 2)])
+    seqs.add_feature(seqid="seq2", biotype="xyzzz", name="abc", spans=[(1, 2)])
 
     with_annos = seqs.get_seq("seq1", copy_annotations=True)
     assert len(with_annos.annotation_db) == 1
 
     without_annos = seqs.get_seq("seq1", copy_annotations=False)
-    assert len(without_annos.annotation_db) == 0
+    assert len(without_annos.annotation_db) == 2
 
 
 def _make_seq(name):
@@ -1112,3 +1142,64 @@ def test_copy_annotations_incompat_type_fails(seqcoll_db, seqs):
 
     with pytest.raises(TypeError):
         seqcoll_db.copy_annotations(seqs)
+
+
+def test_sequence_collection_to_moltype_with_gaps():
+    """correctly convert to specified moltype"""
+    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="text")
+    dna_seqs = seqs.to_moltype("dna")
+    assert dna_seqs.moltype.label == "dna"
+    assert dna_seqs.to_dict() == data
+
+    with pytest.raises(ValueError):
+        seqs.to_moltype("rna")
+
+
+def test_sequence_collection_to_moltype_info():
+    """correctly convert to specified moltype"""
+    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="text", info={"key": "value"})
+    dna = seqs.to_moltype("dna")
+    assert dna.info["key"] == "value"
+
+
+def test_sequence_collection_to_moltype_annotation_db():
+    """correctly convert to specified moltype"""
+    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="text")
+    db = GffAnnotationDb()
+    db.add_feature(seqid="seq1", biotype="exon", name="annotation1", spans=[(3, 8)])
+    db.add_feature(seqid="seq2", biotype="exon", name="annotation2", spans=[(1, 2)])
+    db.add_feature(seqid="seq3", biotype="exon", name="annotation3", spans=[(3, 6)])
+    seqs.annotation_db = db
+    dna = seqs.to_moltype("dna")
+    assert len(dna.annotation_db) == 3
+
+
+def test_dotplot():
+    """exercising dotplot method"""
+    seqs = new_aln.make_unaligned_seqs(
+        {
+            "Human": "CAGATTTGGCAGTT-",
+            "Mouse": "CAGATTCAGCAGGTG",
+            "Rat": "CAGATTCAGCAGGTG",
+        },
+        moltype="dna",
+    )
+    _ = seqs.dotplot()
+    with pytest.raises(AssertionError):
+        seqs.dotplot(window=5, k=11)
+
+
+def test_sequence_collection_dotplot_annotated():
+    """exercising dotplot method with annotated sequences"""
+    db = GffAnnotationDb()
+    db.add_feature(seqid="Human", biotype="exon", name="fred", spans=[(10, 15)])
+
+    seqs = new_aln.make_unaligned_seqs(
+        {"Human": "CAGATTTGGCAGTT-", "Mouse": "CAGATTCAGCAGGTG"}, moltype="dna"
+    )
+    seqs.annotation_db = db
+    seqs = seqs.take_seqs(["Human", "Mouse"], copy_annotations=True)
+    _ = seqs.dotplot(show_progress=False)