Repository: cogent3/cogent3
Pull request #1890 (2024-06-06 04:59:45)

SequenceCollection refactor - general and annotation db methods
A few tests marked as xfail - we should probably decide whether these are behaviours we want to support on the new sequence collection

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


### Details

* **180** of **251**   **(71.71%)**  changed or added relevant lines in **2** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.2%**) to **90.491%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67922869/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L511) | 143 | 214 | 66.82%
<!-- | **Total:** | **180** | **251** | **71.71%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67922869/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L881) | 2 | 80.61% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67922869/badge)](https://coveralls.io/builds/67922869) |
| :-- | --: |
| Change from base [Build 9394983695](https://coveralls.io/builds/67922393): |  0.2% |
| Covered Lines: | 31802 |
| Relevant Lines: | 35144 |

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

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


### Details

* **180** of **251**   **(71.71%)**  changed or added relevant lines in **2** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.2%**) to **90.491%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67922931/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L511) | 143 | 214 | 66.82%
<!-- | **Total:** | **180** | **251** | **71.71%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67922931/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L881) | 2 | 80.61% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67922931/badge)](https://coveralls.io/builds/67922931) |
| :-- | --: |
| Change from base [Build 9394983695](https://coveralls.io/builds/67922393): |  0.2% |
| Covered Lines: | 31802 |
| Relevant Lines: | 35144 |

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

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


### Details

* **191** of **265**   **(72.08%)**  changed or added relevant lines in **3** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.04%**) to **90.372%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_moltype.py](https://coveralls.io/builds/67946316/source?filename=src%2Fcogent3%2Fcore%2Fnew_moltype.py#L529) | 14 | 15 | 93.33%
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67946316/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L293) | 140 | 213 | 65.73%
<!-- | **Total:** | **191** | **265** | **72.08%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67946316/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L875) | 2 | 80.2% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67946316/badge)](https://coveralls.io/builds/67946316) |
| :-- | --: |
| Change from base [Build 9394983695](https://coveralls.io/builds/67922393): |  0.04% |
| Covered Lines: | 31772 |
| Relevant Lines: | 35157 |

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

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


### Details

* **191** of **265**   **(72.08%)**  changed or added relevant lines in **3** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.04%**) to **90.372%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/cogent3/core/new_moltype.py](https://coveralls.io/builds/67946366/source?filename=src%2Fcogent3%2Fcore%2Fnew_moltype.py#L529) | 14 | 15 | 93.33%
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67946366/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L293) | 140 | 213 | 65.73%
<!-- | **Total:** | **191** | **265** | **72.08%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [src/cogent3/core/new_alignment.py](https://coveralls.io/builds/67946366/source?filename=src%2Fcogent3%2Fcore%2Fnew_alignment.py#L875) | 2 | 80.2% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67946366/badge)](https://coveralls.io/builds/67946366) |
| :-- | --: |
| Change from base [Build 9394983695](https://coveralls.io/builds/67922393): |  0.04% |
| Covered Lines: | 31772 |
| Relevant Lines: | 35157 |

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


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index d2f909275..fce2b696e 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -484,17 +484,17 @@ class _SequenceCollectionBase:
         self._annotation_db = None
 
     @property
-    def named_seqs(self):
+    def named_seqs(self):  # ported
         if self._named_seqs is None:
             self._named_seqs = _make_named_seqs(self.names, self._seqs)
         return self._named_seqs
 
     @property
-    def annotation_db(self):
+    def annotation_db(self):  # ported
         return self._annotation_db
 
     @annotation_db.setter
-    def annotation_db(self, value):
+    def annotation_db(self, value):  # ported
         if value == self._annotation_db:
             return
 
@@ -503,13 +503,13 @@ class _SequenceCollectionBase:
         for seq in self.seqs:
             seq.replace_annotation_db(value, check=False)
 
-    def __str__(self):
+    def __str__(self):  # ported
         """Returns self in FASTA-format, respecting name order."""
         from cogent3.format.alignment import FORMATTERS
 
         return FORMATTERS["fasta"](self.to_dict())
 
-    def _set_additional_attributes(self, curr_seqs):
+    def _set_additional_attributes(self, curr_seqs):  # will not port
         """Sets additional attributes based on current seqs: class-specific."""
         self.seq_data = curr_seqs
         self._seqs = curr_seqs
@@ -518,7 +518,7 @@ class _SequenceCollectionBase:
         except ValueError:  # got empty sequence, for some reason?
             self.seq_len = 0
 
-    def copy(self):
+    def copy(self):  # ported
         """Returns deep copy of self."""
         result = self.__class__(self, moltype=self.moltype, info=self.info)
         result.annotation_db = deepcopy(self.annotation_db)
@@ -556,7 +556,7 @@ class _SequenceCollectionBase:
         result._repr_policy.update(self._repr_policy)
         return result
 
-    def __eq__(self, other):
+    def __eq__(self, other):  # ported
         """first tests as dict, then as str"""
         c = self.named_seqs == other
         if not c:
@@ -564,7 +564,7 @@ class _SequenceCollectionBase:
 
         return c
 
-    def __ne__(self, other):
+    def __ne__(self, other):  # ported
         """first tests as dict, then as str"""
         c = self.named_seqs != other
         if not c:
@@ -572,7 +572,7 @@ class _SequenceCollectionBase:
 
         return c
 
-    def __lt__(self, other):
+    def __lt__(self, other):  # will not port
         """cmp first tests as dict, then as str."""
         c = self.named_seqs < other
         if not c:
@@ -580,7 +580,7 @@ class _SequenceCollectionBase:
         else:
             return str(self) < str(other)
 
-    def iter_seqs(self, seq_order=None):
+    def iter_seqs(self, seq_order=None):  # ported
         """Iterates over values (sequences) in the alignment, in order.
 
         seq_order: list of keys giving the order in which seqs will be returned.
@@ -602,12 +602,14 @@ class _SequenceCollectionBase:
             yield get(key)
 
     @property
-    def seqs(self):
+    def seqs(self):  # ported
         return list(self.iter_seqs())
 
     # access as attribute if using default order.
 
-    def take_seqs(self, seqs: Union[str, typing.Sequence[str]], negate=False, **kwargs):
+    def take_seqs(
+        self, seqs: Union[str, typing.Sequence[str]], negate=False, **kwargs
+    ):  # ported
         """Returns new Alignment containing only specified seqs.
 
         Note that the seqs in the new alignment will be references to the
@@ -645,7 +647,7 @@ class _SequenceCollectionBase:
             )
         return result
 
-    def get_seq_indices(self, f, negate=False):
+    def get_seq_indices(self, f, negate=False):  # ported
         """Returns list of keys of seqs where f(row) is True.
 
         List will be in the same order as self.names, if present.
@@ -662,7 +664,7 @@ class _SequenceCollectionBase:
         # get all the seqs where the function is True
         return [key for key in self.names if new_f(get(key))]
 
-    def take_seqs_if(self, f, negate=False, **kwargs):
+    def take_seqs_if(self, f, negate=False, **kwargs):  # ported
         """Returns new Alignment containing seqs where f(row) is True.
 
         Note that the seqs in the new Alignment are the same objects as the
@@ -671,7 +673,7 @@ class _SequenceCollectionBase:
         # pass negate to get SeqIndices
         return self.take_seqs(self.get_seq_indices(f, negate), **kwargs)
 
-    def iter_selected(self, seq_order=None, pos_order=None):
+    def iter_selected(self, seq_order=None, pos_order=None):  # ported
         """Iterates over elements in the alignment.
 
         seq_order (names) can be used to select a subset of seqs.
@@ -693,7 +695,7 @@ class _SequenceCollectionBase:
                 for i in row:
                     yield i
 
-    def get_identical_sets(self, mask_degen=False):
+    def get_identical_sets(self, mask_degen=False):  # ported
         """returns sets of names for sequences that are identical
 
         Parameters
@@ -781,7 +783,7 @@ class _SequenceCollectionBase:
         max_similarity=1.0,
         metric=frac_same,
         transform=None,
-    ):
+    ):  # ported
         """Returns new Alignment containing sequences similar to target.
 
         Parameters
@@ -830,7 +832,7 @@ class _SequenceCollectionBase:
 
         return self.take_seqs_if(f)
 
-    def is_ragged(self):
+    def is_ragged(self):  # ported
         """Returns True if alignment has sequences of different lengths."""
         seqs = self.seqs  # Get all sequences in alignment
         length = len(seqs[0])  # Get length of first sequence
@@ -841,7 +843,7 @@ class _SequenceCollectionBase:
         # lengths were all equal
         return False
 
-    def to_phylip(self):
+    def to_phylip(self):  # ported
         """
         Return alignment in PHYLIP format and mapping to sequence ids
 
@@ -883,7 +885,7 @@ class _SequenceCollectionBase:
         """returns json formatted string"""
         return json.dumps(self.to_rich_dict())
 
-    def to_fasta(self, block_size: int = 60) -> str:
+    def to_fasta(self, block_size: int = 60) -> str:  # ported
         """Return alignment in Fasta format.
 
         Parameters
@@ -898,7 +900,7 @@ class _SequenceCollectionBase:
         """
         return alignment_to_fasta(self.to_dict(), block_size=block_size)
 
-    def to_nexus(self, seq_type, wrap=50):
+    def to_nexus(self, seq_type, wrap=50):  # will not port
         """
         Return alignment in NEXUS format and mapping to sequence ids
 
@@ -913,11 +915,11 @@ class _SequenceCollectionBase:
         return nexus_from_alignment(self, seq_type, wrap=wrap)
 
     @property
-    def num_seqs(self):
+    def num_seqs(self):  # ported
         """Returns the number of sequences in the alignment."""
         return len(self.named_seqs)
 
-    def __add__(self, other):
+    def __add__(self, other):  # will not port
         """Concatenates sequence data for same names"""
         aligned = isinstance(self, Alignment)
 
@@ -940,7 +942,7 @@ class _SequenceCollectionBase:
         new.annotation_db = None
         return new
 
-    def add_seqs(self, other, before_name=None, after_name=None):
+    def add_seqs(self, other, before_name=None, after_name=None):  # ported
         """Returns new object of class self with sequences from other added.
 
         Parameters
@@ -1049,13 +1051,13 @@ class _SequenceCollectionBase:
 
         save_to_filename(align_dict, filename, format, **kwargs)
 
