Physical Units
Added in version 0.10.0.
Many numbers in pulse2percept stand for physical quantities: a current, a
duration, a distance on the retina, or a position in the visual field. The
units module lets you say which, so that the library
can make sure you meant it:
from pulse2percept.stimuli import BiphasicPulse
from pulse2percept.units import mA, us
pulse = BiphasicPulse(50, 0.45) # microamps and milliseconds
pulse = BiphasicPulse(0.05 * mA, 450 * us) # exactly the same pulse
Both lines mean the same thing and produce the same numbers. Units are optional. Bare numbers remain valid and are interpreted in the documented canonical unit. Supplying units adds dimensional checking and automatic conversion.
# To import a few units:
from pulse2percept.units import ms, uA
pulse = BiphasicPulse(50 * uA, 0.45 * ms)
# To import many units:
import pulse2percept.units as u
pulse = BiphasicPulse(50 * u.uA, 0.45 * u.ms)
pulse2percept does not check magnitudes (e.g., 450 * ms where you meant
450 * us), but it will alert you when you passed the wrong units:
>>> from pulse2percept.stimuli import BiphasicPulse
>>> from pulse2percept.units import ms, uA
>>> BiphasicPulse(0.45 * ms, 50 * uA) # arguments swapped
Traceback (most recent call last):
...
DimensionMismatchError: Parameter 'amp' expects electric current (uA), got time (ms).
What bare numbers mean
Each domain has one canonical unit. A bare number is read as being in it, and a unitful value is converted into it:
Quantity |
A bare number means |
|---|---|
stimulus current |
microamps (µA) [1] |
stimulus and percept time |
milliseconds (ms) [1] |
electrode and tissue geometry |
microns (µm) [1] |
visual field coordinates |
degrees of visual angle (dva) |
frequency |
hertz (Hz) |
image and video intensity |
dimensionless |
rotation of an implant |
plain degrees [2] |
Working with quantities
Multiply a number by a unit to get a
Quantity:
>>> from pulse2percept.units import uA, mA
>>> q = 500 * uA
>>> q
500 uA
>>> q.to(mA)
0.5 mA
>>> q.to_value(mA)
0.5
to() gives you another quantity;
to_value() gives you a plain number and
is the explicit way to remove a unit. Nothing removes one implicitly.
Lists and arrays work too, and quantities do arithmetic:
>>> from pulse2percept.units import mA, ms, nC, s, uA
>>> amps = [10, 20, 50] * uA
>>> amps.to_value(mA)
array([0.01, 0.02, 0.05])
>>> 20 * ms + 0.03 * s
50.0 ms
>>> (3 * uA * (2 * ms)).to(nC)
6 nC
Units compose, so a quantity the vocabulary does not name directly can still be built out of the ones it does:
>>> from pulse2percept.units import uA, mm
>>> 675 * uA / mm ** 2
675 uA/mm^2
Boundaries you cannot cross
Dimensions are checked, so a quantity of the wrong kind is refused rather than reinterpreted:
>>> from pulse2percept.implants import DiskElectrode
>>> from pulse2percept.units import dva
>>> DiskElectrode(2 * dva, 0, 0, 100)
Traceback (most recent call last):
...
DimensionMismatchError: Parameter 'x' expects length (um), got visual angle (dva).
Visual angle is not a length. dva has its own dimension. There is no
factor that turns degrees of visual angle into microns of tissue, because the
relationship is (usually) not a constant: it depends on where in the visual
field you are, and on which retinotopic map you believe in. That is what a
VisualFieldMap is for:
from pulse2percept.topography import Watson2014Map
from pulse2percept.units import dva
x_um, y_um = Watson2014Map().dva_to_ret(2 * dva, 3 * dva)
Gray levels are not small currents. An image or a video is dimensionless,
and a model that stimulates tissue will refuse one. An
Encoder is what says how much current a
gray level stands for:
from pulse2percept.stimuli import AmplitudeEncoder, ImageStimulus
from pulse2percept.implants import ArgusII
from pulse2percept.models import ScoreboardModel
img = ImageStimulus('path-to-image.png')
stim = AmplitudeEncoder(ArgusII(), amp_range=(0, 50)).encode(img)
model = ScoreboardModel().build()
percept = model.predict_percept(ArgusII(stim=stim))
Asking what an object’s numbers mean
Objects store plain numbers, and separately record what those numbers are. Reading them back in another unit never changes what is stored:
stim.unit # uA
stim.values(mA) # the data, in milliamps
stim.time_unit # ms
stim.time_quantity # the time axis, with its unit
implant.earray.coordinates() # (n, 3) array of microns
implant.earray.coordinates(mm) # the same array, in millimeters
percept.time_unit # the model's own time unit
percept.times(s) # the time axis, in seconds
Model parameters answer the same question through
get_param_units():
>>> from pulse2percept.models import ScoreboardModel
>>> ScoreboardModel().get_param_units()['rho']
um
Deliberately not supported
Important
This is a small, fixed unit system, not a general one. It does not have:
a unit registry or user-defined units — the vocabulary is the handful of units that appear in pulse2percept’s own APIs, plus whatever you build out of them with
*,/and**;string parsing —
"5 mA"is a string,5 * mAis a quantity;automatic propagation through NumPy — a
Quantityis not anndarrayand does not survivenp.meanornp.concatenate;conversion between visual angle and length — that is a visual field map, not a unit;
quantities inside Cython or Torch kernels;
any requirement that you use units at all.
See also
pulse2percept.unitsfor the full API