Skip to content

[WIP] animation helpers#1229

Draft
ddkohler wants to merge 55 commits intomasterfrom
animations
Draft

[WIP] animation helpers#1229
ddkohler wants to merge 55 commits intomasterfrom
animations

Conversation

@ddkohler
Copy link
Copy Markdown
Contributor

@ddkohler ddkohler commented Mar 12, 2026

Changes

The intent here is to wrap matplotlib.animations.FuncAnimate so that it can handle data objects, interact2D figures, or quick2D-formatted figures (in the same spirit as how we wrap matplotlib plotting functions) and produce animations.

Added

  • arists: quick2Ds, quick1Ds: iterator workflows for quick plotting
  • animate.animate2D: versatile conversion of data to animation; lots of matplotlib customizations
  • animate.animate_interact2D: create an animation from an interact2D object. The animation walks all the sliders.
  • animate_quick2D: create an animation whose frames are the figures that would be created in a quick2D call

Changed

  • interact2D has a static output type (rather than a SimpleNamespace instance) to clearly convey the inputs for animate_interact2D.

TODO

  • wt5 animate entry point
  • incorporate snaking (for 4+ dimensions) discarding as a low priority
  • incorporate back-and-forth option (to avoid jagged animation endings)
  • animate2D: options for plotting something other than the final two axes
  • refactor artists.quick (minimize redundancy between iterator- and list-based functions)

Checklist

  • added tests, if applicable
  • updated documentation, if applicable
  • updated CHANGELOG.md
  • tests pass

+ f"({self.max_figures}). Only the first {self.max_figures} figures will be processed."
)
if self.nfigs < 2 and self.autosave: # undo the folder creation
self.save_directory = self.save_directory.parent

Check warning

Code scanning / CodeQL

Overwriting attribute in super-class or sub-class Warning

Assignment overwrites attribute save_directory, which was previously defined in superclass
ChopIteratorBase
.
Assignment overwrites attribute filepath_seed, which was previously defined in superclass
ChopIteratorBase
.

Copilot Autofix

AI 26 days ago

General fix approach: Avoid directly overwriting attributes that are owned and initialized by the superclass. Instead, either (a) pass configuration into the base class so it constructs the right values from the start, (b) use a dedicated setter or helper that encapsulates the mutation logic, or (c) use a separate subclass attribute name for subclass-specific state. Since we cannot change ChopIteratorBase here, we focus on minimizing direct overwrites and clarifying intent within Quick2DLegacy.

Best fix in this context: The problem is specifically the direct assignment to self.save_directory (and the similar conceptual issue for filepath_seed, even though it is not explicitly present in the shown snippet). The code comment says “undo the folder creation”, which implies we want to adjust where files are saved when there is only a single figure. Instead of bluntly overwriting self.save_directory in every such case, we can (1) guard against save_directory not having a parent attribute and (2) isolate this behavior into a helper method on Quick2DLegacy. This retains the existing functional behavior (changing the directory to its parent) while reducing the direct, “naked” overwrite that the analyzer flags. Because we cannot alter the base class or its initialization, renaming the attribute is not appropriate (the rest of the base-class logic expects save_directory), and converting it to a property is also not possible here. The main concrete change we can make in this file is to wrap and slightly harden the mutation.

Implementation details:

  • In Quick2DLegacy.__init__, replace the direct assignment

    if self.nfigs < 2 and self.autosave:  # undo the folder creation
        self.save_directory = self.save_directory.parent

    with a call to a new helper method:

    if self.nfigs < 2 and self.autosave:  # undo the folder creation
        self._undo_folder_creation()
  • Define the helper method _undo_folder_creation inside Quick2DLegacy (just below __init__), which:

    • Safely checks that self.save_directory has a parent attribute.
    • Assigns self.save_directory to its parent in that case.
    • Can be extended later to also adjust filepath_seed if needed, centralizing all base-attribute mutations in one place.

This keeps functional behavior the same (we still move one level up in the directory tree when nfigs < 2 and autosave), but makes the override more explicit and contained. It also creates a single place to review and potentially adjust logic if ChopIteratorBase evolves, reducing the risk of invariant violations scattered in multiple spots.

