Repository: UC-Davis-molecular-computing/nuad (language: python)

Issue:
fix Domain.name docstring
In messing with writing a design script, I noticed that the `Domain.name` docstring wasn't right...


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 715b9467..da8b2d46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 .idea
 
+output
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
diff --git a/README.md b/README.md
index f79cdf53..ac915433 100644
--- a/README.md
+++ b/README.md
@@ -146,7 +146,7 @@ In more detail, there are five main types of objects you create to describe your
         
             - `DomainConstraint`: This only looks at a single `Domain`. In practice this is not used much, since there's not much information in a `Domain` other than its DNA sequence, so a `SequenceConstraint` or `NumpyConstraint` typically would already have filtered out any DNA sequence not satisfying such a constraint.
 
-            - `StrandConstraint`: This evaluates a whole `Strand`. A common example is that NUPACK's `pfunc` should indicate a complex free energy above a certain threshold, indicating the `Strand` has little secondary structure. This example constraint is available in the library by calling [nupack_strand_complex_free_energy_constraint](https://nuad.readthedocs.io/en/latest/#constraints.nupack_strand_complex_free_energy_constraint).
+            - `StrandConstraint`: This evaluates a whole `Strand`. A common example is that NUPACK's `pfunc` should indicate a complex free energy above a certain threshold, indicating the `Strand` has little secondary structure. This example constraint is available in the library by calling [nupack_strand_free_energy_constraint](https://nuad.readthedocs.io/en/latest/#constraints.nupack_strand_free_energy_constraint).
 
             - `DomainPairConstraint`: This evaluates a pair of `Domain`'s.
 
diff --git a/examples/sst_canvas.py b/examples/sst_canvas.py
index d94fc877..08e6fcd2 100644
--- a/examples/sst_canvas.py
+++ b/examples/sst_canvas.py
@@ -1,4 +1,3 @@
-import itertools
 from dataclasses import dataclass
 from typing import Optional
 import argparse
@@ -16,16 +15,19 @@ def main() -> None:
     args: CLArgs = parse_command_line_arguments()
 
     design = create_design(width=args.width, height=args.height)
-    thresholds = Thresholds()
-    constraints = create_constraints(design, thresholds)
+
+    constraints = create_constraints(design)
+
     params = ns.SearchParameters(
         constraints=constraints,
         out_directory=args.directory,
         restart=args.restart,
         random_seed=args.seed,
-        log_time=True,
+        scrolling_output=False,
+        save_report_for_all_updates=True,
+        force_overwrite=args.force_overwrite,
+        # log_time=True,
     )
-
     ns.search_for_sequences(design, params)
 
 
@@ -47,6 +49,9 @@ class CLArgs:
     seed: Optional[int] = None
     """seed for random number generator; set to fixed integer for reproducibility"""
 
+    force_overwrite: bool = False
+    """whether to overwrite output files without prompting the user"""
+
 
 def parse_command_line_arguments() -> CLArgs:
     default_directory = os.path.join('output', ns.script_name_no_ext())
@@ -74,13 +79,19 @@ def parse_command_line_arguments() -> CLArgs:
                              'numbering from there (i.e., the next files to be written upon improving the '
                              'design will have index 85).')
 
+    parser.add_argument('-f', '--force', action='store_true',
+                        help='If true, then overwrites the output files without prompting the user.')
+
     args = parser.parse_args()
 
-    return CLArgs(directory=args.output_dir,
-                  width=args.width,
-                  height=args.height,
-                  seed=args.seed,
-                  restart=args.restart)
+    return CLArgs(
+        directory=args.output_dir,
+        width=args.width,
+        height=args.height,
+        seed=args.seed,
+        restart=args.restart,
+        force_overwrite=args.force,
+    )
 
 
 def create_design(width: int, height: int) -> nc.Design:
@@ -205,54 +216,20 @@ class Thresholds:
     """RNAduplex complex free energy threshold for pairs tiles with 1 complementary domain."""
 
 
-def create_constraints(design: nc.Design, thresholds: Thresholds) -> List[nc.Constraint]:
+def create_constraints(design: nc.Design) -> List[nc.Constraint]:
+    thresholds = Thresholds()
+
     strand_individual_ss_constraint = nc.nupack_strand_free_energy_constraint(
         threshold=thresholds.tile_ss, temperature=thresholds.temperature, short_description='StrandSS')
 