-    def __len__(self):
+    def __len__(self):  # ported
         """len of SequenceCollection returns length of longest sequence."""
         return self.seq_len
 
     def get_translation(
         self, gc=None, incomplete_ok=False, include_stop=False, trim_stop=True, **kwargs
-    ):
+    ):  # will not port
         """translate from nucleic acid to protein
 
         Parameters
@@ -1098,11 +1100,11 @@ class _SequenceCollectionBase:
         kwargs["moltype"] = pep.moltype
         return self.__class__(translated, info=self.info, **kwargs)
 
-    def get_seq(self, seqname):
+    def get_seq(self, seqname):  # ported
         """Return a sequence object for the specified seqname."""
         return self.named_seqs[seqname]
 
-    def to_dict(self) -> dict[str, str]:
+    def to_dict(self) -> dict[str, str]:  # ported
         """Returns the alignment as a dict of sequence names -> strings.
 
         Note the mapping goes to strings, not Sequence objects.
@@ -1132,7 +1134,7 @@ class _SequenceCollectionBase:
                     ambig[i] = motif
         return result
 
-    def degap(self, **kwargs):
+    def degap(self, **kwargs):  # ported
         """Returns copy in which sequences have no gaps.
 
         Parameters
@@ -1153,7 +1155,7 @@ class _SequenceCollectionBase:
         )
         return result
 
-    def with_modified_termini(self):
+    def with_modified_termini(self):  # will not port
         """Changes the termini to include termini char instead of gapmotif.
 
         Useful to correct the standard gap char output by most
@@ -1169,7 +1171,7 @@ class _SequenceCollectionBase:
             result.annotation_db = self.annotation_db
         return result
 
-    def has_terminal_stop(self, gc: Any = None, strict: bool = False) -> bool:
+    def has_terminal_stop(self, gc: Any = None, strict: bool = False) -> bool:  # ported
         """Returns True if any sequence has a terminal stop codon.
 
         Parameters
@@ -1190,7 +1192,9 @@ class _SequenceCollectionBase:
                 return True
         return False
 
-    def trim_stop_codons(self, gc: Any = None, strict: bool = False, **kwargs):
+    def trim_stop_codons(
+        self, gc: Any = None, strict: bool = False, **kwargs
+    ):  # will not port
         """Removes any terminal stop codons from the sequences
 
         Parameters
@@ -1214,7 +1218,7 @@ class _SequenceCollectionBase:
             result.annotation_db = self.annotation_db
         return result
 
-    def get_lengths(self, include_ambiguity=False, allow_gap=False):
+    def get_lengths(self, include_ambiguity=False, allow_gap=False):  # ported
         """returns {name: seq length, ...}
 
         Parameters
@@ -1238,7 +1242,7 @@ class _SequenceCollectionBase:
         allow_gap=False,
         exclude_unobserved=False,
         warn=False,
-    ):
+    ):  # ported
         """counts of motifs per sequence
 
         Parameters
@@ -1289,7 +1293,7 @@ class _SequenceCollectionBase:
         include_ambiguity=False,
         allow_gap=False,
         exclude_unobserved=False,
-    ):
+    ):  # ported
         """counts of motifs
 
         Parameters
@@ -1324,7 +1328,7 @@ class _SequenceCollectionBase:
         exclude_unobserved=False,
         allow_gap=False,
         pseudocount=0,
-    ):
+    ):  # ported
         """Return a dictionary of motif probs, calculated as the averaged
         frequency across sequences.
 
@@ -1389,7 +1393,7 @@ class _SequenceCollectionBase:
 
         return probs
 
-    def omit_gap_seqs(self, allowed_gap_frac=0):
+    def omit_gap_seqs(self, allowed_gap_frac=0):  # will not port
         """Returns new alignment with seqs that have <= allowed_gap_frac.
 
         allowed_gap_frac should be a fraction between 0 and 1 inclusive.
@@ -1414,7 +1418,7 @@ class _SequenceCollectionBase:
 
         return self.take_seqs_if(ok_gap_run)
 
-    def to_moltype(self, moltype):
+    def to_moltype(self, moltype):  # ported
         """returns copy of self with moltype seqs"""
         import cogent3
 
@@ -1708,7 +1712,9 @@ class _SequenceCollectionBase:
 
         return array(result)
 
-    def set_repr_policy(self, num_seqs=None, num_pos=None, ref_name=None, wrap=None):
+    def set_repr_policy(
+        self, num_seqs=None, num_pos=None, ref_name=None, wrap=None
+    ):  # ported
         """specify policy for repr(self)
 
         Parameters
@@ -1820,7 +1826,7 @@ class SequenceCollection(_SequenceCollectionBase):
         anno_db = merged_db_collection(self.seqs)
         self.annotation_db = anno_db or DEFAULT_ANNOTATION_DB()
 
-    def copy_annotations(self, seq_db: SupportsFeatures) -> None:
+    def copy_annotations(self, seq_db: SupportsFeatures) -> None:  # ported
         """copy annotations into attached annotation db
 
         Parameters
@@ -1860,7 +1866,7 @@ class SequenceCollection(_SequenceCollectionBase):
 
     def annotate_from_gff(
         self, f: os.PathLike, seq_ids: Optional[Union[list[str], str]] = None
-    ):
+    ):  # will not port
         """copies annotations from a gff file to a sequence in self
 
         Parameters
@@ -1887,7 +1893,7 @@ class SequenceCollection(_SequenceCollectionBase):
         self,
         *,
         feature: FeatureDataType,
-    ) -> Feature:
+    ) -> Feature:  # ported
         """
         create a feature on named sequence, or on the alignment itself
 
@@ -1915,7 +1921,7 @@ class SequenceCollection(_SequenceCollectionBase):
         spans: List[Tuple[int, int]],
         parent_id: Optional[str] = None,
         strand: str = "+",
-    ) -> Feature:
+    ) -> Feature:  # ported
         """
         add feature on named sequence
 
@@ -1958,7 +1964,7 @@ class SequenceCollection(_SequenceCollectionBase):
         biotype: Optional[str] = None,
         name: Optional[str] = None,
         allow_partial: bool = False,
-    ) -> Iterator[Feature]:
+    ) -> Iterator[Feature]:  # ported
         """yields Feature instances
 
         Parameters
@@ -2044,7 +2050,7 @@ class SequenceCollection(_SequenceCollectionBase):
 
         return dist_calc_app(self)
 
-    def __repr__(self):
+    def __repr__(self):  # ported
         seqs = []
         limit = 10
         delimiter = ""
@@ -2070,7 +2076,7 @@ class SequenceCollection(_SequenceCollectionBase):
 
         return f"{len(self.names)}x ({seqs}) {self.moltype.get_type()} seqcollection"
 
-    def _repr_html_(self) -> str:
+    def _repr_html_(self) -> str:  # ported
         settings = self._repr_policy.copy()
         env_vals = get_setting_from_environ(
             "COGENT3_ALIGNMENT_REPR_POLICY",
@@ -2091,7 +2097,7 @@ class SequenceCollection(_SequenceCollectionBase):
         colors: Optional[Mapping[str, str]] = None,
         font_size: int = 12,
         font_family: str = "Lucida Console",
-    ) -> str:
+    ) -> str:  # ported
         """returns html with embedded styles for sequence colouring
 
         Parameters
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 3e9239540..7434642b3 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -16,9 +16,16 @@ import cogent3.core.new_alphabet as new_alpha
 import cogent3.core.new_moltype as new_moltype
 import cogent3.core.new_sequence as new_seq
 
-from cogent3.core.annotation_db import BasicAnnotationDb, SupportsFeatures
+from cogent3.core.annotation import Feature
+from cogent3.core.annotation_db import (
+    BasicAnnotationDb,
+    FeatureDataType,
+    SupportsFeatures,
+)
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import IndelMap
+from cogent3.format.fasta import alignment_to_fasta
+from cogent3.format.phylip import alignment_to_phylip
 from cogent3.util import warning as c3warn
 from cogent3.util.misc import get_setting_from_environ, negate_condition
 
@@ -84,7 +91,7 @@ class SeqDataView(new_seq.SliceRecordABC):
     @property
     def _zero_slice(self):
         return self.__class__(
-            seqs=self.seqs, seqid=self.seqid, seq_len=self.seq_len, start=0, stop=0
+            seqs=self.seqs, seqid=self.seqid, seq_len=self._seq_len, start=0, stop=0
         )
 
     @property