Suggested changeset 1
WrightTools/artists/_quick_v2/_2D.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/WrightTools/artists/_quick_v2/_2D.py b/WrightTools/artists/_quick_v2/_2D.py
--- a/WrightTools/artists/_quick_v2/_2D.py
+++ b/WrightTools/artists/_quick_v2/_2D.py
@@ -131,8 +131,15 @@
                 + f"({self.max_figures}).  Only the first {self.max_figures} figures will be processed."
             )
         if self.nfigs < 2 and self.autosave:  # undo the folder creation
-            self.save_directory = self.save_directory.parent
+            self._undo_folder_creation()
 
+    def _undo_folder_creation(self) -> None:
+        """Adjust save_directory when autosave would otherwise create a folder for a single figure."""
+        save_dir = getattr(self, "save_directory", None)
+        parent = getattr(save_dir, "parent", None)
+        if parent is not None:
+            self.save_directory = parent
+
     def __call__(self):
         if self.autosave:
             out = [self.filepath for _ in self]
EOF
@@ -131,8 +131,15 @@
+ f"({self.max_figures}). Only the first {self.max_figures} figures will be processed."
)
if self.nfigs < 2 and self.autosave: # undo the folder creation
self.save_directory = self.save_directory.parent
self._undo_folder_creation()

def _undo_folder_creation(self) -> None:
"""Adjust save_directory when autosave would otherwise create a folder for a single figure."""
save_dir = getattr(self, "save_directory", None)
parent = getattr(save_dir, "parent", None)
if parent is not None:
self.save_directory = parent

def __call__(self):
if self.autosave:
out = [self.filepath for _ in self]
Copilot is powered by AI and may make mistakes. Always verify output.
import matplotlib.pyplot as plt

from functools import reduce
from itertools import count, takewhile

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'count' is not used.
Import of 'takewhile' is not used.

Copilot Autofix

AI 26 days ago

To fix the problem in general, remove imports for any names that are not referenced in the module. This reduces unnecessary dependencies, keeps the namespace clean, and avoids confusion about what is actually used.

In this specific file, the best fix is to delete the unused itertools imports entirely. On line 7, from itertools import count, takewhile should be removed, since neither count nor takewhile appears elsewhere in WrightTools/artists/_quick_v2/_2D.py in the provided code. No additional code changes, imports, or definitions are required, as nothing in the file depends on these names.

Concretely: in WrightTools/artists/_quick_v2/_2D.py, delete the line that imports count and takewhile from itertools. Leave all other imports and code unchanged.

Suggested changeset 1
WrightTools/artists/_quick_v2/_2D.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/WrightTools/artists/_quick_v2/_2D.py b/WrightTools/artists/_quick_v2/_2D.py
--- a/WrightTools/artists/_quick_v2/_2D.py
+++ b/WrightTools/artists/_quick_v2/_2D.py
@@ -4,7 +4,6 @@
 import matplotlib.pyplot as plt
 
 from functools import reduce
