Skip to content

Commit

Permalink
Fix #29 - Units can now be changed
Browse files Browse the repository at this point in the history
Changes in the 'unit'-attribute now directly reflect on the unit_dict
  • Loading branch information
JR-1991 committed Mar 16, 2022
1 parent 9a92598 commit 5e154dd
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 50 deletions.
5 changes: 4 additions & 1 deletion pyenzyme/enzymeml/core/abstract_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ class AbstractSpeciesDataclass(BaseModel):
boundary: bool
unit: Optional[str] = None
ontology: SBOTerm
_unit_id: Optional[str] = PrivateAttr(default=None)
uri: Optional[str]
creator_id: Optional[str]

# * Private attributes
_unit_id: Optional[str] = PrivateAttr(default=None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)


class AbstractSpecies(ABC, AbstractSpeciesDataclass):
"""Due to inheritance and type-checking issues, the dataclass has to be mixed in."""
Expand Down
29 changes: 27 additions & 2 deletions pyenzyme/enzymeml/core/enzymemlbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import json
import logging

from pydantic import BaseModel
from pydantic import BaseModel, PrivateAttr
from pyenzyme.utils.log import log_change
from typing import Optional

logger = logging.getLogger("pyenzyme")

Expand All @@ -29,7 +30,7 @@ def json(self, indent: int = 2, **kwargs):
"protein_dict": {"Protein": {"__all__": {"_unit_id"}}},
},
by_alias=True,
**kwargs
**kwargs,
)

@classmethod
Expand All @@ -38,8 +39,31 @@ def fromJSON(cls, json_string):

def __setattr__(self, name, value):
"""Modified attribute setter to document changes in the EnzymeML document"""

# Check for changing units and assign a new one
if "unit" in name and not name.startswith("_") and hasattr(self, "_enzmldoc"):
if self._enzmldoc:
# When the object has already been assigned to a document
# use this to set and add the new unit

# Create a new UnitDef and get the ID
new_unit_id = self._enzmldoc._convertToUnitDef(value)
value = self._enzmldoc.unit_dict[new_unit_id]._get_unit_name()

# Set the unit ID to the object
attr_name = f"_{name}_id"
super().__setattr__(attr_name, new_unit_id)

# Perform logging of the new attribute to history
old_value = getattr(self, name)

# Whenever a new ID is assigned, make sure the names are compliant with our standards
if "unit_id" in name and hasattr(self, "_enzmldoc"):
if self._enzmldoc:
unit_name = self._enzmldoc.unit_dict[value]._get_unit_name()
attr_name = name.replace("_id", "")[1::]
super().__setattr__(attr_name, unit_name)

if (
isinstance(old_value, list) is False
and name.startswith("_") is False
Expand Down Expand Up @@ -69,4 +93,5 @@ def __setattr__(self, name, value):
value,
)

