From 7bd4a298d15545d0c73026b96c1402ec799bef18 Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Mon, 16 Sep 2024 12:53:50 +1000 Subject: [PATCH 1/5] Add dump_hashes function to write sorted records to file or list. --- src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0c3afba..6240069 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,10 @@ use pyo3::prelude::*; use sourmash::encodings::HashFunctions; use sourmash::signature::SeqToHashes; +use pyo3::PyResult; +use std::fs::File; +use std::io::{BufWriter, Write}; + #[pyclass] struct KmerCountTable { counts: HashMap, @@ -119,10 +123,53 @@ impl KmerCountTable { // Default sort by count // Option sort kmers lexicographically - // TODO: Add method "dump_hash" - // Output tab delimited hash:count pairs - // Default sort by count - // Option sort on keys + /// Dump hash:count pairs, sorted by count (default) or by hash key. + /// + /// # Arguments + /// * `file` - Optional file path to write the output. If not provided, returns a list of tuples. + /// * `sortkeys` - Optional flag to sort by hash keys (default: False). + /// + /// By default, the records are sorted by count in ascending order. If two records have the same + /// count value, they are sorted by the hash value. If `sortkeys` is set to `True`, sorting is done + /// by the hash key instead. + #[pyo3(signature = (file=None, sortkeys=false))] + pub fn dump_hashes(&self, file: Option, sortkeys: bool) -> PyResult> { + let mut hash_count_pairs: Vec<(&u64, &u64)> = self.counts.iter().collect(); + + // Sort by count, with secondary sort by hash (default behavior) + if sortkeys { + // Sort by hash keys if `sortkeys` is set to true + hash_count_pairs.sort_by_key(|&(hash, _)| *hash); + } else { + // Default sorting by count, secondary sort by hash + hash_count_pairs.sort_by(|&(hash1, count1), &(hash2, count2)| { + count1.cmp(count2).then_with(|| hash1.cmp(hash2)) + }); + } + + // If a file is provided, write to the file + if let Some(filepath) = file { + let f = File::create(filepath)?; + let mut writer = BufWriter::new(f); + + // Write each hash:count pair to the file + for (hash, count) in hash_count_pairs { + writeln!(writer, "{}\t{}", hash, count)?; + } + + writer.flush()?; // Flush the buffer + Ok(vec![]) // Return empty vector to Python + } else { + // Convert the vector of references to owned values + let result: Vec<(u64, u64)> = hash_count_pairs + .into_iter() + .map(|(&hash, &count)| (hash, count)) + .collect(); + + // Return the vector of (hash, count) tuples + Ok(result) + } + } // TODO: Add method "histo" // Output frequency counts From b7f5805e9d2f75ab136734283f2c1ba89654066b Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Mon, 16 Sep 2024 12:54:11 +1000 Subject: [PATCH 2/5] Add tests for dump_hashes() --- src/python/tests/test_dump.py | 100 ++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/python/tests/test_dump.py diff --git a/src/python/tests/test_dump.py b/src/python/tests/test_dump.py new file mode 100644 index 0000000..3104da5 --- /dev/null +++ b/src/python/tests/test_dump.py @@ -0,0 +1,100 @@ +import pytest +import tempfile +from os import remove +from oxli import KmerCountTable + +@pytest.fixture +def kmer_count_table(): + """Fixture to set up a KmerCountTable instance with sample data.""" + kct = KmerCountTable(ksize=4) + kct.count("AAAA") # 17832910516274425539 + kct.count("TTTT") # 17832910516274425539 + kct.count("AATT") # 382727017318141683 + kct.count("GGGG") # 73459868045630124 + kct.count("GGGG") # 73459868045630124 + return kct + +@pytest.fixture +def empty_kmer_count_table(): + """Fixture to set up an empty KmerCountTable instance.""" + return KmerCountTable(ksize=4) + +def test_dump_hashes_return_vector(kmer_count_table): + """Test the dump_hashes function when not writing to a file. + + This test checks if the function returns the correct list of (hash, count) tuples. + """ + result = kmer_count_table.dump_hashes(file=None, sortkeys=False) + + # Expected output sorted by count then hash (default behavior) + expected = [ + (382727017318141683, 1), # 'AATT' + (73459868045630124, 2), # 'GGGG' + (17832910516274425539, 2) # 'AAAA'/'TTTT' + ] + + assert result == expected, f"Expected {expected}, but got {result}" + +def test_dump_hashes_write_to_file(kmer_count_table): + """Test the dump_hashes function when writing to a file. + + This test checks if the function correctly writes the hash:count pairs to a file. + """ + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file_path = temp_file.name + + kmer_count_table.dump_hashes(file=temp_file_path, sortkeys=False) + + with open(temp_file_path, 'r') as f: + lines = f.readlines() + + # Expected output sorted by count then hash (default behavior) + expected_lines = [ + f"{382727017318141683}\t1\n", # 'AATT' + f"{73459868045630124}\t2\n", # 'GGGG' + f"{17832910516274425539}\t2\n" # 'AAAA'/'TTTT' + ] + + assert lines == expected_lines, f"Expected {expected_lines}, but got {lines}" + + # Cleanup + remove(temp_file_path) + +def test_dump_hashes_sortkeys(kmer_count_table): + """Test the dump_hashes function with sortkeys=True. + + This test verifies if the function sorts by hash keys when `sortkeys` is set to True. + """ + result = kmer_count_table.dump_hashes(file=None, sortkeys=True) + + # Expected output sorted by hash key + expected = [ + (73459868045630124, 2), # 'GGGG' + (382727017318141683, 1), # 'AATT' + (17832910516274425539, 2) # 'AAAA'/'TTTT' + ] + + assert result == expected, f"Expected {expected}, but got {result}" + +def test_dump_hash_empty_table(empty_kmer_count_table): + """Test the dump_hashes function on an empty KmerCountTable. + + This test checks that the function handles an empty table correctly. + """ + # Test that calling dump_hashes without file returns an empty list + result = empty_kmer_count_table.dump_hashes(file=None, sortkeys=False) + assert result == [], "Expected an empty list from an empty KmerCountTable" + + # Test that calling dump_hashes with a file writes nothing to the file + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file_path = temp_file.name + + empty_kmer_count_table.dump_hashes(file=temp_file_path, sortkeys=False) + + with open(temp_file_path, 'r') as f: + lines = f.readlines() + + assert lines == [], "Expected an empty file for an empty KmerCountTable" + + # Cleanup + remove(temp_file_path) \ No newline at end of file From a490b33c33828a3be9aa63f62f0102c3ad7620ed Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Fri, 20 Sep 2024 18:58:56 +1000 Subject: [PATCH 3/5] update dump() tests --- src/python/tests/test_dump.py | 93 ++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/src/python/tests/test_dump.py b/src/python/tests/test_dump.py index 3104da5..bfa22cd 100644 --- a/src/python/tests/test_dump.py +++ b/src/python/tests/test_dump.py @@ -19,31 +19,59 @@ def empty_kmer_count_table(): """Fixture to set up an empty KmerCountTable instance.""" return KmerCountTable(ksize=4) -def test_dump_hashes_return_vector(kmer_count_table): - """Test the dump_hashes function when not writing to a file. +def test_dump_conflicting_sort_options(kmer_count_table): + """Test that passing both sortcounts=True and sortkeys=True raises a ValueError.""" + with pytest.raises(ValueError, match="Cannot sort by both counts and keys at the same time."): + kmer_count_table.dump(file=None, sortcounts=True, sortkeys=True) - This test checks if the function returns the correct list of (hash, count) tuples. - """ - result = kmer_count_table.dump_hashes(file=None, sortkeys=False) +def test_dump_no_sorting(kmer_count_table): + """Test the dump function with no sorting (both sortcounts and sortkeys are False).""" + result = kmer_count_table.dump(file=None, sortcounts=False, sortkeys=False) - # Expected output sorted by count then hash (default behavior) + # Expected output same order as for iterator + expected = list(kmer_count_table) + #[(17832910516274425539, 2), (382727017318141683, 1), (73459868045630124, 2)] + + assert result == expected, f"Expected {expected}, but got {result}" + + +def test_dump_sortcounts_with_ties(kmer_count_table): + """Test the dump function with sortcounts=True, ensuring it handles ties in counts.""" + result = kmer_count_table.dump(file=None, sortcounts=True, sortkeys=False) + + # Expected output sorted by count, with secondary sorting by hash for ties expected = [ (382727017318141683, 1), # 'AATT' - (73459868045630124, 2), # 'GGGG' + (73459868045630124, 2), # 'GGGG' (lower hash than 'AAAA') (17832910516274425539, 2) # 'AAAA'/'TTTT' ] assert result == expected, f"Expected {expected}, but got {result}" -def test_dump_hashes_write_to_file(kmer_count_table): - """Test the dump_hashes function when writing to a file. + +def test_dump_single_kmer(): + """Test the dump function with only a single k-mer counted.""" + kct = KmerCountTable(ksize=4) + kct.count("AAAA") # Hash for 'AAAA'/'TTTT' + + result = kct.dump(file=None, sortcounts=True, sortkeys=False) + + expected = [ + (17832910516274425539, 1) # 'AAAA'/'TTTT' + ] + + assert result == expected, f"Expected {expected}, but got {result}" + + +def test_dump_write_to_file(kmer_count_table): + """Test the dump function when writing to a file. This test checks if the function correctly writes the hash:count pairs to a file. """ with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file_path = temp_file.name - kmer_count_table.dump_hashes(file=temp_file_path, sortkeys=False) + kmer_count_table.dump(file=temp_file_path, sortcounts=True, sortkeys=False) with open(temp_file_path, 'r') as f: lines = f.readlines() @@ -60,12 +88,35 @@ def test_dump_hashes_write_to_file(kmer_count_table): # Cleanup remove(temp_file_path) -def test_dump_hashes_sortkeys(kmer_count_table): - """Test the dump_hashes function with sortkeys=True. +def test_dump_write_to_file_sortkeys(kmer_count_table): + """Test the dump function with sortkeys=True when writing to a file.""" + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file_path = temp_file.name + + kmer_count_table.dump(file=temp_file_path, sortkeys=True) + + with open(temp_file_path, 'r') as f: + lines = f.readlines() + + # Expected output sorted by hash keys + expected_lines = [ + f"{73459868045630124}\t2\n", # 'GGGG' + f"{382727017318141683}\t1\n", # 'AATT' + f"{17832910516274425539}\t2\n" # 'AAAA'/'TTTT' + ] + + assert lines == expected_lines, f"Expected {expected_lines}, but got {lines}" + + # Cleanup + remove(temp_file_path) + + +def test_dump_sortkeys(kmer_count_table): + """Test the dump function with sortkeys=True. This test verifies if the function sorts by hash keys when `sortkeys` is set to True. """ - result = kmer_count_table.dump_hashes(file=None, sortkeys=True) + result = kmer_count_table.dump(file=None, sortkeys=True) # Expected output sorted by hash key expected = [ @@ -76,20 +127,26 @@ def test_dump_hashes_sortkeys(kmer_count_table): assert result == expected, f"Expected {expected}, but got {result}" +def test_dump_invalid_file_path(kmer_count_table): + """Test that passing an invalid file path raises an error.""" + with pytest.raises(OSError): + kmer_count_table.dump(file="", sortkeys=True) + + def test_dump_hash_empty_table(empty_kmer_count_table): - """Test the dump_hashes function on an empty KmerCountTable. + """Test the dump function on an empty KmerCountTable. This test checks that the function handles an empty table correctly. """ - # Test that calling dump_hashes without file returns an empty list - result = empty_kmer_count_table.dump_hashes(file=None, sortkeys=False) + # Test that calling dump without file returns an empty list + result = empty_kmer_count_table.dump(file=None, sortkeys=False) assert result == [], "Expected an empty list from an empty KmerCountTable" - # Test that calling dump_hashes with a file writes nothing to the file + # Test that calling dump with a file writes nothing to the file with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file_path = temp_file.name - empty_kmer_count_table.dump_hashes(file=temp_file_path, sortkeys=False) + empty_kmer_count_table.dump(file=temp_file_path, sortkeys=False) with open(temp_file_path, 'r') as f: lines = f.readlines() From 207272d6809f830cd2dc0e6c9a4e7fce6ed0bf9c Mon Sep 17 00:00:00 2001 From: Adam Taranto Date: Fri, 20 Sep 2024 18:59:31 +1000 Subject: [PATCH 4/5] convert dump_hashes to dump, make sort optional. --- src/lib.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e918587..59108b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,34 +189,40 @@ impl KmerCountTable { // TODO: Static method to load KmerCountTable from serialized JSON. Yield new object. - // TODO: Add method "dump" - // Output tab delimited kmer:count pairs - // Default sort by count - // Option sort kmers lexicographically - - /// Dump hash:count pairs, sorted by count (default) or by hash key. + /// Dump (hash,count) pairs, optional sorted by count or hash key. /// /// # Arguments /// * `file` - Optional file path to write the output. If not provided, returns a list of tuples. /// * `sortkeys` - Optional flag to sort by hash keys (default: False). - /// - /// By default, the records are sorted by count in ascending order. If two records have the same - /// count value, they are sorted by the hash value. If `sortkeys` is set to `True`, sorting is done - /// by the hash key instead. - #[pyo3(signature = (file=None, sortkeys=false))] - pub fn dump_hashes(&self, file: Option, sortkeys: bool) -> PyResult> { + /// * `sortcounts` - Sort on counts, secondary sort on keys. (default: False). + #[pyo3(signature = (file=None, sortcounts=false, sortkeys=false))] + pub fn dump( + &self, + file: Option, + sortcounts: bool, + sortkeys: bool, + ) -> PyResult> { + // Raise an error if both sortcounts and sortkeys are true + if sortcounts && sortkeys { + return Err(PyValueError::new_err( + "Cannot sort by both counts and keys at the same time.", + )); + } + + // Collect hashes and counts let mut hash_count_pairs: Vec<(&u64, &u64)> = self.counts.iter().collect(); - // Sort by count, with secondary sort by hash (default behavior) + // Handle sorting based on the flags if sortkeys { // Sort by hash keys if `sortkeys` is set to true hash_count_pairs.sort_by_key(|&(hash, _)| *hash); - } else { - // Default sorting by count, secondary sort by hash + } else if sortcounts { + // Sort by count, secondary sort by hash if `sortcounts` is true hash_count_pairs.sort_by(|&(hash1, count1), &(hash2, count2)| { count1.cmp(count2).then_with(|| hash1.cmp(hash2)) }); } + // If both sortcounts and sortkeys are false, no sorting is done. // If a file is provided, write to the file if let Some(filepath) = file { From 08cd5273a8988aca3056607e69c9e54b34c9d500 Mon Sep 17 00:00:00 2001 From: Adamtaranto Date: Fri, 20 Sep 2024 09:00:00 +0000 Subject: [PATCH 5/5] Style fixes by Ruff --- src/python/tests/test_basic.py | 3 +- src/python/tests/test_dump.py | 92 +++++++++++++++++-------------- src/python/tests/test_kmer_map.py | 4 +- src/python/tests/test_output.py | 14 +++-- src/python/tests/test_setops.py | 1 + 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/src/python/tests/test_basic.py b/src/python/tests/test_basic.py index 97ed7eb..9cc6813 100644 --- a/src/python/tests/test_basic.py +++ b/src/python/tests/test_basic.py @@ -164,8 +164,9 @@ def test_get_hash_array(): ), "Hash array counts should match the counts of 'AAA' and 'AAC' and return zero for 'GGG'." assert rev_counts == [0, 1, 2], "Count should be in same order as input list" + def test_get_array(): """ Get vector of counts corresponding to vector of kmers. """ - pass \ No newline at end of file + pass diff --git a/src/python/tests/test_dump.py b/src/python/tests/test_dump.py index bfa22cd..b7fa478 100644 --- a/src/python/tests/test_dump.py +++ b/src/python/tests/test_dump.py @@ -3,6 +3,7 @@ from os import remove from oxli import KmerCountTable + @pytest.fixture def kmer_count_table(): """Fixture to set up a KmerCountTable instance with sample data.""" @@ -14,23 +15,28 @@ def kmer_count_table(): kct.count("GGGG") # 73459868045630124 return kct + @pytest.fixture def empty_kmer_count_table(): """Fixture to set up an empty KmerCountTable instance.""" return KmerCountTable(ksize=4) + def test_dump_conflicting_sort_options(kmer_count_table): """Test that passing both sortcounts=True and sortkeys=True raises a ValueError.""" - with pytest.raises(ValueError, match="Cannot sort by both counts and keys at the same time."): + with pytest.raises( + ValueError, match="Cannot sort by both counts and keys at the same time." + ): kmer_count_table.dump(file=None, sortcounts=True, sortkeys=True) + def test_dump_no_sorting(kmer_count_table): """Test the dump function with no sorting (both sortcounts and sortkeys are False).""" result = kmer_count_table.dump(file=None, sortcounts=False, sortkeys=False) - + # Expected output same order as for iterator - expected = list(kmer_count_table) - #[(17832910516274425539, 2), (382727017318141683, 1), (73459868045630124, 2)] + expected = list(kmer_count_table) + # [(17832910516274425539, 2), (382727017318141683, 1), (73459868045630124, 2)] assert result == expected, f"Expected {expected}, but got {result}" @@ -38,14 +44,14 @@ def test_dump_no_sorting(kmer_count_table): def test_dump_sortcounts_with_ties(kmer_count_table): """Test the dump function with sortcounts=True, ensuring it handles ties in counts.""" result = kmer_count_table.dump(file=None, sortcounts=True, sortkeys=False) - + # Expected output sorted by count, with secondary sorting by hash for ties expected = [ - (382727017318141683, 1), # 'AATT' - (73459868045630124, 2), # 'GGGG' (lower hash than 'AAAA') - (17832910516274425539, 2) # 'AAAA'/'TTTT' + (382727017318141683, 1), # 'AATT' + (73459868045630124, 2), # 'GGGG' (lower hash than 'AAAA') + (17832910516274425539, 2), # 'AAAA'/'TTTT' ] - + assert result == expected, f"Expected {expected}, but got {result}" @@ -53,13 +59,13 @@ def test_dump_single_kmer(): """Test the dump function with only a single k-mer counted.""" kct = KmerCountTable(ksize=4) kct.count("AAAA") # Hash for 'AAAA'/'TTTT' - + result = kct.dump(file=None, sortcounts=True, sortkeys=False) - + expected = [ (17832910516274425539, 1) # 'AAAA'/'TTTT' ] - + assert result == expected, f"Expected {expected}, but got {result}" @@ -70,43 +76,44 @@ def test_dump_write_to_file(kmer_count_table): """ with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file_path = temp_file.name - + kmer_count_table.dump(file=temp_file_path, sortcounts=True, sortkeys=False) - - with open(temp_file_path, 'r') as f: + + with open(temp_file_path, "r") as f: lines = f.readlines() - + # Expected output sorted by count then hash (default behavior) expected_lines = [ - f"{382727017318141683}\t1\n", # 'AATT' - f"{73459868045630124}\t2\n", # 'GGGG' - f"{17832910516274425539}\t2\n" # 'AAAA'/'TTTT' + f"{382727017318141683}\t1\n", # 'AATT' + f"{73459868045630124}\t2\n", # 'GGGG' + f"{17832910516274425539}\t2\n", # 'AAAA'/'TTTT' ] - + assert lines == expected_lines, f"Expected {expected_lines}, but got {lines}" - + # Cleanup remove(temp_file_path) + def test_dump_write_to_file_sortkeys(kmer_count_table): """Test the dump function with sortkeys=True when writing to a file.""" with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file_path = temp_file.name - + kmer_count_table.dump(file=temp_file_path, sortkeys=True) - - with open(temp_file_path, 'r') as f: + + with open(temp_file_path, "r") as f: lines = f.readlines() - + # Expected output sorted by hash keys expected_lines = [ - f"{73459868045630124}\t2\n", # 'GGGG' - f"{382727017318141683}\t1\n", # 'AATT' - f"{17832910516274425539}\t2\n" # 'AAAA'/'TTTT' + f"{73459868045630124}\t2\n", # 'GGGG' + f"{382727017318141683}\t1\n", # 'AATT' + f"{17832910516274425539}\t2\n", # 'AAAA'/'TTTT' ] - + assert lines == expected_lines, f"Expected {expected_lines}, but got {lines}" - + # Cleanup remove(temp_file_path) @@ -117,16 +124,17 @@ def test_dump_sortkeys(kmer_count_table): This test verifies if the function sorts by hash keys when `sortkeys` is set to True. """ result = kmer_count_table.dump(file=None, sortkeys=True) - + # Expected output sorted by hash key expected = [ - (73459868045630124, 2), # 'GGGG' - (382727017318141683, 1), # 'AATT' - (17832910516274425539, 2) # 'AAAA'/'TTTT' + (73459868045630124, 2), # 'GGGG' + (382727017318141683, 1), # 'AATT' + (17832910516274425539, 2), # 'AAAA'/'TTTT' ] - + assert result == expected, f"Expected {expected}, but got {result}" + def test_dump_invalid_file_path(kmer_count_table): """Test that passing an invalid file path raises an error.""" with pytest.raises(OSError): @@ -141,17 +149,17 @@ def test_dump_hash_empty_table(empty_kmer_count_table): # Test that calling dump without file returns an empty list result = empty_kmer_count_table.dump(file=None, sortkeys=False) assert result == [], "Expected an empty list from an empty KmerCountTable" - + # Test that calling dump with a file writes nothing to the file with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file_path = temp_file.name - + empty_kmer_count_table.dump(file=temp_file_path, sortkeys=False) - - with open(temp_file_path, 'r') as f: + + with open(temp_file_path, "r") as f: lines = f.readlines() - + assert lines == [], "Expected an empty file for an empty KmerCountTable" - + # Cleanup - remove(temp_file_path) \ No newline at end of file + remove(temp_file_path) diff --git a/src/python/tests/test_kmer_map.py b/src/python/tests/test_kmer_map.py index f4bf376..e894d18 100644 --- a/src/python/tests/test_kmer_map.py +++ b/src/python/tests/test_kmer_map.py @@ -4,5 +4,5 @@ def test_kmermap(): - '''Test option to add kmermap''' - pass \ No newline at end of file + """Test option to add kmermap""" + pass diff --git a/src/python/tests/test_output.py b/src/python/tests/test_output.py index 8394602..382b8a3 100644 --- a/src/python/tests/test_output.py +++ b/src/python/tests/test_output.py @@ -4,21 +4,25 @@ def test_serialise(): - '''Serialise object to JSON ''' + """Serialise object to JSON""" pass + def test_deserialise(): - '''Load object from file.''' + """Load object from file.""" pass + def test_dump(): - '''Write tab delimited kmer:count pairs''' + """Write tab delimited kmer:count pairs""" pass + def test_dump_hash(): - '''Write tab delimited hash_count pairs ''' + """Write tab delimited hash_count pairs""" pass + def test_histo(): - '''Write frequency counts.''' + """Write frequency counts.""" pass diff --git a/src/python/tests/test_setops.py b/src/python/tests/test_setops.py index df71ee3..a332faf 100644 --- a/src/python/tests/test_setops.py +++ b/src/python/tests/test_setops.py @@ -3,6 +3,7 @@ from test_basic import create_sample_kmer_table + # Set operations def test_union(): table1 = create_sample_kmer_table(3, ["AAA", "AAC"])