Skip to content

Commit c358e55

Browse files
committed
refactor and improve brain data dunder math. improve first tutorial
1 parent 96ba2fe commit c358e55

File tree

4 files changed

+291
-142
lines changed

4 files changed

+291
-142
lines changed

docs/tutorials/basic/01_brain_data_basics.ipynb

Lines changed: 31 additions & 5 deletions
Large diffs are not rendered by default.

nltools/data/brain_data.py

Lines changed: 171 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -395,185 +395,225 @@ def __setitem__(self, index, value):
395395
def __len__(self):
396396
return self.shape()[0]
397397

398+
def _validate_arithmetic_operands(self, other, operation_name):
399+
"""Validate operand types for arithmetic operations.
400+
401+
Args:
402+
other: The other operand
403+
operation_name: Name of the operation (e.g., 'add', 'multiply')
404+
405+
Returns:
406+
str: Type of operand ('scalar', 'brain_data', or 'array')
407+
408+
Raises:
409+
ValueError: If operand type is not supported
410+
"""
411+
if isinstance(other, (int, np.integer, float, np.floating)):
412+
return "scalar"
413+
elif isinstance(other, Brain_Data):
414+
return "brain_data"
415+
elif isinstance(other, (list, np.ndarray)) and operation_name == "multiply":
416+
return "array"
417+
else:
418+
valid_types = "int, float, or Brain_Data"
419+
if operation_name == "multiply":
420+
valid_types = "int, float, list, np.ndarray, or Brain_Data"
421+
raise ValueError(
422+
f"Can only {operation_name} {valid_types}. Provided {type(other)}"
423+
)
424+
425+
def _check_shape_compatibility(self, other, operation_name):
426+
"""Check shape compatibility between two Brain_Data objects.
427+
428+
Args:
429+
other: The other Brain_Data object
430+
operation_name: Name of the operation for error messages
431+
432+
Raises:
433+
ValueError: If shapes are incompatible
434+
"""
435+
self_shape, other_shape = self.shape(), other.shape()
436+
self_is_single = len(self_shape) == 1
437+
other_is_single = len(other_shape) == 1
438+
439+
if self_is_single and other_is_single:
440+
if self_shape[0] != other_shape[0]:
441+
raise ValueError("Both images must have the same number of voxels")
442+
elif self_is_single and not other_is_single:
443+
raise ValueError(
444+
f"Cannot {operation_name} multiple images to a single image"
445+
)
446+
elif not self_is_single and other_is_single:
447+
if self_shape[1] != other_shape[0]:
448+
raise ValueError("Both images must have the same number of voxels")
449+
elif not self_is_single and not other_is_single:
450+
if self_shape[0] != other_shape[0] or self_shape[1] != other_shape[1]:
451+
raise ValueError(
452+
f"Cannot {operation_name} multiple images of a different shape"
453+
)
454+
398455
def __add__(self, y):
399456
new = deepcopy(self)
400-
if isinstance(y, (int, np.integer, float, np.floating)):
457+
operand_type = self._validate_arithmetic_operands(y, "add")
458+
459+
if operand_type == "scalar":
401460
new.data = new.data + y
402-
elif isinstance(y, Brain_Data):
403-
self_shape, y_shape = self.shape(), y.shape()
404-
self_is_single = len(self_shape) == 1
405-
y_is_single = len(y_shape) == 1
406-
407-
if self_is_single and y_is_single:
408-
if self_shape[0] != y_shape[0]:
409-
raise ValueError("Both images must have the same number of voxels")
410-
elif self_is_single and not y_is_single:
411-
raise ValueError("Cannot add multiple images to a single image")
412-
elif not self_is_single and y_is_single:
413-
if self_shape[1] != y_shape[0]:
414-
raise ValueError("Both images must have the same number of voxels")
415-
elif not self_is_single and not y_is_single:
416-
if self_shape[0] != y_shape[0] or self_shape[1] != y_shape[1]:
417-
raise ValueError("Cannot add multiple images of a different shape")
418-
else:
419-
raise ValueError(f"Data shape mismatch {self_shape} and {y_shape}")
420-
else:
421-
raise ValueError("Can only add int, float, or Brain_Data")
461+
elif operand_type == "brain_data":
462+
self._check_shape_compatibility(y, "add")
463+
new.data = new.data + y.data
464+
422465
return new
423466