-    # This reduces the number of times we have to create these sets from quadratic to linear
-    unstarred_domains_sets = {}
-    starred_domains_sets = {}
-    for strand in design.strands:
-        unstarred_domains_sets[strand.name] = strand.unstarred_domains_set()
-        starred_domains_sets[strand.name] = strand.starred_domains_set()
-
-    # determine which pairs of strands have 0 complementary domains and which have 1
-    # so we can set different RNAduplex energy constraints for each of them
-    strand_pairs_0_comp = []
-    strand_pairs_1_comp = []
-    for strand1, strand2 in itertools.combinations_with_replacement(design.strands, 2):
-        domains1_unstarred = unstarred_domains_sets[strand1.name]
-        domains2_unstarred = unstarred_domains_sets[strand2.name]
-        domains1_starred = starred_domains_sets[strand1.name]
-        domains2_starred = starred_domains_sets[strand2.name]
-
-        complementary_domains = (domains1_unstarred & domains2_starred) | \
-                                (domains2_unstarred & domains1_starred)
-        complementary_domain_names = [domain.name for domain in complementary_domains]
-        num_complementary_domains = len(complementary_domain_names)
-
-        if num_complementary_domains == 0:
-            strand_pairs_0_comp.append((strand1, strand2))
-        elif num_complementary_domains == 1:
-            strand_pairs_1_comp.append((strand1, strand2))
-        else:
-            raise AssertionError('each pair of strands should have exactly 0 or 1 complementary domains')
-
-    strand_pairs_rna_duplex_constraint_0comp = nc.rna_duplex_strand_pairs_constraint(
-        threshold=thresholds.tile_pair_0comp, temperature=thresholds.temperature,
-        short_description='StrandPairRNA0Comp', pairs=strand_pairs_0_comp)
-    strand_pairs_rna_duplex_constraint_1comp = nc.rna_duplex_strand_pairs_constraint(
-        threshold=thresholds.tile_pair_1comp, temperature=thresholds.temperature,
-        short_description='StrandPairRNA1Comp', pairs=strand_pairs_1_comp)
+    strand_pairs_rna_duplex_constraint_0comp, strand_pairs_rna_duplex_constraint_1comp = \
+        nc.rna_duplex_strand_pairs_constraints_by_number_matching_domains(
+            thresholds={0: thresholds.tile_pair_0comp, 1: thresholds.tile_pair_1comp},
+            temperature=thresholds.temperature,
+            short_descriptions={0: 'StrandPairRNA0Comp', 1: 'StrandPairRNA1Comp'},
+            strands=design.strands,
+        )
 
