diff --git a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_calculated_documents.py b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_calculated_documents.py index c1a0058aa..5342cf8c6 100644 --- a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_calculated_documents.py +++ b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_calculated_documents.py @@ -605,6 +605,9 @@ def iter_comparative_ct_calc_docs( calc_docs: list[CalculatedDocument | None] = [] for sample, target in view_st_data.iter_keys(): for well_item in view_st_data.get_leaf_item(sample, target): + if not well_item.has_result: + continue + calc_docs.append(build_quantity(view_tr_data, target, well_item)) calc_docs.append(build_amp_score(well_item)) calc_docs.append(build_cq_conf(well_item)) @@ -636,6 +639,9 @@ def iter_standard_curve_calc_docs( calc_docs: list[CalculatedDocument | None] = [] for sample, target in view_st_data.iter_keys(): for well_item in view_st_data.get_leaf_item(sample, target): + if not well_item.has_result: + continue + calc_docs.append(build_quantity(view_tr_data, target, well_item)) calc_docs.append(build_amp_score(well_item)) calc_docs.append(build_cq_conf(well_item)) @@ -666,6 +672,9 @@ def iter_relative_standard_curve_calc_docs( calc_docs: list[CalculatedDocument | None] = [] for sample, target in view_st_data.iter_keys(): for well_item in view_st_data.get_leaf_item(sample, target): + if not well_item.has_result: + continue + calc_docs.append(build_quantity(view_tr_data, target, well_item)) calc_docs.append(build_amp_score(well_item)) calc_docs.append(build_cq_conf(well_item)) @@ -699,6 +708,9 @@ def iter_presence_absence_calc_docs( calc_docs: list[CalculatedDocument | None] = [] for sample, target in view_data.iter_keys(): for well_item in view_data.get_leaf_item(sample, target): + if not well_item.has_result: + continue + calc_docs.append(build_quantity(None, target, well_item)) calc_docs.append(build_amp_score(well_item)) calc_docs.append(build_cq_conf(well_item)) diff --git a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py index 3d824efd3..ed8b7f9f1 100644 --- a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py +++ b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py @@ -15,7 +15,6 @@ Metadata, ProcessedData, ) -from allotropy.exceptions import AllotropeConversionError from allotropy.parsers.appbio_quantstudio import constants from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import ( AmplificationData, @@ -157,9 +156,11 @@ def _create_measurement( header: Header, multicomponent_data: MulticomponentData | None, melt_curve_raw_data: MeltCurveRawData | None, - amplification_data: AmplificationData, - result: Result, -) -> Measurement: + amplification_data: AmplificationData | None, + result: Result | None, +) -> Measurement | None: + if not result: + return None # TODO: temp workaround for cal doc result well_item._result = result @@ -184,12 +185,16 @@ def _create_measurement( sample_role_type=well_item.sample_role_type, well_location_identifier=well_item.well_location_identifier, well_plate_identifier=header.barcode, - total_cycle_number_setting=amplification_data.total_cycle_number_setting, + total_cycle_number_setting=amplification_data.total_cycle_number_setting + if amplification_data + else None, pcr_detection_chemistry=header.pcr_detection_chemistry, reporter_dye_setting=well_item.reporter_dye_setting, quencher_dye_setting=well_item.quencher_dye_setting, passive_reference_dye_setting=header.passive_reference_dye_setting, - processed_data=_create_processed_data(amplification_data, result), + processed_data=_create_processed_data(amplification_data, result) + if amplification_data + else None, sample_custom_info=well_item.extra_data, data_cubes=data_cubes, ) @@ -241,27 +246,46 @@ def create_calculated_data( def get_well_item_results( well_item: WellItem, results_data: dict[int, dict[str, Result]], -) -> Result: - results_data_element = results_data.get(well_item.identifier, {}).get( +) -> Result | None: + return results_data.get(well_item.identifier, {}).get( well_item.target_dna_description.replace(" ", "") ) - if results_data_element is None: - msg = f"No result data for well item {well_item.identifier} and target DNA {well_item.target_dna_description}" - raise AllotropeConversionError(msg) - return results_data_element def get_well_item_amp_data( well_item: WellItem, amp_data: dict[int, dict[str, AmplificationData]], -) -> AmplificationData: - amp_data_element = amp_data.get(well_item.identifier, {}).get( - well_item.target_dna_description +) -> AmplificationData | None: + return amp_data.get(well_item.identifier, {}).get(well_item.target_dna_description) + + +def _create_measurement_group( + header: Header, + well: Well, + amp_data: dict[int, dict[str, AmplificationData]], + multi_data: dict[int, MulticomponentData], + results_data: dict[int, dict[str, Result]], + melt_data: dict[int, MeltCurveRawData], +) -> MeasurementGroup | None: + measurements = [ + _create_measurement( + well_item, + header, + multi_data.get(well.identifier), + melt_data.get(well.identifier), + get_well_item_amp_data(well_item, amp_data), + get_well_item_results(well_item, results_data), + ) + for well_item in well.items + if get_well_item_results(well_item, results_data) + ] + group = MeasurementGroup( + analyst=header.analyst, + experimental_data_identifier=header.experimental_data_identifier, + plate_well_count=try_int_or_nan(header.plate_well_count), + measurements=[m for m in measurements if m is not None], ) - if amp_data_element is None: - msg = f"No amplification data for well item {well_item.identifier} and target DNA {well_item.target_dna_description}" - raise AllotropeConversionError(msg) - return amp_data_element + return group if group.measurements else None def create_measurement_groups( @@ -272,22 +296,10 @@ def create_measurement_groups( results_data: dict[int, dict[str, Result]], melt_data: dict[int, MeltCurveRawData], ) -> list[MeasurementGroup]: - return [ - MeasurementGroup( - analyst=header.analyst, - experimental_data_identifier=header.experimental_data_identifier, - plate_well_count=try_int_or_nan(header.plate_well_count), - measurements=[ - _create_measurement( - well_item, - header, - multi_data.get(well.identifier), - melt_data.get(well.identifier), - get_well_item_amp_data(well_item, amp_data), - get_well_item_results(well_item, results_data), - ) - for well_item in well.items - ], + groups = [ + _create_measurement_group( + header, well, amp_data, multi_data, results_data, melt_data ) for well in wells ] + return [group for group in groups if group] diff --git a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_structure.py b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_structure.py index d8b29a531..b3a8d0aca 100644 --- a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_structure.py +++ b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_structure.py @@ -126,6 +126,10 @@ class WellItem(Referenceable): def __hash__(self) -> int: return hash(self.identifier) + @property + def has_result(self) -> bool: + return self._result is not None + @property def result(self) -> Result: return assert_not_none(self._result) diff --git a/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.json b/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.json new file mode 100644 index 000000000..fb1195c4e --- /dev/null +++ b/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.json @@ -0,0 +1,292 @@ +{ + "$asm.manifest": "http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest", + "qPCR aggregate document": { + "device system document": { + "device identifier": "Sponge_Bob_32", + "model number": "QuantStudio(TM) 7 Flex System", + "device serial number": "123456789" + }, + "qPCR document": [ + { + "measurement aggregate document": { + "plate well count": { + "value": 96, + "unit": "#" + }, + "measurement document": [ + { + "measurement identifier": "APPBIO_QUANTSTUDIO_TEST_ID_0", + "measurement time": "2010-09-16T07:35:29-04:00", + "target DNA description": "CYP19_2-Allele 1", + "sample document": { + "sample identifier": "NTC", + "sample role type": "NTC", + "well location identifier": "A1", + "custom information document": { + "well identifier": 1, + "sample color": "RGB(238,238,0)" + } + }, + "device control aggregate document": { + "device control document": [ + { + "device type": "qPCR", + "measurement method identifier": "Ct", + "total cycle number setting": { + "value": 2.0, + "unit": "#" + }, + "PCR detection chemistry": "TAQMAN", + "reporter dye setting": "VIC", + "passive reference dye setting": "ROX" + } + ] + }, + "processed data aggregate document": { + "processed data document": [ + { + "data processing document": { + "cycle threshold value setting": { + "value": 0.219, + "unit": "(unitless)" + }, + "automatic cycle threshold enabled setting": true, + "automatic baseline determination enabled setting": true, + "baseline determination start cycle setting": { + "value": 3, + "unit": "#" + }, + "baseline determination end cycle setting": { + "value": 39, + "unit": "#" + } + }, + "cycle threshold result": { + "value": null, + "unit": "(unitless)" + }, + "normalized reporter data cube": { + "label": "normalized reporter", + "cube-structure": { + "dimensions": [ + { + "@componentDatatype": "integer", + "concept": "cycle count", + "unit": "#" + } + ], + "measures": [ + { + "@componentDatatype": "double", + "concept": "normalized report result", + "unit": "(unitless)" + } + ] + }, + "data": { + "dimensions": [ + [ + 1, + 2 + ] + ], + "measures": [ + [ + 0.275, + 0.277 + ] + ] + } + }, + "baseline corrected reporter result": { + "value": 0.016, + "unit": "(unitless)" + }, + "baseline corrected reporter data cube": { + "label": "baseline corrected reporter", + "cube-structure": { + "dimensions": [ + { + "@componentDatatype": "integer", + "concept": "cycle count", + "unit": "#" + } + ], + "measures": [ + { + "@componentDatatype": "double", + "concept": "baseline corrected reporter result", + "unit": "(unitless)" + } + ] + }, + "data": { + "dimensions": [ + [ + 1, + 2 + ] + ], + "measures": [ + [ + -0.003, + -0.001 + ] + ] + } + }, + "genotyping determination result": "Negative Control (NC)", + "custom information document": { + "omit": false + } + } + ] + } + }, + { + "measurement identifier": "APPBIO_QUANTSTUDIO_TEST_ID_1", + "measurement time": "2010-09-16T07:35:29-04:00", + "target DNA description": "CYP19_2-Allele 2", + "sample document": { + "sample identifier": "NTC", + "sample role type": "NTC", + "well location identifier": "A1", + "custom information document": { + "well identifier": 1, + "sample color": "RGB(238,238,0)" + } + }, + "device control aggregate document": { + "device control document": [ + { + "device type": "qPCR", + "measurement method identifier": "Ct", + "total cycle number setting": { + "value": 2.0, + "unit": "#" + }, + "PCR detection chemistry": "TAQMAN", + "reporter dye setting": "FAM", + "passive reference dye setting": "ROX" + } + ] + }, + "processed data aggregate document": { + "processed data document": [ + { + "data processing document": { + "cycle threshold value setting": { + "value": 0.132, + "unit": "(unitless)" + }, + "automatic cycle threshold enabled setting": true, + "automatic baseline determination enabled setting": true, + "baseline determination start cycle setting": { + "value": 3, + "unit": "#" + }, + "baseline determination end cycle setting": { + "value": 39, + "unit": "#" + } + }, + "cycle threshold result": { + "value": null, + "unit": "(unitless)" + }, + "normalized reporter data cube": { + "label": "normalized reporter", + "cube-structure": { + "dimensions": [ + { + "@componentDatatype": "integer", + "concept": "cycle count", + "unit": "#" + } + ], + "measures": [ + { + "@componentDatatype": "double", + "concept": "normalized report result", + "unit": "(unitless)" + } + ] + }, + "data": { + "dimensions": [ + [ + 1, + 2 + ] + ], + "measures": [ + [ + 0.825, + 0.831 + ] + ] + } + }, + "baseline corrected reporter result": { + "value": 0.029, + "unit": "(unitless)" + }, + "baseline corrected reporter data cube": { + "label": "baseline corrected reporter", + "cube-structure": { + "dimensions": [ + { + "@componentDatatype": "integer", + "concept": "cycle count", + "unit": "#" + } + ], + "measures": [ + { + "@componentDatatype": "double", + "concept": "baseline corrected reporter result", + "unit": "(unitless)" + } + ] + }, + "data": { + "dimensions": [ + [ + 1, + 2 + ] + ], + "measures": [ + [ + -0.016, + -0.011 + ] + ] + } + }, + "genotyping determination result": "Negative Control (NC)", + "custom information document": { + "omit": false + } + } + ] + } + } + ], + "experimental data identifier": "QuantStudio 96-Well SNP Genotyping Example", + "experiment type": "genotyping qPCR experiment", + "container type": "qPCR reaction block" + } + } + ], + "data system document": { + "data system instance identifier": "localhost", + "file name": "appbio_quantstudio_minimal_missing_results.txt", + "UNC path": "", + "software name": "Thermo QuantStudio", + "software version": "1.0", + "ASM converter name": "allotropy_appbio_quantstudio_rt_pcr", + "ASM converter version": "0.1.56" + } + } +} diff --git a/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.txt b/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.txt new file mode 100644 index 000000000..877f877df --- /dev/null +++ b/tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_minimal_missing_results.txt @@ -0,0 +1,41 @@ +* Block Type = 96-Well Block (0.2mL) +* Calibration Background is expired = No +* Calibration Background performed on = 09-12-2010 +* Calibration Pure Dye FAM is expired = No +* Calibration Pure Dye FAM performed on = 09-12-2010 +* Calibration Pure Dye ROX is expired = No +* Calibration Pure Dye ROX performed on = 09-12-2010 +* Chemistry = TAQMAN +* Date Created = 2014-09-21 22:05:31 PM EDT +* Experiment Barcode = NA +* Experiment Comment = NA +* Experiment File Name = C:\Program Files (x86)\Applied BioSystems\QuantStudio Real-Time PCR Software\examples\QS7Flex\QS7_96-Well SNP Genotyping Example.eds +* Experiment Name = QuantStudio 96-Well SNP Genotyping Example +* Experiment Run End Time = 2010-09-16 07:35:29 AM EDT +* Experiment Type = Genotyping +* Instrument Name = Sponge_Bob_32 +* Instrument Serial Number = 123456789 +* Instrument Type = QuantStudio(TM) 7 Flex System +* Passive Reference = ROX +* Quantification Cycle Method = Ct +* Signal Smoothing On = true +* Stage/ Cycle where Analysis is performed = Stage 3, Step 2 +* User Name = NA + +[Sample Setup] +Well Well Position Sample Name Sample Color SNP Assay Name SNP Assay Color Task Allele1 Name Allele1 Color Allele1 Reporter Allele1 Quencher Allele2 Name Allele2 Color Allele2 Reporter Allele2 Quencher Comments +1 A1 NTC "RGB(238,238,0)" CYP19_2 "RGB(176,23,31)" NTC Allele 1 "RGB(0,0,255)" VIC NFQ-MGB Allele 2 "RGB(0,139,69)" FAM NFQ-MGB +2 A2 NTC "RGB(238,238,0)" CYP19_2 "RGB(238,121,66)" NTC Allele 1 "RGB(142,23,31)" VIC NFQ-MGB Allele 2 "RGB(0,139,69)" FAM NFQ-MGB + + +[Amplification Data] +Well Cycle Target Name Rn Delta Rn +1 1 CYP19_2-Allele 2 0.825 -0.016 +1 2 CYP19_2-Allele 2 0.831 -0.011 +1 1 CYP19_2-Allele 1 0.275 -0.003 +1 2 CYP19_2-Allele 1 0.277 -0.001 + + +[Results] +Well Well Position Omit Sample Name SNP Assay Name Task Allele1 Delta Rn Allele2 Delta Rn Pass.Ref Quality(%) Call Method Allele1 Automatic Ct Threshold Allele1 Ct Threshold Allele1 Automatic Baseline Allele1 Baseline Start Allele1 Baseline End Allele2 Automatic Ct Threshold Allele2 Ct Threshold Allele2 Automatic Baseline Allele2 Baseline Start Allele2 Baseline End Allele1 Ct Allele2 Ct Comments Allele1 Amp Score Allele2 Amp Score Allele1 Cq Conf Allele2 Cq Conf +1 A1 false NTC CYP19_2 NTC 0.016 0.029 846,041.750 100.000 Negative Control (NC) Auto true 0.219 true 3 39 true 0.132 true 3 39 Undetermined Undetermined 0.000 0.000 0.000 0.000