424467
def __radd__(self, y):
425468
new = deepcopy(self)
426-
if isinstance(y, (int, np.integer, float, np.floating)):
469+
operand_type = self._validate_arithmetic_operands(y, "add")
470+
471+
if operand_type == "scalar":
427472
new.data = y + new.data
428-
elif isinstance(y, Brain_Data):
429-
self_shape, y_shape = self.shape(), y.shape()
430-
self_is_single = len(self_shape) == 1
431-
y_is_single = len(y_shape) == 1
432-
433-
if self_is_single and y_is_single:
434-
if self_shape[0] != y_shape[0]:
435-
raise ValueError("Both images must have the same number of voxels")
436-
elif self_is_single and not y_is_single:
437-
raise ValueError("Cannot add multiple images to a single image")
438-
elif not self_is_single and y_is_single:
439-
if self_shape[1] != y_shape[0]:
440-
raise ValueError("Both images must have the same number of voxels")
441-
elif not self_is_single and not y_is_single:
442-
if self_shape[0] != y_shape[0] or self_shape[1] != y_shape[1]:
443-
raise ValueError("Cannot add multiple images of a different shape")
444-
else:
445-
raise ValueError(f"Data shape mismatch {self_shape} and {y_shape}")
446-
else:
447-
raise ValueError("Can only add int, float, or Brain_Data")
448-
new.data = y + new.data
473+
elif operand_type == "brain_data":
474+
self._check_shape_compatibility(y, "add")
475+
new.data = y.data + new.data
476+
449477
return new
450478

451479
def __sub__(self, y):
452480
new = deepcopy(self)
453-
if isinstance(y, (int, np.integer, float, np.floating)):
481+
operand_type = self._validate_arithmetic_operands(y, "subtract")
482+
483+
if operand_type == "scalar":
454484
new.data = new.data - y
455-
elif isinstance(y, Brain_Data):
456-
self_shape, y_shape = self.shape(), y.shape()
457-
self_is_single = len(self_shape) == 1
458-
y_is_single = len(y_shape) == 1
459-
460-
if self_is_single and y_is_single:
461-
if self_shape[0] != y_shape[0]:
462-
raise ValueError("Both images must have the same number of voxels")
463-
elif self_is_single and not y_is_single:
464-
raise ValueError("Cannot subtract multiple images from a single image")
465-
elif not self_is_single and y_is_single:
466-
if self_shape[1] != y_shape[0]:
467-
raise ValueError("Both images must have the same number of voxels")
468-
elif not self_is_single and not y_is_single:
469-
if self_shape[0] != y_shape[0] or self_shape[1] != y_shape[1]:
470-
raise ValueError(
471-
"Cannot subtract multiple images from multiple images of a different shape"
472-
)
473-
else:
474-
raise ValueError(f"Data shape mismatch {self_shape} and {y_shape}")
475-
else:
476-
raise ValueError("Can only add int, float, or Brain_Data")
477-
new.data = new.data - y.data
485+
elif operand_type == "brain_data":
486+
self._check_shape_compatibility(y, "subtract")
487+
new.data = new.data - y.data
488+
478489
return new
479490

480491
def __rsub__(self, y):
481492
new = deepcopy(self)
482-
if isinstance(y, (int, np.integer, float, np.floating)):
493+
operand_type = self._validate_arithmetic_operands(y, "subtract")
494+
495+
if operand_type == "scalar":
483496
new.data = y - new.data
484-
elif isinstance(y, Brain_Data):
485-
if self.shape() != y.shape():
486-
raise ValueError(
487-
"Both Brain_Data() instances need to be the same shape."
488-
)
497+
elif operand_type == "brain_data":
498+
self._check_shape_compatibility(y, "subtract")
489499
new.data = y.data - new.data
490-
else:
491-
raise ValueError("Can only add int, float, or Brain_Data")
500+
492501
return new
493502

