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": [ + "
" + ] + }, + "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": [ "
" ] @@ -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 @@ -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: 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)