A Python library for converting instrument data into Allotrope Simple Model (ASM)
#!/usr/bin/env python3
from pathlib import Path

from allotropy.allotrope.schema_parser.generate_schemas import generate_schemas

ROOT_DIR = Path(__file__).parent.parent


if __name__ == "__main__":
    generate_schemas(ROOT_DIR)

scripts/generate_schemas.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from dataclasses import fields, is_dataclass
from typing import Any, Callable, cast

import cattrs
from cattrs.gen import make_dict_unstructure_fn, override

from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteDocumentItem,
    MeasurementDocumentItem,
)
from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import (
    ProcessedDataDocumentItem,
)


class AllotropeConversionError(Exception):
    pass


# TODO: gather exceptions when parsing models from schema and publish them in model
SPECIAL_KEYS = {
    "manifest": "$asm.manifest",
    "cube_structure": "cube-structure",
    "field_componentDatatype": "@componentDatatype",
    "field_asm_fill_value": "$asm.fill-value",
    "field_type": "@type",
    "field_index": "@index",
    "scan_position_setting__plate_reader_": "scan position setting (plate reader)",
    "detector_distance_setting__plate_reader_": "detector distance setting (plate reader)",
    "cell_type__cell_counter_": "cell type (cell counter)",
    "dead_cell_density__cell_counter_": "dead cell density (cell counter)",
    "average_dead_cell_diameter__cell_counter_": "average dead cell diameter (cell counter)",
    "viability__cell_counter_": "viability (cell counter)",
    "total_cell_density__cell_counter_": "total cell density (cell counter)",
    "viable_cell_density__cell_counter_": "viable cell density (cell counter)",
    "average_live_cell_diameter__cell_counter_": "average live cell diameter (cell counter)",
    "average_total_cell_diameter__cell_counter_": "average total cell diameter (cell counter)",
    "total_cell_diameter_distribution__cell_counter_": "total cell diameter distribution (cell counter)",
    "viable_cell_count__cell_counter_": "viable cell count (cell counter)",
    "total_cell_count__cell_counter_": "total cell count (cell counter)",
    "pco2": "pCO2",
    "co2_saturation": "CO2 saturation",
    "po2": "pO2",
    "o2_saturation": "O2 saturation",
}


CELL_CULTURE_NULLABLE_VALUE_CLASSES: dict[Any, set[str]] = {
    MeasurementDocumentItem: {
        "pco2",
        "co2_saturation",
        "po2",
        "o2_saturation",
        "optical_density",
        "pH",
        "osmolality",
        "viability__cell_counter_",
        "total_cell_density__cell_counter_",
        "viable_cell_density__cell_counter_",
        "average_live_cell_diameter__cell_counter_",
        "average_total_cell_diameter__cell_counter_",
        "total_cell_diameter_distribution__cell_counter_",
        "viable_cell_count__cell_counter_",
        "total_cell_count__cell_counter_",
    },
    AnalyteDocumentItem: {"molar_concentration"},
}

QPCR_NULLABLE_VALUE_CLASSES: dict[Any, set[str]] = {
    ProcessedDataDocumentItem: {"cycle_threshold_result"}
}

EMPTY_VALUE_CLASS_AND_FIELD = {
    **CELL_CULTURE_NULLABLE_VALUE_CLASSES,
    **QPCR_NULLABLE_VALUE_CLASSES,
}


def should_allow_empty_value_field(cls: Any, key: str) -> bool:
    return key in EMPTY_VALUE_CLASS_AND_FIELD.get(cls, set())


def get_key(key: str) -> str:
    return SPECIAL_KEYS.get(key, key.replace("_", " "))


def serialize_allotrope(model: Any) -> dict[str, Any]:
    converter = cattrs.Converter()

    # Default check for omitting values skips if value is None
    def should_omit(_: str, v: Any) -> bool:
        return v is None

    # Special should_omit check for allowing an empty value for 'value' keys, controlled by should_allow_empty_value_field
    def should_omit_allow_empty_value_field(k: str, v: Any) -> bool:
        return v is None and k != "value"

    unstructure_fn_cache = {}

    def unstructure_dataclass_fn(
        cls: Any, should_omit: Callable[[str, Any], bool] = should_omit
    ) -> Callable[[Any], dict]:
        def unstructure(obj: Any) -> Any:
            # Break out of dataclass recursion by calling back to converter.unstructure
            if not is_dataclass(obj):
                return converter.unstructure(obj)

            return {
                get_key(k): v
                for k, v in make_unstructure_fn(type(obj))(obj).items()
                if not should_omit(k, v)
            }

        # This custom unstructure function overrides the unstruct_hook when we should should_allow_empty_value_field.
        # We need to do this at this level because we need to know both the parent class and the field name at the
        # same time.
        def make_unstructure_fn(subcls: Any) -> Callable[[Any], dict[str, Any]]:
            if (cls, subcls) not in unstructure_fn_cache:
                unstructure_fn_cache[(cls, subcls)] = make_dict_unstructure_fn(
                    subcls,
                    converter,
                    **{
                        a.name: override(
                            unstruct_hook=unstructure_dataclass_fn(
                                subcls, should_omit_allow_empty_value_field
                            )
                            if should_allow_empty_value_field(cls, a.name)
                            else None
                        )
                        for a in fields(cls)
                    },
                )
            return unstructure_fn_cache[(cls, subcls)]

        return unstructure

    converter.register_unstructure_hook_factory(is_dataclass, unstructure_dataclass_fn)
    result = converter.unstructure(model)
    return cast(dict[str, Any], result)

src/allotropy/allotrope/allotrope.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  cell-counter.json
#   timestamp: 2023-09-17T15:29:05+00:00

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueCell,
    TQuantityValueMicroliter,
    TQuantityValueMicrometer,
    TQuantityValueMillionCellsPerMilliliter,
    TQuantityValueMilliSecond,
    TQuantityValueNanometer,
    TQuantityValuePercent,
    TQuantityValueTODO,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacubeData,
    TDatacubeStructure,
    TDateTimeValue,
    TStringValue,
)


@dataclass
class DeviceSystemDocument:
    device_identifier: Optional[TStringValue] = None
    model_number: Optional[TStringValue] = None
    device_serial_number: Optional[TStringValue] = None


@dataclass
class DataSystemDocument:
    data_system_instance_identifier: Optional[TStringValue] = None
    file_name: Optional[TStringValue] = None
    UNC_path: Optional[TStringValue] = None
    software_name: Optional[TStringValue] = None
    software_version: Optional[TStringValue] = None
    ASM_converter_name: Optional[TStringValue] = None
    ASM_converter_version: Optional[TStringValue] = None


@dataclass
class SampleDocument:
    sample_identifier: Optional[TStringValue] = None
    batch_identifier: Optional[TStringValue] = None
    sample_role_type: Optional[TStringValue] = None
    location_identifier: Optional[TStringValue] = None


@dataclass
class ImageDocumentItem:
    experimental_data_identifier: Optional[TStringValue] = None
    field_index: Optional[int] = None


@dataclass
class ImageAggregateDocument:
    image_document: Optional[list[ImageDocumentItem]] = None


@dataclass
class DeviceControlDocumentItem:
    device_type: TStringValue
    sample_volume_setting: Optional[TQuantityValueMicroliter] = None
    detection_type: Optional[TStringValue] = None
    excitation_wavelength_setting: Optional[TQuantityValueNanometer] = None
    emission_wavelength_setting: Optional[TQuantityValueNanometer] = None
    illumination_setting: Optional[TQuantityValuePercent] = None
    exposure_duration_setting: Optional[TQuantityValueMilliSecond] = None
    detector_gain_setting: Optional[TStringValue] = None
    fluorescence_tag_setting: Optional[TStringValue] = None
    field_index: Optional[int] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: list[DeviceControlDocumentItem]


@dataclass
class DataProcessingDocument:
    cell_type_processing_method: Optional[TStringValue] = None
    minimum_cell_diameter: Optional[TQuantityValueMicrometer] = None
    maximum_cell_diameter: Optional[TQuantityValueMicrometer] = None
    cell_density_dilution_factor: Optional[TQuantityValueUnitless] = None


@dataclass
class TotalCellDiameterDistributionDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class ProcessedDataDocument:
    data_processing_document: DataProcessingDocument
    viability__cell_counter_: TQuantityValuePercent
    viable_cell_density__cell_counter_: TQuantityValueMillionCellsPerMilliliter
    total_cell_count: TQuantityValueCell
    total_cell_density__cell_counter_: Optional[
        TQuantityValueMillionCellsPerMilliliter
    ] = None
    dead_cell_density__cell_counter_: Optional[
        TQuantityValueMillionCellsPerMilliliter
    ] = None
    average_total_cell_diameter: Optional[TQuantityValueMicrometer] = None
    average_live_cell_diameter__cell_counter_: Optional[TQuantityValueMicrometer] = None
    average_dead_cell_diameter__cell_counter_: Optional[TQuantityValueMicrometer] = None
    total_cell_diameter_distribution_data_cube: Optional[
        TotalCellDiameterDistributionDataCube
    ] = None
    viable_cell_count: Optional[TQuantityValueCell] = None
    dead_cell_count: Optional[TQuantityValueCell] = None
    average_total_cell_circularity: Optional[TQuantityValueUnitless] = None
    average_viable_cell_circularity: Optional[TQuantityValueUnitless] = None
    fluorescence_tag_positive_cell_count: Optional[TQuantityValueTODO] = None
    fluorescence_tag_positive_cell_density: Optional[TQuantityValueTODO] = None
    fluorescence_tag_positive_cell_percentage: Optional[TQuantityValueTODO] = None


@dataclass
class MeasurementDocumentItem:
    measurement_identifier: TStringValue
    measurement_time: TDateTimeValue
    sample_document: SampleDocument
    device_control_aggregate_document: DeviceControlAggregateDocument
    processed_data_document: ProcessedDataDocument
    image_aggregate_document: Optional[ImageAggregateDocument] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_document: list[MeasurementDocumentItem]


@dataclass
class CellCountingDocumentItem:
    analyst: TStringValue
    measurement_aggregate_document: MeasurementAggregateDocument
    submitter: Optional[TStringValue] = None


@dataclass
class CellCountingAggregateDocument:
    device_system_document: DeviceSystemDocument
    cell_counting_document: list[CellCountingDocumentItem]
    data_system_document: Optional[DataSystemDocument] = None


@dataclass
class Model:
    cell_counting_aggregate_document: CellCountingAggregateDocument
    manifest: str = "http://purl.allotrope.org/manifests/cell-counter/BENCHLING/2023/09/cell-counter.manifest"

src/allotropy/allotrope/models/cell_counter_benchling_2023_09_cell_counter.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  cell-culture-analyzer.json
#   timestamp: 2023-09-21T17:20:28+00:00

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Union

from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueCell,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueMicrometer,
    TNullableQuantityValueMillimeterOfMercury,
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueMillionCellsPerMilliliter,
    TNullableQuantityValueMilliOsmolesPerKilogram,
    TNullableQuantityValueOpticalDensity,
    TNullableQuantityValuePercent,
    TNullableQuantityValuePH,
    TNullableQuantityValueTODO,
    TNullableQuantityValueUnitPerLiter,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDateTimeValue,
    TStringValue,
)


@dataclass
class DeviceSystemDocument:
    device_identifier: Optional[TStringValue] = None
    model_number: Optional[TStringValue] = None
    device_serial_number: Optional[TStringValue] = None


@dataclass
class SampleDocument:
    sample_identifier: Optional[TStringValue] = None
    batch_identifier: Optional[TStringValue] = None
    sample_role_type: Optional[str] = None


@dataclass
class AnalyteDocumentItem:
    analyte_name: TStringValue
    molar_concentration: Optional[
        Union[
            TNullableQuantityValueMillimolePerLiter,
            TNullableQuantityValueGramPerLiter,
            TNullableQuantityValueUnitPerLiter,
        ]
    ] = None


@dataclass
class AnalyteAggregateDocument:
    analyte_document: Optional[list[AnalyteDocumentItem]] = None


@dataclass
class MeasurementDocumentItem:
    sample_document: SampleDocument
    measurement_time: TDateTimeValue
    analyte_aggregate_document: AnalyteAggregateDocument
    pco2: Optional[TNullableQuantityValueMillimeterOfMercury] = None
    co2_saturation: Optional[TNullableQuantityValuePercent] = None
    po2: Optional[TNullableQuantityValueMillimeterOfMercury] = None
    o2_saturation: Optional[TNullableQuantityValuePercent] = None
    optical_density: Optional[TNullableQuantityValueOpticalDensity] = None
    pH: Optional[TNullableQuantityValuePH] = None
    osmolality: Optional[TNullableQuantityValueMilliOsmolesPerKilogram] = None
    viability__cell_counter_: Optional[TNullableQuantityValuePercent] = None
    total_cell_density__cell_counter_: Optional[
        TNullableQuantityValueMillionCellsPerMilliliter
    ] = None
    viable_cell_density__cell_counter_: Optional[
        TNullableQuantityValueMillionCellsPerMilliliter
    ] = None
    average_live_cell_diameter__cell_counter_: Optional[
        TNullableQuantityValueMicrometer
    ] = None
    average_total_cell_diameter__cell_counter_: Optional[
        TNullableQuantityValueMicrometer
    ] = None
    total_cell_diameter_distribution__cell_counter_: Optional[
        TNullableQuantityValueTODO
    ] = None
    viable_cell_count__cell_counter_: Optional[TNullableQuantityValueCell] = None
    total_cell_count__cell_counter_: Optional[TNullableQuantityValueCell] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_identifier: TStringValue
    analyst: TStringValue
    device_system_document: DeviceSystemDocument
    measurement_document: list[MeasurementDocumentItem]
    data_processing_time: Optional[TDateTimeValue] = None


@dataclass
class Model:
    measurement_aggregate_document: Optional[MeasurementAggregateDocument] = None
    manifest: str = "http://purl.allotrope.org/manifests/cell-culture-analyzer/BENCHLING/2023/09/cell-culture-analyzer.manifest"

src/allotropy/allotrope/models/cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  fluorescence.json
#   timestamp: 2023-09-17T16:00:55+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional

from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMicroliter,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
    TQuantityValueNumber,
    TQuantityValuePicogramPerMilliliter,
    TQuantityValueSecondTime,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacube,
    TDateTimeValue,
    TStringValue,
)


class ContainerType(Enum):
    reactor = "reactor"
    controlled_lab_reactor = "controlled lab reactor"
    tube = "tube"
    well_plate = "well plate"
    differential_scanning_calorimetry_pan = "differential scanning calorimetry pan"
    qPCR_reaction_block = "qPCR reaction block"
    vial_rack = "vial rack"
    pan = "pan"
    reservoir = "reservoir"
    array_card_block = "array card block"
    capillary = "capillary"
    disintegration_apparatus_basket = "disintegration apparatus basket"
    jar = "jar"
    container = "container"
    tray = "tray"
    basket = "basket"
    cell_holder = "cell holder"


class ScanPositionSettingPlateReader(Enum):
    bottom_scan_position__plate_reader_ = "bottom scan position (plate reader)"
    scan_position_configuration__plate_reader_ = (
        "scan position configuration (plate reader)"
    )
    top_scan_position__plate_reader_ = "top scan position (plate reader)"


@dataclass
class DeviceSystemDocument:
    device_identifier: TStringValue
    model_number: TStringValue


@dataclass
class DeviceControlDocumentItem:
    device_type: Optional[TStringValue] = None
    shaking_configuration_description: Optional[TStringValue] = None
    detector_distance_setting__plate_reader_: Optional[TQuantityValueMillimeter] = None
    integration_time: Optional[TQuantityValueSecondTime] = None
    number_of_averages: Optional[TQuantityValueNumber] = None
    detector_gain_setting: Optional[TStringValue] = None
    scan_position_setting__plate_reader_: Optional[
        ScanPositionSettingPlateReader
    ] = None
    detector_carriage_speed_setting: Optional[TStringValue] = None
    detector_wavelength_setting: Optional[TQuantityValueNanometer] = None
    detector_bandwidth_setting: Optional[TQuantityValueNanometer] = None
    excitation_bandwidth_setting: Optional[TQuantityValueNanometer] = None
    excitation_wavelength_setting: Optional[TQuantityValueNanometer] = None
    wavelength_filter_cutoff_setting: Optional[TQuantityValueNanometer] = None
    field_index: Optional[int] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: Optional[list[DeviceControlDocumentItem]] = None


@dataclass
class MeasurementDocumentItem:
    device_control_aggregate_document: DeviceControlAggregateDocument
    sample_document: SampleDocument
    data_cube: Optional[TDatacube] = None
    compartment_temperature: Optional[TQuantityValueDegreeCelsius] = None
    processed_data_aggregate_document: Optional[ProcessedDataAggregateDocument] = None
    mass_concentration: Optional[TQuantityValuePicogramPerMilliliter] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_identifier: TStringValue
    plate_well_count: TQuantityValueNumber
    measurement_document: list[MeasurementDocumentItem]
    measurement_time: Optional[TDateTimeValue] = None
    analyst: Optional[TStringValue] = None
    analytical_method_identifier: Optional[TStringValue] = None
    experimental_data_identifier: Optional[TStringValue] = None
    experiment_type: Optional[TStringValue] = None
    container_type: Optional[ContainerType] = None
    well_volume: Optional[TQuantityValueMicroliter] = None
    device_system_document: Optional[DeviceSystemDocument] = None


@dataclass
class Model:
    measurement_aggregate_document: Optional[MeasurementAggregateDocument] = None
    manifest: str = "http://purl.allotrope.org/manifests/fluorescence/BENCHLING/2023/09/fluorescence.manifest"

src/allotropy/allotrope/models/fluorescence_benchling_2023_09_fluorescence.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  liquid-chromatography.json
#   timestamp: 2023-09-18T15:35:45+00:00

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Optional

from allotropy.allotrope.models.shared.components.liquid_chromatography import (
    PeakList,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueCentimeter,
    TQuantityValueCubicMillimeter,
    TQuantityValueHertz,
    TQuantityValueMicrometer,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacube,
    TDateTimeValue,
    TQuantityValue,
    TStringValue,
)


@dataclass
class DeviceDocumentItem:
    device_type: TStringValue
    device_identifier: Optional[TStringValue] = None
    model_number: Optional[TStringValue] = None
    field_index: Optional[int] = None


@dataclass
class DeviceSystemDocument:
    asset_management_identifier: TStringValue
    description: Optional[Any] = None
    brand_name: Optional[TStringValue] = None
    product_manufacturer: Optional[TStringValue] = None
    pump_model_number: Optional[TStringValue] = None
    detector_model_number: Optional[TStringValue] = None
    device_document: Optional[list[DeviceDocumentItem]] = None


@dataclass
class SampleDocument:
    sample_identifier: TStringValue
    description: Optional[Any] = None
    written_name: Optional[TStringValue] = None


@dataclass
class ChromatographyColumnDocument:
    chromatography_column_particle_size: Optional[TQuantityValueMicrometer] = None
    chromatography_column_chemistry_type: Optional[TStringValue] = None
    chromatography_column_serial_number: Optional[TStringValue] = None
    column_inner_diameter: Optional[TQuantityValueMillimeter] = None
    product_manufacturer: Optional[TStringValue] = None
    chromatography_column_length: Optional[TQuantityValueCentimeter] = None
    chromatography_column_part_number: Optional[TStringValue] = None


@dataclass
class DeviceControlDocumentItem:
    device_type: TStringValue
    device_identifier: Optional[TStringValue] = None
    product_manufacturer: Optional[TStringValue] = None
    brand_name: Optional[TStringValue] = None
    equipment_serial_number: Optional[TStringValue] = None
    model_number: Optional[TStringValue] = None
    firmware_version: Optional[TStringValue] = None
    detection_type: Optional[TStringValue] = None
    electronic_absorbance_wavelength_setting: Optional[TQuantityValueNanometer] = None
    electronic_absorbance_bandwidth_setting: Optional[TQuantityValueNanometer] = None
    electronic_absorbance_reference_bandwidth_setting: Optional[
        TQuantityValueNanometer
    ] = None
    electronic_absorbance_reference_wavelength_setting: Optional[
        TQuantityValueNanometer
    ] = None
    detector_offset_setting: Optional[TQuantityValue] = None
    detector_sampling_rate_setting: Optional[TQuantityValueHertz] = None
    field_index: Optional[int] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: list[DeviceControlDocumentItem]


@dataclass
class InjectionDocument:
    autosampler_injection_volume_setting__chromatography_: TQuantityValueCubicMillimeter
    injection_identifier: TStringValue
    injection_time: TDateTimeValue


@dataclass
class ProcessedDataDocument:
    peak_list: Optional[PeakList] = None


@dataclass
class DiagnosticTraceDocumentItem:
    description: Any
    data_cube: Optional[TDatacube] = None


@dataclass
class DiagnosticTraceAggregateDocument:
    diagnostic_trace_document: Optional[list[DiagnosticTraceDocumentItem]] = None


@dataclass
class MeasurementDocumentItem:
    chromatography_column_document: ChromatographyColumnDocument
    device_control_aggregate_document: DeviceControlAggregateDocument
    sample_document: SampleDocument
    injection_document: InjectionDocument
    detection_type: TStringValue
    chromatogram_data_cube: TDatacube
    measurement_identifier: Optional[TStringValue] = None
    three_dimensional_ultraviolet_spectrum_data_cube: Optional[TDatacube] = None
    processed_data_document: Optional[ProcessedDataDocument] = None
    diagnostic_trace_aggregate_document: Optional[
        DiagnosticTraceAggregateDocument
    ] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_document: list[MeasurementDocumentItem]


@dataclass
class LiquidChromatographyDocumentItem:
    analyst: TStringValue
    measurement_aggregate_document: MeasurementAggregateDocument
    submitter: Optional[TStringValue] = None


@dataclass
class LiquidChromatographyAggregateDocument:
    device_system_document: DeviceSystemDocument
    liquid_chromatography_document: list[LiquidChromatographyDocumentItem]


@dataclass
class Model:
    liquid_chromatography_aggregate_document: Optional[
        LiquidChromatographyAggregateDocument
    ] = None
    manifest: str = "http://purl.allotrope.org/manifests/liquid-chromatography/REC/2023/03/liquid-chromatography.manifest"

src/allotropy/allotrope/models/liquid_chromatography_rec_2023_03_liquid_chromatography.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  luminescence.json
#   timestamp: 2023-09-17T16:00:49+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional

from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMicroliter,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
    TQuantityValueNumber,
    TQuantityValuePicogramPerMilliliter,
    TQuantityValueSecondTime,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacube,
    TDateTimeValue,
    TStringValue,
)


class ContainerType(Enum):
    reactor = "reactor"
    controlled_lab_reactor = "controlled lab reactor"
    tube = "tube"
    well_plate = "well plate"
    differential_scanning_calorimetry_pan = "differential scanning calorimetry pan"
    qPCR_reaction_block = "qPCR reaction block"
    vial_rack = "vial rack"
    pan = "pan"
    reservoir = "reservoir"
    array_card_block = "array card block"
    capillary = "capillary"
    disintegration_apparatus_basket = "disintegration apparatus basket"
    jar = "jar"
    container = "container"
    tray = "tray"
    basket = "basket"
    cell_holder = "cell holder"


class ScanPositionSettingPlateReader(Enum):
    bottom_scan_position__plate_reader_ = "bottom scan position (plate reader)"
    scan_position_configuration__plate_reader_ = (
        "scan position configuration (plate reader)"
    )
    top_scan_position__plate_reader_ = "top scan position (plate reader)"


@dataclass
class DeviceSystemDocument:
    device_identifier: TStringValue
    model_number: TStringValue


@dataclass
class DeviceControlDocumentItem:
    device_type: Optional[TStringValue] = None
    shaking_configuration_description: Optional[TStringValue] = None
    detector_distance_setting__plate_reader_: Optional[TQuantityValueMillimeter] = None
    integration_time: Optional[TQuantityValueSecondTime] = None
    number_of_averages: Optional[TQuantityValueNumber] = None
    detector_gain_setting: Optional[TStringValue] = None
    scan_position_setting__plate_reader_: Optional[
        ScanPositionSettingPlateReader
    ] = None
    detector_carriage_speed_setting: Optional[TStringValue] = None
    detector_wavelength_setting: Optional[TQuantityValueNanometer] = None
    detector_bandwidth_setting: Optional[TQuantityValueNanometer] = None
    field_index: Optional[int] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: Optional[list[DeviceControlDocumentItem]] = None


@dataclass
class MeasurementDocumentItem:
    device_control_aggregate_document: DeviceControlAggregateDocument
    sample_document: SampleDocument
    data_cube: Optional[TDatacube] = None
    compartment_temperature: Optional[TQuantityValueDegreeCelsius] = None
    processed_data_aggregate_document: Optional[ProcessedDataAggregateDocument] = None
    mass_concentration: Optional[TQuantityValuePicogramPerMilliliter] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_identifier: TStringValue
    plate_well_count: TQuantityValueNumber
    measurement_document: list[MeasurementDocumentItem]
    measurement_time: Optional[TDateTimeValue] = None
    analyst: Optional[TStringValue] = None
    analytical_method_identifier: Optional[TStringValue] = None
    experimental_data_identifier: Optional[TStringValue] = None
    experiment_type: Optional[TStringValue] = None
    container_type: Optional[ContainerType] = None
    well_volume: Optional[TQuantityValueMicroliter] = None
    device_system_document: Optional[DeviceSystemDocument] = None


@dataclass
class Model:
    measurement_aggregate_document: Optional[MeasurementAggregateDocument] = None
    manifest: str = "http://purl.allotrope.org/manifests/luminescence/BENCHLING/2023/09/luminescence.manifest"

src/allotropy/allotrope/models/luminescence_benchling_2023_09_luminescence.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  dpcr.json
#   timestamp: 2023-10-04T16:33:51+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional

from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMicroliter,
    TQuantityValueNumber,
    TQuantityValueNumberPerMicroliter,
    TQuantityValueSecondTime,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDateTimeValue,
    TQuantityValue,
    TStringValue,
)


class ExperimentType(Enum):
    dPCR_experiment = "dPCR experiment"


class ContainerType(Enum):
    reactor = "reactor"
    controlled_lab_reactor = "controlled lab reactor"
    tube = "tube"
    well_plate = "well plate"
    differential_scanning_calorimetry_pan = "differential scanning calorimetry pan"
    qPCR_reaction_block = "qPCR reaction block"
    vial_rack = "vial rack"
    pan = "pan"
    reservoir = "reservoir"
    array_card_block = "array card block"
    capillary = "capillary"
    disintegration_apparatus_basket = "disintegration apparatus basket"
    jar = "jar"
    container = "container"
    tray = "tray"
    basket = "basket"
    cell_holder = "cell holder"


@dataclass
class DeviceSystemDocument:
    device_identifier: TStringValue
    asset_management_identifier: Optional[TStringValue] = None
    model_number: Optional[TStringValue] = None
    device_serial_number: Optional[TStringValue] = None
    firmware_version: Optional[TStringValue] = None
    description: Optional[TStringValue] = None
    brand_name: Optional[TStringValue] = None
    product_manufacturer: Optional[TStringValue] = None


@dataclass
class DataSystemDocument:
    data_system_instance_identifier: Optional[TStringValue] = None
    file_name: Optional[TStringValue] = None
    UNC_path: Optional[TStringValue] = None
    software_name: Optional[TStringValue] = None
    software_version: Optional[TStringValue] = None
    ASM_converter_name: Optional[TStringValue] = None
    ASM_converter_version: Optional[TStringValue] = None


@dataclass
class SampleDocument:
    sample_identifier: TStringValue
    batch_identifier: Optional[TStringValue] = None
    sample_role_type: Optional[TStringValue] = None
    well_location_identifier: Optional[TStringValue] = None
    well_plate_identifier: Optional[TStringValue] = None


@dataclass
class DataSourceDocumentItem:
    data_source_identifier: Optional[TStringValue] = None
    data_source_feature: Optional[TStringValue] = None


@dataclass
class DataSourceAggregateDocument:
    data_source_document: Optional[list[DataSourceDocumentItem]] = None


@dataclass
class DataProcessingDocument1:
    reference_DNA_description: Optional[TStringValue] = None
    reference_DNA_copy_number_setting: Optional[TQuantityValueNumber] = None


@dataclass
class CalculatedDataDocumentItem:
    calculated_data_identifier: Optional[TStringValue] = None
    data_source_aggregate_document: Optional[DataSourceAggregateDocument] = None
    data_processing_document: Optional[DataProcessingDocument1] = None
    calculated_data_name: Optional[TStringValue] = None
    calculated_data_description: Optional[TStringValue] = None
    calculated_datum: Optional[TQuantityValue] = None


@dataclass
class TCalculatedDataAggregateDocument:
    calculated_data_document: Optional[list[CalculatedDataDocumentItem]] = None


@dataclass
class DeviceControlDocumentItem:
    device_type: TStringValue
    device_identifier: Optional[TStringValue] = None
    detection_type: Optional[TStringValue] = None
    measurement_method_identifier: Optional[TStringValue] = None
    total_cycle_number_setting: Optional[TQuantityValueNumber] = None
    denaturing_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    denaturing_time_setting: Optional[TQuantityValueSecondTime] = None
    annealing_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    annealing_time_setting: Optional[TQuantityValueSecondTime] = None
    extension_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    extension_time_setting: Optional[TQuantityValueSecondTime] = None
    PCR_detection_chemistry: Optional[TStringValue] = None
    reporter_dye_setting: Optional[TStringValue] = None
    quencher_dye_setting: Optional[TStringValue] = None
    passive_reference_dye_setting: Optional[TStringValue] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: list[DeviceControlDocumentItem]


@dataclass
class DataProcessingDocument:
    flourescence_intensity_threshold_setting: Optional[TQuantityValueUnitless] = None


@dataclass
class ProcessedDataDocumentItem:
    number_concentration: TQuantityValueNumberPerMicroliter
    positive_partition_count: TQuantityValueNumber
    data_processing_document: Optional[DataProcessingDocument] = None
    negative_partition_count: Optional[TQuantityValueNumber] = None


@dataclass
class ProcessedDataAggregateDocument:
    processed_data_document: list[ProcessedDataDocumentItem]


@dataclass
class MeasurementDocumentItem:
    measurement_identifier: TStringValue
    measurement_time: TDateTimeValue
    target_DNA_description: TStringValue
    sample_document: SampleDocument
    device_control_aggregate_document: DeviceControlAggregateDocument
    total_partition_count: TQuantityValueNumber
    processed_data_aggregate_document: ProcessedDataAggregateDocument


@dataclass
class MeasurementAggregateDocument:
    plate_well_count: TQuantityValueNumber
    measurement_document: list[MeasurementDocumentItem]
    analytical_method_identifier: Optional[TStringValue] = None
    experimental_data_identifier: Optional[TStringValue] = None
    experiment_type: Optional[ExperimentType] = None
    container_type: Optional[ContainerType] = None
    well_volume: Optional[TQuantityValueMicroliter] = None


@dataclass
class DPCRDocumentItem:
    measurement_aggregate_document: MeasurementAggregateDocument
    analyst: Optional[TStringValue] = None
    submitter: Optional[TStringValue] = None
    calculated_data_aggregate_document: Optional[
        TCalculatedDataAggregateDocument
    ] = None


@dataclass
class DPCRAggregateDocument:
    device_system_document: DeviceSystemDocument
    dPCR_document: list[DPCRDocumentItem]
    data_system_document: Optional[DataSystemDocument] = None
    calculated_data_aggregate_document: Optional[
        TCalculatedDataAggregateDocument
    ] = None


@dataclass
class Model:
    dPCR_aggregate_document: Optional[DPCRAggregateDocument] = None
    manifest: str = (
        "http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/dpcr.manifest"
    )

src/allotropy/allotrope/models/pcr_benchling_2023_09_dpcr.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  qpcr.json
#   timestamp: 2023-10-03T19:40:49+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional

from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueUnitless,
    TQuantityValueDegreeCelsius,
    TQuantityValueMicroliter,
    TQuantityValueNumber,
    TQuantityValuePicogramPerMilliliter,
    TQuantityValueSecondTime,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TBooleanValue,
    TDatacubeData,
    TDatacubeStructure,
    TDateTimeValue,
    TStringValue,
)


class ExperimentType(Enum):
    genotyping_qPCR_experiment = "genotyping qPCR experiment"
    relative_standard_curve_qPCR_experiment = "relative standard curve qPCR experiment"
    melt_curve_qPCR_experiment = "melt curve qPCR experiment"
    comparative_CT_qPCR_experiment = "comparative CT qPCR experiment"
    standard_curve_qPCR_experiment = "standard curve qPCR experiment"
    qPCR_experiment = "qPCR experiment"
    presence_absence_qPCR_experiment = "presence/absence qPCR experiment"


class ContainerType(Enum):
    reactor = "reactor"
    controlled_lab_reactor = "controlled lab reactor"
    tube = "tube"
    well_plate = "well plate"
    differential_scanning_calorimetry_pan = "differential scanning calorimetry pan"
    qPCR_reaction_block = "qPCR reaction block"
    vial_rack = "vial rack"
    pan = "pan"
    reservoir = "reservoir"
    array_card_block = "array card block"
    capillary = "capillary"
    disintegration_apparatus_basket = "disintegration apparatus basket"
    jar = "jar"
    container = "container"
    tray = "tray"
    basket = "basket"
    cell_holder = "cell holder"


@dataclass
class DeviceSystemDocument:
    device_identifier: TStringValue
    model_number: TStringValue
    device_serial_number: TStringValue
    asset_management_identifier: Optional[TStringValue] = None
    firmware_version: Optional[TStringValue] = None
    description: Optional[TStringValue] = None
    brand_name: Optional[TStringValue] = None
    product_manufacturer: Optional[TStringValue] = None


@dataclass
class DataSystemDocument:
    data_system_instance_identifier: Optional[TStringValue] = None
    file_name: Optional[TStringValue] = None
    UNC_path: Optional[TStringValue] = None
    software_name: Optional[TStringValue] = None
    software_version: Optional[TStringValue] = None
    ASM_converter_name: Optional[TStringValue] = None
    ASM_converter_version: Optional[TStringValue] = None


@dataclass
class DataSourceDocumentItem:
    data_source_identifier: Optional[TStringValue] = None
    data_source_feature: Optional[TStringValue] = None


@dataclass
class DataSourceAggregateDocument:
    data_source_document: Optional[list[DataSourceDocumentItem]] = None


@dataclass
class DataProcessingDocument1:
    reference_DNA_description: Optional[TStringValue] = None
    reference_sample_description: Optional[TStringValue] = None


@dataclass
class CalculatedDataDocumentItem:
    calculated_data_identifier: Optional[TStringValue] = None
    data_source_aggregate_document: Optional[DataSourceAggregateDocument] = None
    data_processing_document: Optional[DataProcessingDocument1] = None
    calculated_data_name: Optional[TStringValue] = None
    calculated_data_description: Optional[TStringValue] = None
    calculated_datum: Optional[TQuantityValueUnitless] = None


@dataclass
class TCalculatedDataAggregateDocument:
    calculated_data_document: Optional[list[CalculatedDataDocumentItem]] = None


@dataclass
class SampleDocument:
    sample_identifier: TStringValue
    batch_identifier: Optional[TStringValue] = None
    sample_role_type: Optional[TStringValue] = None
    well_location_identifier: Optional[TStringValue] = None
    well_plate_identifier: Optional[TStringValue] = None
    mass_concentration: Optional[TQuantityValuePicogramPerMilliliter] = None


@dataclass
class DeviceControlDocumentItem:
    device_type: TStringValue
    measurement_method_identifier: TStringValue
    PCR_detection_chemistry: TStringValue
    device_identifier: Optional[TStringValue] = None
    detection_type: Optional[TStringValue] = None
    total_cycle_number_setting: Optional[TQuantityValueNumber] = None
    denaturing_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    denaturing_time_setting: Optional[TQuantityValueSecondTime] = None
    annealing_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    annealing_time_setting: Optional[TQuantityValueSecondTime] = None
    extension_temperature_setting: Optional[TQuantityValueDegreeCelsius] = None
    extension_time_setting: Optional[TQuantityValueSecondTime] = None
    reporter_dye_setting: Optional[TStringValue] = None
    quencher_dye_setting: Optional[TStringValue] = None
    passive_reference_dye_setting: Optional[TStringValue] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: list[DeviceControlDocumentItem]


@dataclass
class DataProcessingDocument:
    cycle_threshold_value_setting: TQuantityValueUnitless
    automatic_cycle_threshold_enabled_setting: Optional[TBooleanValue] = None
    automatic_baseline_determination_enabled_setting: Optional[TBooleanValue] = None
    baseline_determination_start_cycle_setting: Optional[TQuantityValueNumber] = None
    baseline_determination_end_cycle_setting: Optional[TQuantityValueNumber] = None
    genotyping_determination_method: Optional[TStringValue] = None
    genotyping_determination_method_setting: Optional[TQuantityValueUnitless] = None


@dataclass
class NormalizedReporterDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class BaselineCorrectedReporterDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class ProcessedDataDocumentItem:
    data_processing_document: DataProcessingDocument
    cycle_threshold_result: TNullableQuantityValueUnitless
    normalized_reporter_result: Optional[TQuantityValueUnitless] = None
    normalized_reporter_data_cube: Optional[NormalizedReporterDataCube] = None
    baseline_corrected_reporter_result: Optional[TQuantityValueUnitless] = None
    baseline_corrected_reporter_data_cube: Optional[
        BaselineCorrectedReporterDataCube
    ] = None
    genotyping_determination_result: Optional[TStringValue] = None


@dataclass
class ProcessedDataAggregateDocument:
    processed_data_document: list[ProcessedDataDocumentItem]


@dataclass
class ReporterDyeDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class PassiveReferenceDyeDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class MeltingCurveDataCube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None


@dataclass
class MeasurementDocumentItem:
    measurement_identifier: TStringValue
    measurement_time: TDateTimeValue
    target_DNA_description: TStringValue
    sample_document: SampleDocument
    device_control_aggregate_document: DeviceControlAggregateDocument
    processed_data_aggregate_document: ProcessedDataAggregateDocument
    reporter_dye_data_cube: Optional[ReporterDyeDataCube] = None
    passive_reference_dye_data_cube: Optional[PassiveReferenceDyeDataCube] = None
    melting_curve_data_cube: Optional[MeltingCurveDataCube] = None


@dataclass
class MeasurementAggregateDocument:
    plate_well_count: TQuantityValueNumber
    measurement_document: list[MeasurementDocumentItem]
    analytical_method_identifier: Optional[TStringValue] = None
    experimental_data_identifier: Optional[TStringValue] = None
    experiment_type: Optional[ExperimentType] = None
    container_type: Optional[ContainerType] = None
    well_volume: Optional[TQuantityValueMicroliter] = None


@dataclass
class QPCRDocumentItem:
    measurement_aggregate_document: MeasurementAggregateDocument
    analyst: Optional[TStringValue] = None
    submitter: Optional[TStringValue] = None
    calculated_data_aggregate_document: Optional[
        TCalculatedDataAggregateDocument
    ] = None


@dataclass
class QPCRAggregateDocument:
    device_system_document: DeviceSystemDocument
    qPCR_document: list[QPCRDocumentItem]
    data_system_document: Optional[DataSystemDocument] = None
    calculated_data_aggregate_document: Optional[
        TCalculatedDataAggregateDocument
    ] = None


@dataclass
class Model:
    qPCR_aggregate_document: Optional[QPCRAggregateDocument] = None
    manifest: str = (
        "http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest"
    )

src/allotropy/allotrope/models/pcr_benchling_2023_09_qpcr.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValuePercent,
    TQuantityValueSecondTime,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TQuantityValue,
    TStringValue,
)


@dataclass
class PeakItem:
    retention_time: TQuantityValueSecondTime
    peak_end: Optional[TQuantityValueSecondTime] = None
    identifier: Optional[TStringValue] = None
    relative_peak_height: Optional[TQuantityValuePercent] = None
    written_name: Optional[TStringValue] = None
    peak_height: Optional[TQuantityValue] = None
    capacity_factor__chromatography_: Optional[TQuantityValueUnitless] = None
    peak_area: Optional[TQuantityValue] = None
    relative_peak_area: Optional[TQuantityValuePercent] = None
    peak_start: Optional[TQuantityValueSecondTime] = None
    peak_selectivity__chromatography_: Optional[TQuantityValueUnitless] = None

    peak_width_at_4_4___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_13_4___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_32_4___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_60_7___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_half_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_5___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width_at_baseline: Optional[TQuantityValueSecondTime] = None
    peak_width_at_inflection: Optional[TQuantityValueSecondTime] = None
    peak_width_at_10___of_height: Optional[TQuantityValueSecondTime] = None
    peak_width: Optional[TQuantityValueSecondTime] = None
    statistical_skew__chromatography_: Optional[TQuantityValueUnitless] = None
    asymmetry_factor_measured_at_5___height: Optional[TQuantityValueUnitless] = None
    asymmetry_factor_measured_at_10___height: Optional[TQuantityValueUnitless] = None
    asymmetry_factor_squared_measured_at_10___height: Optional[
        TQuantityValueUnitless
    ] = None
    asymmetry_factor_squared_measured_at_4_4___height: Optional[
        TQuantityValueUnitless
    ] = None
    asymmetry_factor_measured_at_4_4___height: Optional[TQuantityValueUnitless] = None
    chromatographic_peak_asymmetry_factor: Optional[TQuantityValueUnitless] = None
    asymmetry_factor_measured_at_baseline: Optional[TQuantityValueUnitless] = None
    chromatographic_peak_resolution: Optional[TQuantityValueUnitless] = None
    chromatographic_peak_resolution_using_baseline_peak_widths: Optional[
        TQuantityValueUnitless
    ] = None
    chromatographic_peak_resolution_using_peak_width_at_half_height: Optional[
        TQuantityValueUnitless
    ] = None
    chromatographic_peak_resolution_using_statistical_moments: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates__chromatography_: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_measured_at_60_7___of_peak_height: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_measured_at_32_4___of_peak_height: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_measured_at_13_4___of_peak_height: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_measured_at_4_4___of_peak_height: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_by_tangent_method: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_by_peak_width_at_half_height: Optional[
        TQuantityValueUnitless
    ] = None
    number_of_theoretical_plates_by_peak_width_at_half_height__JP14_: Optional[
        TQuantityValueUnitless
    ] = None

    def __post_init__(self) -> None:
        any_of_keys = [
            "peak_width_at_4_4___of_height",
            "peak_width_at_13_4___of_height",
            "peak_width_at_32_4___of_height",
            "peak_width_at_60_7___of_height",
            "peak_width_at_half_height",
            "peak_width_at_5___of_height",
            "peak_width_at_baseline",
            "peak_width_at_inflection",
            "peak_width_at_10___of_height",
            "peak_width",
            "statistical_skew__chromatography_",
            "asymmetry_factor_measured_at_5___height",
            "asymmetry_factor_measured_at_10___height",
            "asymmetry_factor_squared_measured_at_10___height",
            "asymmetry_factor_squared_measured_at_4_4___height",
            "asymmetry_factor_measured_at_4_4___height",
            "chromatographic_peak_asymmetry_factor",
            "asymmetry_factor_measured_at_baseline",
            "chromatographic_peak_resolution",
            "chromatographic_peak_resolution_using_baseline_peak_widths",
            "chromatographic_peak_resolution_using_peak_width_at_half_height",
            "chromatographic_peak_resolution_using_statistical_moments",
            "number_of_theoretical_plates__chromatography_",
            "number_of_theoretical_plates_measured_at_60_7___of_peak_height",
            "number_of_theoretical_plates_measured_at_32_4___of_peak_height",
            "number_of_theoretical_plates_measured_at_13_4___of_peak_height",
            "number_of_theoretical_plates_measured_at_4_4___of_peak_height",
            "number_of_theoretical_plates_by_tangent_method",
            "number_of_theoretical_plates_by_peak_width_at_half_height",
            "number_of_theoretical_plates_by_peak_width_at_half_height__JP14_",
        ]
        # Logic for enforcing anyOf
        if all(getattr(self, key) is None for key in any_of_keys):
            error = f"At least one of {any_of_keys} must be set."
            raise ValueError(error)


@dataclass
class PeakList:
    peak: Optional[list[PeakItem]] = None

src/allotropy/allotrope/models/shared/components/liquid_chromatography.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional, Union

from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacube,
    TStringValue,
)


class SampleRoleType(Enum):
    control_sample_role = "control sample role"
    standard_sample_role = "standard sample role"
    validation_sample_role = "validation sample role"
    experiment_sample_role = "experiment sample role"
    sample_role = "sample role"
    spiked_sample_role = "spiked sample role"
    blank_role = "blank role"
    unknown_sample_role = "unknown sample role"
    undefined_sample_role = "undefined sample role"
    calibration_sample_role = "calibration sample role"
    unspiked_sample_role = "unspiked sample role"
    specimen_role = "specimen role"
    quality_control_sample_role = "quality control sample role"
    reference_sample_role = "reference sample role"


@dataclass
class SampleDocument:
    well_location_identifier: TStringValue
    sample_identifier: Optional[TStringValue] = None
    batch_identifier: Optional[TStringValue] = None
    sample_role_type: Optional[SampleRoleType] = None
    # TODO: plate_barcode isn't actually in the schema yet, needs to be modified to add it.
    plate_barcode: Optional[str] = None


@dataclass
class ProcessedDataDocumentItem:
    processed_data: Union[float, str, TDatacube]
    data_format_specification_type: Optional[TStringValue] = None
    data_processing_description: Optional[TStringValue] = None


@dataclass
class ProcessedDataAggregateDocument:
    processed_data_document: Optional[list[ProcessedDataDocumentItem]] = None

src/allotropy/allotrope/models/shared/components/plate_reader.py
Benchling-Open-Source/allotropy
from dataclasses import dataclass

from allotropy.allotrope.models.shared.definitions.definitions import (
    TNullableQuantityValueWithOptionalUnit,
    TQuantityValueWithOptionalUnit,
)
from allotropy.allotrope.models.shared.definitions.units import (
    Cell,
    Centimeter,
    CubicMillimeter,
    DegreeCelsius,
    GramPerLiter,
    Hertz,
    Microliter,
    Micrometer,
    Millimeter,
    MillimeterOfMercury,
    MillimolePerLiter,
    MillionCellsPerMilliliter,
    MilliOsmolesPerKilogram,
    MilliSecond,
    Nanometer,
    Number,
    NumberPerMicroliter,
    OpticalDensity,
    Percent,
    PH,
    PicogramPerMilliliter,
    SecondTime,
    TODO,
    Unitless,
    UnitPerLiter,
)


@dataclass
class TQuantityValueCentimeter(Centimeter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMillimeter(Millimeter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueNanometer(Nanometer, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMicrometer(Micrometer, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueHertz(Hertz, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueCubicMillimeter(CubicMillimeter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueUnitless(Unitless, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueSecondTime(SecondTime, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValuePercent(Percent, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueCell(Cell, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMicroliter(Microliter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueNumberPerMicroliter(
    NumberPerMicroliter, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TQuantityValueMilliSecond(MilliSecond, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMillionCellsPerMilliliter(
    MillionCellsPerMilliliter, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TQuantityValueTODO(TODO, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueGramPerLiter(GramPerLiter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueDegreeCelsius(DegreeCelsius, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueNumber(Number, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValuePicogramPerMilliliter(
    PicogramPerMilliliter, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TQuantityValueUnitPerLiter(UnitPerLiter, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMillimeterOfMercury(
    MillimeterOfMercury, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TQuantityValueOpticalDensity(OpticalDensity, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValuePH(PH, TQuantityValueWithOptionalUnit):
    pass


@dataclass
class TQuantityValueMilliOsmolesPerKilogram(
    MilliOsmolesPerKilogram, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TQuantityValueMillimolePerLiter(
    MillimolePerLiter, TQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueCell(Cell, TNullableQuantityValueWithOptionalUnit):
    pass


@dataclass
class TNullableQuantityValueGramPerLiter(
    GramPerLiter, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueMicrometer(
    Micrometer, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueMilliOsmolesPerKilogram(
    MilliOsmolesPerKilogram, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueMillimeterOfMercury(
    MillimeterOfMercury, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueMillimolePerLiter(
    MillimolePerLiter, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueMillionCellsPerMilliliter(
    MillionCellsPerMilliliter, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueOpticalDensity(
    OpticalDensity, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValuePH(PH, TNullableQuantityValueWithOptionalUnit):
    pass


@dataclass
class TNullableQuantityValuePercent(Percent, TNullableQuantityValueWithOptionalUnit):
    pass


@dataclass
class TNullableQuantityValueTODO(TODO, TNullableQuantityValueWithOptionalUnit):
    pass


@dataclass
class TNullableQuantityValueUnitPerLiter(
    UnitPerLiter, TNullableQuantityValueWithOptionalUnit
):
    pass


@dataclass
class TNullableQuantityValueUnitless(Unitless, TNullableQuantityValueWithOptionalUnit):
    pass

src/allotropy/allotrope/models/shared/definitions/custom.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional, Union

TTupleData = list[Optional[Union[float, bool, str]]]


TUnit = str


TBooleanArray = list[bool]


TBooleanOrNullArray = list[Optional[bool]]


TNumberArray = list[float]


TNumberOrNullArray = list[Optional[float]]


TStringArray = list[str]


TStringOrNullArray = list[Optional[str]]


TClass = str


@dataclass
class TStringValueItem:
    value: str
    field_type: str


TStringValue = Union[str, TStringValueItem]


@dataclass
class TDateTimeValueItem:
    field_type: TClass
    value: str


TDateTimeValue = Union[str, TDateTimeValueItem]


@dataclass
class TBooleanValueItem:
    field_type: TClass
    value: bool


TBooleanValue = Union[bool, TBooleanValueItem]


class TStatisticDatumRole(Enum):
    arithmetic_mean_role = "arithmetic mean role"
    median_role = "median role"
    relative_standard_deviation_role = "relative standard deviation role"
    skewness_role = "skewness role"
    standard_deviation_role = "standard deviation role"
    variance_role = "variance role"
    maximum_value_role = "maximum value role"
    minimum_value_role = "minimum value role"


@dataclass
class TQuantityValue:
    value: float
    unit: TUnit
    has_statistic_datum_role: Optional[TStatisticDatumRole] = None
    field_type: Optional[TClass] = None


@dataclass
class TNullableQuantityValue:
    value: Optional[float]
    unit: TUnit
    has_statistic_datum_role: Optional[TStatisticDatumRole] = None
    field_type: Optional[TClass] = None


# NOTE: this is defined to allow override of unit default for TQuaniityValue<Unit> (otherwise mypy gets mad)
@dataclass
class TQuantityValueWithOptionalUnit:
    value: float
    unit: Optional[TUnit]
    has_statistic_datum_role: Optional[TStatisticDatumRole] = None
    field_type: Optional[TClass] = None


@dataclass
class TNullableQuantityValueWithOptionalUnit:
    value: Optional[float]
    unit: Optional[TUnit]
    has_statistic_datum_role: Optional[TStatisticDatumRole] = None
    field_type: Optional[TClass] = None


class FieldComponentDatatype(Enum):
    double = "double"
    float = "float"  # noqa: A003
    decimal = "decimal"
    integer = "integer"
    byte = "byte"
    int = "int"  # noqa: A003
    short = "short"
    long = "long"
    string = "string"
    boolean = "boolean"
    dateTime = "dateTime"


class Scale(Enum):
    nominal = "nominal"
    ordinal = "ordinal"
    cardinal = "cardinal"
    interval = "interval"
    range = "range"  # noqa: A003


class Type(Enum):
    linear = "linear"
    logarithmic = "logarithmic"


@dataclass
class TFunction:
    type: Optional[Type] = Type.linear  # noqa: A003
    start: Optional[float] = 1
    length: Optional[float] = None
    incr: Optional[float] = 1


@dataclass
class TDatacubeComponent:
    field_componentDatatype: FieldComponentDatatype
    concept: TClass
    unit: Optional[TUnit] = None
    scale: Optional[Scale] = None
    field_asm_fill_value: Optional[Union[str, float, int, bool]] = None


TDimensionArray = Union[TNumberArray, TBooleanArray, TStringArray]


TMeasureArray = Union[TNumberOrNullArray, TBooleanOrNullArray, TStringOrNullArray]


@dataclass
class TDatacubeStructure:
    dimensions: list[TDatacubeComponent]
    measures: list[TDatacubeComponent]


"""
TODO: datamodel-codegen cannot properly generate the models for TDatacubeData with the
oneOf{measures, points} constraint. I can't figure out how to do it correctly right now either.
We need to either figure this out and file a bug with datamodel-codegen or fix the schema.

@dataclass
class TDimensionData:
    dimensions: List[Union[TDimensionArray, TFunction]]


@dataclass
class TMeasureDatum:
    measures: List[TMeasureArray]


@dataclass
class TMeasureDatum1:
    points: List[TTupleData]


TMeasureData = Union[TMeasureDatum, TMeasureDatum1]


@dataclass
class TDatacubeData(TDimensionData):
    pass
"""


@dataclass
class TDatacubeData:
    dimensions: list[Union[TDimensionArray, TFunction]]
    measures: Optional[list[TMeasureArray]] = None
    points: Optional[list[TTupleData]] = None

    def __post_init__(self) -> None:
        # Logic for enforcing oneOf
        if not (self.measures is None) ^ (self.points is None):
            error = "Exactly one of measures or points must be set"
            raise ValueError(error)


@dataclass
class TDatacube:
    label: Optional[str] = None
    cube_structure: Optional[TDatacubeStructure] = None
    data: Optional[TDatacubeData] = None

src/allotropy/allotrope/models/shared/definitions/definitions.py
Benchling-Open-Source/allotropy
from dataclasses import dataclass
from typing import Optional


@dataclass
class Centimeter:
    unit: Optional[str] = "cm"


@dataclass
class Millimeter:
    unit: Optional[str] = "mm"


@dataclass
class Nanometer:
    unit: Optional[str] = "nm"


@dataclass
class Micrometer:
    unit: Optional[str] = "μm"


@dataclass
class Hertz:
    unit: Optional[str] = "Hz"


@dataclass
class CubicMillimeter:
    unit: Optional[str] = "mm^3"


@dataclass
class Unitless:
    unit: Optional[str] = "(unitless)"


@dataclass
class SecondTime:
    unit: Optional[str] = "s"


@dataclass
class Percent:
    unit: Optional[str] = "%"


@dataclass
class Cell:
    unit: Optional[str] = "cell"


@dataclass
class Microliter:
    unit: Optional[str] = "μL"


@dataclass
class NumberPerMicroliter:
    unit: Optional[str] = "#/μL"


@dataclass
class MilliSecond:
    unit: Optional[str] = "ms"


@dataclass
class MillionCellsPerMilliliter:
    unit: Optional[str] = "10^6 cells/mL"


@dataclass
class TODO:
    unit: Optional[str] = "TODO"


@dataclass
class DegreeCelsius:
    unit: Optional[str] = "degC"


@dataclass
class Number:
    unit: Optional[str] = "#"


@dataclass
class PicogramPerMilliliter:
    unit: Optional[str] = "pg/mL"


@dataclass
class GramPerLiter:
    unit: Optional[str] = "g/L"


@dataclass
class UnitPerLiter:
    unit: Optional[str] = "U/L"


@dataclass
class MillimeterOfMercury:
    unit: Optional[str] = "mmHg"


@dataclass
class OpticalDensity:
    unit: Optional[str] = "OD"


@dataclass
class PH:
    unit: Optional[str] = "pH"


@dataclass
class MilliOsmolesPerKilogram:
    unit: Optional[str] = "mosm/kg"


@dataclass
class MillimolePerLiter:
    unit: Optional[str] = "mmol/L"

src/allotropy/allotrope/models/shared/definitions/units.py
Benchling-Open-Source/allotropy
# generated by datamodel-codegen:
#   filename:  ultraviolet-absorbance.json
#   timestamp: 2023-09-17T16:00:51+00:00

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional

from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMicroliter,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
    TQuantityValueNumber,
    TQuantityValuePicogramPerMilliliter,
    TQuantityValueSecondTime,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    TDatacube,
    TDateTimeValue,
    TStringValue,
)


class ContainerType(Enum):
    reactor = "reactor"
    controlled_lab_reactor = "controlled lab reactor"
    tube = "tube"
    well_plate = "well plate"
    differential_scanning_calorimetry_pan = "differential scanning calorimetry pan"
    qPCR_reaction_block = "qPCR reaction block"
    vial_rack = "vial rack"
    pan = "pan"
    reservoir = "reservoir"
    array_card_block = "array card block"
    capillary = "capillary"
    disintegration_apparatus_basket = "disintegration apparatus basket"
    jar = "jar"
    container = "container"
    tray = "tray"
    basket = "basket"
    cell_holder = "cell holder"


class ScanPositionSettingPlateReader(Enum):
    bottom_scan_position__plate_reader_ = "bottom scan position (plate reader)"
    scan_position_configuration__plate_reader_ = (
        "scan position configuration (plate reader)"
    )
    top_scan_position__plate_reader_ = "top scan position (plate reader)"


@dataclass
class DeviceSystemDocument:
    device_identifier: TStringValue
    model_number: TStringValue


@dataclass
class DeviceControlDocumentItem:
    device_type: Optional[TStringValue] = None
    shaking_configuration_description: Optional[TStringValue] = None
    detector_distance_setting__plate_reader_: Optional[TQuantityValueMillimeter] = None
    integration_time: Optional[TQuantityValueSecondTime] = None
    number_of_averages: Optional[TQuantityValueNumber] = None
    detector_gain_setting: Optional[TStringValue] = None
    scan_position_setting__plate_reader_: Optional[
        ScanPositionSettingPlateReader
    ] = None
    detector_carriage_speed_setting: Optional[TStringValue] = None
    detector_wavelength_setting: Optional[TQuantityValueNanometer] = None
    detector_bandwidth_setting: Optional[TQuantityValueNanometer] = None
    field_index: Optional[int] = None


@dataclass
class DeviceControlAggregateDocument:
    device_control_document: Optional[list[DeviceControlDocumentItem]] = None


@dataclass
class MeasurementDocumentItem:
    device_control_aggregate_document: DeviceControlAggregateDocument
    sample_document: SampleDocument
    data_cube: Optional[TDatacube] = None
    compartment_temperature: Optional[TQuantityValueDegreeCelsius] = None
    processed_data_aggregate_document: Optional[ProcessedDataAggregateDocument] = None
    mass_concentration: Optional[TQuantityValuePicogramPerMilliliter] = None


@dataclass
class MeasurementAggregateDocument:
    measurement_identifier: TStringValue
    plate_well_count: TQuantityValueNumber
    measurement_document: list[MeasurementDocumentItem]
    measurement_time: Optional[TDateTimeValue] = None
    analyst: Optional[TStringValue] = None
    analytical_method_identifier: Optional[TStringValue] = None
    experimental_data_identifier: Optional[TStringValue] = None
    experiment_type: Optional[TStringValue] = None
    container_type: Optional[ContainerType] = None
    well_volume: Optional[TQuantityValueMicroliter] = None
    device_system_document: Optional[DeviceSystemDocument] = None


@dataclass
class Model:
    measurement_aggregate_document: Optional[MeasurementAggregateDocument] = None
    manifest: str = "http://purl.allotrope.org/manifests/ultraviolet-absorbance/BENCHLING/2023/09/ultraviolet-absorbance.manifest"

src/allotropy/allotrope/models/ultraviolet_absorbance_benchling_2023_09_ultraviolet_absorbance.py
Benchling-Open-Source/allotropy
import json
import os
from pathlib import Path
import re
import subprocess  # noqa: S404, RUF100

from autoflake import fix_file  # type: ignore[import-untyped]
from datamodel_code_generator import (
    DataModelType,
    generate,
    InputFileType,
    PythonVersion,
)

from allotropy.allotrope.schema_parser.model_class_editor import modify_file
from allotropy.allotrope.schemas import get_schema

SCHEMA_DIR_PATH = "src/allotropy/allotrope/schemas"
MODEL_DIR_PATH = "src/allotropy/allotrope/models"


def lint_file(model_path: str) -> None:
    # The first run of ruff changes typing annotations and causes unused imports. We catch failure
    # due to unused imports.
    try:
        subprocess.check_call(
            f"ruff {model_path} --fix",
            shell=True,  # noqa: S602
            stdout=subprocess.DEVNULL,
        )
    except subprocess.CalledProcessError:
        pass
    # The call to autoflake.fix_file removes unused imports.
    fix_file(
        model_path,
        {
            "in_place": True,
            "remove_unused_variables": True,
            "write_to_stdout": False,
            "ignore_init_module_imports": False,
            "expand_star_imports": False,
            "remove_all_unused_imports": True,
            "remove_duplicate_keys": True,
            "remove_rhs_for_unused_variables": False,
            "ignore_pass_statements": False,
            "ignore_pass_after_docstring": False,
            "check": False,
            "check_diff": False,
        },
    )
    # The second call to ruff checks for additional rules.
    subprocess.check_call(
        f"ruff {model_path} --fix", shell=True, stdout=subprocess.DEVNULL  # noqa: S602
    )
    subprocess.check_call(
        f"black {model_path}", shell=True, stderr=subprocess.DEVNULL  # noqa: S602
    )


def files_equal(path1: str, path2: str) -> bool:
    with open(path1) as file1, open(path2) as file2:
        for line1, line2 in zip(file1, file2):
            if line1 != line2 and not line1.startswith("#   timestamp:"):
                return False
    return True


def generate_schemas(root_dir: Path) -> int:
    """Generate schemas from JSON schema files.

    :root_dir: The root directory of the project.
    :return: The number of schemas generated.
    """

    os.chdir(os.path.join(root_dir, SCHEMA_DIR_PATH))
    schema_paths = list(Path(".").rglob("*.json"))
    os.chdir(os.path.join(root_dir))
    number_generated = 0
    for rel_schema_path in schema_paths:
        if str(rel_schema_path).startswith("shared"):
            continue
        print(f"Generating models for schema: {rel_schema_path}...")  # noqa: T201
        schema_name = rel_schema_path.stem
        schema_path = os.path.join(root_dir, SCHEMA_DIR_PATH, rel_schema_path)
        output_file = re.sub(
            "/|-", "_", f"{rel_schema_path.parent}_{schema_name}.py"
        ).lower()
        model_path = os.path.join(root_dir, MODEL_DIR_PATH, output_file)
        model_backup_path = f"{model_path}.bak"
        schema_backup_path = f"{schema_path}.bak"

        # Backup model to do diff after generation
        if os.path.exists(model_path):
            os.rename(model_path, model_backup_path)

        # Backup schema and override with extra defs
        schema = get_schema(str(rel_schema_path))
        os.rename(schema_path, schema_backup_path)
        with open(schema_path, "w") as f:
            json.dump(schema, f)

        # Generate models
        generate(
            input_=Path(schema_path),
            output=Path(model_path),
            output_model_type=DataModelType.DataclassesDataclass,
            input_file_type=InputFileType.JsonSchema,
            # Specify base_class as empty when using dataclass
            base_class="",
            target_python_version=PythonVersion.PY_39,
            use_union_operator=False,
        )
        # Import classes from shared files, remove unused classes, format.
        modify_file(model_path, schema_path)
        lint_file(model_path)

        # Restore backups
        if os.path.exists(model_backup_path):
            if files_equal(model_path, model_backup_path):
                os.rename(model_backup_path, model_path)
            else:
                os.remove(model_backup_path)
        os.rename(schema_backup_path, schema_path)

        number_generated += 1

    return number_generated

src/allotropy/allotrope/schema_parser/generate_schemas.py
Benchling-Open-Source/allotropy
from collections import defaultdict
from dataclasses import dataclass
import io
import json
import os
from pathlib import Path
import re
from typing import Optional

from allotropy.allotrope.schema_parser.schema_model import (
    get_all_schema_components,
    get_schema_definitions_mapping,
)

SCHEMA_DIR_PATH = "src/allotropy/allotrope/schemas"
SHARED_FOLDER_MODULE = "allotropy.allotrope.models.shared"


def get_shared_schema_info(schema_path: str) -> tuple[set[str], dict[str, set[str]]]:
    with open(schema_path) as f:
        schema = json.load(f)

    classes_to_skip = set()
    imports_to_add = defaultdict(set)

    # Get all properties from the schema and all definitions.
    schema_mapping = get_all_schema_components(schema)
    if "$defs" in schema:
        schema_mapping.update(schema["$defs"])
    if "$custom" in schema:
        schema_mapping.update(schema["$custom"])

    # Get schemas defined in models/shared
    shared_schema_mapping = get_schema_definitions_mapping()

    # Match schemas to get classes to remove from the generated code and imports to add.
    for name, component_schema in schema_mapping.items():
        for schema_model in shared_schema_mapping.get(name, []):
            # TODO: log warning visible in script output if schema name is found but no schemas match.
            if schema_model.schema == component_schema:
                classes_to_skip.add(schema_model.import_info[1])
                imports_to_add[schema_model.import_info[0]].add(
                    schema_model.import_info[1]
                )

    return classes_to_skip, dict(imports_to_add)


def get_manifest_from_schema_path(schema_path: str) -> str:
    relpath = Path(os.path.relpath(schema_path, SCHEMA_DIR_PATH))
    return (
        f"http://purl.allotrope.org/manifests/{relpath.parent}/{relpath.stem}.manifest"
    )


@dataclass
class ClassLines:
    """Represents a set of lines defining a class."""

    lines: list[str]

    @property
    def class_name(self) -> str:
        match = None
        if self.lines[0].startswith("@dataclass"):
            match = re.match("class ([^\\(:]*)", self.lines[1])
        elif self.lines[0].startswith("class"):
            match = re.match("class ([^\\(:]*)", self.lines[0])
        elif " = " in self.lines[0]:
            match = re.match("(\\S+) =", self.lines[0])
        if not match:
            error = f"Could not determine class name for : {''.join(self.lines)}"
            raise ValueError(error)
        return match.groups()[0]


class ModelClassEditor:
    """
    Iterates over a file of generated models and rewrites it with modifications, including:

    - Adding manifest to base model
    - Factoring out models found in shared/models
    """

    def __init__(
        self,
        manifest: str,
        classes_to_skip: set[str],
        imports_to_add: dict[str, set[str]],
    ):
        self.manifest = manifest
        self.classes_to_skip = classes_to_skip
        self.imports_to_add = imports_to_add

    def _handle_class_lines(self, class_lines: ClassLines) -> Optional[list[str]]:
        # Given a ClassLines object, returns the lines to add to the file.
        if class_lines.class_name in self.classes_to_skip:
            return None
        # Add manifest to base Model class.
        if class_lines.class_name == "Model" and "manifest" not in "".join(
            class_lines.lines
        ):
            class_lines.lines.append(f'    manifest: str = "{self.manifest}"\n')
        return class_lines.lines

    def _get_class_lines(
        self, file: io.TextIOBase, existing_lines: Optional[list[str]] = None
    ) -> Optional[ClassLines]:
        # Reads lines for the next class and returns as a ClassLines object.
        lines: list[str] = existing_lines or []
        started = False
        while True:
            line = file.readline()
            # Skip empty lines at the start, return when we hit a newline after the class.
            if line == "\n":
                if started:
                    return ClassLines(lines)
                else:
                    continue
            # End of file, return the class if we've read one in on this run.
            if not line:
                return ClassLines(lines) if started else None
            started = True
            lines.append(line)

    def modify_file(self, file_contents: str) -> str:
        new_contents: list[str] = []

        f = io.StringIO(file_contents)

        # Scan past comments and import lines.
        while True:
            line = f.readline()
            # TODO: this needs work to be more robust.
            if (
                line == "\n"
                or line.startswith("#")
                or line.startswith("from")
                or line.startswith("import")
            ):
                new_contents.append(line)
            else:
                break

        # We read the first line in the first class, so we need to add it back in
        existing_lines = [line]

        # Remove one newline after default imports
        if self.imports_to_add:
            new_contents = new_contents[:-1]
            # Add new imports
            for module, classes in self.imports_to_add.items():
                new_contents.append(
                    f"from {SHARED_FOLDER_MODULE}.{module} import {', '.join(sorted(classes))}\n"
                )
        else:
            new_contents = new_contents[:-2]

        # Parse classes
        all_classes: set[str] = set()
        while True:
            class_lines = self._get_class_lines(f, existing_lines)
            existing_lines = []
            if not class_lines:
                break
            updated_lines = self._handle_class_lines(class_lines)
            if updated_lines:
                new_contents.extend(["\n", "\n", *updated_lines])
                all_classes.add(class_lines.class_name)

        # If there are any unused classes, remove them.
        new = "".join(new_contents)
        classes_to_remove: set[str] = set()
        for class_name in all_classes:
            if class_name == "Model":
                continue
            if not re.search(rf"(?<!class )(?<!\w){class_name}(?!\w)(?! = )", new):
                classes_to_remove.add(class_name)

        if classes_to_remove:
            self.classes_to_skip.update(classes_to_remove)
            self.imports_to_add = {}
            return self.modify_file(new)

        return new


def modify_file(model_path: str, schema_path: str) -> None:
    classes_to_skip, imports_to_add = get_shared_schema_info(schema_path)
    manifest = get_manifest_from_schema_path(schema_path)

    editor = ModelClassEditor(manifest, classes_to_skip, imports_to_add)

    with open(model_path) as f:
        contents = f.read()

    with open(model_path, "w") as f:
        f.write(editor.modify_file(contents))

src/allotropy/allotrope/schema_parser/model_class_editor.py
Benchling-Open-Source/allotropy
from collections import defaultdict
from dataclasses import dataclass
import json
import os
from pathlib import Path
import re
from typing import Any


# Used to match and locate the model definiton defined in allotropy/allotrope/models/shared
@dataclass
class SchemaModel:
    name: str
    # The ASM schema of the model, used to match against schemas when refactoring genrated models.
    schema: dict[str, Any]
    # The location of the defined shared class, relative to allotropy.allotrope.models.shared
    import_info: tuple[str, str]


# TODO: roughly copied from datamodel-codegen. There is probably more to use/import here to cover all cases.
def snake_to_upper_camel(word: str, delimiter: str = "_") -> str:
    prefix = ""
    if word.startswith(delimiter):
        prefix = "_"
        word = word[1:]

    return prefix + "".join(
        x[0].upper() + x[1:] for x in re.split(delimiter, word) if x
    )


# TODO: this isn't complete. It covers the current schemas in shared/models, but needs:
# - Hardening (e.g. check type and use that to find properties)
# - More cases (e.g. oneOf / allOf)
def get_all_schema_components(schema: dict[str, Any]) -> dict[str, dict[str, Any]]:
    if "items" in schema:
        schema = schema["items"]

    if "allOf" in schema:
        mapping = {}
        for item in schema["allOf"]:
            mapping.update(get_all_schema_components(item))
        return mapping

    if not isinstance(schema, dict) or "properties" not in schema:
        return {}

    mapping = {}
    for prop_name, prop in schema["properties"].items():
        if "oneOf" in prop:
            count = 0
            for item in prop["oneOf"]:
                if item.get("type") == "object":
                    mapping[f"{prop_name} item{count if count else ''}"] = item
                    mapping.update(get_all_schema_components(item))
                    count += 1
        else:
            mapping[prop_name] = prop
            mapping.update(get_all_schema_components(prop))

    return mapping


SHARED_SCHEMAS_DIR = os.path.join(Path(__file__).parent.parent, "schemas", "shared")


def get_schema_definitions_mapping() -> dict[str, list[SchemaModel]]:
    schema_mapping = defaultdict(list)

    definition_files = [
        (filename, directory)
        for directory in ["definitions", "components"]
        for filename in os.listdir(os.path.join(SHARED_SCHEMAS_DIR, directory))
    ]

    for schema_file, directory in definition_files:
        schema_module = f"{directory}.{schema_file[:-5]}"

        with open(os.path.join(SHARED_SCHEMAS_DIR, directory, schema_file)) as f:
            schemas = json.load(f)

        for schema_name, schema in schemas.items():
            schema_mapping[schema_name].append(
                SchemaModel(
                    schema_name,
                    schema,
                    (schema_module, snake_to_upper_camel(schema_name, "\\W")),
                )
            )
            components = get_all_schema_components(schema)
            for component_name, component_schema in components.items():
                schema_mapping[component_name].append(
                    SchemaModel(
                        component_name,
                        component_schema,
                        (schema_module, snake_to_upper_camel(component_name, "\\W")),
                    )
                )

    return dict(schema_mapping)

src/allotropy/allotrope/schema_parser/schema_model.py
Benchling-Open-Source/allotropy
import json
import os
from pathlib import Path
from typing import Any

SCHEMAS_DIR = os.path.join(Path(__file__).parent, "schemas")


def add_definitions(schema: dict[str, Any]) -> dict[str, Any]:
    for file, section in [
        ("definitions", "defs"),
        ("units", "defs"),
        ("custom", "custom"),
    ]:
        existing = schema.get(f"${section}", {})
        with open(
            os.path.join(SCHEMAS_DIR, "shared", "definitions", f"{file}.json")
        ) as f:
            additional = json.load(f)
        additional.update(existing)
        schema[f"${section}"] = additional
    return schema


def get_schema(schema_relative_path: str) -> dict[str, Any]:
    with open(os.path.join(SCHEMAS_DIR, schema_relative_path)) as f:
        return add_definitions(json.load(f))

src/allotropy/allotrope/schemas.py
Benchling-Open-Source/allotropy
from allotropy.__about__ import __version__

ASM_CONVERTER_NAME = "allotropy"
ASM_CONVERTER_VERSION = __version__

src/allotropy/constants.py
Benchling-Open-Source/allotropy
from datetime import timezone
from enum import Enum
from typing import Optional, Union

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.agilent_gen5.agilent_gen5_parser import AgilentGen5Parser
from allotropy.parsers.appbio_absolute_q.appbio_absolute_q_parser import (
    AppbioAbsoluteQParser,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_parser import (
    AppBioQuantStudioParser,
)
from allotropy.parsers.beckman_vi_cell_blu.vi_cell_blu_parser import ViCellBluParser
from allotropy.parsers.beckman_vi_cell_xr.vi_cell_xr_parser import ViCellXRParser
from allotropy.parsers.moldev_softmax_pro.softmax_pro_parser import SoftmaxproParser
from allotropy.parsers.novabio_flex2.novabio_flex2_parser import NovaBioFlexParser
from allotropy.parsers.perkin_elmer_envision.elmer_envision_parser import (
    ElmerEnvisionParser,
)
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_parser import CedexBiohtParser
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from allotropy.parsers.vendor_parser import VendorParser


class Vendor(Enum):
    AGILENT_GEN5 = "AGILENT_GEN5"
    APPBIO_QUANTSTUDIO = "APPBIO_QUANTSTUDIO"
    APPBIO_ABSOLUTE_Q = "APPBIO_ABSOLUTE_Q"
    BECKMAN_VI_CELL_BLU = "BECKMAN_VI_CELL_BLU"
    BECKMAN_VI_CELL_XR = "BECKMAN_VI_CELL_XR"
    MOLDEV_SOFTMAX_PRO = "MOLDEV_SOFTMAX_PRO"
    NOVABIO_FLEX2 = "NOVABIO_FLEX2"
    PERKIN_ELMER_ENVISION = "PERKIN_ELMER_ENVISION"
    ROCHE_CEDEX_BIOHT = "ROCHE_CEDEX_BIOHT"


VendorType = Union[Vendor, str]


_VENDOR_TO_PARSER: dict[Vendor, type[VendorParser]] = {
    Vendor.AGILENT_GEN5: AgilentGen5Parser,
    Vendor.APPBIO_QUANTSTUDIO: AppBioQuantStudioParser,
    Vendor.APPBIO_ABSOLUTE_Q: AppbioAbsoluteQParser,
    Vendor.BECKMAN_VI_CELL_BLU: ViCellBluParser,
    Vendor.BECKMAN_VI_CELL_XR: ViCellXRParser,
    Vendor.NOVABIO_FLEX2: NovaBioFlexParser,
    Vendor.MOLDEV_SOFTMAX_PRO: SoftmaxproParser,
    Vendor.PERKIN_ELMER_ENVISION: ElmerEnvisionParser,
    Vendor.ROCHE_CEDEX_BIOHT: CedexBiohtParser,
}


class ParserFactory:
    def __init__(self) -> None:
        pass

    def create(
        self, vendor_type: VendorType, default_timezone: Optional[timezone] = None
    ) -> VendorParser:
        try:
            timestamp_parser = TimestampParser(default_timezone)
            return _VENDOR_TO_PARSER[Vendor(vendor_type)](timestamp_parser)
        except (ValueError, KeyError) as e:
            error = f"Failed to create parser, unregistered vendor: {vendor_type}"
            raise AllotropeConversionError(error) from e


def get_parser(
    vendor_type: VendorType, default_timezone: Optional[timezone] = None
) -> VendorParser:
    return PARSER_FACTORY.create(vendor_type, default_timezone=default_timezone)


PARSER_FACTORY: ParserFactory = ParserFactory()

src/allotropy/parser_factory.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValuePicogramPerMilliliter,
)
from allotropy.allotrope.models.ultraviolet_absorbance_benchling_2023_09_ultraviolet_absorbance import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementDocumentItem,
)
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.data_point import DataPoint


class AbsorbanceDataPoint(DataPoint):
    read_mode = ReadMode.ABSORBANCE
    unit = "mAU"

    def to_measurement_doc(self) -> MeasurementDocumentItem:
        measurement_doc = MeasurementDocumentItem(
            DeviceControlAggregateDocument([DeviceControlDocumentItem()]),
            self.generate_sample_doc(),
        )
        measurement_doc.data_cube = self.generate_data_cube()

        if self.concentration:
            measurement_doc.mass_concentration = TQuantityValuePicogramPerMilliliter(
                self.concentration
            )
        if self.processed_data:
            measurement_doc.processed_data_aggregate_document = (
                self.processed_data_doc()
            )
        if self.temperature:
            measurement_doc.compartment_temperature = TQuantityValueDegreeCelsius(
                float(self.temperature)
            )
        return measurement_doc

src/allotropy/parsers/agilent_gen5/absorbance_data_point.py
Benchling-Open-Source/allotropy
import uuid

from allotropy.allotrope.models.shared.definitions.custom import TQuantityValueNumber
from allotropy.allotrope.models.ultraviolet_absorbance_benchling_2023_09_ultraviolet_absorbance import (
    ContainerType,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.parsers.agilent_gen5.absorbance_data_point import AbsorbanceDataPoint
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.plate_data import PlateData


class AbsorbancePlateData(PlateData):
    read_mode = ReadMode.ABSORBANCE
    data_point_cls = AbsorbanceDataPoint

    def to_allotrope(self, measurement_docs: list[MeasurementDocumentItem]) -> Model:
        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                measurement_time=self.datetime,
                analytical_method_identifier=self.protocol_file_path,
                experimental_data_identifier=self.experiment_file_path,
                container_type=ContainerType.well_plate,
                plate_well_count=TQuantityValueNumber(len(self.wells)),
                # TODO read_type=self.read_type.value?,
                measurement_document=measurement_docs,
            )
        )

src/allotropy/parsers/agilent_gen5/absorbance_plate_data.py
Benchling-Open-Source/allotropy
import io
import itertools
from typing import Any

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.agilent_gen5.create_plate_data import create_plate_data
from allotropy.parsers.agilent_gen5.plate_data import PlateData
from allotropy.parsers.vendor_parser import VendorParser


class AgilentGen5Parser(VendorParser):
    def _get_plate_chunks(self, contents: str) -> list[str]:
        return contents.strip().replace("\r\n", "\n").split(4 * "\n")

    def _parse(self, contents: io.IOBase, filename: str) -> Any:  # noqa: ARG002
        plates: list[PlateData] = []
        software_version_chunk = None
        file_paths_chunk = None
        all_data_chunk = None

        plate_chunks = self._get_plate_chunks(self._read_contents(contents))
        completed_plate = False
        for plate_chunk in plate_chunks:
            if plate_chunk.startswith("Software Version"):
                if completed_plate:
                    new_plate = create_plate_data(
                        software_version_chunk, file_paths_chunk, all_data_chunk
                    )
                    plates.append(new_plate)
                    completed_plate = False
                software_version_chunk = plate_chunk
            elif plate_chunk.startswith("Experiment File Path"):
                file_paths_chunk = plate_chunk
            elif plate_chunk.startswith("Plate Number"):
                all_data_chunk = plate_chunk
                completed_plate = True
            elif plate_chunk.startswith("Actual Temperature"):
                all_data_chunk = f"{all_data_chunk}\n\n{plate_chunk}"

        if completed_plate:
            new_plate = create_plate_data(
                software_version_chunk, file_paths_chunk, all_data_chunk
            )
            plates.append(new_plate)

        if not plates:
            msg = "No plate data found in file"
            raise AllotropeConversionError(msg)

        first_plate = plates[0]
        # TODO we just use the metadata for the first plate, but in theory they could all have
        # different metadata in particular, timestamp is known to be, but not sure on procedures/
        # plate dimensions/etc., need to follow up
        measurement_docs = list(
            itertools.chain.from_iterable([plate.measurement_docs for plate in plates])
        )
        model = first_plate.to_allotrope(measurement_docs)
        model.measurement_aggregate_document.measurement_time = self.get_date_time(
            model.measurement_aggregate_document.measurement_time
        )
        return model

        # TODO stats docs
        # statistics_docs = [plate.statistics_doc for plate in plates]
        # all_statistics_docs = list(itertools.chain.from_iterable(statistics_docs))
        # if all_statistics_docs:
        #     allotrope_dict["measurement aggregate document"]["statistics aggregate document"] = {
        #         "statistics document": all_statistics_docs,
        #     }

src/allotropy/parsers/agilent_gen5/agilent_gen5_parser.py
Benchling-Open-Source/allotropy
from enum import Enum
from typing import Optional


class ReadMode(str, Enum):
    ABSORBANCE = "Absorbance"
    FLUORESCENCE = "Fluorescence"
    LUMINESCENCE = "Luminescence"


class ReadType(str, Enum):
    ENDPOINT = "Endpoint"
    KINETIC = "Kinetic"
    AREASCAN = "Area Scan"
    SPECTRAL = "Spectral"


READTYPE_TO_DIMENSIONS: dict[ReadType, list[tuple[str, str, Optional[str]]]] = {
    ReadType.ENDPOINT: [("int", "wavelength", "nm")],
    ReadType.KINETIC: [("double", "time", "s")],
    ReadType.AREASCAN: [
        ("int", "x", None),
        ("int", "y", None),
    ],
    ReadType.SPECTRAL: [("int", "wavelength", "nm")],
}

src/allotropy/parsers/agilent_gen5/constants.py
Benchling-Open-Source/allotropy
from typing import Optional

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.agilent_gen5.absorbance_plate_data import AbsorbancePlateData
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.fluorescence_plate_data import FluorescencePlateData
from allotropy.parsers.agilent_gen5.luminescence_plate_data import LuminescencePlateData
from allotropy.parsers.agilent_gen5.plate_data import PlateData
from allotropy.parsers.utils.values import assert_not_none


def create_plate_data(
    software_version_chunk: Optional[str],
    file_paths_chunk: Optional[str],
    all_data_chunk: Optional[str],
) -> PlateData:
    software_version_chunk = assert_not_none(
        software_version_chunk, "software_version_chunk"
    )
    file_paths_chunk = assert_not_none(file_paths_chunk, "file_paths_chunk")
    all_data_chunk = assert_not_none(all_data_chunk, "all_data_chunk")

    all_data_sections = all_data_chunk.split("\n\n")
    for data_section in all_data_sections:
        if data_section.startswith("Plate Type"):
            if ReadMode.ABSORBANCE.value in data_section:
                return AbsorbancePlateData(
                    software_version_chunk, file_paths_chunk, all_data_chunk
                )
            elif ReadMode.FLUORESCENCE.value in data_section:
                return FluorescencePlateData(
                    software_version_chunk, file_paths_chunk, all_data_chunk
                )
            elif ReadMode.LUMINESCENCE.value in data_section:
                return LuminescencePlateData(
                    software_version_chunk, file_paths_chunk, all_data_chunk
                )
    msg = "Read mode not found"
    raise AllotropeConversionError(msg)

src/allotropy/parsers/agilent_gen5/create_plate_data.py
Benchling-Open-Source/allotropy
from abc import ABC, abstractmethod
from typing import Any, Optional

from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacube,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)
from allotropy.parsers.agilent_gen5.constants import (
    ReadMode,
    ReadType,
    READTYPE_TO_DIMENSIONS,
)


class DataPoint(ABC):
    read_mode: ReadMode
    unit: str

    def __init__(
        self,
        read_type: ReadType,
        measurements: list[Any],
        well_location: str,
        plate_barcode: str,
        sample_identifier: Optional[str],
        concentration: Optional[float],
        processed_data: list[list],
        temperature: Optional[float],
    ):
        self.read_type = read_type
        self.measurements = measurements
        self.well_location = well_location
        self.plate_barcode = plate_barcode
        self.sample_identifier = sample_identifier
        self.concentration = concentration
        self.processed_data = processed_data
        self.temperature = temperature

    def generate_sample_doc(self) -> SampleDocument:
        sample_doc = SampleDocument(
            well_location_identifier=self.well_location,
            plate_barcode=self.plate_barcode,
            # TODO extract from Layout and map values to Allotrope's enum
            # sample_role_type=self.sample_role_type,
        )
        if self.sample_identifier:
            sample_doc.sample_identifier = self.sample_identifier
        return sample_doc

    def generate_data_cube(self) -> TDatacube:
        structure_dimensions = READTYPE_TO_DIMENSIONS[self.read_type]
        structure_measures = [("double", self.read_mode.lower(), self.unit)]
        num_measurements_with_temperature = 3
        if self.read_type == ReadType.AREASCAN:
            data_dimensions = [
                [m[0] for m in self.measurements],
                [m[1] for m in self.measurements],
            ]
            data_measures = [[m[2] for m in self.measurements]]
        elif (
            self.read_type == ReadType.KINETIC
            and len(self.measurements[0]) == num_measurements_with_temperature
        ):
            structure_measures.append(("double", "temperature", "C"))
            data_dimensions = [[m[0] for m in self.measurements]]
            data_measures = [
                [m[1] for m in self.measurements],
                [m[2] for m in self.measurements],
            ]
        else:
            data_dimensions = [[m[0] for m in self.measurements]]
            data_measures = [[m[1] for m in self.measurements]]

        return TDatacube(
            label=f"{self.read_type.value.lower()} data",
            cube_structure=TDatacubeStructure(
                [
                    TDatacubeComponent(FieldComponentDatatype(data_type), concept, unit)
                    for data_type, concept, unit in structure_dimensions
                ],
                [
                    TDatacubeComponent(FieldComponentDatatype(data_type), concept, unit)
                    for data_type, concept, unit in structure_measures
                ],
            ),
            data=TDatacubeData(data_dimensions, data_measures),  # type: ignore[arg-type]
        )

    def processed_data_doc(self) -> ProcessedDataAggregateDocument:
        return ProcessedDataAggregateDocument(
            [
                ProcessedDataDocumentItem(data[1], data_processing_description=data[0])
                for data in self.processed_data
            ]
        )

    @abstractmethod
    def to_measurement_doc(self) -> Any:
        raise NotImplementedError

src/allotropy/parsers/agilent_gen5/data_point.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementDocumentItem,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValuePicogramPerMilliliter,
)
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.data_point import DataPoint


class FluorescenceDataPoint(DataPoint):
    read_mode = ReadMode.FLUORESCENCE
    unit = "RFU"

    def to_measurement_doc(self) -> MeasurementDocumentItem:
        measurement_doc = MeasurementDocumentItem(
            DeviceControlAggregateDocument([DeviceControlDocumentItem()]),
            self.generate_sample_doc(),
        )
        measurement_doc.data_cube = self.generate_data_cube()

        if self.concentration:
            measurement_doc.mass_concentration = TQuantityValuePicogramPerMilliliter(
                self.concentration
            )
        if self.processed_data:
            measurement_doc.processed_data_aggregate_document = (
                self.processed_data_doc()
            )
        if self.temperature:
            measurement_doc.compartment_temperature = TQuantityValueDegreeCelsius(
                float(self.temperature)
            )
        return measurement_doc

src/allotropy/parsers/agilent_gen5/fluorescence_data_point.py
Benchling-Open-Source/allotropy
import uuid

from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    ContainerType,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.definitions.custom import TQuantityValueNumber
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.fluorescence_data_point import FluorescenceDataPoint
from allotropy.parsers.agilent_gen5.plate_data import PlateData


class FluorescencePlateData(PlateData):
    read_mode = ReadMode.FLUORESCENCE
    data_point_cls = FluorescenceDataPoint

    def to_allotrope(self, measurement_docs: list[MeasurementDocumentItem]) -> Model:
        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                measurement_time=self.datetime,
                analytical_method_identifier=self.protocol_file_path,
                experimental_data_identifier=self.experiment_file_path,
                container_type=ContainerType.well_plate,
                plate_well_count=TQuantityValueNumber(len(self.wells)),
                # TODO read_type=self.read_type.value?,
                measurement_document=measurement_docs,
            )
        )

src/allotropy/parsers/agilent_gen5/fluorescence_plate_data.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.models.luminescence_benchling_2023_09_luminescence import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementDocumentItem,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValuePicogramPerMilliliter,
)
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.data_point import DataPoint


class LuminescenceDataPoint(DataPoint):
    read_mode = ReadMode.LUMINESCENCE
    unit = "RLU"

    def to_measurement_doc(self) -> MeasurementDocumentItem:
        measurement_doc = MeasurementDocumentItem(
            DeviceControlAggregateDocument([DeviceControlDocumentItem()]),
            self.generate_sample_doc(),
        )
        measurement_doc.data_cube = self.generate_data_cube()

        if self.concentration:
            measurement_doc.mass_concentration = TQuantityValuePicogramPerMilliliter(
                self.concentration
            )
        if self.processed_data:
            measurement_doc.processed_data_aggregate_document = (
                self.processed_data_doc()
            )
        if self.temperature:
            measurement_doc.compartment_temperature = TQuantityValueDegreeCelsius(
                float(self.temperature)
            )
        return measurement_doc

src/allotropy/parsers/agilent_gen5/luminescence_data_point.py
Benchling-Open-Source/allotropy
import uuid

from allotropy.allotrope.models.luminescence_benchling_2023_09_luminescence import (
    ContainerType,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.definitions.custom import TQuantityValueNumber
from allotropy.parsers.agilent_gen5.constants import ReadMode
from allotropy.parsers.agilent_gen5.luminescence_data_point import LuminescenceDataPoint
from allotropy.parsers.agilent_gen5.plate_data import PlateData


class LuminescencePlateData(PlateData):
    read_mode = ReadMode.LUMINESCENCE
    data_point_cls = LuminescenceDataPoint

    def to_allotrope(self, measurement_docs: list[MeasurementDocumentItem]) -> Model:
        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                measurement_time=self.datetime,
                analytical_method_identifier=self.protocol_file_path,
                experimental_data_identifier=self.experiment_file_path,
                container_type=ContainerType.well_plate,
                plate_well_count=TQuantityValueNumber(len(self.wells)),
                # TODO read_type=self.read_type.value?,
                measurement_document=measurement_docs,
            )
        )

src/allotropy/parsers/agilent_gen5/luminescence_plate_data.py
Benchling-Open-Source/allotropy
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime
from io import StringIO
from typing import Any, Optional, Union

import numpy as np
import pandas as pd

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacube,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)
from allotropy.parsers.agilent_gen5.constants import (
    ReadMode,
    ReadType,
    READTYPE_TO_DIMENSIONS,
)
from allotropy.parsers.agilent_gen5.data_point import DataPoint

METADATA_PREFIXES = frozenset(
    {
        "Plate Number",
        "Date",
        "Time",
        "Reader Type:",
        "Reader Serial Number:",
        "Reading Type",
    }
)
GEN5_DATETIME_FORMAT = "%m/%d/%Y %I:%M:%S %p"


def try_float(value: str) -> Union[str, float]:
    try:
        return float(value)
    except ValueError:
        return value


class PlateData(ABC):
    measurements: defaultdict[str, list]
    processed_datas: defaultdict[str, list]
    temperatures: list
    kinetic_times: Optional[list[int]]
    measurement_docs: list
    wells: list
    layout: dict
    concentrations: dict
    read_names: list
    datetime: Optional[str]
    experiment_file_path: Optional[str]
    protocol_file_path: Optional[str]
    read_mode: ReadMode
    read_type: ReadType
    plate_barcode: str
    statistics_doc: list
    actual_temperature: Optional[float]
    data_point_cls: type[DataPoint]

    def __init__(
        self,
        software_version_chunk: str,
        file_paths_chunk: str,
        all_data_chunk: str,
    ):
        self.software_version_chunk = software_version_chunk
        self.file_paths_chunk = file_paths_chunk
        self.all_data_chunk = all_data_chunk

        self.measurements = defaultdict(list)
        self.processed_datas = defaultdict(list)
        self.temperatures = []
        self.kinetic_times = None

        self.measurement_docs = []
        self.wells = []
        self.layout = {}
        self.concentrations = {}
        self.read_names = []
        self.datetime = None
        self.experiment_file_path = None
        self.protocol_file_path = None
        self.statistics_doc = []
        self.actual_temperature = None

        self._parse_software_version_chunk(self.software_version_chunk)
        self._parse_file_paths_chunk(self.file_paths_chunk)
        self._parse_all_data_chunk(self.all_data_chunk)

    def _parse_software_version_chunk(self, software_version_chunk: str) -> None:
        self.software_version = software_version_chunk.split("\t")[1]

    def _parse_file_paths_chunk(self, file_paths_chunk: str) -> None:
        file_paths = file_paths_chunk.split("\n")
        self.experiment_file_path = f"{file_paths[0]}\t".split("\t")[1]
        self.protocol_file_path = f"{file_paths[1]}\t".split("\t")[1]

    def _parse_all_data_chunk(self, all_data_chunk: str) -> None:
        all_data_sections = all_data_chunk.split("\n\n")

        is_kinetic_data = False
        # kinetic_data_label = None
        is_blank_kinetic_data = False
        blank_kinetic_data_label = None

        for data_section in all_data_sections:
            if data_section.startswith("Plate Number"):
                self._parse_metadata(data_section)
            elif data_section.startswith("Plate Type"):
                self._parse_procedure_details(data_section)
            elif data_section.startswith("Layout"):
                self._parse_layout(data_section)
            elif data_section.startswith("Actual Temperature"):
                self._parse_actual_temperature(data_section)
            elif data_section.startswith("Results"):
                self._parse_results(data_section)
            elif data_section.startswith("Curve Name"):
                self._parse_stdcurve(data_section)
            elif is_kinetic_data:
                self._parse_kinetic_data(data_section)
                is_kinetic_data = False
            elif is_blank_kinetic_data:
                self._parse_blank_kinetic_data(data_section, blank_kinetic_data_label)
                is_blank_kinetic_data = False
            elif len(data_section.split("\n")) == 1 and any(
                read_name in data_section for read_name in self.read_names
            ):
                if data_section.startswith("Blank"):
                    is_blank_kinetic_data = True
                    blank_kinetic_data_label = data_section.strip()
                else:
                    is_kinetic_data = True
                    # kinetic_data_label = data_section.strip()

    def _parse_metadata(self, metadata: str) -> None:
        metadata_dict: dict = {}
        metadata_lines = metadata.splitlines()
        for metadata_line in metadata_lines:
            line_split = metadata_line.split("\t")
            if line_split[0] not in METADATA_PREFIXES:
                msg = f"Unrecognized metadata {line_split[0]}"
                raise AllotropeConversionError(msg)
            metadata_dict[line_split[0]] = line_split[1]
        # TODO put more metadata in the right spots

        self.datetime = datetime.strptime(  # noqa: DTZ007
            f"{metadata_dict['Date']} {metadata_dict['Time']}", GEN5_DATETIME_FORMAT
        ).isoformat()

        self.plate_barcode = metadata_dict["Plate Number"]

    def _parse_procedure_details(
        self, procedure_details: str
    ) -> None:  # TODO parse the rest of the procedure details
        if ReadType.KINETIC.value in procedure_details:
            self.read_type = ReadType.KINETIC
        elif ReadType.AREASCAN.value in procedure_details:
            self.read_type = ReadType.AREASCAN
        elif ReadType.SPECTRAL.value in procedure_details:
            self.read_type = ReadType.SPECTRAL
        else:
            self.read_type = (
                ReadType.ENDPOINT
            )  # check for this last, because other modes still contain the word "Endpoint"

        procedure_chunks = self._parse_procedure_chunks(procedure_details)
        for procedure_chunk in procedure_chunks:
            self._parse_procedure_chunk(procedure_chunk)

    def _parse_procedure_chunk(self, procedure_chunk: list[str]) -> None:
        # if no user-defined name is specified for protocols,
        # e.g. it just says "Absorbance Endpoint",
        # Gen5 defaults to using the wavelength as the name
        use_wavelength_names = False
        read_line_length = 2
        wavelength_line_length = 2
        for line in procedure_chunk:
            split_line = line.strip().split("\t")
            if split_line[0] == "Read":
                if len(split_line) != read_line_length:
                    msg = f"Unrecognized Read data {split_line}"
                    raise AllotropeConversionError(msg)
                if split_line[-1] == f"{self.read_mode.title()} Endpoint":
                    use_wavelength_names = True
                else:
                    self.read_names.append(split_line[-1])
            elif split_line[0].startswith("Wavelengths"):
                if use_wavelength_names:
                    split_line_colon = split_line[0].split(":  ")
                    if len(split_line_colon) != wavelength_line_length:
                        msg = f"Unrecognized Wavelengths data {split_line}"
                        raise AllotropeConversionError(msg)
                    self.read_names.extend(split_line_colon[-1].split(", "))

    def _parse_procedure_chunks(self, procedure_details: str) -> list[list[str]]:
        procedure_chunks = []
        current_chunk: list[str] = []
        procedure_lines = procedure_details.splitlines()
        for procedure_line in procedure_lines:
            if procedure_line[0] != "\t":
                procedure_chunks.append(current_chunk)
                current_chunk = []
            current_chunk.append(procedure_line)
        del procedure_chunks[0]
        procedure_chunks.append(current_chunk)

        return procedure_chunks

    def _parse_layout(self, layout: str) -> None:
        layout_lines = layout.splitlines()
        # first line is "Layout", second line is column numbers
        current_row = "A"
        for i in range(2, len(layout_lines)):
            split_line = layout_lines[i].split("\t")
            if split_line[0]:
                current_row = split_line[0]
            label = split_line[-1]
            for j in range(1, len(split_line) - 1):
                well_loc = f"{current_row}{j}"
                if label == "Well ID":
                    self.layout[well_loc] = split_line[j]
                elif label == "Conc/Dil" and split_line[j]:
                    self.concentrations[well_loc] = float(split_line[j])

    def _parse_actual_temperature(self, actual_temperature: str) -> None:
        if len(actual_temperature.split("\n")) != 1:
            msg = f"Unrecognized temperature data {actual_temperature}"
            raise AllotropeConversionError(msg)
        self.actual_temperature = float(actual_temperature.strip().split("\t")[-1])

    def _parse_results(self, results: str) -> None:
        result_lines = results.splitlines()
        if result_lines[0].strip() != "Results":
            msg = f"Unrecognized results data {result_lines[0]}"
            raise AllotropeConversionError(msg)
        # result_lines[1] contains column numbers

        current_row = "A"
        for row_num in range(2, len(result_lines)):
            values = result_lines[row_num].split("\t")
            if values[0]:
                current_row = values[0]
            label = values[-1]  # last column gives information about the type of read
            for col_num in range(1, len(values) - 1):
                well_pos = f"{current_row}{col_num}"
                if well_pos not in self.wells:
                    self.wells.append(well_pos)
                well_value: Union[str, float] = try_float(values[col_num])
                if self._is_processed_data_label(label):
                    self.processed_datas[well_pos].append([label, well_value])
                else:
                    label_only = label.split(":")[-1]
                    self.measurements[well_pos].append([label_only, well_value])

        for well_pos in self.wells:
            datapoint = self.data_point_cls(
                self.read_type,
                self.measurements[well_pos],
                well_pos,
                self.plate_barcode,
                self.layout.get(well_pos),
                self.concentrations.get(well_pos),
                self.processed_datas[well_pos],
                self.actual_temperature,
            )
            self.measurement_docs.append(datapoint.to_measurement_doc())

    def _is_processed_data_label(self, label: str) -> bool:
        if self.read_mode == ReadMode.LUMINESCENCE:
            return not any(
                (label.startswith(read_name) and label.split(":")[-1] == "Lum")
                for read_name in self.read_names
            )
        else:
            return not any(
                (label.startswith(read_name) and label.split(":")[-1][0].isdigit())
                for read_name in self.read_names
            )

    def _parse_stdcurve(self, stdcurve: str) -> None:
        lines = stdcurve.splitlines()
        num_lines = 2
        if len(lines) != num_lines:
            msg = f"Unrecognized std curve data {lines}"
            raise AllotropeConversionError(msg)
        keys = lines[0].split("\t")
        values = lines[1].split("\t")
        self.statistics_doc = [
            {
                "statistical feature": key,
                "feature": try_float(value),
                "group": f"{self.plate_barcode} {self.wells[0]}-{self.wells[-1]}",
            }
            for key, value in zip(keys, values)
        ]

    def _parse_kinetic_data(self, kinetic_data: str) -> None:
        kinetic_data_io = StringIO(kinetic_data)
        df = pd.read_table(kinetic_data_io)
        df_columns = kinetic_data.split("\n")[0].split("\t")
        df = df[
            df["A1"].notna()
        ]  # drop incomplete rows, particularly rows only with "0:00:00"

        self.kinetic_times = [self._hhmmss_to_sec(hhmmss) for hhmmss in df["Time"]]
        self.temperatures = df[df_columns[1]].replace(np.nan, None).tolist()
        has_temperatures = any(temp is not None for temp in self.temperatures)
        for well_pos in df_columns[
            2:
        ]:  # first column is Time, second column is T∞ READ_NAME with no values
            self.wells.append(well_pos)
            values = df[well_pos].tolist()
            if has_temperatures:
                self.measurements[well_pos].extend(
                    list(zip(self.kinetic_times, values, self.temperatures))
                )
            else:
                self.measurements[well_pos].extend(
                    list(zip(self.kinetic_times, values))
                )

    def _parse_blank_kinetic_data(
        self, blank_kinetic_data: str, blank_kinetic_data_label: Optional[str]
    ) -> None:
        blank_kinetic_data_io = StringIO(blank_kinetic_data)
        df = pd.read_table(blank_kinetic_data_io)
        df.dropna(axis=0)  # drop incomplete rows
        df_columns = blank_kinetic_data.split("\n")[0].split("\t")

        for well_pos in df_columns[1:]:  # first column is Time
            measures = df[well_pos].tolist()
            self.processed_datas[well_pos].append(
                [blank_kinetic_data_label, self._blank_data_cube(measures)]
            )

    def _blank_data_cube(self, measures: list[float]) -> TDatacube:
        structure_dimensions = READTYPE_TO_DIMENSIONS[self.read_type]
        structure_measures = [
            ("double", self.read_mode.lower(), self.data_point_cls.unit)
        ]
        return TDatacube(
            label=f"{self.read_type.value.lower()} data",
            cube_structure=TDatacubeStructure(
                [
                    TDatacubeComponent(FieldComponentDatatype(data_type), concept, unit)
                    for data_type, concept, unit in structure_dimensions
                ],
                [
                    TDatacubeComponent(FieldComponentDatatype(data_type), concept, unit)
                    for data_type, concept, unit in structure_measures
                ],
            ),
            data=TDatacubeData([self.kinetic_times], [measures]),  # type: ignore[list-item]
        )

    def _hhmmss_to_sec(
        self, hhmmss: str
    ) -> int:  # convert to seconds, as specified by Allotrope
        hours, minutes, seconds = tuple(int(num) for num in hhmmss.split(":"))
        return (3600 * hours) + (60 * minutes) + seconds

    @abstractmethod
    def to_allotrope(self, measurement_docs: list) -> Any:
        raise NotImplementedError

src/allotropy/parsers/agilent_gen5/plate_data.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from collections import defaultdict
from io import IOBase
from typing import Any
import uuid

import pandas as pd

from allotropy.allotrope.models.pcr_benchling_2023_09_dpcr import (
    CalculatedDataDocumentItem,
    ContainerType,
    DataSourceAggregateDocument,
    DataSourceDocumentItem,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    DPCRAggregateDocument,
    DPCRDocumentItem,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    SampleDocument,
    TCalculatedDataAggregateDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueNumber,
    TQuantityValueNumberPerMicroliter,
)
from allotropy.allotrope.models.shared.definitions.definitions import TQuantityValue
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parsers.appbio_absolute_q.appbio_absolute_q_reader import AbsoluteQReader
from allotropy.parsers.appbio_absolute_q.constants import (
    AGGREGATION_LOOKUP,
    CALCULATED_DATA_REFERENCE,
    CalculatedDataItem,
    CalculatedDataSource,
)
from allotropy.parsers.vendor_parser import VendorParser


class AppbioAbsoluteQParser(VendorParser):
    def _parse(self, raw_contents: IOBase, filename: str) -> Model:
        reader = AbsoluteQReader(raw_contents)
        return self._get_model(reader.wells, reader.group_rows, filename)

    def _get_model(
        self, wells: pd.DataFrame, group_rows: pd.DataFrame, filename: str
    ) -> Model:
        well_groups = wells.groupby(["Well"]).groups.keys()
        group_ids: dict[Any, list] = defaultdict(list)

        dpcr_document = [
            self._get_dpcr_document(wells[wells["Well"] == well_name], group_ids)
            for well_name in well_groups
        ]

        calculated_data_aggregate_document = None
        calculated_data_document = self.get_calculated_data_document(
            group_ids, group_rows
        )

        if calculated_data_document:
            calculated_data_aggregate_document = TCalculatedDataAggregateDocument(
                calculated_data_document=calculated_data_document
            )

        return Model(
            dPCR_aggregate_document=DPCRAggregateDocument(
                device_system_document=DeviceSystemDocument(
                    device_identifier=wells.iloc[0]["Instrument"],
                    brand_name="QuantStudio Absolute Q Digital PCR System",
                    product_manufacturer="ThermoFisher Scientific",
                ),
                data_system_document=DataSystemDocument(
                    file_name=filename,
                    software_name="QuantStudio Absolute Q Digital PCR Software",
                    ASM_converter_name=ASM_CONVERTER_NAME,
                    ASM_converter_version=ASM_CONVERTER_VERSION,
                ),
                dPCR_document=dpcr_document,
                calculated_data_aggregate_document=calculated_data_aggregate_document,
            )
        )

    def _get_dpcr_document(
        self, well_data: pd.DataFrame, group_ids: dict[Any, list]
    ) -> DPCRDocumentItem:
        measurement_documents = []
        for _, well_item in well_data.iterrows():
            measurement_identifier = str(uuid.uuid4())

            key = str((well_item["Group"], well_item["Target"]))
            group_ids[key].append(measurement_identifier)

            measurement_documents.append(
                MeasurementDocumentItem(
                    measurement_identifier=measurement_identifier,
                    measurement_time=self.get_date_time(str(well_item["Date"])),
                    target_DNA_description=well_item["Target"],
                    total_partition_count=TQuantityValueNumber(
                        value=well_item["Total"]
                    ),
                    sample_document=SampleDocument(
                        sample_identifier=well_item["Name"],
                        well_location_identifier=well_item["Well"],
                        well_plate_identifier=well_item["Plate"],
                    ),
                    device_control_aggregate_document=DeviceControlAggregateDocument(
                        device_control_document=[
                            DeviceControlDocumentItem(
                                device_type="dPCR",
                                reporter_dye_setting=well_item["Dye"],
                            )
                        ]
                    ),
                    processed_data_aggregate_document=ProcessedDataAggregateDocument(
                        processed_data_document=[
                            ProcessedDataDocumentItem(
                                number_concentration=TQuantityValueNumberPerMicroliter(
                                    value=well_item["Conc. cp/uL"]
                                ),
                                positive_partition_count=TQuantityValueNumber(
                                    value=well_item["Positives"]
                                ),
                            )
                        ]
                    ),
                )
            )
        return DPCRDocumentItem(
            measurement_aggregate_document=MeasurementAggregateDocument(
                experimental_data_identifier=well_data.iloc[0]["Run"],
                plate_well_count=TQuantityValueNumber(value=16),
                container_type=ContainerType.well_plate,
                measurement_document=measurement_documents,
            )
        )

    @staticmethod
    def get_calculated_data_document(
        group_ids: dict, group_rows: pd.DataFrame
    ) -> list[CalculatedDataDocumentItem]:
        calculated_data_document: list[CalculatedDataDocumentItem] = []

        for _, group in group_rows.iterrows():
            aggregation_type = AGGREGATION_LOOKUP[group["Well"]]

            # Samples designated as "Individual" include no calculated data
            if aggregation_type not in CALCULATED_DATA_REFERENCE:
                continue

            group_name = group["Group"].split("(")[0].strip()
            key = str((group_name, group["Target"]))

            ids = group_ids[key]
            calculated_data_ids = {}
            defered_calculated_data_items: list[CalculatedDataItem] = []

            for calculated_data_item in CALCULATED_DATA_REFERENCE[aggregation_type]:
                # TODO: if aggregation type is Replicate(Average), check for required columns
                # Raise if column(s) do not exist

                # Calculated data items that have another calculated data as data source
                # need to be created after the later
                if calculated_data_item.source == CalculatedDataSource.CALCULATED_DATA:
                    defered_calculated_data_items.append(calculated_data_item)
                    continue

                datum_value = float(group[calculated_data_item.column])
                calculated_data_id = str(uuid.uuid4())
                calculated_data_ids[calculated_data_item.name] = calculated_data_id

                data_source_document = [
                    DataSourceDocumentItem(
                        data_source_identifier=identifier,
                        data_source_feature=calculated_data_item.source_feature,
                    )
                    for identifier in ids
                ]
                calculated_data_document.append(
                    CalculatedDataDocumentItem(
                        calculated_data_identifier=calculated_data_id,
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=data_source_document
                        ),
                        calculated_data_name=calculated_data_item.name,
                        calculated_datum=TQuantityValue(
                            value=datum_value,
                            unit=calculated_data_item.unit,
                        ),
                    )
                )

            # TODO: this should be inproved (repeat less code)
            for calculated_data_item in defered_calculated_data_items:
                datum_value = float(group[calculated_data_item.column])
                calculated_data_id = str(uuid.uuid4())

                data_source_document = [
                    DataSourceDocumentItem(
                        data_source_identifier=calculated_data_ids[source_feature],
                        data_source_feature=source_feature,
                    )
                    for source_feature in calculated_data_item.source_feature.split(",")
                ]

                calculated_data_document.append(
                    CalculatedDataDocumentItem(
                        calculated_data_identifier=calculated_data_id,
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=data_source_document
                        ),
                        calculated_data_name=calculated_data_item.name,
                        calculated_datum=TQuantityValue(
                            value=datum_value,
                            unit=calculated_data_item.unit,
                        ),
                    )
                )

        return calculated_data_document

src/allotropy/parsers/appbio_absolute_q/appbio_absolute_q_parser.py
Benchling-Open-Source/allotropy
from io import IOBase

import numpy as np
import pandas as pd


class AbsoluteQReader:
    def __init__(self, contents: IOBase):
        absolute_q_data = pd.read_csv(  # type: ignore[call-overload]
            filepath_or_buffer=contents, parse_dates=["Date"]
        ).replace(np.nan, None)
        self.wells: pd.DataFrame = absolute_q_data.dropna(subset=["Name"])
        self.group_rows: pd.DataFrame = absolute_q_data[absolute_q_data["Name"].isna()]

src/allotropy/parsers/appbio_absolute_q/appbio_absolute_q_reader.py
Benchling-Open-Source/allotropy
from enum import Enum

from attr import dataclass


class AggregationType(str, Enum):
    INDIVIDUAL = "INDIVIDUAL"
    AVERAGE = "AVERAGE"
    POOLED = "POOLED"


class CalculatedDataSource(str, Enum):
    MEASUREMENT = "MEASUREMENT"
    CALCULATED_DATA = "CALCULATED_DATA"


AGGREGATION_LOOKUP = {
    "Average": AggregationType.AVERAGE,
    "Pooled": AggregationType.POOLED,
    None: AggregationType.INDIVIDUAL,
}


@dataclass
class CalculatedDataItem:
    column: str
    name: str
    source: CalculatedDataSource
    source_feature: str
    unit: str


CALCULATED_DATA_REFERENCE: dict[AggregationType, list[CalculatedDataItem]] = {
    AggregationType.AVERAGE: [
        CalculatedDataItem(
            column="Total",
            name="Average Total",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="total partition count",
            unit="#",
        ),
        CalculatedDataItem(
            column="Conc. cp/uL",
            name="Average Concentration",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="number concentration",
            unit="#/μL",
        ),
        CalculatedDataItem(
            column="SD",
            name="SD Concentration",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="number concentration",
            unit="#/μL",
        ),
        CalculatedDataItem(
            column="CV%",
            name="CV",
            source=CalculatedDataSource.CALCULATED_DATA,
            source_feature="Average Concentration,SD Concentration",
            unit="%",
        ),
        CalculatedDataItem(
            column="Positives",
            name="Average Positives",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="positive partition count",
            unit="#",
        ),
    ],
    AggregationType.POOLED: [
        CalculatedDataItem(
            column="Total",
            name="Pooled Total",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="total partition count",
            unit="#",
        ),
        CalculatedDataItem(
            column="Conc. cp/uL",
            name="Pooled Concentration",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="number concentration",
            unit="#/μL",
        ),
        CalculatedDataItem(
            column="Positives",
            name="Pooled Positives",
            source=CalculatedDataSource.MEASUREMENT,
            source_feature="positive partition count",
            unit="#",
        ),
    ],
}

src/allotropy/parsers/appbio_absolute_q/constants.py
Benchling-Open-Source/allotropy
from io import StringIO
from typing import Any, Optional
import uuid

import numpy as np
import pandas as pd

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import (
    ExperimentType,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_calculated_documents import (
    build_quantity,
    iter_calculated_data_documents,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import (
    AmplificationData,
    Data,
    Header,
    MeltCurveRawData,
    MulticomponentData,
    RawData,
    Result,
    Well,
    WellItem,
    WellList,
)
from allotropy.parsers.lines_reader import LinesReader


def df_to_series(df: pd.DataFrame) -> pd.Series:
    return pd.Series(df.iloc[0], index=df.columns)


def float_or_none(value: Any) -> Optional[float]:
    try:
        return float(value)
    except Exception:
        return None


def get_str(data: pd.Series, key: str, default: Optional[str] = None) -> Optional[str]:
    value = data.get(key, default)
    return None if value is None else str(value)


def get_int(data: pd.Series, key: str) -> Optional[int]:
    try:
        value = data.get(key)
        return None if value is None else int(value)  # type: ignore[arg-type]
    except Exception as e:
        msg = f"Unable to convert {key} to integer value"
        raise AllotropeConversionError(msg) from e


def get_float(data: pd.Series, key: str) -> Optional[float]:
    try:
        value = data.get(key)
        return float_or_none(value)
    except Exception as e:
        msg = f"Unable to convert {key} to float value"
        raise AllotropeConversionError(msg) from e


def get_bool(data: pd.Series, key: str) -> Optional[bool]:
    try:
        value = data.get(key)
        return None if value is None else bool(value)
    except Exception as e:
        msg = f"Unable to convert {key} to bool value"
        raise AllotropeConversionError(msg) from e


class HeaderBuilder:
    @staticmethod
    def build(reader: LinesReader) -> Header:
        data = HeaderBuilder.get_data(reader)

        device_identifier = get_str(data, "Instrument Name")
        if device_identifier is None:
            msg = "Unable to get device identifier"
            raise AllotropeConversionError(msg)

        model_number = get_str(data, "Instrument Type")
        if model_number is None:
            msg = "Unable to get model number"
            raise AllotropeConversionError(msg)

        device_serial_number = get_str(data, "Instrument Serial Number")
        if device_serial_number is None:
            msg = "Unable to get device serial number"
            raise AllotropeConversionError(msg)

        measurement_method_identifier = get_str(data, "Quantification Cycle Method")
        if measurement_method_identifier is None:
            msg = "Unable to get measurement method identifier"
            raise AllotropeConversionError(msg)

        pcr_detection_chemistry = get_str(data, "Chemistry")
        if pcr_detection_chemistry is None:
            msg = "Unable to get PCR detection chemistry"
            raise AllotropeConversionError(msg)

        return Header(
            measurement_time=HeaderBuilder.get_measurement_time(data),
            plate_well_count=HeaderBuilder.get_plate_well_count(data),
            experiment_type=HeaderBuilder.get_experiment_type(data),
            device_identifier=device_identifier,
            model_number=model_number,
            device_serial_number=device_serial_number,
            measurement_method_identifier=measurement_method_identifier,
            pcr_detection_chemistry=pcr_detection_chemistry,
            passive_reference_dye_setting=get_str(data, "Passive Reference"),
            barcode=get_str(data, "Experiment Barcode"),
            analyst=get_str(data, "Experiment User Name"),
            experimental_data_identifier=get_str(data, "Experiment Name"),
        )

    @staticmethod
    def get_experiment_type(data: pd.Series) -> ExperimentType:
        experiments_type_options = {
            "Standard Curve": ExperimentType.standard_curve_qPCR_experiment,
            "Relative Standard Curve": ExperimentType.relative_standard_curve_qPCR_experiment,
            "Comparative Cт (ΔΔCт)": ExperimentType.comparative_CT_qPCR_experiment,
            "Melt Curve": ExperimentType.melt_curve_qPCR_experiment,
            "Genotyping": ExperimentType.genotyping_qPCR_experiment,
            "Presence/Absence": ExperimentType.presence_absence_qPCR_experiment,
        }

        experiments_type = experiments_type_options.get(data.get("Experiment Type"))  # type: ignore[arg-type]
        if experiments_type is None:
            msg = "Unable to get valid experiment type"
            raise AllotropeConversionError(msg)
        return experiments_type

    @staticmethod
    def get_data(reader: LinesReader) -> pd.Series:
        lines = [line.replace("*", "", 1) for line in reader.pop_until(r"^\[.+\]")]
        csv_stream = StringIO("\n".join(lines))
        raw_data = pd.read_csv(
            csv_stream, header=None, sep="=", names=["index", "values"]
        )
        data = pd.Series(raw_data["values"].values, index=raw_data["index"])
        data.index = data.index.str.strip()
        return data.str.strip().replace("NA", None)

    @staticmethod
    def get_measurement_time(data: pd.Series) -> str:
        return str(data.get("Experiment Run End Time"))

    @staticmethod
    def get_plate_well_count(data: pd.Series) -> int:
        block_type = get_str(data, "Block Type", default="")

        if block_type is None:
            msg = "Unable to get plate well count"
            raise AllotropeConversionError(msg)

        if "96" in block_type:
            return 96
        elif "384" in block_type:
            return 384

        msg = "Unable to interpret plate well count"
        raise AllotropeConversionError(msg)


class GenericWellBuilder:
    @staticmethod
    def build(data: pd.DataFrame) -> WellList:
        return WellList(
            [
                Well(
                    identifier=identifier,  # type: ignore[arg-type]
                    items={
                        item_data["Target Name"]: GenericWellBuilder.build_well_item(
                            item_data
                        )
                        for _, item_data in well_data.iterrows()
                    },
                )
                for identifier, well_data in data.groupby("Well")
            ]
        )

    @staticmethod
    def build_well_item(data: pd.Series) -> WellItem:
        identifier = get_int(data, "Well")
        if identifier is None:
            msg = "Unable to get well identifier"
            raise AllotropeConversionError(msg)

        target_dna_description = get_str(data, "Target Name")
        if target_dna_description is None:
            msg = f"Unable to get target dna description for well {identifier}"
            raise AllotropeConversionError(msg)

        sample_identifier = get_str(data, "Sample Name")
        if sample_identifier is None:
            msg = f"Unable to get sample identifier for well {identifier}"
            raise AllotropeConversionError(msg)

        return WellItem(
            uuid=str(uuid.uuid4()),
            identifier=identifier,
            target_dna_description=target_dna_description,
            sample_identifier=sample_identifier,
            reporter_dye_setting=get_str(data, "Reporter"),
            position=get_str(data, "Well Position", default="UNDEFINED"),
            well_location_identifier=get_str(data, "Well Position"),
            quencher_dye_setting=get_str(data, "Quencher"),
            sample_role_type=get_str(data, "Task"),
        )


class GenotypingWellBuilder:
    @staticmethod
    def build(data: pd.DataFrame) -> WellList:
        return WellList(
            [
                Well(
                    identifier=well_id,  # type: ignore[arg-type]
                    items={
                        well_item.target_dna_description: well_item
                        for well_item in GenotypingWellBuilder.build_well_items(
                            well_data
                        )
                    },
                )
                for well_id, well_data in data.iterrows()
            ]
        )

    @staticmethod
    def build_well_items(data: pd.Series) -> list[WellItem]:
        identifier = get_int(data, "Well")
        if identifier is None:
            msg = "Unable to get well identifier"
            raise AllotropeConversionError(msg)

        snp_name = get_str(data, "SNP Assay Name")
        if snp_name is None:
            msg = f"Unable to get snp name for well {identifier}"
            raise AllotropeConversionError(msg)

        allele1 = get_str(data, "Allele1 Name")
        if allele1 is None:
            msg = f"Unable to get allele 1 for well {identifier}"
            raise AllotropeConversionError(msg)

        allele2 = get_str(data, "Allele2 Name")
        if allele2 is None:
            msg = f"Unable to get allele 2 for well {identifier}"
            raise AllotropeConversionError(msg)

        sample_identifier = get_str(data, "Sample Name")
        if sample_identifier is None:
            msg = "Unable to get sample identifier"
            raise AllotropeConversionError(msg)

        return [
            WellItem(
                uuid=str(uuid.uuid4()),
                identifier=identifier,
                target_dna_description=f"{snp_name}-{allele}",
                sample_identifier=sample_identifier,
                reporter_dye_setting=get_str(data, "Reporter"),
                position=get_str(data, "Well Position", default="UNDEFINED"),
                well_location_identifier=get_str(data, "Well Position"),
                quencher_dye_setting=get_str(data, "Quencher"),
                sample_role_type=get_str(data, "Task"),
            )
            for allele in [allele1, allele2]
        ]


class WellBuilder:
    @staticmethod
    def build(reader: LinesReader, experiment_type: ExperimentType) -> WellList:
        raw_data = WellBuilder.get_data(reader)
        data = raw_data[raw_data["Sample Name"].notnull()]
        if experiment_type == ExperimentType.genotyping_qPCR_experiment:
            return GenotypingWellBuilder.build(data)
        return GenericWellBuilder.build(data)

    @staticmethod
    def get_data(reader: LinesReader) -> pd.DataFrame:
        if reader.drop_until(r"^\[Sample Setup\]") is None:
            msg = "Unable to find Sample Setup section in input file"
            raise AllotropeConversionError(msg)

        reader.pop()  # remove title
        lines = list(reader.pop_until(r"^\[.+\]"))
        csv_stream = StringIO("\n".join(lines))
        return pd.read_csv(csv_stream, sep="\t").replace(np.nan, None)


class RawDataBuilder:
    @staticmethod
    def build(reader: LinesReader) -> Optional[RawData]:
        if not reader.match(r"^\[Raw Data\]"):
            return None
        reader.pop()  # remove title
        lines = list(reader.pop_until(r"^\[.+\]"))
        return RawData(lines)


class AmplificationDataBuilder:
    @staticmethod
    def build(
        amplification_data: pd.DataFrame, well_item: WellItem
    ) -> AmplificationData:
        target_data = AmplificationDataBuilder.filter_target_data(
            amplification_data, well_item
        )
        return AmplificationData(
            total_cycle_number_setting=float(target_data["Cycle"].max()),
            cycle=target_data["Cycle"].tolist(),
            rn=target_data["Rn"].tolist(),
            delta_rn=target_data["Delta Rn"].tolist(),
        )

    @staticmethod
    def filter_target_data(
        amplification_data: pd.DataFrame, well_item: WellItem
    ) -> pd.DataFrame:
        well_data = amplification_data[
            amplification_data["Well"] == well_item.identifier
        ]
        if well_data.empty:
            msg = f"Unable to get amplification data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        target_data = well_data[
            well_data["Target Name"] == well_item.target_dna_description
        ]
        if target_data.empty:
            msg = f"Unable to get amplification data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        return target_data

    @staticmethod
    def get_data(reader: LinesReader) -> pd.DataFrame:
        if reader.drop_until(r"^\[Amplification Data\]") is None:
            msg = "Unable to find Amplification Data section in input file"
            raise AllotropeConversionError(msg)

        reader.pop()  # remove title
        lines = list(reader.pop_until(r"^\[.+\]"))
        csv_stream = StringIO("\n".join(lines))
        return pd.read_csv(csv_stream, sep="\t", thousands=r",")


class MulticomponentDataBuilder:
    @staticmethod
    def build(data: pd.DataFrame, well: Well) -> MulticomponentData:
        well_data = MulticomponentDataBuilder.filter_well_data(data, well)
        return MulticomponentData(
            cycle=well_data["Cycle"].tolist(),
            columns={
                name: well_data[name].tolist()  # type: ignore[misc]
                for name in well_data
                if name not in ["Well", "Cycle", "Well Position"]
            },
        )

    @staticmethod
    def filter_well_data(data: pd.DataFrame, well: Well) -> pd.DataFrame:
        well_data = data[data["Well"] == well.identifier]
        if well_data.empty:
            msg = f"Unable to find multi component data for well {well.identifier}"
            raise AllotropeConversionError(msg)

        return well_data

    @staticmethod
    def get_data(reader: LinesReader) -> Optional[pd.DataFrame]:
        if not reader.match(r"^\[Multicomponent Data\]"):
            return None
        reader.pop()  # remove title
        lines = list(reader.pop_until(r"^\[.+\]"))
        csv_stream = StringIO("\n".join(lines))
        return pd.read_csv(csv_stream, sep="\t", thousands=r",")


class GenericResultsBuilder:
    @staticmethod
    def build(data: pd.DataFrame, well_item: WellItem) -> Result:
        target_data = GenericResultsBuilder.filter_target_data(data, well_item)
        cycle_threshold_value_setting = get_float(target_data, "Ct Threshold")
        if cycle_threshold_value_setting is None:
            msg = "Unable to get cycle threshold value setting"
            raise AllotropeConversionError(msg)

        cycle_threshold_result = target_data.get("CT")
        if cycle_threshold_result is None:
            msg = "Unable to get cycle threshold result"
            raise AllotropeConversionError(msg)

        return Result(
            cycle_threshold_value_setting=cycle_threshold_value_setting,
            cycle_threshold_result=float_or_none(cycle_threshold_result),
            automatic_cycle_threshold_enabled_setting=get_bool(
                target_data, "Automatic Ct Threshold"
            ),
            automatic_baseline_determination_enabled_setting=get_bool(
                target_data, "Automatic Baseline"
            ),
            normalized_reporter_result=get_float(target_data, "Rn"),
            baseline_corrected_reporter_result=get_float(target_data, "Delta Rn"),
            genotyping_determination_result=get_str(target_data, "Call"),
            genotyping_determination_method_setting=get_float(
                target_data, "Threshold Value"
            ),
            quantity=get_float(target_data, "Quantity"),
            quantity_mean=get_float(target_data, "Quantity Mean"),
            quantity_sd=get_float(target_data, "Quantity SD"),
            ct_mean=get_float(target_data, "Ct Mean"),
            ct_sd=get_float(target_data, "Ct SD"),
            delta_ct_mean=get_float(target_data, "Delta Ct Mean"),
            delta_ct_se=get_float(target_data, "Delta Ct SE"),
            delta_delta_ct=get_float(target_data, "Delta Delta Ct"),
            rq=get_float(target_data, "RQ"),
            rq_min=get_float(target_data, "RQ Min"),
            rq_max=get_float(target_data, "RQ Max"),
            rn_mean=get_float(target_data, "Rn Mean"),
            rn_sd=get_float(target_data, "Rn SD"),
            y_intercept=get_float(target_data, "Y-Intercept"),
            r_squared=get_float(target_data, "R(superscript 2)"),
            slope=get_float(target_data, "Slope"),
            efficiency=get_float(target_data, "Efficiency"),
        )

    @staticmethod
    def filter_target_data(data: pd.DataFrame, well_item: WellItem) -> pd.Series:
        well_data = data[data["Well"] == well_item.identifier]
        if well_data.empty:
            msg = f"Unable to get result data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        target_data = well_data[
            well_data["Target Name"] == well_item.target_dna_description
        ]
        if target_data.empty:
            msg = f"Unable to get result data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        n_rows, _ = target_data.shape
        if n_rows != 1:
            msg = f"Unexpected number of results associated to well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        return df_to_series(target_data)


class GenotypingResultsBuilder:
    @staticmethod
    def build(data: pd.DataFrame, well_item: WellItem) -> Result:
        target_data = GenotypingResultsBuilder.filter_target_data(data, well_item)
        _, raw_allele = well_item.target_dna_description.split("-")
        allele = raw_allele.replace(" ", "")
        cycle_threshold_value_setting = get_float(target_data, f"{allele} Ct Threshold")
        if cycle_threshold_value_setting is None:
            msg = f"Unable to get cycle threshold value setting for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        cycle_threshold_result = target_data.get(f"{allele} Ct")
        if cycle_threshold_result is None:
            msg = "Unable to get cycle threshold result"
            raise AllotropeConversionError(msg)

        return Result(
            cycle_threshold_value_setting=cycle_threshold_value_setting,
            cycle_threshold_result=float_or_none(cycle_threshold_result),
            automatic_cycle_threshold_enabled_setting=get_bool(
                target_data, f"{allele} Automatic Ct Threshold"
            ),
            automatic_baseline_determination_enabled_setting=get_bool(
                target_data, f"{allele} Automatic Baseline"
            ),
            normalized_reporter_result=get_float(target_data, "Rn"),
            baseline_corrected_reporter_result=get_float(
                target_data, f"{allele} Delta Rn"
            ),
            genotyping_determination_result=get_str(target_data, "Call"),
            genotyping_determination_method_setting=get_float(
                target_data, "Threshold Value"
            ),
            quantity=get_float(target_data, "Quantity"),
            quantity_mean=get_float(target_data, "Quantity Mean"),
            quantity_sd=get_float(target_data, "Quantity SD"),
            ct_mean=get_float(target_data, "Ct Mean"),
            ct_sd=get_float(target_data, "Ct SD"),
            delta_ct_mean=get_float(target_data, "Delta Ct Mean"),
            delta_ct_se=get_float(target_data, "Delta Ct SE"),
            delta_delta_ct=get_float(target_data, "Delta Delta Ct"),
            rq=get_float(target_data, "RQ"),
            rq_min=get_float(target_data, "RQ Min"),
            rq_max=get_float(target_data, "RQ Max"),
            rn_mean=get_float(target_data, "Rn Mean"),
            rn_sd=get_float(target_data, "Rn SD"),
            y_intercept=get_float(target_data, "Y-Intercept"),
            r_squared=get_float(target_data, "R(superscript 2)"),
            slope=get_float(target_data, "Slope"),
            efficiency=get_float(target_data, "Efficiency"),
        )

    @staticmethod
    def filter_target_data(data: pd.DataFrame, well_item: WellItem) -> pd.Series:
        well_data = data[data["Well"] == well_item.identifier]
        if well_data.empty:
            msg = f"Unable to get result data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        snp_assay_name, _ = well_item.target_dna_description.split("-")
        target_data = well_data[well_data["SNP Assay Name"] == snp_assay_name]
        if target_data.empty:
            msg = f"Unable to get result data for well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        n_rows, _ = target_data.shape
        if n_rows != 1:
            msg = f"Unexpected number of results associated to well {well_item.identifier}"
            raise AllotropeConversionError(msg)

        return df_to_series(target_data)


class ResultsBuilder:
    @staticmethod
    def build(
        data: pd.DataFrame, well_item: WellItem, experiment_type: ExperimentType
    ) -> Result:
        if experiment_type == ExperimentType.genotyping_qPCR_experiment:
            return GenotypingResultsBuilder.build(data, well_item)
        return GenericResultsBuilder.build(data, well_item)

    @staticmethod
    def get_data(reader: LinesReader) -> tuple[pd.DataFrame, pd.Series]:
        if reader.drop_until(r"^\[Results\]") is None:
            msg = "Unable to find Results section in input file"
            raise AllotropeConversionError(msg)

        reader.pop()  # remove title
        data_lines = list(reader.pop_until_empty())
        csv_stream = StringIO("\n".join(data_lines))
        data = pd.read_csv(csv_stream, sep="\t", thousands=r",").replace(np.nan, None)

        reader.drop_empty()

        if reader.match(r"\[.+\]"):
            return data, pd.Series()

        metadata_lines = list(reader.pop_until_empty())
        csv_stream = StringIO("\n".join(metadata_lines))
        raw_data = pd.read_csv(
            csv_stream, header=None, sep="=", names=["index", "values"]
        )
        metadata = pd.Series(raw_data["values"].values, index=raw_data["index"])
        metadata.index = metadata.index.str.strip()

        reader.drop_empty()

        return data, metadata.str.strip()


class MeltCurveRawDataBuilder:
    @staticmethod
    def build(data: pd.DataFrame, well: Well) -> MeltCurveRawData:
        well_data = MeltCurveRawDataBuilder.filter_well_data(data, well)
        return MeltCurveRawData(
            reading=well_data["Reading"].tolist(),
            fluorescence=well_data["Fluorescence"].tolist(),
            derivative=well_data["Derivative"].tolist(),
        )

    @staticmethod
    def filter_well_data(data: pd.DataFrame, well: Well) -> pd.DataFrame:
        well_data = data[data["Well"] == well.identifier]
        if well_data.empty:
            msg = f"Unable to get melt curve raw data for well {well.identifier}"
            raise AllotropeConversionError(msg)

        return well_data

    @staticmethod
    def get_data(reader: LinesReader) -> Optional[pd.DataFrame]:
        if not reader.match(r"^\[Melt Curve Raw Data\]"):
            return None
        reader.pop()  # remove title
        lines = list(reader.pop_until_empty())
        csv_stream = StringIO("\n".join(lines))
        return pd.read_csv(csv_stream, sep="\t", thousands=r",")


class DataBuilder:
    @staticmethod
    def build(reader: LinesReader) -> Data:
        header = HeaderBuilder.build(reader)
        wells = WellBuilder.build(reader, header.experiment_type)
        raw_data = RawDataBuilder.build(reader)

        amp_data = AmplificationDataBuilder.get_data(reader)
        multi_data = MulticomponentDataBuilder.get_data(reader)
        results_data, results_metadata = ResultsBuilder.get_data(reader)
        melt_data = MeltCurveRawDataBuilder.get_data(reader)
        for well in wells:
            if multi_data is not None:
                well.add_multicomponent_data(
                    MulticomponentDataBuilder.build(multi_data, well)
                )
            if melt_data is not None:
                well.add_melt_curve_raw_data(
                    MeltCurveRawDataBuilder.build(melt_data, well)
                )
            for well_item in well.items.values():
                well_item.add_amplification_data(
                    AmplificationDataBuilder.build(amp_data, well_item)
                )
                well_item.add_result(
                    ResultsBuilder.build(
                        results_data, well_item, header.experiment_type
                    )
                )

            if an_well_item := well.get_an_well_item():
                well.calculated_document = build_quantity(an_well_item)

        endogenous_control = get_str(results_metadata, "Endogenous Control") or ""
        reference_sample = get_str(results_metadata, "Reference Sample") or ""

        return Data(
            header,
            wells,
            raw_data,
            endogenous_control,
            reference_sample,
            list(
                iter_calculated_data_documents(
                    wells,
                    header.experiment_type,
                    reference_sample,
                    endogenous_control,
                )
            ),
        )

src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_builders.py
Benchling-Open-Source/allotropy
from collections.abc import Iterator
from typing import Optional
from uuid import uuid4

from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import ExperimentType
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import (
    WellItem,
    WellList,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_views import (
    SampleView,
    TargetRoleView,
    TargetView,
)
from allotropy.parsers.appbio_quantstudio.calculated_document import (
    CalculatedDocument,
    DataSource,
)
from allotropy.parsers.appbio_quantstudio.decorators import cache
from allotropy.parsers.appbio_quantstudio.views import ViewData


@cache
def build_quantity(well_item: WellItem) -> Optional[CalculatedDocument]:
    if (quantity := well_item.result.quantity) is None:
        return None

    # quantity calc docs should not be included in calculated aggregate document
    # so they are marked as already iterated on creation

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="quantity",
        value=quantity,
        iterated=True,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item),
        ],
    )


def build_quantity_mean(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (quantity_mean := well_items[0].result.quantity_mean) is None:
        return None

    data_sources = []
    for well_item in well_items:
        quantity_ref = build_quantity(well_item)
        if quantity_ref is None:
            return None

        data_sources.append(
            DataSource(
                feature="quantity",
                reference=quantity_ref,
            )
        )

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="quantity mean",
        value=quantity_mean,
        data_sources=data_sources,
    )


def build_quantity_sd(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (quantity_sd := well_items[0].result.quantity_sd) is None:
        return None

    data_sources = []
    for well_item in well_items:
        quantity_ref = build_quantity(well_item)
        if quantity_ref is None:
            return None

        data_sources.append(
            DataSource(
                feature="quantity",
                reference=quantity_ref,
            )
        )

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="quantity sd",
        value=quantity_sd,
        data_sources=data_sources,
    )


@cache
def build_ct_mean(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (ct_mean := well_items[0].result.ct_mean) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="ct mean",
        value=ct_mean,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_ct_sd(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (ct_sd := well_items[0].result.ct_sd) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="ct sd",
        value=ct_sd,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_delta_ct_mean(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
    r_target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (delta_ct_mean := well_items[0].result.delta_ct_mean) is None:
        return None

    ct_mean_ref = build_ct_mean(view_data, sample, target)
    if ct_mean_ref is None:
        return None

    r_ct_mean_ref = build_ct_mean(view_data, sample, r_target)
    if r_ct_mean_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="delta ct mean",
        value=delta_ct_mean,
        data_sources=[
            DataSource(
                feature="ct mean",
                reference=ct_mean_ref,
            ),
            DataSource(
                feature="ct mean",
                reference=r_ct_mean_ref,
            ),
        ],
    )


def build_delta_ct_se(
    view_data: ViewData[WellItem], sample: str, target: str, r_target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (delta_ct_se := well_items[0].result.delta_ct_se) is None:
        return None

    source = [
        DataSource(feature="ct mean", reference=well_item) for well_item in well_items
    ]

    r_target_source = [
        DataSource(feature="ct mean", reference=well_item)
        for well_item in view_data.get_leaf_item(sample, r_target)
    ]

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="delta ct se",
        value=delta_ct_se,
        data_sources=source + r_target_source,
    )


def build_delta_delta_ct(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
    r_sample: str,
    r_target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (delta_delta_ct := well_items[0].result.delta_delta_ct) is None:
        return None

    delta_ct_mean_ref = build_delta_ct_mean(view_data, sample, target, r_target)
    if delta_ct_mean_ref is None:
        return None

    r_delta_ct_mean_ref = build_delta_ct_mean(view_data, r_sample, target, r_target)
    if r_delta_ct_mean_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="delta delta ct",
        value=delta_delta_ct,
        data_sources=[
            DataSource(
                feature="delta ct mean",
                reference=delta_ct_mean_ref,
            ),
            DataSource(
                feature="delta ct mean",
                reference=r_delta_ct_mean_ref,
            ),
        ],
    )


@cache
def build_rq(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
    r_sample: str,
    r_target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq := well_items[0].result.rq) is None:
        return None

    delta_delta_ct_ref = build_delta_delta_ct(
        view_data, sample, target, r_sample, r_target
    )
    if delta_delta_ct_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq",
        value=rq,
        data_sources=[
            DataSource(
                feature="delta delta ct",
                reference=delta_delta_ct_ref,
            )
        ],
    )


def build_rq_min(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
    r_sample: str,
    r_target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq_min := well_items[0].result.rq_min) is None:
        return None

    rq_ref = build_rq(view_data, sample, target, r_sample, r_target)
    if rq_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq min",
        value=rq_min,
        data_sources=[
            DataSource(
                feature="rq",
                reference=rq_ref,
            )
        ],
    )


def build_rq_max(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
    r_sample: str,
    r_target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq_max := well_items[0].result.rq_max) is None:
        return None

    rq_ref = build_rq(view_data, sample, target, r_sample, r_target)
    if rq_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq max",
        value=rq_max,
        data_sources=[
            DataSource(
                feature="rq",
                reference=rq_ref,
            ),
        ],
    )


@cache
def build_relative_rq(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq := well_items[0].result.rq) is None:
        return None

    quantity_mean_ref = build_quantity_mean(view_data, sample, target)
    if quantity_mean_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq",
        value=rq,
        data_sources=[
            DataSource(
                feature="quantity mean",
                reference=quantity_mean_ref,
            )
        ],
    )


def build_relative_rq_min(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq_min := well_items[0].result.rq_min) is None:
        return None

    relative_rq_ref = build_relative_rq(view_data, sample, target)
    if relative_rq_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq min",
        value=rq_min,
        data_sources=[
            DataSource(
                feature="rq",
                reference=relative_rq_ref,
            )
        ],
    )


def build_relative_rq_max(
    view_data: ViewData[WellItem],
    sample: str,
    target: str,
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rq_max := well_items[0].result.rq_max) is None:
        return None

    relative_rq_ref = build_relative_rq(view_data, sample, target)
    if relative_rq_ref is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rq max",
        value=rq_max,
        data_sources=[
            DataSource(
                feature="rq",
                reference=relative_rq_ref,
            ),
        ],
    )


def build_rn_mean(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rn_mean := well_items[0].result.rn_mean) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rn mean",
        value=rn_mean,
        data_sources=[
            DataSource(feature="normalized reporter result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_rn_sd(
    view_data: ViewData[WellItem], sample: str, target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(sample, target)
    if (rn_sd := well_items[0].result.rn_sd) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="rn sd",
        value=rn_sd,
        data_sources=[
            DataSource(feature="normalized reporter result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_y_intercept(
    view_data: ViewData[WellItem], target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(target)
    if (y_intercept := well_items[0].result.y_intercept) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="y intercept",
        value=y_intercept,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_r_squared(
    view_data: ViewData[WellItem], target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(target)
    if (r_squared := well_items[0].result.r_squared) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="r^2",
        value=r_squared,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_slope(
    view_data: ViewData[WellItem], target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(target)
    if (slope := well_items[0].result.slope) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="slope",
        value=slope,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def build_efficiency(
    view_data: ViewData[WellItem], target: str
) -> Optional[CalculatedDocument]:
    well_items = view_data.get_leaf_item(target)
    if (efficiency := well_items[0].result.efficiency) is None:
        return None

    return CalculatedDocument(
        uuid=str(uuid4()),
        name="efficiency",
        value=efficiency,
        data_sources=[
            DataSource(feature="cycle threshold result", reference=well_item)
            for well_item in well_items
        ],
    )


def iter_comparative_ct_calc_docs(
    view_data: ViewData[WellItem],
    r_sample: str,
    r_target: str,
) -> Iterator[CalculatedDocument]:
    # Quantity, Quantity Mean, Quantity SD, Ct Mean, Ct SD, Delta Ct Mean,
    # Delta Ct SE, Delta Delta Ct, RQ, RQ min, RQ max
    for sample, target in view_data.iter_keys():
        if calc_doc := build_quantity_mean(view_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_quantity_sd(view_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_ct_sd(view_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_delta_ct_se(view_data, sample, target, r_target):
            yield from calc_doc.iter_struct()

        if target != r_target:
            if calc_doc := build_rq_min(view_data, sample, target, r_sample, r_target):
                yield from calc_doc.iter_struct()

            if calc_doc := build_rq_max(view_data, sample, target, r_sample, r_target):
                yield from calc_doc.iter_struct()


def iter_standard_curve_calc_docs(
    view_st_data: ViewData[WellItem],
    view_tr_data: ViewData[WellItem],
) -> Iterator[CalculatedDocument]:
    # Quantity, Quantity Mean, Quantity SD, Ct Mean, Ct SD, Y-Intercept,
    # R(superscript 2), Slope, Efficiency
    for sample, target in view_st_data.iter_keys():
        if calc_doc := build_quantity_mean(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_quantity_sd(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_ct_mean(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_ct_sd(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

    for target in view_tr_data.data:
        if calc_doc := build_y_intercept(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_r_squared(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_slope(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_efficiency(view_tr_data, target):
            yield from calc_doc.iter_struct()


def iter_relative_standard_curve_calc_docs(
    view_st_data: ViewData[WellItem],
    view_tr_data: ViewData[WellItem],
) -> Iterator[CalculatedDocument]:
    # Quantity, Quantity Mean, Quantity SD, Ct Mean, Ct SD, RQ, RQ min,
    # RQ max, Y-Intercept, R(superscript 2), Slope, Efficiency
    for sample, target in view_st_data.iter_keys():
        if calc_doc := build_quantity_mean(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_quantity_sd(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_ct_sd(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_relative_rq_min(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_relative_rq_max(view_st_data, sample, target):
            yield from calc_doc.iter_struct()

    for target in view_tr_data.data:
        if calc_doc := build_y_intercept(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_r_squared(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_slope(view_tr_data, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_efficiency(view_tr_data, target):
            yield from calc_doc.iter_struct()


def iter_presence_absence_calc_docs(
    view_data: ViewData[WellItem],
) -> Iterator[CalculatedDocument]:
    # Rn Mean, Rn SD
    for sample, target in view_data.iter_keys():
        if calc_doc := build_rn_mean(view_data, sample, target):
            yield from calc_doc.iter_struct()

        if calc_doc := build_rn_sd(view_data, sample, target):
            yield from calc_doc.iter_struct()


def iter_calculated_data_documents(
    wells: WellList,
    experiment_type: ExperimentType,
    r_sample: str,
    r_target: str,
) -> Iterator[CalculatedDocument]:
    view_st = SampleView(sub_view=TargetView())
    view_st_data = view_st.apply(wells.iter_well_items())  # type: ignore[arg-type]

    view_tr = TargetRoleView()
    view_tr_data = view_tr.apply(wells.iter_well_items())  # type: ignore[arg-type]

    if experiment_type == ExperimentType.relative_standard_curve_qPCR_experiment:
        yield from iter_relative_standard_curve_calc_docs(
            view_st_data,
            view_tr_data,
        )
    elif experiment_type == ExperimentType.comparative_CT_qPCR_experiment:
        yield from iter_comparative_ct_calc_docs(
            view_st_data,
            r_sample,
            r_target,
        )
    elif experiment_type == ExperimentType.standard_curve_qPCR_experiment:
        yield from iter_standard_curve_calc_docs(
            view_st_data,
            view_tr_data,
        )
    elif experiment_type == ExperimentType.presence_absence_qPCR_experiment:
        yield from iter_presence_absence_calc_docs(
            view_st_data,
        )

src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_calculated_documents.py
Benchling-Open-Source/allotropy
from io import IOBase
from typing import Optional

from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import (
    BaselineCorrectedReporterDataCube,
    CalculatedDataDocumentItem,
    ContainerType,
    DataProcessingDocument,
    DataProcessingDocument1,
    DataSourceAggregateDocument,
    DataSourceDocumentItem,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    ExperimentType,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    MeltingCurveDataCube,
    Model,
    NormalizedReporterDataCube,
    PassiveReferenceDyeDataCube,
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    QPCRAggregateDocument,
    QPCRDocumentItem,
    ReporterDyeDataCube,
    SampleDocument,
    TCalculatedDataAggregateDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueUnitless,
    TQuantityValueNumber,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_builders import (
    DataBuilder,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import (
    Data,
    Well,
    WellItem,
)
from allotropy.parsers.lines_reader import LinesReader
from allotropy.parsers.vendor_parser import VendorParser


class AppBioQuantStudioParser(VendorParser):
    def _parse(self, raw_contents: IOBase, file_name: str) -> Model:
        reader = LinesReader(raw_contents)
        data = DataBuilder.build(reader)
        return self._get_model(data, file_name)

    def _get_model(self, data: Data, file_name: str) -> Model:
        return Model(
            qPCR_aggregate_document=QPCRAggregateDocument(
                device_system_document=DeviceSystemDocument(
                    device_identifier=data.header.device_identifier,
                    model_number=data.header.model_number,
                    device_serial_number=data.header.device_serial_number,
                ),
                data_system_document=DataSystemDocument(
                    data_system_instance_identifier="localhost",
                    file_name=file_name,
                    UNC_path="",  # unknown
                    software_name="Thermo QuantStudio",
                    software_version="1.0",
                    ASM_converter_name=ASM_CONVERTER_NAME,
                    ASM_converter_version=ASM_CONVERTER_VERSION,
                ),
                qPCR_document=[
                    QPCRDocumentItem(
                        analyst=data.header.analyst,
                        measurement_aggregate_document=MeasurementAggregateDocument(
                            experimental_data_identifier=data.header.experimental_data_identifier,
                            experiment_type=data.header.experiment_type,
                            container_type=ContainerType.qPCR_reaction_block,
                            plate_well_count=TQuantityValueNumber(
                                value=data.header.plate_well_count
                            ),
                            measurement_document=[
                                self.get_measurement_document_item(
                                    data, well, well_item
                                )
                                for well_item in well.items.values()
                            ],
                        ),
                        calculated_data_aggregate_document=self.get_inner_calculated_data_aggregate_document(
                            well
                        ),
                    )
                    for well in data.wells
                ],
                calculated_data_aggregate_document=self.get_outter_calculated_data_aggregate_document(
                    data
                ),
            )
        )

    def get_inner_calculated_data_aggregate_document(
        self, well: Well
    ) -> Optional[TCalculatedDataAggregateDocument]:
        if not well.calculated_document:
            return None

        return TCalculatedDataAggregateDocument(
            calculated_data_document=[
                CalculatedDataDocumentItem(
                    calculated_data_identifier=well.calculated_document.uuid,
                    data_source_aggregate_document=DataSourceAggregateDocument(
                        data_source_document=[
                            DataSourceDocumentItem(
                                data_source_identifier=data_source.reference.uuid,
                                data_source_feature=data_source.feature,
                            )
                            for data_source in well.calculated_document.data_sources
                        ],
                    ),
                    calculated_data_name=well.calculated_document.name,
                    calculated_data_description=None,
                    calculated_datum=TQuantityValueUnitless(
                        value=well.calculated_document.value
                    ),
                )
            ],
        )

    def get_outter_calculated_data_aggregate_document(
        self, data: Data
    ) -> TCalculatedDataAggregateDocument:
        if data.header.experiment_type in [
            ExperimentType.comparative_CT_qPCR_experiment,
            ExperimentType.relative_standard_curve_qPCR_experiment,
        ]:
            data_processing_document = DataProcessingDocument1(
                reference_DNA_description=data.endogenous_control,
                reference_sample_description=data.reference_sample,
            )
        else:
            data_processing_document = None

        return TCalculatedDataAggregateDocument(
            calculated_data_document=[
                CalculatedDataDocumentItem(
                    calculated_data_identifier=calc_doc.uuid,
                    data_source_aggregate_document=DataSourceAggregateDocument(
                        data_source_document=[
                            DataSourceDocumentItem(
                                data_source_identifier=(
                                    data_source.reference.uuid
                                    if data_source.reference
                                    else None
                                ),
                                data_source_feature=data_source.feature,
                            )
                            for data_source in calc_doc.data_sources
                        ],
                    ),
                    data_processing_document=data_processing_document,
                    calculated_data_name=calc_doc.name,
                    calculated_data_description=None,
                    calculated_datum=TQuantityValueUnitless(value=calc_doc.value),
                )
                for calc_doc in data.calculated_documents
            ],
        )

    def get_measurement_document_item(
        self, data: Data, well: Well, well_item: WellItem
    ) -> MeasurementDocumentItem:
        return MeasurementDocumentItem(
            measurement_identifier=well_item.uuid,
            measurement_time=self.get_date_time(data.header.measurement_time),
            target_DNA_description=well_item.target_dna_description,
            sample_document=SampleDocument(
                sample_identifier=well_item.sample_identifier,
                sample_role_type=well_item.sample_role_type,
                well_location_identifier=well_item.well_location_identifier,
                well_plate_identifier=data.header.barcode,
            ),
            device_control_aggregate_document=DeviceControlAggregateDocument(
                device_control_document=[
                    self.get_device_control_document_item(data, well_item),
                ],
            ),
            processed_data_aggregate_document=ProcessedDataAggregateDocument(
                processed_data_document=[
                    self.get_processed_data_document(well_item),
                ]
            ),
            reporter_dye_data_cube=self.get_reporter_dye_data_cube(well, well_item),
            passive_reference_dye_data_cube=self.get_passive_reference_dye_data_cube(
                data, well
            ),
            melting_curve_data_cube=self.get_melting_curve_data_cube(well),
        )

    def get_device_control_document_item(
        self, data: Data, well_item: WellItem
    ) -> DeviceControlDocumentItem:
        return DeviceControlDocumentItem(
            device_type="qPCR",
            measurement_method_identifier=data.header.measurement_method_identifier,
            total_cycle_number_setting=TQuantityValueNumber(
                value=well_item.amplification_data.total_cycle_number_setting,
            ),
            PCR_detection_chemistry=data.header.pcr_detection_chemistry,
            reporter_dye_setting=well_item.reporter_dye_setting,
            quencher_dye_setting=well_item.quencher_dye_setting,
            passive_reference_dye_setting=data.header.passive_reference_dye_setting,
        )

    def get_processed_data_document(
        self, well_item: WellItem
    ) -> ProcessedDataDocumentItem:
        normalized_reporter_result = (
            None
            if well_item.result.normalized_reporter_result is None
            else TQuantityValueUnitless(
                value=well_item.result.normalized_reporter_result
            )
        )

        baseline_corrected_reporter_result = (
            None
            if well_item.result.baseline_corrected_reporter_result is None
            else TQuantityValueUnitless(
                value=well_item.result.baseline_corrected_reporter_result
            )
        )

        genotyping_determination_method_setting = (
            None
            if well_item.result.genotyping_determination_method_setting is None
            else TQuantityValueUnitless(
                value=well_item.result.genotyping_determination_method_setting
            )
        )

        return ProcessedDataDocumentItem(
            data_processing_document=DataProcessingDocument(
                automatic_cycle_threshold_enabled_setting=well_item.result.automatic_baseline_determination_enabled_setting,
                cycle_threshold_value_setting=TQuantityValueUnitless(
                    value=well_item.result.cycle_threshold_value_setting,
                ),
                automatic_baseline_determination_enabled_setting=well_item.result.automatic_baseline_determination_enabled_setting,
                genotyping_determination_method_setting=genotyping_determination_method_setting,
            ),
            cycle_threshold_result=TNullableQuantityValueUnitless(
                value=well_item.result.cycle_threshold_result,
            ),
            normalized_reporter_result=normalized_reporter_result,
            baseline_corrected_reporter_result=baseline_corrected_reporter_result,
            genotyping_determination_result=well_item.result.genotyping_determination_result,
            normalized_reporter_data_cube=NormalizedReporterDataCube(
                label="normalized reporter",
                cube_structure=TDatacubeStructure(
                    dimensions=[
                        TDatacubeComponent(
                            field_componentDatatype=FieldComponentDatatype.integer,
                            concept="cycle count",
                            unit="#",
                        ),
                    ],
                    measures=[
                        TDatacubeComponent(
                            field_componentDatatype=FieldComponentDatatype.double,
                            concept="normalized report result",
                            unit="(unitless)",
                        ),
                    ],
                ),
                data=TDatacubeData(
                    dimensions=[well_item.amplification_data.cycle],
                    measures=[well_item.amplification_data.rn],
                ),
            ),
            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                label="baseline corrected reporter",
                cube_structure=TDatacubeStructure(
                    dimensions=[
                        TDatacubeComponent(
                            field_componentDatatype=FieldComponentDatatype.integer,
                            concept="cycle count",
                            unit="#",
                        ),
                    ],
                    measures=[
                        TDatacubeComponent(
                            field_componentDatatype=FieldComponentDatatype.double,
                            concept="baseline corrected reporter result",
                            unit="(unitless)",
                        ),
                    ],
                ),
                data=TDatacubeData(
                    dimensions=[well_item.amplification_data.cycle],
                    measures=[well_item.amplification_data.delta_rn],
                ),
            ),
        )

    def get_reporter_dye_data_cube(
        self, well: Well, well_item: WellItem
    ) -> Optional[ReporterDyeDataCube]:
        if well.multicomponent_data is None or well_item.reporter_dye_setting is None:
            return None

        return ReporterDyeDataCube(
            label="reporter dye",
            cube_structure=TDatacubeStructure(
                dimensions=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.integer,
                        concept="cycle count",
                        unit="#",
                    ),
                ],
                measures=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.double,
                        concept="reporter dye fluorescence",
                        unit="RFU",
                    ),
                ],
            ),
            data=TDatacubeData(
                dimensions=[well.multicomponent_data.cycle],
                measures=[
                    well.multicomponent_data.get_column(well_item.reporter_dye_setting)
                ],
            ),
        )

    def get_passive_reference_dye_data_cube(
        self, data: Data, well: Well
    ) -> Optional[PassiveReferenceDyeDataCube]:
        if (
            well.multicomponent_data is None
            or data.header.passive_reference_dye_setting is None
        ):
            return None

        return PassiveReferenceDyeDataCube(
            label="passive reference dye",
            cube_structure=TDatacubeStructure(
                dimensions=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.integer,
                        concept="cycle count",
                        unit="#",
                    ),
                ],
                measures=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.double,
                        concept="passive reference dye fluorescence",
                        unit="RFU",
                    ),
                ],
            ),
            data=TDatacubeData(
                dimensions=[well.multicomponent_data.cycle],
                measures=[
                    well.multicomponent_data.get_column(
                        data.header.passive_reference_dye_setting
                    )
                ],
            ),
        )

    def get_melting_curve_data_cube(self, well: Well) -> Optional[MeltingCurveDataCube]:
        if well.melt_curve_raw_data is None:
            return None

        return MeltingCurveDataCube(
            label="melting curve",
            cube_structure=TDatacubeStructure(
                dimensions=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.double,
                        concept="temperature",
                        unit="degrees C",
                    ),
                ],
                measures=[
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.double,
                        concept="reporter dye fluorescence",
                        unit="(unitless)",
                    ),
                    TDatacubeComponent(
                        field_componentDatatype=FieldComponentDatatype.double,
                        concept="slope",
                        unit="(unitless)",
                    ),
                ],
            ),
            data=TDatacubeData(
                dimensions=[well.melt_curve_raw_data.reading],
                measures=[
                    well.melt_curve_raw_data.fluorescence,
                    well.melt_curve_raw_data.derivative,
                ],
            ),
        )

src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_parser.py
Benchling-Open-Source/allotropy
# header
# sample setup
# raw data
# amplification data
# multicomponent data
# results
# melt curve raw data

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import Optional

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import (
    ExperimentType,
)
from allotropy.parsers.appbio_quantstudio.calculated_document import CalculatedDocument
from allotropy.parsers.appbio_quantstudio.referenceable import Referenceable


@dataclass
class Header:
    measurement_time: str
    plate_well_count: int
    experiment_type: ExperimentType
    device_identifier: str
    model_number: str
    device_serial_number: str
    measurement_method_identifier: str
    pcr_detection_chemistry: str
    passive_reference_dye_setting: Optional[str]
    barcode: Optional[str]
    analyst: Optional[str]
    experimental_data_identifier: Optional[str]


@dataclass
class WellItem(Referenceable):
    uuid: str
    identifier: int
    target_dna_description: str
    sample_identifier: str
    reporter_dye_setting: Optional[str]
    position: Optional[str]
    well_location_identifier: Optional[str]
    quencher_dye_setting: Optional[str]
    sample_role_type: Optional[str]
    amplification_data_obj: Optional[AmplificationData] = None
    result_obj: Optional[Result] = None

    def add_amplification_data(self, amplification_data: AmplificationData) -> None:
        self.amplification_data_obj = amplification_data

    @property
    def amplification_data(self) -> AmplificationData:
        if self.amplification_data_obj is None:
            msg = f"Unable to find amplification data for well {self.identifier}"
            raise AllotropeConversionError(msg)
        return self.amplification_data_obj

    def add_result(self, result: Result) -> None:
        self.result_obj = result

    @property
    def result(self) -> Result:
        if self.result_obj is None:
            msg = f"Unablt to find result data for well {self.identifier}"
            raise AllotropeConversionError(msg)
        return self.result_obj


@dataclass
class Well:
    identifier: int
    items: dict[str, WellItem]
    multicomponent_data: Optional[MulticomponentData] = None
    melt_curve_raw_data: Optional[MeltCurveRawData] = None
    calculated_document: Optional[CalculatedDocument] = None

    def get_well_item(self, target: str) -> WellItem:
        well_item = self.items.get(target)
        if well_item is None:
            msg = f"Unable to find target dna {target} for well {self.identifier}"
            raise AllotropeConversionError(msg)
        return well_item

    def get_an_well_item(self) -> Optional[WellItem]:
        if not self.items:
            return None
        target, *_ = self.items.keys()
        return self.items[target]

    def add_multicomponent_data(self, multicomponent_data: MulticomponentData) -> None:
        self.multicomponent_data = multicomponent_data

    def add_melt_curve_raw_data(self, melt_curve_raw_data: MeltCurveRawData) -> None:
        self.melt_curve_raw_data = melt_curve_raw_data


@dataclass
class WellList:
    wells: list[Well]

    def iter_well_items(self) -> Iterator[WellItem]:
        for well in self.wells:
            yield from well.items.values()

    def __iter__(self) -> Iterator[Well]:
        return iter(self.wells)


@dataclass
class RawData:
    lines: list[str]


@dataclass
class AmplificationData:
    total_cycle_number_setting: float
    cycle: list[float]
    rn: list[Optional[float]]
    delta_rn: list[Optional[float]]


@dataclass
class MulticomponentData:
    cycle: list[float]
    columns: dict[str, list[Optional[float]]]

    def get_column(self, name: str) -> list[Optional[float]]:
        column = self.columns.get(name)
        if column is None:
            msg = f"Unable to obtain {name} from multicomponent data"
            raise AllotropeConversionError(msg)
        return column


@dataclass
class Result:
    cycle_threshold_value_setting: float
    cycle_threshold_result: Optional[float]
    automatic_cycle_threshold_enabled_setting: Optional[bool]
    automatic_baseline_determination_enabled_setting: Optional[bool]
    normalized_reporter_result: Optional[float]
    baseline_corrected_reporter_result: Optional[float]
    genotyping_determination_result: Optional[str]
    genotyping_determination_method_setting: Optional[float]
    quantity: Optional[float]
    quantity_mean: Optional[float]
    quantity_sd: Optional[float]
    ct_mean: Optional[float]
    ct_sd: Optional[float]
    delta_ct_mean: Optional[float]
    delta_ct_se: Optional[float]
    delta_delta_ct: Optional[float]
    rq: Optional[float]
    rq_min: Optional[float]
    rq_max: Optional[float]
    rn_mean: Optional[float]
    rn_sd: Optional[float]
    y_intercept: Optional[float]
    r_squared: Optional[float]
    slope: Optional[float]
    efficiency: Optional[float]


@dataclass
class MeltCurveRawData:
    reading: list[float]
    fluorescence: list[Optional[float]]
    derivative: list[Optional[float]]


@dataclass
class Data:
    header: Header
    wells: WellList
    raw_data: Optional[RawData]
    endogenous_control: str
    reference_sample: str
    calculated_documents: list[CalculatedDocument]

src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_structure.py
Benchling-Open-Source/allotropy
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import WellItem
from allotropy.parsers.appbio_quantstudio.views import View


class SampleView(View[WellItem]):
    def sort_elements(self, well_items: list[WellItem]) -> dict[str, list[WellItem]]:
        items: dict[str, list[WellItem]] = {}
        for well_item in well_items:
            if well_item.sample_identifier not in items:
                items[well_item.sample_identifier] = []
            items[well_item.sample_identifier].append(well_item)
        return items


class TargetView(View[WellItem]):
    def sort_elements(self, well_items: list[WellItem]) -> dict[str, list[WellItem]]:
        items: dict[str, list[WellItem]] = {}
        for well_item in well_items:
            if well_item.target_dna_description not in items:
                items[well_item.target_dna_description] = []
            items[well_item.target_dna_description].append(well_item)
        return items


class TargetRoleView(View[WellItem]):
    def sort_elements(self, well_items: list[WellItem]) -> dict[str, list[WellItem]]:
        items: dict[str, list[WellItem]] = {}
        for well_item in well_items:
            if well_item.sample_role_type == "STANDARD":
                if well_item.target_dna_description not in items:
                    items[well_item.target_dna_description] = []
                items[well_item.target_dna_description].append(well_item)
        return items

src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_views.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import Union

from allotropy.parsers.appbio_quantstudio.referenceable import Referenceable


@dataclass
class DataSource:
    feature: str
    reference: Union[CalculatedDocument, Referenceable]


@dataclass
class CalculatedDocument(Referenceable):
    uuid: str
    name: str
    value: float
    data_sources: list[DataSource]
    iterated: bool = False

    def iter_struct(self) -> Iterator[CalculatedDocument]:
        if self.iterated:
            return

        self.iterated = True
        yield self
        for data_source in self.data_sources:
            if isinstance(data_source.reference, CalculatedDocument):
                yield from data_source.reference.iter_struct()

src/allotropy/parsers/appbio_quantstudio/calculated_document.py
Benchling-Open-Source/allotropy
from typing import Any, Callable


def cache(fun: Callable) -> Callable:
    cache: dict[str, Any] = {}

    def inner(*args: Any) -> Any:
        key = "".join([str(arg) for arg in args])
        if key in cache:
            return cache[key]
        result = fun(*args)
        cache[key] = result
        return result

    return inner

src/allotropy/parsers/appbio_quantstudio/decorators.py
Benchling-Open-Source/allotropy
from dataclasses import dataclass


@dataclass
class Referenceable:
    uuid: str

src/allotropy/parsers/appbio_quantstudio/referenceable.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from collections.abc import Iterator
from typing import Generic, Optional, TypeVar, Union

from allotropy.allotrope.allotrope import AllotropeConversionError

T = TypeVar("T")


class ViewData(Generic[T]):
    def __init__(self, data: dict[str, Union[ViewData, list[T]]]):
        self.data = data

    def get_first_key(self) -> str:
        key, *_ = self.data.keys()
        return key

    def iter_keys(self) -> Iterator[list[str]]:
        for key, item in self.data.items():
            if isinstance(item, ViewData):
                for sub_keys in item.iter_keys():
                    yield [key, *sub_keys]
            else:
                yield [key]

    def get_item(self, *keys: str) -> Union[ViewData, list[T]]:
        if len(keys) == 0:
            return self.data[self.get_first_key()]

        key, *sub_keys = keys
        item = self.data[key]
        if isinstance(item, ViewData):
            return item.get_item(*sub_keys)
        else:
            return item

    def get_sub_view_data(self, *keys: str) -> ViewData:
        item = self.get_item(*keys)
        if isinstance(item, ViewData):
            return item
        msg = f"Unable to get sub view data with keys {keys}"
        raise AllotropeConversionError(msg)

    def get_leaf_item(self, *keys: str) -> list[T]:
        item = self.get_item(*keys)
        if isinstance(item, ViewData):
            msg = f"Unable to get leaf item of view data with keys {keys}"
            raise AllotropeConversionError(msg)
        return item


class View(Generic[T]):
    def __init__(self, sub_view: Optional[View] = None):
        self.sub_view = sub_view

    def sort_elements(self, _: list[T]) -> dict[str, list[T]]:
        return {}

    def apply(self, elements: list[T]) -> ViewData:
        return ViewData(
            {
                id_: self.sub_view.apply(element) if self.sub_view else element
                for id_, element in self.sort_elements(elements).items()
            }
        )

src/allotropy/parsers/appbio_quantstudio/views.py
Benchling-Open-Source/allotropy
DEFAULT_ANALYST = "Vi-CELL"
DEFAULT_MODEL_NUMBER = "Vi-Cell BLU"
VICELL_BLU_SOFTWARE_NAME = "Vi-Cell BLU"

src/allotropy/parsers/beckman_vi_cell_blu/constants.py
Benchling-Open-Source/allotropy
import io
from typing import Any
import uuid

import pandas as pd

from allotropy.allotrope.models.cell_counter_benchling_2023_09_cell_counter import (
    CellCountingAggregateDocument,
    CellCountingDocumentItem,
    DataProcessingDocument,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    ProcessedDataDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueCell,
    TQuantityValueMicrometer,
    TQuantityValueMillionCellsPerMilliliter,
    TQuantityValuePercent,
    TQuantityValueUnitless,
)
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parsers.beckman_vi_cell_blu.constants import (
    DEFAULT_ANALYST,
    DEFAULT_MODEL_NUMBER,
    VICELL_BLU_SOFTWARE_NAME,
)
from allotropy.parsers.beckman_vi_cell_blu.vi_cell_blu_reader import ViCellBluReader
from allotropy.parsers.vendor_parser import VendorParser

property_lookup = {
    "Average viable diameter (μm)": TQuantityValueMicrometer,
    "Average circularity": TQuantityValueUnitless,
    "Average diameter (μm)": TQuantityValueMicrometer,
    "Average viable circularity": TQuantityValueUnitless,
    "Dilution": TQuantityValueUnitless,
    "Maximum Diameter (μm)": TQuantityValueMicrometer,
    "Minimum Diameter (μm)": TQuantityValueMicrometer,
    "Cell count": TQuantityValueCell,
    "Total (x10^6) cells/mL": TQuantityValueMillionCellsPerMilliliter,
    "Viability (%)": TQuantityValuePercent,
    "Viable cells": TQuantityValueCell,
    "Viable (x10^6) cells/mL": TQuantityValueMillionCellsPerMilliliter,
}


def get_property_from_sample(sample: pd.Series, property_name: str) -> Any:
    return property_lookup[property_name](value=value) if (value := sample.get(property_name)) else None  # type: ignore[arg-type]


class ViCellBluParser(VendorParser):
    def _parse(self, contents: io.IOBase, filename: str) -> Model:
        return self._get_model(ViCellBluReader.read(contents), filename)

    def _get_model(self, data: pd.DataFrame, filename: str) -> Model:
        return Model(
            cell_counting_aggregate_document=CellCountingAggregateDocument(
                device_system_document=DeviceSystemDocument(
                    model_number=DEFAULT_MODEL_NUMBER,
                ),
                data_system_document=DataSystemDocument(
                    file_name=filename,
                    software_name=VICELL_BLU_SOFTWARE_NAME,
                    ASM_converter_name=ASM_CONVERTER_NAME,
                    ASM_converter_version=ASM_CONVERTER_VERSION,
                ),
                cell_counting_document=self._get_cell_counting_document(data),
            ),
        )

    def _get_cell_counting_document(
        self, data: pd.DataFrame
    ) -> list[CellCountingDocumentItem]:
        return [
            self._get_cell_counting_document_item(sample)
            for _, sample in data.iterrows()
            if sample.get("Cell count")
        ]

    def _get_cell_counting_document_item(
        self, sample: pd.Series
    ) -> CellCountingDocumentItem:
        return CellCountingDocumentItem(
            analyst=sample.get("Analysis by") or DEFAULT_ANALYST,  # type: ignore[arg-type]
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_document=[
                    MeasurementDocumentItem(
                        measurement_identifier=str(uuid.uuid4()),
                        measurement_time=self.get_date_time(
                            sample.get("Analysis date/time")
                        ),
                        sample_document=SampleDocument(sample_identifier=sample.get("Sample ID")),  # type: ignore[arg-type]
                        device_control_aggregate_document=DeviceControlAggregateDocument(
                            device_control_document=[
                                DeviceControlDocumentItem(
                                    device_type="brightfield imager (cell counter)",
                                    detection_type="brightfield",
                                )
                            ]
                        ),
                        processed_data_document=ProcessedDataDocument(
                            data_processing_document=DataProcessingDocument(
                                cell_type_processing_method=sample.get("Cell type"),  # type: ignore[arg-type]
                                minimum_cell_diameter=get_property_from_sample(
                                    sample, "Minimum Diameter (μm)"
                                ),
                                maximum_cell_diameter=get_property_from_sample(
                                    sample, "Maximum Diameter (μm)"
                                ),
                                cell_density_dilution_factor=get_property_from_sample(
                                    sample, "Dilution"
                                ),
                            ),
                            viability__cell_counter_=get_property_from_sample(
                                sample, "Viability (%)"
                            ),
                            viable_cell_density__cell_counter_=get_property_from_sample(
                                sample, "Viable (x10^6) cells/mL"
                            ),
                            total_cell_count=get_property_from_sample(
                                sample, "Cell count"
                            ),
                            total_cell_density__cell_counter_=get_property_from_sample(
                                sample, "Total (x10^6) cells/mL"
                            ),
                            average_total_cell_diameter=get_property_from_sample(
                                sample, "Average diameter (μm)"
                            ),
                            average_live_cell_diameter__cell_counter_=get_property_from_sample(
                                sample, "Average viable diameter (μm)"
                            ),
                            viable_cell_count=get_property_from_sample(
                                sample, "Viable cells"
                            ),
                            average_total_cell_circularity=get_property_from_sample(
                                sample, "Average circularity"
                            ),
                            average_viable_cell_circularity=get_property_from_sample(
                                sample, "Average viable circularity"
                            ),
                        ),
                    )
                ],
            ),
        )

src/allotropy/parsers/beckman_vi_cell_blu/vi_cell_blu_parser.py
Benchling-Open-Source/allotropy
import io

import numpy as np
import pandas as pd


def convert_datetime(x: pd.Series) -> pd.Series:
    return pd.to_datetime(x).dt.strftime("%Y-%m-%d %H:%M:%S")


def convert_float(x: pd.Series) -> pd.Series:
    return pd.to_numeric(x, errors="coerce")


def convert_int(x: pd.Series) -> pd.Series:
    return pd.to_numeric(x, errors="coerce")


def convert_string(x: pd.Series) -> pd.Series:
    return x.astype(str)


conversors = {
    np.datetime64: convert_datetime,
    np.float64: convert_float,
    np.int64: convert_int,
    np.dtypes.ObjectDType: convert_string,
}


desired_columns = {
    "Analysis by": np.dtypes.ObjectDType,
    "Sample ID": np.dtypes.ObjectDType,
    "Minimum Diameter (μm)": np.float64,
    "Maximum Diameter (μm)": np.float64,
    "Analysis date/time": np.datetime64,
    "Cell type": np.dtypes.ObjectDType,
    "Dilution": np.float64,
    "Viability (%)": np.float64,
    "Total (x10^6) cells/mL": np.float64,
    "Viable (x10^6) cells/mL": np.float64,
    "Average diameter (μm)": np.float64,
    "Average viable diameter (μm)": np.float64,
    "Cell count": np.int64,
    "Viable cells": np.int64,
    "Average circularity": np.float64,
    "Average viable circularity": np.float64,
}


class ViCellBluReader:
    @classmethod
    def read(cls, contents: io.IOBase) -> pd.DataFrame:
        raw_data = pd.read_csv(contents, index_col=False)  # type: ignore[call-overload]

        columns: list[pd.Series] = []
        for column, desired_type in desired_columns.items():
            col = raw_data.get(column)
            if not isinstance(col, pd.Series):
                continue

            new_col = (
                col if col.dtype == desired_type else conversors[desired_type](col)
            )
            columns.append(new_col)

        return pd.concat(columns, axis=1).replace(np.nan, None)

src/allotropy/parsers/beckman_vi_cell_blu/vi_cell_blu_reader.py
Benchling-Open-Source/allotropy
from enum import Enum


class XrVersion(str, Enum):
    _2_06 = "2.06"
    _2_04 = "2.04"


DATE_HEADER = {
    XrVersion._2_06: "Sample date/time",
    XrVersion._2_04: "Sample date",
}

MODEL_RE = r"Vi-CELL XR (?P<version>\d{1,}\.\d{2,}(.\d{1,})?)"
DEFAULT_VERSION = XrVersion._2_06

DEFAULT_ANALYST = "Vi-Cell XR"
MODEL_NUMBER = "Vi-Cell XR"
SOFTWARE_NAME = "Vi-Cell XR"

src/allotropy/parsers/beckman_vi_cell_xr/constants.py
Benchling-Open-Source/allotropy
import io
from typing import Any, Optional
import uuid

import pandas as pd

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.cell_counter_benchling_2023_09_cell_counter import (
    CellCountingAggregateDocument,
    CellCountingDocumentItem,
    DataProcessingDocument,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    ProcessedDataDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueCell,
    TQuantityValueMicrometer,
    TQuantityValueMillionCellsPerMilliliter,
    TQuantityValuePercent,
    TQuantityValueUnitless,
)
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parsers.beckman_vi_cell_xr.constants import (
    DATE_HEADER,
    DEFAULT_ANALYST,
    MODEL_NUMBER,
    SOFTWARE_NAME,
    XrVersion,
)
from allotropy.parsers.beckman_vi_cell_xr.vi_cell_xr_reader import ViCellXRReader
from allotropy.parsers.vendor_parser import VendorParser

property_lookup = {
    "Dilution factor": TQuantityValueUnitless,
    "Total cells/ml (x10^6)": TQuantityValueMillionCellsPerMilliliter,
    "Avg. diam. (microns)": TQuantityValueMicrometer,
    "Viable cells": TQuantityValueCell,
    "Avg. circ.": TQuantityValueUnitless,
}


def get_property_from_sample(sample: pd.Series, property_name: str) -> Any:
    return property_lookup[property_name](value=value) if (value := sample.get(property_name)) else None  # type: ignore[arg-type]


class ViCellXRParser(VendorParser):
    def _parse(self, contents: io.IOBase, filename: str) -> Model:
        reader = ViCellXRReader(contents)

        return Model(
            cell_counting_aggregate_document=CellCountingAggregateDocument(
                device_system_document=DeviceSystemDocument(
                    model_number=MODEL_NUMBER,
                    device_serial_number=self._get_device_serial_number(
                        reader.file_info
                    ),
                ),
                data_system_document=DataSystemDocument(
                    file_name=filename,
                    software_name=SOFTWARE_NAME,
                    software_version=reader.file_version.value,
                    ASM_converter_name=ASM_CONVERTER_NAME,
                    ASM_converter_version=ASM_CONVERTER_VERSION,
                ),
                cell_counting_document=[
                    self._get_cell_counting_document_item(sample, reader.file_version)
                    for _, sample in reader.data.iterrows()
                ],
            ),
        )

    def _get_device_serial_number(self, file_info: pd.Series) -> Optional[str]:
        serial = str(file_info["serial"])
        try:
            serial_number = serial[serial.rindex(":") + 1 :].strip()
        except ValueError:
            return None

        return serial_number

    def _get_cell_counting_document_item(
        self, sample: pd.Series, file_version: XrVersion
    ) -> CellCountingDocumentItem:
        # Required fields
        try:
            viability__cell_counter_ = TQuantityValuePercent(
                value=sample["Viability (%)"]
            )
            total_cell_count = TQuantityValueCell(value=sample["Total cells"])
            viable_cell_density__cell_counter_ = (
                TQuantityValueMillionCellsPerMilliliter(
                    value=sample["Viable cells/ml (x10^6)"]
                )
            )
        except KeyError as e:
            error = f"required value not found {e}"
            raise AllotropeConversionError(error) from e

        return CellCountingDocumentItem(
            analyst=DEFAULT_ANALYST,
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_document=[
                    MeasurementDocumentItem(
                        measurement_identifier=str(uuid.uuid4()),
                        measurement_time=self.get_date_time(
                            sample.get(DATE_HEADER[file_version])
                        ),
                        sample_document=SampleDocument(
                            sample_identifier=sample["Sample ID"]
                        ),
                        device_control_aggregate_document=DeviceControlAggregateDocument(
                            device_control_document=[
                                DeviceControlDocumentItem(
                                    device_type="brightfield imager (cell counter)",
                                    detection_type="brightfield",
                                )
                            ]
                        ),
                        processed_data_document=ProcessedDataDocument(
                            data_processing_document=DataProcessingDocument(
                                cell_type_processing_method=sample.get("Cell type"),  # type: ignore[arg-type]
                                cell_density_dilution_factor=get_property_from_sample(
                                    sample, "Dilution factor"
                                ),
                            ),
                            viability__cell_counter_=viability__cell_counter_,
                            viable_cell_density__cell_counter_=viable_cell_density__cell_counter_,
                            total_cell_count=total_cell_count,
                            total_cell_density__cell_counter_=get_property_from_sample(
                                sample, "Total cells/ml (x10^6)"
                            ),
                            average_total_cell_diameter=get_property_from_sample(
                                sample, "Avg. diam. (microns)"
                            ),
                            viable_cell_count=get_property_from_sample(
                                sample, "Viable cells"
                            ),
                            average_total_cell_circularity=get_property_from_sample(
                                sample, "Avg. circ."
                            ),
                        ),
                    )
                ],
            ),
        )

src/allotropy/parsers/beckman_vi_cell_xr/vi_cell_xr_parser.py
Benchling-Open-Source/allotropy
from io import IOBase
import re

import pandas as pd

from allotropy.parsers.beckman_vi_cell_xr.constants import (
    DATE_HEADER,
    DEFAULT_VERSION,
    MODEL_RE,
    XrVersion,
)


class ViCellXRReader:
    def __init__(self, contents: IOBase) -> None:
        self.contents = contents
        self.file_info = self._get_file_info()
        self.file_version = self._get_file_version()
        self.data = self._read_data()

    def _read_data(self) -> pd.DataFrame:
        header_row = 4
        if self.file_version == XrVersion._2_04:
            header_row = 3

        header = self._get_file_header(header_row)
        skiprows = header_row + 1
        file_data: pd.DataFrame = pd.read_excel(
            self.contents,
            skiprows=skiprows,
            names=header,
            parse_dates=[DATE_HEADER[self.file_version]],
            date_format="%d %b %Y  %I:%M:%S %p",
        )
        return file_data

    def _get_file_header(self, header_row: int) -> list[str]:
        """Combine the two rows that forms the header."""
        header = pd.read_excel(
            self.contents,
            nrows=2,
            skiprows=header_row,
            header=None,
        ).fillna("")

        header_list: list = header.agg(
            lambda x: " ".join(x).replace(" /ml", "/ml").strip()
        ).to_list()
        return header_list

    def _get_file_info(self) -> pd.Series:
        info: pd.Series = pd.read_excel(
            self.contents, nrows=3, header=None, usecols=[0]
        ).squeeze()
        info.index = pd.Index(["model", "filepath", "serial"])
        return info

    def _get_file_version(self) -> XrVersion:
        match = re.match(MODEL_RE, self.file_info["model"], flags=re.IGNORECASE)
        try:
            version = match.groupdict()["version"]  # type: ignore[union-attr]
        except AttributeError:
            return DEFAULT_VERSION
        # TODO: raise exception for unsupported versions
        version = ".".join(version.split(".")[0:2])
        return XrVersion(version)

src/allotropy/parsers/beckman_vi_cell_xr/vi_cell_xr_reader.py
Benchling-Open-Source/allotropy
from collections.abc import Iterator
from io import IOBase, StringIO
from re import search
from typing import Optional

import pandas as pd


class LinesReader:
    def __init__(self, io_: IOBase):
        raw_contents = io_.read()
        if isinstance(raw_contents, bytes):
            raw_contents = raw_contents.decode("UTF-8")
        self.contents = raw_contents.replace("\r\n", "\n")
        self.lines: list[str] = self.contents.split("\n")
        self.n_lines = len(self.lines)
        self.current_line = 0

    def current_line_exists(self) -> bool:
        return 0 <= self.current_line < self.n_lines

    def get(self) -> Optional[str]:
        return self.lines[self.current_line] if self.current_line_exists() else None

    def match(self, pattern: str) -> bool:
        line = self.get()
        return False if line is None else bool(search(pattern, line))

    def is_empty(self) -> bool:
        return self.match("^\\s*$")

    def pop(self) -> Optional[str]:
        line = self.get()
        if line is not None:
            self.current_line += 1
        return line

    def pop_if_match(self, pattern: str) -> Optional[str]:
        if self.match(pattern):
            return self.pop()
        return None

    def pop_data(self) -> Optional[str]:
        self.drop_empty()
        return self.pop()

    def drop_until(self, pattern: str) -> Optional[str]:
        while self.current_line_exists() and not self.match(pattern):
            self.pop()
        return self.get()

    def drop_empty(self) -> Optional[str]:
        while self.current_line_exists() and self.is_empty():
            self.pop()
        return self.get()

    def drop_until_empty(self) -> Optional[str]:
        while self.current_line_exists() and not self.is_empty():
            self.pop()
        return self.get()

    def pop_until(self, pattern: str) -> Iterator[str]:
        while self.current_line_exists() and not self.match(pattern):
            line = self.pop()
            if line is not None:
                yield line

    def pop_until_empty(self) -> Iterator[str]:
        while self.current_line_exists() and not self.is_empty():
            line = self.pop()
            if line is not None:
                yield line

    def pop_csv_block(self, pattern: Optional[str] = None) -> pd.DataFrame:
        self.drop_empty()
        if pattern:
            if not self.match(pattern):
                msg = f"Did not find {pattern}"
                raise Exception(msg)
            self.pop()  # remove title

        csv_stream = StringIO("\n".join(self.pop_until_empty()))
        self.drop_empty()

        return pd.read_csv(csv_stream, header=None)

    def drop_sections(self, pattern: str) -> None:
        self.drop_empty()
        while True:
            if not self.match(pattern):
                return
            self.drop_until_empty()
            self.drop_empty()

src/allotropy/parsers/lines_reader.py
Benchling-Open-Source/allotropy
from typing import Any, Optional
import uuid

from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueNanometer,
    TQuantityValueNumber,
)
from allotropy.allotrope.models.ultraviolet_absorbance_benchling_2023_09_ultraviolet_absorbance import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.parsers.moldev_softmax_pro.plate_block import PlateBlock, WellData
from allotropy.parsers.utils.values import assert_not_none, natural_sort_key, try_int


class AbsorbancePlateBlock(PlateBlock):
    READ_MODE = "Absorbance"
    CONCEPT = "absorbance"
    UNIT = "mAU"
    DATA_TYPE_IDX = 6

    def parse_read_mode_header(self, header: list[Optional[str]]) -> None:
        [self.first_row, num_rows] = header[19:21]
        self.pmt_gain = None
        self.num_rows = assert_not_none(try_int(num_rows), "num_rows")

    def generate_device_control_doc(self) -> DeviceControlDocumentItem:
        device_control_doc = DeviceControlDocumentItem(
            detector_gain_setting=self.pmt_gain
        )

        if self.is_single_wavelength:
            device_control_doc.detector_wavelength_setting = TQuantityValueNanometer(
                self.wavelengths[0]
            )

        return device_control_doc

    def generate_measurement_doc(
        self, well: str, well_data: WellData
    ) -> MeasurementDocumentItem:
        measurement = MeasurementDocumentItem(
            DeviceControlAggregateDocument([self.generate_device_control_doc()]),
            self.generate_sample_document(well),
        )

        if well_data.temperature is not None:
            measurement.compartment_temperature = TQuantityValueDegreeCelsius(
                float(well_data.temperature)
            )

        if not well_data.is_empty:
            measurement.data_cube = self.generate_data_cube(well_data)

        if well_data.processed_data:
            measurement.processed_data_aggregate_document = (
                self.generate_processed_data_aggreate_document(well_data)
            )

        return measurement

    def to_allotrope(self) -> Any:
        wells = sorted(self.well_data.keys(), key=natural_sort_key)

        allotrope_file = Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                plate_well_count=TQuantityValueNumber(self.num_wells),
                measurement_document=[
                    self.generate_measurement_doc(well, self.well_data[well])
                    for well in wells
                ],
            )
        )

        return allotrope_file

src/allotropy/parsers/moldev_softmax_pro/absorbance_plate_block.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.moldev_softmax_pro.absorbance_plate_block import (
    AbsorbancePlateBlock,
)
from allotropy.parsers.moldev_softmax_pro.fluorescence_plate_block import (
    FluorescencePlateBlock,
)
from allotropy.parsers.moldev_softmax_pro.luminescence_plate_block import (
    LuminescencePlateBlock,
)
from allotropy.parsers.moldev_softmax_pro.plate_block import (
    Block,
    GroupBlock,
    NoteBlock,
    PlateBlock,
)
from allotropy.parsers.utils.values import value_or_none

DELIMITER = "\t"


def create_plate_block(raw_lines: list[str]) -> PlateBlock:
    split_lines = [
        [value_or_none(value) for value in raw_line.split(DELIMITER)]
        for raw_line in raw_lines
    ]
    header = split_lines[0]
    read_mode = header[5]
    plate_block_cls: type[PlateBlock]
    if read_mode == "Absorbance":
        plate_block_cls = AbsorbancePlateBlock
    elif read_mode == "Fluorescence":
        plate_block_cls = FluorescencePlateBlock
    elif read_mode == "Luminescence":
        plate_block_cls = LuminescencePlateBlock
    else:
        error = f"unrecognized read mode {read_mode}"
        raise AllotropeConversionError(error)
    plate_block = plate_block_cls(header, split_lines, raw_lines)
    return plate_block


ALL_BLOCKS: list[type[Block]] = [GroupBlock, NoteBlock, PlateBlock]
BLOCK_CLS_BY_TYPE = {cls.BLOCK_TYPE: cls for cls in ALL_BLOCKS}


def create_block(lines: list[str]) -> Block:
    for key, block_cls in BLOCK_CLS_BY_TYPE.items():
        if lines[0].startswith(key):
            if block_cls == PlateBlock:
                return create_plate_block(lines)
            return block_cls(lines)
    error = f"unrecognized block {lines[0]}"
    raise AllotropeConversionError(error)

src/allotropy/parsers/moldev_softmax_pro/block_factory.py
Benchling-Open-Source/allotropy
from typing import Any, ClassVar, Optional
import uuid

from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueNanometer,
    TQuantityValueNumber,
)
from allotropy.parsers.moldev_softmax_pro.plate_block import (
    PlateBlock,
    split_wavelengths,
    WellData,
)
from allotropy.parsers.utils.values import assert_not_none, natural_sort_key, try_int


class FluorescenceOrLuminescencePlateBlock(PlateBlock):
    EXCITATION_WAVELENGTHS_IDX: ClassVar[int]

    def parse_read_mode_header(self, header: list[Optional[str]]) -> None:
        [
            excitation_wavelengths_str,
            self.cutoff,
            cutoff_filters_str,
            self.sweep_wave,
            self.sweep_wavelength,
            self.reads_per_well,
            self.pmt_gain,
            self.start_integration_time,
            self.end_integration_time,
            self.first_row,
            num_rows,
        ] = header[
            self.EXCITATION_WAVELENGTHS_IDX : self.EXCITATION_WAVELENGTHS_IDX + 11
        ]
        self.excitation_wavelengths = split_wavelengths(excitation_wavelengths_str)
        self.cutoff_filters = split_wavelengths(cutoff_filters_str)
        self.num_rows = assert_not_none(try_int(num_rows), "num_rows")


class FluorescencePlateBlock(FluorescenceOrLuminescencePlateBlock):
    READ_MODE = "Fluorescence"
    CONCEPT = "fluorescence"
    UNIT = "RFU"
    DATA_TYPE_IDX = 7
    EXCITATION_WAVELENGTHS_IDX = 20

    # TODO: the reason we can't factor out DeviceControlDocumentItem and the enclosing classes is because the
    # Fluorescence ASM model has these extra fields. We may be able to fix this by templating the PlateBlock
    # class on these classes, or some clever shared/models refactoring.
    def generate_device_control_doc(self) -> DeviceControlDocumentItem:
        device_control_doc = DeviceControlDocumentItem(
            detector_gain_setting=self.pmt_gain
        )

        if self.is_single_wavelength:
            device_control_doc.detector_wavelength_setting = TQuantityValueNanometer(
                self.wavelengths[0]
            )
        if self.excitation_wavelengths and len(self.excitation_wavelengths) == 1:
            device_control_doc.excitation_wavelength_setting = TQuantityValueNanometer(
                self.excitation_wavelengths[0]
            )
        if self.cutoff_filters and len(self.cutoff_filters) == 1:
            device_control_doc.wavelength_filter_cutoff_setting = (
                TQuantityValueNanometer(self.cutoff_filters[0])
            )

        return device_control_doc

    def generate_measurement_doc(
        self, well: str, well_data: WellData
    ) -> MeasurementDocumentItem:
        measurement = MeasurementDocumentItem(
            DeviceControlAggregateDocument([self.generate_device_control_doc()]),
            self.generate_sample_document(well),
        )

        if well_data.temperature is not None:
            measurement.compartment_temperature = TQuantityValueDegreeCelsius(
                float(well_data.temperature)
            )

        if not well_data.is_empty:
            measurement.data_cube = self.generate_data_cube(well_data)

        if well_data.processed_data:
            measurement.processed_data_aggregate_document = (
                self.generate_processed_data_aggreate_document(well_data)
            )

        return measurement

    def to_allotrope(self) -> Any:
        wells = sorted(self.well_data.keys(), key=natural_sort_key)

        allotrope_file = Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                plate_well_count=TQuantityValueNumber(self.num_wells),
                measurement_document=[
                    self.generate_measurement_doc(well, self.well_data[well])
                    for well in wells
                ],
            )
        )

        return allotrope_file

src/allotropy/parsers/moldev_softmax_pro/fluorescence_plate_block.py
Benchling-Open-Source/allotropy
from typing import Any
import uuid

from allotropy.allotrope.models.luminescence_benchling_2023_09_luminescence import (
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueNanometer,
    TQuantityValueNumber,
)
from allotropy.parsers.moldev_softmax_pro.fluorescence_plate_block import (
    FluorescenceOrLuminescencePlateBlock,
)
from allotropy.parsers.moldev_softmax_pro.plate_block import WellData
from allotropy.parsers.utils.values import natural_sort_key


class LuminescencePlateBlock(FluorescenceOrLuminescencePlateBlock):
    READ_MODE = "Luminescence"
    CONCEPT = "luminescence"
    UNIT = "RLU"
    DATA_TYPE_IDX = 6
    EXCITATION_WAVELENGTHS_IDX = 19

    def generate_device_control_doc(self) -> DeviceControlDocumentItem:
        device_control_doc = DeviceControlDocumentItem(
            detector_gain_setting=self.pmt_gain
        )

        if self.is_single_wavelength:
            device_control_doc.detector_wavelength_setting = TQuantityValueNanometer(
                self.wavelengths[0]
            )

        return device_control_doc

    def generate_measurement_doc(
        self, well: str, well_data: WellData
    ) -> MeasurementDocumentItem:
        measurement = MeasurementDocumentItem(
            DeviceControlAggregateDocument([self.generate_device_control_doc()]),
            self.generate_sample_document(well),
        )

        if well_data.temperature is not None:
            measurement.compartment_temperature = TQuantityValueDegreeCelsius(
                float(well_data.temperature)
            )

        if not well_data.is_empty:
            measurement.data_cube = self.generate_data_cube(well_data)

        if well_data.processed_data:
            measurement.processed_data_aggregate_document = (
                self.generate_processed_data_aggreate_document(well_data)
            )

        return measurement

    def to_allotrope(self) -> Any:
        wells = sorted(self.well_data.keys(), key=natural_sort_key)

        allotrope_file = Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                plate_well_count=TQuantityValueNumber(self.num_wells),
                measurement_document=[
                    self.generate_measurement_doc(well, self.well_data[well])
                    for well in wells
                ],
            )
        )

        return allotrope_file

src/allotropy/parsers/moldev_softmax_pro/luminescence_plate_block.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from abc import ABC
from collections import defaultdict
from enum import Enum
from typing import Any, ClassVar, Optional

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacube,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)
from allotropy.parsers.utils.values import assert_not_none, PrimitiveValue, try_int

START_LETTER_CODE = ord("A")
EXPORT_VERSION = "1.3"


class ReadType(Enum):
    ENDPOINT = "Endpoint"
    KINETIC = "Kinetic"
    SPECTRUM = "Spectrum"
    WELL_SCAN = "Well Scan"


class ExportFormat(Enum):
    TIME_FORMAT = "TimeFormat"
    PLATE_FORMAT = "PlateFormat"


class DataType(Enum):
    RAW = "Raw"
    REDUCED = "Reduced"


class Block:
    BLOCK_TYPE: ClassVar[str]

    def __init__(self, raw_lines: list[str]):
        self.raw_lines = raw_lines


class GroupBlock(Block):
    GROUP_PREFIX = "Group: "
    GROUP_COLUMN_PREFIX = "Group Column"
    BLOCK_TYPE = "Group"
    name: str
    group_data: list[str]

    def __init__(self, raw_lines: list[str]):
        super().__init__(raw_lines)
        self.name = raw_lines[0][len(self.GROUP_PREFIX) :]
        for i, line in enumerate(raw_lines):
            if line.startswith(self.GROUP_COLUMN_PREFIX):
                self.group_data = raw_lines[1 : i - 1]
        # TODO handle group data


# TODO do we need to do anything with these?
class NoteBlock(Block):
    BLOCK_TYPE = "Note"


def split_wavelengths(
    values: Optional[str], delimiter: str = " "
) -> Optional[list[int]]:
    if values is None:
        return None
    return [int(value) for value in values.split(delimiter)]


def get_well_coordinates(row_number: int, column_number: str) -> str:
    row_letters = ""
    while row_number > 0:
        row_number, remainder = divmod(row_number - 1, 26)
        row_letters = chr(START_LETTER_CODE + remainder) + row_letters
    return row_letters + str(column_number)


class WellData:
    values: list[Optional[float]]
    dimensions: list[Optional[PrimitiveValue]]
    wavelengths: list[Optional[int]]
    temperature: Optional[str]
    processed_data: list[float]

    def __init__(self) -> None:
        self.values = []
        self.dimensions = []
        self.wavelengths = []
        self.temperature = None
        self.processed_data = []

    def add_value(
        self,
        value: float,
        dimension: Optional[PrimitiveValue],
        temperature: Optional[str],
        wavelength: Optional[int],
    ) -> None:
        self.values.append(value)
        self.dimensions.append(dimension)
        self.wavelengths.append(wavelength)
        if temperature is not None:
            if self.temperature is not None and temperature != self.temperature:
                error = "Expected all measurements to have the same temperature"
                raise AllotropeConversionError(error)
            self.temperature = temperature

    @property
    def is_empty(self) -> bool:
        return not self.dimensions or not self.values


class PlateBlock(Block, ABC):
    BLOCK_TYPE = "Plate"
    CONCEPT: ClassVar[str]
    MANIFEST: ClassVar[str]
    READ_MODE: ClassVar[str]
    UNIT: ClassVar[str]
    DATA_TYPE_IDX: ClassVar[int]
    data_header: list[Optional[str]]
    data_type: Optional[str]
    export_format: Optional[str]
    name: Optional[str]
    kinetic_points: int
    num_columns: int
    num_rows: int
    num_wavelengths: Optional[int]
    num_wells: int
    pmt_gain: Optional[str]
    read_type: Optional[str]
    wavelengths: list[int]
    well_data: defaultdict[str, WellData]

    def __init__(
        self,
        header: list[Optional[str]],
        split_lines: list[list[Optional[str]]],
        raw_lines: list[str],
    ):
        super().__init__(raw_lines)
        self.parse_header(header)
        self.well_data = defaultdict(WellData)
        self.data_header = split_lines[1]
        data_lines = split_lines[2:]
        if self.export_format == ExportFormat.TIME_FORMAT.value:
            self._parse_time_format_data(data_lines)
        elif self.export_format == ExportFormat.PLATE_FORMAT.value:
            self._parse_plate_format_data(data_lines)
        else:
            error = f"unrecognized export format {self.export_format}"
            raise AllotropeConversionError(error)

    def _parse_reduced_plate_rows(
        self, reduced_data_rows: list[list[Optional[str]]]
    ) -> None:
        for i, row in enumerate(reduced_data_rows):
            for j, value in enumerate(row[2 : self.num_columns + 2]):
                if value is None:
                    continue
                col_number = assert_not_none(self.data_header[j + 2], "column number")
                well = get_well_coordinates(i + 1, col_number)
                self.well_data[well].processed_data.append(float(value))

    def _parse_reduced_columns(self, reduced_data_row: list[Optional[str]]) -> None:
        for i, value in enumerate(reduced_data_row[2:]):
            if value is None:
                continue
            well = assert_not_none(self.data_header[i + 2], "well")
            self.well_data[well].processed_data.append(float(value))

    def _add_data_point(
        self,
        well: str,
        value: str,
        data_key: Optional[str],
        temperature: Optional[str],
        wavelength: Optional[int],
    ) -> None:
        dimension = (
            wavelength if self.read_type == ReadType.ENDPOINT.value else data_key
        )
        self.well_data[well].add_value(
            value=float(value),
            dimension=dimension,
            temperature=temperature,
            wavelength=wavelength,
        )

    def _parse_time_format_data(self, data_lines: list[list[Optional[str]]]) -> None:
        if self.data_type == DataType.RAW.value:
            num_row_blocks = (
                self.num_wavelengths if self.num_wavelengths is not None else 1
            )
            for wavelength_index in range(num_row_blocks):
                start_index = wavelength_index * (self.kinetic_points + 1)
                wavelength_rows = data_lines[
                    start_index : start_index + self.kinetic_points
                ]
                for row in wavelength_rows:
                    for i, value in enumerate(row[2 : self.num_wells + 2]):
                        if value is None:
                            continue
                        well = assert_not_none(self.data_header[i + 2], "well")
                        self._add_data_point(
                            well,
                            value,
                            data_key=row[0],
                            temperature=row[1],
                            wavelength=self.get_wavelength(wavelength_index),
                        )
            if len(data_lines) > (self.kinetic_points + 1) * num_row_blocks:
                reduced_row = data_lines[-1][: self.num_wells + 2]
                self._parse_reduced_columns(reduced_row)
        elif self.data_type == DataType.REDUCED.value:
            reduced_row = data_lines[-1][: self.num_wells + 2]
            self._parse_reduced_columns(reduced_row)
        else:
            error = f"unrecognized data type {self.data_type}"
            raise AllotropeConversionError(error)

    def _parse_plate_format_data(self, data_lines: list[list[Optional[str]]]) -> None:
        end_raw_data_index = 0
        if self.data_type == DataType.RAW.value:
            for read_index in range(self.kinetic_points):
                start_index = read_index * (self.num_rows + 1)
                read_rows = data_lines[start_index : start_index + self.num_rows]
                data_key = read_rows[0][0]
                temperature = read_rows[0][1]
                for i, row in enumerate(read_rows):
                    wavelength_index = 0
                    num_row_blocks = (
                        self.num_wavelengths if self.num_wavelengths is not None else 1
                    )
                    for wavelength_index in range(num_row_blocks):
                        col_start_index = 2 + (
                            wavelength_index * (self.num_columns + 1)
                        )
                        for j, value in enumerate(
                            row[col_start_index : col_start_index + self.num_columns]
                        ):
                            if value is None:
                                continue
                            col_number = assert_not_none(
                                self.data_header[j + 2], "column number"
                            )
                            well = get_well_coordinates(i + 1, col_number)
                            self._add_data_point(
                                well,
                                value,
                                data_key=data_key,
                                temperature=temperature,
                                wavelength=self.get_wavelength(wavelength_index),
                            )
            end_raw_data_index = ((self.num_rows + 1) * self.kinetic_points) + 1
            reduced_data_rows = data_lines[end_raw_data_index:]
            if len(reduced_data_rows) == self.num_rows:
                self._parse_reduced_plate_rows(reduced_data_rows)
        elif self.data_type == DataType.REDUCED.value:
            reduced_data_rows = data_lines[end_raw_data_index:]
            self._parse_reduced_plate_rows(reduced_data_rows)
        else:
            error = f"unrecognized data type {self.data_type}"
            raise AllotropeConversionError(error)

    def parse_read_mode_header(self, header: list[Optional[str]]) -> None:
        raise NotImplementedError

    def parse_header(self, header: list[Optional[str]]) -> None:
        [
            _,  # Plate:
            self.name,
            self.export_version,
            self.export_format,
            self.read_type,
            _,  # Read mode
        ] = header[:6]
        if self.export_version != EXPORT_VERSION:
            error = f"Invalid export version {self.export_version}"
            raise AllotropeConversionError(error)

        [
            self.data_type,
            _,  # Pre-read, always FALSE
            kinetic_points,
            read_time_or_scan_pattern,
            read_interval_or_scan_density,
            self.start_wavelength,
            self.end_wavelength,
            self.wavelength_step,
            num_wavelengths,
            wavelengths_str,
            self.first_column,
            num_columns,
            num_wells,
        ] = header[self.DATA_TYPE_IDX : self.DATA_TYPE_IDX + 13]
        self.kinetic_points = assert_not_none(try_int(kinetic_points), "kinetic_points")
        self.num_columns = assert_not_none(try_int(num_columns), "num_columns")
        self.num_wells = assert_not_none(try_int(num_wells), "num_wells")
        self.num_wavelengths = try_int(num_wavelengths)
        self.wavelengths = split_wavelengths(wavelengths_str) or []
        self.parse_read_mode_header(header)

        if self.read_type == ReadType.KINETIC.value:
            self.read_time = read_time_or_scan_pattern
            self.read_interval = read_interval_or_scan_density
        elif self.read_type == ReadType.WELL_SCAN.value:
            self.scan_pattern = read_time_or_scan_pattern
            self.scan_density = read_interval_or_scan_density

    @property
    def is_single_wavelength(self) -> bool:
        return (
            self.read_type in {ReadType.SPECTRUM.value, ReadType.ENDPOINT.value}
            and len(self.wavelengths) == 1
        )

    @property
    def has_wavelength_dimension(self) -> bool:
        return (
            self.read_type not in {ReadType.SPECTRUM.value, ReadType.ENDPOINT.value}
            and len(self.wavelengths) > 1
        )

    def get_wavelength(self, wavelength_index: int) -> Optional[int]:
        if self.read_type == ReadType.SPECTRUM.value:
            return None
        else:
            return self.wavelengths[wavelength_index] if self.wavelengths else None

    def get_data_cube_dimensions(self) -> list[tuple[str, str, Optional[str]]]:
        dimensions: list[tuple[str, str, Optional[str]]] = []
        if self.read_type == ReadType.KINETIC.value:
            dimensions = [("double", "elapsed time", "s")]
        elif self.read_type == ReadType.WELL_SCAN.value:
            dimensions = [("int", "x", None)]
        elif self.read_type in {ReadType.SPECTRUM.value, ReadType.ENDPOINT.value}:
            dimensions = [("int", "wavelength", "nm")]
        else:
            error = f"cannot make data cube for {self.read_type}"
            raise AllotropeConversionError(error)
        if self.has_wavelength_dimension:
            dimensions.append(("int", "wavelength", "nm"))
        return dimensions

    def generate_sample_document(self, well: str) -> SampleDocument:
        return SampleDocument(well_location_identifier=well, plate_barcode=self.name)

    def generate_data_cube(self, well_data: WellData) -> TDatacube:
        dimension_data = [well_data.dimensions] + (
            [well_data.wavelengths] if self.has_wavelength_dimension else []
        )
        return TDatacube(
            cube_structure=TDatacubeStructure(
                [
                    TDatacubeComponent(FieldComponentDatatype(data_type), concept, unit)
                    for data_type, concept, unit in self.get_data_cube_dimensions()
                ],
                [
                    TDatacubeComponent(
                        FieldComponentDatatype("double"), self.CONCEPT, self.UNIT
                    )
                ],
            ),
            data=TDatacubeData(
                dimension_data,  # type: ignore[arg-type]
                [well_data.values],
            ),
        )

    def generate_processed_data_aggreate_document(
        self, well_data: WellData
    ) -> ProcessedDataAggregateDocument:
        return ProcessedDataAggregateDocument(
            [
                ProcessedDataDocumentItem(
                    val, data_processing_description="processed data"
                )
                for val in well_data.processed_data
            ]
        )

    def to_allotrope(self) -> Any:
        raise NotImplementedError

src/allotropy/parsers/moldev_softmax_pro/plate_block.py
Benchling-Open-Source/allotropy
import io
from typing import Any

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.moldev_softmax_pro.block_factory import create_block
from allotropy.parsers.moldev_softmax_pro.plate_block import PlateBlock
from allotropy.parsers.vendor_parser import VendorParser

BLOCKS_LINE_PREFIX = "##BLOCKS= "
END_LINE_PREFIX = "~End"


class SoftmaxproParser(VendorParser):
    def _parse(self, contents: io.IOBase, filename: str) -> Any:  # noqa: ARG002
        file_contents = self._read_contents(contents)
        lines = file_contents.split("\n")

        # TODO parse whole file, not just blocks
        blocks_line = lines[0]
        if not blocks_line.startswith(BLOCKS_LINE_PREFIX):
            error = f"unrecognized start line {blocks_line}"
            raise AllotropeConversionError(error)

        num_blocks = int(blocks_line[len(BLOCKS_LINE_PREFIX) :])
        blocks = []
        current_block: list[str] = []
        for line in lines[1:]:
            if line.startswith(END_LINE_PREFIX):
                blocks.append(create_block(current_block))
                current_block = []
            elif current_block or line.strip():  # drop empty lines between blocks
                current_block.append(line)

        if len(blocks) != num_blocks:
            error = f"expected number of blocks to match {num_blocks}"
            raise AllotropeConversionError(error)

        plate_blocks = [block for block in blocks if isinstance(block, PlateBlock)]
        if len(plate_blocks) != 1:
            block_types = [block.BLOCK_TYPE for block in blocks]
            block_counts = {bt: block_types.count(bt) for bt in set(block_types)}
            error = f"expected exactly 1 plate block, got {block_counts}"
            raise AllotropeConversionError(error)
        return plate_blocks[0].to_allotrope()

src/allotropy/parsers/moldev_softmax_pro/softmax_pro_parser.py
Benchling-Open-Source/allotropy
from typing import Union

from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueCell,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueMicrometer,
    TNullableQuantityValueMillimeterOfMercury,
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueMillionCellsPerMilliliter,
    TNullableQuantityValueMilliOsmolesPerKilogram,
    TNullableQuantityValueOpticalDensity,
    TNullableQuantityValuePercent,
    TNullableQuantityValuePH,
    TNullableQuantityValueTODO,
    TNullableQuantityValueUnitPerLiter,
)

FILENAME_REGEX = r"^SampleResults(?P<device_identifier>.*)?(?P<processing_time>\d{4}-\d{2}-\d{2}_\d{6})\.csv$"

INVALID_FILENAME_MESSAGE = (
    "{} is not valid. File name is expected to have format of SampleResultsYYYY-MM-DD_HHMMSS.csv "
    "or SampleResults<Analyzer ID>YYYY-MM-DD_HHMMSS.csv where <Analyzer ID> is defined in Settings"
)

MOLAR_CONCENTRATION_CLASSES = Union[
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueUnitPerLiter,
]
ALL_MOLAR_CONCENTRATION_CLASSES: list[
    Union[
        type[TNullableQuantityValueMillimolePerLiter],
        type[TNullableQuantityValueGramPerLiter],
        type[TNullableQuantityValueUnitPerLiter],
    ]
] = [
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueUnitPerLiter,
]
MOLAR_CONCENTRATION_CLS_BY_UNIT = {
    cls.unit: cls for cls in ALL_MOLAR_CONCENTRATION_CLASSES
}

ANALYTE_MAPPINGS: dict[str, dict[str, str]] = {
    "NH4+": {
        "name": "ammonium",
        "unit": "mmol/L",
    },
    "HCO3": {
        "name": "bicarbonate",
        "unit": "mmol/L",
    },
    "Ca++": {
        "name": "calcium",
        "unit": "mmol/L",
    },
    "Gluc": {
        "name": "glucose",
        "unit": "g/L",
    },
    "Glu": {
        "name": "glutamate",
        "unit": "mmol/L",
    },
    "Gln": {
        "name": "glutamine",
        "unit": "mmol/L",
    },
    "Lac": {
        "name": "lactate",
        "unit": "g/L",
    },
    "K+": {
        "name": "potassium",
        "unit": "mmol/L",
    },
    "Na+": {
        "name": "sodium",
        "unit": "mmol/L",
    },
}

PROPERTY_MAPPINGS: dict[str, dict] = {
    "pco2": {
        "col_name": "PCO2",
        "cls": TNullableQuantityValueMillimeterOfMercury,
    },
    "co2_saturation": {
        "col_name": "CO2 Saturation",
        "cls": TNullableQuantityValuePercent,
    },
    "po2": {
        "col_name": "PO2",
        "cls": TNullableQuantityValueMillimeterOfMercury,
    },
    "o2_saturation": {
        "col_name": "O2 Saturation",
        "cls": TNullableQuantityValuePercent,
    },
    "optical_density": {
        "col_name": "Optical Density",
        "cls": TNullableQuantityValueOpticalDensity,
    },
    "pH": {
        "col_name": "pH",
        "cls": TNullableQuantityValuePH,
    },
    "osmolality": {
        "col_name": "Osm",
        "cls": TNullableQuantityValueMilliOsmolesPerKilogram,
    },
    "viability__cell_counter_": {
        "col_name": "Viability",
        "cls": TNullableQuantityValuePercent,
    },
    "total_cell_density__cell_counter_": {
        "col_name": "Total Density",
        "cls": TNullableQuantityValueMillionCellsPerMilliliter,
    },
    "viable_cell_density__cell_counter_": {
        "col_name": "Viable Density",
        "cls": TNullableQuantityValueMillionCellsPerMilliliter,
    },
    "average_live_cell_diameter__cell_counter_": {
        "col_name": "Average Live Cell Diameter",
        "cls": TNullableQuantityValueMicrometer,
    },
    "average_total_cell_diameter__cell_counter_": {
        "col_name": "Average Total Cell Diameter",
        "cls": TNullableQuantityValueMicrometer,
    },
    "total_cell_diameter_distribution__cell_counter_": {
        "col_name": "Total Cell Diameter Distribution",
        "cls": TNullableQuantityValueTODO,
    },
    "viable_cell_count__cell_counter_": {
        "col_name": "Total Live Count",
        "cls": TNullableQuantityValueCell,
    },
    "total_cell_count__cell_counter_": {
        "col_name": "Total Cell Count",
        "cls": TNullableQuantityValueCell,
    },
}

src/allotropy/parsers/novabio_flex2/constants.py
Benchling-Open-Source/allotropy
import io
import uuid

from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteAggregateDocument,
    AnalyteDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    SampleDocument,
)
from allotropy.parsers.novabio_flex2.novabio_flex2_structure import Data, Sample
from allotropy.parsers.vendor_parser import VendorParser


class NovaBioFlexParser(VendorParser):
    def _parse(self, contents: io.IOBase, filename: str) -> Model:
        return self._get_model(Data.create(contents, filename))

    def _get_model(self, data: Data) -> Model:
        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                data_processing_time=self.get_date_time(data.title.processing_time),
                analyst=data.sample_list.analyst,
                device_system_document=DeviceSystemDocument(
                    model_number="NovaBio Flex2",
                    device_identifier=data.title.device_identifier,
                ),
                measurement_document=self._get_measurement_document(data),
            ),
        )

    def _get_measurement_document(self, data: Data) -> list[MeasurementDocumentItem]:
        # get analytes and properties included in the output file
        return [
            self._get_measurement_from_sample(sample)
            for sample in data.sample_list.samples
        ]

    def _get_measurement_from_sample(self, sample: Sample) -> MeasurementDocumentItem:
        sample_measurement = MeasurementDocumentItem(
            sample_document=SampleDocument(
                sample_identifier=sample.identifier,
                sample_role_type=sample.role_type,
                batch_identifier=sample.batch_identifier,
            ),
            measurement_time=self.get_date_time(sample.measurement_time),
            analyte_aggregate_document=AnalyteAggregateDocument(
                analyte_document=[
                    AnalyteDocumentItem(
                        analyte_name=analyte.name,
                        molar_concentration=analyte.molar_concentration,
                    )
                    for analyte in sample.analytes
                ]
            ),
        )

        # properties are not included under the analyte aggregate document
        # and are added directly to the measurement document instead
        for name, property_ in sample.properties.items():
            setattr(sample_measurement, name, property_)

        return sample_measurement

src/allotropy/parsers/novabio_flex2/novabio_flex2_parser.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from dataclasses import dataclass
import io
import re
from typing import Any, Optional

import numpy as np
import pandas as pd

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.novabio_flex2.constants import (
    ANALYTE_MAPPINGS,
    FILENAME_REGEX,
    INVALID_FILENAME_MESSAGE,
    MOLAR_CONCENTRATION_CLASSES,
    MOLAR_CONCENTRATION_CLS_BY_UNIT,
    PROPERTY_MAPPINGS,
)


@dataclass
class Title:
    processing_time: str
    device_identifier: Optional[str]

    @staticmethod
    def create(filename: str) -> Title:
        matches = re.match(FILENAME_REGEX, filename)

        if not matches:
            raise AllotropeConversionError(
                filename, INVALID_FILENAME_MESSAGE.format(filename)
            )

        matches_dict = matches.groupdict()
        return Title(
            processing_time=matches_dict["processing_time"].replace("_", " "),
            device_identifier=matches_dict["device_identifier"] or None,
        )


@dataclass
class Analyte:
    name: str
    molar_concentration: MOLAR_CONCENTRATION_CLASSES

    @staticmethod
    def create(raw_name: str, value: float) -> Analyte:
        if raw_name not in ANALYTE_MAPPINGS:
            msg = "Invalid analyte name"
            raise AllotropeConversionError(msg)

        mapping = ANALYTE_MAPPINGS[raw_name]
        return Analyte(
            mapping["name"],
            MOLAR_CONCENTRATION_CLS_BY_UNIT[mapping["unit"]](value=value),
        )

    def __lt__(self, other: Any) -> bool:
        if not isinstance(other, Analyte):
            return NotImplemented

        return self.name < other.name


@dataclass
class Sample:
    identifier: str
    role_type: str
    measurement_time: str
    batch_identifier: Optional[str]
    analytes: list[Analyte]
    properties: dict[str, Any]

    @staticmethod
    def create(data: pd.Series) -> Sample:
        properties: dict[str, Any] = {
            property_name: property_dict["cls"](data[property_dict["col_name"]])
            for property_name, property_dict in PROPERTY_MAPPINGS.items()
            if property_dict["col_name"] in data
            and data[property_dict["col_name"]] is not None
        }

        batch_identifier = data.get("Batch ID")

        return Sample(
            identifier=data["Sample ID"],
            role_type=data["Sample Type"],
            measurement_time=data["Date & Time"].isoformat(),
            batch_identifier=str(batch_identifier) if batch_identifier else None,
            analytes=sorted(
                [
                    Analyte.create(raw_name, data[raw_name])
                    for raw_name in ANALYTE_MAPPINGS
                    if raw_name in data
                ]
            ),
            properties=properties,
        )


@dataclass
class SampleList:
    analyst: str
    samples: list[Sample]

    @staticmethod
    def create(data: pd.DataFrame) -> SampleList:
        sample_data_rows = [row for _, row in data.iterrows()]

        if not sample_data_rows:
            msg = "Unable to get any sample"
            raise AllotropeConversionError(msg)

        analyst = sample_data_rows[0].get("Operator")

        if analyst is None:
            msg = "Unable to get analyst from data"
            raise AllotropeConversionError(msg)

        return SampleList(
            str(analyst),
            [Sample.create(sample_data) for sample_data in sample_data_rows],
        )


@dataclass
class Data:
    title: Title
    sample_list: SampleList

    @staticmethod
    def create(contents: io.IOBase, filename: str) -> Data:
        # NOTE: type ignore is an issue with pandas typing, it accepts an io.IOBase that implements read.
        data = pd.read_csv(contents, parse_dates=["Date & Time"]).replace(np.nan, None)  # type: ignore[call-overload]
        return Data(Title.create(filename), SampleList.create(data))

src/allotropy/parsers/novabio_flex2/novabio_flex2_structure.py
Benchling-Open-Source/allotropy
from io import IOBase
from typing import Any, Optional, TypeVar
import uuid

from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    ContainerType,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
    TQuantityValueNumber,
)
from allotropy.allotrope.models.shared.definitions.definitions import TDateTimeValue
from allotropy.parsers.lines_reader import LinesReader
from allotropy.parsers.perkin_elmer_envision.elmer_envision_structure import Data, Plate
from allotropy.parsers.vendor_parser import VendorParser

T = TypeVar("T")


def safe_value(cls: type[T], value: Optional[Any]) -> Optional[T]:
    return None if value is None else cls(value=value)  # type: ignore[call-arg]


class ElmerEnvisionParser(VendorParser):
    def _parse(self, raw_contents: IOBase, _: str) -> Model:
        reader = LinesReader(raw_contents)
        return self._get_model(Data.create(reader))

    def _get_model(self, data: Data) -> Model:
        if not self._check_fluorescence(data):
            msg = "Elmer envision currently only accepts fluorescence data"
            raise NotImplementedError(msg)

        if data.number_of_wells is None:
            msg = "Unable to get number of the wells in the plate"
            raise Exception(msg)

        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                measurement_time=self._get_measurement_time(data),
                analytical_method_identifier=data.basic_assay_info.protocol_id,
                experimental_data_identifier=data.basic_assay_info.assay_id,
                container_type=ContainerType.well_plate,
                plate_well_count=TQuantityValueNumber(value=data.number_of_wells),
                device_system_document=DeviceSystemDocument(
                    model_number=data.instrument.serial_number,
                    device_identifier=data.instrument.nickname,
                ),
                measurement_document=self._get_measurement_document(data),
            )
        )

    def _check_fluorescence(self, data: Data) -> bool:
        absorbance_patterns = ["ABS", "Absorbance"]
        luminescence_patterns = ["LUM", "Luminescence"]

        for pattern in absorbance_patterns + luminescence_patterns:
            if pattern in data.labels.label:
                return False

        return True

    def _get_measurement_time(self, data: Data) -> TDateTimeValue:
        dates = [
            plate.plate_info.measurement_time
            for plate in data.plates
            if plate.plate_info.measurement_time
        ]

        if dates:
            return self.get_date_time(min(dates))

        msg = "Unable to find valid measurement date"
        raise Exception(msg)

    def _get_device_control_aggregate_document(
        self, data: Data, plate: Plate
    ) -> DeviceControlAggregateDocument:
        ex_filter = data.labels.excitation_filter
        em_filter = data.labels.get_emission_filter(plate.plate_info.emission_filter_id)
        return DeviceControlAggregateDocument(
            [
                DeviceControlDocumentItem(
                    device_type="fluorescence detector",
                    detector_distance_setting__plate_reader_=safe_value(
                        TQuantityValueMillimeter, plate.plate_info.measured_height
                    ),
                    number_of_averages=safe_value(
                        TQuantityValueNumber, data.labels.number_of_flashes
                    ),
                    detector_gain_setting=data.labels.detector_gain_setting,
                    scan_position_setting__plate_reader_=data.labels.scan_position_setting,
                    detector_wavelength_setting=safe_value(
                        TQuantityValueNanometer,
                        em_filter.wavelength if em_filter else None,
                    ),
                    detector_bandwidth_setting=safe_value(
                        TQuantityValueNanometer,
                        em_filter.bandwidth if em_filter else None,
                    ),
                    excitation_wavelength_setting=safe_value(
                        TQuantityValueNanometer,
                        ex_filter.wavelength if ex_filter else None,
                    ),
                    excitation_bandwidth_setting=safe_value(
                        TQuantityValueNanometer,
                        ex_filter.bandwidth if ex_filter else None,
                    ),
                )
            ]
        )

    def _get_measurement_document(self, data: Data) -> list[MeasurementDocumentItem]:
        items = []
        for plate in data.plates:
            if plate.results is None:
                continue

            try:
                p_map = data.plate_maps[plate.plate_info.number]
            except KeyError as e:
                msg = f"Unable to find plate map of {plate.plate_info.barcode}"
                raise Exception(msg) from e

            device_control_document = self._get_device_control_aggregate_document(
                data, plate
            )

            items += [
                MeasurementDocumentItem(
                    sample_document=SampleDocument(
                        plate_barcode=plate.plate_info.barcode,
                        well_location_identifier=f"{result.col}{result.row}",
                        sample_role_type=p_map.get_sample_role_type(
                            result.col, result.row
                        ),
                    ),
                    device_control_aggregate_document=device_control_document,
                    processed_data_aggregate_document=ProcessedDataAggregateDocument(
                        processed_data_document=[
                            ProcessedDataDocumentItem(
                                processed_data=result.value,
                                data_processing_description="processed data",
                            ),
                        ]
                    ),
                    compartment_temperature=safe_value(
                        TQuantityValueDegreeCelsius,
                        plate.plate_info.chamber_temperature_at_start,
                    ),
                )
                for result in plate.results
            ]
        return items

src/allotropy/parsers/perkin_elmer_envision/elmer_envision_parser.py
Benchling-Open-Source/allotropy
# plate (repeated for N plates measured)
#     Plate information
#     Background information (optional)
#     Calculated results (optional)
#     Measured results (repeated for N measurements performed per plate)
# Basic assay information
# Protocol information
# Plate type
# Platemap (repeated for N plates measured)
# Calculations
# Auto export parameters
# Operations
# Labels
#     Filters (dependent on detection modality)
#     Mirrors (dependent on detection modality)
# Instrument
from __future__ import annotations

from dataclasses import dataclass
import logging
from re import search
from typing import Optional

import numpy as np
import pandas as pd

from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    ScanPositionSettingPlateReader,
)
from allotropy.allotrope.models.shared.components.plate_reader import SampleRoleType
from allotropy.parsers.lines_reader import LinesReader
from allotropy.parsers.utils.values import assert_not_none, try_float


def df_to_series(df: pd.DataFrame) -> pd.Series:
    df.columns = df.iloc[0]  # type: ignore[assignment]
    return pd.Series(df.iloc[-1], index=df.columns)


def num_to_chars(n: int) -> str:
    d, m = divmod(n, 26)  # 26 is the number of ASCII letters
    return "" if n < 0 else num_to_chars(d - 1) + chr(m + 65)  # chr(65) = 'A'


@dataclass
class PlateInfo:
    number: str
    barcode: str
    emission_filter_id: str
    measurement_time: Optional[str]
    measured_height: Optional[float]
    chamber_temperature_at_start: Optional[float]

    @staticmethod
    def create(reader: LinesReader) -> Optional[PlateInfo]:
        data = reader.pop_csv_block("^Plate information")
        series = df_to_series(data).replace(np.nan, None)
        series.index = pd.Series(series.index).replace(np.nan, "empty label")  # type: ignore[assignment]

        plate_number = assert_not_none(
            str(series.get("Plate")), "Plate information: Plate"
        )
        barcode = (
            str(series.get("Barcode") or '=""').removeprefix('="').removesuffix('"')
            or f"Plate {plate_number}"
        )

        search_result = search("De=...", str(series.get("Measinfo", "")))
        if not search_result:
            msg = f"Unable to get emition filter id from plate {barcode}"
            raise Exception(msg)
        emission_filter_id = search_result.group().removeprefix("De=")

        measurement_time = str(series.get("Measurement date", ""))

        return PlateInfo(
            plate_number,
            barcode,
            emission_filter_id,
            measurement_time,
            try_float(str(series.get("Measured height"))),
            try_float(str(series.get("Chamber temperature at start"))),
        )


@dataclass
class Result:
    col: str
    row: str
    value: int

    @staticmethod
    def create(reader: LinesReader) -> list[Result]:
        if not reader.current_line_exists() or reader.match(
            "(^Plate information)|(^Basic assay information)"
        ):
            return []

        # Results may or may not have a title
        reader.pop_if_match("^Results")

        data = reader.pop_csv_block()
        series = (
            data.drop(0, axis=0).drop(0, axis=1) if data.iloc[1, 0] == "A" else data
        )
        rows, cols = series.shape
        series.index = [num_to_chars(i) for i in range(rows)]  # type: ignore[assignment]
        series.columns = [str(i).zfill(2) for i in range(1, cols + 1)]  # type: ignore[assignment]

        return [
            Result(col, row, int(series.loc[col, row]))
            for col, row in series.stack().index
        ]


@dataclass
class Plate:
    plate_info: PlateInfo
    results: list[Result]

    @staticmethod
    def create(reader: LinesReader) -> list[Plate]:
        plates: list[Plate] = []

        while True:
            if not reader.match("^Plate information"):
                break

            plate_info: Optional[PlateInfo] = None
            try:
                plate_info = PlateInfo.create(reader)
            except Exception as e:
                logging.warning(f"Failed to parse plate info with error: {e}")

            reader.drop_sections("^Background information|^Calculated results")

            results = Result.create(reader)

            if plate_info:
                plates.append(Plate(plate_info, results=results))

        return plates


@dataclass
class BasicAssayInfo:
    protocol_id: str
    assay_id: str

    @staticmethod
    def create(reader: LinesReader) -> BasicAssayInfo:
        reader.drop_until("^Basic assay information")
        data = reader.pop_csv_block("^Basic assay information").T
        data.iloc[0].replace(":.*", "", regex=True, inplace=True)
        series = df_to_series(data)
        return BasicAssayInfo(
            str(series.get("Protocol ID")),
            str(series.get("Assay ID")),
        )


@dataclass
class PlateType:
    number_of_wells: Optional[float] = None

    @staticmethod
    def create(reader: LinesReader) -> PlateType:
        reader.drop_until("^Plate type")
        data = reader.pop_csv_block("^Plate type").T
        return PlateType(
            try_float(str(df_to_series(data).get("Number of the wells in the plate")))
        )


def get_sample_role_type(encoding: str) -> SampleRoleType:
    # BL        blank               blank_role
    # CTL       control             control_sample_role
    # LB        lance_blank         blank_role
    # LC        lance_crosstalk     control_sample_role
    # LH        lance_high          control_sample_role
    # S         pl_sample           sample_role
    # STD       standard            standard_sample_role
    # -         undefined           <UNDEFINED>
    # UNK       unknown             unknown_sample_role
    # ZH        z_high              control_sample_role
    # ZL        z_low               control_sample_role
    sample_role_type_map = {
        "BL": SampleRoleType.blank_role,
        "CTL": SampleRoleType.control_sample_role,
        "LB": SampleRoleType.blank_role,
        "LC": SampleRoleType.control_sample_role,
        "LH": SampleRoleType.control_sample_role,
        "STD": SampleRoleType.standard_sample_role,
        "S": SampleRoleType.sample_role,
        "-": SampleRoleType.undefined_sample_role,
        "UNK": SampleRoleType.unknown_sample_role,
        "ZH": SampleRoleType.control_sample_role,
        "ZL": SampleRoleType.control_sample_role,
    }
    for pattern, value in sample_role_type_map.items():
        if encoding.startswith(pattern):
            return value

    msg = f"Unable to determine sample role type of plate map encoding {encoding}"
    raise ValueError(msg)


@dataclass
class PlateMap:
    plate_n: str
    group_n: str
    sample_role_type_mapping: dict[str, dict[str, SampleRoleType]]

    @staticmethod
    def create(reader: LinesReader) -> Optional[PlateMap]:
        if not reader.current_line_exists() or reader.match("^Calculations"):
            return None

        plate_n = assert_not_none(reader.pop(), "Platemap number").split(",")[-1]
        group_n = assert_not_none(reader.pop(), "Platemap group").split(",")[-1]

        data = reader.pop_csv_block().replace(" ", "", regex=True)

        reader.pop_data()  # drop type specification
        reader.drop_empty()

        series = (
            data.drop(0, axis=0).drop(0, axis=1) if data.iloc[1, 0] == "A" else data
        )
        rows, cols = series.shape
        series.index = [num_to_chars(i) for i in range(rows)]  # type: ignore[assignment]
        series.columns = [str(i).zfill(2) for i in range(1, cols + 1)]  # type: ignore[assignment]

        sample_role_type_mapping: dict[str, dict[str, SampleRoleType]] = {}
        for row, row_data in series.replace([np.nan, "''"], None).to_dict().items():
            col_mapping: dict[str, SampleRoleType] = {}
            for col, value in row_data.items():
                if value:
                    role_type = get_sample_role_type(str(value))
                    if role_type:
                        col_mapping[str(col)] = role_type
            if col_mapping:
                sample_role_type_mapping[str(row)] = col_mapping

        return PlateMap(plate_n, group_n, sample_role_type_mapping)

    def get_sample_role_type(self, col: str, row: str) -> SampleRoleType:
        try:
            return self.sample_role_type_mapping[row][col]
        except KeyError as e:
            msg = (
                f"Invalid plate map location for plate map {self.plate_n}: {col} {row}"
            )
            raise Exception(msg) from e


def create_plate_maps(reader: LinesReader) -> dict[str, PlateMap]:
    if reader.drop_until("^Platemap") is None:
        msg = "Unable to get plate map information"
        raise Exception(msg)

    reader.pop()  # remove title

    maps: dict[str, PlateMap] = {}
    while _map := PlateMap.create(reader):
        maps[_map.plate_n] = _map
    return maps


@dataclass
class Filter:
    name: str
    wavelength: float
    bandwidth: float

    @staticmethod
    def create(reader: LinesReader) -> Optional[Filter]:
        if not reader.current_line_exists() or reader.match(
            "(^Mirror modules)|(^Instrument:)"
        ):
            return None

        data = reader.pop_csv_block().T
        series = df_to_series(data)

        name = str(series.index[0])

        description = str(series.get("Description"))

        search_result = search("CWL=\\d*nm", description)
        if search_result is None:
            msg = f"Unable to find wavelength for filter {name}"
            raise Exception(msg)
        wavelength = float(
            search_result.group().removeprefix("CWL=").removesuffix("nm")
        )

        search_result = search("BW=\\d*nm", description)
        if search_result is None:
            msg = f"Unable to find bandwidth for filter {name}"
            raise Exception(msg)
        bandwidth = float(search_result.group().removeprefix("BW=").removesuffix("nm"))

        return Filter(name, wavelength, bandwidth)


def create_filters(reader: LinesReader) -> dict[str, Filter]:
    reader.drop_until("(^Filters:)|^Instrument:")

    if reader.match("^Instrument"):
        return {}

    reader.pop()  # remove title

    filters = {}
    while _filter := Filter.create(reader):
        filters[_filter.name] = _filter
    return filters


@dataclass
class Labels:
    label: str
    excitation_filter: Optional[Filter]
    emission_filters: dict[str, Optional[Filter]]
    scan_position_setting: Optional[ScanPositionSettingPlateReader] = None
    number_of_flashes: Optional[float] = None
    detector_gain_setting: Optional[str] = None

    @staticmethod
    def create(reader: LinesReader) -> Labels:
        reader.drop_until("^Labels")
        data = reader.pop_csv_block("^Labels").T
        series = df_to_series(data).replace(np.nan, None)

        filters = create_filters(reader)

        excitation_filter = filters.get(str(series.get("Exc. filter", "")))

        emission_filters = {
            "1st": filters.get(str(series.get("Ems. filter"))),
            "2nd": filters.get(str(series.get("2nd ems. filter"))),
        }
        filter_position_map = {
            "Bottom": ScanPositionSettingPlateReader.bottom_scan_position__plate_reader_,
            "Top": ScanPositionSettingPlateReader.top_scan_position__plate_reader_,
        }

        return Labels(
            series.index[0],
            excitation_filter,
            emission_filters,
            filter_position_map.get(str(series.get("Using of emission filter")), None),
            number_of_flashes=try_float(str(series.get("Number of flashes"))),
            detector_gain_setting=str(series.get("Reference AD gain")),
        )

    def get_emission_filter(self, id_val: str) -> Optional[Filter]:
        return self.emission_filters.get(id_val)


@dataclass
class Instrument:
    serial_number: str
    nickname: str

    @staticmethod
    def create(reader: LinesReader) -> Instrument:
        if reader.drop_until("^Instrument") is None:
            msg = "Unable to find instrument information"
            raise Exception(msg)

        reader.pop()  # remove title

        serial_number = assert_not_none(reader.pop(), "serial number").split(",")[-1]
        nickname = assert_not_none(reader.pop(), "nickname").split(",")[-1]

        return Instrument(serial_number, nickname)


@dataclass
class Data:
    plates: list[Plate]
    basic_assay_info: BasicAssayInfo
    number_of_wells: Optional[float]
    plate_maps: dict[str, PlateMap]
    labels: Labels
    instrument: Instrument

    @staticmethod
    def create(reader: LinesReader) -> Data:
        return Data(
            plates=Plate.create(reader),
            basic_assay_info=BasicAssayInfo.create(reader),
            number_of_wells=PlateType.create(reader).number_of_wells,
            plate_maps=create_plate_maps(reader),
            labels=Labels.create(reader),
            instrument=Instrument.create(reader),
        )

src/allotropy/parsers/perkin_elmer_envision/elmer_envision_structure.py
Benchling-Open-Source/allotropy
import io
import uuid

from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteAggregateDocument,
    AnalyteDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    SampleDocument,
)
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_reader import CedexBiohtReader
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_structure import Data, Sample
from allotropy.parsers.vendor_parser import VendorParser


class CedexBiohtParser(VendorParser):
    def _parse(self, contents: io.IOBase, filename: str) -> Model:  # noqa: ARG002
        reader = CedexBiohtReader(contents)
        return self._get_model(Data.create(reader))

    def _get_model(self, data: Data) -> Model:
        return Model(
            measurement_aggregate_document=MeasurementAggregateDocument(
                measurement_identifier=str(uuid.uuid4()),
                data_processing_time=self.get_date_time(
                    data.title.data_processing_time
                ),
                analyst=data.title.analyst,
                device_system_document=DeviceSystemDocument(
                    model_number=data.title.model_number,
                    device_serial_number=data.title.device_serial_number,
                ),
                measurement_document=self._get_measurement_document(data),
            )
        )

    def _get_measurement_document(self, data: Data) -> list[MeasurementDocumentItem]:
        measurement_document = []
        for sample in data.samples:
            measurement_document.extend(self._get_measurements_from_sample(sample))
        return measurement_document

    def _get_measurements_from_sample(
        self, sample: Sample
    ) -> list[MeasurementDocumentItem]:
        sample_measurements = [
            self._create_sample_measurement(sample)
            for _ in range(sample.analyte_list.num_measurement_docs)
        ]

        for (
            analyte_name,
            molar_concentrations,
        ) in sample.analyte_list.molar_concentration_dict.items():
            for sample_measurement, molar_concentration in zip(
                sample_measurements, molar_concentrations
            ):
                sample_measurement.analyte_aggregate_document.analyte_document.append(  # type: ignore[union-attr]
                    AnalyteDocumentItem(
                        analyte_name=analyte_name,
                        molar_concentration=molar_concentration,
                    )
                )

        for analyte_name, values in sample.analyte_list.non_aggregrable_dict.items():
            for sample_measurement, value in zip(sample_measurements, values):
                setattr(sample_measurement, analyte_name, value)

        return sample_measurements

    def _create_sample_measurement(self, sample: Sample) -> MeasurementDocumentItem:
        return MeasurementDocumentItem(
            sample_document=SampleDocument(
                sample_identifier=sample.name,
                sample_role_type=sample.role_type,
                batch_identifier=sample.batch,
            ),
            measurement_time=self.get_date_time(sample.measurement_time),
            analyte_aggregate_document=AnalyteAggregateDocument(analyte_document=[]),
        )

src/allotropy/parsers/roche_cedex_bioht/cedex_bioht_parser.py
Benchling-Open-Source/allotropy
import io

import numpy as np
import pandas as pd

from allotropy.parsers.roche_cedex_bioht.constants import (
    ANALYTES_LOOKUP,
    DATA_HEADER,
    INFO_HEADER,
    SAMPLE_ROLE_TYPES,
)


def to_num(data: pd.Series) -> pd.Series:
    return pd.to_numeric(data, errors="coerce").replace(np.nan, None)


class CedexBiohtReader:
    def __init__(self, contents: io.IOBase):
        self.title_data = self.read_title_data(contents)
        self.samples_data = self.read_samples_data(contents)

    def read_title_data(self, contents: io.IOBase) -> pd.Series:
        contents.seek(0)
        return pd.read_csv(  # type: ignore[call-overload, no-any-return]
            contents,
            delimiter="\t",
            usecols=INFO_HEADER,
            names=INFO_HEADER,
            nrows=1,
        ).T[0]

    def read_samples_data(self, contents: io.IOBase) -> pd.DataFrame:
        contents.seek(0)
        sample_rows: pd.DataFrame = pd.read_csv(  # type: ignore[call-overload]
            contents,
            delimiter="\t",
            usecols=DATA_HEADER,
            names=DATA_HEADER,
            skiprows=[0],
        )

        sample_rows = sample_rows.replace(
            {"analyte name": ANALYTES_LOOKUP, "sample role type": SAMPLE_ROLE_TYPES}
        )

        sample_rows["batch identifier"] = sample_rows["batch identifier"].fillna("")

        # concentration values under the test threshold will have a preceding < character
        # this will turn those values into NaN, which is the expected output
        sample_rows["concentration value"] = to_num(sample_rows["concentration value"])

        return sample_rows

src/allotropy/parsers/roche_cedex_bioht/cedex_bioht_reader.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from typing import Optional

import pandas as pd

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_reader import CedexBiohtReader
from allotropy.parsers.roche_cedex_bioht.constants import (
    MOLAR_CONCENTRATION_CLS_BY_UNIT,
    NON_AGGREGABLE_PROPERTIES,
)


@dataclass
class Title:
    data_processing_time: Optional[str]
    analyst: str
    model_number: Optional[str]
    device_serial_number: Optional[str]

    @staticmethod
    def create(title_data: pd.Series) -> Title:
        analyst = title_data.get("analyst")
        if analyst is None:
            msg = "Unable to obtain analyst from input data"
            raise AllotropeConversionError(msg)

        device_serial_number = title_data.get("device serial number")
        if device_serial_number is None:
            msg = "Unable to obtain device serial number from input data"
            raise AllotropeConversionError(msg)

        return Title(
            title_data.get("data processing time"),  # type: ignore[arg-type]
            analyst,  # type: ignore[arg-type]
            title_data.get("model number"),  # type: ignore[arg-type]
            str(device_serial_number),
        )


@dataclass
class Analyte:
    name: str
    concentration_value: Optional[float]
    unit: Optional[str]

    @staticmethod
    def create(data: pd.Series) -> Analyte:
        analyte_name: str = data.get("analyte name")  # type: ignore[assignment]
        concentration_value: Optional[float] = data.get("concentration value")  # type: ignore[assignment]
        unit: Optional[str] = data.get("concentration unit")  # type: ignore[assignment]

        return Analyte(analyte_name, concentration_value, unit)


@dataclass
class AnalyteList:
    analytes: list[Analyte]
    molar_concentration_dict: dict
    molar_concentration_nans: dict
    non_aggregrable_dict: dict
    non_aggregable_nans: dict
    num_measurement_docs: int

    @staticmethod
    def create(data: pd.DataFrame) -> AnalyteList:
        analytes = [Analyte.create(analyte_data) for _, analyte_data in data.iterrows()]
        molar_concentration_dict = defaultdict(list)
        molar_concentration_nans = {}
        non_aggregrable_dict = defaultdict(list)
        non_aggregable_nans = {}
        num_measurement_docs = 1

        for analyte in analytes:
            analyte_name = analyte.name
            concentration_value = analyte.concentration_value

            if analyte_cls := NON_AGGREGABLE_PROPERTIES.get(analyte_name):
                if concentration_value is None:
                    non_aggregable_nans[analyte_name] = analyte_cls(
                        value=concentration_value
                    )
                else:
                    non_aggregrable_dict[analyte_name].append(
                        analyte_cls(value=concentration_value)
                    )

                num_measurement_docs = max(
                    len(non_aggregrable_dict[analyte_name]), num_measurement_docs
                )
            else:
                unit = analyte.unit
                if unit is None:
                    continue

                molar_concentration_item_cls = MOLAR_CONCENTRATION_CLS_BY_UNIT.get(unit)
                if molar_concentration_item_cls is None:
                    continue

                molar_concentration_item = molar_concentration_item_cls(
                    value=concentration_value
                )
                if concentration_value is None:
                    molar_concentration_nans[analyte_name] = molar_concentration_item
                else:
                    molar_concentration_dict[analyte_name].append(
                        molar_concentration_item
                    )
                num_measurement_docs = max(
                    len(molar_concentration_dict[analyte_name]), num_measurement_docs
                )

        # Only include None values if there is not a valid value for that analyte
        for analyte_name in non_aggregable_nans:
            if len(non_aggregrable_dict[analyte_name]) == 0:
                non_aggregrable_dict[analyte_name].append(
                    non_aggregable_nans[analyte_name]
                )
                num_measurement_docs = max(
                    len(non_aggregrable_dict[analyte_name]), num_measurement_docs
                )

        for analyte_name in molar_concentration_nans:
            if len(molar_concentration_dict[analyte_name]) == 0:
                molar_concentration_dict[analyte_name].append(
                    molar_concentration_nans[analyte_name]
                )
                num_measurement_docs = max(
                    len(molar_concentration_dict[analyte_name]), num_measurement_docs
                )

        return AnalyteList(
            analytes,
            molar_concentration_dict,
            molar_concentration_nans,
            non_aggregrable_dict,
            non_aggregable_nans,
            num_measurement_docs,
        )


@dataclass
class Sample:
    name: str
    role_type: str
    measurement_time: str
    analyte_list: AnalyteList
    batch: Optional[str] = None

    @staticmethod
    def create(name: str, batch: Optional[str], samples_data: pd.DataFrame) -> Sample:
        condition = samples_data["sample identifier"] == name
        condition &= samples_data["batch identifier"] == batch
        sample_data = samples_data[condition].sort_values(by="analyte name")

        role_type = sample_data.iloc[0]["sample role type"]
        measurement_time = str(sample_data.iloc[0]["measurement time"])

        return Sample(
            name,
            role_type,
            measurement_time,
            AnalyteList.create(sample_data),
            batch=batch or None,
        )


@dataclass
class Data:
    title: Title
    samples: list[Sample]

    @staticmethod
    def create(reader: CedexBiohtReader) -> Data:
        # A sample group is defined by both the sample and the batch identifier
        sample_groups = reader.samples_data.groupby(
            ["sample identifier", "batch identifier"]
        ).groups.keys()

        return Data(
            title=Title.create(reader.title_data),
            samples=[Sample.create(name, batch, reader.samples_data) for name, batch in sample_groups],  # type: ignore[has-type, misc]
        )

src/allotropy/parsers/roche_cedex_bioht/cedex_bioht_structure.py
Benchling-Open-Source/allotropy
from typing import Union

from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueCell,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueMicrometer,
    TNullableQuantityValueMillimeterOfMercury,
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueMillionCellsPerMilliliter,
    TNullableQuantityValueMilliOsmolesPerKilogram,
    TNullableQuantityValueOpticalDensity,
    TNullableQuantityValuePercent,
    TNullableQuantityValuePH,
    TNullableQuantityValueTODO,
    TNullableQuantityValueUnitPerLiter,
)

MOLAR_CONCENTRATION_CLASSES: list[
    Union[
        type[TNullableQuantityValueMillimolePerLiter],
        type[TNullableQuantityValueGramPerLiter],
        type[TNullableQuantityValueUnitPerLiter],
    ]
] = [
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueUnitPerLiter,
]

MOLAR_CONCENTRATION_CLS_BY_UNIT = {cls.unit: cls for cls in MOLAR_CONCENTRATION_CLASSES}

NON_AGGREGABLE_PROPERTIES = {
    "pco2": TNullableQuantityValueMillimeterOfMercury,
    "co2_saturation": TNullableQuantityValuePercent,
    "po2": TNullableQuantityValueMillimeterOfMercury,
    "o2_saturation": TNullableQuantityValuePercent,
    "optical_density": TNullableQuantityValueOpticalDensity,
    "pH": TNullableQuantityValuePH,
    "osmolality": TNullableQuantityValueMilliOsmolesPerKilogram,
    "viability__cell_counter_": TNullableQuantityValuePercent,
    "total_cell_density__cell_counter_": TNullableQuantityValueMillionCellsPerMilliliter,
    "viable_cell_density__cell_counter_": TNullableQuantityValueMillionCellsPerMilliliter,
    "average_live_cell_diameter__cell_counter_": TNullableQuantityValueMicrometer,
    "average_total_cell_diameter__cell_counter_": TNullableQuantityValueMicrometer,
    "total_cell_diameter_distribution__cell_counter_": TNullableQuantityValueTODO,
    "viable_cell_count__cell_counter_": TNullableQuantityValueCell,
    "total_cell_count__cell_counter_": TNullableQuantityValueCell,
}


INFO_HEADER = [
    "row type",
    "data processing time",
    "col3",
    "col4",
    "col5",
    "col6",
    "model number",
    "device serial number",
    "col9",
    "analyst",
]

DATA_HEADER = [
    "row type",
    "measurement time",
    "sample identifier",
    "batch identifier",
    "sample role type",
    "col6",
    "analyte name",
    "col8",
    "concentration unit",
    "flag",
    "concentration value",
    "col12",
    "col13",
]


ANALYTES_LOOKUP = {
    "AC2B": "acetate",
    "AC2D": "acetate",
    "AQB": "alanine-glutamine",
    "AQD": "alanine-glutamine",
    "AQQB": "alanine-glutamine corrected",
    "NH3LB": "ammonia",
    "NH3B": "ammonia",
    "NH3D": "ammonia",
    "ARAB": "arabinose",
    "ARAD": "arabinose",
    "ASNLB": "asparagine",
    "ASNHB": "asparagine",
    "ASNHD": "asparagine",
    "ASPB": "aspartate",
    "ASPD": "aspartate",
    "CA2B": "calcium",
    "CA2D": "calcium",
    "CHO2B": "cholesterol",
    "CHO2D": "cholesterol",
    "ETOHB": "ethanol",
    "ETOHD": "ethanol",
    "FORB": "formate",
    "FORD": "formate",
    "GAL2B": "galactose",
    "GAL2D": "galactose",
    "GLC2L": "glucose",
    "GLC2B": "glucose",
    "GLC2D": "glucose",
    "GLC3D": "glucose",
    "GLC3B": "glucose",
    "GLC3L": "glucose",
    "GLU2B": "glutamate",
    "GLU2D": "glutamate",
    "GLN2B": "glutamine",
    "GLN2D": "glutamine",
    "GLYB": "glycerol",
    "GLYD": "glycerol",
    "GLYE": "glycerol",
    "FABLA": "Ig Fab human",
    "FABLB": "Ig Fab human",
    "FABHB": "Ig Fab human",
    "FABHD": "Ig Fab human",
    "IGGLB": "IgG human",
    "IGGHB": "IgG human",
    "IGGHD": "IgG human",
    "MIGLB": "IgG mouse",
    "MIGHB": "IgG mouse",
    "MIGHD": "IgG mouse",
    "FE2B": "iron",
    "FE2D": "iron",
    "LAC2B": "lactate",
    "LAC2D": "lactate",
    "LDH2B": "ldh",
    "LDH2D": "ldh",
    "MG2L": "magnesium",
    "MG2B": "magnesium",
    "MG2D": "magnesium",
    "NO3L": "nitrate",
    "NO3B": "nitrate",
    "NO3D": "nitrate",
    "PHO2L": "phosphate",
    "PHO2B": "phosphate",
    "PHO2D": "phosphate",
    "KB": "potassium",
    "KD": "potassium",
    "PYRB": "pyruvate",
    "PYRD": "pyruvate",
    "NAB": "sodium",
    "NAD": "sodium",
    "SUCB": "sucrose",
    "SUCD": "sucrose",
    "SUGLB": "sucrose corrected",
    "SUGLD": "sucrose corrected",
    "TP2B": "total protein",
    "TP2D": "total protein",
    "TP2LB": "total protein",
    "ODB": "optical_density",
    "ODD": "optical_density",
    "OSM2B": "osmolality",
}

SAMPLE_ROLE_TYPES = {"SAM": "Sample"}

src/allotropy/parsers/roche_cedex_bioht/constants.py
Benchling-Open-Source/allotropy
from datetime import timedelta, timezone
from typing import Optional

from dateutil import parser
import pytz

from allotropy.allotrope.allotrope import AllotropeConversionError

TIMEZONE_CODES_MAP = {
    **{code: pytz.timezone(code) for code in pytz.all_timezones},
    # Add daylight savings time codes for USA
    **{
        "EDT": timezone(timedelta(hours=-4), "EDT"),
        "CDT": timezone(timedelta(hours=-5), "CDT"),
        "MDT": timezone(timedelta(hours=-6), "MDT"),
        "PDT": timezone(timedelta(hours=-7), "PDT"),
    },
}


class TimestampParser:
    def __init__(self, default_timezone: Optional[timezone] = None):
        if default_timezone and not isinstance(default_timezone, timezone):
            msg = f"Invalid default timezone '{default_timezone}'"
            raise AllotropeConversionError(msg)
        self.default_timezone = default_timezone or timezone.utc

    def parse(self, time: Optional[str]) -> Optional[str]:
        if not time:
            return None
        try:
            timestamp = parser.parse(time, tzinfos=TIMEZONE_CODES_MAP)
        except ValueError:
            return None
        if not timestamp.tzinfo:
            timestamp = timestamp.replace(tzinfo=self.default_timezone)
        return timestamp.isoformat()

src/allotropy/parsers/utils/timestamp_parser.py
Benchling-Open-Source/allotropy
import re
from typing import Optional, TypeVar, Union

from allotropy.allotrope.allotrope import AllotropeConversionError

PrimitiveValue = Union[str, int, float]


def try_int(value: Optional[str]) -> Optional[int]:
    try:
        return int(value or "")
    except ValueError:
        return None


def try_float(value: Optional[str]) -> Optional[float]:
    try:
        return float(value or "")
    except ValueError:
        return None


def natural_sort_key(key: str) -> list[str]:
    """Returns a sort key that treats numeric substrings as parsed integers for comparison."""
    tokens = [token for token in re.split(r"(\d+)", key) if token]
    return [
        f"{int(token):>10}" if token.isdecimal() else token.lower() for token in tokens
    ]


T = TypeVar("T")


def assert_not_none(
    value: Optional[T], name: Optional[str] = None, msg: Optional[str] = None
) -> T:
    if value is None:
        error = msg or f"Expected non-null value{f' for {name}' if name else ''}"
        raise AllotropeConversionError(error)
    return value


def value_or_none(value: str) -> Optional[str]:
    return value.strip() or None

src/allotropy/parsers/utils/values.py
Benchling-Open-Source/allotropy
from abc import ABC, abstractmethod
import io
from typing import Any, Optional

import chardet

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.shared.definitions.definitions import TDateTimeValue
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from allotropy.parsers.utils.values import assert_not_none


class VendorParser(ABC):
    def __init__(self, timestamp_parser: TimestampParser):
        self.timestamp_parser = timestamp_parser

    @abstractmethod
    def _parse(self, contents: io.IOBase, filename: str) -> Any:
        raise NotImplementedError

    def to_allotrope(self, contents: io.IOBase, filename: str) -> Any:
        return self._parse(contents, filename)

    def _read_contents(self, contents: io.IOBase) -> Any:
        file_bytes = contents.read()
        encoding = chardet.detect(file_bytes)["encoding"]
        if not encoding:
            error = "Did not detect encoding in input file"
            raise AllotropeConversionError(error)
        return file_bytes.decode(encoding)

    def parse_timestamp(self, time: Optional[str]) -> Optional[str]:
        return self.timestamp_parser.parse(time)

    def get_date_time(self, time: Any) -> TDateTimeValue:
        return assert_not_none(
            self.parse_timestamp(str(time)), msg=f"Invalid timestamp: '{time}'"
        )

src/allotropy/parsers/vendor_parser.py
Benchling-Open-Source/allotropy
from datetime import timezone
import io
from pathlib import Path
from typing import Any, Optional

from allotropy.allotrope.allotrope import serialize_allotrope
from allotropy.parser_factory import PARSER_FACTORY, VendorType


def allotrope_from_io(
    contents: io.IOBase,
    filename: str,
    vendor_type: VendorType,
    default_timezone: Optional[timezone] = None,
) -> dict[str, Any]:
    parser = PARSER_FACTORY.create(vendor_type, default_timezone=default_timezone)
    return serialize_allotrope(parser.to_allotrope(contents, filename))


def allotrope_from_file(
    filepath: str,
    vendor_type: VendorType,
    default_timezone: Optional[timezone] = None,
) -> dict[str, Any]:
    with open(filepath, "rb") as f:
        return allotrope_from_io(
            f, Path(filepath).name, vendor_type, default_timezone=default_timezone
        )

src/allotropy/to_allotrope.py
Benchling-Open-Source/allotropy
# SPDX-FileCopyrightText: 2023-present Brian Worth <brian@benchling.com>
#
# SPDX-License-Identifier: MIT

tests/__init__.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.allotrope import serialize_allotrope
from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteDocumentItem,
)
from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    DeviceControlAggregateDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
)
from allotropy.allotrope.models.shared.components.plate_reader import SampleDocument
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueMillimolePerLiter,
    TQuantityValueNumber,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacube,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)


def test_basic() -> None:
    model = Model()
    model.measurement_aggregate_document = MeasurementAggregateDocument(
        measurement_identifier="blah",
        plate_well_count=TQuantityValueNumber(1.0),
        measurement_document=[
            MeasurementDocumentItem(
                DeviceControlAggregateDocument(),
                SampleDocument(
                    well_location_identifier="well1", sample_identifier="sample1"
                ),
            )
        ],
    )
    assert serialize_allotrope(model) == {
        "$asm.manifest": "http://purl.allotrope.org/manifests/fluorescence/BENCHLING/2023/09/fluorescence.manifest",
        "measurement aggregate document": {
            "measurement identifier": "blah",
            "plate well count": {
                "value": 1.0,
                "unit": "#",
            },
            "measurement document": [
                {
                    "device control aggregate document": {},
                    "sample document": {
                        "well location identifier": "well1",
                        "sample identifier": "sample1",
                    },
                }
            ],
        },
    }


def test_data_cube() -> None:
    data_cube = TDatacube(
        cube_structure=TDatacubeStructure(
            [
                TDatacubeComponent(
                    FieldComponentDatatype("double"), "elapsed time", "s"
                ),
                TDatacubeComponent(FieldComponentDatatype("int"), "wavelength", None),
            ],
            [
                TDatacubeComponent(
                    FieldComponentDatatype("double"), "fluorescence", "RFU"
                )
            ],
        ),
        data=TDatacubeData(
            [[1.1, 2.2, 3.3], [1.0, 2.0, 3.0]],
            [[4.0, 5.0, None]],
        ),
    )
    assert serialize_allotrope(data_cube) == {
        "cube-structure": {
            "dimensions": [
                {
                    "@componentDatatype": "double",
                    "concept": "elapsed time",
                    "unit": "s",
                },
                {"@componentDatatype": "int", "concept": "wavelength"},
            ],
            "measures": [
                {
                    "@componentDatatype": "double",
                    "concept": "fluorescence",
                    "unit": "RFU",
                }
            ],
        },
        "data": {
            "dimensions": [[1.1, 2.2, 3.3], [1.0, 2.0, 3.0]],
            "measures": [[4.0, 5.0, None]],
        },
    }


def test_omits_null_values_except_for_specified_classes() -> None:
    item = AnalyteDocumentItem(
        "test", TNullableQuantityValueMillimolePerLiter(value=None)
    )

    assert serialize_allotrope(item) == {
        "analyte name": "test",
        "molar concentration": {"unit": "mmol/L", "value": None},
    }

tests/allotrope/allotrope_test.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.allotrope.models.shared.definitions.definitions import TDatacubeData


def test_data_cube_data() -> None:
    cube = TDatacubeData(
        dimensions=[["A", "B"]],
        measures=[[1.0, None]],
    )
    assert cube.dimensions == [["A", "B"]]
    assert cube.measures == [[1.0, None]]

    cube = TDatacubeData(
        dimensions=[[1.0, 2.0]],
        points=[[True, False]],
    )
    assert cube.dimensions == [[1.0, 2.0]]
    assert cube.points == [[True, False]]


def test_data_cube_data_oneof_post_init() -> None:
    with pytest.raises(
        ValueError, match="Exactly one of measures or points must be set"
    ):
        TDatacubeData(dimensions=[["A", "B"]])

    with pytest.raises(
        ValueError, match="Exactly one of measures or points must be set"
    ):
        TDatacubeData(dimensions=[["A", "B"]], measures=[], points=[])


def test_data_cube_data_oneof_post_init_empty_list_ok() -> None:
    cube = TDatacubeData(dimensions=[["A", "B"]], measures=[])
    assert cube.measures == []
    assert cube.points is None

    cube = TDatacubeData(dimensions=[["A", "B"]], points=[])
    assert cube.measures is None
    assert cube.points == []

tests/allotrope/models/shared/definitions_test.py
Benchling-Open-Source/allotropy
from pathlib import Path

from allotropy.allotrope.schema_parser.generate_schemas import generate_schemas


def test_generate_schemas_runs_to_completion() -> None:
    root_dir = Path(__file__).parent.parent.parent.parent
    assert generate_schemas(root_dir) == 8

tests/allotrope/schema_parser/generate_schemas_test.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.schema_parser.model_class_editor import (
    get_manifest_from_schema_path,
    ModelClassEditor,
)


def test_get_manifest_from_schema_path() -> None:
    schema_path = "src/allotropy/allotrope/schemas/cell_counter/BENCHLING/2023/09/cell-counter.json"
    expected = "http://purl.allotrope.org/manifests/cell_counter/BENCHLING/2023/09/cell-counter.manifest"
    assert get_manifest_from_schema_path(schema_path) == expected


def test_modify_file() -> None:
    classes_to_skip = {"ClassA"}
    imports_to_add = {"definitions": {"Thing1", "Thing2"}}
    editor = ModelClassEditor("fake_manifest", classes_to_skip, imports_to_add)

    model_file_contents = """
import json
from typing import Union


@dataclass
class ClassA:
    key: str
    value: str


@dataclass
class ClassB:
    key: str
    value: str


@dataclass
class UnusedClass:
    value: int


@dataclass
class Model:
    key: str
    value: str
    b_thing: ClassB
"""

    expected = """
import json
from typing import Union

from allotropy.allotrope.models.shared.definitions import Thing1, Thing2


@dataclass
class ClassB:
    key: str
    value: str


@dataclass
class Model:
    key: str
    value: str
    b_thing: ClassB
    manifest: str = \"fake_manifest\"
"""
    assert editor.modify_file(model_file_contents) == expected

tests/allotrope/schema_parser/model_class_editor_test.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.schema_parser.schema_model import (
    get_all_schema_components,
    snake_to_upper_camel,
)


def test_snake_to_upper_camel() -> None:
    assert "SampleDocument" == snake_to_upper_camel("sample document", " ")


def test_get_all_schema_components() -> None:
    schema = {
        "type": "object",
        "properties": {
            "prop1": {"type": "string"},
            "nested_prop": {
                "type": "object",
                "properties": {
                    "nested_prop2": {"type": "string"},
                },
            },
        },
    }

    assert {
        "prop1": {"type": "string"},
        "nested_prop": {
            "type": "object",
            "properties": {
                "nested_prop2": {"type": "string"},
            },
        },
        "nested_prop2": {"type": "string"},
    } == get_all_schema_components(schema)


def test_get_all_schema_components_list() -> None:
    schema = {
        "type": "array",
        "items": {"type": "object", "properties": {"prop1": {"type": "string"}}},
    }

    assert {"prop1": {"type": "string"}} == get_all_schema_components(schema)


def test_get_all_schema_components_all_of() -> None:
    schema = {
        "type": "array",
        "items": {
            "allOf": [
                {
                    "type": "object",
                    "properties": {
                        "allOf prop1": {
                            "type": "object",
                            "properties": {"value": {"type": "number"}},
                        },
                        "allOf prop2": {"type": "boolean"},
                    },
                },
                {
                    "properties": {
                        "allOf prop3": {"type": "string"},
                    }
                },
            ]
        },
        "minItems": 0,
    }

    assert {
        "allOf prop1": {
            "type": "object",
            "properties": {"value": {"type": "number"}},
        },
        "value": {"type": "number"},
        "allOf prop2": {"type": "boolean"},
        "allOf prop3": {"type": "string"},
    } == get_all_schema_components(schema)


def test_get_all_schema_components_one_of() -> None:
    schema = {
        "type": "object",
        "properties": {
            "oneOf prop": {
                "oneOf": [
                    {"type": "string"},
                    {
                        "type": "object",
                        "properties": {
                            "value": {"type": "string"},
                        },
                    },
                    {
                        "type": "object",
                        "properties": {
                            "other": {"type": "string"},
                        },
                    },
                ],
            },
        },
    }

    assert {
        "oneOf prop item": {
            "type": "object",
            "properties": {
                "value": {"type": "string"},
            },
        },
        "value": {"type": "string"},
        "oneOf prop item1": {
            "type": "object",
            "properties": {
                "other": {"type": "string"},
            },
        },
        "other": {"type": "string"},
    } == get_all_schema_components(schema)

tests/allotrope/schema_parser/schema_model_test.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parser_factory import Vendor
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

VENDOR_TYPE = Vendor.AGILENT_GEN5


@pytest.mark.parametrize(
    "filename",
    [
        "endpoint_pathlength_correct_singleplate",
        "endpoint_stdcurve_singleplate",
        "endpoint_stdcurve_singleplate_2",
        "endpoint_stdcurve_multiplate",
        "kinetic_helper_gene_growth_curve",
        "kinetic_singleplate",
        "kinetic_multiplate",
    ],
)
def test_to_allotrope_absorbance(filename: str) -> None:
    test_filepath = f"tests/parsers/agilent_gen5/testdata/absorbance/{filename}.txt"
    expected_filepath = (
        f"tests/parsers/agilent_gen5/testdata/absorbance/{filename}.json"
    )
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(
        allotrope_dict,
        "ultraviolet-absorbance/BENCHLING/2023/09/ultraviolet-absorbance.json",
    )
    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.parametrize(
    "filename",
    [
        "endpoint_singleplate",
        "endpoint_multiplate",
    ],
)
def test_to_allotrope_fluorescence(filename: str) -> None:
    test_filepath = f"tests/parsers/agilent_gen5/testdata/fluorescence/{filename}.txt"
    expected_filepath = (
        f"tests/parsers/agilent_gen5/testdata/fluorescence/{filename}.json"
    )
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, "fluorescence/BENCHLING/2023/09/fluorescence.json")
    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.parametrize(
    "filename",
    [
        "endpoint_singleplate",
        "endpoint_multiplate",
    ],
)
def test_to_allotrope_luminescence(filename: str) -> None:
    test_filepath = f"tests/parsers/agilent_gen5/testdata/luminescence/{filename}.txt"
    expected_filepath = (
        f"tests/parsers/agilent_gen5/testdata/luminescence/{filename}.json"
    )
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, "luminescence/BENCHLING/2023/09/luminescence.json")
    validate_contents(allotrope_dict, expected_filepath)


def test_to_allotrope_invalid_plate_data() -> None:
    with pytest.raises(AllotropeConversionError, match="No plate data found in file"):
        from_file("tests/parsers/agilent_gen5/testdata/garbage.txt", VENDOR_TYPE)

tests/parsers/agilent_gen5/to_allotrope_test.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.parser_factory import Vendor
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "Appbio_AbsoluteQ_example01.csv",
    "Appbio_AbsoluteQ_example02.csv",
    "Appbio_AbsoluteQ_example03.csv",
    "Appbio_AbsoluteQ_example04.csv",
    "Appbio_AbsoluteQ_example05.csv",
)

VENDOR_TYPE = Vendor.APPBIO_ABSOLUTE_Q


@pytest.mark.parametrize("output_file", output_files)
def test_parse_appbio_absolute_q_to_asm_schema(output_file: str) -> None:
    test_filepath = f"tests/parsers/appbio_absolute_q/testdata/{output_file}"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, "pcr/BENCHLING/2023/09/dpcr.json")


@pytest.mark.parametrize("output_file", output_files)
def test_parse_appbio_absolute_q_to_asm_contents(output_file: str) -> None:
    test_filepath = f"tests/parsers/appbio_absolute_q/testdata/{output_file}"
    expected_filepath = test_filepath.replace(".csv", ".json")
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)

    validate_contents(allotrope_dict, expected_filepath)

tests/parsers/appbio_absolute_q/appbio_absolute_q_parser_test.py
Benchling-Open-Source/allotropy
from __future__ import annotations

from io import BytesIO
from typing import Optional

import pandas as pd
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import ExperimentType
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_builders import (
    DataBuilder,
    HeaderBuilder,
    ResultsBuilder,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import (
    Data,
    Header,
    Result,
    WellItem,
)
from allotropy.parsers.appbio_quantstudio.calculated_document import CalculatedDocument
from allotropy.parsers.appbio_quantstudio.referenceable import Referenceable
from allotropy.parsers.lines_reader import LinesReader
from tests.parsers.appbio_quantstudio.appbio_quantstudio_data import (
    get_data,
    get_data2,
    get_genotyping_data,
    get_rel_std_curve_data,
)


def rm_uuid(data: Data) -> Data:
    for well in data.wells:
        if well.calculated_document:
            rm_uuid_calc_doc(well.calculated_document)

        for well_item in well.items.values():
            well_item.uuid = ""

    for calc_doc in data.calculated_documents:
        rm_uuid_calc_doc(calc_doc)

    return data


def rm_uuid_calc_doc(calc_doc: CalculatedDocument) -> None:
    calc_doc.uuid = ""
    for source in calc_doc.data_sources:
        if isinstance(source.reference, CalculatedDocument):
            rm_uuid_calc_doc(source.reference)
        elif isinstance(source.reference, Referenceable):
            source.reference.uuid = ""


@pytest.mark.short
def test_header_builder_returns_header_instance() -> None:
    header_contents = get_raw_header_contents()

    assert isinstance(HeaderBuilder.build(LinesReader(header_contents)), Header)


def test_header_builder() -> None:
    device_identifier = "device1"
    model_number = "123"
    device_serial_number = "1234"
    measurement_method_identifier = "measurement ID"
    pcr_detection_chemistry = "detection1"
    passive_reference_dye_setting = "blue"
    experimental_data_identifier = "data Identifier"

    header_contents = get_raw_header_contents(
        measurement_time="2010-10-01 01:44:54 AM EDT",
        plate_well_count="96 plates",
        experiment_type="Genotyping",
        device_identifier=device_identifier,
        model_number=model_number,
        device_serial_number=device_serial_number,
        measurement_method_identifier=measurement_method_identifier,
        pcr_detection_chemistry=pcr_detection_chemistry,
        passive_reference_dye_setting=passive_reference_dye_setting,
        experimental_data_identifier=experimental_data_identifier,
    )

    assert HeaderBuilder.build(LinesReader(header_contents)) == Header(
        measurement_time="2010-10-01 01:44:54 AM EDT",
        plate_well_count=96,
        experiment_type=ExperimentType.genotyping_qPCR_experiment,
        device_identifier=device_identifier,
        model_number=model_number,
        device_serial_number=device_serial_number,
        measurement_method_identifier=measurement_method_identifier,
        pcr_detection_chemistry=pcr_detection_chemistry,
        passive_reference_dye_setting=passive_reference_dye_setting,
        barcode=None,
        analyst=None,
        experimental_data_identifier=experimental_data_identifier,
    )


@pytest.mark.parametrize(
    "parameter,expected_error",
    [
        ("device_identifier", "Unable to get device identifier"),
        ("model_number", "Unable to get model number"),
        ("device_serial_number", "Unable to get device serial number"),
        (
            "measurement_method_identifier",
            "Unable to get measurement method identifier",
        ),
        ("pcr_detection_chemistry", "Unable to get PCR detection chemistry"),
        ("plate_well_count", "Unable to interpret plate well count"),
    ],
)
@pytest.mark.short
def test_header_builder_required_parameter_none_then_raise(
    parameter: str, expected_error: str
) -> None:
    header_contents = get_raw_header_contents(**{parameter: None})

    with pytest.raises(AllotropeConversionError, match=expected_error):
        HeaderBuilder.build(LinesReader(header_contents))


@pytest.mark.short
def test_header_builder_invalid_plate_well_count() -> None:
    header_contents = get_raw_header_contents(plate_well_count="0 plates")

    with pytest.raises(AllotropeConversionError):
        HeaderBuilder.build(LinesReader(header_contents))


@pytest.mark.short
def test_header_builder_no_header_then_raise() -> None:
    header_contents = get_raw_header_contents(raw_text="")

    with pytest.raises(AllotropeConversionError):
        HeaderBuilder.build(LinesReader(header_contents))


@pytest.mark.short
def test_results_builder() -> None:

    data = pd.DataFrame(
        {
            "Well": [1],
            "SNP Assay Name": ["CYP19_2"],
            "Allele1 Automatic Ct Threshold": [True],
            "Allele1 Ct Threshold": [0.219],
            "Allele2 Ct Threshold": [9999],
            "Allele1 Ct": ["Undetermined"],
        }
    )
    well_item = WellItem(
        uuid="be566c58-41f3-40f8-900b-ef1dff20d264",
        identifier=1,
        position="A1",
        target_dna_description="CYP19_2-Allele 1",
        sample_identifier="NTC",
        well_location_identifier="A1",
        reporter_dye_setting="SYBR",
        quencher_dye_setting=None,
        sample_role_type="PC_ALLELE_1",
    )
    result = ResultsBuilder.build(
        data, well_item, ExperimentType.genotyping_qPCR_experiment
    )
    assert isinstance(result, Result)
    assert result.cycle_threshold_value_setting == 0.219
    assert result.cycle_threshold_result is None
    assert result.automatic_cycle_threshold_enabled_setting is True


@pytest.mark.short
@pytest.mark.parametrize(
    "test_filepath,expected_data",
    [
        (
            "tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_test01.txt",
            get_data(),
        ),
        (
            "tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_test02.txt",
            get_data2(),
        ),
        (
            "tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_test03.txt",
            get_genotyping_data(),
        ),
        (
            "tests/parsers/appbio_quantstudio/testdata/appbio_quantstudio_test04.txt",
            get_rel_std_curve_data(),
        ),
    ],
)
def test_data_builder(test_filepath: str, expected_data: Data) -> None:
    with open(test_filepath, "rb") as raw_contents:
        reader = LinesReader(raw_contents)
    assert rm_uuid(DataBuilder.build(reader)) == rm_uuid(expected_data)


def get_raw_header_contents(
    raw_text: Optional[str] = None,
    measurement_time: Optional[str] = "2010-10-01 01:44:54 AM EDT",
    plate_well_count: Optional[str] = "96-Well Block (0.2mL)",
    experiment_type: Optional[str] = "Presence/Absence",
    device_identifier: Optional[str] = "278880034",
    model_number: Optional[str] = "QuantStudio(TM) 6 Flex System",
    device_serial_number: Optional[str] = "278880034",
    measurement_method_identifier: Optional[str] = "Ct",
    pcr_detection_chemistry: Optional[str] = "TAQMAN",
    passive_reference_dye_setting: Optional[str] = "ROX",
    barcode: Optional[str] = "NA",
    analyst: Optional[str] = "NA",
    experimental_data_identifier: Optional[
        str
    ] = "QuantStudio 96-Well Presence-Absence Example",
) -> BytesIO:
    if raw_text is not None:
        return BytesIO(raw_text.encode("utf-8"))

    header_dict = {
        "Experiment Run End Time": measurement_time,
        "Block Type": plate_well_count,
        "Experiment Type ": experiment_type,
        "Instrument Name": device_identifier,
        "Instrument Type": model_number,
        "Instrument Serial Number": device_serial_number,
        "Quantification Cycle Method": measurement_method_identifier,
        "Chemistry": pcr_detection_chemistry,
        "Passive Reference": passive_reference_dye_setting,
        "Experiment Barcode": barcode,
        "Experiment User Name": analyst,
        "Experiment Name": experimental_data_identifier,
    }

    raw_text = "\n".join(
        [
            f"* {header_name} = {header_value}"
            for header_name, header_value in header_dict.items()
            if header_value is not None
        ]
    )

    return BytesIO(raw_text.encode("utf-8"))

tests/parsers/appbio_quantstudio/appbio_quantstudio_builders_test.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import (
    BaselineCorrectedReporterDataCube,
    CalculatedDataDocumentItem,
    ContainerType,
    DataProcessingDocument,
    DataProcessingDocument1,
    DataSourceAggregateDocument,
    DataSourceDocumentItem,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    ExperimentType,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    MeltingCurveDataCube,
    Model,
    NormalizedReporterDataCube,
    PassiveReferenceDyeDataCube,
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    QPCRAggregateDocument,
    QPCRDocumentItem,
    ReporterDyeDataCube,
    SampleDocument,
    TCalculatedDataAggregateDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueUnitless,
    TQuantityValueNumber,
    TQuantityValueUnitless,
)
from allotropy.allotrope.models.shared.definitions.definitions import (
    FieldComponentDatatype,
    TDatacubeComponent,
    TDatacubeData,
    TDatacubeStructure,
)
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import (
    AmplificationData,
    Data,
    Header,
    MeltCurveRawData,
    MulticomponentData,
    RawData,
    Result,
    Well,
    WellItem,
    WellList,
)
from allotropy.parsers.appbio_quantstudio.calculated_document import (
    CalculatedDocument,
    DataSource,
)


def get_data() -> Data:
    well = Well(
        identifier=1,
        items={
            "IPC": WellItem(
                uuid="d170f2df-7ff8-4978-b13d-a7ad19fab824",
                identifier=1,
                position="A1",
                target_dna_description="IPC",
                sample_identifier="NAC",
                well_location_identifier="A1",
                reporter_dye_setting="VIC",
                quencher_dye_setting="NFQ-MGB",
                sample_role_type="BlockedIPC",
                amplification_data_obj=AmplificationData(
                    total_cycle_number_setting=1,
                    cycle=[1],
                    rn=[1.064],
                    delta_rn=[-0.002],
                ),
                result_obj=Result(
                    cycle_threshold_value_setting=0.2,
                    cycle_threshold_result=None,
                    automatic_cycle_threshold_enabled_setting=False,
                    automatic_baseline_determination_enabled_setting=False,
                    normalized_reporter_result=1.13,
                    baseline_corrected_reporter_result=None,
                    genotyping_determination_result="Blocked IPC Control",
                    genotyping_determination_method_setting=0.0,
                    quantity=None,
                    quantity_mean=None,
                    quantity_sd=None,
                    ct_mean=None,
                    ct_sd=None,
                    delta_ct_mean=None,
                    delta_ct_se=None,
                    delta_delta_ct=None,
                    rq=None,
                    rq_min=None,
                    rq_max=None,
                    rn_mean=1.261,
                    rn_sd=0.088,
                    y_intercept=None,
                    r_squared=None,
                    slope=None,
                    efficiency=None,
                ),
            ),
            "TGFb": WellItem(
                uuid="ba281c64-0605-4e76-8e9c-2a183be3cc08",
                identifier=1,
                position="A1",
                target_dna_description="TGFb",
                sample_identifier="NAC",
                well_location_identifier="A1",
                reporter_dye_setting="FAM",
                quencher_dye_setting="NFQ-MGB",
                sample_role_type="NTC",
                amplification_data_obj=AmplificationData(
                    total_cycle_number_setting=1,
                    cycle=[1],
                    rn=[0.343],
                    delta_rn=[-0.007],
                ),
                result_obj=Result(
                    cycle_threshold_value_setting=0.2,
                    cycle_threshold_result=None,
                    automatic_cycle_threshold_enabled_setting=False,
                    automatic_baseline_determination_enabled_setting=False,
                    normalized_reporter_result=0.402,
                    baseline_corrected_reporter_result=None,
                    genotyping_determination_result="Negative Control",
                    genotyping_determination_method_setting=0.0,
                    quantity=None,
                    quantity_mean=None,
                    quantity_sd=None,
                    ct_mean=None,
                    ct_sd=None,
                    delta_ct_mean=None,
                    delta_ct_se=None,
                    delta_delta_ct=None,
                    rq=None,
                    rq_min=None,
                    rq_max=None,
                    rn_mean=0.397,
                    rn_sd=0.006,
                    y_intercept=None,
                    r_squared=None,
                    slope=None,
                    efficiency=None,
                ),
            ),
        },
        multicomponent_data=MulticomponentData(
            cycle=[1],
            columns={
                "FAM": [502840.900],
                "ROX": [1591197.500],
                "VIC": [1654662.500],
            },
        ),
        melt_curve_raw_data=None,
        calculated_document=None,
    )

    return Data(
        header=Header(
            measurement_time="2010-10-01 01:44:54 AM EDT",
            plate_well_count=96,
            barcode=None,
            device_identifier="278880034",
            model_number="QuantStudio(TM) 6 Flex System",
            device_serial_number="278880034",
            analyst=None,
            experimental_data_identifier="QuantStudio 96-Well Presence-Absence Example",
            experiment_type=ExperimentType.presence_absence_qPCR_experiment,
            measurement_method_identifier="Ct",
            pcr_detection_chemistry="TAQMAN",
            passive_reference_dye_setting="ROX",
        ),
        wells=WellList([well]),
        raw_data=RawData(
            [
                "Well\tWell Position\tCycle\tx1-m1\tx2-m2\tx3-m3\tx4-m4\tx5-m5",
                "1\t      A1\t1\t882,830.500\t1,748,809.500\t1,648,195.400\t1,513,508.200\t3,796.012",
                "",
                "",
            ]
        ),
        endogenous_control="",
        reference_sample="",
        calculated_documents=[
            CalculatedDocument(
                uuid="d006e7e7-fbe4-47cf-821b-904e85202803",
                name="rn mean",
                value=1.261,
                iterated=True,
                data_sources=[
                    DataSource(
                        feature="normalized reporter result",
                        reference=well.items["IPC"],
                    ),
                ],
            ),
            CalculatedDocument(
                uuid="f4fee39c-5861-4203-afcf-94ee755ac0b4",
                name="rn sd",
                value=0.088,
                iterated=True,
                data_sources=[
                    DataSource(
                        feature="normalized reporter result",
                        reference=well.items["IPC"],
                    ),
                ],
            ),
            CalculatedDocument(
                uuid="e6707b0c-4494-412f-8a8e-ef51d01f25b3",
                name="rn mean",
                value=0.397,
                iterated=True,
                data_sources=[
                    DataSource(
                        feature="normalized reporter result",
                        reference=well.items["TGFb"],
                    ),
                ],
            ),
            CalculatedDocument(
                uuid="da78a225-2c34-40b3-b487-97893bfe491a",
                name="rn sd",
                value=0.006,
                iterated=True,
                data_sources=[
                    DataSource(
                        feature="normalized reporter result",
                        reference=well.items["TGFb"],
                    ),
                ],
            ),
        ],
    )


def get_data2() -> Data:
    well = Well(
        identifier=1,
        items={
            "B2M-Qiagen": WellItem(
                uuid="1e572828d-45fd-49bd-ab66-2a7f06aea3b6",
                identifier=1,
                position="A1",
                target_dna_description="B2M-Qiagen",
                sample_identifier="1. 200217 U251p14_-ab_-SEMA3F_8h_pA_1",
                well_location_identifier="A1",
                reporter_dye_setting="SYBR",
                quencher_dye_setting=None,
                sample_role_type="UNKNOWN",
                amplification_data_obj=AmplificationData(
                    total_cycle_number_setting=1,
                    cycle=[1],
                    rn=[0.612],
                    delta_rn=[-0.007],
                ),
                result_obj=Result(
                    cycle_threshold_value_setting=0.277,
                    cycle_threshold_result=18.717,
                    automatic_cycle_threshold_enabled_setting=True,
                    automatic_baseline_determination_enabled_setting=True,
                    normalized_reporter_result=None,
                    baseline_corrected_reporter_result=None,
                    genotyping_determination_result=None,
                    genotyping_determination_method_setting=None,
                    quantity=None,
                    quantity_mean=None,
                    quantity_sd=None,
                    ct_mean=18.717,
                    ct_sd=None,
                    delta_ct_mean=None,
                    delta_ct_se=None,
                    delta_delta_ct=None,
                    rq=None,
                    rq_min=None,
                    rq_max=None,
                    rn_mean=None,
                    rn_sd=None,
                    y_intercept=None,
                    r_squared=None,
                    slope=None,
                    efficiency=None,
                ),
            ),
        },
        multicomponent_data=MulticomponentData(
            cycle=[1],
            columns={
                "ROX": [55573.94],
                "SYBR": [34014.32],
            },
        ),
        melt_curve_raw_data=MeltCurveRawData(
            reading=[1],
            fluorescence=[3.478],
            derivative=[0.093],
        ),
    )

    return Data(
        header=Header(
            measurement_time="2001-12-31 09:09:19 PM EST",
            plate_well_count=384,
            barcode=None,
            device_identifier="278880086",
            model_number="ViiA 7",
            device_serial_number="278880086",
            analyst=None,
            experimental_data_identifier="200224 U251p14 200217_14v9_SEMA3F_trial 1",
            experiment_type=ExperimentType.comparative_CT_qPCR_experiment,
            measurement_method_identifier="Ct",
            pcr_detection_chemistry="SYBR_GREEN",
            passive_reference_dye_setting="ROX",
        ),
        wells=WellList([well]),
        raw_data=RawData(
            [
                "Well\tWell Position\tCycle\tx1-m1\tx2-m2\tx3-m3\tx4-m4\tx5-m5",
                "1\t      A1\t1\t36,431.130\t4,790.476\t4,560.821\t56,089.960\t-387.130",
                "",
                "",
            ]
        ),
        endogenous_control="",
        reference_sample="",
        calculated_documents=[],
    )


def get_model() -> Model:
    return Model(
        qPCR_aggregate_document=QPCRAggregateDocument(
            device_system_document=DeviceSystemDocument(
                device_identifier="278880034",
                model_number="QuantStudio(TM) 6 Flex System",
                device_serial_number="278880034",
                asset_management_identifier=None,
                firmware_version=None,
                description=None,
                brand_name=None,
                product_manufacturer=None,
            ),
            qPCR_document=[
                QPCRDocumentItem(
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        plate_well_count=TQuantityValueNumber(
                            value=96,
                            unit="#",
                            field_type=None,
                        ),
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="d170f2df-7ff8-4978-b13d-a7ad19fab824",
                                measurement_time="2010-10-01T01:44:54-04:00",
                                target_DNA_description="IPC",
                                sample_document=SampleDocument(
                                    sample_identifier="NAC",
                                    batch_identifier=None,
                                    sample_role_type="BlockedIPC",
                                    well_location_identifier="A1",
                                    well_plate_identifier=None,
                                    mass_concentration=None,
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            device_identifier=None,
                                            detection_type=None,
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=1.0,
                                                unit="#",
                                                field_type=None,
                                            ),
                                            denaturing_temperature_setting=None,
                                            denaturing_time_setting=None,
                                            annealing_temperature_setting=None,
                                            annealing_time_setting=None,
                                            extension_temperature_setting=None,
                                            extension_time_setting=None,
                                            reporter_dye_setting="VIC",
                                            quencher_dye_setting="NFQ-MGB",
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.2,
                                                    unit="(unitless)",
                                                    field_type=None,
                                                ),
                                                automatic_cycle_threshold_enabled_setting=False,
                                                automatic_baseline_determination_enabled_setting=False,
                                                baseline_determination_start_cycle_setting=None,
                                                baseline_determination_end_cycle_setting=None,
                                                genotyping_determination_method=None,
                                                genotyping_determination_method_setting=TQuantityValueUnitless(
                                                    value=0.0,
                                                    unit="(unitless)",
                                                    field_type=None,
                                                ),
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=None,
                                                unit="(unitless)",
                                                field_type=None,
                                            ),
                                            normalized_reporter_result=TQuantityValueUnitless(
                                                value=1.13,
                                                unit="(unitless)",
                                                field_type=None,
                                            ),
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[1.064]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=None,
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[-0.002]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            genotyping_determination_result="Blocked IPC Control",
                                        )
                                    ]
                                ),
                                reporter_dye_data_cube=ReporterDyeDataCube(
                                    label="reporter dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="reporter dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[1654662.500]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                passive_reference_dye_data_cube=PassiveReferenceDyeDataCube(
                                    label="passive reference dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="passive reference dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[1591197.500]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                melting_curve_data_cube=None,
                            ),
                            MeasurementDocumentItem(
                                measurement_identifier="ba281c64-0605-4e76-8e9c-2a183be3cc08",
                                measurement_time="2010-10-01T01:44:54-04:00",
                                target_DNA_description="TGFb",
                                sample_document=SampleDocument(
                                    sample_identifier="NAC",
                                    batch_identifier=None,
                                    sample_role_type="NTC",
                                    well_location_identifier="A1",
                                    well_plate_identifier=None,
                                    mass_concentration=None,
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            device_identifier=None,
                                            detection_type=None,
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=1.0,
                                                unit="#",
                                                field_type=None,
                                            ),
                                            denaturing_temperature_setting=None,
                                            denaturing_time_setting=None,
                                            annealing_temperature_setting=None,
                                            annealing_time_setting=None,
                                            extension_temperature_setting=None,
                                            extension_time_setting=None,
                                            reporter_dye_setting="FAM",
                                            quencher_dye_setting="NFQ-MGB",
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.2,
                                                    unit="(unitless)",
                                                    field_type=None,
                                                ),
                                                automatic_cycle_threshold_enabled_setting=False,
                                                automatic_baseline_determination_enabled_setting=False,
                                                baseline_determination_start_cycle_setting=None,
                                                baseline_determination_end_cycle_setting=None,
                                                genotyping_determination_method=None,
                                                genotyping_determination_method_setting=TQuantityValueUnitless(
                                                    value=0.0,
                                                    unit="(unitless)",
                                                    field_type=None,
                                                ),
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=None,
                                                unit="(unitless)",
                                                field_type=None,
                                            ),
                                            normalized_reporter_result=TQuantityValueUnitless(
                                                value=0.402,
                                                unit="(unitless)",
                                                field_type=None,
                                            ),
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[0.343]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=None,
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[-0.007]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            genotyping_determination_result="Negative Control",
                                        )
                                    ]
                                ),
                                reporter_dye_data_cube=ReporterDyeDataCube(
                                    label="reporter dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="reporter dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[502840.900]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                passive_reference_dye_data_cube=PassiveReferenceDyeDataCube(
                                    label="passive reference dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="passive reference dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[1591197.500]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                melting_curve_data_cube=None,
                            ),
                        ],
                        analytical_method_identifier=None,
                        experimental_data_identifier="QuantStudio 96-Well Presence-Absence Example",
                        experiment_type=ExperimentType.presence_absence_qPCR_experiment,
                        container_type=ContainerType.qPCR_reaction_block,
                        well_volume=None,
                    ),
                    analyst=None,
                    submitter=None,
                )
            ],
            data_system_document=DataSystemDocument(
                data_system_instance_identifier="localhost",
                file_name="appbio_quantstudio_test01.txt",
                UNC_path="",
                software_name="Thermo QuantStudio",
                software_version="1.0",
                ASM_converter_name=ASM_CONVERTER_NAME,
                ASM_converter_version=ASM_CONVERTER_VERSION,
            ),
            calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                calculated_data_document=[
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="d006e7e7-fbe4-47cf-821b-904e85202803",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="d170f2df-7ff8-4978-b13d-a7ad19fab824",
                                    data_source_feature="normalized reporter result",
                                )
                            ]
                        ),
                        data_processing_document=None,
                        calculated_data_name="rn mean",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=1.261,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="f4fee39c-5861-4203-afcf-94ee755ac0b4",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="d170f2df-7ff8-4978-b13d-a7ad19fab824",
                                    data_source_feature="normalized reporter result",
                                )
                            ]
                        ),
                        data_processing_document=None,
                        calculated_data_name="rn sd",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.088,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="e6707b0c-4494-412f-8a8e-ef51d01f25b3",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="ba281c64-0605-4e76-8e9c-2a183be3cc08",
                                    data_source_feature="normalized reporter result",
                                )
                            ]
                        ),
                        data_processing_document=None,
                        calculated_data_name="rn mean",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.397,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="da78a225-2c34-40b3-b487-97893bfe491a",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="ba281c64-0605-4e76-8e9c-2a183be3cc08",
                                    data_source_feature="normalized reporter result",
                                )
                            ]
                        ),
                        data_processing_document=None,
                        calculated_data_name="rn sd",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.006,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                ]
            ),
        ),
        manifest="http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest",
    )


def get_model2() -> Model:
    return Model(
        qPCR_aggregate_document=QPCRAggregateDocument(
            device_system_document=DeviceSystemDocument(
                device_identifier="278880086",
                model_number="ViiA 7",
                device_serial_number="278880086",
                asset_management_identifier=None,
                firmware_version=None,
                description=None,
                brand_name=None,
                product_manufacturer=None,
            ),
            qPCR_document=[
                QPCRDocumentItem(
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        plate_well_count=TQuantityValueNumber(
                            value=384,
                            unit="#",
                            field_type=None,
                        ),
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="1e572828d-45fd-49bd-ab66-2a7f06aea3b6",
                                measurement_time="2001-12-31T21:09:19-05:00",
                                target_DNA_description="B2M-Qiagen",
                                sample_document=SampleDocument(
                                    sample_identifier="1. 200217 U251p14_-ab_-SEMA3F_8h_pA_1",
                                    batch_identifier=None,
                                    sample_role_type="UNKNOWN",
                                    well_location_identifier="A1",
                                    well_plate_identifier=None,
                                    mass_concentration=None,
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="SYBR_GREEN",
                                            device_identifier=None,
                                            detection_type=None,
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=1.0,
                                                unit="#",
                                                field_type=None,
                                            ),
                                            denaturing_temperature_setting=None,
                                            denaturing_time_setting=None,
                                            annealing_temperature_setting=None,
                                            annealing_time_setting=None,
                                            extension_temperature_setting=None,
                                            extension_time_setting=None,
                                            reporter_dye_setting="SYBR",
                                            quencher_dye_setting=None,
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.277,
                                                    unit="(unitless)",
                                                    field_type=None,
                                                ),
                                                automatic_cycle_threshold_enabled_setting=True,
                                                automatic_baseline_determination_enabled_setting=True,
                                                baseline_determination_start_cycle_setting=None,
                                                baseline_determination_end_cycle_setting=None,
                                                genotyping_determination_method=None,
                                                genotyping_determination_method_setting=None,
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=18.717,
                                                unit="(unitless)",
                                                field_type=None,
                                            ),
                                            normalized_reporter_result=None,
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[0.612]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=None,
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[-0.007]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            genotyping_determination_result=None,
                                        )
                                    ]
                                ),
                                reporter_dye_data_cube=ReporterDyeDataCube(
                                    label="reporter dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="reporter dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[34014.32]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                passive_reference_dye_data_cube=PassiveReferenceDyeDataCube(
                                    label="passive reference dye",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.integer,
                                                concept="cycle count",
                                                unit="#",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="passive reference dye fluorescence",
                                                unit="RFU",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[[55573.94]],  # type: ignore[list-item]
                                        points=None,
                                    ),
                                ),
                                melting_curve_data_cube=MeltingCurveDataCube(
                                    label="melting curve",
                                    cube_structure=TDatacubeStructure(
                                        dimensions=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="temperature",
                                                unit="degrees C",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            )
                                        ],
                                        measures=[
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="reporter dye fluorescence",
                                                unit="(unitless)",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            ),
                                            TDatacubeComponent(
                                                field_componentDatatype=FieldComponentDatatype.double,
                                                concept="slope",
                                                unit="(unitless)",
                                                scale=None,
                                                field_asm_fill_value=None,
                                            ),
                                        ],
                                    ),
                                    data=TDatacubeData(
                                        dimensions=[[1.0]],
                                        measures=[
                                            [3.478],  # type: ignore[list-item]
                                            [0.093],  # type: ignore[list-item]
                                        ],
                                        points=None,
                                    ),
                                ),
                            )
                        ],
                        analytical_method_identifier=None,
                        experimental_data_identifier="200224 U251p14 200217_14v9_SEMA3F_trial 1",
                        experiment_type=ExperimentType.comparative_CT_qPCR_experiment,
                        container_type=ContainerType.qPCR_reaction_block,
                        well_volume=None,
                    ),
                    analyst=None,
                    submitter=None,
                )
            ],
            data_system_document=DataSystemDocument(
                data_system_instance_identifier="localhost",
                file_name="appbio_quantstudio_test02.txt",
                UNC_path="",
                software_name="Thermo QuantStudio",
                software_version="1.0",
                ASM_converter_name=ASM_CONVERTER_NAME,
                ASM_converter_version=ASM_CONVERTER_VERSION,
            ),
            calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                calculated_data_document=[],
            ),
        ),
        manifest="http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest",
    )


def get_genotyping_data() -> Data:
    well = Well(
        identifier=0,
        items={
            "CYP19_2-Allele 1": WellItem(
                uuid="3c40d2c2-0a64-433f-b639-e9c6e896313e",
                identifier=1,
                position="A1",
                target_dna_description="CYP19_2-Allele 1",
                sample_identifier="NTC",
                well_location_identifier="A1",
                reporter_dye_setting=None,
                quencher_dye_setting=None,
                sample_role_type="NTC",
                amplification_data_obj=AmplificationData(
                    total_cycle_number_setting=2,
                    cycle=[1, 2],
                    rn=[0.275, 0.277],
                    delta_rn=[-0.003, -0.001],
                ),
                result_obj=Result(
                    cycle_threshold_value_setting=0.219,
                    cycle_threshold_result=None,
                    automatic_cycle_threshold_enabled_setting=True,
                    automatic_baseline_determination_enabled_setting=True,
                    normalized_reporter_result=None,
                    baseline_corrected_reporter_result=0.016,
                    genotyping_determination_result="Negative Control (NC)",
                    genotyping_determination_method_setting=None,
                    quantity=None,
                    quantity_mean=None,
                    quantity_sd=None,
                    ct_mean=None,
                    ct_sd=None,
                    delta_ct_mean=None,
                    delta_ct_se=None,
                    delta_delta_ct=None,
                    rq=None,
                    rq_min=None,
                    rq_max=None,
                    rn_mean=None,
                    rn_sd=None,
                    y_intercept=None,
                    r_squared=None,
                    slope=None,
                    efficiency=None,
                ),
            ),
            "CYP19_2-Allele 2": WellItem(
                uuid="cba0ba82-1486-4e0c-90ec-2abdfaf254b0",
                identifier=1,
                position="A1",
                target_dna_description="CYP19_2-Allele 2",
                sample_identifier="NTC",
                well_location_identifier="A1",
                reporter_dye_setting=None,
                quencher_dye_setting=None,
                sample_role_type="NTC",
                amplification_data_obj=AmplificationData(
                    total_cycle_number_setting=2,
                    cycle=[1, 2],
                    rn=[0.825, 0.831],
                    delta_rn=[-0.016, -0.011],
                ),
                result_obj=Result(
                    cycle_threshold_value_setting=0.132,
                    cycle_threshold_result=None,
                    automatic_cycle_threshold_enabled_setting=True,
                    automatic_baseline_determination_enabled_setting=True,
                    normalized_reporter_result=None,
                    baseline_corrected_reporter_result=0.029,
                    genotyping_determination_result="Negative Control (NC)",
                    genotyping_determination_method_setting=None,
                    quantity=None,
                    quantity_mean=None,
                    quantity_sd=None,
                    ct_mean=None,
                    ct_sd=None,
                    delta_ct_mean=None,
                    delta_ct_se=None,
                    delta_delta_ct=None,
                    rq=None,
                    rq_min=None,
                    rq_max=None,
                    rn_mean=None,
                    rn_sd=None,
                    y_intercept=None,
                    r_squared=None,
                    slope=None,
                    efficiency=None,
                ),
            ),
        },
    )

    return Data(
        header=Header(
            measurement_time="2010-09-16 07:35:29 AM EDT",
            plate_well_count=96,
            barcode=None,
            device_identifier="Sponge_Bob_32",
            model_number="QuantStudio(TM) 7 Flex System",
            device_serial_number="123456789",
            analyst=None,
            experimental_data_identifier="QuantStudio 96-Well SNP Genotyping Example",
            experiment_type=ExperimentType.genotyping_qPCR_experiment,
            measurement_method_identifier="Ct",
            pcr_detection_chemistry="TAQMAN",
            passive_reference_dye_setting="ROX",
        ),
        wells=WellList([well]),
        raw_data=None,
        endogenous_control="",
        reference_sample="",
        calculated_documents=[],
    )


def get_genotyping_model() -> Model:
    return Model(
        qPCR_aggregate_document=QPCRAggregateDocument(
            device_system_document=DeviceSystemDocument(
                device_identifier="Sponge_Bob_32",
                model_number="QuantStudio(TM) 7 Flex System",
                device_serial_number="123456789",
            ),
            qPCR_document=[
                QPCRDocumentItem(
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        plate_well_count=TQuantityValueNumber(
                            value=96,
                        ),
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="3c40d2c2-0a64-433f-b639-e9c6e896313e",
                                measurement_time="2010-09-16T07:35:29-04:00",
                                target_DNA_description="CYP19_2-Allele 1",
                                sample_document=SampleDocument(
                                    sample_identifier="NTC",
                                    sample_role_type="NTC",
                                    well_location_identifier="A1",
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=2.0
                                            ),
                                            reporter_dye_setting=None,
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.219
                                                ),
                                                automatic_cycle_threshold_enabled_setting=True,
                                                automatic_baseline_determination_enabled_setting=True,
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=None
                                            ),
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0, 2.0]],
                                                    measures=[[0.275, 0.277]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=TQuantityValueUnitless(
                                                value=0.016
                                            ),
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0, 2.0]],
                                                    measures=[[-0.003, -0.001]],  # type: ignore[list-item]
                                                ),
                                            ),
                                            genotyping_determination_result="Negative Control (NC)",
                                        )
                                    ]
                                ),
                            ),
                            MeasurementDocumentItem(
                                measurement_identifier="cba0ba82-1486-4e0c-90ec-2abdfaf254b0",
                                measurement_time="2010-09-16T07:35:29-04:00",
                                target_DNA_description="CYP19_2-Allele 2",
                                sample_document=SampleDocument(
                                    sample_identifier="NTC",
                                    sample_role_type="NTC",
                                    well_location_identifier="A1",
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=2.0
                                            ),
                                            reporter_dye_setting=None,
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.132
                                                ),
                                                automatic_cycle_threshold_enabled_setting=True,
                                                automatic_baseline_determination_enabled_setting=True,
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=None
                                            ),
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0, 2.0]],
                                                    measures=[[0.825, 0.831]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=TQuantityValueUnitless(
                                                value=0.029
                                            ),
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0, 2.0]],
                                                    measures=[[-0.016, -0.011]],  # type: ignore[list-item]
                                                ),
                                            ),
                                            genotyping_determination_result="Negative Control (NC)",
                                        )
                                    ]
                                ),
                            ),
                        ],
                        analytical_method_identifier=None,
                        experimental_data_identifier="QuantStudio 96-Well SNP Genotyping Example",
                        experiment_type=ExperimentType.genotyping_qPCR_experiment,
                        container_type=ContainerType.qPCR_reaction_block,
                    ),
                ),
            ],
            data_system_document=DataSystemDocument(
                data_system_instance_identifier="localhost",
                file_name="appbio_quantstudio_test03.txt",
                UNC_path="",
                software_name="Thermo QuantStudio",
                software_version="1.0",
                ASM_converter_name=ASM_CONVERTER_NAME,
                ASM_converter_version=ASM_CONVERTER_VERSION,
            ),
            calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                calculated_data_document=[],
            ),
        ),
        manifest="http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest",
    )


def get_rel_std_curve_data() -> Data:
    return Data(
        header=Header(
            measurement_time="2010-10-20 02:23:34 AM EDT",
            plate_well_count=96,
            experiment_type=ExperimentType.relative_standard_curve_qPCR_experiment,
            device_identifier="278880034",
            model_number="QuantStudio(TM) 7 Flex System",
            device_serial_number="278880034",
            measurement_method_identifier="Ct",
            pcr_detection_chemistry="TAQMAN",
            passive_reference_dye_setting="ROX",
            barcode="",
            analyst=None,
            experimental_data_identifier="QuantStudio96-Well Relative Standard Curve Example",
        ),
        wells=WellList(
            wells=[
                Well(
                    identifier=37,
                    items={
                        "RNaseP": WellItem(
                            uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                            identifier=37,
                            target_dna_description="RNaseP",
                            sample_identifier="800",
                            reporter_dye_setting="FAM",
                            position="D1",
                            well_location_identifier="D1",
                            quencher_dye_setting="NFQ-MGB",
                            sample_role_type="UNKNOWN",
                            amplification_data_obj=AmplificationData(
                                total_cycle_number_setting=1.0,
                                cycle=[1],
                                rn=[0.627],
                                delta_rn=[0.001],
                            ),
                            result_obj=Result(
                                cycle_threshold_value_setting=0.133,
                                cycle_threshold_result=30.155,
                                automatic_cycle_threshold_enabled_setting=True,
                                automatic_baseline_determination_enabled_setting=True,
                                normalized_reporter_result=None,
                                baseline_corrected_reporter_result=None,
                                genotyping_determination_result=None,
                                genotyping_determination_method_setting=None,
                                quantity=794.91,
                                quantity_mean=818.012,
                                quantity_sd=29.535,
                                ct_mean=30.115,
                                ct_sd=0.051,
                                delta_ct_mean=None,
                                delta_ct_se=None,
                                delta_delta_ct=None,
                                rq=0.798,
                                rq_min=0.658,
                                rq_max=0.967,
                                rn_mean=None,
                                rn_sd=None,
                                y_intercept=39.662,
                                r_squared=0.999,
                                slope=-3.278,
                                efficiency=101.866,
                            ),
                        )
                    },
                    multicomponent_data=None,
                    melt_curve_raw_data=None,
                    calculated_document=CalculatedDocument(
                        uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                        name="quantity",
                        value=794.91,
                        data_sources=[
                            DataSource(
                                feature="cycle threshold result",
                                reference=WellItem(
                                    uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                    identifier=37,
                                    target_dna_description="RNaseP",
                                    sample_identifier="800",
                                    reporter_dye_setting="FAM",
                                    position="D1",
                                    well_location_identifier="D1",
                                    quencher_dye_setting="NFQ-MGB",
                                    sample_role_type="UNKNOWN",
                                    amplification_data_obj=AmplificationData(
                                        total_cycle_number_setting=1.0,
                                        cycle=[1],
                                        rn=[0.627],
                                        delta_rn=[0.001],
                                    ),
                                    result_obj=Result(
                                        cycle_threshold_value_setting=0.133,
                                        cycle_threshold_result=30.155,
                                        automatic_cycle_threshold_enabled_setting=True,
                                        automatic_baseline_determination_enabled_setting=True,
                                        normalized_reporter_result=None,
                                        baseline_corrected_reporter_result=None,
                                        genotyping_determination_result=None,
                                        genotyping_determination_method_setting=None,
                                        quantity=794.91,
                                        quantity_mean=818.012,
                                        quantity_sd=29.535,
                                        ct_mean=30.115,
                                        ct_sd=0.051,
                                        delta_ct_mean=None,
                                        delta_ct_se=None,
                                        delta_delta_ct=None,
                                        rq=0.798,
                                        rq_min=0.658,
                                        rq_max=0.967,
                                        rn_mean=None,
                                        rn_sd=None,
                                        y_intercept=39.662,
                                        r_squared=0.999,
                                        slope=-3.278,
                                        efficiency=101.866,
                                    ),
                                ),
                            )
                        ],
                        iterated=True,
                    ),
                ),
                Well(
                    identifier=38,
                    items={
                        "RNaseP": WellItem(
                            uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                            identifier=38,
                            target_dna_description="RNaseP",
                            sample_identifier="800",
                            reporter_dye_setting="FAM",
                            position="D2",
                            well_location_identifier="D2",
                            quencher_dye_setting="NFQ-MGB",
                            sample_role_type="UNKNOWN",
                            amplification_data_obj=AmplificationData(
                                total_cycle_number_setting=1.0,
                                cycle=[1],
                                rn=[0.612],
                                delta_rn=[-0.001],
                            ),
                            result_obj=Result(
                                cycle_threshold_value_setting=0.133,
                                cycle_threshold_result=30.2,
                                automatic_cycle_threshold_enabled_setting=True,
                                automatic_baseline_determination_enabled_setting=True,
                                normalized_reporter_result=None,
                                baseline_corrected_reporter_result=None,
                                genotyping_determination_result=None,
                                genotyping_determination_method_setting=None,
                                quantity=769.776,
                                quantity_mean=818.012,
                                quantity_sd=29.535,
                                ct_mean=30.115,
                                ct_sd=0.051,
                                delta_ct_mean=None,
                                delta_ct_se=None,
                                delta_delta_ct=None,
                                rq=0.798,
                                rq_min=0.658,
                                rq_max=0.967,
                                rn_mean=None,
                                rn_sd=None,
                                y_intercept=39.662,
                                r_squared=0.999,
                                slope=-3.278,
                                efficiency=101.866,
                            ),
                        )
                    },
                    multicomponent_data=None,
                    melt_curve_raw_data=None,
                    calculated_document=CalculatedDocument(
                        uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                        name="quantity",
                        value=769.776,
                        data_sources=[
                            DataSource(
                                feature="cycle threshold result",
                                reference=WellItem(
                                    uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                    identifier=38,
                                    target_dna_description="RNaseP",
                                    sample_identifier="800",
                                    reporter_dye_setting="FAM",
                                    position="D2",
                                    well_location_identifier="D2",
                                    quencher_dye_setting="NFQ-MGB",
                                    sample_role_type="UNKNOWN",
                                    amplification_data_obj=AmplificationData(
                                        total_cycle_number_setting=1.0,
                                        cycle=[1],
                                        rn=[0.612],
                                        delta_rn=[-0.001],
                                    ),
                                    result_obj=Result(
                                        cycle_threshold_value_setting=0.133,
                                        cycle_threshold_result=30.2,
                                        automatic_cycle_threshold_enabled_setting=True,
                                        automatic_baseline_determination_enabled_setting=True,
                                        normalized_reporter_result=None,
                                        baseline_corrected_reporter_result=None,
                                        genotyping_determination_result=None,
                                        genotyping_determination_method_setting=None,
                                        quantity=769.776,
                                        quantity_mean=818.012,
                                        quantity_sd=29.535,
                                        ct_mean=30.115,
                                        ct_sd=0.051,
                                        delta_ct_mean=None,
                                        delta_ct_se=None,
                                        delta_delta_ct=None,
                                        rq=0.798,
                                        rq_min=0.658,
                                        rq_max=0.967,
                                        rn_mean=None,
                                        rn_sd=None,
                                        y_intercept=39.662,
                                        r_squared=0.999,
                                        slope=-3.278,
                                        efficiency=101.866,
                                    ),
                                ),
                            )
                        ],
                        iterated=True,
                    ),
                ),
            ]
        ),
        raw_data=None,
        endogenous_control="RNaseP",
        reference_sample="800",
        calculated_documents=[
            CalculatedDocument(
                uuid="9b24577d-08bf-4312-8d1e-13feea315b0f",
                name="quantity mean",
                value=818.012,
                data_sources=[
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                            name="quantity",
                            value=794.91,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                        identifier=37,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D1",
                                        well_location_identifier="D1",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.627],
                                            delta_rn=[0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.155,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=794.91,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                            name="quantity",
                            value=769.776,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                        identifier=38,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D2",
                                        well_location_identifier="D2",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.612],
                                            delta_rn=[-0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.2,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=769.776,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="7434c27a-11d2-41d4-b20d-7407b8dec622",
                name="quantity sd",
                value=29.535,
                data_sources=[
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                            name="quantity",
                            value=794.91,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                        identifier=37,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D1",
                                        well_location_identifier="D1",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.627],
                                            delta_rn=[0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.155,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=794.91,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                            name="quantity",
                            value=769.776,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                        identifier=38,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D2",
                                        well_location_identifier="D2",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.612],
                                            delta_rn=[-0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.2,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=769.776,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="88749c03-5cdb-49c1-b829-092e95e863ff",
                name="ct sd",
                value=0.051,
                data_sources=[
                    DataSource(
                        feature="cycle threshold result",
                        reference=WellItem(
                            uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                            identifier=37,
                            target_dna_description="RNaseP",
                            sample_identifier="800",
                            reporter_dye_setting="FAM",
                            position="D1",
                            well_location_identifier="D1",
                            quencher_dye_setting="NFQ-MGB",
                            sample_role_type="UNKNOWN",
                            amplification_data_obj=AmplificationData(
                                total_cycle_number_setting=1.0,
                                cycle=[1],
                                rn=[0.627],
                                delta_rn=[0.001],
                            ),
                            result_obj=Result(
                                cycle_threshold_value_setting=0.133,
                                cycle_threshold_result=30.155,
                                automatic_cycle_threshold_enabled_setting=True,
                                automatic_baseline_determination_enabled_setting=True,
                                normalized_reporter_result=None,
                                baseline_corrected_reporter_result=None,
                                genotyping_determination_result=None,
                                genotyping_determination_method_setting=None,
                                quantity=794.91,
                                quantity_mean=818.012,
                                quantity_sd=29.535,
                                ct_mean=30.115,
                                ct_sd=0.051,
                                delta_ct_mean=None,
                                delta_ct_se=None,
                                delta_delta_ct=None,
                                rq=0.798,
                                rq_min=0.658,
                                rq_max=0.967,
                                rn_mean=None,
                                rn_sd=None,
                                y_intercept=39.662,
                                r_squared=0.999,
                                slope=-3.278,
                                efficiency=101.866,
                            ),
                        ),
                    ),
                    DataSource(
                        feature="cycle threshold result",
                        reference=WellItem(
                            uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                            identifier=38,
                            target_dna_description="RNaseP",
                            sample_identifier="800",
                            reporter_dye_setting="FAM",
                            position="D2",
                            well_location_identifier="D2",
                            quencher_dye_setting="NFQ-MGB",
                            sample_role_type="UNKNOWN",
                            amplification_data_obj=AmplificationData(
                                total_cycle_number_setting=1.0,
                                cycle=[1],
                                rn=[0.612],
                                delta_rn=[-0.001],
                            ),
                            result_obj=Result(
                                cycle_threshold_value_setting=0.133,
                                cycle_threshold_result=30.2,
                                automatic_cycle_threshold_enabled_setting=True,
                                automatic_baseline_determination_enabled_setting=True,
                                normalized_reporter_result=None,
                                baseline_corrected_reporter_result=None,
                                genotyping_determination_result=None,
                                genotyping_determination_method_setting=None,
                                quantity=769.776,
                                quantity_mean=818.012,
                                quantity_sd=29.535,
                                ct_mean=30.115,
                                ct_sd=0.051,
                                delta_ct_mean=None,
                                delta_ct_se=None,
                                delta_delta_ct=None,
                                rq=0.798,
                                rq_min=0.658,
                                rq_max=0.967,
                                rn_mean=None,
                                rn_sd=None,
                                y_intercept=39.662,
                                r_squared=0.999,
                                slope=-3.278,
                                efficiency=101.866,
                            ),
                        ),
                    ),
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="75798875-017f-4026-9131-5d7661e01213",
                name="rq min",
                value=0.658,
                data_sources=[
                    DataSource(
                        feature="rq",
                        reference=CalculatedDocument(
                            uuid="4606bf27-7d50-4ef0-a643-6d105b080057",
                            name="rq",
                            value=0.798,
                            data_sources=[
                                DataSource(
                                    feature="quantity mean",
                                    reference=CalculatedDocument(
                                        uuid="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                                        name="quantity mean",
                                        value=818.012,
                                        data_sources=[
                                            DataSource(
                                                feature="quantity",
                                                reference=CalculatedDocument(
                                                    uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                                                    name="quantity",
                                                    value=794.91,
                                                    data_sources=[
                                                        DataSource(
                                                            feature="cycle threshold result",
                                                            reference=WellItem(
                                                                uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                                                identifier=37,
                                                                target_dna_description="RNaseP",
                                                                sample_identifier="800",
                                                                reporter_dye_setting="FAM",
                                                                position="D1",
                                                                well_location_identifier="D1",
                                                                quencher_dye_setting="NFQ-MGB",
                                                                sample_role_type="UNKNOWN",
                                                                amplification_data_obj=AmplificationData(
                                                                    total_cycle_number_setting=1.0,
                                                                    cycle=[1],
                                                                    rn=[0.627],
                                                                    delta_rn=[0.001],
                                                                ),
                                                                result_obj=Result(
                                                                    cycle_threshold_value_setting=0.133,
                                                                    cycle_threshold_result=30.155,
                                                                    automatic_cycle_threshold_enabled_setting=True,
                                                                    automatic_baseline_determination_enabled_setting=True,
                                                                    normalized_reporter_result=None,
                                                                    baseline_corrected_reporter_result=None,
                                                                    genotyping_determination_result=None,
                                                                    genotyping_determination_method_setting=None,
                                                                    quantity=794.91,
                                                                    quantity_mean=818.012,
                                                                    quantity_sd=29.535,
                                                                    ct_mean=30.115,
                                                                    ct_sd=0.051,
                                                                    delta_ct_mean=None,
                                                                    delta_ct_se=None,
                                                                    delta_delta_ct=None,
                                                                    rq=0.798,
                                                                    rq_min=0.658,
                                                                    rq_max=0.967,
                                                                    rn_mean=None,
                                                                    rn_sd=None,
                                                                    y_intercept=39.662,
                                                                    r_squared=0.999,
                                                                    slope=-3.278,
                                                                    efficiency=101.866,
                                                                ),
                                                            ),
                                                        )
                                                    ],
                                                    iterated=True,
                                                ),
                                            ),
                                            DataSource(
                                                feature="quantity",
                                                reference=CalculatedDocument(
                                                    uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                                    name="quantity",
                                                    value=769.776,
                                                    data_sources=[
                                                        DataSource(
                                                            feature="cycle threshold result",
                                                            reference=WellItem(
                                                                uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                                                identifier=38,
                                                                target_dna_description="RNaseP",
                                                                sample_identifier="800",
                                                                reporter_dye_setting="FAM",
                                                                position="D2",
                                                                well_location_identifier="D2",
                                                                quencher_dye_setting="NFQ-MGB",
                                                                sample_role_type="UNKNOWN",
                                                                amplification_data_obj=AmplificationData(
                                                                    total_cycle_number_setting=1.0,
                                                                    cycle=[1],
                                                                    rn=[0.612],
                                                                    delta_rn=[-0.001],
                                                                ),
                                                                result_obj=Result(
                                                                    cycle_threshold_value_setting=0.133,
                                                                    cycle_threshold_result=30.2,
                                                                    automatic_cycle_threshold_enabled_setting=True,
                                                                    automatic_baseline_determination_enabled_setting=True,
                                                                    normalized_reporter_result=None,
                                                                    baseline_corrected_reporter_result=None,
                                                                    genotyping_determination_result=None,
                                                                    genotyping_determination_method_setting=None,
                                                                    quantity=769.776,
                                                                    quantity_mean=818.012,
                                                                    quantity_sd=29.535,
                                                                    ct_mean=30.115,
                                                                    ct_sd=0.051,
                                                                    delta_ct_mean=None,
                                                                    delta_ct_se=None,
                                                                    delta_delta_ct=None,
                                                                    rq=0.798,
                                                                    rq_min=0.658,
                                                                    rq_max=0.967,
                                                                    rn_mean=None,
                                                                    rn_sd=None,
                                                                    y_intercept=39.662,
                                                                    r_squared=0.999,
                                                                    slope=-3.278,
                                                                    efficiency=101.866,
                                                                ),
                                                            ),
                                                        )
                                                    ],
                                                    iterated=True,
                                                ),
                                            ),
                                        ],
                                        iterated=True,
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    )
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="4606bf27-7d50-4ef0-a643-6d105b080057",
                name="rq",
                value=0.798,
                data_sources=[
                    DataSource(
                        feature="quantity mean",
                        reference=CalculatedDocument(
                            uuid="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                            name="quantity mean",
                            value=818.012,
                            data_sources=[
                                DataSource(
                                    feature="quantity",
                                    reference=CalculatedDocument(
                                        uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                                        name="quantity",
                                        value=794.91,
                                        data_sources=[
                                            DataSource(
                                                feature="cycle threshold result",
                                                reference=WellItem(
                                                    uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                                    identifier=37,
                                                    target_dna_description="RNaseP",
                                                    sample_identifier="800",
                                                    reporter_dye_setting="FAM",
                                                    position="D1",
                                                    well_location_identifier="D1",
                                                    quencher_dye_setting="NFQ-MGB",
                                                    sample_role_type="UNKNOWN",
                                                    amplification_data_obj=AmplificationData(
                                                        total_cycle_number_setting=1.0,
                                                        cycle=[1],
                                                        rn=[0.627],
                                                        delta_rn=[0.001],
                                                    ),
                                                    result_obj=Result(
                                                        cycle_threshold_value_setting=0.133,
                                                        cycle_threshold_result=30.155,
                                                        automatic_cycle_threshold_enabled_setting=True,
                                                        automatic_baseline_determination_enabled_setting=True,
                                                        normalized_reporter_result=None,
                                                        baseline_corrected_reporter_result=None,
                                                        genotyping_determination_result=None,
                                                        genotyping_determination_method_setting=None,
                                                        quantity=794.91,
                                                        quantity_mean=818.012,
                                                        quantity_sd=29.535,
                                                        ct_mean=30.115,
                                                        ct_sd=0.051,
                                                        delta_ct_mean=None,
                                                        delta_ct_se=None,
                                                        delta_delta_ct=None,
                                                        rq=0.798,
                                                        rq_min=0.658,
                                                        rq_max=0.967,
                                                        rn_mean=None,
                                                        rn_sd=None,
                                                        y_intercept=39.662,
                                                        r_squared=0.999,
                                                        slope=-3.278,
                                                        efficiency=101.866,
                                                    ),
                                                ),
                                            )
                                        ],
                                        iterated=True,
                                    ),
                                ),
                                DataSource(
                                    feature="quantity",
                                    reference=CalculatedDocument(
                                        uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                        name="quantity",
                                        value=769.776,
                                        data_sources=[
                                            DataSource(
                                                feature="cycle threshold result",
                                                reference=WellItem(
                                                    uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                                    identifier=38,
                                                    target_dna_description="RNaseP",
                                                    sample_identifier="800",
                                                    reporter_dye_setting="FAM",
                                                    position="D2",
                                                    well_location_identifier="D2",
                                                    quencher_dye_setting="NFQ-MGB",
                                                    sample_role_type="UNKNOWN",
                                                    amplification_data_obj=AmplificationData(
                                                        total_cycle_number_setting=1.0,
                                                        cycle=[1],
                                                        rn=[0.612],
                                                        delta_rn=[-0.001],
                                                    ),
                                                    result_obj=Result(
                                                        cycle_threshold_value_setting=0.133,
                                                        cycle_threshold_result=30.2,
                                                        automatic_cycle_threshold_enabled_setting=True,
                                                        automatic_baseline_determination_enabled_setting=True,
                                                        normalized_reporter_result=None,
                                                        baseline_corrected_reporter_result=None,
                                                        genotyping_determination_result=None,
                                                        genotyping_determination_method_setting=None,
                                                        quantity=769.776,
                                                        quantity_mean=818.012,
                                                        quantity_sd=29.535,
                                                        ct_mean=30.115,
                                                        ct_sd=0.051,
                                                        delta_ct_mean=None,
                                                        delta_ct_se=None,
                                                        delta_delta_ct=None,
                                                        rq=0.798,
                                                        rq_min=0.658,
                                                        rq_max=0.967,
                                                        rn_mean=None,
                                                        rn_sd=None,
                                                        y_intercept=39.662,
                                                        r_squared=0.999,
                                                        slope=-3.278,
                                                        efficiency=101.866,
                                                    ),
                                                ),
                                            )
                                        ],
                                        iterated=True,
                                    ),
                                ),
                            ],
                            iterated=True,
                        ),
                    )
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                name="quantity mean",
                value=818.012,
                data_sources=[
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                            name="quantity",
                            value=794.91,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                        identifier=37,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D1",
                                        well_location_identifier="D1",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.627],
                                            delta_rn=[0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.155,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=794.91,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                    DataSource(
                        feature="quantity",
                        reference=CalculatedDocument(
                            uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                            name="quantity",
                            value=769.776,
                            data_sources=[
                                DataSource(
                                    feature="cycle threshold result",
                                    reference=WellItem(
                                        uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                        identifier=38,
                                        target_dna_description="RNaseP",
                                        sample_identifier="800",
                                        reporter_dye_setting="FAM",
                                        position="D2",
                                        well_location_identifier="D2",
                                        quencher_dye_setting="NFQ-MGB",
                                        sample_role_type="UNKNOWN",
                                        amplification_data_obj=AmplificationData(
                                            total_cycle_number_setting=1.0,
                                            cycle=[1],
                                            rn=[0.612],
                                            delta_rn=[-0.001],
                                        ),
                                        result_obj=Result(
                                            cycle_threshold_value_setting=0.133,
                                            cycle_threshold_result=30.2,
                                            automatic_cycle_threshold_enabled_setting=True,
                                            automatic_baseline_determination_enabled_setting=True,
                                            normalized_reporter_result=None,
                                            baseline_corrected_reporter_result=None,
                                            genotyping_determination_result=None,
                                            genotyping_determination_method_setting=None,
                                            quantity=769.776,
                                            quantity_mean=818.012,
                                            quantity_sd=29.535,
                                            ct_mean=30.115,
                                            ct_sd=0.051,
                                            delta_ct_mean=None,
                                            delta_ct_se=None,
                                            delta_delta_ct=None,
                                            rq=0.798,
                                            rq_min=0.658,
                                            rq_max=0.967,
                                            rn_mean=None,
                                            rn_sd=None,
                                            y_intercept=39.662,
                                            r_squared=0.999,
                                            slope=-3.278,
                                            efficiency=101.866,
                                        ),
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    ),
                ],
                iterated=True,
            ),
            CalculatedDocument(
                uuid="d23ae1c4-e750-43eb-a6df-ed66d96bcd20",
                name="rq max",
                value=0.967,
                data_sources=[
                    DataSource(
                        feature="rq",
                        reference=CalculatedDocument(
                            uuid="4606bf27-7d50-4ef0-a643-6d105b080057",
                            name="rq",
                            value=0.798,
                            data_sources=[
                                DataSource(
                                    feature="quantity mean",
                                    reference=CalculatedDocument(
                                        uuid="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                                        name="quantity mean",
                                        value=818.012,
                                        data_sources=[
                                            DataSource(
                                                feature="quantity",
                                                reference=CalculatedDocument(
                                                    uuid="94cf744d-d243-47bd-a723-aca6c6517af5",
                                                    name="quantity",
                                                    value=794.91,
                                                    data_sources=[
                                                        DataSource(
                                                            feature="cycle threshold result",
                                                            reference=WellItem(
                                                                uuid="2b290825-2a40-4768-a21f-a8a603c32c85",
                                                                identifier=37,
                                                                target_dna_description="RNaseP",
                                                                sample_identifier="800",
                                                                reporter_dye_setting="FAM",
                                                                position="D1",
                                                                well_location_identifier="D1",
                                                                quencher_dye_setting="NFQ-MGB",
                                                                sample_role_type="UNKNOWN",
                                                                amplification_data_obj=AmplificationData(
                                                                    total_cycle_number_setting=1.0,
                                                                    cycle=[1],
                                                                    rn=[0.627],
                                                                    delta_rn=[0.001],
                                                                ),
                                                                result_obj=Result(
                                                                    cycle_threshold_value_setting=0.133,
                                                                    cycle_threshold_result=30.155,
                                                                    automatic_cycle_threshold_enabled_setting=True,
                                                                    automatic_baseline_determination_enabled_setting=True,
                                                                    normalized_reporter_result=None,
                                                                    baseline_corrected_reporter_result=None,
                                                                    genotyping_determination_result=None,
                                                                    genotyping_determination_method_setting=None,
                                                                    quantity=794.91,
                                                                    quantity_mean=818.012,
                                                                    quantity_sd=29.535,
                                                                    ct_mean=30.115,
                                                                    ct_sd=0.051,
                                                                    delta_ct_mean=None,
                                                                    delta_ct_se=None,
                                                                    delta_delta_ct=None,
                                                                    rq=0.798,
                                                                    rq_min=0.658,
                                                                    rq_max=0.967,
                                                                    rn_mean=None,
                                                                    rn_sd=None,
                                                                    y_intercept=39.662,
                                                                    r_squared=0.999,
                                                                    slope=-3.278,
                                                                    efficiency=101.866,
                                                                ),
                                                            ),
                                                        )
                                                    ],
                                                    iterated=True,
                                                ),
                                            ),
                                            DataSource(
                                                feature="quantity",
                                                reference=CalculatedDocument(
                                                    uuid="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                                    name="quantity",
                                                    value=769.776,
                                                    data_sources=[
                                                        DataSource(
                                                            feature="cycle threshold result",
                                                            reference=WellItem(
                                                                uuid="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                                                identifier=38,
                                                                target_dna_description="RNaseP",
                                                                sample_identifier="800",
                                                                reporter_dye_setting="FAM",
                                                                position="D2",
                                                                well_location_identifier="D2",
                                                                quencher_dye_setting="NFQ-MGB",
                                                                sample_role_type="UNKNOWN",
                                                                amplification_data_obj=AmplificationData(
                                                                    total_cycle_number_setting=1.0,
                                                                    cycle=[1],
                                                                    rn=[0.612],
                                                                    delta_rn=[-0.001],
                                                                ),
                                                                result_obj=Result(
                                                                    cycle_threshold_value_setting=0.133,
                                                                    cycle_threshold_result=30.2,
                                                                    automatic_cycle_threshold_enabled_setting=True,
                                                                    automatic_baseline_determination_enabled_setting=True,
                                                                    normalized_reporter_result=None,
                                                                    baseline_corrected_reporter_result=None,
                                                                    genotyping_determination_result=None,
                                                                    genotyping_determination_method_setting=None,
                                                                    quantity=769.776,
                                                                    quantity_mean=818.012,
                                                                    quantity_sd=29.535,
                                                                    ct_mean=30.115,
                                                                    ct_sd=0.051,
                                                                    delta_ct_mean=None,
                                                                    delta_ct_se=None,
                                                                    delta_delta_ct=None,
                                                                    rq=0.798,
                                                                    rq_min=0.658,
                                                                    rq_max=0.967,
                                                                    rn_mean=None,
                                                                    rn_sd=None,
                                                                    y_intercept=39.662,
                                                                    r_squared=0.999,
                                                                    slope=-3.278,
                                                                    efficiency=101.866,
                                                                ),
                                                            ),
                                                        )
                                                    ],
                                                    iterated=True,
                                                ),
                                            ),
                                        ],
                                        iterated=True,
                                    ),
                                )
                            ],
                            iterated=True,
                        ),
                    )
                ],
                iterated=True,
            ),
        ],
    )


def get_rel_std_curve_model() -> Model:
    return Model(
        qPCR_aggregate_document=QPCRAggregateDocument(
            device_system_document=DeviceSystemDocument(
                device_identifier="278880034",
                model_number="QuantStudio(TM) 7 Flex System",
                device_serial_number="278880034",
                asset_management_identifier=None,
                firmware_version=None,
                description=None,
                brand_name=None,
                product_manufacturer=None,
            ),
            qPCR_document=[
                QPCRDocumentItem(
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        plate_well_count=TQuantityValueNumber(
                            value=96,
                            unit="#",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="2b290825-2a40-4768-a21f-a8a603c32c85",
                                measurement_time="2010-10-20T02:23:34-04:00",
                                target_DNA_description="RNaseP",
                                sample_document=SampleDocument(
                                    sample_identifier="800",
                                    batch_identifier=None,
                                    sample_role_type="UNKNOWN",
                                    well_location_identifier="D1",
                                    well_plate_identifier="",
                                    mass_concentration=None,
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            device_identifier=None,
                                            detection_type=None,
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=1.0,
                                                unit="#",
                                                has_statistic_datum_role=None,
                                                field_type=None,
                                            ),
                                            denaturing_temperature_setting=None,
                                            denaturing_time_setting=None,
                                            annealing_temperature_setting=None,
                                            annealing_time_setting=None,
                                            extension_temperature_setting=None,
                                            extension_time_setting=None,
                                            reporter_dye_setting="FAM",
                                            quencher_dye_setting="NFQ-MGB",
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.133,
                                                    unit="(unitless)",
                                                    has_statistic_datum_role=None,
                                                    field_type=None,
                                                ),
                                                automatic_cycle_threshold_enabled_setting=True,
                                                automatic_baseline_determination_enabled_setting=True,
                                                baseline_determination_start_cycle_setting=None,
                                                baseline_determination_end_cycle_setting=None,
                                                genotyping_determination_method=None,
                                                genotyping_determination_method_setting=None,
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=30.155,
                                                unit="(unitless)",
                                                has_statistic_datum_role=None,
                                                field_type=None,
                                            ),
                                            normalized_reporter_result=None,
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[0.627]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=None,
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[0.001]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            genotyping_determination_result=None,
                                        )
                                    ]
                                ),
                                reporter_dye_data_cube=None,
                                passive_reference_dye_data_cube=None,
                                melting_curve_data_cube=None,
                            )
                        ],
                        analytical_method_identifier=None,
                        experimental_data_identifier="QuantStudio96-Well Relative Standard Curve Example",
                        experiment_type=ExperimentType.relative_standard_curve_qPCR_experiment,
                        container_type=ContainerType.qPCR_reaction_block,
                        well_volume=None,
                    ),
                    analyst=None,
                    submitter=None,
                    calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                        calculated_data_document=[
                            CalculatedDataDocumentItem(
                                calculated_data_identifier="94cf744d-d243-47bd-a723-aca6c6517af5",
                                data_source_aggregate_document=DataSourceAggregateDocument(
                                    data_source_document=[
                                        DataSourceDocumentItem(
                                            data_source_identifier="2b290825-2a40-4768-a21f-a8a603c32c85",
                                            data_source_feature="cycle threshold result",
                                        )
                                    ]
                                ),
                                data_processing_document=None,
                                calculated_data_name="quantity",
                                calculated_data_description=None,
                                calculated_datum=TQuantityValueUnitless(
                                    value=794.91,
                                    unit="(unitless)",
                                    has_statistic_datum_role=None,
                                    field_type=None,
                                ),
                            )
                        ]
                    ),
                ),
                QPCRDocumentItem(
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        plate_well_count=TQuantityValueNumber(
                            value=96,
                            unit="#",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                measurement_time="2010-10-20T02:23:34-04:00",
                                target_DNA_description="RNaseP",
                                sample_document=SampleDocument(
                                    sample_identifier="800",
                                    batch_identifier=None,
                                    sample_role_type="UNKNOWN",
                                    well_location_identifier="D2",
                                    well_plate_identifier="",
                                    mass_concentration=None,
                                ),
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="qPCR",
                                            measurement_method_identifier="Ct",
                                            PCR_detection_chemistry="TAQMAN",
                                            device_identifier=None,
                                            detection_type=None,
                                            total_cycle_number_setting=TQuantityValueNumber(
                                                value=1.0,
                                                unit="#",
                                                has_statistic_datum_role=None,
                                                field_type=None,
                                            ),
                                            denaturing_temperature_setting=None,
                                            denaturing_time_setting=None,
                                            annealing_temperature_setting=None,
                                            annealing_time_setting=None,
                                            extension_temperature_setting=None,
                                            extension_time_setting=None,
                                            reporter_dye_setting="FAM",
                                            quencher_dye_setting="NFQ-MGB",
                                            passive_reference_dye_setting="ROX",
                                        )
                                    ]
                                ),
                                processed_data_aggregate_document=ProcessedDataAggregateDocument(
                                    processed_data_document=[
                                        ProcessedDataDocumentItem(
                                            data_processing_document=DataProcessingDocument(
                                                cycle_threshold_value_setting=TQuantityValueUnitless(
                                                    value=0.133,
                                                    unit="(unitless)",
                                                    has_statistic_datum_role=None,
                                                    field_type=None,
                                                ),
                                                automatic_cycle_threshold_enabled_setting=True,
                                                automatic_baseline_determination_enabled_setting=True,
                                                baseline_determination_start_cycle_setting=None,
                                                baseline_determination_end_cycle_setting=None,
                                                genotyping_determination_method=None,
                                                genotyping_determination_method_setting=None,
                                            ),
                                            cycle_threshold_result=TNullableQuantityValueUnitless(
                                                value=30.2,
                                                unit="(unitless)",
                                                has_statistic_datum_role=None,
                                                field_type=None,
                                            ),
                                            normalized_reporter_result=None,
                                            normalized_reporter_data_cube=NormalizedReporterDataCube(
                                                label="normalized reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="normalized report result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[0.612]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            baseline_corrected_reporter_result=None,
                                            baseline_corrected_reporter_data_cube=BaselineCorrectedReporterDataCube(
                                                label="baseline corrected reporter",
                                                cube_structure=TDatacubeStructure(
                                                    dimensions=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.integer,
                                                            concept="cycle count",
                                                            unit="#",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                    measures=[
                                                        TDatacubeComponent(
                                                            field_componentDatatype=FieldComponentDatatype.double,
                                                            concept="baseline corrected reporter result",
                                                            unit="(unitless)",
                                                            scale=None,
                                                            field_asm_fill_value=None,
                                                        )
                                                    ],
                                                ),
                                                data=TDatacubeData(
                                                    dimensions=[[1.0]],
                                                    measures=[[-0.001]],  # type: ignore[list-item]
                                                    points=None,
                                                ),
                                            ),
                                            genotyping_determination_result=None,
                                        )
                                    ]
                                ),
                                reporter_dye_data_cube=None,
                                passive_reference_dye_data_cube=None,
                                melting_curve_data_cube=None,
                            )
                        ],
                        analytical_method_identifier=None,
                        experimental_data_identifier="QuantStudio96-Well Relative Standard Curve Example",
                        experiment_type=ExperimentType.relative_standard_curve_qPCR_experiment,
                        container_type=ContainerType.qPCR_reaction_block,
                        well_volume=None,
                    ),
                    analyst=None,
                    submitter=None,
                    calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                        calculated_data_document=[
                            CalculatedDataDocumentItem(
                                calculated_data_identifier="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                data_source_aggregate_document=DataSourceAggregateDocument(
                                    data_source_document=[
                                        DataSourceDocumentItem(
                                            data_source_identifier="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                            data_source_feature="cycle threshold result",
                                        )
                                    ]
                                ),
                                data_processing_document=None,
                                calculated_data_name="quantity",
                                calculated_data_description=None,
                                calculated_datum=TQuantityValueUnitless(
                                    value=769.776,
                                    unit="(unitless)",
                                    has_statistic_datum_role=None,
                                    field_type=None,
                                ),
                            )
                        ]
                    ),
                ),
            ],
            data_system_document=DataSystemDocument(
                data_system_instance_identifier="localhost",
                file_name="appbio_quantstudio_test04.txt",
                UNC_path="",
                software_name="Thermo QuantStudio",
                software_version="1.0",
                ASM_converter_name="allotropy",
                ASM_converter_version=ASM_CONVERTER_VERSION,
            ),
            calculated_data_aggregate_document=TCalculatedDataAggregateDocument(
                calculated_data_document=[
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="9b24577d-08bf-4312-8d1e-13feea315b0f",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="94cf744d-d243-47bd-a723-aca6c6517af5",
                                    data_source_feature="quantity",
                                ),
                                DataSourceDocumentItem(
                                    data_source_identifier="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                    data_source_feature="quantity",
                                ),
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="quantity mean",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=818.012,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="7434c27a-11d2-41d4-b20d-7407b8dec622",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="94cf744d-d243-47bd-a723-aca6c6517af5",
                                    data_source_feature="quantity",
                                ),
                                DataSourceDocumentItem(
                                    data_source_identifier="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                    data_source_feature="quantity",
                                ),
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="quantity sd",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=29.535,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="88749c03-5cdb-49c1-b829-092e95e863ff",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="2b290825-2a40-4768-a21f-a8a603c32c85",
                                    data_source_feature="cycle threshold result",
                                ),
                                DataSourceDocumentItem(
                                    data_source_identifier="42788e09-e972-4c9b-b8d0-7263e07371fa",
                                    data_source_feature="cycle threshold result",
                                ),
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="ct sd",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.051,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="75798875-017f-4026-9131-5d7661e01213",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="4606bf27-7d50-4ef0-a643-6d105b080057",
                                    data_source_feature="rq",
                                )
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="rq min",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.658,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="4606bf27-7d50-4ef0-a643-6d105b080057",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                                    data_source_feature="quantity mean",
                                )
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="rq",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.798,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="0eb90790-c6a6-476e-b5cb-8b0c6a3e62f2",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="94cf744d-d243-47bd-a723-aca6c6517af5",
                                    data_source_feature="quantity",
                                ),
                                DataSourceDocumentItem(
                                    data_source_identifier="51066bef-10b0-4782-a8c9-ee7383fe3256",
                                    data_source_feature="quantity",
                                ),
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="quantity mean",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=818.012,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                    CalculatedDataDocumentItem(
                        calculated_data_identifier="d23ae1c4-e750-43eb-a6df-ed66d96bcd20",
                        data_source_aggregate_document=DataSourceAggregateDocument(
                            data_source_document=[
                                DataSourceDocumentItem(
                                    data_source_identifier="4606bf27-7d50-4ef0-a643-6d105b080057",
                                    data_source_feature="rq",
                                )
                            ]
                        ),
                        data_processing_document=DataProcessingDocument1(
                            reference_DNA_description="RNaseP",
                            reference_sample_description="800",
                        ),
                        calculated_data_name="rq max",
                        calculated_data_description=None,
                        calculated_datum=TQuantityValueUnitless(
                            value=0.967,
                            unit="(unitless)",
                            has_statistic_datum_role=None,
                            field_type=None,
                        ),
                    ),
                ]
            ),
        ),
        manifest="http://purl.allotrope.org/manifests/pcr/BENCHLING/2023/09/qpcr.manifest",
    )

tests/parsers/appbio_quantstudio/appbio_quantstudio_data.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.allotrope.models.pcr_benchling_2023_09_qpcr import Model
from allotropy.parser_factory import Vendor
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_parser import (
    AppBioQuantStudioParser,
)
from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_structure import Data
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from tests.parsers.appbio_quantstudio.appbio_quantstudio_data import (
    get_data,
    get_data2,
    get_genotyping_data,
    get_genotyping_model,
    get_model,
    get_model2,
    get_rel_std_curve_data,
    get_rel_std_curve_model,
)
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "appbio_quantstudio_example01",
    "appbio_quantstudio_example02",
    "appbio_quantstudio_example03",
)

VENDOR_TYPE = Vendor.APPBIO_QUANTSTUDIO


@pytest.mark.parametrize("output_file", output_files)
def test_parse_appbio_quantstudio_to_asm_schema(output_file: str) -> None:
    test_filepath = f"tests/parsers/appbio_quantstudio/testdata/{output_file}.txt"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, "pcr/BENCHLING/2023/09/qpcr.json")


@pytest.mark.parametrize("output_file", output_files)
def test_parse_appbio_quantstudio_to_asm_contents(output_file: str) -> None:
    test_filepath = f"tests/parsers/appbio_quantstudio/testdata/{output_file}.txt"
    expected_filepath = test_filepath.replace(".txt", ".json")
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)

    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.short
@pytest.mark.parametrize(
    "file_name,data,model",
    [
        ("appbio_quantstudio_test01.txt", get_data(), get_model()),
        ("appbio_quantstudio_test02.txt", get_data2(), get_model2()),
        (
            "appbio_quantstudio_test03.txt",
            get_genotyping_data(),
            get_genotyping_model(),
        ),
        (
            "appbio_quantstudio_test04.txt",
            get_rel_std_curve_data(),
            get_rel_std_curve_model(),
        ),
    ],
)
def test_get_model(file_name: str, data: Data, model: Model) -> None:
    parser = AppBioQuantStudioParser(TimestampParser())
    generated = parser._get_model(data, file_name)
    assert generated == model

tests/parsers/appbio_quantstudio/appbio_quantstudio_parser_test.py
Benchling-Open-Source/allotropy
import pandas as pd

from allotropy.allotrope.models.cell_counter_benchling_2023_09_cell_counter import (
    CellCountingAggregateDocument,
    CellCountingDocumentItem,
    DataProcessingDocument,
    DataSystemDocument,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    ProcessedDataDocument,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueCell,
    TQuantityValueMicrometer,
    TQuantityValueMillionCellsPerMilliliter,
    TQuantityValuePercent,
    TQuantityValueUnitless,
)
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION


def get_filename() -> str:
    return "example.csv"


def get_data() -> pd.DataFrame:
    return pd.DataFrame(
        data=[
            [
                "Vi-CELL",
                "TF-B0256-EXPN-SF",
                "6.0",
                "30.0",
                "2022-03-21 16:56:00",
                "Mammalian",
                "1.0",
                "96.9",
                "0.78",
                "0.75",
                "18.51",
                "18.72",
                "2191.0",
                "2123.0",
                "0.88",
                "0.88",
            ]
        ],
        columns=[
            "Analysis by",
            "Sample ID",
            "Minimum Diameter (μm)",
            "Maximum Diameter (μm)",
            "Analysis date/time",
            "Cell type",
            "Dilution",
            "Viability (%)",
            "Total (x10^6) cells/mL",
            "Viable (x10^6) cells/mL",
            "Average diameter (μm)",
            "Average viable diameter (μm)",
            "Cell count",
            "Viable cells",
            "Average circularity",
            "Average viable circularity",
        ],
    )


def get_model() -> Model:
    data = get_data()
    filename = get_filename()
    sample = data.loc[0]
    return Model(
        cell_counting_aggregate_document=CellCountingAggregateDocument(
            device_system_document=DeviceSystemDocument(
                model_number="Vi-Cell BLU",
            ),
            data_system_document=DataSystemDocument(
                file_name=filename,
                software_name="Vi-Cell BLU",
                ASM_converter_name=ASM_CONVERTER_NAME,
                ASM_converter_version=ASM_CONVERTER_VERSION,
            ),
            cell_counting_document=[
                CellCountingDocumentItem(
                    analyst=sample.get("Analysis by"),  # type: ignore[arg-type]
                    measurement_aggregate_document=MeasurementAggregateDocument(
                        measurement_document=[
                            MeasurementDocumentItem(
                                measurement_identifier="",
                                measurement_time="2022-03-21T16:56:00+00:00",
                                sample_document=SampleDocument(sample_identifier=sample.get("Sample ID")),  # type: ignore[arg-type]
                                device_control_aggregate_document=DeviceControlAggregateDocument(
                                    device_control_document=[
                                        DeviceControlDocumentItem(
                                            device_type="brightfield imager (cell counter)",
                                            detection_type="brightfield",
                                        )
                                    ]
                                ),
                                processed_data_document=ProcessedDataDocument(
                                    data_processing_document=DataProcessingDocument(
                                        cell_type_processing_method=sample.get("Cell type"),  # type: ignore[arg-type]
                                        minimum_cell_diameter=TQuantityValueMicrometer(
                                            value=sample.get("Minimum Diameter (μm)"),  # type: ignore[arg-type]
                                        ),
                                        maximum_cell_diameter=TQuantityValueMicrometer(
                                            value=sample.get("Maximum Diameter (μm)"),  # type: ignore[arg-type]
                                        ),
                                        cell_density_dilution_factor=TQuantityValueUnitless(
                                            value=sample.get("Dilution"),  # type: ignore[arg-type]
                                        ),
                                    ),
                                    viability__cell_counter_=TQuantityValuePercent(
                                        value=sample.get("Viability (%)"),  # type: ignore[arg-type]
                                    ),
                                    viable_cell_density__cell_counter_=TQuantityValueMillionCellsPerMilliliter(
                                        value=sample.get("Viable (x10^6) cells/mL"),  # type: ignore[arg-type]
                                    ),
                                    total_cell_count=TQuantityValueCell(
                                        value=sample.get("Cell count"),  # type: ignore[arg-type]
                                    ),
                                    total_cell_density__cell_counter_=TQuantityValueMillionCellsPerMilliliter(
                                        value=sample.get("Total (x10^6) cells/mL"),  # type: ignore[arg-type]
                                    ),
                                    average_total_cell_diameter=TQuantityValueMicrometer(
                                        value=sample.get("Average diameter (μm)"),  # type: ignore[arg-type]
                                    ),
                                    average_live_cell_diameter__cell_counter_=TQuantityValueMicrometer(
                                        value=sample.get("Average viable diameter (μm)"),  # type: ignore[arg-type]
                                    ),
                                    viable_cell_count=TQuantityValueCell(
                                        value=sample.get("Viable cells"),  # type: ignore[arg-type]
                                    ),
                                    average_total_cell_circularity=TQuantityValueUnitless(
                                        value=sample.get("Average circularity"),  # type: ignore[arg-type]
                                    ),
                                    average_viable_cell_circularity=TQuantityValueUnitless(
                                        value=sample.get("Average viable circularity"),  # type: ignore[arg-type]
                                    ),
                                ),
                            )
                        ],
                    ),
                )
            ],
        ),
    )

tests/parsers/beckman_vi_cell_blu/vi_cell_blu_data.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.parser_factory import Vendor
from allotropy.parsers.beckman_vi_cell_blu.vi_cell_blu_parser import ViCellBluParser
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from tests.parsers.beckman_vi_cell_blu.vi_cell_blu_data import (
    get_data,
    get_filename,
    get_model,
)
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "Beckman_Vi-Cell-BLU_example01",
    "Beckman_Vi-Cell-BLU_example02",
)

VENDOR_TYPE = Vendor.BECKMAN_VI_CELL_BLU
SCHEMA_FILE = "cell-counter/BENCHLING/2023/09/cell-counter.json"


@pytest.mark.parametrize("output_file", output_files)
def test_parse_vi_cell_blu_to_asm_schema_is_valid(output_file: str) -> None:
    test_filepath = f"tests/parsers/beckman_vi_cell_blu//testdata/{output_file}.csv"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, SCHEMA_FILE)


@pytest.mark.parametrize("output_file", output_files)
def test_parse_vi_cell_blu_to_asm_expected_contents(output_file: str) -> None:
    test_filepath = f"tests/parsers/beckman_vi_cell_blu//testdata/{output_file}.csv"
    expected_filepath = f"tests/parsers/beckman_vi_cell_blu/testdata/{output_file}.json"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_contents(allotrope_dict, expected_filepath)


def test_get_model() -> None:
    parser = ViCellBluParser(TimestampParser())
    result = parser._get_model(get_data(), get_filename())

    cell_counting_document_item = (
        result.cell_counting_aggregate_document.cell_counting_document[0]
    )
    measurement_document_item = (
        cell_counting_document_item.measurement_aggregate_document.measurement_document[
            0
        ]
    )
    measurement_document_item.measurement_identifier = ""

    assert result == get_model()

tests/parsers/beckman_vi_cell_blu/vi_cell_blu_parser_test.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parser_factory import Vendor
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "v2.04/Beckman_Vi-Cell-XR_example03_instrumentOutput.xls",
    "v2.06/Beckman_Vi-Cell-XR_example01_instrumentOutput.xlsx",
    "v2.06/Beckman_Vi-Cell-XR_example04_instrumentOutput.xlsx",
    "v2.06/Beckman_Vi-Cell-XR_example05_instrumentOutput.xlsx",
    "v2.06/Beckman_Vi-Cell-XR_example06_instrumentOutput.xlsx",
)

VENDOR_TYPE = Vendor.BECKMAN_VI_CELL_XR
SCHEMA_FILE = "cell-counter/BENCHLING/2023/09/cell-counter.json"


@pytest.mark.parametrize("output_file", output_files)
def test_parse_vi_cell_xr_to_asm_schema_is_valid(output_file: str) -> None:
    test_filepath = f"tests/parsers/beckman_vi_cell_xr/testdata/{output_file}"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)

    validate_schema(allotrope_dict, SCHEMA_FILE)


@pytest.mark.parametrize("output_file", output_files)
def test_parse_vi_cell_xr_to_asm_expected_contents(output_file: str) -> None:
    test_filepath = f"tests/parsers/beckman_vi_cell_xr/testdata/{output_file}"
    target_filename = output_file.replace(".xlsx", ".json").replace(".xls", ".json")
    expected_filepath = f"tests/parsers/beckman_vi_cell_xr/testdata/{target_filename}"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)

    validate_contents(allotrope_dict, expected_filepath)


def test_perse_vi_cell_xr_file_without_required_fields_then_raise() -> None:
    test_filepath = "tests/parsers/beckman_vi_cell_xr/testdata/v2.04/Beckman_Vi-Cell-XR_example02_instrumentOutput.xls"
    with pytest.raises(AllotropeConversionError):
        from_file(test_filepath, VENDOR_TYPE)

tests/parsers/beckman_vi_cell_xr/vi_cell_xr_parser_test.py
Benchling-Open-Source/allotropy
from io import BytesIO

from allotropy.parsers.lines_reader import LinesReader


def get_input_lines() -> list[str]:
    return [
        "data section",
        "col1, col2, col3",
        ", ,",
        ", ,",
        ", ,",
        "",
        "header section",
        "element1",
        "element2",
        "element3",
        "",
        "",
        "information",
        "name",
        "123",
        "",
    ]


def get_input_text() -> str:
    return "\n".join(get_input_lines())


def get_input_stream() -> BytesIO:
    return BytesIO(get_input_text().encode("UTF-8"))


def get_test_reader() -> LinesReader:
    return LinesReader(get_input_stream())


def test_reader_constructure() -> None:
    test_reader = get_test_reader()
    assert test_reader.contents == get_input_text()
    assert test_reader.lines == get_input_lines()
    assert test_reader.n_lines == 16
    assert test_reader.current_line == 0


def test_current_line_exists() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = -1
    assert not test_reader.current_line_exists()

    test_reader.current_line = 1
    assert test_reader.current_line_exists()

    test_reader.current_line = 16
    assert not test_reader.current_line_exists()


def test_reader_get() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 0
    assert test_reader.get() == "data section"

    test_reader.current_line = 6
    assert test_reader.get() == "header section"

    test_reader.current_line = 16
    assert test_reader.get() is None


def test_reader_match() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 6
    assert test_reader.match("^header section")

    test_reader.current_line = 12
    assert test_reader.match("^information")

    test_reader.current_line = 0
    assert not test_reader.match("^BAD PATTERN$")


def test_reader_is_empty() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 1
    assert not test_reader.is_empty()

    test_reader.current_line = 5
    assert test_reader.is_empty()


def test_reader_pop() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 6
    assert test_reader.pop() == "header section"
    assert test_reader.current_line == 7

    test_reader.current_line = 15
    assert test_reader.pop() == ""
    assert test_reader.current_line == 16
    assert test_reader.pop() is None
    assert test_reader.current_line == 16


def test_reader_pop_data() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 10
    assert test_reader.pop_data() == "information"
    assert test_reader.current_line == 13

    test_reader.current_line = 16
    assert test_reader.pop_data() is None
    assert test_reader.current_line == 16


def test_reader_drop_until() -> None:
    test_reader = get_test_reader()
    assert test_reader.drop_until("^header section") == "header section"
    assert test_reader.current_line == 6

    assert test_reader.drop_until("^BAD PATTERN$") is None
    assert test_reader.current_line == 16


def test_reader_drop_empty() -> None:
    test_reader = get_test_reader()
    test_reader.current_line = 10
    assert test_reader.drop_empty() == "information"
    assert test_reader.current_line == 12

    test_reader.current_line = 15
    assert test_reader.drop_empty() is None
    assert test_reader.current_line == 16


def test_reader_drop_until_empty() -> None:
    test_reader = get_test_reader()
    assert test_reader.drop_until_empty() == ""
    assert test_reader.current_line == 5

    test_reader.current_line = 16
    assert test_reader.drop_until_empty() is None
    assert test_reader.current_line == 16


def test_reader_pop_until() -> None:
    input_lines = get_input_lines()
    test_reader = get_test_reader()
    assert list(test_reader.pop_until("^header section")) == input_lines[:6]


def test_reader_pop_until_empty() -> None:
    input_lines = get_input_lines()
    test_reader = get_test_reader()
    assert list(test_reader.pop_until_empty()) == input_lines[:5]

tests/parsers/lines_reader_test.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parser_factory import Vendor
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

VENDOR_TYPE = Vendor.MOLDEV_SOFTMAX_PRO


@pytest.mark.parametrize(
    "test_file,schema_file,expected_file",
    [
        (
            "tests/parsers/moldev_softmax_pro/testdata/fl_kinetic_plates.txt",
            "fluorescence/BENCHLING/2023/09/fluorescence.json",
            "tests/parsers/moldev_softmax_pro/testdata/expected_fl_kinetic.json",
        ),
        (
            "tests/parsers/moldev_softmax_pro/testdata/lum_spectrum_columns.txt",
            "luminescence/BENCHLING/2023/09/luminescence.json",
            "tests/parsers/moldev_softmax_pro/testdata/expected_lum_spectrum.json",
        ),
        (
            "tests/parsers/moldev_softmax_pro/testdata/abs_endpoint_plates.txt",
            "ultraviolet-absorbance/BENCHLING/2023/09/ultraviolet-absorbance.json",
            "tests/parsers/moldev_softmax_pro/testdata/expected_abs_endpoint.json",
        ),
    ],
)
def test_to_allotrope(test_file: str, schema_file: str, expected_file: str) -> None:
    allotrope_dict = from_file(test_file, VENDOR_TYPE)
    validate_schema(allotrope_dict, schema_file)
    validate_contents(allotrope_dict, expected_file)
    assert (
        len(allotrope_dict["measurement aggregate document"]["measurement document"])
        == 96
    )


def test_handles_unrecognized_read_mode() -> None:
    with pytest.raises(
        AllotropeConversionError, match="unrecognized read mode Time Resolved"
    ):
        from_file(
            "tests/parsers/moldev_softmax_pro/testdata/trf_well_scan_plates.txt",
            VENDOR_TYPE,
        )

tests/parsers/moldev_softmax_pro/to_allotrope_test.py
Benchling-Open-Source/allotropy
from io import StringIO

from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteAggregateDocument,
    AnalyteDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueGramPerLiter,
    TNullableQuantityValueMillimeterOfMercury,
    TNullableQuantityValueMillimolePerLiter,
    TNullableQuantityValuePercent,
    TNullableQuantityValuePH,
)
from allotropy.parsers.novabio_flex2.constants import PROPERTY_MAPPINGS
from allotropy.parsers.novabio_flex2.novabio_flex2_structure import (
    Analyte,
    Data,
    Sample,
    SampleList,
    Title,
)


def get_input_title() -> str:
    return "SampleResults2022-06-28_142558.csv"


def get_input_stream() -> StringIO:
    data = {
        "Date & Time": "6-24-2022  14:34:52",
        "Sample ID": "BP_R10_KP_008_D0",
        "Sample Type": "Spent Media",
        "Gln": "1.83",
        "Glu": "0.33",
        "Gluc": "2.65",
        "Lac": "0.18",
        "NH4+": "0.48",
        "Na+": "151.6",
        "K+": "4.22",
        "Ca++": "0.82",
        "pH": "7.400",
        "PO2": "191.6",
        "PCO2": "46.8",
        "Vessel ID": "3L Bioreactor",
        "Batch ID": "KP_008",
        "Cell Type": "Workhog",
        "Vessel Temperature (°C)": "37.0",
        "Vessel Pressure (psi)": "30.0",
        "Sparging O2%": "320.9",
        "Chemistry Dilution Ratio": "1:1",
        "pH @ Temp": "7.400",
        "PCO2 @ Temp": "46.8",
        "PO2 @ Temp": "191.6",
        "O2 Saturation": "100.0",
        "CO2 Saturation": "6.6",
        "HCO3": "29.3",
        "Chemistry Flow Time": "6.67",
        "pH / Gas Flow Time": "3.65",
        "Tray Location": "",
        "Time In Tray": "",
        "Sample Time": "24-6-2022  14:34:52",
        "Operator": "BioprocessingTeam",
        "": "",
    }
    return StringIO("\n".join([",".join(data.keys()), ",".join(data.values())]))


def get_data() -> Data:
    return Data(
        title=Title(
            processing_time="2022-06-28 142558",
            device_identifier=None,
        ),
        sample_list=SampleList(
            analyst="BioprocessingTeam",
            samples=[
                Sample(
                    identifier="BP_R10_KP_008_D0",
                    role_type="Spent Media",
                    measurement_time="2022-06-24T14:34:52",
                    batch_identifier="KP_008",
                    analytes=sorted(
                        [
                            Analyte.create("Gln", 1.83),
                            Analyte.create("Glu", 0.33),
                            Analyte.create("Gluc", 2.65),
                            Analyte.create("Lac", 0.18),
                            Analyte.create("NH4+", 0.48),
                            Analyte.create("Na+", 151.6),
                            Analyte.create("K+", 4.22),
                            Analyte.create("Ca++", 0.82),
                            Analyte.create("HCO3", 29.3),
                        ]
                    ),
                    properties={
                        "co2_saturation": PROPERTY_MAPPINGS["co2_saturation"]["cls"](
                            value=6.6
                        ),
                        "o2_saturation": PROPERTY_MAPPINGS["o2_saturation"]["cls"](
                            value=100.0
                        ),
                        "pH": PROPERTY_MAPPINGS["pH"]["cls"](value=7.4),
                        "po2": PROPERTY_MAPPINGS["po2"]["cls"](value=191.6),
                        "pco2": PROPERTY_MAPPINGS["pco2"]["cls"](value=46.8),
                    },
                )
            ],
        ),
    )


def get_model() -> Model:
    return Model(
        measurement_aggregate_document=MeasurementAggregateDocument(
            measurement_identifier="",
            analyst="BioprocessingTeam",
            device_system_document=DeviceSystemDocument(
                device_identifier=None,
                model_number="NovaBio Flex2",
                device_serial_number=None,
            ),
            measurement_document=[
                MeasurementDocumentItem(
                    sample_document=SampleDocument(
                        sample_identifier="BP_R10_KP_008_D0",
                        batch_identifier="KP_008",
                        sample_role_type="Spent Media",
                    ),
                    measurement_time="2022-06-24T14:34:52+00:00",
                    analyte_aggregate_document=AnalyteAggregateDocument(
                        analyte_document=[
                            AnalyteDocumentItem(
                                analyte_name="ammonium",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=0.48,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="bicarbonate",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=29.3,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="calcium",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=0.82,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="glucose",
                                molar_concentration=TNullableQuantityValueGramPerLiter(
                                    value=2.65,
                                    has_statistic_datum_role=None,
                                    unit="g/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="glutamate",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=0.33,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="glutamine",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=1.83,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="lactate",
                                molar_concentration=TNullableQuantityValueGramPerLiter(
                                    value=0.18,
                                    has_statistic_datum_role=None,
                                    unit="g/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="potassium",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=4.22,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="sodium",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=151.6,
                                    has_statistic_datum_role=None,
                                    unit="mmol/L",
                                    field_type=None,
                                ),
                            ),
                        ]
                    ),
                    pco2=TNullableQuantityValueMillimeterOfMercury(
                        value=46.8,
                        has_statistic_datum_role=None,
                        unit="mmHg",
                        field_type=None,
                    ),
                    co2_saturation=TNullableQuantityValuePercent(
                        value=6.6,
                        has_statistic_datum_role=None,
                        unit="%",
                        field_type=None,
                    ),
                    po2=TNullableQuantityValueMillimeterOfMercury(
                        value=191.6,
                        has_statistic_datum_role=None,
                        unit="mmHg",
                        field_type=None,
                    ),
                    o2_saturation=TNullableQuantityValuePercent(
                        value=100.0,
                        has_statistic_datum_role=None,
                        unit="%",
                        field_type=None,
                    ),
                    optical_density=None,
                    pH=TNullableQuantityValuePH(
                        value=7.4,
                        has_statistic_datum_role=None,
                        unit="pH",
                        field_type=None,
                    ),
                    osmolality=None,
                    viability__cell_counter_=None,
                    total_cell_density__cell_counter_=None,
                    viable_cell_density__cell_counter_=None,
                    average_live_cell_diameter__cell_counter_=None,
                    average_total_cell_diameter__cell_counter_=None,
                    total_cell_diameter_distribution__cell_counter_=None,
                    viable_cell_count__cell_counter_=None,
                    total_cell_count__cell_counter_=None,
                )
            ],
            data_processing_time="2022-06-28T14:25:58+00:00",
        ),
        manifest="http://purl.allotrope.org/manifests/cell-culture-analyzer/BENCHLING/2023/09/cell-culture-analyzer.manifest",
    )

tests/parsers/novabio_flex2/novabio_flex2_data.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.parser_factory import Vendor
from allotropy.parsers.novabio_flex2.novabio_flex2_parser import NovaBioFlexParser
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from tests.parsers.novabio_flex2.novabio_flex2_data import get_data, get_model
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "SampleResultsDEVICE1232021-02-18_104838",
    "SampleResults2022-06-28_142558",
)

VENDOR_TYPE = Vendor.NOVABIO_FLEX2


@pytest.mark.parametrize("output_file", output_files)
def test_parse_novabio_flex_to_asm(output_file: str) -> None:
    test_filepath = f"tests/parsers/novabio_flex2/testdata/{output_file}.csv"
    expected_filepath = f"tests/parsers/novabio_flex2/testdata/{output_file}.json"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(
        allotrope_dict,
        "cell-culture-analyzer/BENCHLING/2023/09/cell-culture-analyzer.json",
    )
    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.short
def test_get_model() -> None:
    parser = NovaBioFlexParser(TimestampParser())
    model = parser._get_model(get_data())

    if model.measurement_aggregate_document:
        model.measurement_aggregate_document.measurement_identifier = ""

    assert model == get_model()

tests/parsers/novabio_flex2/novabio_flex2_parser_test.py
Benchling-Open-Source/allotropy
from typing import Optional

import pandas as pd
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.parsers.novabio_flex2.constants import (
    MOLAR_CONCENTRATION_CLS_BY_UNIT,
    PROPERTY_MAPPINGS,
)
from allotropy.parsers.novabio_flex2.novabio_flex2_structure import (
    Analyte,
    Data,
    Sample,
    SampleList,
    Title,
)
from tests.parsers.novabio_flex2.novabio_flex2_data import (
    get_data,
    get_input_stream,
    get_input_title,
)


@pytest.mark.parametrize(
    "filename,processing_time,device_identifier",
    [
        ("SampleResults2022-06-28_142558.csv", "2022-06-28 142558", None),
        (
            "SampleResultsT26918070C2021-02-18_104838.csv",
            "2021-02-18 104838",
            "T26918070C",
        ),
    ],
)
@pytest.mark.short
def test_create_title(
    filename: str, processing_time: str, device_identifier: Optional[str]
) -> None:
    title = Title.create(filename)
    assert title.processing_time == processing_time
    assert title.device_identifier == device_identifier


@pytest.mark.parametrize(
    "filename",
    (
        "invalid_filename",  # no extension
        "T26918070C2021-02-18_104838.csv",  # filename does not start with SampleResults
        "SampleResults2021-02-18_104838T26918070C.csv",  # wrong order of timestamp and identifier
    ),
)
@pytest.mark.short
def test_create_title_invalid_filename(filename: str) -> None:
    with pytest.raises(AllotropeConversionError):
        Title.create(filename)


@pytest.mark.short
def test_create_analyte() -> None:
    nh4_analyte = Analyte.create("NH4+", 100)
    assert nh4_analyte.name == "ammonium"
    assert nh4_analyte.molar_concentration == MOLAR_CONCENTRATION_CLS_BY_UNIT["mmol/L"](
        value=100
    )

    gluc_analyte = Analyte.create("Gluc", 1.1)
    assert gluc_analyte.name == "glucose"
    assert gluc_analyte.molar_concentration == MOLAR_CONCENTRATION_CLS_BY_UNIT["g/L"](
        value=1.1
    )


@pytest.mark.short
def test_create_invalid_analyte() -> None:
    with pytest.raises(AllotropeConversionError):
        Analyte.create("FAKE", 100)


@pytest.mark.short
def test_create_sample() -> None:
    data = {
        "Sample ID": "BP_R10_KP_008_D0",
        "Sample Type": "Spent Media",
        "Date & Time": pd.Timestamp("2022-06-24 14:34:52"),
        "Batch ID": "KP_008",
        "Operator": "Kermit",
        "Gln": 1.83,
        "Ca++": 0.82,
        "O2 Saturation": 100.0,
        "CO2 Saturation": 0,  # zero here makes sure we allow falsey values
        "Unmapped column": 5,
    }
    sample = Sample.create(pd.Series(data=data))

    assert sample.identifier == "BP_R10_KP_008_D0"
    assert sample.role_type == "Spent Media"
    assert sample.measurement_time == "2022-06-24T14:34:52"
    assert sample.batch_identifier == "KP_008"
    assert sorted(sample.analytes) == sorted(
        [
            Analyte.create("Gln", 1.83),
            Analyte.create("Ca++", 0.82),
        ]
    )
    assert sample.properties == {
        "co2_saturation": PROPERTY_MAPPINGS["co2_saturation"]["cls"](value=0),
        "o2_saturation": PROPERTY_MAPPINGS["o2_saturation"]["cls"](value=100.0),
    }


@pytest.mark.short
def test_sample_sorting() -> None:
    analytes = [
        Analyte.create("Gln", 1.83),
        Analyte.create("Glu", 0.33),
        Analyte.create("Gluc", 2.65),
        Analyte.create("Lac", 0.18),
        Analyte.create("NH4+", 0.48),
    ]
    assert sorted(analytes) == [
        Analyte.create("NH4+", 0.48),
        Analyte.create("Gluc", 2.65),
        Analyte.create("Glu", 0.33),
        Analyte.create("Gln", 1.83),
        Analyte.create("Lac", 0.18),
    ]


@pytest.mark.short
def test_create_sample_list() -> None:
    sample_list = SampleList.create(
        pd.DataFrame(
            {
                "Sample ID": ["SAMPLE_1", "SAMPLE_2"],
                "Sample Type": ["Spent Media", "Spent Media"],
                "Date & Time": [
                    pd.Timestamp("2022-06-24 14:34:52"),
                    pd.Timestamp("2022-06-24 14:34:52"),
                ],
                "Operator": ["Kermit", "Other"],
            }
        )
    )

    assert sample_list.analyst == "Kermit"
    assert len(sample_list.samples) == 2
    assert sample_list.samples[0].identifier == "SAMPLE_1"
    assert sample_list.samples[1].identifier == "SAMPLE_2"


@pytest.mark.short
def test_create_sample_list_invalid_no_samples() -> None:
    with pytest.raises(AllotropeConversionError):
        SampleList.create(pd.DataFrame({}))


@pytest.mark.short
def test_create_sample_list_invalid_no_analyst() -> None:
    with pytest.raises(AllotropeConversionError):
        SampleList.create(
            pd.DataFrame(
                {
                    "Sample ID": ["SAMPLE_1", "SAMPLE_2"],
                    "Sample Type": ["Spent Media", "Spent Media"],
                    "Date & Time": [
                        pd.Timestamp("2022-06-24 14:34:52"),
                        pd.Timestamp("2022-06-24 14:34:52"),
                    ],
                }
            )
        )


@pytest.mark.short
def test_create_data() -> None:
    assert Data.create(get_input_stream(), get_input_title()) == get_data()

tests/parsers/novabio_flex2/novabio_flex2_structure_test.py
Benchling-Open-Source/allotropy
from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    ContainerType,
    DeviceControlAggregateDocument,
    DeviceControlDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    ScanPositionSettingPlateReader,
)
from allotropy.allotrope.models.shared.components.plate_reader import (
    ProcessedDataAggregateDocument,
    ProcessedDataDocumentItem,
    SampleDocument,
    SampleRoleType,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TQuantityValueDegreeCelsius,
    TQuantityValueMillimeter,
    TQuantityValueNanometer,
    TQuantityValueNumber,
)
from allotropy.parsers.perkin_elmer_envision.elmer_envision_structure import (
    BasicAssayInfo,
    Data,
    Filter,
    Instrument,
    Labels,
    Plate,
    PlateInfo,
    PlateMap,
    Result,
)


def get_data() -> Data:
    return Data(
        plates=[
            Plate(
                plate_info=PlateInfo(
                    number="1",
                    barcode="Plate 1",
                    emission_filter_id="1st",
                    measurement_time="10/13/2022 3:08:06 PM",
                    measured_height=11.9,
                    chamber_temperature_at_start=23.17,
                ),
                results=[Result(col="A", row="01", value=31441)],
            ),
            Plate(
                plate_info=PlateInfo(
                    number="1",
                    barcode="Plate 1",
                    emission_filter_id="2nd",
                    measurement_time="10/13/2022 3:08:06 PM",
                    measured_height=11.9,
                    chamber_temperature_at_start=23.17,
                ),
                results=[Result(col="A", row="01", value=80368)],
            ),
        ],
        basic_assay_info=BasicAssayInfo("100302", "3134"),
        number_of_wells=96.0,
        plate_maps={
            "1": PlateMap(
                plate_n="1",
                group_n="1",
                sample_role_type_mapping={
                    "01": {"A": SampleRoleType.undefined_sample_role}
                },
            ),
        },
        labels=Labels(
            label="AC HTRF Laser [Eu]",
            excitation_filter=Filter("UV2 (TRF) 320", 320, 75),
            emission_filters={
                "1st": Filter("APC 665", 665, 75),
                "2nd": Filter("Cy5 620", 620, 10),
            },
            scan_position_setting=ScanPositionSettingPlateReader.top_scan_position__plate_reader_,
            number_of_flashes=50,
            detector_gain_setting="2",
        ),
        instrument=Instrument(
            serial_number="1050209",
            nickname="EnVision",
        ),
    )


def get_model() -> Model:
    return Model(
        manifest="http://purl.allotrope.org/manifests/fluorescence/BENCHLING/2023/09/fluorescence.manifest",
        measurement_aggregate_document=MeasurementAggregateDocument(
            measurement_identifier="",
            measurement_time="2022-10-13T15:08:06+00:00",
            analytical_method_identifier="100302",
            experimental_data_identifier="3134",
            container_type=ContainerType.well_plate,
            plate_well_count=TQuantityValueNumber(value=96.0),
            device_system_document=DeviceSystemDocument(
                model_number="1050209",
                device_identifier="EnVision",
            ),
            measurement_document=[
                MeasurementDocumentItem(
                    device_control_aggregate_document=DeviceControlAggregateDocument(
                        device_control_document=[
                            DeviceControlDocumentItem(
                                device_type="fluorescence detector",
                                shaking_configuration_description=None,
                                detector_distance_setting__plate_reader_=TQuantityValueMillimeter(
                                    value=11.9,
                                    has_statistic_datum_role=None,
                                    unit="mm",
                                    field_type=None,
                                ),
                                integration_time=None,
                                number_of_averages=TQuantityValueNumber(
                                    value=50.0,
                                    has_statistic_datum_role=None,
                                    unit="#",
                                    field_type=None,
                                ),
                                detector_gain_setting="2",
                                scan_position_setting__plate_reader_=ScanPositionSettingPlateReader.top_scan_position__plate_reader_,
                                detector_carriage_speed_setting=None,
                                detector_wavelength_setting=TQuantityValueNanometer(
                                    value=665.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                detector_bandwidth_setting=TQuantityValueNanometer(
                                    value=75.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                excitation_bandwidth_setting=TQuantityValueNanometer(
                                    value=75.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                excitation_wavelength_setting=TQuantityValueNanometer(
                                    value=320.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                wavelength_filter_cutoff_setting=None,
                                field_index=None,
                            )
                        ]
                    ),
                    sample_document=SampleDocument(
                        well_location_identifier="A01",
                        sample_identifier=None,
                        batch_identifier=None,
                        sample_role_type=SampleRoleType.undefined_sample_role,
                        plate_barcode="Plate 1",
                    ),
                    data_cube=None,
                    compartment_temperature=TQuantityValueDegreeCelsius(
                        value=23.17,
                        has_statistic_datum_role=None,
                        unit="degC",
                        field_type=None,
                    ),
                    processed_data_aggregate_document=ProcessedDataAggregateDocument(
                        processed_data_document=[
                            ProcessedDataDocumentItem(
                                processed_data=31441,
                                data_format_specification_type=None,
                                data_processing_description="processed data",
                            )
                        ]
                    ),
                    mass_concentration=None,
                ),
                MeasurementDocumentItem(
                    device_control_aggregate_document=DeviceControlAggregateDocument(
                        device_control_document=[
                            DeviceControlDocumentItem(
                                device_type="fluorescence detector",
                                shaking_configuration_description=None,
                                detector_distance_setting__plate_reader_=TQuantityValueMillimeter(
                                    value=11.9,
                                    has_statistic_datum_role=None,
                                    unit="mm",
                                    field_type=None,
                                ),
                                integration_time=None,
                                number_of_averages=TQuantityValueNumber(
                                    value=50.0,
                                    has_statistic_datum_role=None,
                                    unit="#",
                                    field_type=None,
                                ),
                                detector_gain_setting="2",
                                scan_position_setting__plate_reader_=ScanPositionSettingPlateReader.top_scan_position__plate_reader_,
                                detector_carriage_speed_setting=None,
                                detector_wavelength_setting=TQuantityValueNanometer(
                                    value=620.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                detector_bandwidth_setting=TQuantityValueNanometer(
                                    value=10.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                excitation_bandwidth_setting=TQuantityValueNanometer(
                                    value=75.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                excitation_wavelength_setting=TQuantityValueNanometer(
                                    value=320.0,
                                    has_statistic_datum_role=None,
                                    unit="nm",
                                    field_type=None,
                                ),
                                wavelength_filter_cutoff_setting=None,
                                field_index=None,
                            )
                        ]
                    ),
                    sample_document=SampleDocument(
                        well_location_identifier="A01",
                        sample_identifier=None,
                        batch_identifier=None,
                        sample_role_type=SampleRoleType.undefined_sample_role,
                        plate_barcode="Plate 1",
                    ),
                    data_cube=None,
                    compartment_temperature=TQuantityValueDegreeCelsius(
                        value=23.17,
                        has_statistic_datum_role=None,
                        unit="degC",
                        field_type=None,
                    ),
                    processed_data_aggregate_document=ProcessedDataAggregateDocument(
                        processed_data_document=[
                            ProcessedDataDocumentItem(
                                processed_data=80368,
                                data_format_specification_type=None,
                                data_processing_description="processed data",
                            )
                        ]
                    ),
                    mass_concentration=None,
                ),
            ],
        ),
    )

tests/parsers/perkin_elmer_envision/elmer_envision_data.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.parser_factory import Vendor
from allotropy.parsers.perkin_elmer_envision.elmer_envision_parser import (
    ElmerEnvisionParser,
)
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from tests.parsers.perkin_elmer_envision.elmer_envision_data import get_data, get_model
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "PE_Envision_example01",
    "PE_Envision_example02",
    "PE_Envision_example03",
)

VENDOR_TYPE = Vendor.PERKIN_ELMER_ENVISION


@pytest.mark.parametrize("output_file", output_files)
def test_parse_elmer_envision_to_asm(output_file: str) -> None:
    test_filepath = f"tests/parsers/perkin_elmer_envision/testdata/{output_file}.csv"
    expected_filepath = (
        f"tests/parsers/perkin_elmer_envision/testdata/{output_file}.json"
    )
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, "fluorescence/BENCHLING/2023/09/fluorescence.json")
    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.short
def test_get_model() -> None:
    parser = ElmerEnvisionParser(TimestampParser())
    model = parser._get_model(get_data())

    if model.measurement_aggregate_document:
        model.measurement_aggregate_document.measurement_identifier = ""

    assert model == get_model()

tests/parsers/perkin_elmer_envision/elmer_envision_parser_test.py
Benchling-Open-Source/allotropy
from io import StringIO

from allotropy.allotrope.models.fluorescence_benchling_2023_09_fluorescence import (
    ScanPositionSettingPlateReader,
)
from allotropy.allotrope.models.shared.components.plate_reader import SampleRoleType
from allotropy.parsers.lines_reader import LinesReader
from allotropy.parsers.perkin_elmer_envision.elmer_envision_structure import (
    BasicAssayInfo,
    create_plate_maps,
    Filter,
    Instrument,
    Labels,
    Plate,
    PlateInfo,
    PlateMap,
    PlateType,
    Result,
)


def get_reader_from_lines(lines: list[str]) -> LinesReader:
    return LinesReader(StringIO("\n".join(lines)))


def test_create_plate_info() -> None:
    reader = get_reader_from_lines(
        [
            "Plate information",
            "Plate,Repeat,Barcode,Measured height,Chamber temperature at start,Measinfo,Kinetics,Measurement date,",
            '1,1,"=""BAR_123""",11.9,23.17,De=1st Ex=Top Em=Top Wdw=1 (14),0,10/13/2022 3:08:06 PM,',
        ]
    )

    expected = PlateInfo(
        number="1",
        barcode="BAR_123",
        emission_filter_id="1st",
        measurement_time="10/13/2022 3:08:06 PM",
        measured_height=11.9,
        chamber_temperature_at_start=23.17,
    )

    assert PlateInfo.create(reader) == expected


def test_create_plate_info_default_barcode() -> None:
    reader = get_reader_from_lines(
        [
            "Plate information",
            "Plate,Repeat,Barcode,Measured height,Chamber temperature at start,Measinfo,Kinetics,Measurement date,",
            '1,1,"=""""",11.9,23.17,De=1st Ex=Top Em=Top Wdw=1 (14),0,10/13/2022 3:08:06 PM,',
        ]
    )
    plate = PlateInfo.create(reader)
    assert plate and plate.barcode == "Plate 1"
    reader = get_reader_from_lines(
        [
            "Plate information",
            "Plate,Repeat,Barcode,Measured height,Chamber temperature at start,Measinfo,Kinetics,Measurement date,",
            "1,1,,11.9,23.17,De=1st Ex=Top Em=Top Wdw=1 (14),0,10/13/2022 3:08:06 PM,",
        ]
    )
    plate = PlateInfo.create(reader)
    assert plate and plate.barcode == "Plate 1"


def test_create_result() -> None:
    reader = get_reader_from_lines(
        [
            "Results",
            "1,2,,",
            "3,4,,",
            ",,,",
            "",
        ]
    )

    expected = [
        Result(col="A", row="01", value=1),
        Result(col="A", row="02", value=2),
        Result(col="B", row="01", value=3),
        Result(col="B", row="02", value=4),
    ]
    assert Result.create(reader) == expected

    # After no more results, return empty list.
    assert Result.create(reader) == []


def test_create_result_early_return() -> None:
    reader = get_reader_from_lines(
        [
            "Plate information",
            "1,2,,",
            "3,4,,",
        ]
    )
    assert Result.create(reader) == []


def test_create_plates() -> None:
    reader = get_reader_from_lines(
        [
            "Plate information",
            "Plate,Repeat,Barcode,Measured height,Chamber temperature at start,Measinfo,Kinetics,Measurement date,",
            "2,1,,1.1,14.5,De=2nd Ex=Top Em=Top Wdw=1 (14),0,10/13/2022 3:08:06 PM,",
            "",
            "Background information",
            "junk",
            "",
            "Calculated results",
            "junk",
            "",
            "Results",
            "6,,7,",
            ",,,",
            ",8,,",
        ]
    )

    expected = [
        Plate(
            plate_info=PlateInfo(
                number="2",
                barcode="Plate 2",
                emission_filter_id="2nd",
                measurement_time="10/13/2022 3:08:06 PM",
                measured_height=1.1,
                chamber_temperature_at_start=14.5,
            ),
            results=[
                Result(col="A", row="01", value=6),
                Result(col="A", row="03", value=7),
                Result(col="C", row="02", value=8),
            ],
        )
    ]

    assert Plate.create(reader) == expected


def test_create_basic_assay_info() -> None:
    reader = get_reader_from_lines(
        [
            "Basic assay information",
            "Assay ID: ,,,,3134",
            "Assay Started: ,,,,10/13/2022 3:06:23 PM",
            "Assay Finished: ,,,,10/13/2022 3:08:15 PM",
            "Assay Exported: ,,,,10/13/2022 3:08:15 PM",
            "Protocol ID: ,,,,100302",
            "Protocol Name: ,,,,HTRF LASER Eu 665/620",
            "Serial#: ,,,,='1050209'",
        ]
    )

    expected = BasicAssayInfo(
        protocol_id="100302",
        assay_id="3134",
    )

    assert BasicAssayInfo.create(reader) == expected


def test_create_plate_type() -> None:
    reader = get_reader_from_lines(
        [
            "Protocol information",
            "Protocol:",
            "Protocol name,,,,protocol",
            "property,,,,1",
            "",
            "Plate type:",
            "Name of the plate type,,,,plate",
            "Number of the wells in the plate,,,,2",
        ]
    )

    expected = PlateType(number_of_wells=2)

    assert PlateType.create(reader) == expected


def test_create_plate_maps() -> None:
    reader = get_reader_from_lines(
        [
            "Platemap:",
            "Plate,,,,1",
            "Group,,,,1",
            "",
            ",01,02,03",
            "A,- ,'',",
            "B,nan,LH01,",
            "C,STD,,S01",
            "",
            "- - Undefined",
            "",
            "",
            "",
            "Plate,,,,2",
            "Group,,,,3",
            "",
            ",01,02,03,04",
            "A,,'',,BL...",
            "B,nan,nan,,",
            "C,,,,",
            "",
            "- - Undefined",
            "",
        ]
    )

    expected = {
        "1": PlateMap(
            plate_n="1",
            group_n="1",
            sample_role_type_mapping={
                "01": {
                    "A": SampleRoleType.undefined_sample_role,
                    "C": SampleRoleType.standard_sample_role,
                },
                "02": {"B": SampleRoleType.control_sample_role},
                "03": {"C": SampleRoleType.sample_role},
            },
        ),
        "2": PlateMap(
            plate_n="2",
            group_n="3",
            sample_role_type_mapping={
                "04": {"A": SampleRoleType.blank_role},
            },
        ),
    }

    assert create_plate_maps(reader) == expected


def test_create_labels() -> None:
    reader = get_reader_from_lines(
        [
            "Labels",
            "FAKE_INSTRUMENT,,,,4000013",
            "Exc. filter,,,,Filter_1",
            "Using of excitation filter,,,,Top",
            "2nd exc. filter,,,,N/A",
            "Ems. filter,,,,Filter_2",
            "2nd ems. filter,,,,N/A",
            "Measurement height,,,,5 mm",
            "Using of emission filter,,,,Top",
            "Number of flashes,,,,10",
            "Number of flashes integrated,,,,1",
            "Reference AD gain,,,,2",
            "Last edited,,,,4/6/2022 5:55:00 PM",
            "Factory preset,,,,No",
            "",
            "",
            "Filters:",
            "Filter_1,,,,102",
            "Description,,,,X485 CWL=485nm BW=14nm Tmin=60%",
            "Factory preset,,,,Yes",
            "",
            "",
            "Filter_2,,,,102",
            "Description,,,,X485 CWL=520nm BW=20nm Tmin=60%",
            "Factory preset,,,,Yes",
        ]
    )

    expected = Labels(
        label="FAKE_INSTRUMENT",
        excitation_filter=Filter(
            name="Filter_1",
            wavelength=485,
            bandwidth=14,
        ),
        emission_filters={
            "1st": Filter(
                name="Filter_2",
                wavelength=520,
                bandwidth=20,
            ),
            "2nd": None,
        },
        scan_position_setting=ScanPositionSettingPlateReader.top_scan_position__plate_reader_,
        number_of_flashes=10.0,
        detector_gain_setting="2",
    )

    assert Labels.create(reader) == expected


def test_create_instrument() -> None:
    reader = get_reader_from_lines(
        [
            "Instrument:",
            "Serial number,,,,1",
            "Nickname,,,,bobby",
        ]
    )

    expected = Instrument(
        serial_number="1",
        nickname="bobby",
    )

    assert Instrument.create(reader) == expected

tests/parsers/perkin_elmer_envision/elmer_envision_structure_test.py
Benchling-Open-Source/allotropy
from io import StringIO

import pandas as pd

from allotropy.allotrope.models.cell_culture_analyzer_benchling_2023_09_cell_culture_analyzer import (
    AnalyteAggregateDocument,
    AnalyteDocumentItem,
    DeviceSystemDocument,
    MeasurementAggregateDocument,
    MeasurementDocumentItem,
    Model,
    SampleDocument,
)
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueMillimolePerLiter,
)
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_reader import CedexBiohtReader
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_structure import (
    Analyte,
    AnalyteList,
    Data,
    Sample,
    Title,
)


def get_data_stream() -> StringIO:
    title = [
        "0",
        "2021-06-01 13:04:06",
        "#ARC-FILE#",
        "1.1a",
        "2021-05-01",
        "2021-06-01",
        "CEDEX BIO HT",
        "620103",
        "6.0.0.1905 (1905)",
        "ADMIN",
    ]

    body = [
        [
            "40",
            "2021-05-20 16:55:51",
            "PPDTEST1",
            "",
            "SAM",
            "        ",
            "GLN2B",
            "        ",
            "mmol/L",
            " ",
            "  2.45",
            "  0.17138",
            "R",
        ],
        [
            "40",
            "2021-05-20 16:55:51",
            "PPDTEST1",
            "",
            "Sample",
            "        ",
            "NH3LB",
            "        ",
            "mmol/L",
            " ",
            "  1.846",
            "0",
            "R",
        ],
    ]
    title_text = "\t".join(title)
    body_text = "\n".join(["\t".join(row) for row in body])
    return StringIO("\n".join([title_text, body_text]))


def get_reader() -> CedexBiohtReader:
    return CedexBiohtReader(get_data_stream())


def get_reader_title() -> pd.Series:
    return pd.Series(
        {
            "row type": 0,
            "data processing time": "2021-06-01 13:04:06",
            "col3": "#ARC-FILE#",
            "col4": "1.1a",
            "col5": "2021-05-01",
            "col6": "2021-06-01",
            "model number": "CEDEX BIO HT",
            "device serial number": 620103,
            "col9": "6.0.0.1905 (1905)",
            "analyst": "ADMIN",
        }
    )


def get_reader_samples() -> pd.DataFrame:
    return pd.DataFrame(
        columns=[
            "row type",
            "measurement time",
            "sample identifier",
            "batch identifier",
            "sample role type",
            "col6",
            "analyte name",
            "col8",
            "concentration unit",
            "flag",
            "concentration value",
            "col12",
            "col13",
        ],
        data=[
            [
                40,
                "2021-05-20 16:55:51",
                "PPDTEST1",
                "",
                "Sample",
                "        ",
                "glutamine",
                "        ",
                "mmol/L",
                " ",
                2.45,
                0.17138,
                "R",
            ],
            [
                40,
                "2021-05-20 16:55:51",
                "PPDTEST1",
                "",
                "Sample",
                "        ",
                "ammonia",
                "        ",
                "mmol/L",
                " ",
                1.846,
                0,
                "R",
            ],
        ],
    )


def get_data() -> Data:
    return Data(
        title=Title(
            data_processing_time="2021-06-01 13:04:06",
            analyst="ADMIN",
            model_number="CEDEX BIO HT",
            device_serial_number="620103",
        ),
        samples=[
            Sample(
                name="PPDTEST1",
                role_type="Sample",
                measurement_time="2021-05-20 16:55:51",
                analyte_list=AnalyteList(
                    analytes=[
                        Analyte("ammonia", 1.846, "mmol/L"),
                        Analyte("glutamine", 2.45, "mmol/L"),
                    ],
                    molar_concentration_dict={
                        "ammonia": [
                            TNullableQuantityValueMillimolePerLiter(
                                value=1.846,
                            )
                        ],
                        "glutamine": [
                            TNullableQuantityValueMillimolePerLiter(
                                value=2.45,
                            )
                        ],
                    },
                    molar_concentration_nans={},
                    non_aggregrable_dict={},
                    non_aggregable_nans={},
                    num_measurement_docs=1,
                ),
            )
        ],
    )


def get_model() -> Model:
    return Model(
        measurement_aggregate_document=MeasurementAggregateDocument(
            measurement_identifier="",
            analyst="ADMIN",
            device_system_document=DeviceSystemDocument(
                device_identifier=None,
                model_number="CEDEX BIO HT",
                device_serial_number="620103",
            ),
            measurement_document=[
                MeasurementDocumentItem(
                    sample_document=SampleDocument(
                        sample_identifier="PPDTEST1",
                        batch_identifier=None,
                        sample_role_type="Sample",
                    ),
                    measurement_time="2021-05-20T16:55:51+00:00",
                    analyte_aggregate_document=AnalyteAggregateDocument(
                        analyte_document=[
                            AnalyteDocumentItem(
                                analyte_name="ammonia",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=1.846,
                                ),
                            ),
                            AnalyteDocumentItem(
                                analyte_name="glutamine",
                                molar_concentration=TNullableQuantityValueMillimolePerLiter(
                                    value=2.45,
                                ),
                            ),
                        ]
                    ),
                    pco2=None,
                    co2_saturation=None,
                    po2=None,
                    o2_saturation=None,
                    optical_density=None,
                    pH=None,
                    osmolality=None,
                    viability__cell_counter_=None,
                    total_cell_density__cell_counter_=None,
                    viable_cell_density__cell_counter_=None,
                    average_live_cell_diameter__cell_counter_=None,
                    average_total_cell_diameter__cell_counter_=None,
                    total_cell_diameter_distribution__cell_counter_=None,
                    viable_cell_count__cell_counter_=None,
                    total_cell_count__cell_counter_=None,
                )
            ],
            data_processing_time="2021-06-01T13:04:06+00:00",
        ),
        manifest="http://purl.allotrope.org/manifests/cell-culture-analyzer/BENCHLING/2023/09/cell-culture-analyzer.manifest",
    )

tests/parsers/roche_cedex_bioht/cedex_bioht_data.py
Benchling-Open-Source/allotropy
import pytest

from allotropy.parser_factory import Vendor
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_parser import CedexBiohtParser
from allotropy.parsers.utils.timestamp_parser import TimestampParser
from tests.parsers.roche_cedex_bioht.cedex_bioht_data import get_data, get_model
from tests.parsers.test_utils import from_file, validate_contents, validate_schema

output_files = (
    "cedex_bioht_example01",
    "cedex_bioht_example02",
    "cedex_bioht_example03",
    "cedex_bioht_example04",
)

VENDOR_TYPE = Vendor.ROCHE_CEDEX_BIOHT
SCHEMA_FILE = "cell-culture-analyzer/BENCHLING/2023/09/cell-culture-analyzer.json"


@pytest.mark.parametrize("output_file", output_files)
def test_parse_cedex_bioht_to_asm(output_file: str) -> None:
    test_filepath = f"tests/parsers/roche_cedex_bioht/testdata/{output_file}.txt"
    expected_filepath = f"tests/parsers/roche_cedex_bioht/testdata/{output_file}.json"
    allotrope_dict = from_file(test_filepath, VENDOR_TYPE)
    validate_schema(allotrope_dict, SCHEMA_FILE)
    validate_contents(allotrope_dict, expected_filepath)


@pytest.mark.short
def test_get_model() -> None:
    parser = CedexBiohtParser(TimestampParser())
    model = parser._get_model(get_data())

    if model.measurement_aggregate_document:
        model.measurement_aggregate_document.measurement_identifier = ""

    assert model == get_model()

tests/parsers/roche_cedex_bioht/cedex_bioht_parser_test.py
Benchling-Open-Source/allotropy
import pytest

from tests.parsers.roche_cedex_bioht.cedex_bioht_data import (
    get_reader,
    get_reader_samples,
    get_reader_title,
)


@pytest.mark.short
def test_data_reader() -> None:
    reader = get_reader()

    assert reader.title_data.equals(get_reader_title())
    assert reader.samples_data.equals(get_reader_samples())

tests/parsers/roche_cedex_bioht/cedex_bioht_reader_test.py
Benchling-Open-Source/allotropy
import pandas as pd
import pytest

from allotropy.allotrope.allotrope import AllotropeConversionError
from allotropy.allotrope.models.shared.definitions.custom import (
    TNullableQuantityValueMilliOsmolesPerKilogram,
)
from allotropy.parsers.roche_cedex_bioht.cedex_bioht_structure import (
    Analyte,
    AnalyteList,
    Data,
    Sample,
    Title,
)
from tests.parsers.roche_cedex_bioht.cedex_bioht_data import get_data, get_reader


@pytest.mark.parametrize(
    "processing_time,model_number,device_serial,analyst",
    [
        ("2021-06-01 13:04:06", "CEDEX BIO HT", 12345, "Analyst1"),
        ("2022-06-01 00:10:06", "CEDEX BIO HT", 99999, "Analyst2"),
        ("2023-06-01 24:35:25", "CEDEX BIO HT", 00000, "Analyst3"),
    ],
)
@pytest.mark.short
def test_create_title(
    processing_time: str, model_number: str, device_serial: int, analyst: str
) -> None:
    title_data = {
        "data processing time": processing_time,
        "model number": model_number,
        "device serial number": device_serial,
        "analyst": analyst,
    }
    title = Title.create(pd.Series(title_data))

    assert title.data_processing_time == processing_time
    assert title.analyst == analyst
    assert title.model_number == model_number
    assert title.device_serial_number == str(device_serial)


@pytest.mark.short
def test_create_title_with_no_analyst() -> None:
    title_data = {"device serial number": 1234}
    with pytest.raises(
        AllotropeConversionError, match="Unable to obtain analyst from input data"
    ):
        Title.create(pd.Series(title_data))


@pytest.mark.short
def test_create_title_with_no_serial_number() -> None:
    title_data = {"analyst": "dummy"}
    with pytest.raises(
        AllotropeConversionError,
        match="Unable to obtain device serial number from input data",
    ):
        Title.create(pd.Series(title_data))


@pytest.mark.short
def test_create_analyte() -> None:
    data = pd.Series(
        {
            "analyte name": "glutamine",
            "concentration value": 2.45,
            "concentration unit": "mmol/L",
        }
    )
    analyte = Analyte.create(data)
    assert analyte.name == "glutamine"
    assert analyte.concentration_value == 2.45
    assert analyte.unit == "mmol/L"


@pytest.mark.short
def test_create_analyte_with_no_unit() -> None:
    data = pd.Series(
        {
            "analyte name": "aspartate",
            "concentration value": 1.45,
        }
    )
    analyte = Analyte.create(data)
    assert analyte.name == "aspartate"
    assert analyte.concentration_value == 1.45
    assert analyte.unit is None


@pytest.mark.short
def test_create_analyte_list() -> None:
    data = pd.DataFrame(
        {
            "analyte name": ["lactate", "glutamine", "osmolality"],
            "concentration unit": ["g/L", "mmol/L", "mosm/kg"],
            "concentration value": [2.45, 4.35, 3.7448],
        }
    )
    analyte_list = AnalyteList.create(data)

    assert analyte_list.analytes == [
        Analyte("lactate", 2.45, "g/L"),
        Analyte("glutamine", 4.35, "mmol/L"),
        Analyte("osmolality", 3.7448, "mosm/kg"),
    ]
    assert analyte_list.non_aggregrable_dict == {
        "osmolality": [TNullableQuantityValueMilliOsmolesPerKilogram(3.7448)]
    }
    assert analyte_list.num_measurement_docs == 1


@pytest.mark.short
def test_create_analyte_list_more_than_one_mesurement_docs() -> None:
    data = pd.DataFrame(
        {
            "analyte name": ["lactate", "glutamine", "glutamine"],
            "concentration unit": ["g/L", "mmol/L", "mmol/L"],
            "concentration value": [2.45, 4.35, 3.45],
        },
    )
    analyte_list = AnalyteList.create(data)

    assert analyte_list.analytes == [
        Analyte("lactate", 2.45, "g/L"),
        Analyte("glutamine", 4.35, "mmol/L"),
        Analyte("glutamine", 3.45, "mmol/L"),
    ]
    assert analyte_list.num_measurement_docs == 2
    assert analyte_list.non_aggregrable_dict == {}


@pytest.mark.short
def test_create_sample() -> None:
    samples_data = pd.DataFrame(
        {
            "sample identifier": ["PPDTEST1", "PPDTEST1", "PPDTEST2"],
            "batch identifier": ["batch_id", "batch_id", ""],
            "analyte name": ["lactate", "glutamine", "glutamine"],
            "concentration unit": ["g/L", "mmol/L", "mmol/L"],
            "concentration value": [2.45, 4.35, 0.4],
            "sample role type": ["Sample", "Sample", "Sample"],
            "measurement time": [
                "2021-05-20 16:55:51",
                "2021-05-20 16:56:51",
                "2023-05-20 16:55:51",
            ],
        }
    )
    sample = Sample.create(
        name="PPDTEST1",
        batch="batch_id",
        samples_data=samples_data,
    )
    assert sample.name == "PPDTEST1"
    assert sample.batch == "batch_id"
    assert sample.role_type == "Sample"
    assert sample.measurement_time == "2021-05-20 16:56:51"
    assert len(sample.analyte_list.analytes) == 2


@pytest.mark.short
def test_create_data() -> None:
    assert Data.create(get_reader()) == get_data()

tests/parsers/roche_cedex_bioht/cedex_bioht_structure_test.py
Benchling-Open-Source/allotropy
import json
from typing import Any

from deepdiff import DeepDiff
import jsonschema
import numpy as np
import pandas as pd

from allotropy.allotrope.schemas import get_schema
from allotropy.constants import ASM_CONVERTER_NAME, ASM_CONVERTER_VERSION
from allotropy.parser_factory import VendorType
from allotropy.to_allotrope import allotrope_from_file


def replace_asm_converter_name_and_version(allotrope_dict: dict) -> None:
    for key, value in allotrope_dict.items():
        if key == "data system document":
            value["ASM converter name"] = ASM_CONVERTER_NAME
            value["ASM converter version"] = ASM_CONVERTER_VERSION
        if isinstance(value, dict):
            replace_asm_converter_name_and_version(value)


def assert_allotrope_dicts_equal(expected: dict, actual: dict) -> None:
    replace_asm_converter_name_and_version(expected)
    exclude_regex = [
        r"\['measurement identifier'\]",
        r"\['data source identifier'\]",
        r"\['calculated data identifier'\]",
    ]
    # Uncomment for more info
    # print(DeepDiff(expected, actual, exclude_regex_paths=exclude_regex))
    assert not DeepDiff(
        expected,
        actual,
        exclude_regex_paths=exclude_regex,
        ignore_type_in_groups=[(float, np.float64)],
    )


def from_file(test_file: str, vendor_type: VendorType) -> dict[str, Any]:
    return allotrope_from_file(test_file, vendor_type)


def validate_schema(allotrope_dict: dict[str, Any], schema_relative_path: str) -> None:
    allotrope_schema = get_schema(schema_relative_path)
    jsonschema.validate(
        allotrope_dict,
        allotrope_schema,
        format_checker=jsonschema.validators.Draft202012Validator.FORMAT_CHECKER,
    )


def validate_contents(allotrope_dict: dict[str, Any], expected_file: str) -> None:
    with open(expected_file) as f:
        expected_dict = json.load(f)
        assert_allotrope_dicts_equal(expected_dict, allotrope_dict)


def build_series(elements: list[tuple]) -> pd.Series:
    index, data = list(zip(*elements))
    # pd.Series has a Generic.
    return pd.Series(data=data, index=index)  # type: ignore[no-any-return]

tests/parsers/test_utils.py
Benchling-Open-Source/allotropy
from datetime import timedelta, timezone

import pytest

from allotropy.parsers.utils.timestamp_parser import TimestampParser


@pytest.mark.short
@pytest.mark.parametrize(
    "time_str,expected",
    [
        ("10-11-08", "2008-10-11T00:00:00+00:00"),
        ("Fri, 11 Nov 2011 03:18:09", "2011-11-11T03:18:09+00:00"),
        ("Fri, 11 Nov 2011 03:18:09 -0400", "2011-11-11T03:18:09-04:00"),
        ("Tue Jun 22 07:46:22 EST 2010", "2010-06-22T07:46:22-05:00"),
        ("Tue Jun 22 07:46:22 EDT 2010", "2010-06-22T07:46:22-04:00"),
        ("Tue Jun 22 07:46:22 GMT 2010", "2010-06-22T07:46:22+00:00"),
    ],
)
def test_timestamp_parser_default_utc(time_str: str, expected: str) -> None:
    assert TimestampParser().parse(time_str) == expected


@pytest.mark.short
@pytest.mark.parametrize(
    "time_str,expected",
    [
        ("10-11-08", "2008-10-11T00:00:00-07:00"),
        ("Fri, 11 Nov 2011 03:18:09", "2011-11-11T03:18:09-07:00"),
        ("Fri, 11 Nov 2011 03:18:09 -0400", "2011-11-11T03:18:09-04:00"),
        ("Tue Jun 22 07:46:22 EST 2010", "2010-06-22T07:46:22-05:00"),
    ],
)
def test_timestamp_parser_provided_timezone(time_str: str, expected: str) -> None:
    assert TimestampParser(timezone(timedelta(hours=-7))).parse(time_str) == expected


@pytest.mark.short
@pytest.mark.parametrize("time_str", ["blah"])
def test_timestamp_parser_returns_none_on_invalid_timestamp(time_str: str) -> None:
    assert TimestampParser().parse(time_str) is None

tests/parsers/utils/timestamp_parser_test.py
Benchling-Open-Source/allotropy