Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/source/depth/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ For example, a :class:`~gemd.entity.object.material_run.MaterialRun` links to it
Uni-directional links are typically used when the multiplicity of a relationship can be large.
For example, a material may be referenced in thousands of ingredients.

Runs may also link directly to a template.
A :class:`~gemd.entity.object.material_run.MaterialRun`, :class:`~gemd.entity.object.process_run.ProcessRun` or :class:`~gemd.entity.object.measurement_run.MeasurementRun` carries its own ``template`` field, serialized alongside ``spec``.
When that field is empty, the run's ``template`` property falls back to the template of its spec.
An :class:`~gemd.entity.object.ingredient_run.IngredientRun` carries its own ``name`` and ``labels`` fields in the same way, falling back to the values on its spec.

In GEMD, bi-directional links are readable but only a single direction is writable.
For example, a :class:`~gemd.entity.object.measurement_run.MeasurementRun` can set the :class:`~gemd.entity.object.material_run.MaterialRun` material that it was performed on,
but a :class:`~gemd.entity.object.material_run.MaterialRun` cannot set the :class:`~gemd.entity.object.measurement_run.MeasurementRun`s it contains.
Expand Down
2 changes: 1 addition & 1 deletion gemd/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.2.5"
__version__ = "2.3.0"
23 changes: 21 additions & 2 deletions gemd/entity/object/has_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,31 @@ def _spec_type() -> Type:

@property
def template(self) -> Optional[Union[BaseTemplate, LinkByUID]]:
"""Get the template associated with the spec."""
"""The template that bounds this object.

Objects that also mix in :class:`~gemd.entity.object.has_template.HasTemplate` return
their own template when it is set. Otherwise, this is the template of the spec, if the
spec is an object that has one.
"""
if isinstance(self, HasTemplate):
own = HasTemplate.template.fget(self)
if own is not None:
return own
if isinstance(self.spec, HasTemplate):
return self.spec.template
else:
return None

@template.setter
def template(self, template: Optional[Union[BaseTemplate, LinkByUID]]):
"""Set the object's own template, if it can carry one."""
if not isinstance(self, HasTemplate):
raise AttributeError(f"{type(self).__name__} does not carry its own template.")
HasTemplate.template.fset(self, template)

def _local_dependencies(self) -> Set[Union[BaseEntity, LinkByUID]]:
"""Return a set of all immediate dependencies (no recursion)."""
return {self.spec} if self.spec is not None else set()
result = {self.spec} if self.spec is not None else set()
if isinstance(self, HasTemplate):
result |= HasTemplate._local_dependencies(self)
return result
3 changes: 2 additions & 1 deletion gemd/entity/object/has_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ def template(self, template: Optional[Union[BaseTemplate, LinkByUID]]):

def _local_dependencies(self) -> Set[Union[BaseEntity, LinkByUID]]:
"""Return a set of all immediate dependencies (no recursion)."""
return {self.template} if self.template is not None else set()
template = getattr(self, "_template", None) # Only the template this object stores
return {template} if template is not None else set()
79 changes: 37 additions & 42 deletions gemd/entity/object/ingredient_run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Any, Iterable, List, Mapping, Optional, Type, Union
from typing import Iterable, List, Mapping, Optional, Type, Union

from gemd.entity.dict_serializable import DictSerializable
from gemd.entity.file_link import FileLink
from gemd.entity.link_by_uid import LinkByUID
from gemd.entity.object.base_object import BaseObject
Expand All @@ -11,7 +10,7 @@
from gemd.entity.object.ingredient_spec import IngredientSpec
from gemd.entity.object.material_run import MaterialRun
from gemd.entity.object.process_run import ProcessRun
from gemd.entity.setters import validate_list
from gemd.entity.setters import validate_list, validate_str
from gemd.entity.value.continuous_value import ContinuousValue

