# -*- coding: utf-8 -*-
#
#
# TheVirtualBrain-Scientific Package. This package holds all simulators, and
# analysers necessary to run brain-simulations. You can use it stand alone or
# in conjunction with TheVirtualBrain-Framework Package. See content of the
# documentation-folder for more details. See also http://www.thevirtualbrain.org
#
# (c) 2012-2023, Baycrest Centre for Geriatric Care ("Baycrest") and others
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
#
#
# CITATION:
# When using The Virtual Brain for scientific publications, please cite it as explained here:
# https://www.thevirtualbrain.org/tvb/zwei/neuroscience-publications
#
#
"""
This module implements neotraits.
It is private only to shield public usage of the imports and logger.
"""
import copy
import uuid
from enum import Enum
import numpy
import typing
from six import add_metaclass
from numpy.random import RandomState
from scipy.sparse import csc_matrix, spmatrix
from ._attr import Attr
from ._declarative_base import _Property, MetaType
from .info import trait_object_str, trait_object_repr_html, narray_summary_info
from .ex import TraitAttributeError, TraitTypeError, TraitError, TraitFinalAttributeError
from tvb.basic.logger.builder import get_logger
[docs]class CachedTraitProperty(_Property):
# This is a *non-data* descriptor
# Once a field with the same name exists on the instance it will
# take precedence before this non-data descriptor
# This means that after the first __get__ which sets a same-name instance attribute
# this will not be called again. Thus this is a cache.
# To refresh the cache one could delete the instance attr.
def __get__(self, instance, owner):
# type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any
if instance is None:
return self
ret = self.fget(instance)
# mhtodo the error messages generated by this will be confusing
# noinspection PyProtectedMember
ret = self.attr._validate_set(instance, ret)
# set the instance same-named attribute which becomes the cache
setattr(instance, self.attr.field_name, ret)
return ret
[docs]class TraitProperty(_Property):
def __get__(self, instance, owner):
# type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any
if instance is None:
return self
ret = self.fget(instance)
# mhtodo the error messages generated by this will be confusing
# noinspection PyProtectedMember
ret = self.attr._validate_set(instance, ret)
return ret
[docs] def setter(self, fset):
# return a copy of self that has fset. It will overwrite the current one in the
# owning class as the attributes have the same name and the setter comes after the getter
return type(self)(self.fget, self.attr, fset)
def __set__(self, instance, value):
# type: ('HasTraits', typing.Any) -> None
if self.fset is None:
raise TraitAttributeError("Can't set attribute. Property is read only. In " + str(self))
# mhtodo the error messages generated by this will be confusing
# noinspection PyProtectedMember
value = self.attr._validate_set(instance, value)
self.fset(instance, value)
def __delete__(self, instance):
raise TraitAttributeError("can't delete a traitproperty")
def __str__(self):
return 'TraitProperty(attr={}, fget={}'.format(self.attr, self.fget)
[docs]def trait_property(attr):
# type: (Attr) -> typing.Callable[[typing.Callable], TraitProperty]
"""
A read only property that has a declarative attribute associated with.
:param attr: the declarative attribute that describes this property
"""
if not isinstance(attr, Attr):
raise TypeError('@trait_property(attr) attribute argument required.')
def deco(func):
return TraitProperty(func, attr)
return deco
[docs]def cached_trait_property(attr):
# type: (Attr) -> typing.Callable[[typing.Callable], CachedTraitProperty]
"""
A lazy evaluated attribute.
Transforms the decorated method into a cached property.
The method will be called once to compute a value.
The value will be stored in an instance attribute with
the same name as the decorated function.
:param attr: the declarative attribute that describes this property
"""
if not isinstance(attr, Attr):
raise TypeError('@cached_trait_property(attr) attribute argument required.')
def deco(func):
return CachedTraitProperty(func, attr)
return deco
[docs]class TVBEnum(Enum):
"""Super class for all enums used in TVB"""
def __str__(self):
return str(self.value)
[docs] @staticmethod
def string_to_enum(choices, data):
for choice in choices:
if data == str(choice):
return choice
return None
[docs]class TupleEnum(TVBEnum):
"""Super class for all enums which represent classes. The values of these enums are tuples of two elements,
where the first element is a class and the second is a string representing how the parameter is displayed
in the UI."""
@property
def value(self):
tuple_value = super(TupleEnum, self).value
return tuple_value[0]
def __str__(self):
tuple_value = super(TupleEnum, self).value
return tuple_value[1]
@property
def instance(self):
return self.value()
[docs]@add_metaclass(MetaType)
class HasTraits(object):
# The base __init__ and __str__ rely upon metadata gathered by MetaType
# we could have injected these in MetaType, but we don't need meta powers
# this is simpler to grok
gid = Attr(field_type=uuid.UUID)
TYPES_TO_DEEPCOPY = (RandomState, csc_matrix, spmatrix, list, tuple)
def __init__(self, **kwargs):
"""
The default init accepts kwargs for all declarative attrs
and sets them to the given values
"""
# cls just to emphasise that the metadata is on the class not on instances
cls = type(self)
# defined before the kwargs loop, so that a title or gid Attr can overwrite this defaults
self.gid = uuid.uuid4()
"""
gid identifies a specific instance of the hastraits
it is used by serializers as an identifier.
For non-datatype HasTraits this is less usefull but still
provides a unique id for example for a model configuration
""" # these strings are interpreted as docstrings by many tools, not by python though
self.set_title()
""" a generic name that the user can set to easily recognize the instance """
for k, v in kwargs.items():
if k not in cls.declarative_attrs:
raise TraitTypeError(
'Valid kwargs for type {!r} are: {}. You have given: {!r}'.format(
cls, repr(cls.declarative_attrs), k
)
)
setattr(self, k, v)
self.tags = {}
"""
a generic collections of tags. The trait system is not using them
nor should any other code. They should not alter behaviour
They should describe the instance for the user
"""
self.log = get_logger(self.__class__.__module__)
def __str__(self):
return trait_object_str(self)
def _repr_html_(self):
return trait_object_repr_html(self)
[docs] def set_title(self):
self.title = '{} gid: {}'.format(self.__class__.__name__, self.gid)
[docs] def tag(self, tag_name, tag_value=None):
# type: (str, str) -> None
"""
Add a tag to this trait instance.
The tags are for user to recognize and categorize the instances
They should never influence the behaviour of the program
:param tag_name: an arbitrary tag
:param tag_value: an optional tag value
"""
self.tags[str(tag_name)] = str(tag_value)
[docs] def validate(self):
"""
Check that the internal invariants of this class are satisfied.
Not meant to ensure that that is the case.
Use configure for that.
The default configure calls this before it returns.
It complains about missing required attrs
Can be overridden in subclasses
"""
cls = type(self)
for k in cls.declarative_attrs:
# read all declarative attributes. This will trigger errors if they are
# in an invalid state, like beeing required but not set
getattr(self, k)
[docs] def summary_info(self):
# type: () -> typing.Dict[str, str]
"""
A more structured __str__
A 2 column table represented as a dict of str->str
The default __str__ and html representations of this object are derived from
this table.
Override this method and return such a table filled with instance information
that informs the user about your instance
"""
cls = type(self)
ret = {'Type': cls.__name__}
if self.title:
ret['title'] = str(self.title)
for aname in cls.declarative_attrs:
try:
attr_field = getattr(self, aname)
if isinstance(attr_field, numpy.ndarray):
ret.update(narray_summary_info(attr_field, ar_name=aname, condensed=True))
elif isinstance(attr_field, HasTraits):
ret[aname] = attr_field.title
elif isinstance(attr_field, tuple):
ret[aname] = repr(tuple(i if isinstance(i, str) else i.__class__.__name__ for i in attr_field))
else:
ret[aname] = repr(attr_field)
except TraitError:
ret[aname] = 'unavailable'
return ret
def __deepcopy__(self, memodict={}):
"""
Method for deep copying a HasTraits object correctly, event when its attributes are of type HasTraits as well.
"""
cls = type(self)
copied = cls()
for k in cls.declarative_attrs:
attr = getattr(self, k)
if isinstance(attr, (HasTraits, self.TYPES_TO_DEEPCOPY)):
attr = copy.deepcopy(attr)
try:
setattr(copied, k, attr)
except TraitFinalAttributeError:
pass
# set title on copy with same gid as the original after deepcopy finishes
# (because when 'copied' is created it has a random gid and the title is set with that random gid)
copied.set_title()
return copied
[docs] def duplicate(self):
duplicate = copy.deepcopy(self)
duplicate.gid = uuid.uuid4()
# set new title after changing gid
duplicate.set_title()
return duplicate