"""base.py: OMF Project and base classes for its components"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from collections import OrderedDict
import datetime
import properties
[docs]class UidModel(properties.HasProperties):
"""UidModel is a HasProperties object with uid"""
_REGISTRY = OrderedDict()
uid = properties.Uuid(
'Unique identifier',
serializer=lambda val, **kwargs: None,
deserializer=lambda val, **kwargs: None
)
date_created = properties.GettableProperty(
'Date project was created',
default=datetime.datetime.utcnow,
serializer=properties.DateTime.to_json,
deserializer=lambda val, **kwargs: None
)
date_modified = properties.GettableProperty(
'Date project was modified',
default=datetime.datetime.utcnow,
serializer=properties.DateTime.to_json,
deserializer=lambda val, **kwargs: None
)
@properties.observer(properties.everything)
def _modify(self, _):
"""Update date_modified whenever anything changes"""
self._backend['date_modified'] = datetime.datetime.utcnow()
@properties.validator
def _update_date_modified(self):
"""Update date_modified if any contained UidModel has been modified"""
for val in self._backend.values():
if (
isinstance(val, UidModel) and
val.date_modified > self.date_modified
):
self._backend['date_modified'] = val.date_modified
def serialize(self, include_class=True, registry=None, #pylint: disable=arguments-differ
skip_validation=False, **kwargs):
"""Serialize nested UidModels to a flat dictionary with pointers"""
if registry is None:
if not skip_validation:
self.validate()
registry = dict()
root = True
else:
root = False
if str(self.uid) not in registry:
registry.update({
str(self.uid): super(UidModel, self).serialize(
include_class, registry=registry, **kwargs
)
})
if root:
return registry
return str(self.uid)
@classmethod
def deserialize(cls, uid, trusted=True, registry=None, **kwargs): #pylint: disable=arguments-differ
"""Deserialize nested UidModels from flat pointer dictionary"""
if registry is None:
raise ValueError('no registry provided')
if uid not in registry:
raise ValueError('uid not found: {}'.format(uid))
if not isinstance(registry[uid], UidModel):
date_created = registry[uid]['date_created']
date_modified = registry[uid]['date_modified']
kwargs.update({'verbose': False})
new_model = super(UidModel, cls).deserialize(
value=registry[uid],
registry=registry,
trusted=trusted,
**kwargs
)
new_model._backend.update({
'uid': properties.Uuid.from_json(uid),
'date_created': properties.DateTime.from_json(date_created),
'date_modified': properties.DateTime.from_json(date_modified)
})
registry.update({uid: new_model})
return registry[uid]
[docs]class ContentModel(UidModel):
"""ContentModel is a UidModel with title and description"""
name = properties.String(
'Title',
default=''
)
description = properties.String(
'Description',
default=''
)
[docs]class ProjectElementData(ContentModel):
"""Data array with values at specific locations on the mesh"""
location = properties.StringChoice(
'Location of the data on mesh',
choices=('vertices', 'segments', 'faces', 'cells')
)
@property
def array(self):
"""Data subclasses should override array with their data array"""
raise ValueError('Cannot access array of base ProjectElementData')
class ProjectElementGeometry(UidModel):
"""Base class for all ProjectElement meshes"""
_valid_locations = None
origin = properties.Vector3(
'Origin of the Mesh relative to origin of the Project',
default=[0., 0., 0.]
)
def location_length(self, location):
"""Return correct data length based on location"""
raise NotImplementedError()
@property
def num_nodes(self):
"""get number of nodes"""
raise NotImplementedError()
@property
def num_cells(self):
"""get number of cells"""
raise NotImplementedError()
[docs]class ProjectElement(ContentModel):
"""Base ProjectElement class for OMF file
ProjectElement subclasses must define their mesh.
ProjectElements include PointSet, LineSet, Surface, and Volume
"""
data = properties.List(
'Data defined on the element',
prop=ProjectElementData,
required=False,
default=list,
)
color = properties.Color(
'Solid color',
default='random'
)
geometry = None
@properties.validator
def _validate_data(self):
"""Check if element is built correctly"""
assert self.geometry is not None, 'ProjectElement must have a mesh'
for i, dat in enumerate(self.data):
if dat.location not in self.geometry._valid_locations: #pylint: disable=protected-access
raise ValueError(
'Invalid location {loc} - valid values: {locs}'.format(
loc=dat.location,
locs=', '.join(self.geometry._valid_locations) #pylint: disable=protected-access
)
)
valid_length = self.geometry.location_length(dat.location)
if len(dat.array) != valid_length:
raise ValueError(
'data[{index}] length {datalen} does not match '
'{loc} length {meshlen}'.format(
index=i,
datalen=len(dat.array),
loc=dat.location,
meshlen=valid_length
)
)
return True
[docs]class Project(ContentModel):
"""OMF Project for serializing to .omf file"""
author = properties.String(
'Author',
default=''
)
revision = properties.String(
'Revision',
default=''
)
date = properties.DateTime(
'Date associated with the project data',
required=False
)
units = properties.String(
'Spatial units of project',
default=''
)
elements = properties.List(
'Project Elements',
prop=ProjectElement,
default=list,
)
origin = properties.Vector3(
'Origin point for all elements in the project',
default=[0., 0., 0.]
)