# Finally, set the new attribute's value
super().__setattr__(name, value)
48 changes: 43 additions & 5 deletions pyenzyme/enzymeml/core/enzymemldocument.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,10 @@ def addGlobalParameter(

if param.unit:
param._unit_id = self._convertToUnitDef(param.unit)
param.unit = self.unit_dict[param._unit_id]._get_unit_name()

# Assign the current EnzymeMLDocument
param._enzmldoc = self

# Add the parameter to the parameter_dict
self.global_parameters[param.name] = param
Expand Down Expand Up @@ -1054,13 +1058,20 @@ def _addSpecies(
if species.unit and use_parser:
unit_id = self._convertToUnitDef(species.unit)
species._unit_id = unit_id
species.unit = self.unit_dict[species._unit_id]._get_unit_name()

elif species.unit and use_parser is False:
species._unit_id = species.unit
species.unit = self.getUnitString(species._unit_id)
species.unit = self.unit_dict[species._unit_id]._get_unit_name()

# Log creation of the object
log_object(logger, species)

# Finally, set the current document to the
# object attribute _enzmldoc to allow unit changes
species._enzmldoc = self

# Add species to dictionary
dictionary[species.id] = species

Expand Down Expand Up @@ -1120,7 +1131,8 @@ def addReaction(self, reaction: EnzymeReaction, use_parser: bool = True) -> str:
# Unit conversion
self._convert_kinetic_model_units(reaction.model.parameters, enzmldoc=self)

# Finally add the reaction to the document
# Finally add the reaction to the document and assign the doc
reaction._enzmldoc = self
self.reaction_dict[reaction.id] = reaction

# Log the object
Expand Down Expand Up @@ -1202,6 +1214,8 @@ def _convert_kinetic_model_units(
for parameter in parameters:
if parameter.unit:
parameter._unit_id = enzmldoc._convertToUnitDef(parameter.unit)
parameter.unit = enzmldoc.unit_dict[parameter._unit_id]._get_unit_name()
parameter._enzmldoc = enzmldoc

def addReactions(self, reactions: List[EnzymeReaction]):
"""Adds multiple reactions to an EnzymeML document.
Expand Down Expand Up @@ -1252,6 +1266,12 @@ def addMeasurement(self, measurement: Measurement) -> str:
measurement_id (String): Assigned measurement identifier.
"""

# Assign the current EnzymeMLDocument to
# propagate towards sub-elements such
# that unit changes can be done comliant
# to UnitDefinitions
measurement._enzmldoc = self

# Check consistency
self._checkMeasurementConsistency(measurement)

Expand Down Expand Up @@ -1292,17 +1312,31 @@ def _convertMeasurementUnits(self, measurement: Measurement) -> None:
measurement.global_time_unit
)

# Set correct string
measurement.global_time_unit = self.unit_dict[
measurement._global_time_unit_id
]._get_unit_name()

# Update temperature unit of the measurement
if measurement.temperature_unit:
measurement._temperature_unit_id = self._convertToUnitDef(
measurement.temperature_unit
)

# Set correct string
measurement.temperature_unit = self.unit_dict[
measurement._temperature_unit_id
]._get_unit_name()

def update_dict_units(
measurement_data_dict: Dict[str, MeasurementData]
measurement_data_dict: Dict[str, MeasurementData], measurement: Measurement
) -> None:
"""Helper function to update units"""
"""Helper function to update units and assignment of the coupled EnzymeMLDocument"""
for measurement_data in measurement_data_dict.values():

# Assign the measurements enzmldoc
measurement_data._enzmldoc = measurement._enzmldoc

measurement_data._unit_id = self._convertToUnitDef(
measurement_data.unit
)
Expand All @@ -1313,8 +1347,8 @@ def update_dict_units(
measurement.global_time = global_time

# Perform update
update_dict_units(measurement.species_dict["proteins"])
update_dict_units(measurement.species_dict["reactants"])
update_dict_units(measurement.species_dict["proteins"], measurement)
update_dict_units(measurement.species_dict["reactants"], measurement)

def _convertReplicateUnits(
self, measurement_data: MeasurementData
Expand All @@ -1330,6 +1364,10 @@ def _convertReplicateUnits(

for replicate in measurement_data.replicates:

# Assign the EnzymeML document for compliant changes
# of units when already added to the document
replicate._enzmldoc = measurement_data._enzmldoc

# Convert unit
time_unit_id = self._convertToUnitDef(replicate.time_unit)
data_unit_id = self._convertToUnitDef(replicate.data_unit)
Expand Down
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/core/enzymereaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class EnzymeReaction(EnzymeMLBase):

# * Private attributes
_temperature_unit_id: str = PrivateAttr(None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

# ! Validators
@validator("id")
Expand Down
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/core/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Measurement(EnzymeMLBase):
# * Private attributes
_temperature_unit_id: str = PrivateAttr(None)
_global_time_unit_id: str = PrivateAttr(None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

# ! Validators
@validator("temperature_unit")
Expand Down
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/core/measurementData.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class MeasurementData(EnzymeMLBase):

# * Private
_unit_id: Optional[str] = PrivateAttr(default=None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

# ! Validators

Expand Down
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/core/replicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class Replicate(EnzymeMLBase):
# * Private
_time_unit_id: Optional[str] = PrivateAttr(None)
_data_unit_id: Optional[str] = PrivateAttr(None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

@validator("data")
def check_data_completeness(cls, data: List[float], values: dict):
Expand Down
88 changes: 79 additions & 9 deletions pyenzyme/enzymeml/core/unitdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,63 @@ def get_id(self) -> str:
else:
raise AttributeError("No species ID given.")

# @validator("kind")
# def check_sbml_unit_enum(cls, kind_int: int):
# kind_string: str = libsbml.UnitKind_toString(kind_int)
def get_name(self) -> str:
"""Returns the appropriate name of the unit"""

# if "Invalid UnitKind" in kind_string:
# raise UnitKindError()
# Get mappings
prefix_mapping, kind_mapping = self._setup_mappings()

# Retrieve values to generate the name
prefix = prefix_mapping[self.scale]
unit = kind_mapping[self.kind]

# Special case for time
if unit == "s":
if self.multiplier == 60:
unit = "min"
if self.multiplier == 60 * 60:
unit = "hours"

if abs(self.exponent) != 1:
exponent = f"^{abs(int(self.exponent))}"
else:
exponent = ""

return f"{prefix}{unit}{exponent}"

@staticmethod
def _setup_mappings():
# TODO integrate this to unitcreator
# Create a mappings
prefix_mapping = {
-15: "f",
-12: "p",
-9: "n",
-6: "u",
-3: "m",
-2: "c",
-1: "d",
1: "",
3: "k",
}

kind_mapping = {
"litre": "l",
"gram": "g",
"second": "s",
"kelvin": "K",
"dimensionless": "dimensionless",
"mole": "mole",
}

return prefix_mapping, kind_mapping


@static_check_init_args
class UnitDef(EnzymeMLBase):

name: str = Field(
name: Optional[str] = Field(
None,
description="Name of the SI unit.",
)

Expand Down Expand Up @@ -106,6 +151,30 @@ def check_meta_id(cls, meta_id: Optional[str], values: dict):

return None

def _get_unit_name(self):
"""Generates the unit name based of the given baseunits"""

nominator, denominator = [], []
for unit in self.units:
if unit.exponent > 0:
nominator.append(unit.get_name())
elif unit.exponent < 0:
denominator.append(unit.get_name())

# Catch empty nominators
if not nominator:
nominator = "1"

# Combine each side and construct the SI string
nominator = " ".join(nominator)
denominator = " ".join(denominator)

if denominator:
return " / ".join([nominator, denominator])
else:
return nominator

# ! Adders
@validate_arguments
def addBaseUnit(
self, kind: str, exponent: float, scale: int, multiplier: float
Expand All @@ -125,8 +194,9 @@ def addBaseUnit(
)

# Merge both and sort them via kind
self.units.append(baseunit)
self.units = sorted(self.units, key=lambda unit: unit.kind)
if baseunit not in self.units:
self.units.append(baseunit)
self.units = sorted(self.units, key=lambda unit: unit.kind)

# ! Utilities
def calculateTransformValue(self, kind: str, scale: int):
Expand Down Expand Up @@ -222,4 +292,4 @@ def getOntology(self):

def getFootprint(self):
sorted_units = [base_unit.dict() for base_unit in self.units]
return sorted(sorted_units, key=lambda unit: unit["kind"])
return list(sorted(sorted_units, key=lambda unit: unit["kind"]))
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/core/vessel.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Vessel(EnzymeMLBase):

# * Private
_unit_id: Optional[str] = PrivateAttr(None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

# ! Validators
@validator("id")
Expand Down
1 change: 1 addition & 0 deletions pyenzyme/enzymeml/models/kineticmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class KineticParameter(EnzymeMLBase):

# * Private attributes
_unit_id: Optional[str] = PrivateAttr(None)
_enzmldoc: Optional["EnzymeMLDocument"] = PrivateAttr(default=None)

def get_id(self):
"""For logging. Dont bother."""
Expand Down

0 comments on commit 5e154dd

Please sign in to comment.