Skip to content

Commit

Permalink
Merge pull request #232 from nerdstrike/actually_render_product_metrics
Browse files Browse the repository at this point in the history
Actually render product metrics
  • Loading branch information
mgcam authored Jul 11, 2024
2 parents 58c53bf + e4fadb8 commit ddecaa6
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 39 deletions.
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { RouterView, useRoute } from 'vue-router';
import { onMounted, provide, ref } from "vue";
import { ElMessage } from "element-plus";
import { ElMessage, ElTooltip } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import router from "@/router/index.js";
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/components/PoolStats.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup>
import { ref } from "vue";
import { ElTooltip, ElCollapse, ElCollapseItem } from "element-plus";
const props = defineProps({
pool: Object,
})
const active = ref([])
</script>

<template>
<el-collapse v-model="active" accordion>
<el-collapse-item name="1">
<template #title><slot>Pool composition</slot></template>
<el-tooltip content="Caution, this value is of low signficance when the pool size is small">
<p>Coefficient of Variance: {{ props.pool.pool_coeff_of_variance }}</p>
</el-tooltip>
<table>
<tr>
<th>Tag 1</th>
<th>Tag 2</th>
<th>Deplexing barcode ID</th>
<th>HiFi bases (Gb)</th>
<th>HiFi reads</th>
<th>HiFi mean read length</th>
<th>Percentage of HiFi bases</th>
<th>Percentage of total reads</th>
</tr>
<tr :key="library.id_product" v-for="library in props.pool.products">
<td>{{ library.tag1_name }}</td>
<td>{{ library.tag2_name }}</td>
<td>{{ library.deplexing_barcode }}</td>
<td>{{ library.hifi_read_bases }}</td>
<td>{{ library.hifi_num_reads }}</td>
<td>{{ library.hifi_read_length_mean }}</td>
<td>{{ library.hifi_bases_percent }}</td>
<td>{{ library.percentage_total_reads }}</td>
</tr>
</table>
</el-collapse-item>
</el-collapse>
</template>
33 changes: 30 additions & 3 deletions frontend/src/components/QcView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,36 @@
* An information view containing run data and metrics useful for QC assessment
*/
import { computed } from "vue";
import { computed, ref, watch } from "vue";
import groupMetrics from "../utils/metrics.js";
import { combineLabelWithPlate } from "../utils/text.js"
import PoolStats from "./PoolStats.vue";
import LangQc from "../utils/langqc";
const dataClient = new LangQc()
const props = defineProps({
// Well object representing one prepared input for the instrument
// Expects content in the form of lang_qc/models/pacbio/well.py:PacBioWellFull
well: Object,
});
})
const poolStats = ref(null)
watch(() => props.well, () => {
poolStats.value = null // empty in case next well doesn't have a pool
dataClient.getPoolMetrics(props.well.id_product).then(
(response) => { poolStats.value = response }
).catch((error) => {
if (error.message.match("Conflict")) {
// Nothing to do
} else {
console.log(error)
// make a banner show this error?
}
})
}, { immediate: true}
)
const slURL = computed(() => {
let hostname = props.well.metrics.smrt_link.hostname
Expand Down Expand Up @@ -97,7 +118,6 @@
}
return ''
})
</script>
<template>
Expand Down Expand Up @@ -165,6 +185,8 @@
</table>
</div>
<PoolStats v-if="poolStats" :pool="poolStats">Pool composition</PoolStats>
<div id="Metrics">
<table>
<tr>
Expand Down Expand Up @@ -197,6 +219,11 @@
td {
padding-left: 5px;
padding-right: 5px;
align-self: flex-start;
}
tr>td:first-child {
vertical-align: top;
/* Pin first text element to the top of the td */
}
table.summary {
border: 0px;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/WellTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defineEmits(['wellSelected'])
<th>Well time complete</th>
</tr>
<tr :key="wellObj.id_product" v-for="wellObj in wellCollection">
<td>{{ wellObj.run_name }}</td>
<td :id="wellObj.run_name">{{ wellObj.run_name }}</td>
<td class="well_selector">
<el-tooltip placement="top" effect="light" :show-after="tooltipDelay"
:content="'<span>'.concat(listStudiesForTooltip(wellObj.study_names)).concat('</span>')"
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/components/__tests__/PoolStats.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import ElementPlus from 'element-plus'

import PoolStats from '../PoolStats.vue'

const wrapper = mount(PoolStats, {
global: {
plugins: [ElementPlus],
},
props: {
pool: {
pool_coeff_of_variance: 47.2,
products: [{
id_product: 'A'.repeat(64),
tag1_name: 'TTTTTTTT',
tag2_name: null,
deplexing_barcode: 'bc10--bc10',
hifi_read_bases: 900,
hifi_num_reads: 20,
hifi_read_length_mean: 45,
hifi_bases_percent: 90.001,
percentage_total_reads: 66.6
},{
id_product: 'B'.repeat(64),
tag1_name: 'GGGGGGGG',
tag2_name: null,
deplexing_barcode: 'bc11--bc11',
hifi_read_bases: 100,
hifi_num_reads: 10,
hifi_read_length_mean: 10,
hifi_bases_percent: 100,
percentage_total_reads: 33.3
}]
}
}
})

describe('Create poolstats table with good data', () => {
test('Component is "folded" by default', () => {
expect(wrapper.getComponent('transition-stub').attributes()['appear']).toEqual('false')
})

test('Coefficient of variance showing', async () => {
let topStat = wrapper.find('p')
await topStat.trigger('focus')
expect(topStat.classes('el-tooltip__trigger')).toBeTruthy()

expect(topStat.text()).toEqual('Coefficient of Variance: 47.2')
})

test('Table looks about right', () => {
let rows = wrapper.findAll('tr')
expect(rows.length).toEqual(3)

// Check tag 1 has been set
expect(rows[1].find('td').text()).toEqual('TTTTTTTT')
expect(rows[2].find('td').text()).toEqual('GGGGGGGG')
})
})
5 changes: 5 additions & 0 deletions frontend/src/components/__tests__/QcView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import QcView from '../QcView.vue';

describe('Component renders', () => {

// Supply PoolStats element with some data
fetch.mockResponse(
JSON.stringify({})
)

afterEach(async () => {
cleanup()
});
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/utils/__tests__/langqc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ describe('Example fake remote api call', () => {

client.getWellsForRunPromise('blah')
expect(fetch.mock.calls[6][0]).toEqual('/api/pacbio/run/blah?page_size=100&page=1')

client.getPoolMetrics('A12345');
expect(fetch.mock.calls[7][0]).toEqual('/api/pacbio/products/A12345/seq_level/pool')
});
});

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/utils/langqc.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,10 @@ export default class LangQc {
}
)
}

