# File: measurementData.py
# Project: core
# Author: Jan Range
# License: BSD-2 clause
# Copyright (c) 2022 Institute of Biochemistry and Technical Biochemistry Stuttgart
from pydantic import PositiveFloat, validate_arguments, validator, Field, PrivateAttr
from dataclasses import dataclass
from typing import List, Optional, TYPE_CHECKING
from pyenzyme.enzymeml.core.enzymemlbase import EnzymeMLBase
from pyenzyme.enzymeml.core.replicate import Replicate
from pyenzyme.enzymeml.core.exceptions import MeasurementDataSpeciesIdentifierError
from pyenzyme.enzymeml.core.unitdef import UnitDef
from pyenzyme.enzymeml.core.utils import type_checking, deprecated_getter
if TYPE_CHECKING: # pragma: no cover
static_check_init_args = dataclass
else:
static_check_init_args = type_checking
[docs]@static_check_init_args
class MeasurementData(EnzymeMLBase):
"""Helper class to organize elements"""
init_conc: float = Field(
...,
description="Initial concentration of the measurement data.",
)
unit: str = Field(
...,
description="The unit of the measurement data.",
)
measurement_id: Optional[str] = Field(
None,
description="Unique measurement identifier this dataset belongs to.",
)
reactant_id: Optional[str] = Field(
None,
description="The identifier for the described reactant.",
)
protein_id: Optional[str] = Field(
None,
description="The identifier for the described protein.",
)
replicates: List[Replicate] = Field(
default_factory=list,
description="A list of replicate objects holding raw data of the measurement.",
)
# * Private
_unit_id: Optional[str] = PrivateAttr(default=None)
_enzmldoc = PrivateAttr(default=None)
# ! Validators
[docs] @validator("protein_id")
def check_id_occurences(cls, protein_id: str, values: dict):
reactant_id = values.get("reactant_id")
if reactant_id is None and protein_id is None:
raise MeasurementDataSpeciesIdentifierError()
elif reactant_id and protein_id:
raise MeasurementDataSpeciesIdentifierError(both=[reactant_id, protein_id])
return protein_id
# ! Utilities
[docs] def unifyUnits(self, kind: str, scale: int, enzmldoc) -> Optional[str]:
"""Rescales all replicates data_unit to match the desired scale.
Args:
replicate (Replicate): Replicate object containing time course data.
kind (str): The kind of unit that will be rescaled.
scale (int): The scale to whih the data will be transformed.
enzmldoc ([type]): The EnzymeML document to which the new unit will be added.
"""
unit_id = None
# Transform initial concentration
unitdef: UnitDef = enzmldoc._unit_dict[self._unit_id].copy()
transform_value, new_unit_name, unit_id = self._getTransformation(
unitdef, kind, scale, enzmldoc
)
self.init_conc *= transform_value
self._unit_id = unit_id
self.unit = new_unit_name
# Apply to replicates
for replicate in self.replicates:
self._rescaleReplicateUnits(
replicate=replicate, kind=kind, scale=scale, enzmldoc=enzmldoc
)
@staticmethod
def _getTransformation(unitdef: UnitDef, kind: str, scale: int, enzmldoc):
"""Calculates the new transformation value and returns new UnitDef"""
# Calculate transformation value
transform_value = unitdef.calculateTransformValue(kind=kind, scale=scale)
# Create a new unit that matches the new scale
new_unitdef = UnitDef(**unitdef.dict())
correction_factor = 1 if scale == 0 else 0
for base_unit in new_unitdef.units:
if base_unit.kind == kind:
base_unit.scale = scale + correction_factor
new_unit_name = new_unitdef._getNewName()
unit_id = enzmldoc._convertToUnitDef(new_unit_name)
return transform_value, new_unit_name, unit_id
def _rescaleReplicateUnits(
self, replicate: Replicate, kind: str, scale: int, enzmldoc
) -> None:
"""Rescales a replicates data_unit to match the desired scale.
Args:
replicate (Replicate): Replicate object containing time course data.
kind (str): The kind of unit that will be rescaled.
scale (int): The scale to whih the data will be transformed.
enzmldoc ([type]): The EnzymeML document to which the new unit will be added.
"""
data_unit_id = replicate._data_unit_id
unitdef: UnitDef = enzmldoc._unit_dict[data_unit_id].copy()
# Calculate the scale to transform the unit
transform_value, new_unit_name, unit_id = self._getTransformation(
unitdef, kind, scale, enzmldoc
)
# Re-scale and assign the new data of the replicate
replicate.data = [data_point * transform_value for data_point in replicate.data]
replicate._data_unit_id = unit_id
replicate.data_unit = new_unit_name
[docs] @validate_arguments
def addReplicate(self, replicate: Replicate) -> None:
self.replicates.append(replicate)
[docs] @validate_arguments
def setMeasurementIDs(self, id: str) -> None:
for replicate in self.replicates:
replicate.measurement_id = id
[docs] def get_id(self) -> str:
"""Internal usage to get IDs from objects without ID attribute"""
if self.reactant_id:
return self.reactant_id
elif self.protein_id:
return self.protein_id
else:
raise AttributeError("Neither reactant nor protein ID are given.")
# ! Getters
[docs] def unitdef(self):
"""Returns the appropriate unitdef if an enzmldoc is given"""
if not self._enzmldoc:
return None
return self._enzmldoc._unit_dict[self._unit_id]
[docs] @deprecated_getter("reactant_id")
def getReactantID(self) -> Optional[str]:
return self.reactant_id
[docs] @deprecated_getter("protein_id")
def getProteinID(self) -> Optional[str]:
return self.protein_id
[docs] @deprecated_getter("init_conc")
def getInitConc(self) -> PositiveFloat:
return self.init_conc
[docs] @deprecated_getter("unit")
def getUnit(self) -> str:
return self.unit
[docs] @deprecated_getter("replicates")
def getReplicates(self) -> List[Replicate]:
return self.replicates