@@ -148,15 +155,16 @@ class SeqsDataABC(ABC):
     """
 
     alphabet: new_alpha.CharAlphabet
-    make_seq: Callable = None
+    make_seq: Callable = (
+        None  # refactor: type hint for callable should specificy input/return type
+    )
 
     @abstractmethod
     def seq_lengths(self) -> dict[str, int]: ...
 
     @property
     @abstractmethod
-    def names(self) -> list:  # refactor: design
-        ...
+    def names(self) -> list: ...
 
     @property
     @abstractmethod
@@ -194,6 +202,9 @@ class SeqsDataABC(ABC):
     @abstractmethod
     def to_alphabet(self, alphabet: new_alpha.CharAlphabet) -> SeqsDataABC: ...
 
+    @abstractmethod
+    def add_seqs(self, seqs) -> SeqsDataABC: ...
+
     @abstractmethod
     def __len__(self): ...
 
@@ -220,7 +231,7 @@ class SeqsData(SeqsDataABC):
         }
 
     @property
-    def names(self) -> list:  # refactor: design
+    def names(self) -> list:
         return list(self._data.keys())
 
     @property
@@ -239,8 +250,6 @@ class SeqsData(SeqsDataABC):
     def make_seq(self, make_seq: Callable) -> None:
         self._make_seq = make_seq
 
-    # todo: kath, do we want a num_seqs property for SeqsData?
-
     @property
     def alphabet(self) -> new_alpha.CharAlphabet:
         return self._alphabet
@@ -270,14 +279,22 @@ class SeqsData(SeqsDataABC):
     def subset(self, names: Union[str, typing.Sequence[str]]) -> SeqsData:
         """Returns a new SeqsData object with only the specified names."""
         names = [names] if isinstance(names, str) else names
-        if data := {name: self._data.get(name) for name in names}:
+        if data := {name: self._data.get(name) for name in names if name in self.names}:
             return self.__class__(
                 data=data, alphabet=self.alphabet, make_seq=self.make_seq
             )
         else:
             raise ValueError(f"provided {names=} not found in collection")
 
-    def to_alphabet(self, alphabet: new_alpha.CharAlphabet) -> SeqsData:
+    def add_seqs(
+        self, seqs: dict[str, PrimitiveSeqTypes], force_unique_keys=True
+    ) -> SeqsData:
+        # todo: kath
+        ...
+
+    def to_alphabet(
+        self, alphabet: new_alpha.CharAlphabet, check_valid=True
+    ) -> SeqsData:
         # todo: kath
         ...
 
@@ -304,78 +321,6 @@ class SeqsData(SeqsDataABC):
         return self[self.names[index]]
 
 
-@singledispatch
-@singledispatch
-def make_seqsdata_dict(
-    data, label_to_name: Callable = None
-) -> dict[str, Union[PrimitiveSeqTypes, new_seq.Sequence]]:
-    if isinstance(data, SeqsData):
-        return data
-    raise NotImplementedError(f"make_data_dict not implemented for {type(data)}")
-
-
-@make_seqsdata_dict.register
-def _(
-    data: dict, label_to_name: Callable = None
-) -> dict[str, Union[PrimitiveSeqTypes, new_seq.Sequence]]:
-    return {label_to_name(k): v for k, v in data.items()} if label_to_name else data
-
-
-@make_seqsdata_dict.register
-def _(
-    data: list, label_to_name: Callable = None
-) -> dict[str, Union[PrimitiveSeqTypes, new_seq.Sequence]]:
-    names = assign_sequential_names(len(data))
-    return make_seqsdata_dict(dict(zip(names, data)), label_to_name=label_to_name)
-
-
-def make_unaligned_seqs(
-    *,
-    data: Union[dict[str, PrimitiveSeqTypes], SeqsData, list],
-    moltype: Union[str, new_moltype.MolType],
-    label_to_name: Callable = None,
-    info: dict = None,
-    source: Union[str, Path] = None,
-    annotation_db: SupportsFeatures = None,
-    **kwargs,
-):
-    """Initialize an unaligned collection of sequences.
-
-    Parameters
-    ----------
-    data
-        sequences, assumes a dict {name: seq, ...} or a SeqsData
-    moltype
-        string representation of the moltype, e.g., 'dna', 'protein'.
-    label_to_name
-        function for converting original names into other names.
-    info
-        a dict from which to make an info object
-    source
-        origins of this data, defaults to 'unknown'. Converted to a string
-        and added to info["source"].
-    **kwargs
-        other keyword arguments to be passed to SequenceCollection
-    """
-    moltype = new_moltype.get_moltype(moltype)
-
-    if len(data) == 0:
-        raise ValueError("data must be at least one sequence.")
-
-    seqs_data = make_seqsdata_dict(data, label_to_name=label_to_name)
-
-    if isinstance(seqs_data, dict):
-        seqs_data = SeqsData(data=seqs_data, alphabet=moltype.degen_gapped_alphabet)
-    elif not moltype.is_compatible_alphabet(data.alphabet):
-        raise ValueError(
-            f"Provided moltype: {moltype} is not compatible with SeqsData alphabet {data.alphabet}"
-        )
-
-    return SequenceCollection(
-        seqs_data=seqs_data, moltype=moltype, info=info, annotation_db=annotation_db
-    )
-
-
 class SequenceCollection:
     def __init__(
         self,
@@ -429,7 +374,6 @@ class SequenceCollection:
 
         self._annotation_db = value
 
-        # todo: kath
         for seq in self.seqs:
             seq.replace_annotation_db(value, check=False)
 
@@ -535,7 +479,399 @@ class SequenceCollection:
         # pass take_seqs the result of get_seq_indices
         return self.take_seqs(self.get_seq_names_if(f, negate), **kwargs)
 
-    def __eq__(self, other: Union[new_seq.SequenceCollection, dict]) -> bool:
+    def get_seq(self, seqname: str, copy_annotations: bool = False):
+        """Return a sequence object for the specified seqname."""
+        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)
+
+        return seq
+
+    def add_seqs(self, seqs: dict[str, PrimitiveSeqTypes], **kwargs):
+        # todo: kath, add this method once its functional on SeqsData
+        ...
+
+    def to_dict(self, as_array: bool = False) -> dict[str, Union[str, numpy.ndarray]]:
+        """Return a dictionary of sequences.
+
+        Parameters
+        ----------
+        as_array
+            if True, sequences are returned as numpy arrays, otherwise as strings
+        """
+
+        get = self.seqs.get_seq_array if as_array else self.seqs.get_seq_str
+        return {name: get(seqid=name) for name in self.names}
+
+    def degap(self, **kwargs):
+        """Returns copy in which sequences have no gaps.
+
+        Parameters
+        ----------
+        kwargs
+            passed to class constructor
+        """
+        # refactor: array - chars > gap char
+        seqs = {name: self.seqs[name].degap() for name in self.names}
+        return make_unaligned_seqs(seqs, moltype=self.moltype, info=self.info, **kwargs)
+
+    def get_lengths(
+        self, include_ambiguity: bool = False, allow_gap: bool = False
+    ) -> dict[str, int]:
+        """returns sequence lengths as a dict of {seqid: length}
+
+        Parameters
+        ----------
+        include_ambiguity
+            if True, motifs containing ambiguous characters
+            from the seq moltype are included. No expansion of those is attempted.
+        allow_gap
+            if True, motifs containing a gap character are included.
+
+        """
+        counts = self.counts_per_seq(
+            motif_length=1, include_ambiguity=include_ambiguity, allow_gap=allow_gap
+        )
+        return counts.row_sum()
+
+    def copy_annotations(self, seq_db: SupportsFeatures) -> None:
+        """copy annotations into attached annotation db
+
+        Parameters
+        ----------
+        seq_db
+            compatible annotation db
+
+        Notes
+        -----
+        Only copies annotations for records with seqid in self.names
+        """
+        if not isinstance(seq_db, SupportsFeatures):
+            raise TypeError(
+                f"type {type(seq_db)} does not match SupportsFeatures interface"
+            )
+
+        num = 0
+        for seqid in self.names:
+            num += seq_db.num_matches(seqid=seqid)
+            if num > 0:
+                break
+        else:
+            # no matching ID's, nothing to do
+            return
+
+        if self.annotation_db is None:
+            self.annotation_db = type(seq_db)()
+
+        if self.annotation_db.compatible(seq_db, symmetric=False):
+            # our db contains the tables in other, so we update in place
+            self.annotation_db.update(annot_db=seq_db, seqids=self.names)
+        else:
+            # we use the union method to define a new one
+            # the setter handles propagation of the new instance to bound
+            # sequences
+            self.annotation_db = self.annotation_db.union(seq_db)
+
+    def make_feature(
+        self,
+        *,
+        feature: FeatureDataType,
+    ) -> Feature:
+        """
+        create a feature on named sequence, or on the collection itself
+
+        Parameters
+        ----------
+        feature
+            a dict with all the necessary data to construct a feature
+
+        Returns
+        -------
+        Feature
+
+        Notes
+        -----
+        To get a feature AND add it to annotation_db, use add_feature().
+        """
+        return self.seqs[feature["seqid"]].make_feature(feature)
+
+    def add_feature(
+        self,
+        *,
+        seqid: str,
+        biotype: str,
+        name: str,
+        spans: list[tuple[int, int]],
+        parent_id: Optional[str] = None,
+        strand: str = "+",
+    ) -> Feature:
+        """
+        add feature on named sequence
+
+        Parameters
+        ----------
+        seqid
+            seq name to associate with
+        parent_id
+            name of the parent feature
+        biotype
+            biological type
+        name
+            feature name
+        spans
+            plus strand coordinates
+        strand
+            either '+' or '-'
+
+        Returns
+        -------
+        Feature
+        """
+        if not self.annotation_db:
+            self.annotation_db = DEFAULT_ANNOTATION_DB()
+
+        if seqid and seqid not in self.names:
+            raise ValueError(f"unknown {seqid=}")
+
+        feature = {k: v for k, v in locals().items() if k != "self"}
+
+        self.annotation_db.add_feature(**feature)
+        feature.pop("parent_id", None)
+        return self.make_feature(feature=feature)
+
+    def get_features(
+        self,
+        *,
+        seqid: Union[str, Iterator[str]] = None,
+        biotype: Optional[str] = None,
+        name: Optional[str] = None,
+        allow_partial: bool = False,
+    ) -> Iterator[Feature]:
+        """yields Feature instances
+
+        Parameters
+        ----------
+        seqid
+            limit search to features on this named sequence, defaults to search all
+        biotype
+            biotype of the feature, e.g. CDS, gene
+        name
+            name of the feature
+        allow_partial
+            allow features partially overlaping self
+
+        Notes
+        -----
+        When dealing with a nucleic acid moltype, the returned features will
+        yield a sequence segment that is consistently oriented irrespective
+        of strand of the current instance.
+        """
+        if not self.annotation_db:
+            return None
+
+        seqid_to_seqname = {seq.parent_coordinates()[0]: seq.name for seq in self.seqs}
+        if seqid and (
+            seqid not in seqid_to_seqname and seqid not in seqid_to_seqname.values()
+        ):
+            raise ValueError(f"unknown {seqid=}")
+
+        for feature in self.annotation_db.get_features_matching(
+            seqid=seqid,
+            biotype=biotype,
+            name=name,
+            on_alignment=False,
+            allow_partial=allow_partial,
+        ):
+            seqname = seqid_to_seqname[feature["seqid"]]
+            seq = self.seqs[seqname]
+            if offset := seq.annotation_offset:
+                feature["spans"] = (numpy.array(feature["spans"]) - offset).tolist()
+            yield seq.make_feature(feature, self)
+
+    def to_fasta(self, block_size: int = 60) -> str:
+        """Return collection in Fasta format.
+
+        Parameters
+        ----------
+        block_size
+            the sequence length to write to each line,
+            by default 60
+
+        Returns
+        -------
+        The collection in Fasta format.
+        """
+        return alignment_to_fasta(self.to_dict(), block_size=block_size)
+
+    def to_phylip(self):
+        """
+        Return collection in PHYLIP format and mapping to sequence ids
+
+        Notes
+        -----
+        raises exception if invalid sequences do not all have the same length
+        """
+        if len(set(self.seqs.seq_lengths().values())) > 1:
+            raise ValueError("not all seqs same length, cannot convert to phylip")
+
+        return alignment_to_phylip(self.to_dict())
+
+    def has_terminal_stop(self, gc: typing.Any = None, strict: bool = False) -> bool:
+        """Returns True if any sequence has a terminal stop codon.
+
+        Parameters
+        ----------
+        gc
+            valid input to cogent3.get_code(), a genetic code object, number
+            or name
+        strict
+            If True, raises an exception if a seq length not divisible by 3
+        """
+
+        for seq_name in self.names:
+            seq = self.seqs[seq_name]
+            if seq.has_terminal_stop(gc=gc, strict=strict):
+                return True
+        return False
+
+    def get_identical_sets(self, mask_degen: bool = False):  # refactor: array
+        """returns sets of names for sequences that are identical
+
+        Parameters
+        ----------
+        mask_degen
+            if True, degenerate characters are ignored
+
+        """
+
+        def reduced(seq, indices):
+            return "".join(seq[i] for i in range(len(seq)) if i not in indices)
+
+        identical_sets = []
+        mask_posns = {}
+        seen = []
+        # if strict, we do a sort and one pass through the list
+        seqs = self.to_dict()
+        if not mask_degen:
+            seqs_names = [(s, n) for n, s in seqs.items()]
+            seqs_names.sort()
+            matched = None
+            dupes = defaultdict(set)
+            for i in range(len(seqs_names) - 1):
+                if seqs_names[i][0] == seqs_names[i + 1][0]:
+                    matched = seqs_names[i][1] if matched is None else matched
+                    dupes[matched].update([seqs_names[i + 1][1], matched])
+                else:
+                    matched = None
+            identical_sets = list(dupes.values())
+            return identical_sets
+
+        for i in range(len(self.names) - 1):
+            n1 = self.names[i]
+            if n1 in seen:
+                continue
+
+            seq1 = seqs[n1]
+            if n1 not in mask_posns:
+                pos = self.moltype.get_degenerate_positions(seq1, include_gap=True)
+                mask_posns[n1] = pos
+
+            group = set()
+            for j in range(i + 1, len(self.names)):
+                n2 = self.names[j]
+                if n2 in seen:
+                    continue
+
+                seq2 = seqs[n2]
+                if n2 not in mask_posns:
+                    pos = self.moltype.get_degenerate_positions(seq2, include_gap=True)
+                    mask_posns[n2] = pos
+
+                seq2 = seqs[n2]
+
+                s1, s2 = seq1, seq2
+
+                pos = mask_posns[n1] + mask_posns[n2]
+                if pos:
+                    s1 = reduced(seq1, pos)
+                    s2 = reduced(seq2, pos)
+
+                if s1 == s2:
+                    seen.append(n2)
+                    group.update([n1, n2])
+
+            if group:
+                identical_sets.append(group)
+
+        return identical_sets
+
+    def get_similar(
+        self,
+        target: new_seq.Sequence,
+        min_similarity: float = 0.0,
+        max_similarity: float = 1.0,
+        metric: float = new_seq.frac_same,
+        transform: bool = None,
+    ) -> new_seq.SequenceCollection:
+        """Returns new SequenceCollection containing sequences similar to target.
+
+        Parameters
+        ----------
+        target
+            sequence object to compare to. Can be in the collection.
+        min_similarity
+            minimum similarity that will be kept. Default 0.0.
+        max_similarity
+            maximum similarity that will be kept. Default 1.0.
+            (Note that both min_similarity and max_similarity are inclusive.)
+        metric
+            similarity function to use. Must be f(first_seq, second_seq).
+            The default metric is fraction similarity, ranging from 0.0 (0%
+            identical) to 1.0 (100% identical). The Sequence classes have lots
+            of methods that can be passed in as unbound methods to act as the
+            metric, e.g. frac_same_gaps.
+        transform
+            transformation function to use on the sequences before
+            the metric is calculated. If None, uses the whole sequences in each
+            case. A frequent transformation is a function that returns a specified
+            range of a sequence, e.g. eliminating the ends. Note that the
+            transform applies to both the real sequence and the target sequence.
+
+        WARNING: if the transformation changes the type of the sequence (e.g.
+        extracting a string from an RnaSequence object), distance metrics that
+        depend on instance data of the original class may fail.
+        """
+        if transform:
+            target = transform(target)
+
+        def m(x):
+            return metric(target, x)
+
+        if transform:
+
+            def f(x):
+                result = m(transform(x))
+                return min_similarity <= result <= max_similarity
+
+        else:
+
+            def f(x):
+                result = m(x)
+                return min_similarity <= result <= max_similarity
+
+        return self.take_seqs_if(f)
+
+    def __str__(self):
+        """Returns self in FASTA-format, respecting name order."""
+        from cogent3.format.alignment import FORMATTERS
+
+        return FORMATTERS["fasta"](self.to_dict())
+
+    def __eq__(
+        self, other: Union[new_seq.SequenceCollection, dict]
+    ) -> bool:  # refactor: design
         return id(self) == id(other)
 
     def __ne__(self, other: new_seq.SequenceCollection) -> bool:
@@ -596,9 +932,9 @@ class SequenceCollection:
         name_order
             order of names for display.
         wrap
-            number of alignment columns per row
+            number of columns per row
         limit
-            truncate alignment to this length
+            truncate view of collection to this length
         colors
             {character
             moltype.
@@ -628,7 +964,7 @@ class SequenceCollection:
             colors=colors, font_size=font_size, font_family=font_family
         )
 
-        seq_lengths = numpy.array([len(seq) for seq in self.seqs])
+        seq_lengths = numpy.array(list(self.seqs.seq_lengths().values()))
         min_val = seq_lengths.min()
         max_val = seq_lengths.max()
         med_val = numpy.median(seq_lengths)
@@ -779,6 +1115,180 @@ class SequenceCollection:
             self._repr_policy["wrap"] = wrap
 
 
+@singledispatch
+def merged_db_collection(seqs) -> SupportsFeatures:
+    """return one AnnotationDb from a collection of sequences
+
+    Parameters
+    ----------
+    seqs
+        iterable list of data
+
+    Returns
+    -------
+    list of all annotation db's
+
+    Raises
+    ------
+    TypeError if different classes of AnnotationDb
+    """
+    first = None
+    merged = None
+    for seq in seqs:
+        if not isinstance(seq, new_seq.Sequence):
+            continue
+
+        db = seq.annotation_db
+
+        if first is None and db:
+            # todo gah should this be a copy so immutable?
+            first = db
+            merged = first
+            continue
+
+        if first is None or db is None or first is db:
+            continue
+        first.update(db)
+
+    return merged
+
+
+@merged_db_collection.register
+def _(seqs: dict) -> SupportsFeatures:
+    return merged_db_collection(seqs.values())
+
+
+@singledispatch
+def coerce_to_seqs_data_dict(
+    data, label_to_name: Callable = None
+) -> dict[
+    str, Union[PrimitiveSeqTypes, new_seq.Sequence]
+]:  # refactor: type hint for callable should specificy input/return type
+    raise NotImplementedError(
+        f"coerce_to_seqs_data_dict not implemented for {type(data)}"
+    )
+
+
+@coerce_to_seqs_data_dict.register
+def _(data: dict, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
+    is_sequence = isinstance(next(iter(data.values()), None), new_seq.Sequence)
+    return {
+        (label_to_name(k) if label_to_name else k): (str(v) if is_sequence else v)
+        for k, v in data.items()
+    }
+
+
+@coerce_to_seqs_data_dict.register
+def _(data: list, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
+    first = data[0]
+    labelled_seqs = assign_names(first, data=data)
+    return coerce_to_seqs_data_dict(labelled_seqs, label_to_name=label_to_name)
+
+
+@coerce_to_seqs_data_dict.register
+def _(data: set, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
+    first = next(iter(data))
+    labelled_seqs = assign_names(first, data=data)
+    return coerce_to_seqs_data_dict(labelled_seqs, label_to_name=label_to_name)
+
+
+@singledispatch
+def assign_names(
+    first, data: Union[list, set]
+) -> dict[str, Union[PrimitiveSeqTypes, new_seq.Sequence]]:
+    if isinstance(first, (str, bytes, numpy.ndarray)):
+        names = assign_sequential_names(len(data))
+        return dict(zip(names, data))
+    raise NotImplementedError(f"assign_names not implemented for {type(data)}")
+
+
+@assign_names.register
+def _(first: new_seq.Sequence, data: Union[list, set]) -> dict[str, new_seq.Sequence]:
+    return {seq.name: seq for seq in data}
+
+
+@assign_names.register
+def _(first: list, data: Union[list, set]) -> dict[str, PrimitiveSeqTypes]:
+    return {name_seq[0]: name_seq[1] for name_seq in data}
+
+
+@singledispatch
+def make_unaligned_seqs(
+    data: Union[dict[str, PrimitiveSeqTypes], SeqsData, list],
+    *,
+    moltype: Union[str, new_moltype.MolType],
+    label_to_name: Callable = None,
+    info: dict = None,
+    source: Union[str, Path] = None,
+    annotation_db: SupportsFeatures = None,
+) -> SequenceCollection:  # refactor: design
+    """Initialise an unaligned collection of sequences.
+
+    Parameters
+    ----------
+    data
+        sequence data, a SeqsData, a dict {name: seq, ...}, an iterable of sequences
+    moltype
+        string representation of the moltype, e.g., 'dna', 'protein'.
+    label_to_name
+        function for converting original names into other names.
+    info
+        a dict from which to make an info object
+    source
+        origins of this data, defaults to 'unknown'. Converted to a string
+        and added to info["source"].
+    """
+    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
+
+    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)
+    return make_unaligned_seqs(
+        seqs_data,
+        moltype=moltype,
+        label_to_name=label_to_name,
+        info=info,
+        source=source,
+        annotation_db=annotation_db,
+    )
+
+
+@make_unaligned_seqs.register
+def _(
+    data: SeqsData,
+    *,
+    moltype: Union[str, new_moltype.MolType],
+    label_to_name: Callable = None,
+    info: dict = None,
+    source: Union[str, Path] = None,
+    annotation_db: SupportsFeatures = None,
+) -> SequenceCollection:
+
+    moltype = new_moltype.get_moltype(moltype)
+    if not moltype.is_compatible_alphabet(data.alphabet):
+        raise ValueError(
+            f"Provided moltype: {moltype} is not compatible with SeqsData alphabet {data.alphabet}"
+        )
+
+    info = info if isinstance(info, dict) else {}
+    info["source"] = str(info.get("source", "unknown"))
+
+    return SequenceCollection(
+        seqs_data=data, moltype=moltype, info=info, annotation_db=annotation_db
+    )
+
+
 @singledispatch
 def seq_to_gap_coords(
     seq: Union[str, numpy.ndarray], moltype: new_moltype.MolType
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index 4950f9bc9..f05ed9358 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -528,6 +528,31 @@ class MolType:
     def _(self, seq: numpy.ndarray) -> bool:
         return (seq == self.degen_gapped_alphabet.gap_index).any()
 
+    @functools.singledispatchmethod
+    def get_degenerate_positions(self, seq, include_gap=True) -> numpy.ndarray:
+        """returns a boolean array indicating degenerate positions"""
+        raise TypeError(f"{type(seq)} not supported")
+
+    @get_degenerate_positions.register
+    def _(self, seq: numpy.ndarray, include_gap=True) -> numpy.ndarray:
+
+        for index, val in enumerate(self.degen_gapped_alphabet):
+            if include_gap and val in self.gap or val in self.ambiguities:
+                break
+        return seq >= index
+
+    @get_degenerate_positions.register
+    def _(self, seq: str, include_gap=True) -> numpy.ndarray:
+        return self.get_degenerate_positions(
+            self.degen_gapped_alphabet.to_indices(seq), include_gap
+        )
+
+    @get_degenerate_positions.register
+    def _(self, seq: bytes, include_gap=True) -> numpy.ndarray:
+        return self.get_degenerate_positions(
+            self.degen_gapped_alphabet.to_indices(seq), include_gap
+        )
+
     def get_css_style(
         self,
         colors: typing.Optional[dict[str, str]] = None,


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 1553762f4..384fc3721 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -126,7 +126,7 @@ class SequenceCollectionBaseTests(object):
 
     test_init_aln.__doc__ = Class.__name__ + test_init_aln.__doc__
 
-    def test_names_attribute(self):
+    def test_names_attribute(self):  # ported
         """expected to be a list"""
         seqs = self.Class({"a": b"AAAAA", "b": b"BBBBB"}, names=("a", "b"))
         self.assertIsInstance(seqs.names, list)
@@ -145,7 +145,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(a, d_upper)
         self.assertEqual(sorted(a.named_seqs.items()), sorted(d_upper.items()))
 
-    def test_init_seq(self):
+    def test_init_seq(self):  # ported
         """SequenceCollection init from list of sequences should use indices as keys"""
         seqs = ["AAAAA", "BBBBB", "CCCCC"]
         a = self.Class(seqs)
@@ -156,7 +156,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(a.names, ["seq_0", "seq_1", "seq_2"])
         self.assertEqual(list(a.seqs), ["AAAAA", "BBBBB", "CCCCC"])
 
-    def test_init_pairs(self):
+    def test_init_pairs(self):  # ported
         """SequenceCollection init from list of (key,val) pairs should work correctly"""
         seqs = [["x", "XXX"], ["b", "BBB"], ["c", "CCC"]]
         a = self.Class(seqs)
@@ -167,7 +167,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(a.names, ["x", "b", "c"])
         self.assertEqual(list(a.seqs), ["XXX", "BBB", "CCC"])
 
-    def test_init_ordered(self):
+    def test_init_ordered(self):  # ported
         """SequenceCollection should iterate over seqs correctly even if ordered"""
         first = self.ordered1
         sec = self.ordered2
@@ -188,14 +188,14 @@ class SequenceCollectionBaseTests(object):
         self.assertTrue((un_list == first_list) or (un_list == sec_list))
         self.assertNotEqual(first_list, sec_list)
 
-    def test_init_ambig(self):
+    def test_init_ambig(self):  # ported
         """SequenceCollection should tolerate ambiguous chars"""
         aln = self.Class(["AAA", "CCC"], moltype=DNA)
         aln = self.Class(["ANS", "CWC"], moltype=DNA)
         aln = self.Class(["A-A", "CC-"], moltype=DNA)
         aln = self.Class(["A?A", "CC-"], moltype=DNA)
 
-    def test_seq_len_get(self):
+    def test_seq_len_get(self):  # ported
         """SequenceCollection seq_len should return length of longest seq"""
         self.assertEqual(self.one_seq.seq_len, 5)
         self.assertEqual(self.identical.seq_len, 4)
@@ -230,7 +230,7 @@ class SequenceCollectionBaseTests(object):
         self.assertIs(seqs[1], seqs[2])
         self.assertIs(seqs[0], self.ragged_padded.named_seqs["b"])
 
-    def test_Items(self):
+    def test_Items(self):  # will not port
         """SequenceCollection iter_selected should iterate over items in specified order."""
         # should work if one row
         self.assertEqual(list(self.one_seq.iter_selected()), ["A"] * 5)
@@ -318,7 +318,7 @@ class SequenceCollectionBaseTests(object):
         # should be able to negate
         self.assertEqual(srp.take_seqs_if(is_med, negate=True), {"b": "AAA---"})
 
-    def test_get_identical_sets(self):
+    def test_get_identical_sets(self):  # ported
         """correctly identify sets of identical sequences"""
         from warnings import catch_warnings, filterwarnings
 
@@ -390,12 +390,12 @@ class SequenceCollectionBaseTests(object):
         got = frozenset(frozenset(s) for s in got)
         self.assertEqual(got, expect)
 
-    def test_is_ragged(self):
+    def test_is_ragged(self):  # ported
         """SequenceCollection is_ragged should return true if ragged alignment"""
         assert not self.identical.is_ragged()
         assert not self.gaps.is_ragged()
 
-    def test_to_phylip(self):
+    def test_to_phylip(self):  # ported
         """SequenceCollection should return PHYLIP string format correctly"""
         align_norm = self.Class(
             [
@@ -411,7 +411,7 @@ class SequenceCollectionBaseTests(object):
             """4  22\nseq_0     ACDEFGHIKLMNPQRSTUVWY-\nseq_1     ACDEFGHIKLMNPQRSUUVWF-\nseq_2     ACDEFGHIKLMNPERSKUVWC-\nseq_3     ACNEFGHIKLMNPQRS-UVWP-\n""",
         )
 
-    def test_to_fasta(self):
+    def test_to_fasta(self):  # ported
         """SequenceCollection should return correct FASTA string"""
         aln1 = self.Class(["AAA", "CCC"])
         self.assertEqual(aln1.to_fasta(), ">seq_0\nAAA\n>seq_1\nCCC\n")
@@ -426,7 +426,7 @@ class SequenceCollectionBaseTests(object):
             aln2.to_fasta(block_size=3), ">seq_0\nGCA\nTGC\nAT\n>seq_1\nTCA\nGAC\nGT\n"
         )
 
-    def test_to_nexus(self):
+    def test_to_nexus(self):  # will not port
         """SequenceCollection should return correct Nexus string format"""
         align_norm = self.Class(
             [
@@ -484,18 +484,18 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(result, ">a\nAAAA\n>b\nTTTT\n>c\nCCCC\n")
         remove(fn)
 
-    def test_len(self):
+    def test_len(self):  # ported
         """len(SequenceCollection) returns length of longest sequence"""
         aln = self.Class([("a", "AAAA"), ("b", "TTTT"), ("c", "CCCC")])
         self.assertEqual(len(aln), 4)
 
-    def test_get_seq(self):
+    def test_get_seq(self):  # ported
         """SequenceCollection.get_seq should return specified seq"""
         aln = self.Class({"seq1": "GATTTT", "seq2": "GATC??"})
         self.assertEqual(aln.get_seq("seq1"), "GATTTT")
         self.assertRaises(KeyError, aln.get_seq, "seqx")
 
-    def test_to_dict(self):
+    def test_to_dict(self):  # ported
         """SequenceCollection.to_dict should return dict of strings (not obj)"""
         aln = self.Class({"seq1": "GATTTT", "seq2": "GATC??"})
         self.assertEqual(aln.to_dict(), {"seq1": "GATTTT", "seq2": "GATC??"})
@@ -510,7 +510,7 @@ class SequenceCollectionBaseTests(object):
             {"s2": {4: "?", 5: "?"}, "s1": {3: "R", 4: "Y", 5: "?"}},
         )
 
-    def test_degap(self):
+    def test_degap(self):  # ported
         """SequenceCollection.degap should strip gaps from each seq"""
         aln = self.Class({"s1": "ATGRY?", "s2": "T-AG??"}, moltype=DNA)
         self.assertEqual(aln.degap(), {"s1": "ATGRY", "s2": "TAG"})
@@ -832,7 +832,7 @@ class SequenceCollectionTests(SequenceCollectionBaseTests, TestCase):
         self.ragged = SequenceCollection({"a": "AAAAAA", "b": "AAA", "c": "AAAA"})
         super(SequenceCollectionTests, self).setUp()
 
-    def test_seq_len_get_ragged(self):
+    def test_seq_len_get_ragged(self):  # ported
         """SequenceCollection seq_len get should work for ragged seqs"""
         self.assertEqual(self.ragged.seq_len, 6)
 
@@ -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):
+    def test_counts_per_seq_default_moltype(self):  # will not port
         """produce correct counts per seq with default moltypes"""
         data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
         coll = self.Class(data=data)
@@ -2074,7 +2074,7 @@ def test_to_dna_raises():
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_annotate_from_gff3(cls):
+def test_annotate_from_gff3(cls):  # ported for SequenceCollection
     """annotate_from_gff should work on data from gff3 files"""
     fasta_path = os.path.join("data/c_elegans_WS199_dna_shortened.fasta")
     gff3_path = os.path.join("data/c_elegans_WS199_shortened_gff.gff3")
@@ -2135,7 +2135,7 @@ def test_align_get_features():
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_init_annotated_seqs(cls):
+def test_init_annotated_seqs(cls):  # ported for SequenceCollection
     """correctly construct from list with annotated seq"""
     seq = make_seq("GCCAGGGGGGAAAG-GGAGAA", name="seq1")
     _ = seq.add_feature(biotype="exon", name="name", spans=[(4, 10)])
@@ -2145,7 +2145,7 @@ def test_init_annotated_seqs(cls):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_get_annotations_from_any_seq(cls):
+def test_get_annotations_from_any_seq(cls):  # ported for SequenceCollection
     """get_annotations_from_any_seq returns correct annotations"""
     data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
     seqs = cls(data, moltype=DNA)
@@ -2219,7 +2219,7 @@ def gff_db(DATA_DIR):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_copy_annotations(cls, gff_db):
+def test_copy_annotations(cls, gff_db):  # ported for SequenceCollection
     """copy_annotations copies records from annotation db"""
 
     seq_coll = cls({"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"})
@@ -2242,7 +2242,7 @@ def _make_seq(name):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_init_seqs_have_annotations(cls, gff_db):
+def test_init_seqs_have_annotations(cls, gff_db):  # ported for SequenceCollection
     """annotations on input seqs correctly merged and propagated"""
 
     seq_coll = cls({"seq1": _make_seq("seq1"), "seq2": _make_seq("seq2")})
@@ -2257,7 +2257,7 @@ def test_init_seqs_have_annotations(cls, gff_db):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_add_to_seq_updates_coll(cls, gff_db):
+def test_add_to_seq_updates_coll(cls, gff_db):  # ported for SequenceCollection
     """annotating a seq updates the db of the propagated"""
     seq_coll = cls(
         {"x": "AACCCAAAATTTTTTGGGGGGGGGGCCCC", "y": "AACCCAAAATTTTTTGGGGGGGGGGCCCC"}
@@ -2269,7 +2269,7 @@ def test_add_to_seq_updates_coll(cls, gff_db):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_assign_none(cls, gff_db):
+def test_assign_none(cls, gff_db):  # ported for SequenceCollection
     """assigning None to annotation_db breaks conection"""
 
     seq_coll = cls({"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"})
@@ -2279,7 +2279,7 @@ def test_assign_none(cls, gff_db):
     assert seq_coll.annotation_db is None
 
 
-def test_copy_annotations_incompat_fails(seqcoll_db, gb_db):
+def test_copy_annotations_incompat_fails(seqcoll_db, gb_db):  # ported
     """copy_annotations copies records from annotation db"""
     db = seqcoll_db.annotation_db
     seqcoll = seqcoll_db.rename_seqs(
@@ -2290,7 +2290,7 @@ def test_copy_annotations_incompat_fails(seqcoll_db, gb_db):
         seqcoll.copy_annotations(gb_db)
 
 
-def test_copy_annotations_incompat_type_fails(seqcoll_db, gb_db):
+def test_copy_annotations_incompat_type_fails(seqcoll_db, gb_db):  # ported
     """copy_annotations copies records from annotation db"""
     with pytest.raises(TypeError):
         seqcoll_db.copy_annotations({"a": "ACGGT"})
@@ -2656,7 +2656,7 @@ def test_get_translation_info(cls, seqs):
         (2, ("GATTTT", "TCCAGG")),
     ),
 )
-def test_has_terminal_stop_true(cls, gc, seqs):
+def test_has_terminal_stop_true(cls, gc, seqs):  # ported
     gc = get_code(gc)
     data = {f"s{i}": s for i, s in enumerate(seqs)}
     seqs = cls(data=data, moltype="dna")
@@ -2668,7 +2668,7 @@ def test_has_terminal_stop_true(cls, gc, seqs):
     "gc,seqs",
     ((1, ("TCCTCA", "GATTTT")), (2, ("GATTTT", "TCCCGG")), (1, ("CCTCA", "ATTTT"))),
 )
-def test_has_terminal_stop_false(cls, gc, seqs):
+def test_has_terminal_stop_false(cls, gc, seqs):  # ported
     gc = get_code(gc)
     data = {f"s{i}": s for i, s in enumerate(seqs)}
     seqs = cls(data=data, moltype="dna")
@@ -2972,7 +2972,7 @@ def test_filtered(cls):
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
-def test_counts_per_seq(cls):
+def test_counts_per_seq(cls):  # ported
     """SequenceCollection.counts_per_seq handles motif length, allow_gaps etc.."""
     data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
     coll = cls(data=data, moltype="dna")
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 97bf10aef..dbe620cc4 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -10,8 +10,7 @@ import cogent3.core.new_sequence as new_seq
 
 # todo: kath, update this to using new Sequence load_seq when it is ready
 from cogent3 import load_seq
-from cogent3._version import __version__
-from cogent3.core.annotation_db import load_annotations
+from cogent3.core.annotation_db import GffAnnotationDb, load_annotations
 
 
 @pytest.fixture
@@ -58,23 +57,42 @@ def sdv_s2(dna_sd: new_aln.SeqsData) -> new_aln.SeqDataView:
 @pytest.fixture
 def seqs() -> new_aln.SequenceCollection:
     data = {"seq1": "AAAAAA", "seq2": "TTTT", "seq3": "ATTCCCC"}
-    return new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    return new_aln.make_unaligned_seqs(data, moltype="dna")
 
 
 @pytest.fixture
 def ragged_padded():
     return new_aln.make_unaligned_seqs(
-        data={"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}, moltype="dna"
+        {"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}, moltype="dna"
     )
 
 
 @pytest.fixture
 def ragged():
     return new_aln.make_unaligned_seqs(
-        data={"a": "AAAAAA", "b": "AAA", "c": "AAAA"}, moltype="dna"
+        {"a": "AAAAAA", "b": "AAA", "c": "AAAA"}, moltype="dna"
     )
 
 
+@pytest.fixture
+def unordered():
+    return new_aln.make_unaligned_seqs({"a": "AAAAA", "c": "CCCCC"}, moltype="dna")
+
+
+@pytest.fixture
+def ordered1():
+    seqs = new_aln.make_unaligned_seqs({"a": "AAAAA", "c": "CCCCC"}, moltype="dna")
+    seqs.names = ["a", "c"]
+    return seqs
+
+
+@pytest.fixture
+def ordered2():
+    seqs = new_aln.make_unaligned_seqs({"a": "AAAAA", "c": "CCCCC"}, moltype="dna")
+    seqs.names = ["c", "a"]
+    return seqs
+
+
 @pytest.fixture
 def aligned_dict():
     return dict(seq1="ACG--T", seq2="-CGAAT")
@@ -109,7 +127,7 @@ def seqcoll_db():
     fasta_path = os.path.join("data/c_elegans_WS199_dna_shortened.fasta")
     gff3_path = os.path.join("data/c_elegans_WS199_shortened_gff.gff3")
     seq = load_seq(fasta_path, moltype="dna")
-    seq_coll = new_aln.make_unaligned_seqs(data={seq.name: seq}, moltype="dna")
+    seq_coll = new_aln.make_unaligned_seqs({seq.name: seq}, moltype="dna")
     seq_coll.annotate_from_gff(gff3_path)
     return seq_coll
 
@@ -206,6 +224,18 @@ def test_seqs_data_names(str_seqs_dict, alpha):
     assert list(got) == list(expect)
 
 
+def test_seqs_data_seq_lengths(str_seqs_dict, arr_seqs_dict, alpha):
+    expect = {k: len(v) for k, v in str_seqs_dict.items()}
+    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    got = sd.seq_lengths()
+    assert got == expect
+
+    expect = {k: len(v) for k, v in arr_seqs_dict.items()}
+    sd = new_aln.SeqsData(data=arr_seqs_dict, alphabet=alpha)
+    got = sd.seq_lengths()
+    assert got == expect
+
+
 def test_seqs_data_get_seq_array(str_seqs_dict, alpha):
     # todo: kath, edit and parametrize for all moltypes
     # seq1
@@ -283,41 +313,23 @@ def tests_seqs_data_subset(dna_sd):
     assert got.get_seq_str(seqid="seq2") == dna_sd.get_seq_str(seqid="seq2")
 
     got = dna_sd.subset(["seq2", "seq1"])
-    assert isinstance(got, new_aln.SeqsData)
     assert got.names == ["seq2", "seq1"]
     assert got.get_seq_str(seqid="seq1") == dna_sd.get_seq_str(seqid="seq1")
     assert got.get_seq_str(seqid="seq2") == dna_sd.get_seq_str(seqid="seq2")
 
     got = dna_sd.subset(["seq3"])
-    assert isinstance(got, new_aln.SeqsData)
     assert got.names == ["seq3"]
     assert got.get_seq_str(seqid="seq3") == dna_sd.get_seq_str(seqid="seq3")
 
 
-@pytest.mark.xfail(
-    reason="todo: kath, decide how to support equality testing between SeqsData"
-)
-def test_seqs_data_eq(alpha):
-    data = dict(seq1="ACGT", seq2="GTTTGCA", seq3="ACGTACGT")
-    sd_1 = new_aln.SeqsData(data=data, alphabet=alpha)
-    sd_2 = new_aln.SeqsData(data=data, alphabet=alpha)
-
-    assert sd_1 == sd_2
-    assert sd_1 == data
-
-
-@pytest.mark.xfail(
-    reason="todo: kath, decide how to support equality testing between SeqsData"
-)
-def test_seqs_data_eq_diff_alphabets():
-    rna_data = dict(seq1="ACGU", seq2="GUUUGCA", seq3="ACGUACGU")
-    dna_data = dict(seq1="ACGT", seq2="GTTTGCA", seq3="ACGTACGT")
-
-    dna_alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
-    dna_sd = new_aln.SeqsData(data=dna_data, alphabet=dna_alpha)
+def test_seqs_data_subset_raises(dna_sd):
+    # should handle missing seqids
+    got = dna_sd.subset(["seq1", "seq4"])
+    assert got.names == ["seq1"]
 
-    assert dna_data == dna_sd
-    assert dna_sd == rna_data
+    # but will raise error if resulting in empty selection
+    with pytest.raises(ValueError):
+        _ = dna_sd.subset(["seq4"])
 
 
 @pytest.mark.parametrize(
@@ -488,13 +500,13 @@ def test_seq_to_gap_coords_arr(gap_seqs, i):
 def test_make_unaligned_seqs_dict(moltype):
     """test SequenceCollection constructor utility function"""
     data = {"a": "AGGCCC", "b": "AGAAAA"}
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    got = new_aln.make_unaligned_seqs(data, moltype=moltype)
     assert isinstance(got, new_aln.SequenceCollection)
     assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
 
     # should also work if seqs are arrays
     data = {"a": numpy.array(["AGGCCC"]), "b": numpy.array(["AGAAAA"])}
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    got = new_aln.make_unaligned_seqs(data, moltype=moltype)
     assert isinstance(got, new_aln.SequenceCollection)
     assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
 
@@ -503,13 +515,13 @@ def test_make_unaligned_seqs_dict(moltype):
 def test_make_unaligned_seqs_list(moltype):
     """test SequenceCollection constructor utility function"""
     data = ["AGGCCC", "AGAAAA"]
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    got = new_aln.make_unaligned_seqs(data, moltype=moltype)
     assert isinstance(got, new_aln.SequenceCollection)
     assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
 
     # should also work if seqs are arrays
     data = [numpy.array(["AGGCCC"]), numpy.array(["AGAAAA"])]
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
+    got = new_aln.make_unaligned_seqs(data, moltype=moltype)
     assert isinstance(got, new_aln.SequenceCollection)
     assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
 
@@ -521,7 +533,7 @@ def test_make_unaligned_seqs_label_to_name():
     def f(x):
         return x.upper()
 
-    got = new_aln.make_unaligned_seqs(data=data, moltype="dna", label_to_name=f)
+    got = new_aln.make_unaligned_seqs(data, moltype="dna", label_to_name=f)
     assert list(got.names) == ["A", "B"]
     assert got._seqs_data.make_seq == new_moltype.get_moltype("dna").make_seq
 
@@ -529,18 +541,18 @@ def test_make_unaligned_seqs_label_to_name():
 def test_make_unaligned_seqs_raises():
     data = "AGTCCTGA"
     with pytest.raises(NotImplementedError):
-        new_aln.make_unaligned_seqs(data=data, moltype="dna")
+        new_aln.make_unaligned_seqs(data, moltype="dna")
 
 
 def test_make_unaligned_seqs_incompatible_moltype(dna_sd):
     with pytest.raises(ValueError):
-        _ = new_aln.make_unaligned_seqs(data=dna_sd, moltype="rna")
+        _ = new_aln.make_unaligned_seqs(dna_sd, moltype="rna")
 
 
 def test_make_unaligned_seqs_no_seqs():
     data = {}
     with pytest.raises(ValueError):
-        new_aln.make_unaligned_seqs(data=data, moltype="dna")
+        new_aln.make_unaligned_seqs(data, moltype="dna")
 
 
 def test_sequence_collection_init(seqs):
@@ -548,6 +560,12 @@ def test_sequence_collection_init(seqs):
     assert seqs.seqs.make_seq is not None
 
 
+def test_sequence_collection_names_is_list():
+    """expected to be a list"""
+    seqs = new_aln.make_unaligned_seqs({"a": b"AAAAA", "b": b"TTTTT"}, moltype="dna")
+    assert isinstance(seqs.names, list)
+
+
 def test_sequence_collection_names(seqs):
     assert seqs.names == ["seq1", "seq2", "seq3"]
     seqs.names = ["seq2", "seq3", "seq1"]
@@ -558,6 +576,48 @@ def test_sequence_collection_names(seqs):
         seqs.names = ["seq1", "seq2", "seq3", "seq4"]
 
 
+def test_init_seq():
+    """SequenceCollection init from list of sequences should use indices as keys"""
+    seqs = ["TTTTT", "CCCCC", "GGGGG"]
+    a = new_aln.make_unaligned_seqs(seqs, moltype="dna")
+    assert len(a.seqs) == 3
+    assert a.seqs["seq_0"] == "TTTTT"
+    assert a.seqs["seq_1"] == "CCCCC"
+    assert a.seqs["seq_2"] == "GGGGG"
+    assert a.names == ["seq_0", "seq_1", "seq_2"]
+
+
+@pytest.mark.xfail(
+    reason="todo: decide if we support init from list of (key,val) pairs"
+)
+def test_init_pairs():
+    """SequenceCollection init from list of (key,val) pairs should work correctly"""
+    seqs = [["a", "AAA"], ["t", "TTT"], ["c", "CCC"]]
+    a = new_aln.make_unaligned_seqs(seqs, moltype="dna")
+    assert len(a.seqs) == 3
+    assert a.seqs["a"] == "AAA"
+    assert a.seqs["t"] == "TTT"
+    assert a.seqs["c"] == "CCC"
+    assert a.names == ["a", "t", "c"]
+
+
+def test_init_ordered(ordered1, ordered2):
+    """SequenceCollection should iterate over seqs correctly even if ordered"""
+    first = ordered1
+    sec = ordered2
+
+    assert first.names == ["a", "c"]
+    assert sec.names == ["c", "a"]
+
+
+def test_sequence_collection_init_ambig():
+    """SequenceCollection should tolerate ambiguous chars"""
+    _ = new_aln.make_unaligned_seqs(["AAA", "CCC"], moltype="dna")
+    _ = new_aln.make_unaligned_seqs(["ANS", "CWC"], moltype="dna")
+    _ = new_aln.make_unaligned_seqs(["A-A", "CC-"], moltype="dna")
+    _ = new_aln.make_unaligned_seqs(["A?A", "CC-"], moltype="dna")
+
+
 def test_iter_seqs_ragged_padded(ragged_padded):
     """SequenceCollection.iter_seqs() method should support reordering of seqs"""
     seqs = list(ragged_padded.iter_seqs())
@@ -583,7 +643,7 @@ def test_sequence_collection_repr():
         "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
     }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     assert (
         repr(seqs)
         == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
@@ -593,7 +653,7 @@ def test_sequence_collection_repr():
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
         "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
     }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     assert (
         repr(seqs)
         == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
@@ -602,20 +662,20 @@ def test_sequence_collection_repr():
     data = {
         "a": "TCGAT",
     }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     assert repr(seqs) == "1x (a[TCGAT]) dna seqcollection"
 
     data = {
         "a": "TCGAT" * 2,
     }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     assert repr(seqs) == "1x (a[TCGATTCGAT]) dna seqcollection"
 
 
 def test_sequence_collection_set_wrap_affects_repr_html():
     """the wrap argument affects the number of columns"""
     # indirectly tested via counting number of occurrences of 'class="label"'
-    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs({"a": "AAAAA", "b": "AAA--"}, moltype="dna")
     orig = seqs._repr_html_()
     seqs.set_repr_policy(wrap=3)  # break alignment into 2
     got = seqs._repr_html_()
@@ -625,7 +685,7 @@ def test_sequence_collection_set_wrap_affects_repr_html():
     # using environment variable
     env_name = "COGENT3_ALIGNMENT_REPR_POLICY"
     os.environ[env_name] = "wrap=2"
-    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
+    seqs = new_aln.make_unaligned_seqs({"a": "AAAAA", "b": "AAA--"}, moltype="dna")
     got = seqs._repr_html_()
     os.environ.pop(env_name, None)
     assert got.count(token) == 3 * orig.count(token)
@@ -688,34 +748,38 @@ def test_sequence_collection_set_repr_policy_valid_input(seqs):
     assert seqs._repr_policy == dict(num_seqs=5, num_pos=40, ref_name="seq1", wrap=10)
 
 
-@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
 def test_sequence_collection_take_seqs(ragged_padded):
     """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
     a = ragged_padded.take_seqs(list("bc"))
     assert isinstance(a, new_aln.SequenceCollection)