494503
def __mul__(self, y):
495504
new = deepcopy(self)
496-
if isinstance(y, (int, np.integer, float, np.floating)):
505+
operand_type = self._validate_arithmetic_operands(y, "multiply")
506+
507+
if operand_type == "scalar":
497508
new.data = new.data * y
498-
elif isinstance(y, Brain_Data):
499-
self_shape, y_shape = self.shape(), y.shape()
500-
self_is_single = len(self_shape) == 1
501-
y_is_single = len(y_shape) == 1
502-
503-
if self_is_single and y_is_single:
504-
if self_shape[0] != y_shape[0]:
505-
raise ValueError("Both images must have the same number of voxels")
506-
elif self_is_single and not y_is_single:
507-
raise ValueError("Cannot multiply multiple images by a single image")
508-
elif not self_is_single and y_is_single:
509-
if self_shape[1] != y_shape[0]:
510-
raise ValueError("Both images must have the same number of voxels")
511-
elif not self_is_single and not y_is_single:
512-
if self_shape[0] != y_shape[0] or self_shape[1] != y_shape[1]:
513-
raise ValueError(
514-
"Cannot multiply multiple images by multiple images of a different shape"
515-
)
516-
else:
517-
raise ValueError(f"Data shape mismatch {self_shape} and {y_shape}")
509+
elif operand_type == "brain_data":
510+
self._check_shape_compatibility(y, "multiply")
518511
new.data = np.multiply(new.data, y.data)
519-
elif isinstance(y, (list, np.ndarray)):
512+
elif operand_type == "array":
520513
if len(y) != len(self):
521514
raise ValueError(
522515
"Vector multiplication requires that the "
523516
"length of the vector match the number of "
524517
"images in Brain_Data instance."
525518
)
526-
else:
527-
new.data = np.dot(new.data.T, y).T
528-
else:
529-
raise ValueError("Can only multiply int, float, list, or Brain_Data")
519+
new.data = np.dot(new.data.T, y).T
520+
530521
return new
531522

532523
def __rmul__(self, y):
533524
new = deepcopy(self)
534-
if isinstance(y, (int, np.integer, float, np.floating)):
525+
operand_type = self._validate_arithmetic_operands(y, "multiply")
526+
527+
if operand_type == "scalar":
535528
new.data = y * new.data
536-
elif isinstance(y, Brain_Data):
537-
if self.shape() != y.shape():
529+
elif operand_type == "brain_data":
530+
self._check_shape_compatibility(y, "multiply")
531+
new.data = np.multiply(y.data, new.data)
532+
elif operand_type == "array":
533+
# For right multiplication with array, it's typically not supported
534+
# but we'll keep consistent behavior
535+
if len(y) != len(self):
538536
raise ValueError(
539-
"Both Brain_Data() instances need to be the same shape."
537+
"Vector multiplication requires that the "
538+
"length of the vector match the number of "
539+
"images in Brain_Data instance."
540540
)
541-
new.data = np.multiply(y.data, new.data)
542-
else:
543-
raise ValueError("Can only multiply int, float, or Brain_Data")
541+
new.data = np.dot(new.data.T, y).T
542+
544543
return new
545544

546545
def __truediv__(self, y):
547546
new = deepcopy(self)
548-
if isinstance(y, (int, np.integer, float, np.floating)):
547+
operand_type = self._validate_arithmetic_operands(y, "divide")
548+
549+
if operand_type == "scalar":
549550
with np.errstate(invalid="ignore", divide="ignore"):
550551
new.data = new.data / y
551-
elif isinstance(y, Brain_Data):
552-
self_shape, y_shape = self.shape(), y.shape()
553-
self_is_single = len(self_shape) == 1
554-
y_is_single = len(y_shape) == 1
555-
556-
if self_is_single and y_is_single:
557-
if self_shape[0] != y_shape[0]:
558-
raise ValueError("Both images must have the same number of voxels")
559-
elif self_is_single and not y_is_single:
560-
raise ValueError("Cannot divide a single image by multiple images")
561-
elif not self_is_single and y_is_single:
562-
if self_shape[1] != y_shape[0]:
563-
raise ValueError("Both images must have the same number of voxels")
564-
elif not self_is_single and not y_is_single:
565-
if self_shape[0] != y_shape[0] or self_shape[1] != y_shape[1]:
566-
raise ValueError(
567-
"Cannot divide multiple images by multiple images of a different shape"
568-
)
569-
else:
570-
raise ValueError(f"Data shape mismatch {self_shape} and {y_shape}")
552+
elif operand_type == "brain_data":
553+
self._check_shape_compatibility(y, "divide")
571554
with np.errstate(invalid="ignore", divide="ignore"):
572555
new.data = np.divide(new.data, y.data)
573-
else:
574-
raise ValueError("Can only divide int, float, list, or Brain_Data")
556+
575557
return new
576558

