Skip to content

Commit 7120011

Browse files
committed
update the coverage calculation method
1 parent ffefe40 commit 7120011

6 files changed

Lines changed: 76 additions & 97 deletions

File tree

frontend/src/modules/analysis/analysis-data/analysis-data.component.tsx

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
1010
const [coverageData, setCoverageData] = useState<any[]>([]);
1111
const [listenerRunning, setListenerRunning] = useState(false);
1212
const [error, setError] = useState("");
13-
const [metric, setMetric] = useState<'avg_depth' | 'breadth'>('avg_depth');
13+
const [metric, setMetric] = useState<'fold_coverage'>('fold_coverage');
1414
const [showConfirmModal, setShowConfirmModal] = useState(false);
1515

16+
// Extract threshold from the first query, default to 100 if invalid
17+
const threshold = data.data.queries[0]?.threshold ? parseFloat(data.data.queries[0].threshold) : 100;
18+
1619
useEffect(() => {
1720
socket.emit('check_fastq_file_listener', { projectId: analysisData.data.projectId });
1821

@@ -72,29 +75,27 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
7275
const formatCoverageData = () => {
7376
const refs = [...new Set(coverageData.map(d => d.reference))];
7477
const times = [...new Set(coverageData.map(d => d.timestamp))].sort();
75-
const startTime = new Date(times[0]).getTime(); // Earliest timestamp in milliseconds
76-
77-
// Define the header with column types and roles
78-
const header = [{ type: 'number', label: 'Elapsed Time (s)' }];
78+
const startTime = new Date(times[0]).getTime();
79+
80+
const header = [{ type: 'number', label: 'Elapsed Time (s)', role: '' }];
7981
refs.forEach(ref => {
80-
header.push({ type: 'number', label: ref });
81-
header.push({ type: 'string', label: '' }); // Tooltip column, no 'role' property
82+
header.push({ type: 'number', label: ref, role: '' });
83+
header.push({ type: 'string', label: 'for', role: 'tooltip' });
8284
});
83-
84-
// Create data rows with elapsed time and tooltips
85+
8586
const rows = times.map(time => {
86-
const elapsedSeconds = (new Date(time).getTime() - startTime) / 1000; // Convert to seconds
87-
const row: any[] = [elapsedSeconds];
87+
const elapsedSeconds = (new Date(time).getTime() - startTime) / 1000;
88+
const row: (number | string)[] = [elapsedSeconds];
8889
refs.forEach(ref => {
8990
const entry = coverageData.find(d => d.timestamp === time && d.reference === ref);
90-
const y = entry ? entry[metric] : 0;
91-
const tooltip = `Time: ${time}\n${ref}: ${y.toFixed(2)}`;
91+
const y = entry ? entry.fold_coverage : 0;
92+
const tooltip = `Time: ${time}\n${ref}: ${y.toFixed(2)}x`;
9293
row.push(y);
9394
row.push(tooltip);
9495
});
9596
return row;
9697
});
97-
98+
9899
return [header, ...rows];
99100
};
100101

@@ -136,6 +137,23 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
136137
setShowConfirmModal(false);
137138
};
138139

140+
// Compute number of references for series indexing
141+
const refs = [...new Set(coverageData.map(d => d.reference))];
142+
const numRefs = refs.length;
143+
144+
const chartOptions = {
145+
title: 'Fold Coverage Over Time',
146+
hAxis: { title: 'Elapsed Time (s)' },
147+
vAxis: { title: 'Fold Coverage (x)', minValue: 0 },
148+
legend: { position: 'bottom' },
149+
colors: ['#00B0BD', '#004E5A', '#FF6A45', '#27AE60'],
150+
chartArea: { width: '80%', height: '70%' },
151+
animation: { startup: true, duration: 1000, easing: 'out' },
152+
series: !isNaN(threshold) ? {
153+
[numRefs]: { lineDashStyle: [4, 4], color: 'red', lineWidth: 2 }
154+
} : {}
155+
};
156+
139157
return (
140158
<div className="nano-analysis-container">
141159
<h2 className="nano-section-title">Analysis Dashboard</h2>
@@ -188,10 +206,10 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
188206
<h3 className="nano-card-title">Coverage Over Time</h3>
189207
<select
190208
value={metric}
191-
onChange={(e) => setMetric(e.target.value as 'avg_depth' | 'breadth')}
209+
onChange={(e) => setMetric(e.target.value as 'fold_coverage')}
192210
className="form-control w-auto d-inline-block ml-2"
193211
>
194-
<option value="avg_depth">Average Depth</option>
212+
<option value="fold_coverage">Average Depth</option>
195213
<option value="breadth">Breadth of Coverage (%)</option>
196214
</select>
197215
</div>
@@ -200,19 +218,7 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
200218
<Chart
201219
chartType="LineChart"
202220
data={formatCoverageData()}
203-
options={{
204-
title: metric === 'avg_depth' ? 'Average Coverage Depth Over Time' : 'Breadth of Coverage Over Time',
205-
hAxis: { title: 'Elapsed Time (s)' },
206-
vAxis: {
207-
title: metric === 'avg_depth' ? 'Average Depth (reads/position)' : 'Breadth (%)',
208-
minValue: 0,
209-
maxValue: metric === 'breadth' ? 100 : undefined
210-
},
211-
legend: { position: 'bottom' },
212-
colors: ['#00B0BD', '#004E5A', '#FF6A45', '#27AE60'],
213-
chartArea: { width: '80%', height: '70%' },
214-
animation: { startup: true, duration: 1000, easing: 'out' }
215-
}}
221+
options={chartOptions}
216222
width="100%"
217223
height="400px"
218224
/>

frontend/src/modules/setup/setup-steps/database-setup/additional-sequences-setup/additional-sequences-setup.component.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type IKeys = "name" | "file" | "threshold" | "alert";
55

66
const AdditionalSequencesSetupComponent: FunctionComponent<IDatabaseSetupConstituent> = ({ updateConfig }) => {
77
const [queries, setQueries] = useState([
8-
{ name: "", file: "", threshold: "", current_breadth: 0, alert: false }
8+
{ name: "", file: "", threshold: "", current_fold_change: 0, alert: false, header: "" }
99
]);
1010

1111
const handleDataChange = (idx: number, key: IKeys) => (evt: React.ChangeEvent<HTMLInputElement>) => {
@@ -23,7 +23,7 @@ const AdditionalSequencesSetupComponent: FunctionComponent<IDatabaseSetupConstit
2323
alert("Please fill all fields in the current query before adding a new one.");
2424
return;
2525
}
26-
setQueries((prev) => [...prev, { name: "", file: "", threshold: "", current_breadth: 0, alert: false }]);
26+
setQueries((prev) => [...prev, { name: "", file: "", threshold: "", current_fold_change: 0, alert: false, header: "" }]);
2727
};
2828

2929
const handleRemoveQuery = (idx: number) => () => {
@@ -87,11 +87,10 @@ const AdditionalSequencesSetupComponent: FunctionComponent<IDatabaseSetupConstit
8787
id="thresholdText"
8888
name="thresholdText"
8989
type="number"
90-
placeholder="Breadth Coverage % Threshold"
90+
placeholder="Fold Coverage Threshold (x)"
9191
onChange={handleDataChange(i, "threshold")}
9292
className="form-control"
9393
min="0"
94-
max="100"
9594
/>
9695
</div>
9796
<div className="col-sm-1">

frontend/src/modules/setup/setup-steps/database-setup/additional-sequences-setup/additional-sequences-setup.interfaces.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ type IQuery = {
66
name: string,
77
file: string,
88
threshold: string,
9-
current_breadth: number,
10-
alert: boolean
9+
current_fold_change: number,
10+
alert: boolean,
11+
header: string,
1112
}
1213

1314
export type {

frontend/src/modules/setup/setup.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {IAlertNotifSetupInput} from "./setup-steps/alert-notif-setup/alert-notif
1616

1717
const qrs: IAdditionalSequences = {
1818
queries: [
19-
{name: "", file: "", threshold: "", current_breadth: 0, alert: false}
19+
{name: "", file: "", threshold: "", current_fold_change: 0, alert: false, header: ""},
2020
]
2121
};
2222

server/app/main/routes.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,12 @@ def get_coverage():
255255
lines = f.readlines()[1:] # Skip header
256256
data = []
257257
for line in lines:
258-
timestamp, ref, avg_depth, breadth, read_count = line.strip().split(',')
258+
timestamp, ref, fold_coverage, read_count = line.strip().split(',')
259259
name = ref_to_name.get(ref, ref) # Map reference to alert sequence name
260260
data.append({
261261
'timestamp': timestamp,
262262
'reference': name,
263-
'avg_depth': float(avg_depth),
264-
'breadth': float(breadth),
263+
'fold_coverage': float(fold_coverage),
265264
'read_count': int(read_count)
266265
})
267266
return jsonify(data)

server/app/main/utils/FileHandler.py

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, app_loc: str):
3232
self.file_type = self.config.get('fileType', 'FASTQ')
3333
if not os.path.exists(self.coverage_file):
3434
with open(self.coverage_file, 'w') as f:
35-
f.write("timestamp,reference,avg_depth,breadth,read_count\n")
35+
f.write("timestamp,reference,fold_coverage,read_count\n")
3636

3737
def on_moved(self, event):
3838
self.on_any_event(event)
@@ -150,7 +150,7 @@ def merge_bam(self, new_bam: str):
150150
logger.error(f"Error sorting/indexing merged BAM: {e}")
151151

152152
def calculate_and_record_coverage(self):
153-
"""Calculate and record average depth, breadth of coverage, and read count per reference."""
153+
"""Calculate and record fold coverage (average depth) and read count per reference."""
154154
try:
155155
bam = pysam.AlignmentFile(self.merged_bam, "rb", check_sq=False)
156156
if not bam.has_index():
@@ -159,22 +159,23 @@ def calculate_and_record_coverage(self):
159159
coverage_data = {}
160160
for ref in bam.references:
161161
ref_length = bam.lengths[bam.references.index(ref)]
162-
pileup = bam.pileup(ref)
163-
depths = [p.nsegments for p in pileup if p.nsegments > 0]
164-
covered_positions = len(depths)
165-
avg_depth = sum(depths) / len(depths) if depths else 0
166-
breadth = (covered_positions / ref_length) * 100 if ref_length > 0 else 0
162+
# Get coverage depth across all positions for this reference
163+
coverage = bam.count_coverage(ref)
164+
# Sum depths across all bases (A, C, G, T) at each position
165+
total_depth = sum(sum(cov) for cov in coverage)
166+
# Calculate fold coverage as total depth divided by reference length
167+
fold_coverage = total_depth / ref_length if ref_length > 0 else 0
167168
read_count = bam.count(ref) # Count reads mapping to this reference
168-
coverage_data[ref] = {"avg_depth": avg_depth, "breadth": breadth, "read_count": read_count}
169-
# Check if breadth exceeds the threshold and trigger alert if necessary
170-
print(f"Reference: {ref}, Avg Depth: {avg_depth:.2f}, Breadth: {breadth:.2f}%, Read Count: {read_count}")
171-
self.check_breadth_alert(ref, breadth)
169+
coverage_data[ref] = {"fold_coverage": fold_coverage, "read_count": read_count}
170+
print(f"Reference: {ref}, Fold Coverage: {fold_coverage:.2f}x, Read Count: {read_count}")
171+
# Update alert check to use fold coverage if needed
172+
self.check_fold_coverage_alert(ref, fold_coverage)
172173
bam.close()
173174

174175
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
175176
with open(self.coverage_file, 'a') as f:
176177
for ref, cov in coverage_data.items():
177-
f.write(f"{timestamp},{ref},{cov['avg_depth']},{cov['breadth']},{cov['read_count']}\n")
178+
f.write(f"{timestamp},{ref},{cov['fold_coverage']},{cov['read_count']}\n")
178179
logger.debug(f"Coverage and read counts recorded at {timestamp}")
179180

180181
# Emit coverage update via Socket.IO
@@ -186,49 +187,22 @@ def calculate_and_record_coverage(self):
186187
except Exception as e:
187188
logger.error(f"Error calculating coverage: {e}")
188189

189-
def check_breadth_alert(self, ref: str, breadth: float):
190-
"""Check if breadth exceeds threshold and send alerts."""
191-
print(f"Checking breadth alert for {ref} with breadth {breadth:.2f}%")
192-
alertinfo_cfg_file = os.path.join(self.app_loc, 'alertinfo.cfg')
193-
try:
194-
with open(alertinfo_cfg_file, 'r') as f:
195-
alertinfo_cfg_data = json.load(f)
196-
queries = alertinfo_cfg_data.get("queries", [])
197-
device = alertinfo_cfg_data.get("device", "")
198-
print(alertinfo_cfg_data)
199-
for query in queries:
200-
print(f"Checking query: {query}")
201-
if ref == query.get("header", ""):
202-
print(f"Matched query: {query}")
203-
threshold = float(query.get("threshold", 0))
204-
current_breadth = query.get("current_breadth", 0)
205-
# Check alert condition before updating current_breadth
206-
if breadth >= threshold and current_breadth < threshold:
207-
alert_str = f"Alert: {query['name']} breadth coverage reached {breadth:.2f}% (threshold: {threshold}%)"
208-
logger.critical(alert_str)
209-
if device:
210-
LinuxNotification.send_notification(device, alert_str)
211-
# Send alerts if configured
212-
if "alertNotifConfig" in alertinfo_cfg_data:
213-
print("Sending email")
214-
email_config = alertinfo_cfg_data['alertNotifConfig']
215-
subject = "nanoCAS Alert"
216-
body = alert_str
217-
send_email(subject, body, email_config)
218-
# Send SMS via Twilio if configured in .env
219-
if os.getenv('TWILIO_ACCOUNT_SID'):
220-
print("Sending SMS")
221-
send_sms(alert_str)
222-
else:
223-
print("Twilio not configured; skipping SMS")
224-
logger.debug("Twilio not configured in .env; SMS skipped")
225-
# Update current_breadth after checking
226-
if breadth > current_breadth:
227-
query["current_breadth"] = breadth
228-
with open(alertinfo_cfg_file, 'w') as f:
229-
json.dump(alertinfo_cfg_data, f, indent=4)
230-
else:
231-
print(f"No match for query: {query}")
232-
logger.debug(f"No match for query: {query}")
233-
except Exception as e:
234-
logger.error(f"Error checking breadth alert: {e}")
190+
def check_fold_coverage_alert(self, ref: str, fold_coverage: float):
191+
"""Check if fold coverage exceeds threshold and send alerts."""
192+
with open(os.path.join(self.app_loc, 'alertinfo.cfg'), 'r') as f:
193+
alertinfo_cfg_data = json.load(f)
194+
queries = alertinfo_cfg_data.get("queries", [])
195+
device = alertinfo_cfg_data.get("device", "")
196+
for query in queries:
197+
if ref == query.get("header", ""):
198+
threshold = float(query.get("threshold", 0))
199+
if fold_coverage >= threshold:
200+
alert_str = f"Alert: {query['name']} fold coverage reached {fold_coverage:.2f}x (threshold: {threshold}x)"
201+
logger.critical(alert_str)
202+
if device:
203+
LinuxNotification.send_notification(device, alert_str)
204+
if "alertNotifConfig" in alertinfo_cfg_data:
205+
email_config = alertinfo_cfg_data['alertNotifConfig']
206+
send_email("nanoCAS Alert", alert_str, email_config)
207+
if os.getenv('TWILIO_ACCOUNT_SID'):
208+
send_sms(alert_str)

0 commit comments

Comments
 (0)