-    assert a == {"b": "AAA---", "c": "AAAA--"}
+    assert a.names == ["b", "c"]
+    assert str(a.seqs["b"]) == "AAA---"
+    assert str(a.seqs["c"]) == "AAAA--"
+    assert a.num_seqs == 2
     # should be able to negate
     a = ragged_padded.take_seqs(list("bc"), negate=True)
-    assert a == {"a": "AAAAAA"}
+    assert a.names == ["a"]
+    assert str(a.seqs["a"]) == "AAAAAA"
 
 
-@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
 def test_sequence_collection_take_seqs_str(ragged_padded):
     """string arg to SequenceCollection take_seqs should work."""
     a = ragged_padded.take_seqs("a")
-    assert a == {"a": "AAAAAA"}
+    assert a.names == ["a"]
+    assert a.num_seqs == 1
 
     # should be able to negate
     a = ragged_padded.take_seqs("a", negate=True)
     assert isinstance(a, new_aln.SequenceCollection)
-    assert a == {"b": "AAA---", "c": "AAAA--"}
+    assert a.names == ["b", "c"]
+    assert a.num_seqs == 2
 
 
 def test_sequence_collection_take_seqs_info():
     """take_seqs should preserve info attribute"""
     moltype = new_moltype.get_moltype("dna")
     orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"},