559+
def __iadd__(self, y):
560+
"""In-place addition (+=)."""
561+
operand_type = self._validate_arithmetic_operands(y, "add")
562+
563+
if operand_type == "scalar":
564+
self.data = self.data + y
565+
elif operand_type == "brain_data":
566+
self._check_shape_compatibility(y, "add")
567+
self.data = self.data + y.data
568+
569+
return self
570+
571+
def __isub__(self, y):
572+
"""In-place subtraction (-=)."""
573+
operand_type = self._validate_arithmetic_operands(y, "subtract")
574+
575+
if operand_type == "scalar":
576+
self.data = self.data - y
577+
elif operand_type == "brain_data":
578+
self._check_shape_compatibility(y, "subtract")
579+
self.data = self.data - y.data
580+
581+
return self
582+
583+
def __imul__(self, y):
584+
"""In-place multiplication (*=)."""
585+
operand_type = self._validate_arithmetic_operands(y, "multiply")
586+
587+
if operand_type == "scalar":
588+
self.data = self.data * y
589+
elif operand_type == "brain_data":
590+
self._check_shape_compatibility(y, "multiply")
591+
self.data = np.multiply(self.data, y.data)
592+
elif operand_type == "array":
593+
if len(y) != len(self):
594+
raise ValueError(
595+
"Vector multiplication requires that the "
596+
"length of the vector match the number of "
597+
"images in Brain_Data instance."
598+
)
599+
self.data = np.dot(self.data.T, y).T
600+
601+
return self
602+
603+
def __itruediv__(self, y):
604+
"""In-place true division (/=)."""
605+
operand_type = self._validate_arithmetic_operands(y, "divide")
606+
607+
if operand_type == "scalar":
608+
with np.errstate(invalid="ignore", divide="ignore"):
609+
self.data = self.data / y
610+
elif operand_type == "brain_data":
611+
self._check_shape_compatibility(y, "divide")
612+
with np.errstate(invalid="ignore", divide="ignore"):
613+
self.data = np.divide(self.data, y.data)
614+
615+
return self
616+
577617
def __iter__(self):
578618
for x in range(len(self)):
579619
yield self[x]

nltools/plotting.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,15 @@ def plot_t_brain(
245245
return
246246

247247

248-
def plot_brain(objIn, how="full", thr_upper=None, thr_lower=None, save=False, **kwargs):
248+
def plot_brain(
249+
objIn,
250+
how="full",
251+
thr_upper=None,
252+
thr_lower=None,
253+
save=False,
254+
verbose=False,
255+
**kwargs,
256+
):
249257
"""
250258
More complete brain plotting of a Brain_Data instance
251259
@@ -272,16 +280,18 @@ def plot_brain(objIn, how="full", thr_upper=None, thr_lower=None, save=False, **
272280
cmap = "RdBu_r"
273281

274282
if thr_upper is None and thr_lower is None:
275-
print("Plotting unthresholded image")
283+
msg = "Plotting unthresholded image"
276284
else:
277285
if isinstance(thr_upper, str):
278-
print("Plotting top %s of voxels" % thr_upper)
286+
msg = "Plotting top %s of voxels" % thr_upper
279287
elif isinstance(thr_upper, (float, int)):
280-
print("Plotting voxels with stat value >= %s" % thr_upper)
288+
msg = "Plotting voxels with stat value >= %s" % thr_upper
281289
if isinstance(thr_lower, str):
282-
print("Plotting lower %s of voxels" % thr_lower)
290+
msg = "Plotting lower %s of voxels" % thr_lower
283291
elif isinstance(thr_lower, (float, int)):
284-
print("Plotting voxels with stat value <= %s" % thr_lower)
292+
msg = "Plotting voxels with stat value <= %s" % thr_lower
293+
if verbose:
294+
print(msg)
285295

286296
if save:
287297
path, filename = os.path.split(save)

0 commit comments

Comments
 (0)