krakenparser 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ from .kpplot.stackedbar import stacked_barplot
2
+ from .kpplot.streamgraph import streamgraph
3
+ from .kpplot.clustermap import clustermap
4
+
5
+ __all__ = [
6
+ "stacked_barplot",
7
+ "streamgraph",
8
+ "clustermap",
9
+ ]
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python
2
+
3
+ import pandas as pd
4
+ import numpy as np
5
+ import sys
6
+ import shutil
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+
11
+ # Define Shannon index
12
+ def shannon_index(counts):
13
+ counts = np.array(counts)
14
+ counts = counts[counts > 0]
15
+ proportions = counts / counts.sum()
16
+ return -np.sum(proportions * np.log(proportions))
17
+
18
+
19
+ # Define Pielou's evenness
20
+ def pielou_evenness(counts):
21
+ counts = np.array(counts)
22
+ counts = counts[counts > 0]
23
+ H = shannon_index(counts)
24
+ S = len(counts)
25
+ return H / np.log(S) if S > 1 else 0
26
+
27
+
28
+ # Define Chao1 richness estimator
29
+ def chao1_index(counts):
30
+ counts = np.array(counts)
31
+ S_obs = np.sum(counts > 0)
32
+ F1 = np.sum(counts == 1)
33
+ F2 = np.sum(counts == 2)
34
+ if F2 == 0:
35
+ return S_obs + F1 * (F1 - 1) / 2
36
+ return S_obs + (F1 * F1) / (2 * F2)
37
+
38
+
39
+ def calc_alpha_div(source_file, destination_file):
40
+ df = pd.read_csv(source_file, index_col=0)
41
+
42
+ results = []
43
+ for sample_id, row in df.iterrows():
44
+ counts = row.values
45
+ results.append(
46
+ {
47
+ "Sample": sample_id,
48
+ "Shannon": shannon_index(counts),
49
+ "Pielou": pielou_evenness(counts),
50
+ "Chao1": chao1_index(counts),
51
+ }
52
+ )
53
+
54
+ alpha_div_df = pd.DataFrame(results).set_index("Sample")
55
+ alpha_div_df.to_csv(destination_file)
56
+
57
+ # Get the path to the current directory (same location as the script)
58
+ current_dir = Path(__file__).resolve().parent
59
+ pycache_dir = current_dir / "__pycache__"
60
+
61
+ # Check if __pycache__ exists and remove it
62
+ if pycache_dir.exists() and pycache_dir.is_dir():
63
+ shutil.rmtree(pycache_dir)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ # Use argparse to parse command-line arguments
68
+ parser = argparse.ArgumentParser(description="Calculates α-diversity per sample.")
69
+ parser.add_argument(
70
+ "-i",
71
+ "--input",
72
+ required=True,
73
+ help="Path to the source file (total abundance on species level).",
74
+ )
75
+ parser.add_argument(
76
+ "-o",
77
+ "--output",
78
+ required=True,
79
+ help="Path to the destination file.",
80
+ )
81
+
82
+ args = parser.parse_args()
83
+
84
+ # Call the function with parsed arguments
85
+ calc_alpha_div(args.input, args.output)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python
2
+
3
+ import pandas as pd
4
+ import numpy as np
5
+ import sys
6
+ import shutil
7
+ import argparse
8
+ from pathlib import Path
9
+ from skbio.diversity import beta_diversity
10
+ from skbio.stats.subsample import subsample_counts
11
+
12
+
13
+ def calc_beta_div(source_file, output_prefix, rarefaction_depth=1000):
14
+ df = pd.read_csv(source_file, index_col=0)
15
+
16
+ # Rarefy samples
17
+ rarefied_counts = []
18
+ sample_ids = []
19
+
20
+ for sample, row in df.iterrows():
21
+ counts = row.values.astype(int)
22
+ if counts.sum() >= rarefaction_depth:
23
+ rarefied = subsample_counts(counts, n=rarefaction_depth)
24
+ rarefied_counts.append(rarefied)
25
+ sample_ids.append(sample)
26
+
27
+ if len(rarefied_counts) < 2:
28
+ raise ValueError("Not enough samples passed the rarefaction threshold.")
29
+
30
+ # Compute Bray-Curtis and Jaccard
31
+ bray_dm = beta_diversity("braycurtis", rarefied_counts, ids=sample_ids)
32
+ jaccard_dm = beta_diversity("jaccard", rarefied_counts, ids=sample_ids)
33
+
34
+ # Save to CSV
35
+ bray_df = bray_dm.to_data_frame()
36
+ jaccard_df = jaccard_dm.to_data_frame()
37
+
38
+ bray_df.to_csv(f"{output_prefix}_braycurtis.csv")
39
+ jaccard_df.to_csv(f"{output_prefix}_jaccard.csv")
40
+
41
+ # Clean up __pycache__
42
+ current_dir = Path(__file__).resolve().parent
43
+ pycache_dir = current_dir / "__pycache__"
44
+ if pycache_dir.exists() and pycache_dir.is_dir():
45
+ shutil.rmtree(pycache_dir)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ parser = argparse.ArgumentParser(
50
+ description="Calculate β-diversity (Bray-Curtis and Jaccard) with rarefaction."
51
+ )
52
+ parser.add_argument(
53
+ "-i", "--input", required=True, help="Input CSV count table (samples as rows)."
54
+ )
55
+ parser.add_argument(
56
+ "-o", "--output", required=True, help="Output prefix for distance matrices."
57
+ )
58
+ parser.add_argument(
59
+ "-d",
60
+ "--depth",
61
+ type=int,
62
+ default=1000,
63
+ help="Rarefaction depth (default: 1000).",
64
+ )
65
+ args = parser.parse_args()
66
+
67
+ calc_beta_div(args.input, args.output, args.depth)
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python
2
+ ####################################################################
3
+ #combine_mpa.py converts multiple outputs from kreport2mpa.py
4
+ #Copyright (C) 2020 Jennifer Lu, jennifer.lu717@gmail.com
5
+
6
+ #This file is part of KrakenTools.
7
+ #KrakenTools is free software; you can redistribute it and/or modify
8
+ #it under the terms of the GNU General Public License as published by
9
+ #the Free Software Foundation; either version 3 of the license, or
10
+ #(at your option) any later version.
11
+
12
+ #This program is distributed in the hope that it will be useful,
13
+ #but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ #GNU General Public License for more details.
16
+
17
+ #You should have received a copy of the GNU General Public License
18
+ #along with this program; if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ ####################################################################
21
+ #Jennifer Lu, jlu26@jhmi.edu
22
+ #Updated: 07/12/2020
23
+ #
24
+ #This program reads multiple files in the
25
+ #an mpa-format (MetaPhlAn) style report (as output from kreport2mpa.py).
26
+ #Each line represents a possible taxon classification. The first column is lists the
27
+ #domain, kingdom, phyla, etc, leading up to each taxon.
28
+ #The levels are separated by the | delimiter, with the type of
29
+ #level specified before each name with a single letter and underscore
30
+ #(d_ for domain, k_ for kingdom, etc).
31
+ #The second column is the number of reads classified within
32
+ #that taxon's subtree.
33
+ #
34
+ #Input files:
35
+ # - MetaPhlAn format (mpa-format) files with two columns
36
+ # - All files must be generated from the same database, with the same
37
+ # options from kreport2krona.py or errors may occur
38
+ #
39
+ #Input Parameters to Specify [OPTIONAL]:
40
+ # - header_line = prints a header line in mpa-report
41
+ # [Default: no header]
42
+ # - intermediate-ranks = includes non-traditional taxon levels
43
+ # (traditional levels: domain, kingdom, phylum, class, order,
44
+ # family, genus, species)
45
+ # [Default: no intermediate ranks]
46
+ #Output file format (tab-delimited)
47
+ # - Taxonomy tree levels |-delimited, with level type [d,k,p,c,o,f,g,s,x]
48
+ # - Number of reads within subtree of the specified level
49
+ #
50
+ #Methods
51
+ # - main
52
+ #
53
+ import os, sys, argparse
54
+
55
+ #Main method
56
+ def main():
57
+ #Parse arguments
58
+ parser = argparse.ArgumentParser()
59
+ parser.add_argument('-i', '--input', required=True,
60
+ nargs='+', dest='in_files',
61
+ help='Input files for this program (files generated by kreport2mpa.py)')
62
+ parser.add_argument('-o', '--output', required=True,
63
+ dest='o_file', help='Single mpa-report file name')
64
+ args=parser.parse_args()
65
+
66
+ #Process each file
67
+ samples = {} #Map number to name
68
+ sample_count = 0
69
+ values = {} #Map taxon tree to sample to number
70
+ parent2child = {}
71
+ toparse = []
72
+ sys.stdout.write(" Number of files to parse: %i\n" % len(args.in_files))
73
+ for in_file in args.in_files:
74
+ i_file = open(in_file,'r')
75
+ sample_count += 1
76
+ sample_name = "Sample #" + str(sample_count)
77
+ for line in i_file:
78
+ #Check for header line
79
+ if line[0] == "#":
80
+ sample_name = line.strip().split('\t')[-1]
81
+ continue
82
+ #Otherwise
83
+ [classification, val] = line.strip().split('\t')
84
+ #Check for parents
85
+ split_vals = classification.split("|")
86
+ curr_parent = ''
87
+ for i in range(0,len(split_vals)):
88
+ test_val = "|".join(split_vals[0:i])
89
+ if test_val in values:
90
+ curr_parent = test_val
91
+ #No parent
92
+ if curr_parent == '':
93
+ if classification not in values:
94
+ toparse.append(classification)
95
+ #Most specific parent found
96
+ if curr_parent != '':
97
+ if curr_parent not in parent2child:
98
+ parent2child[curr_parent] = []
99
+ if classification not in parent2child[curr_parent]:
100
+ parent2child[curr_parent].append(classification)
101
+ #Save classification to value map
102
+ if classification not in values:
103
+ values[classification] = {}
104
+ values[classification][sample_count] = val
105
+ #Save sample name
106
+ samples[sample_count] = sample_name
107
+
108
+ sys.stdout.write(" Number of classifications to write: %i\n" % len(values))
109
+ sys.stdout.write("\t%i classifications printed" % 0)
110
+ #Write header
111
+ o_file = open(args.o_file, 'w')
112
+ o_file.write("#Classification")
113
+ for i in range(1, sample_count+1):
114
+ o_file.write("\t" + samples[i])
115
+ o_file.write("\n")
116
+
117
+ #Write each line
118
+ parsed = {}
119
+ count_c = 0
120
+ while len(toparse) > 0:
121
+ curr_c = toparse.pop(0)
122
+ #Add all children to stack
123
+ if curr_c in parent2child:
124
+ for child in parent2child[curr_c]:
125
+ toparse.insert(0, child)
126
+ #For the current classification, print per sample
127
+ o_file.write(curr_c)
128
+ for i in range(1,sample_count + 1):
129
+ if i in values[curr_c]:
130
+ o_file.write("\t" + values[curr_c][i])
131
+ else:
132
+ o_file.write("\t0")
133
+ o_file.write("\n")
134
+ count_c += 1
135
+ sys.stdout.write("\r\t%i classifications printed" % count_c)
136
+ sys.stdout.flush()
137
+ o_file.close()
138
+ sys.stdout.write("\r\t%i classifications printed\n" % count_c)
139
+ sys.stdout.flush()
140
+
141
+ if __name__ == "__main__":
142
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ import argparse
6
+ import pandas as pd
7
+
8
+
9
+ def convert_to_csv(input_file, output_file):
10
+ # Read the entire file into a DataFrame
11
+ data = pd.read_csv(input_file, sep="\t", header=None)
12
+
13
+ # Set the first row as the header
14
+ data.columns = data.iloc[0]
15
+ data = data.drop(data.index[0])
16
+
17
+ # Transpose the DataFrame so that sample names become rows and microbial taxa with their abundance become columns
18
+ data_transposed = data.T
19
+ data_transposed.columns = data_transposed.iloc[0]
20
+ data_transposed = data_transposed.drop(data_transposed.index[0])
21
+
22
+ # Save the transposed data to a new CSV file
23
+ data_transposed.to_csv(output_file, index_label="Sample_id")
24
+ print(f"Data has been successfully converted and saved as '{output_file}'.")
25
+
26
+ # Remove pycache if it exists
27
+ current_dir = Path(__file__).resolve().parent
28
+ pycache_dir = current_dir / "__pycache__"
29
+ if pycache_dir.exists() and pycache_dir.is_dir():
30
+ shutil.rmtree(pycache_dir)
31
+
32
+
33
+ if __name__ == "__main__":
34
+ # Use argparse to handle command-line arguments
35
+ parser = argparse.ArgumentParser(
36
+ description="Reads a TXT file, reorganizes the data, and converts it into a CSV file."
37
+ )
38
+ parser.add_argument(
39
+ "-i",
40
+ "--input",
41
+ required=True,
42
+ help="Path to the input TXT file. This file should contain sample names in columns and microbial taxa in rows.",
43
+ )
44
+ parser.add_argument(
45
+ "-o",
46
+ "--output",
47
+ required=True,
48
+ help="Path to the output CSV file. The script will restructure the data and save it here.",
49
+ )
50
+
51
+ args = parser.parse_args()
52
+
53
+ # Call function with parsed arguments
54
+ convert_to_csv(args.input, args.output)
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+
3
+ # Function to display detailed usage information
4
+ usage() {
5
+ echo "Usage: $(basename "$0") -i PATH_TO_SOURCE_FILE -o PATH_TO_DESTINATION"
6
+ echo
7
+ echo " -i, --input PATH_TO_SOURCE_FILE Path to the Combined MPA input file to be processed."
8
+ echo " -o, --output PATH_TO_DESTINATION Path to the directory where processed output files will be stored."
9
+ echo " -h, --help Display this help message and exit."
10
+ echo
11
+ echo "Description:"
12
+ echo " This script processes a combined mpa file by extracting different taxonomic levels"
13
+ echo " (species, genus, family, order, class, and phylum) and saving the results as separate text files."
14
+ echo " Additionally, it removes human-related sequences to improve data accuracy."
15
+ echo
16
+ echo "Processing Details:"
17
+ echo " - Extracts taxonomic levels using 'grep' with specific patterns."
18
+ echo " - Filters out undesired taxonomic entries such as:"
19
+ echo " - Species: Homo sapiens"
20
+ echo " - Genus: Homo"
21
+ echo " - Family: Hominidae"
22
+ echo " - Order: Primates"
23
+ echo " - Class: Mammalia"
24
+ echo " - Phylum: Chordata"
25
+ echo " - Outputs are stored as text files inside the specified destination directory."
26
+ exit 0
27
+ }
28
+
29
+ # Initialize variables
30
+ SOURCE_FILE=""
31
+ DESTINATION_DIR=""
32
+
33
+ # Parse command-line arguments
34
+ while [[ "$#" -gt 0 ]]; do
35
+ case "$1" in
36
+ -i|--input) SOURCE_FILE="$2"; shift 2 ;;
37
+ -o|--output) DESTINATION_DIR="$2"; shift 2 ;;
38
+ -h|--help) usage ;;
39
+ *) echo "Error: Unknown option $1"; usage ;;
40
+ esac
41
+ done
42
+
43
+ # Check if required arguments are provided
44
+ if [[ -z "$SOURCE_FILE" || -z "$DESTINATION_DIR" ]]; then
45
+ echo "Error: Both input (-i) and output (-o) paths are required."
46
+ usage
47
+ fi
48
+
49
+ # Check if source file exists
50
+ if [[ ! -f "$SOURCE_FILE" ]]; then
51
+ echo "Error: Input file '$SOURCE_FILE' not found!"
52
+ exit 1
53
+ fi
54
+
55
+ # Create destination directories
56
+ mkdir -p "${DESTINATION_DIR}/txt"
57
+ mkdir -p "${DESTINATION_DIR}/csv"
58
+
59
+ # Process input file and generate output files
60
+ grep -E "s__" "${SOURCE_FILE}" \
61
+ | grep -v "t__" \
62
+ | grep -v "s__Homo_sapiens" \
63
+ | sed "s/^.*|//g" \
64
+ | sed "s/SRS[0-9]*-//g" \
65
+ > "${DESTINATION_DIR}/txt/counts_species.txt"
66
+
67
+ grep -E "g__" "${SOURCE_FILE}" \
68
+ | grep -v "t__" \
69
+ | grep -v "s__" \
70
+ | grep -v "g__Homo" \
71
+ | sed "s/^.*|//g" \
72
+ | sed "s/SRS[0-9]*-//g" \
73
+ > "${DESTINATION_DIR}/txt/counts_genus.txt"
74
+
75
+ grep -E "f__" "${SOURCE_FILE}" \
76
+ | grep -v "t__" \
77
+ | grep -v "s__" \
78
+ | grep -v "g__" \
79
+ | grep -v "f__Hominidae" \
80
+ | sed "s/^.*|//g" \
81
+ | sed "s/SRS[0-9]*-//g" \
82
+ > "${DESTINATION_DIR}/txt/counts_family.txt"
83
+
84
+ grep -E "o__" "${SOURCE_FILE}" \
85
+ | grep -v "t__" \
86
+ | grep -v "s__" \
87
+ | grep -v "g__" \
88
+ | grep -v "f__" \
89
+ | grep -v "o__Primates" \
90
+ | sed "s/^.*|//g" \
91
+ | sed "s/SRS[0-9]*-//g" \
92
+ > "${DESTINATION_DIR}/txt/counts_order.txt"
93
+
94
+ grep -E "c__" "${SOURCE_FILE}" \
95
+ | grep -v "t__" \
96
+ | grep -v "s__" \
97
+ | grep -v "g__" \
98
+ | grep -v "f__" \
99
+ | grep -v "o__" \
100
+ | grep -v "c__Mammalia" \
101
+ | sed "s/^.*|//g" \
102
+ | sed "s/SRS[0-9]*-//g" \
103
+ > "${DESTINATION_DIR}/txt/counts_class.txt"
104
+
105
+ grep -E "p__" "${SOURCE_FILE}" \
106
+ | grep -v "t__" \
107
+ | grep -v "s__" \
108
+ | grep -v "g__" \
109
+ | grep -v "f__" \
110
+ | grep -v "o__" \
111
+ | grep -v "c__" \
112
+ | grep -v "p__Chordata" \
113
+ | sed "s/^.*|//g" \
114
+ | sed "s/SRS[0-9]*-//g" \
115
+ > "${DESTINATION_DIR}/txt/counts_phylum.txt"
116
+
117
+ # Check for errors
118
+ if [ $? -ne 0 ]; then
119
+ echo "Error: Failed to run decombine.sh"
120
+ exit 1
121
+ fi
122
+
123
+ echo "MPA file decombined successfully. Output stored in $DESTINATION_DIR"
@@ -0,0 +1,111 @@
1
+ #!/bin/bash
2
+
3
+ # Function to display detailed usage information
4
+ usage() {
5
+ echo "Usage: $(basename "$0") -i PATH_TO_SOURCE_FILE -o PATH_TO_DESTINATION"
6
+ echo
7
+ echo " -i, --input PATH_TO_SOURCE_FILE Path to the Combined MPA input file to be processed."
8
+ echo " -o, --output PATH_TO_DESTINATION Path to the directory where processed output files will be stored."
9
+ echo " -h, --help Display this help message and exit."
10
+ echo
11
+ echo "Description:"
12
+ echo " This script processes a combined mpa file by extracting different taxonomic levels using only VIRUSES domain"
13
+ echo " (species, genus, family, order, class, and phylum) and saving the results as separate text files."
14
+ echo
15
+ echo "Processing Details:"
16
+ echo " - Extracts taxonomic levels only on VIRUSES domain using 'grep' with specific patterns."
17
+ echo " - Outputs are stored as text files inside the specified destination directory."
18
+ exit 0
19
+ }
20
+
21
+ # Initialize variables
22
+ SOURCE_FILE=""
23
+ DESTINATION_DIR=""
24
+
25
+ # Parse command-line arguments
26
+ while [[ "$#" -gt 0 ]]; do
27
+ case "$1" in
28
+ -i|--input) SOURCE_FILE="$2"; shift 2 ;;
29
+ -o|--output) DESTINATION_DIR="$2"; shift 2 ;;
30
+ -h|--help) usage ;;
31
+ *) echo "Error: Unknown option $1"; usage ;;
32
+ esac
33
+ done
34
+
35
+ # Check if required arguments are provided
36
+ if [[ -z "$SOURCE_FILE" || -z "$DESTINATION_DIR" ]]; then
37
+ echo "Error: Both input (-i) and output (-o) paths are required."
38
+ usage
39
+ fi
40
+
41
+ # Check if source file exists
42
+ if [[ ! -f "$SOURCE_FILE" ]]; then
43
+ echo "Error: Input file '$SOURCE_FILE' not found!"
44
+ exit 1
45
+ fi
46
+
47
+ # Create destination directories
48
+ mkdir -p "${DESTINATION_DIR}/txt"
49
+ mkdir -p "${DESTINATION_DIR}/csv"
50
+
51
+ VIRUSES_BUFFER=$(grep -E "d__Viruses" "${SOURCE_FILE}")
52
+
53
+ # Process input file and generate output files
54
+ echo "$VIRUSES_BUFFER" | grep -E "s__" \
55
+ | grep -v "t__" \
56
+ | sed "s/^.*|//g" \
57
+ | sed "s/SRS[0-9]*-//g" \
58
+ > "${DESTINATION_DIR}/txt/counts_species.txt"
59
+
60
+ echo "$VIRUSES_BUFFER" | grep -E "g__" \
61
+ | grep -v "t__" \
62
+ | grep -v "s__" \
63
+ | sed "s/^.*|//g" \
64
+ | sed "s/SRS[0-9]*-//g" \
65
+ > "${DESTINATION_DIR}/txt/counts_genus.txt"
66
+
67
+ echo "$VIRUSES_BUFFER" | grep -E "f__" \
68
+ | grep -v "t__" \
69
+ | grep -v "s__" \
70
+ | grep -v "g__" \
71
+ | sed "s/^.*|//g" \
72
+ | sed "s/SRS[0-9]*-//g" \
73
+ > "${DESTINATION_DIR}/txt/counts_family.txt"
74
+
75
+ echo "$VIRUSES_BUFFER" | grep -E "o__" \
76
+ | grep -v "t__" \
77
+ | grep -v "s__" \
78
+ | grep -v "g__" \
79
+ | grep -v "f__" \
80
+ | sed "s/^.*|//g" \
81
+ | sed "s/SRS[0-9]*-//g" \
82
+ > "${DESTINATION_DIR}/txt/counts_order.txt"
83
+
84
+ echo "$VIRUSES_BUFFER" | grep -E "c__" \
85
+ | grep -v "t__" \
86
+ | grep -v "s__" \
87
+ | grep -v "g__" \
88
+ | grep -v "f__" \
89
+ | grep -v "o__" \
90
+ | sed "s/^.*|//g" \
91
+ | sed "s/SRS[0-9]*-//g" \
92
+ > "${DESTINATION_DIR}/txt/counts_class.txt"
93
+
94
+ echo "$VIRUSES_BUFFER" | grep -E "p__" \
95
+ | grep -v "t__" \
96
+ | grep -v "s__" \
97
+ | grep -v "g__" \
98
+ | grep -v "f__" \
99
+ | grep -v "o__" \
100
+ | grep -v "c__" \
101
+ | sed "s/^.*|//g" \
102
+ | sed "s/SRS[0-9]*-//g" \
103
+ > "${DESTINATION_DIR}/txt/counts_phylum.txt"
104
+
105
+ # Check for errors
106
+ if [ $? -ne 0 ]; then
107
+ echo "Error: Failed to run decombine_viruses.sh"
108
+ exit 1
109
+ fi
110
+
111
+ echo "MPA file decombined successfully. Output stored in $DESTINATION_DIR"