+        {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"},
         moltype=moltype,
         info={"key": "value"},
     )
@@ -727,7 +791,7 @@ def test_sequence_collection_take_seqs_moltype():
     """take_seqs should preserve the MolType"""
     moltype = new_moltype.get_moltype("dna")
     orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
+        {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
     )
     subset = orig.take_seqs(list("ab"))
     assert set(subset.moltype) == set(orig.moltype)
@@ -736,7 +800,7 @@ def test_sequence_collection_take_seqs_moltype():
 def test_sequence_collection_take_seqs_empty_names():
     moltype = new_moltype.get_moltype("dna")
     orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
+        {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
     )
     with pytest.raises(ValueError):
         _ = orig.take_seqs([])
@@ -746,7 +810,7 @@ def test_sequence_collection_num_seqs():
     """SequenceCollection.num_seqs should count seqs."""
 
     seqs = new_aln.make_unaligned_seqs(
-        data={"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"}, moltype="rna"
+        {"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"}, moltype="rna"
     )
     assert seqs.num_seqs == 3
 
@@ -787,13 +851,264 @@ def test_get_seq_names_if(ragged_padded):
     assert ragged_padded.get_seq_names_if(is_any, negate=True) == []
 
 
-@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
 def test_take_seqs_if(ragged_padded):
     """SequenceCollection take_seqs_if should return seqs where f(seq) is True"""
 
     with pytest.raises(ValueError):
         ragged_padded.take_seqs_if(is_long)
