diff --git a/primer3/thermoanalysis.pyx b/primer3/thermoanalysis.pyx index e58e1c2..70d3065 100644 --- a/primer3/thermoanalysis.pyx +++ b/primer3/thermoanalysis.pyx @@ -2423,3 +2423,14 @@ class ThermoAnalysis(_ThermoAnalysis): ''' pywarnings.warn(SNAKE_CASE_DEPRECATED_MSG % 'calc_tm') return self.calc_tm(seq1) + +cdef extern from "libprimer3.h": + const char* libprimer3_release() + +def get_libprimer3_version() -> str: + '''Get the version of the underlying libprimer3 C library. + + Returns: + str: The version string of libprimer3 + ''' + return libprimer3_release().decode('utf-8') diff --git a/pyproject.toml b/pyproject.toml index 7d53b31..5b3e37f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ classifiers = [ dev = [ "pre-commit~=3.5.0", "pytest~=7.4.0", + "tomli~=2.0.0", ] docs = [ "myst-parser", diff --git a/tests/save_thermo_values.py b/tests/save_thermo_values.py new file mode 100644 index 0000000..15ee79b --- /dev/null +++ b/tests/save_thermo_values.py @@ -0,0 +1,153 @@ +# Copyright (C) 2020-2025. Ben Pruitt & Nick Conway; Wyss Institute +# See LICENSE for full GPLv2 license. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' +tests.save_thermo_values +~~~~~~~~~~~~~~~~~~~~~~~ + +Script to generate and save gold standard thermodynamic values for +regression testing. Imports calculation and saving functions from +test_sequences. + +''' + +import json +import platform +import subprocess +import sys +from datetime import datetime +from importlib.metadata import ( + PackageNotFoundError, + version, +) +from pathlib import Path +from typing import Dict + +import tomli + +from primer3 import __version__ as primer3_py_version +from primer3 import thermoanalysis + +# Add the project root to the Python path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from tests.test_sequences import calculate_thermo_values + + +def get_git_info() -> Dict[str, str]: + '''Get git branch and latest tag information. + + Returns: + Dictionary containing git branch and latest tag + ''' + try: + branch = subprocess.check_output( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + stderr=subprocess.DEVNULL, + ).decode('utf-8').strip() + + latest_tag = subprocess.check_output( + ['git', 'describe', '--tags', '--abbrev=0'], + stderr=subprocess.DEVNULL, + ).decode('utf-8').strip() + + return { + 'branch': branch, + 'latest_tag': latest_tag, + } + except subprocess.CalledProcessError: + return { + 'branch': 'unknown', + 'latest_tag': 'unknown', + } + + +def get_dependency_versions() -> Dict[str, str]: + '''Get versions of dependencies from pyproject.toml. + + Returns: + Dictionary containing versions of Python dependencies + ''' + deps = {} + + # Read pyproject.toml + pyproject_path = Path(__file__).parent.parent / 'pyproject.toml' + with open(pyproject_path, 'rb') as f: + pyproject = tomli.load(f) + + # Get build dependencies + build_deps = pyproject.get('build-system', {}).get('requires', []) + for dep in build_deps: + name = dep.split('>=')[0].split('~=')[0].split('==')[0].strip() + try: + deps[name] = version(name) + except PackageNotFoundError: + deps[name] = 'Not installed' + + # Get dev dependencies + dev_deps = pyproject.get('project', {}).get( + 'optional-dependencies', {}, + ).get('dev', []) + for dep in dev_deps: + name = dep.split('>=')[0].split('~=')[0].split('==')[0].strip() + try: + deps[name] = version(name) + except PackageNotFoundError: + deps[name] = 'Not installed' + + return deps + + +def save_thermo_values_with_metadata( + values, + filename='tests/thermo_standard_values.json', +) -> None: + '''Save thermodynamic values with metadata to a JSON file.''' + metadata = { + 'generation_timestamp': datetime.now().isoformat(), + 'primer3_py_version': primer3_py_version, + 'primer3_lib_version': thermoanalysis.get_libprimer3_version(), + 'python_version': sys.version, + 'platform': { + 'system': platform.system(), + 'release': platform.release(), + 'version': platform.version(), + 'machine': platform.machine(), + }, + 'dependencies': get_dependency_versions(), + 'git_info': get_git_info(), + } + + output = { + 'metadata': metadata, + 'values': values, + } + + with open(filename, 'w') as f: + json.dump(output, f, indent=2) + + +if __name__ == '__main__': + # Calculate standard values using the function from test_sequences + standard_values = calculate_thermo_values() + + # Save standard values to a JSON file with metadata + save_thermo_values_with_metadata( + standard_values, 'tests/thermo_standard_values.json', + ) + + print('Gold standard values saved to tests/thermo_standard_values.json') diff --git a/tests/test_primerdesign.py b/tests/test_primerdesign.py index ad9d5e5..fef9c97 100644 --- a/tests/test_primerdesign.py +++ b/tests/test_primerdesign.py @@ -58,6 +58,25 @@ def _get_mem_usage(): class TestDesignBindings(unittest.TestCase): + def setUp(self) -> None: + '''Set up test case''' + random.seed(42) # Make random operations deterministic + + def _generate_quality_list(self, length: int) -> List[int]: + '''Generate a deterministic list of quality scores + + Args: + length: Length of quality score list to generate + + Returns: + List of quality scores between 30 and 50 with mean around 45 + ''' + # Use triangular distribution to cluster scores around 45 + return [ + int(random.triangular(30, 50, 45)) + for _ in range(length) + ] + def _compare_results( self, binding_res: Dict[str, Any], @@ -208,10 +227,7 @@ def test_compare_sim(self): 'TGAAGGCAAAATGATTAGACATATTGCATTAAGGTAAAAAATGATAACTGAAGAATTATGTGCCA' 'CACTTATTAATAAGAAAGAATATGTGAACCTTGCAGATGTTTCCCTCTAGTAG' ) - quality_list = [ - random.randint(20, 90) - for i in range(len(sequence_template)) - ] + quality_list = self._generate_quality_list(len(sequence_template)) seq_args = { 'SEQUENCE_ID': 'MH1000', 'SEQUENCE_TEMPLATE': sequence_template, diff --git a/tests/test_sequences.py b/tests/test_sequences.py new file mode 100644 index 0000000..95e2431 --- /dev/null +++ b/tests/test_sequences.py @@ -0,0 +1,324 @@ +# Copyright (C) 2020-2025. Ben Pruitt & Nick Conway; Wyss Institute +# See LICENSE for full GPLv2 license. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' +tests.test_sequences +~~~~~~~~~~~~~~~~~~~ + +Unit tests for thermodynamic calculations using standardized test sequences. +Tests verify both regression values and expected thermodynamic relationships +between different types of structures (hairpins, heterodimers, end stability). + +''' + +from __future__ import print_function + +import json +import unittest +from typing import ( + Any, + Dict, +) + +from primer3 import ( + argdefaults, + thermoanalysis, +) + +# Use the default values from argdefaults.py +STANDARD_CONDITIONS: Dict[str, Any] = { + 'mv_conc': argdefaults.Primer3PyArguments.mv_conc, # 50.0 mM + 'dv_conc': argdefaults.Primer3PyArguments.dv_conc, # 1.5 mM + 'dntp_conc': argdefaults.Primer3PyArguments.dntp_conc, # 0.6 mM + 'dna_conc': argdefaults.Primer3PyArguments.dna_conc, # 50.0 nM + 'temp_c': argdefaults.Primer3PyArguments.temp_c, # 37.0 °C + 'max_loop': argdefaults.Primer3PyArguments.max_loop, # 30 + 'dmso_conc': argdefaults.Primer3PyArguments.dmso_conc, # 0.0 % + 'dmso_fact': argdefaults.Primer3PyArguments.dmso_fact, # 0.6 + 'formamide_conc': argdefaults.Primer3PyArguments.formamide_conc, # 0.0 % +} + +# Test sequences organized by structural properties +TEST_SEQUENCES: Dict[str, Dict[str, Any]] = { + 'homopolymers': { + 'polyA': 'AAAAAAAAAAAAAAAAAAAA', + 'polyT': 'TTTTTTTTTTTTTTTTTTTT', + 'polyG': 'GGGGGGGGGGGGGGGGGGGG', + 'polyC': 'CCCCCCCCCCCCCCCCCCCC', + }, + 'palindromes': { + 'gc_rich': 'GCGCGCGCGCGCGCGCGCGC', + 'at_rich': 'ATATATATATATATATATAT', + 'mixed': 'GATCGATCGATCGATCGATC', + }, + 'hairpins': { + # Perfect hairpin with 8bp stem and 4nt loop + 'perfect': 'GCGCGCGCAAAAGCGCGCGC', + # Hairpin with a single mismatch in the stem + 'mismatched': 'GCGTGCGCAAAAGCGTGCGC', + # Hairpin with a 2nt bulge in the stem + 'bulged': 'GCGTTCGCGCGCGCTTGCGC', + }, + 'heterodimers': { + 'complementary': { + 'seq1': 'AGCCCATACCGCTGTTGTTG', + 'seq2': 'CAACAACAGCGGTATGGGCT', + }, + 'mismatched': { + 'seq1': 'ATCGATCGATCGATCGATCG', + 'seq2': 'TTGGTGTCGGCTGGCCTCGG', + }, + 'overlapping': { + 'seq1': 'AGCCCATACCCGATCGATCG', + 'seq2': 'CAACAACAGCATCGATCGAT', + }, + }, + 'end_stability': { + 'perfect': { # seq1 3' end, 8nt, complementary to middle of seq2 + 'seq1': 'ATAAGCACAATTTTAAAGCC', + 'seq2': 'GAGGGAGGCTTTAACTGCTC', + }, + 'partial': { # same as "perfect" with 1 nt mismatch at position -3 + 'seq1': 'ATAAGCACAATTTTAAACCC', + 'seq2': 'GAGGGAGGCTTTAACTGCTC', + }, + 'mismatched': { # seq1 3' end, 8nt, not complementary to middle of seq2 + 'seq1': 'ATAAGCACAATTTTAAAGCC', + 'seq2': 'GAGGGATAGCCCAACTGCTC', + }, + }, +} + + +def calculate_thermo_values() -> Dict[str, Any]: + '''Calculate thermodynamic values for all test sequences under standard + conditions + ''' + thermo = thermoanalysis.ThermoAnalysis() + thermo.set_thermo_args(**STANDARD_CONDITIONS) + + results: Dict[str, Dict[str, Any]] = {} + + # Calculate values for single sequences + for category, sequences in TEST_SEQUENCES.items(): + if category not in ['heterodimers', 'end_stability']: + results[category] = {} + for name, seq in sequences.items(): + results[category][name] = { + 'tm': thermo.calc_tm(seq), + 'hairpin': thermo.calc_hairpin(seq).todict(), + 'homodimer': thermo.calc_homodimer(seq).todict(), + } + + # Calculate values for sequence pairs + for category in ['heterodimers', 'end_stability']: + results[category] = {} + for name, pair in TEST_SEQUENCES[category].items(): + results[category][name] = { + 'heterodimer': thermo.calc_heterodimer( + pair['seq1'], pair['seq2'], + ).todict(), + 'end_stability': thermo.calc_end_stability( + pair['seq1'], pair['seq2'], + ).todict(), + } + + return results + + +def save_thermo_values( + values: Dict[str, Any], + filename: str = 'tests/thermo_standard_values.json', +) -> None: + '''Save thermodynamic values to a JSON file''' + with open(filename, 'w') as f: + json.dump(values, f, indent=2) + + +def load_thermo_values( + filename: str = 'tests/thermo_standard_values.json', +) -> Dict[str, Any]: + '''Load thermodynamic values from a JSON file''' + with open(filename, 'r') as f: + return json.load(f) + + +class TestThermodynamicRelationships(unittest.TestCase): + '''Test suite for verifying expected thermodynamic relationships between + different types of DNA structures and sequences. + ''' + + def setUp(self) -> None: + self.conditions = STANDARD_CONDITIONS.copy() + self.thermo = thermoanalysis.ThermoAnalysis() + self.thermo.set_thermo_args(**self.conditions) + + def test_gc_content_effect(self) -> None: + '''Test that GC-rich sequences have higher melting temperatures than + AT-rich ones + ''' + gc_rich = TEST_SEQUENCES['palindromes']['gc_rich'] + at_rich = TEST_SEQUENCES['palindromes']['at_rich'] + + gc_tm = self.thermo.calc_tm(gc_rich) + at_tm = self.thermo.calc_tm(at_rich) + + self.assertGreater(gc_tm, at_tm) + + def test_hairpin_stability(self) -> None: + '''Test that perfect hairpins are more stable than mismatched ones''' + perfect = TEST_SEQUENCES['hairpins']['perfect'] + mismatched = TEST_SEQUENCES['hairpins']['mismatched'] + + perfect_dg = self.thermo.calc_hairpin(perfect).dg + mismatched_dg = self.thermo.calc_hairpin(mismatched).dg + + self.assertLess(perfect_dg, mismatched_dg) + + def test_hairpin_stability_relationships(self) -> None: + '''Test that hairpin stabilities follow expected order: perfect < + mismatched < bulged. + ''' + perfect = TEST_SEQUENCES['hairpins']['perfect'] + mismatched = TEST_SEQUENCES['hairpins']['mismatched'] + bulged = TEST_SEQUENCES['hairpins']['bulged'] + + perfect_dg = self.thermo.calc_hairpin(perfect).dg + mismatched_dg = self.thermo.calc_hairpin(mismatched).dg + bulged_dg = self.thermo.calc_hairpin(bulged).dg + + self.assertLess(perfect_dg, mismatched_dg) + self.assertLess(mismatched_dg, bulged_dg) + + def test_heterodimer_stability_relationships(self) -> None: + '''Test that heterodimer stabilities follow expected order: + complementary < overlapping < mismatched. + ''' + complementary = TEST_SEQUENCES['heterodimers']['complementary'] + overlapping = TEST_SEQUENCES['heterodimers']['overlapping'] + mismatched = TEST_SEQUENCES['heterodimers']['mismatched'] + + comp_dg = self.thermo.calc_heterodimer( + complementary['seq1'], complementary['seq2'], + ).dg + overlap_dg = self.thermo.calc_heterodimer( + overlapping['seq1'], overlapping['seq2'], + ).dg + mismatch_dg = self.thermo.calc_heterodimer( + mismatched['seq1'], mismatched['seq2'], + ).dg + + self.assertLess(comp_dg, overlap_dg) + self.assertLess(overlap_dg, mismatch_dg) + + def test_end_stability_relationships(self) -> None: + '''Test that end stability follows expected order: perfect < partial < + mismatched. + ''' + perfect = TEST_SEQUENCES['end_stability']['perfect'] + partial = TEST_SEQUENCES['end_stability']['partial'] + mismatched = TEST_SEQUENCES['end_stability']['mismatched'] + + perfect_dg = self.thermo.calc_end_stability( + perfect['seq1'], perfect['seq2'], + ).dg + partial_dg = self.thermo.calc_end_stability( + partial['seq1'], partial['seq2'], + ).dg + mismatched_dg = self.thermo.calc_end_stability( + mismatched['seq1'], mismatched['seq2'], + ).dg + + self.assertLess(perfect_dg, partial_dg) + self.assertLess(partial_dg, mismatched_dg) + + def test_parameter_effects(self) -> None: + '''Test that changing parameters affects all sequences consistently''' + base_conditions = self.conditions.copy() + high_salt_conditions = base_conditions.copy() + high_salt_conditions['mv_conc'] = 200.0 + + # Test a few sequences + for seq in [ + TEST_SEQUENCES['palindromes']['gc_rich'], + TEST_SEQUENCES['palindromes']['at_rich'], + ]: + self.thermo.set_thermo_args(**base_conditions) + base_tm = self.thermo.calc_tm(seq) + self.thermo.set_thermo_args(**high_salt_conditions) + high_salt_tm = self.thermo.calc_tm(seq) + + # Higher salt should increase Tm + self.assertGreater(high_salt_tm, base_tm) + + +class TestThermodynamicRegression(unittest.TestCase): + '''Test suite for verifying that thermodynamic calculations match expected + previously calculated across different types of DNA structures. + ''' + + def setUp(self) -> None: + self.conditions = STANDARD_CONDITIONS.copy() + self.thermo = thermoanalysis.ThermoAnalysis() + self.thermo.set_thermo_args(**self.conditions) + self.standard_values = load_thermo_values()['values'] + + def assert_thermo_result_equal(self, result1, result2, places=2) -> None: + '''Assert that two thermodynamic results are approximately equal''' + for key, value in result1.items(): + if isinstance(value, (int, float)): + self.assertAlmostEqual(value, result2[key], places=places) + elif isinstance(value, dict): + self.assert_thermo_result_equal(value, result2[key], places) + + def test_hairpin_regression(self) -> None: + '''Test that hairpin calculations match standard values''' + for name, seq in TEST_SEQUENCES['hairpins'].items(): + result = self.thermo.calc_hairpin(seq).todict() + standard = self.standard_values['hairpins'][name]['hairpin'] + self.assert_thermo_result_equal(result, standard) + + def test_homodimer_regression(self) -> None: + '''Test that homodimer calculations match standard values''' + for category in ['homopolymers', 'palindromes', 'hairpins']: + for name, seq in TEST_SEQUENCES[category].items(): + result = self.thermo.calc_homodimer(seq).todict() + standard = self.standard_values[category][name]['homodimer'] + self.assert_thermo_result_equal(result, standard) + + def test_heterodimer_regression(self) -> None: + '''Test that heterodimer calculations match standard values''' + for name, pair in TEST_SEQUENCES['heterodimers'].items(): + result = self.thermo.calc_heterodimer( + pair['seq1'], pair['seq2'], + ).todict() + standard = self.standard_values['heterodimers'][name]['heterodimer'] + self.assert_thermo_result_equal(result, standard) + + def test_end_stability_regression(self) -> None: + '''Test that end stability calculations match standard values''' + for name, pair in TEST_SEQUENCES['end_stability'].items(): + result = self.thermo.calc_end_stability( + pair['seq1'], pair['seq2'], + ).todict() + standard = ( + self.standard_values['end_stability'][name]['end_stability'] + ) + self.assert_thermo_result_equal(result, standard) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_thermoanalysis.py b/tests/test_thermoanalysis.py index 802ae4d..3a4e3ef 100644 --- a/tests/test_thermoanalysis.py +++ b/tests/test_thermoanalysis.py @@ -52,6 +52,10 @@ def _get_mem_usage(): # type: ignore class TestLowLevelBindings(unittest.TestCase): + def setUp(self) -> None: + '''Set up test case''' + random.seed(42) # Make random operations deterministic + def rand_args(self) -> None: self.seq1 = ''.join([ random.choice('ATGC') for _ in diff --git a/tests/test_threadsafe.py b/tests/test_threadsafe.py index 74b1ff5..3491f1e 100644 --- a/tests/test_threadsafe.py +++ b/tests/test_threadsafe.py @@ -32,6 +32,7 @@ from typing import ( Any, Dict, + List, ) from primer3 import thermoanalysis # type: ignore @@ -58,26 +59,64 @@ def make_rand_seq_pair() -> Dict[str, str]: ''' Returns: dictionary of 2 unique sequences keyed by seq1 and seq2 - ''' return dict( seq1=''.join([ - random.choice('ATGC') for _ in - range(random.randint(20, 59)) + random.choice('ATGC') + for _ in range(random.randint(20, 59)) ]), seq2=''.join([ - random.choice('ATGC') for _ in - range(random.randint(20, 59)) + random.choice('ATGC') + for _ in range(random.randint(20, 59)) ]), ) class TestThermoAnalysisInThread(unittest.TestCase): + def setUp(self) -> None: + '''Set up test case''' + random.seed(42) # Make random operations deterministic + + def _generate_random_sequence(self, base_seq: str, i: int) -> str: + '''Generate a deterministic random sequence based on index + + Args: + base_seq: Base sequence to append to + i: Thread index to use as seed + + Returns: + Base sequence with deterministic random suffix + ''' + # Use a new seed based on thread index to ensure deterministic but + # different sequences + local_random = random.Random(42 + i) + return base_seq + ''.join([ + local_random.choice('ATGC') for _ in + range(60) + ]) + + def _generate_quality_list(self, length: int, i: int) -> List[int]: + '''Generate a deterministic list of quality scores + + Args: + length: Length of quality score list to generate + i: Thread index to use as seed + + Returns: + List of quality scores between 20 and 90 + ''' + # Use a new seed based on thread index to ensure deterministic but + # different scores + local_random = random.Random(42 + i) + return [ + local_random.randint(20, 90) + for _ in range(length) + ] + def test_calc_heterodimer_thread_safety(self): '''Test `calc_heterodimer` (and by extension all thal-dependents) for - thread safety - + thread safety. ''' def het_thread( _results_list, @@ -132,22 +171,23 @@ def test_primer_design_thread_safety(self): ''' def tdesign_run(_global_args, _results_list, i): - sequence_template = ( - 'GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCCTACATTTTAGCATCAGTGAGTACA' - 'GCATGCTTACTGGAAGAGAGGGTCATGCAACAGATTAGGAGGTAAGTTTGCAAAGGCAGGC' - 'TAAGGAGGAGACGCACTGAATGCCATGGTAAGAACTCTGGACATAAAAATATTGGAAGTTG' - 'TTGAGCAAGTNAAAAAAATGTTTGGAAGTGTTACTTTAGCAATGGCAAGAATGATAGTATG' - 'GAATAGATTGGCAGAATGAAGGCAAAATGATTAGACATATTGCATTAAGGTAAAAAATGAT' - 'AACTGAAGAATTATGTGCCACACTTATTAATAAGAAAGAATATGTGAACCTTGCAGATGTT' - 'TCCCTCTAGTAG' - ) + ''.join([ - random.choice('ATGC') for _ in - range(60) - ]) - quality_list = [ - random.randint(20, 90) - for i in range(len(sequence_template)) - ] + base_seq = ( + 'GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCCTACATTTTAGCATCAGT' + 'GAGTACAGCATGCTTACTGGAAGAGAGGGTCATGCAACAGATTAGGAGGTAAGT' + 'TTGCAAAGGCAGGCTAAGGAGGAGACGCACTGAATGCCATGGTAAGAACTCTGG' + 'ACATAAAAATATTGGAAGTTGTTGAGCAAGTNAAAAAAATGTTTGGAAGTGTTA' + 'CTTTAGCAATGGCAAGAATGATAGTATGGAATAGATTGGCAGAATGAAGGCAAA' + 'ATGATTAGACATATTGCATTAAGGTAAAAAATGATAACTGAAGAATTATGTGCC' + 'ACACTTATTAATAAGAAAGAATATGTGAACCTTGCAGATGTTTCCCTCTAGTAG' + ) + sequence_template = self._generate_random_sequence( + base_seq=base_seq, + i=i, + ) + quality_list = self._generate_quality_list( + length=len(sequence_template), + i=i, + ) seq_args = { 'SEQUENCE_ID': 'MH1000', 'SEQUENCE_TEMPLATE': sequence_template, diff --git a/tests/thermo_standard_values.json b/tests/thermo_standard_values.json new file mode 100644 index 0000000..e562221 --- /dev/null +++ b/tests/thermo_standard_values.json @@ -0,0 +1,336 @@ +{ + "metadata": { + "generation_timestamp": "2025-05-19T09:05:52.910774", + "primer3_py_version": "2.1.0", + "primer3_lib_version": "libprimer3 release 2.6.1", + "python_version": "3.11.5 (main, Sep 11 2023, 08:31:25) [Clang 14.0.6 ]", + "platform": { + "system": "Darwin", + "release": "24.3.0", + "version": "Darwin Kernel Version 24.3.0: Thu Jan 2 20:23:36 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T8112", + "machine": "arm64" + }, + "dependencies": { + "setuptools": "75.8.0", + "wheel": "0.38.4", + "Cython": "not installed", + "pre-commit": "3.4.0", + "pytest": "7.4.0", + "tomli": "2.0.1" + }, + "git_info": { + "branch": "feat/deterministic_tests", + "latest_tag": "v2.1.0" + } + }, + "values": { + "homopolymers": { + "polyA": { + "tm": 41.50854350297266, + "hairpin": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + }, + "homodimer": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + } + }, + "polyT": { + "tm": 41.50854350297266, + "hairpin": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + }, + "homodimer": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + } + }, + "polyG": { + "tm": 77.82778595000809, + "hairpin": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + }, + "homodimer": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + } + }, + "polyC": { + "tm": 77.82778595000809, + "hairpin": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + }, + "homodimer": { + "structure_found": false, + "ascii_structure": null, + "tm": 0.0, + "dg": 0.0, + "dh": 0.0, + "ds": 0.0 + } + } + }, + "palindromes": { + "gc_rich": { + "tm": 83.40529626638005, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 91.72140335141319, + "dg": -11698.0103735326, + "dh": -78000.0, + "ds": -213.77394688527295 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 84.26057136046398, + "dg": -35908.203349516305, + "dh": -193200.0, + "ds": -507.1474984700426 + } + }, + "at_rich": { + "tm": 30.791977097697497, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 42.65951873982726, + "dg": -836.8953735294796, + "dh": -46700.0, + "ds": -147.873946885283 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 31.700770547109187, + "dg": -8063.238349510034, + "dh": -132200.0, + "ds": -400.2474984700628 + } + }, + "mixed": { + "tm": 56.5804226725744, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 66.12299266077463, + "dg": -4291.970373529488, + "dh": -50000.0, + "ds": -147.37394688528298 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 57.46276026089612, + "dg": -20276.6033495163, + "dh": -160200.0, + "ds": -451.1474984700426 + } + } + }, + "hairpins": { + "perfect": { + "tm": 74.38950118493187, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 85.19706340038698, + "dg": -9172.782644611016, + "dh": -68200.0, + "ds": -190.31828907105913 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 63.22726945279118, + "dg": -22942.34793382995, + "dh": -150400.0, + "ds": -410.95486721318736 + } + }, + "mismatched": { + "tm": 69.52387235309823, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 72.1041538167027, + "dg": -4626.27018676785, + "dh": -45500.0, + "ds": -131.78697344263148 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 34.960353145988165, + "dg": -10470.293518143611, + "dh": -112600.0, + "ds": -329.29133155523584 + } + }, + "bulged": { + "tm": 73.6124596735225, + "hairpin": { + "structure_found": true, + "ascii_structure": null, + "tm": 73.12132912200786, + "dg": -3525.8504578462657, + "dh": -33800.0, + "ds": -97.61131562841766 + }, + "homodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 43.24182327994464, + "dg": -13804.021975986792, + "dh": -131200.0, + "ds": -378.5135515847597 + } + } + }, + "heterodimers": { + "complementary": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 60.886596558106135, + "dg": -22428.288349513172, + "dh": -156800.0, + "ds": -433.2474984700527 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": 60.886596558106135, + "dg": -22428.288349513172, + "dh": -156800.0, + "ds": -433.2474984700527 + } + }, + "mismatched": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": -35.93540086407427, + "dg": -2668.137457846265, + "dh": -27800.0, + "ds": -81.03131562841766 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": -56.07147605877984, + "dg": -1654.6612289277862, + "dh": -22300.0, + "ds": -66.56565781419383 + } + }, + "overlapping": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 23.35342550189091, + "dg": -8224.063602454169, + "dh": -65000.0, + "ds": -183.0596046994868 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": 20.8827471190894, + "dg": -7767.858602454182, + "dh": -62900.0, + "ds": -177.75960469948677 + } + } + }, + "end_stability": { + "perfect": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": 13.86103831400402, + "dg": -6483.2686024541745, + "dh": -58700.0, + "ds": -168.3596046994868 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": 13.86103831400402, + "dg": -6483.2686024541745, + "dh": -58700.0, + "ds": -168.3596046994868 + } + }, + "partial": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": -12.481523331351923, + "dg": -4211.1286867678455, + "dh": -36900.0, + "ds": -105.39697344263149 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": -28.399120224383097, + "dg": -3760.617457849364, + "dh": -27900.0, + "ds": -77.83131562840767 + } + }, + "mismatched": { + "heterodimer": { + "structure_found": true, + "ascii_structure": null, + "tm": -43.0225498163484, + "dg": -2800.588686764743, + "dh": -24200.0, + "ds": -68.99697344264149 + }, + "end_stability": { + "structure_found": true, + "ascii_structure": null, + "tm": -65.97631797973199, + "dg": -1821.3862289277836, + "dh": -18900.0, + "ds": -55.065657814193834 + } + } + } + } +}