__all__ = ["IngredientRun"]
Expand All @@ -26,6 +25,12 @@ class IngredientRun(

Parameters
----------
name: str, optional
Name of the ingredient run.
When unset, the name of the spec applies.
labels: List[str], optional
Additional labels on the ingredient that must be unique.
When empty, the labels of the spec apply.
uids: Map[str, str], optional
A collection of
`unique IDs <https://citrineinformatics.github.io/gemd-documentation/
Expand Down Expand Up @@ -58,6 +63,8 @@ class IngredientRun(
def __init__(
self,
*,
name: str = None,
labels: Iterable[str] = None,
material: Union[MaterialRun, LinkByUID] = None,
process: Union[ProcessRun, LinkByUID] = None,
mass_fraction: ContinuousValue = None,
Expand All @@ -71,10 +78,11 @@ def __init__(
file_links: Optional[Union[Iterable[FileLink], FileLink]] = None,
):
BaseObject.__init__(
self, name=None, uids=uids, tags=tags, notes=notes, file_links=file_links
self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links
)
self._labels = None
HasSpec.__init__(self, spec) # this will overwrite name/labels if/when they are set
self.labels = labels
HasSpec.__init__(self, spec)

HasQuantities.__init__(
self,
Expand All @@ -91,24 +99,34 @@ def __init__(

@property
def name(self) -> str:
"""Get name."""
from gemd.entity.object.ingredient_spec import IngredientSpec

if isinstance(self.spec, IngredientSpec):
"""The run's own name when it has one, otherwise the name of its spec."""
if self._name is not None:
return self._name
elif isinstance(self.spec, IngredientSpec):
return self.spec.name
else:
return super().name
return None

@name.setter
def name(self, name: str):
"""Set the run's own name."""
self._name = None if name is None else validate_str(name)

@property
def labels(self) -> List[str]:
"""Get labels."""
from gemd.entity.object.ingredient_spec import IngredientSpec

if isinstance(self.spec, IngredientSpec):
"""The run's own labels when it has any, otherwise the labels of its spec."""
if len(self._labels) > 0:
return self._labels
elif isinstance(self.spec, IngredientSpec):
return self.spec.labels
else:
return self._labels

@labels.setter
def labels(self, labels: Iterable[str]):
"""Set the run's own labels."""
self._labels = validate_list(labels, str)

@property
def material(self) -> Union[MaterialRun, LinkByUID]:
"""Get the material."""
Expand Down Expand Up @@ -156,33 +174,10 @@ def spec(self) -> Union[IngredientSpec, LinkByUID]:
@spec.setter
def spec(self, spec: Union[IngredientSpec, LinkByUID]):
"""Set the spec."""
if isinstance(self.spec, IngredientSpec): # Store values if you had them
self._name = self.spec.name
self._labels = validate_list(self.spec.labels, str)
if isinstance(self.spec, IngredientSpec): # Keep the old spec's values if you have none
if self._name is None:
self._name = self.spec.name
if len(self._labels) == 0:
self._labels = validate_list(self.spec.labels, str)
# Note that the super() mechanism does not work properly for overloaded setters
getattr(HasSpec, "spec").fset(self, spec)

@classmethod
def from_dict(cls, d: Mapping[str, Any]) -> DictSerializable:
"""Overloaded method from DictSerializable to intercept `name` and `labels` fields.

Parameters
----------
d: dict
The object as a dictionary of key-value pairs that correspond to the object's fields.

Returns
-------
DictSerializable
The deserialized object.

"""
clean = dict(d)
name = clean.pop("name", None)
labels = clean.pop("labels", None)
obj = super().from_dict(clean)
if name is not None:
obj._name = name
if labels is not None:
obj._labels = validate_list(labels, str)
return obj
16 changes: 15 additions & 1 deletion gemd/entity/object/material_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
from gemd.entity.object.base_object import BaseObject
from gemd.entity.object.has_process import HasProcess
from gemd.entity.object.has_spec import HasSpec
from gemd.entity.object.has_template import HasTemplate
from gemd.entity.object.material_spec import MaterialSpec
from gemd.entity.object.process_run import ProcessRun
from gemd.entity.setters import validate_list
from gemd.entity.template.material_template import MaterialTemplate
from gemd.enumeration import SampleType

__all__ = ["MaterialRun"]
MeasurementRunType = TypeVar("MeasurementRunType", bound="MeasurementRun") # noqa: F821


class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_measurements"}):
class MaterialRun(
BaseObject, HasSpec, HasTemplate, HasProcess, typ="material_run", skip={"_measurements"}
):
"""A material run.

This includes a link to the originating process and soft links to measurements.
Expand All @@ -40,6 +44,9 @@ class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_m
"unknown." Default is "unknown."
spec: :class:`~gemd.entity.object.material_spec.MaterialSpec`
The material specification of which this is an instance.
template: ~gemd.entity.template.material_template.MaterialTemplate, optional
A template bounding the valid values for this run's attributes.
When unset, the template of the spec applies.
file_links: List[~gemd.entity.file_link.FileLink], optional
Links to associated files, with resource paths into the files API.

Expand All @@ -50,6 +57,7 @@ def __init__(
name: str,
*,
spec: Union[MaterialSpec, LinkByUID] = None,
template: Optional[Union[MaterialTemplate, LinkByUID]] = None,
process: Union[ProcessRun, LinkByUID] = None,
sample_type: Union[SampleType, str] = "unknown",
uids: Mapping[str, str] = None,
Expand All @@ -63,6 +71,7 @@ def __init__(
self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links
)
HasSpec.__init__(self, spec=spec)
HasTemplate.__init__(self, template)
self._process = None
self._measurements = validate_list(None, [MeasurementRun, LinkByUID])
self._sample_type = None
Expand Down Expand Up @@ -113,6 +122,11 @@ def _spec_type() -> Type:
"""Required method to satisfy HasTemplates mix-in."""
return MaterialSpec

@staticmethod
def _template_type() -> Type:
"""Communicate expected template type to parent class."""
return MaterialTemplate

def _dict_for_compare(self) -> Mapping[str, Any]:
"""Support for recursive equals."""
base = super()._dict_for_compare()
Expand Down
13 changes: 13 additions & 0 deletions gemd/entity/object/measurement_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from gemd.entity.object.has_properties import HasProperties
from gemd.entity.object.has_source import HasSource
from gemd.entity.object.has_spec import HasSpec
from gemd.entity.object.has_template import HasTemplate
from gemd.entity.object.material_run import MaterialRun
from gemd.entity.object.measurement_spec import MeasurementSpec
from gemd.entity.source.performed_source import PerformedSource
from gemd.entity.template.measurement_template import MeasurementTemplate

__all__ = ["MeasurementRun"]

Expand All @@ -23,6 +25,7 @@ class MeasurementRun(
BaseObject,
HasMaterial,
HasSpec,
HasTemplate,
HasConditions,
HasProperties,
HasParameters,
Expand Down Expand Up @@ -56,6 +59,9 @@ class MeasurementRun(
Properties that are measured during this measurement run.
spec: ~gemd.entity.object.measurement_spec.MeasurementSpec`
The measurement specification of which this is an instance.
template: ~gemd.entity.template.measurement_template.MeasurementTemplate, optional
A template bounding the valid values for this run's attributes.
When unset, the template of the spec applies.
material: ~gemd.entity.object.material_run.MaterialRun`
The material run being measured.
spec: ~gemd.entity.object.material_spec.MaterialSpec`
Expand All @@ -72,6 +78,7 @@ def __init__(
name: str,
*,
spec: Union[MeasurementSpec, LinkByUID] = None,
template: Optional[Union[MeasurementTemplate, LinkByUID]] = None,
material: Union[MaterialRun, LinkByUID] = None,
properties: Union[Property, Iterable[Property]] = None,
conditions: Union[Condition, Iterable[Condition]] = None,
Expand All @@ -86,6 +93,7 @@ def __init__(
self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links
)
HasSpec.__init__(self, spec=spec)
HasTemplate.__init__(self, template)
HasProperties.__init__(self, properties)
HasConditions.__init__(self, conditions)
HasParameters.__init__(self, parameters)
Expand Down Expand Up @@ -116,3 +124,8 @@ def material(self, value: Union[MaterialRun, LinkByUID]):
def _spec_type() -> Type:
"""Required method to satisfy HasTemplates mix-in."""
return MeasurementSpec

@staticmethod
def _template_type() -> Type:
"""Communicate expected template type to parent class."""
return MeasurementTemplate
13 changes: 13 additions & 0 deletions gemd/entity/object/process_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
from gemd.entity.object.has_parameters import HasParameters
from gemd.entity.object.has_source import HasSource
from gemd.entity.object.has_spec import HasSpec
from gemd.entity.object.has_template import HasTemplate
from gemd.entity.object.process_spec import ProcessSpec
from gemd.entity.setters import validate_list
from gemd.entity.source.performed_source import PerformedSource
from gemd.entity.template.process_template import ProcessTemplate

__all__ = ["ProcessRun"]
MaterialRunType = TypeVar("MaterialRunType", bound="MaterialRun") # noqa: F821
Expand All @@ -21,6 +23,7 @@
class ProcessRun(
BaseObject,
HasSpec,
HasTemplate,
HasConditions,
HasParameters,
HasSource,
Expand Down Expand Up @@ -53,6 +56,9 @@ class ProcessRun(
Parameters of this process run.
spec: ~gemd.entity.object.process_spec.ProcessSpec
Spec for this process run.
template: ~gemd.entity.template.process_template.ProcessTemplate, optional
A template bounding the valid values for this run's attributes.
When unset, the template of the spec applies.
file_links: List[~gemd.entity.file_link.FileLink], optional
Links to associated files, with resource paths into the files API.
source: ~gemd.entity.source.performed_source.PerformedSource, optional
Expand All @@ -65,6 +71,7 @@ def __init__(
name: str,
*,
spec: Union[ProcessSpec, LinkByUID] = None,
template: Optional[Union[ProcessTemplate, LinkByUID]] = None,
conditions: Union[Condition, Iterable[Condition]] = None,
parameters: Union[Parameter, Iterable[Parameter]] = None,
uids: Mapping[str, str] = None,
Expand All @@ -79,6 +86,7 @@ def __init__(
self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links
)
HasSpec.__init__(self, spec=spec)
HasTemplate.__init__(self, template)
HasConditions.__init__(self, conditions)
HasParameters.__init__(self, parameters)
HasSource.__init__(self, source)
Expand Down Expand Up @@ -111,6 +119,11 @@ def _spec_type() -> Type:
"""Required method to satisfy HasTemplates mix-in."""
return ProcessSpec

@staticmethod
def _template_type() -> Type:
"""Communicate expected template type to parent class."""
return ProcessTemplate

def _dict_for_compare(self) -> Dict[str, Any]:
"""Support for recursive equals."""
base = super()._dict_for_compare()
Expand Down
Loading
Loading