getPoolMetrics(id_product) {
// Use the product metrics endpoint to get additional metrics
// for a well.
return this.fetchWrapper(this.buildUrl(['products', id_product, 'seq_level', 'pool']));
}
}
20 changes: 10 additions & 10 deletions frontend/src/views/__tests__/WellsByRun.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ describe('Does it work?', async () => {

test('Click a well selection, QC View appears (because URL alters)', async () => {
// Not providing precisely the right data, but serves for the component
fetch.mockResponseOnce(
JSON.stringify(secondaryRun)
fetch.mockResponses(
[JSON.stringify(secondaryRun)], // well QC data loading
[JSON.stringify({})] // Pool stats loading
)

let buttons = wrapper.findAll('button')
Expand Down Expand Up @@ -168,16 +169,15 @@ describe('Does it work?', async () => {
]
)
await wrapper.setProps({runName: ['TRACTION-RUN-211', 'TRACTION-RUN-210']})
await flushPromises()

test('Table now contains wells from both runs', () => {
const table = wrapper.get('table')
expect(table.exists()).toBe(true)
const table = wrapper.get('table')
expect(table.exists()).toBe(true)

expect(table.find('TRACTION-RUN-211').exists()).toBe(true)
expect(table.find('TRACTION-RUN-210').exists()).toBe(true)
expect(table.find("td#TRACTION-RUN-211").exists()).toBe(true)
expect(table.find("td#TRACTION-RUN-210").exists()).toBe(true)

const rows = table.findAll('tr')
expect(rows.length).toEqual(4)
})
const rows = table.findAll('tr')
expect(rows.length).toEqual(4)
})
})
8 changes: 4 additions & 4 deletions lang_qc/endpoints/pacbio_well.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,17 @@ def get_seq_metrics(
status.HTTP_409_CONFLICT: {"description": "Missing or incomplete LIMS data"},
status.HTTP_422_UNPROCESSABLE_ENTITY: {"description": "Invalid product ID"},
},
response_model=QCPoolMetrics,
response_model=QCPoolMetrics | None,
)
def get_product_metrics(
id_product: PacBioWellSHA256, mlwhdb_session: Session = Depends(get_mlwh_db)
) -> QCPoolMetrics:
) -> QCPoolMetrics | None:

mlwh_well = _find_well_product_or_error(id_product, mlwhdb_session)
try:
metrics = QCPoolMetrics(db_well=mlwh_well)
except MissingLimsDataError as err:
raise HTTPException(409, detail=str(err))
except MissingLimsDataError:
return

return metrics

