86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 | class UnitObject:
"""Base class for things that have units."""
@property
def units(self) -> Union[numbers.Number, np.ndarray]:
"""Units for coordinate space."""
# Note that we are regenerating the pint.Quantity from the string
# That is to avoid problems with pickling e.g. when using multiprocessing
unit_str = getattr(self, "_unit_str", None)
if utils.is_iterable(unit_str):
values = [config.ureg(u) for u in unit_str]
conv = [v.to(values[0]).magnitude for v in values]
return config.ureg.Quantity(np.array(conv), values[0].units)
else:
return config.ureg(unit_str)
@property
def units_xyz(self) -> np.ndarray:
"""Units for coordinate space. Always returns x/y/z array."""
units = self.units
if not utils.is_iterable(units):
units = config.ureg.Quantity([units.magnitude] * 3, units.units)
return units
@units.setter
def units(self, units: Union[pint.Unit, pint.Quantity, str, None]):
# Note that we are storing the string, not the actual pint.Quantity
# That is to avoid problems with pickling e.g. when using multiprocessing
# Do NOT remove the is_iterable condition - otherwise we might
# accidentally strip the units from a pint Quantity vector
if not utils.is_iterable(units):
units = utils.make_iterable(units)
if len(units) not in [1, 3]:
raise ValueError(
"Must provide either a single unit or one for "
"for x, y and z dimension."
)
# Make sure we actually have valid unit(s)
unit_str = []
for v in units:
if isinstance(v, str):
# This makes sure we have meters (i.e. nm, um, etc) because
# "microns", for example, produces odd behaviour like
# "millimicrons" on division
v = v.replace("microns", "um").replace("micron", "um")
unit_str.append(str(v))
elif isinstance(v, (pint.Unit, pint.Quantity)):
unit_str.append(str(v))
elif isinstance(v, type(None)):
unit_str.append(None)
elif isinstance(v, numbers.Number):
unit_str.append(str(config.ureg(f"{v} dimensionless")))
else:
raise TypeError(f'Expect str or pint Unit/Quantity, got "{type(v)}"')
# Some clean-up
if len(set(unit_str)) == 1:
unit_str = unit_str[0]
else:
# Check if all base units (e.g. "microns") are the same
unique_units = set([str(config.ureg(u).units) for u in unit_str])
if len(unique_units) != 1:
raise ValueError(
'Non-isometric units must share the same base,'
f' got: {", ".join(unique_units)}'
)
unit_str = tuple(unit_str)
self._unit_str = unit_str
@property
def is_isometric(self):
"""Test if neuron is isometric."""
u = self.units
if utils.is_iterable(u) and len(set(u)) > 1:
return False
return True
|