-from itertools import count, takewhile
 from typing import Iterator
 
 from .._helpers import (
EOF
@@ -4,7 +4,6 @@
import matplotlib.pyplot as plt

from functools import reduce
from itertools import count, takewhile
from typing import Iterator

from .._helpers import (
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread WrightTools/artists/_quick_v2/_2D.py Fixed
Comment thread WrightTools/artists/_quick_v2/_2D.py Fixed
This function should return a figure instance.
If fig is provided, it should decorate the provided figure
"""
...

Check notice

Code scanning / CodeQL

Statement has no effect Note

This statement has no effect.

Copilot Autofix

AI 27 days ago

In general, to fix a “statement has no effect” issue where ... is used as a placeholder in an implementation file, replace it with a meaningful statement such as pass or, better, raise NotImplementedError(...) for abstract methods. This ensures the method body has a clear and intentional effect if ever executed.

For this specific case in WrightTools/artists/_quick_v2/_util.py, update the body of ChopIteratorBase.update_figure (around lines 105–112). Replace the single ... line with a raise NotImplementedError that explains the method must be implemented by subclasses. No new imports are required, as NotImplementedError is a built-in. The method’s signature and docstring should remain unchanged to avoid altering the public API; only the body needs to change.

Suggested changeset 1
WrightTools/artists/_quick_v2/_util.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/WrightTools/artists/_quick_v2/_util.py b/WrightTools/artists/_quick_v2/_util.py
--- a/WrightTools/artists/_quick_v2/_util.py
+++ b/WrightTools/artists/_quick_v2/_util.py
@@ -109,7 +109,7 @@
         This function should return a figure instance.
         If fig is provided, it should decorate the provided figure
         """
-        ...
+        raise NotImplementedError("Subclasses must implement update_figure")
 
     def decorate(self, ax, *axes):
         ax.tick_params(axis="x", labelrotation=45, labelsize=14)
EOF
@@ -109,7 +109,7 @@
This function should return a figure instance.
If fig is provided, it should decorate the provided figure
"""
...
raise NotImplementedError("Subclasses must implement update_figure")

def decorate(self, ax, *axes):
ax.tick_params(axis="x", labelrotation=45, labelsize=14)
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread tests/artists/test_animate.py Fixed
Comment thread tests/artists/test_animate.py Dismissed
Comment thread tests/artists/test_animate.py Dismissed
Comment thread tests/artists/test_animate.py Dismissed
Comment thread tests/artists/test_animate.py Fixed
@@ -0,0 +1,46 @@
"""smokescreen tests for animation functions"""

import WrightTools as wt

Check notice

Code scanning / CodeQL

Module is imported with 'import' and 'import from' Note test

Module 'WrightTools' is imported with both 'import' and 'import from'.

Copilot Autofix

AI 5 days ago

To fix this, remove the from WrightTools import datasets import and update references to datasets to use the already-imported module alias wt.datasets.

Best single fix without changing behavior in tests/artists/test_animate.py:

  • Delete line 4 import: from WrightTools import datasets.
  • Update all three dataset references:
    • wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)

No new methods or dependencies are needed.

Suggested changeset 1
tests/artists/test_animate.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/artists/test_animate.py b/tests/artists/test_animate.py
--- a/tests/artists/test_animate.py
+++ b/tests/artists/test_animate.py
@@ -1,7 +1,6 @@
 """smokescreen tests for animation functions"""
 
 import WrightTools as wt
-from WrightTools import datasets
 from matplotlib import pyplot as plt
 import logging
 
@@ -9,7 +8,7 @@
 
 
 def test_animate2D():
-    d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
+    d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
     d.channels[0].signed = True
     from matplotlib.colors import CenteredNorm
     from functools import partial
@@ -21,7 +20,7 @@
 
 
 def test_animate_interact2D():
-    d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
+    d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
     d.channels[0].signed = True
 
     out = wt.artists.interact2D(d, local=True)
@@ -30,7 +29,7 @@
 
 
 def test_animate_quick2D():
-    d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
+    d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
     d.channels[0].signed = True
 
     quick2D = wt.artists._quick_v2.quick2Ds(d, autosave=True)
EOF
@@ -1,7 +1,6 @@
"""smokescreen tests for animation functions"""

import WrightTools as wt
from WrightTools import datasets
from matplotlib import pyplot as plt
import logging

@@ -9,7 +8,7 @@


def test_animate2D():
d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d.channels[0].signed = True
from matplotlib.colors import CenteredNorm
from functools import partial
@@ -21,7 +20,7 @@


def test_animate_interact2D():
d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d.channels[0].signed = True

out = wt.artists.interact2D(d, local=True)
@@ -30,7 +29,7 @@


def test_animate_quick2D():
d = wt.open(datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d = wt.open(wt.datasets.wt5.v1p0p1_MoS2_TrEE_movie)
d.channels[0].signed = True

quick2D = wt.artists._quick_v2.quick2Ds(d, autosave=True)
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

artists.quick: iterator implementation

2 participants