Expand Down
12 changes: 6 additions & 6 deletions lang_qc/models/pacbio/qc_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class SampleDeplexingStats(BaseModel):
tag1_name: str | None
tag2_name: str | None
deplexing_barcode: str | None
hifi_read_bases: int | None
hifi_read_bases: float | None = Field(title="HiFi read bases (Gb)")
hifi_num_reads: int | None
hifi_read_length_mean: float | None
hifi_bases_percent: float | None
Expand Down Expand Up @@ -208,7 +208,7 @@ def pre_root(cls, values: dict[str, Any]) -> dict[str, Any]:
if well is None:
raise ValueError(f"None {db_well_key_name} value is not allowed.")

cov: float = None
cov: float | None = None
sample_stats = []

if well.demultiplex_mode and "Instrument" in well.demultiplex_mode:
Expand All @@ -227,21 +227,21 @@ def pre_root(cls, values: dict[str, Any]) -> dict[str, Any]:
cov = None
else:
hifi_reads = [prod.hifi_num_reads for prod in product_metrics]
cov = stdev(hifi_reads) / mean(hifi_reads) * 100
cov = round(stdev(hifi_reads) / mean(hifi_reads) * 100, 2)

for (i, prod) in enumerate(product_metrics):
for i, prod in enumerate(product_metrics):
sample_stats.append(
SampleDeplexingStats(
id_product=prod.id_pac_bio_product,
tag1_name=lib_lims_data[i].tag_identifier,
tag2_name=lib_lims_data[i].tag2_identifier,
deplexing_barcode=prod.barcode4deplexing,
hifi_read_bases=prod.hifi_read_bases,
hifi_read_bases=convert_to_gigabase(prod, "hifi_read_bases"),
hifi_num_reads=prod.hifi_num_reads,
hifi_read_length_mean=prod.hifi_read_length_mean,
hifi_bases_percent=prod.hifi_bases_percent,
percentage_total_reads=(
prod.hifi_num_reads / well.hifi_num_reads * 100
round(prod.hifi_num_reads / well.hifi_num_reads * 100, 2)
if (well.hifi_num_reads and prod.hifi_num_reads)
else None
),
Expand Down
12 changes: 6 additions & 6 deletions tests/fixtures/sample_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ def simplex_run(request, mlwhdb_test_session):
tags=tag1,
).hash_product_id(),
qc=1,
hifi_read_bases=900,
hifi_read_bases=90000000,
hifi_num_reads=10,
hifi_read_length_mean=90,
hifi_read_length_mean=9000000,
barcode_quality_score_mean=34,
hifi_bases_percent=90.001,
pac_bio_run_well_metrics=well_metrics_a1,
Expand Down Expand Up @@ -148,9 +148,9 @@ def multiplexed_run(mlwhdb_test_session):
tags=tag1,
).hash_product_id(),
qc=1,
hifi_read_bases=900,
hifi_read_bases=90000000,
hifi_num_reads=20,
hifi_read_length_mean=45,
hifi_read_length_mean=4500000,
barcode_quality_score_mean=34,
hifi_bases_percent=90.001,
pac_bio_run_well_metrics=well_metrics_b1,
Expand Down Expand Up @@ -180,9 +180,9 @@ def multiplexed_run(mlwhdb_test_session):
tags=tag1_2,
).hash_product_id(),
qc=1,
hifi_read_bases=100,
hifi_read_bases=10000000,
hifi_num_reads=10,
hifi_read_length_mean=10,
hifi_read_length_mean=1000000,
barcode_quality_score_mean=34,
hifi_bases_percent=100.00,
pac_bio_run_well_metrics=well_metrics_b1,
Expand Down
16 changes: 8 additions & 8 deletions tests/test_pac_bio_qc_data_well.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,19 @@ def test_pool_metrics_from_well(mlwhdb_test_session, multiplexed_run):
for metrics in [metrics_via_db, metrics_direct]:
assert (
int(metrics.pool_coeff_of_variance) == 47
), "Variance between 20 and 10 is ~47%"
), "Variance between 20 reads and 10 reads is ~47%"

assert metrics.products[0].hifi_read_bases == 100
assert metrics.products[0].hifi_read_bases == 0.01
assert (
metrics.products[1].hifi_read_bases == 900
), "hifi read base counts are faithfully copied"
metrics.products[1].hifi_read_bases == 0.09
), "hifi read base counts are scaled to Gigabases"

assert (
int(metrics.products[0].percentage_total_reads) == 33
), "10 of 30 reads is 33.3%"
metrics.products[0].percentage_total_reads == 33.33
), "10Mb of 30Mb reads is 33.33% (2 d.p.)"
assert (
int(metrics.products[1].percentage_total_reads) == 66
), "20 of 30 reads is 66.6%"
metrics.products[1].percentage_total_reads == 66.67
), "20Mb of 30Mb reads is 66.67% (2 d.p.)"


def test_errors_instantiating_pool_metrics(mlwhdb_test_session):
Expand Down

0 comments on commit ddecaa6

Please sign in to comment.