-    # We already forbid GGGG in any domain, but let's also ensure we don't get GGGG in any strand
-    # i.e., forbid GGGG that comes from concatenating domains, e.g.,
-    #
-    #               *  ***
-    #      ACGATCGATG  GGGATGCATGA
-    #     +==========--===========>
-    #     |
-    #     +==========--===========]
     no_gggg_constraint = create_tile_no_gggg_constraint(weight=100)
 
     return [
@@ -268,6 +245,15 @@ def create_tile_no_gggg_constraint(weight: float) -> nc.StrandConstraint:
     # sufficient. See also source code of provided constraints in dsd/constraints.py for more examples,
     # particularly for examples that call NUPACK or ViennaRNA.
 
+    # We already forbid GGGG in any domain, but let's also ensure we don't get GGGG in any strand
+    # i.e., forbid GGGG that comes from concatenating domains, e.g.,
+    #
+    #               *  ***
+    #      ACGATCGATG  GGGATGCATGA
+    #     +==========--===========>
+    #     |
+    #     +==========--===========]
+
     def evaluate(seqs: Tuple[str, ...], strand: Optional[nc.Strand]) -> nc.Result:  # noqa
         sequence = seqs[0]
         if 'GGGG' in sequence:
diff --git a/notebooks/Untitled.ipynb b/notebooks/Untitled.ipynb
index e5044c75..554b7d1a 100644
--- a/notebooks/Untitled.ipynb
+++ b/notebooks/Untitled.ipynb
@@ -2,13 +2,63 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 4,
+   "id": "31e867c9-1517-4837-9e10-da4602777b17",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "bytearray(b'ACGATCGTATCAG')\n"
+     ]
+    }
+   ],
+   "source": [
+    "import nuad.vienna_nupack as nv\n",
+    "s = b'ACGATCGTATCAG'\n",
+    "t =  'ACGATCGTATCAG'\n",
+    "b = bytearray(t, encoding='utf-8')\n",
+    "print(b)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "id": "333938b7-9e13-4937-9fbc-4991314615f1",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 1296x576 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import nuad.vienna_nupack as nv\n",
+    "\n",
+    "seqs = [nv.random_dna_seq(42) for _ in range(500)]\n",
+    "cfes = [nv.pfunc(seq) for seq in seqs]\n",
+    "plt.figure(figsize=(18,8))\n",
+    "_ = plt.hist(cfes, bins=40)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
    "id": "cf3567b8-b41b-4ce0-aa83-f8cbd4dd45b3",
    "metadata": {},
    "outputs": [
     {
      "data": {
-      "image/png": "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\n",
+      "image/png": "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\n",
       "text/plain": [
        "<Figure size 1296x576 with 1 Axes>"
       ]
@@ -23,12 +73,15 @@
     "import nuad.np as nn\n",
     "import matplotlib.pyplot as plt\n",
     "\n",
-    "s = nn.DNASeqList(length=21, num_random_seqs=10**5)\n",
-    "energies = s.energies(37)\n",
+    "s10 = nn.DNASeqList(length=10)\n",
+    "# s11 = nn.DNASeqList(length=11)\n",
+    "energies10 = s10.energies(52)\n",
+    "# energies11 = s.energies(52)\n",
+    "# energies = energies10+energies11\n",
     "# print(f'{min(energies)=}')\n",
     "# print(f'{max(energies)=}')\n",
     "plt.figure(figsize=(18,8))\n",
-    "_ = plt.hist(energies, bins=20)"
+    "_ = plt.hist(energies10, bins=20)"
    ]
   },
   {
diff --git a/notebooks/nuad_parallel_time_trials.ipynb b/notebooks/nuad_parallel_time_trials.ipynb
index 795416f9..30e2eabc 100644
--- a/notebooks/nuad_parallel_time_trials.ipynb
+++ b/notebooks/nuad_parallel_time_trials.ipynb
@@ -1,5 +1,35 @@
 {
  "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "ed9e9749-2d37-4bbc-9996-9621fbfe6efb",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "5.61 s ± 366 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
+      "2.04 s ± 45.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
+      "293 ms ± 8.25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "import nuad.vienna_nupack as nv\n",
+    "\n",
+    "def pfunc_all(seqs):\n",
+    "    for s1, s2 in seqs:\n",
+    "        p = nv.pfunc((s1,s2))\n",
+    "\n",
+    "length = 90\n",
+    "seqs = [(nv.random_dna_seq(length), nv.random_dna_seq(length)) for _ in range(500)]\n",
+    "%timeit pfunc_all(seqs)\n",
+    "%timeit nv.rna_duplex_multiple(seqs)\n",
+    "%timeit nv.rna_plex_multiple(seqs)"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": 1,
diff --git a/nuad/__version__.py b/nuad/__version__.py
index 8959ba2f..8f0ab7fd 100644
--- a/nuad/__version__.py
+++ b/nuad/__version__.py
@@ -1,1 +1,1 @@
-version = '0.4.6'  # version line; WARNING: do not remove or change this line or comment
+version = '0.4.7'  # version line; WARNING: do not remove or change this line or comment
diff --git a/nuad/constraints.py b/nuad/constraints.py
index a041f1d2..723c8602 100644
--- a/nuad/constraints.py
+++ b/nuad/constraints.py
@@ -963,7 +963,7 @@ class DomainPool(JSONSerializable):
 
     replace_with_close_sequences: bool = True
     """
-    If True, instead of picking a sequence uniformly at random from all those satisfying the constraints
+    If True, instead of picking a sequence uniformly at random from all those satisfying the filters
     when returning a sequence from :meth:`DomainPool.generate_sequence`,
     one is picked "close" in Hamming distance to the previous sequence of the :any:`Domain`.
     The field :data:`DomainPool.hamming_probability` is used to pick a distance at random, after which
@@ -1633,7 +1633,7 @@ def from_json_serializable(json_map: Dict[str, Any],
     @property
     def name(self) -> str:
         """
-        :return: :any:`DomainPool` of this :any:`Domain`
+        :return: name of this :any:`Domain`
         """
         return self._name
 
@@ -2651,7 +2651,7 @@ def fixed(self) -> bool:
         """True if every :any:`Domain` on this :any:`Strand` has a fixed DNA sequence."""
         return all(domain.fixed for domain in self.domains)
 
-    def unfixed_domains(self) -> Tuple[Domain]:
+    def unfixed_domains(self) -> Tuple[Domain, ...]:
         """
         :return: all :any:`Domain`'s in this :any:`Strand` where :data:`Domain.fixed` is False
         """
@@ -4851,7 +4851,7 @@ def nupack_domain_free_energy_constraint(
     """
     _check_nupack_installed()
 
-    def evaluate(seqs: Tuple[str], _: Domain | None) -> Result:
+    def evaluate(seqs: Tuple[str, ...], _: Domain | None) -> Result:
         sequence = seqs[0]
         energy = nv.free_energy_single_strand(sequence, temperature, sodium, magnesium)
         excess = max(0.0, threshold - energy)
@@ -4919,7 +4919,7 @@ def nupack_strand_free_energy_constraint(
     """
     _check_nupack_installed()
 
-    def evaluate(seqs: Tuple[str], _: Strand | None) -> Result:
+    def evaluate(seqs: Tuple[str, ...], _: Strand | None) -> Result:
         sequence = seqs[0]
         energy = nv.free_energy_single_strand(sequence, temperature, sodium, magnesium)
         excess = max(0.0, threshold - energy)
@@ -5124,7 +5124,7 @@ def nupack_strand_pair_constraints_by_number_matching_domains(
     if descriptions is None:
         descriptions = {
             num_matching: (_pair_default_description('strand', 'NUPACK', threshold, temperature) +
-                           f'\nfor strands with {num_matching} complementary '
+                           f' for strands with {num_matching} complementary '
                            f'{"domain" if num_matching == 1 else "domains"}')
             for num_matching, threshold in thresholds.items()
         }
@@ -5485,7 +5485,7 @@ def evaluate_bulk(domain_pairs: Iterable[DomainPair]) -> List[Result]:
 
 def get_domain_pairs_from_thresholds_dict(
         thresholds: Dict[Tuple[Domain, bool, Domain, bool] | Tuple[Domain, Domain], Tuple[float, float]]
-) -> Tuple[DomainPair]:
+) -> Tuple[DomainPair, ...]:
     # gather pairs of domains referenced in `thresholds`
     domain_pairs = []
     for key, _ in thresholds.items():
@@ -5508,9 +5508,11 @@ def get_domain_pairs_from_thresholds_dict(
     return domain_pairs
 
 
+S = TypeVar('S', str, bytes, bytearray)
+
 PairsEvaluationFunction = Callable[
-    [Sequence[Tuple[str, str]], logging.Logger, float, str, float],
-    Tuple[float]
+    [Sequence[Tuple[S, S]], logging.Logger, float, str, float],
+    Tuple[float, ...]
 ]
 
 
@@ -5701,6 +5703,9 @@ def rna_plex_domain_pairs_nonorthogonal_constraint(
     :param parameters_filename:
         name of parameters file for ViennaRNA; default is
         same as :py:meth:`vienna_nupack.rna_duplex_multiple`
+    :param max_energy:
+        maximum energy to return; if the RNAplex returns a value larger than this, then
+        this value is used instead
     :return:
         constraint
     """
@@ -5889,7 +5894,7 @@ def __call__(self, *,
                  weight: float = 1.0,
                  score_transfer_function: Callable[[float], float] = default_score_transfer_function,
                  description: str | None = None,
-                 short_description: str,
+                 short_description: str = '',
                  parallel: bool = False,
                  pairs: Iterable[Tuple[Strand, Strand]] | None = None,
                  ) -> SPC: ...
@@ -5955,7 +5960,7 @@ def _strand_pairs_constraints_by_number_matching_domains(
 def _normalize_domains_pairs_disjoint_parameters(
         domains: Iterable[Domain] | None,
         pairs: Iterable[Tuple[Domain, Domain]],
-        check_domain_against_itself: bool) -> Iterable[Tuple[Domain, Domain]]:
+        check_domain_against_itself: bool) -> Tuple[Tuple[Domain, Domain], ...]:
     # Enforce that exactly one of domains or pairs is not None, and if domains is specified,
     # set pairs to be all pairs from domains. Return those pairs; if pairs is specified,
     # just return it. Also normalize to return a tuple.
@@ -6020,7 +6025,7 @@ def rna_cofold_strand_pairs_constraints_by_number_matching_domains(
     if descriptions is None:
         descriptions = {
             num_matching: (_pair_default_description('strand', 'RNAcofold', threshold, temperature) +
-                           f'\nfor strands with {num_matching} complementary '
+                           f' for strands with {num_matching} complementary '
                            f'{"domain" if num_matching == 1 else "domains"}')
             for num_matching, threshold in thresholds.items()
         }
@@ -6100,7 +6105,7 @@ def rna_duplex_strand_pairs_constraints_by_number_matching_domains(
     if descriptions is None:
         descriptions = {
             num_matching: (_pair_default_description('strand', 'RNAduplex', threshold, temperature) +
-                           f'\nfor strands with {num_matching} complementary '
+                           f' for strands with {num_matching} complementary '
                            f'{"domain" if num_matching == 1 else "domains"}')
             for num_matching, threshold in thresholds.items()
         }
@@ -6187,7 +6192,7 @@ def rna_plex_strand_pairs_constraints_by_number_matching_domains(
     if descriptions is None:
         descriptions = {
             num_matching: (_pair_default_description('strand', 'RNAplex', threshold, temperature) +
-                           f'\nfor strands with {num_matching} complementary '
+                           f' for strands with {num_matching} complementary '
                            f'{"domain" if num_matching == 1 else "domains"}')
             for num_matching, threshold in thresholds.items()
         }
@@ -6639,22 +6644,21 @@ def lcs_strand_pairs_constraint_with_dummy_parameters(
             *,
             threshold: float,
             temperature: float = nv.default_temperature,
-            weight_: float = 1.0,
-            score_transfer_function_: Callable[[float], float] = default_score_transfer_function,
+            weight: float = 1.0,
+            score_transfer_function: Callable[[float], float] = default_score_transfer_function,
             description: str | None = None,
             short_description: str = 'lcs strand pairs',
-            parallel_: bool = False,
-            pairs_: Iterable[Tuple[Strand, Strand]] | None = None,
-            parameters_filename_: str = nv.default_vienna_rna_parameter_filename
+            parallel: bool = False,
+            pairs: Iterable[Tuple[Strand, Strand]] | None = None,
     ) -> StrandPairsConstraint:
         threshold_int = int(threshold)
         return lcs_strand_pairs_constraint(
             threshold=threshold_int,
-            weight=weight_,
-            score_transfer_function=score_transfer_function_,
+            weight=weight,
+            score_transfer_function=score_transfer_function,
             description=description,
             short_description=short_description,
-            pairs=pairs_,
+            pairs=pairs,
             check_strand_against_itself=True,
             # TODO: rewrite signature of other strand pair constraints to include this
             gc_double=gc_double,
@@ -6663,7 +6667,7 @@ def lcs_strand_pairs_constraint_with_dummy_parameters(
     if descriptions is None:
         descriptions = {
             num_matching: (f'Longest complementary subsequence between strands is > {threshold}' +
-                           f'\nfor strands with {num_matching} complementary '
+                           f' for strands with {num_matching} complementary '
                            f'{"domain" if num_matching == 1 else "domains"}')
             for num_matching, threshold in thresholds.items()
         }
diff --git a/nuad/np.py b/nuad/np.py
index 780eb7fc..c72b30f2 100644
--- a/nuad/np.py
+++ b/nuad/np.py
@@ -257,7 +257,7 @@ def make_array_with_random_subset_of_dna_seqs(
 
     base_bits = np.array([base2bits[base] for base in bases], dtype=np.ubyte)
     num_seqs_to_sample = 2 * num_random_seqs  # c*k in analysis above
-    unique_sorted_arr = None
+    unique_sorted_arr: np.ndarray | None = None
 
     # odds are low to have a collision, so for simplicity we just repeat the whole process if needed
     while unique_sorted_arr is None or len(unique_sorted_arr) < num_random_seqs:
@@ -468,7 +468,7 @@ def longest_common_substring(a1: np.ndarray, a2: np.ndarray, vectorized: bool =
     substring (subarray) of 1D arrays a1 and a2."""
     assert len(a1.shape) == 1
     assert len(a2.shape) == 1
-    counter = np.zeros(shape=(len(a1) + 1, len(a2) + 1), dtype=np.int)
+    counter = np.zeros(shape=(len(a1) + 1, len(a2) + 1), dtype=np.int8)
     a1idx_longest = a2idx_longest = -1
     len_longest = 0
 
@@ -492,6 +492,7 @@ def longest_common_substring(a1: np.ndarray, a2: np.ndarray, vectorized: bool =
                         len_longest = c
                         a1idx_longest = i1 + 1 - c
                         a2idx_longest = i2 + 1 - c
+
     return a1idx_longest, a2idx_longest, len_longest
 
 
@@ -508,7 +509,7 @@ def longest_common_substrings_singlea1(a1: np.ndarray, a2s: np.ndarray) \
     numa2s = a2s.shape[0]
     len_a1 = len(a1)
     len_a2 = a2s.shape[1]
-    counter = np.zeros(shape=(len_a1 + 1, numa2s, len_a2 + 1), dtype=np.int)
+    counter = np.zeros(shape=(len_a1 + 1, numa2s, len_a2 + 1), dtype=np.int8)
 
     for i1 in range(len(a1)):
         idx = (a2s == a1[i1])
@@ -566,7 +567,7 @@ def _longest_common_substrings_pairs(a1s: np.ndarray, a2s: np.ndarray) \
     len_a1 = a1s.shape[1]
     len_a2 = a2s.shape[1]
 
-    counter = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.int)
+    counter = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.int8)
 
     for i1 in range(len_a1):
         a1s_cp_col = a1s[:, i1].reshape(numpairs, 1)
@@ -607,8 +608,8 @@ def _strongest_common_substrings_all_pairs_return_energies_and_counter(
     numpairs = a1s.shape[0]
     len_a1 = a1s.shape[1]
     len_a2 = a2s.shape[1]
-    counter = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.int)
-    energies = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.float)
+    counter = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.int8)
+    energies = np.zeros(shape=(len_a1 + 1, numpairs, len_a2 + 1), dtype=np.float32)
 
     #     if not loop_energies:
     loop_energies = calculate_loop_energies(temperature)
diff --git a/nuad/search.py b/nuad/search.py
index 0996bae2..89e249f6 100644
--- a/nuad/search.py
+++ b/nuad/search.py
@@ -89,7 +89,7 @@ def default_output_directory() -> str:
 # combinations of inputs so it's worth it to maintain a cache.
 @lru_cache()
 def find_parts_to_check(constraint: nc.Constraint[DesignPart], design: nc.Design,
-                        domains_changed: None | Tuple[Domain]) -> Tuple[DesignPart, ...]:
+                        domains_changed: None | Tuple[Domain, ...]) -> Tuple[DesignPart, ...]:
     if domains_changed is not None:
         domains_changed_full: OrderedSet[Domain] = OrderedSet(domains_changed)
         for domain in domains_changed:
@@ -734,7 +734,7 @@ class SearchParameters:
     save_design_for_all_updates: bool = False
     """
     A serialized (JSON) description of the most recently updated :any:`Design` is always written to 
-    a file `current-best-design.json`. If this is True, then in the folder `dsd_designs`, a file unique to 
+    a file `current-best-design.json`. If this is True, then in the folder `designs`, a file unique to 
     that update is also written. Set to False to use less space on disk. 
     """
 
@@ -1054,17 +1054,18 @@ def _setup_directories(params: SearchParameters) -> _Directories:
 
 
 def _reassign_domains(eval_set: EvaluationSet, max_domains_to_change: int,
-                      rng: np.random.Generator) -> Tuple[Tuple[Domain], Dict[Domain, str]]:
+                      rng: np.random.Generator) -> Tuple[Tuple[Domain, ...], Dict[Domain, str]]:
     # pick domain to change, with probability proportional to total score of constraints it violates
     # first weight scores by domain's weight
-    domains = list(eval_set.domain_to_score.keys())
+    domains: List[Domain] = list(eval_set.domain_to_score.keys())
     scores_weighted = [score * domain.weight for domain, score in eval_set.domain_to_score.items()]
     probs_opt = np.asarray(scores_weighted)
     probs_opt /= probs_opt.sum()
     num_domains_to_change = 1 if max_domains_to_change == 1 \
         else rng.choice(a=range(1, max_domains_to_change + 1))
-    domains_changed: Tuple[Domain] = tuple(rng.choice(a=domains, p=probs_opt, replace=False,
-                                                      size=num_domains_to_change))
+    domains_changed_list = rng.choice(a=domains, p=probs_opt, replace=False,  # type: ignore
+                                      size=num_domains_to_change)  # type: ignore
+    domains_changed: Tuple[Domain, ...] = tuple(domains_changed_list)
 
     # fixed Domains should never be blamed for constraint violation
     assert all(not domain_changed.fixed for domain_changed in domains_changed)
@@ -1612,7 +1613,7 @@ def evaluate_all(self, design: Design) -> None:
         self.update_scores_and_counts()
         # _assert_violations_are_accurate(self.evaluations, self.violations)
 
-    def evaluate_new(self, design: Design, domains_new: Tuple[Domain]) -> None:
+    def evaluate_new(self, design: Design, domains_new: Tuple[Domain, ...]) -> None:
         # called only on changed parts of the design and sets self.evaluations_new
         # does quit early optimization since this is only called when comparing to an existing set of evals
         self.reset_new()
@@ -1650,7 +1651,7 @@ def calculate_score_gap(self, fixed: bool | None = None) -> float | None:
                 total_gap += eval_old.score - eval_new.score
         return total_gap
 
-    def calculate_initial_score_gap(self, design: Design, domains_new: Tuple[Domain]) -> float:
+    def calculate_initial_score_gap(self, design: Design, domains_new: Tuple[Domain, ...]) -> float:
         # before evaluations_new is populated, we need to calculate the total score of evaluations
         # on parts affected by domains_new, which is the score gap assuming all new evaluations come up 0
         score_gap = 0.0
@@ -1667,7 +1668,7 @@ def evaluate_singular_constraint_parallel(constraint: SingularConstraint[DesignP
                                               score_gap: float) \
             -> Tuple[List[Tuple[nc.DesignPart, float, str]], float]:
         if len(parts) == 0:
-            return tuple()
+            return [], 0.0
 
         num_cpus = nc.cpu_count()
 
@@ -1696,7 +1697,7 @@ def evaluate_constraint(self,
                             constraint: Constraint[DesignPart],
                             design: Design,  # only used with DesignConstraint
                             score_gap: float | None,
-                            domains_new: Tuple[Domain] | None,
+                            domains_new: Tuple[Domain, ...] | None,
                             ) -> float:
         # returns score gap = score(old evals) - score(new evals);
         # if gap > 0, then new evals haven't added up to
@@ -2159,6 +2160,11 @@ def display_report(design: nc.Design, constraints: Iterable[Constraint],
     When run in a Jupyter notebook cell, creates a :any:`ConstraintsReport` (the one returned from
     :func:`create_constraints_report`) and displays its data graphically in the notebook using matplotlib.
 
+    This is a histogram showing how many "design parts" (e.g., strands, pairs of strands, etc.)
+    had various values for each :any:`Constraint`. The x-axis is the value measured by the :any:`Constraint`
+    (more precisely, the number in the field :data:`Result.value` generated when evaluating the
+    :any:`Constraint`), and the y-axis is the number of design parts with that value.
+
     :param design:
         the :any:`constraints.Design`, with sequences assigned to all :any:`Domain`'s
     :param constraints:
@@ -2212,7 +2218,7 @@ def dm(obj):
                                                    include_only_with_values=False)
 
     # divide into constraints with values (put in histogram) and without (print summary of violations)
-    reports_with_values: List[Tuple[ConstraintReport, List[float], List[tuple]]] = []
+    reports_with_values: List[Tuple[ConstraintReport, List[float], List[str]]] = []
     reports_without_values: List[ConstraintReport] = []
     for i, report in enumerate(constraints_report.reports):
         values = [ev.result.value for ev in report.evaluations if ev.result.value is not None]
diff --git a/nuad/vienna_nupack.py b/nuad/vienna_nupack.py
index 2b708252..8232e392 100644
--- a/nuad/vienna_nupack.py
+++ b/nuad/vienna_nupack.py
@@ -21,7 +21,7 @@
 import subprocess as sub
 import sys
 from multiprocessing.pool import ThreadPool
-from typing import Sequence, Tuple, List, Iterable
+from typing import Sequence, Tuple, List, Iterable, TypeVar
 
 import numpy as np
 
@@ -67,8 +67,11 @@ def calculate_strand_association_penalty(temperature: float, num_seqs: int) -> f
     return adjust * (num_seqs - 1)
 
 
+S = TypeVar('S', str, bytes, bytearray)
+
+
 def pfunc(
-        seqs: str | Tuple[str, ...],
+        seqs: S | Tuple[S, ...],
         temperature: float = default_temperature,
         sodium: float = default_sodium,
         magnesium: float = default_magnesium,
@@ -110,7 +113,7 @@ def pfunc(
     :return:
         complex free energy ("delta G") of ordered complex with strands in given cyclic permutation
     """
-    seqs = tupleize(seqs)
+    seqs: Tuple[S, ...] = tupleize(seqs)
 
     try:
         from nupack import pfunc as nupack_pfunc  # type: ignore
@@ -127,7 +130,7 @@ def pfunc(
         _cached_nupack_models[param] = model
     else:
         model = _cached_nupack_models[param]
-    (_, dg) = nupack_pfunc(strands=seqs, model=model)
+    _, dg = nupack_pfunc(strands=seqs, model=model)
 
     if strand_association_penalty and len(seqs) > 1:
         dg += calculate_strand_association_penalty(temperature, len(seqs))
@@ -135,8 +138,9 @@ def pfunc(
     return dg
 
 
-def tupleize(seqs: str | Iterable[str]) -> Tuple[str, ...]:
-    return (seqs,) if isinstance(seqs, str) else tuple(seqs)
+def tupleize(seqs: S | Iterable[S]) -> Tuple[S, ...]:
+    return (seqs,) if isinstance(seqs, str) or isinstance(seqs, bytes) or isinstance(seqs, bytearray) \
+        else tuple(seqs)
 
 
 try:
@@ -145,12 +149,12 @@ def tupleize(seqs: str | Iterable[str]) -> Tuple[str, ...]:
 
     def pfunc_parallel(
             pool: ProcessPool,
-            all_seqs: Sequence[str | Tuple[str, ...]],
+            all_seqs: Sequence[S | Tuple[S, ...]],
             temperature: float = default_temperature,
             sodium: float = default_sodium,
             magnesium: float = default_magnesium,
             strand_association_penalty: bool = True,
-    ) -> Tuple[float]:
+    ) -> Tuple[float, ...]:
         num_seqs = len(all_seqs)
         if num_seqs == 0:
             return tuple()
@@ -174,7 +178,7 @@ def pfunc_parallel(
                            or (num_seqs <= 1)
                            )
 
-        def calculate_energies_sequential(all_tuples: Sequence[Tuple[str, ...]]) -> Tuple[float]:
+        def calculate_energies_sequential(all_tuples: Sequence[Tuple[S, ...]]) -> Tuple[float, ...]:
             return tuple(pfunc(seqs, temperature, sodium, magnesium, strand_association_penalty)
                          for seqs in all_tuples)
 
@@ -221,8 +225,8 @@ def nupack_complex_base_pair_probabilities(strand_complex: 'nc.Complex',  # circ
         from nupack import Strand as NupackStrand
         from nupack import SetSpec as NupackSetSpec
         from nupack import complex_analysis as nupack_complex_analysis
-        from nupack import PairsMatrix as NupackPairsMatrix
-        from nupack import Model  # type: ignore
+        from nupack import PairMatrix as NupackPairMatrix
+        from nupack import Model
     except ModuleNotFoundError:
         raise ImportError(
             'NUPACK 4 must be installed to use nupack_complex_base_pair_probabilities. '
@@ -242,7 +246,7 @@ def nupack_complex_base_pair_probabilities(strand_complex: 'nc.Complex',  # circ
         nupack_strands, complexes=NupackSetSpec(max_size=0, include=(nupack_complex,)))
     nupack_complex_analysis_result = nupack_complex_analysis(
         nupack_complex_set, compute=['pairs'], model=model)
-    pairs: NupackPairsMatrix = nupack_complex_analysis_result[nupack_complex].pairs
+    pairs: NupackPairMatrix = nupack_complex_analysis_result[nupack_complex].pairs
     nupack_complex_result: np.ndarray = pairs.to_array()
     return nupack_complex_result
 
@@ -281,12 +285,12 @@ def call_subprocess(command_strs: List[str], user_input: str) -> Tuple[str, str]
     return output_decoded, stderr_decoded
 
 
-def rna_duplex_multiple(pairs: Sequence[Tuple[str, str]],
+def rna_duplex_multiple(pairs: Sequence[Tuple[S, S]],
                         logger: logging.Logger = logging.root,
                         temperature: float = default_temperature,
                         parameters_filename: str = default_vienna_rna_parameter_filename,
                         max_energy: float = 0.0,
-                        ) -> Tuple[float]:
+                        ) -> Tuple[float, ...]:
     """
     Calls RNAduplex (from ViennaRNA package: https://www.tbi.univie.ac.at/RNA/)
     on a list of pairs, specifically:
@@ -360,12 +364,12 @@ def rna_duplex_multiple(pairs: Sequence[Tuple[str, str]],
 
 def rna_duplex_multiple_parallel(
         thread_pool: ThreadPool,
-        pairs: Sequence[Tuple[str, str]],
+        pairs: Sequence[Tuple[S, S]],
         logger: logging.Logger = logging.root,
         temperature: float = default_temperature,
         parameters_filename: str = default_vienna_rna_parameter_filename,
         max_energy: float = 0.0,
-) -> Tuple[float]:
+) -> Tuple[float, ...]:
     """
     Parallel version of :meth:`rna_duplex_multiple`. TODO document this
     """
@@ -389,7 +393,7 @@ def rna_duplex_multiple_parallel(
                        or (num_pairs < num_cores)
                        )
 
-    def calculate_energies_sequential(seq_pairs: Sequence[Tuple[str, str]]) -> Tuple[float]:
+    def calculate_energies_sequential(seq_pairs: Sequence[Tuple[str, str]]) -> Tuple[float, ...]:
         return rna_duplex_multiple(pairs=seq_pairs, logger=logger, temperature=temperature,
                                    parameters_filename=parameters_filename, max_energy=max_energy)
 
@@ -402,12 +406,12 @@ def calculate_energies_sequential(seq_pairs: Sequence[Tuple[str, str]]) -> Tuple
     return tuple(energies)
 
 
-def rna_plex_multiple(pairs: Sequence[Tuple[str, str]],
+def rna_plex_multiple(pairs: Sequence[Tuple[S, S]],
                       logger: logging.Logger = logging.root,
                       temperature: float = default_temperature,
                       parameters_filename: str = default_vienna_rna_parameter_filename,
                       max_energy: float = 0.0,
-                      ) -> Tuple[float]:
+                      ) -> Tuple[float, ...]:
     """
     Calls RNAplex (from ViennaRNA package: https://www.tbi.univie.ac.at/RNA/)
     on a list of pairs, specifically:
@@ -513,12 +517,12 @@ def nupack_multiple_with_sodium_magnesium(
     """
 
     def nupack_multiple(
-            pairs: Sequence[Tuple[str, str]],
+            pairs: Sequence[Tuple[S, S]],
             logger: logging.Logger = logging.root,
             temperature: float = default_temperature,
             parameters_filename: str = default_vienna_rna_parameter_filename,
             max_energy: float = 0.0,
-    ) -> Tuple[float]:
+    ) -> Tuple[float, ...]:
         # :param pairs:
         #     sequence (list or tuple) of pairs of DNA sequences
         # :param logger:
@@ -548,12 +552,12 @@ def nupack_multiple(
 
 def rna_plex_multiple_parallel(
         thread_pool: ThreadPool,
-        pairs: Sequence[Tuple[str, str]],
+        pairs: Sequence[Tuple[S, S]],
         logger: logging.Logger = logging.root,
         temperature: float = default_temperature,
         parameters_filename: str = default_vienna_rna_parameter_filename,
         max_energy: float = 0.0,
-) -> Tuple[float]:
+) -> Tuple[float, ...]:
     """
     Parallel version of :meth:`rna_plex_multiple`. TODO document this
     """
@@ -577,7 +581,7 @@ def rna_plex_multiple_parallel(
                        or (num_pairs < num_cores)
                        )
 
-    def calculate_energies_sequential(seq_pairs: Sequence[Tuple[str, str]]) -> Tuple[float]:
+    def calculate_energies_sequential(seq_pairs: Sequence[Tuple[str, str]]) -> Tuple[float, ...]:
         return rna_plex_multiple(pairs=seq_pairs, logger=logger, temperature=temperature,
                                  parameters_filename=parameters_filename, max_energy=max_energy)
 
@@ -602,12 +606,12 @@ def _fix_filename_windows(parameters_filename: str) -> str:
 
 
 def rna_cofold_multiple(
-        seq_pairs: Sequence[Tuple[str, str]],
+        seq_pairs: Sequence[Tuple[S, S]],
         logger: logging.Logger = logging.root,
         temperature: float = default_temperature,
         parameters_filename: str = default_vienna_rna_parameter_filename,
         max_energy: float = 0.0,
-) -> Tuple[float]:
+) -> Tuple[float, ...]:
     """
     Calls RNAcofold (from ViennaRNA package: https://www.tbi.univie.ac.at/RNA/)
     on a list of pairs, specifically:


Test patch:
diff --git a/tests/test.py b/tests/test.py
index fbe791cf..910ef48e 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -337,8 +337,8 @@ def test_write_idt_plate_excel_file(self) -> None:
             os.remove(filename)
 
 
-class TestNumpyConstraints(unittest.TestCase):
-    def test_NearestNeighborEnergyConstraint_raises_exception_if_energies_in_wrong_order(self) -> None:
+class TestNumpyFilters(unittest.TestCase):
+    def test_NearestNeighborEnergyFilter_raises_exception_if_energies_in_wrong_order(self) -> None:
         with self.assertRaises(ValueError):
             nc.NearestNeighborEnergyFilter(-10, -15)