-    assert ragged_padded.take_seqs_if(is_any) == ragged_padded
-    assert isinstance(ragged_padded.take_seqs_if(is_med), new_aln.SequenceCollection)
     with pytest.raises(ValueError):
-        ragged_padded.take_seqs_if(is_med, negate=True)
+        ragged_padded.take_seqs_if(is_any, negate=True)
+    got = ragged_padded.take_seqs_if(is_any)
+    assert got.names == ragged_padded.names
+    assert got.num_seqs == ragged_padded.num_seqs
+    assert isinstance(ragged_padded.take_seqs_if(is_med), new_aln.SequenceCollection)
+
+    got = ragged_padded.take_seqs_if(is_med, negate=True)
+    assert got.names == ["b"]
+    assert got.num_seqs == 1
+
+
+def test_sequence_collection_to_dict():
+    """SequenceCollection.to_dict should return dict of strings (not obj)"""
+    data = {"seq1": "GATTTT", "seq2": "GATC??"}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
+    assert seqs.to_dict() == data
+    for i in list(seqs.to_dict().values()):
+        assert isinstance(i, str)
+
+
+def test_sequence_collection_get_seq():
+    """SequenceCollection.get_seq should return specified seq"""
+    seqs = new_aln.make_unaligned_seqs(
+        {"seq1": "GATTTT", "seq2": "GATC??"}, moltype="dna"
+    )
+    assert seqs.get_seq("seq1") == "GATTTT"
+    with pytest.raises(KeyError):
+        seqs.get_seq("seqx")
+
+
+@pytest.mark.xfail(reason="AttributeError: 'MolType' object has no attribute 'degap'")
+def test_sequence_collection_degap():
+    """SequenceCollection.degap should strip gaps from each seq"""
+    seqs = new_aln.make_unaligned_seqs({"s1": "ATGRY?", "s2": "T-AG??"}, moltype="dna")
+    assert seqs.degap().to_dict() == {"s1": "ATGRY", "s2": "TAG"}
+
+
+def test_sequence_collection_to_fasta():
+    """SequenceCollection should return correct FASTA string"""
+    seqs = new_aln.make_unaligned_seqs(["AAA", "CCC"], moltype="dna")
+    assert seqs.to_fasta() == ">seq_0\nAAA\n>seq_1\nCCC\n"
+    assert seqs.to_fasta(block_size=2) == ">seq_0\nAA\nA\n>seq_1\nCC\nC\n"
+
+    seqs = new_aln.make_unaligned_seqs(["GCATGCAT", "TCAGACGT"], moltype="dna")
+    assert seqs.to_fasta() == ">seq_0\nGCATGCAT\n>seq_1\nTCAGACGT\n"
+    assert seqs.to_fasta(block_size=4) == ">seq_0\nGCAT\nGCAT\n>seq_1\nTCAG\nACGT\n"
+    assert seqs.to_fasta(block_size=3) == ">seq_0\nGCA\nTGC\nAT\n>seq_1\nTCA\nGAC\nGT\n"
+
+
+def test_sequence_collection_to_phylip():
+    """SequenceCollection should return PHYLIP string format correctly"""
+    align_norm = new_aln.make_unaligned_seqs(
+        [
+            "ACDEFGHIKLMNPQRSTUVWY-",
+            "ACDEFGHIKLMNPQRSUUVWF-",
+            "ACDEFGHIKLMNPERSKUVWC-",
+            "ACNEFGHIKLMNPQRS-UVWP-",
+        ],
+        moltype="protein",
+    )
+
+    assert (
+        align_norm.to_phylip()
+        == """4  22\nseq_0     ACDEFGHIKLMNPQRSTUVWY-\nseq_1     ACDEFGHIKLMNPQRSUUVWF-\nseq_2     ACDEFGHIKLMNPERSKUVWC-\nseq_3     ACNEFGHIKLMNPQRS-UVWP-\n"""
+    )
+
+
+@pytest.mark.parametrize(
+    "gc,seqs",
+    (
+        (1, ("TCCTGA", "GATTT?")),
+        (1, ("ACGTAA---", "ACGAC----", "ACGCAATGA")),
+        (2, ("GATTTT", "TCCAGG")),
+    ),
+)
+def test_has_terminal_stop_true(gc, seqs):
+    data = {f"s{i}": s for i, s in enumerate(seqs)}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
+    assert seqs.has_terminal_stop(gc=gc)
+
+
+@pytest.mark.parametrize(
+    "gc,seqs",
+    ((1, ("TCCTCA", "GATTTT")), (2, ("GATTTT", "TCCCGG")), (1, ("CCTCA", "ATTTT"))),
+)
+def test_has_terminal_stop_false(gc, seqs):
+    data = {f"s{i}": s for i, s in enumerate(seqs)}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
+    assert not seqs.has_terminal_stop(gc=gc)
+
+
+def test_get_similar():
+    data = {
+        "a": "AAAAAAAAAA",  # target
+        "b": "AAAAAAAAAA",  # identical
+        "c": "AAAAAAAAAT",  # 90% identical
+        "d": "AAAAAAAATT",  # 80% identical
+        "e": "AATTTTTTTT",  # 20% identical
+        "f": "TTTTTTTTTT",  # 0% identical
+    }
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
+    target = seqs.get_seq("a")
+    got = seqs.get_similar(target, min_similarity=0.8)
+    assert got.names == ["a", "b", "c", "d"]
+    got = seqs.get_similar(target, min_similarity=0.81)
+    assert got.names == ["a", "b", "c"]
+    got = seqs.get_similar(target, min_similarity=0.75, max_similarity=0.9)
+    assert got.names == ["c", "d"]
+    got = seqs.get_similar(target, min_similarity=0.75, max_similarity=0.89)
+    assert got.names == ["d"]
+
+
+def test_sequence_collection_init_annotated_seqs():
+    """correctly construct from list with annotated seq"""
+    seq = new_moltype.DNA.make_seq(seq="GCCAGGGGGGAAAG-GGAGAA", name="seq1")
+    _ = seq.add_feature(biotype="exon", name="name", spans=[(4, 10)])
+    coll = new_aln.make_unaligned_seqs([seq], moltype="dna")
+    features = list(coll.get_features(biotype="exon"))
+    assert len(features) == 1
+
+
+def test_sequence_collection_make_unaligned_seqs_annotated():
+    """annotate_from_gff should work on data from gff3 files"""
+    fasta_path = os.path.join("data/c_elegans_WS199_dna_shortened.fasta")
+    gff3_path = os.path.join("data/c_elegans_WS199_shortened_gff.gff3")
+    seq = load_seq(fasta_path, moltype="dna")
+    seq = new_moltype.DNA.make_seq(seq=str(seq), name="seq1")
+    seq.annotate_from_gff(gff3_path)
+
+
+def test_sequence_collection_annotation_db_assign_none():
+    """assigning None to annotation_db breaks conection"""
+    seq_coll = new_aln.make_unaligned_seqs(
+        {"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"}, moltype="rna"
+    )
+    seq_coll.add_feature(seqid="seq1", biotype="xyz", name="abc", spans=[(1, 2)])
+    seq_coll.add_feature(seqid="seq2", biotype="xyzzz", name="abc", spans=[(1, 2)])
+    seq_coll.annotation_db = None
+    assert seq_coll.annotation_db is None
+
+
+def test_sequence_collection_get_annotations_from_any_seq():
+    """get_annotations_from_any_seq returns correct annotations"""
+    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
+    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
+    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
+    got = list(seqs.get_features())
+    assert len(got) == 3
+    assert "biotype='exon', name='annotation1', map=[3:8]/9" in str(got[0])
+    assert "biotype='exon', name='annotation2', map=[1:2]/9" in str(got[1])
+    assert "biotype='exon', name='annotation3', map=[3:6]/9" in str(got[2])
+
+    got = list(seqs.get_features(name="annotation1"))
+    assert len(got) == 1
+    assert "biotype='exon', name='annotation1', map=[3:8]/9" in str(got[0])
+
+    got = list(seqs.get_features(biotype="exon", name="annotation2"))
+    assert len(got) == 1
+    assert "biotype='exon', name='annotation2', map=[1:2]/9" in str(got[0])
+
+    got = list(seqs.get_features(name="annotation3"))
+    assert len(got) == 1
+    assert "biotype='exon', name='annotation3', map=[3:6]/9" in str(got[0])
+
+
+def test_sequence_collection_copy_annotations(gff_db):
+    """copy_annotations copies records from annotation db"""
+    seq_coll = new_aln.make_unaligned_seqs(
+        {"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"}, moltype="rna"
+    )
+    seq_coll.add_feature(seqid="seq1", biotype="xyz", name="abc", spans=[(1, 2)])
+    seq_coll.add_feature(seqid="seq2", biotype="xyzzz", name="abc", spans=[(1, 2)])
+    expect = seq_coll.annotation_db.num_matches() + gff_db.num_matches()
+    seq_coll.copy_annotations(gff_db)
+    assert seq_coll.annotation_db.num_matches() == expect
+
+
+def test_sequence_collection_get_seq_annotated():
+    """SequenceCollection.get_seq should return specified seq"""
+    seqs = new_aln.make_unaligned_seqs(
+        {"seq1": "GATTTT", "seq2": "GATC??"}, moltype="dna"
+    )
+    seqs.add_feature(seqid="seq1", biotype="xyz", 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
+
+
+def _make_seq(name):
+    raw_seq = "AACCCAAAATTTTTTGGGGGGGGGGCCCC"
+    cds = (15, 25)
+    utr = (12, 15)
+    seq = new_moltype.DNA.make_seq(seq=raw_seq, name=name)
+    seq.add_feature(biotype="CDS", name="CDS", spans=[cds])
+    seq.add_feature(biotype="5'UTR", name="5' UTR", spans=[utr])
+    return seq
+
+
+@pytest.mark.xfail(
+    reason="construction of Sequences from SequenceCollection does not propagate \
+    annotations to get a sequence with annotations, use get_seq with copy_annotations=True"
+)
+def test_sequence_collection_init_seqs_have_annotations():
+    """annotations on input seqs correctly merged and propagated"""
+
+    seq_coll = new_aln.make_unaligned_seqs(
+        {"seq1": _make_seq("seq1"), "seq2": _make_seq("seq2")}, moltype="dna"
+    )
+    coll_db = seq_coll.annotation_db
+    assert len(coll_db) == 4
+    for seq in seq_coll.seqs:
+        db = seq.annotation_db
+        assert db is coll_db
+
+
+@pytest.mark.xfail(
+    reason="annotating sequences does not propagate annotations to the SequenceCollection"
+)
+def test_add_to_seq_updates_coll():
+    """annotating a seq updates the db of the propagated"""
+    seq_coll = new_aln.make_unaligned_seqs(
+        {
+            "x": "AACCCAAAATTTTTTGGGGGGGGGGCCCC",
+            "y": "AACCCAAAATTTTTTGGGGGGGGGGCCCC",
+        },
+        moltype="dna",
+    )
+    x = seq_coll.get_seq("x")
+    assert len(seq_coll.annotation_db) == len(x.annotation_db) == 0
+    x.add_feature(biotype="exon", name="E1", spans=[(3, 8)])
+    assert len(seq_coll.annotation_db) == len(x.annotation_db) == 1
+
+
+@pytest.mark.xfail(reason="load_seq creating old-style Sequence")
+def test_copy_annotations_incompat_fails(seqcoll_db, gb_db):
+    """copy_annotations copies records from annotation db"""
+    db = seqcoll_db.annotation_db
+    seqcoll_db.annotation_db = db
+    with pytest.raises(TypeError):
+        seqcoll_db.copy_annotations(gb_db)
+
+
+@pytest.mark.xfail(reason="load_seq creating old-style Sequence")
+def test_copy_annotations_incompat_type_fails(seqcoll_db, seqs):
+    """copy_annotations copies records from annotation db"""
+
+    with pytest.raises(TypeError):
+        seqcoll_db.copy_annotations(seqs)
diff --git a/tests/test_core/test_new_moltype.py b/tests/test_core/test_new_moltype.py
index 368c1d05b..bbe7d6f64 100644
--- a/tests/test_core/test_new_moltype.py
+++ b/tests/test_core/test_new_moltype.py
@@ -160,3 +160,35 @@ def test_not_is_gapped(seq, data_type):
 def test_gap_index_constant(moltype):
     # make sure gap index is always the same
     assert moltype.gapped_alphabet.gap_index == moltype.degen_gapped_alphabet.gap_index
+
+
+@pytest.mark.parametrize("data_type", (str, bytes, numpy.ndarray))
+@pytest.mark.parametrize("moltype", (new_moltype.DNA, new_moltype.RNA))
+def test_get_degenerate_positions(data_type, moltype):
+    seq = make_typed("ASA", data_type, moltype)
+    got = moltype.get_degenerate_positions(seq)
+    expect = numpy.array([False, True, False], dtype=bool)
+    assert numpy.array_equal(got, expect)
+
+    seq = make_typed("A-SA", data_type, moltype)
+    got = moltype.get_degenerate_positions(seq)
+    expect = numpy.array([False, True, True, False], dtype=bool)
+    assert numpy.array_equal(got, expect)
+
+    got = moltype.get_degenerate_positions(seq, include_gap=False)
+    expect = numpy.array([False, False, True, False], dtype=bool)
+    assert numpy.array_equal(got, expect)
+
+    seq = make_typed("BAB", data_type, moltype)
+    got = moltype.get_degenerate_positions(seq)
+    expect = numpy.array([True, False, True], dtype=bool)
+    assert numpy.array_equal(got, expect)
+
+    seq = make_typed("---", data_type, moltype)
+    got = moltype.get_degenerate_positions(seq)
+    expect = numpy.array([True, True, True], dtype=bool)
+
+    seq = make_typed("", data_type, moltype)
+    got = moltype.get_degenerate_positions(seq)
+    expect = numpy.array([], dtype=bool)
+    assert numpy.array_equal(got, expect)