Repository: yt-project/yt

Issue:
BUG: annotating a sphere then setting font size in a plot results in a crash
### Bug report

**Bug summary**

Discovered by [RevathiJambunathan](https://github.com/RevathiJambunathan). She further reported that this might be a regression in yt 4.2.x

**Code for reproduction**

```python
import yt

ds = yt.load_sample("IsolatedGalaxy")
sl = yt.SlicePlot(ds, 0, ("gas", "density"))

sl.annotate_sphere(
    [0, 0, 0],
    radius=(0.2, "Mpc"),
    circle_args={"color": "white", "linewidth": 2},
)
sl.set_font_size(24) # everything works fine if this line is dropped *or* moved before sl.annotate_sphere(...)

sl.save("/tmp/")
```

**Actual outcome**

<!--The output produced by the above code, which may be a screenshot, console
output, etc.-->

```
/Users/robcleme/dev/yt-project/yt/yt/loaders.py:1670: UserWarning: This dataset appears to be of type EnzoDataset, but the following requirements are currently missing: libconf
Please verify your installation.
  return load(loadable_path, **load_kwargs)
Parsing Hierarchy : 100%|███████████████████████████████████████████████████████████████████████████████████| 173/173 [00:00<00:00, 12522.47it/s]
Traceback (most recent call last):
  File "/Users/robcleme/dev/yt-project/yt/yt/visualization/plot_window.py", line 1285, in run_callbacks
    callback(cbw)
  File "/Users/robcleme/dev/yt-project/yt/yt/visualization/plot_modifications.py", line 2041, in __call__
    self.radius = self.radius.to(units)
                  ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/robcleme/.pyenv/versions/yt-dev/lib/python3.12/site-packages/unyt/array.py", line 947, in to
    return self.in_units(units, equivalence=equivalence, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/robcleme/.pyenv/versions/yt-dev/lib/python3.12/site-packages/unyt/array.py", line 874, in in_units
    (conversion_factor, offset) = self.units.get_conversion_factor(
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/robcleme/.pyenv/versions/yt-dev/lib/python3.12/site-packages/unyt/unit_object.py", line 694, in get_conversion_factor
    return _get_conversion_factor(self, other_units, dtype)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/robcleme/.pyenv/versions/yt-dev/lib/python3.12/site-packages/unyt/unit_object.py", line 939, in _get_conversion_factor
    raise UnitConversionError(
unyt.exceptions.UnitConversionError: Cannot convert between '1/code_length' (dim '1/(length)') and 'code_length' (dim '(length)').

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/robcleme/dev/yt-project/yt/t.py", line 14, in <module>
    sl.save("/tmp/")
  File "/Users/robcleme/dev/yt-project/yt/yt/visualization/_commons.py", line 162, in newfunc
    self._setup_plots()
  File "/Users/robcleme/dev/yt-project/yt/yt/visualization/plot_window.py", line 1226, in _setup_plots
    self.run_callbacks()
  File "/Users/robcleme/dev/yt-project/yt/yt/visualization/plot_window.py", line 1289, in run_callbacks
    raise YTPlotCallbackError(callback._type_name) from e
yt.utilities.exceptions.YTPlotCallbackError: annotate_sphere callback failed
```

**Expected outcome**

No crash

**Version Information**
  * Operating System: macOS
  * Python Version: 3.11.4
  * yt version: 4.3.0
  * Other Libraries (if applicable):




Discussion:
Looks like this is because the callback overwrites the `radius` attribute with a scaled version, which changes the units from length to either dimensionless value or 1/length. So when it runs back through the callback (cause calling `set_font_size` invalidates the plot), the `radius` attribute will be the already-scaled value with units that can't convert to dimensions of length.

https://github.com/yt-project/yt/blob/14c49c4fa832fb7f04485a16134d9adc2916508d/yt/visualization/plot_modifications.py#L2033C1-L2050C59 

```
        if is_sequence(self.radius):
            self.radius = plot.data.ds.quan(self.radius[0], self.radius[1])
            self.radius = np.float64(self.radius.in_units(plot.xlim[0].units))
        if isinstance(self.radius, YTQuantity):
            if isinstance(self.center, YTArray):
                units = self.center.units
            else:
                units = "code_length"
            self.radius = self.radius.to(units)


        # This assures the radius has the appropriate size in
        # the different coordinate systems, since one cannot simply
        # apply a different transform for a length in the same way
        # you can for a coordinate.
        if self.coord_system == "data" or self.coord_system == "plot":
            self.radius = self.radius * self._pixel_scale(plot)[0]
        else:
            self.radius /= (plot.xlim[1] - plot.xlim[0]).v
```

simplest fix would be to not overwrite `self.radius`. but could also add a `self._scaled_radius` attribute to avoid re-calculating if it's already been through once? I'll keep playing around and push something up shortly. 


Resolving patch:
diff --git a/yt/visualization/plot_modifications.py b/yt/visualization/plot_modifications.py
--- a/yt/visualization/plot_modifications.py
+++ b/yt/visualization/plot_modifications.py
@@ -1806,11 +1806,12 @@ def __call__(self, plot):
         xx0, xx1, yy0, yy1 = self._plot_bounds(plot)
         # normalize all of the kwarg lengths to the plot size
         plot_diag = ((yy1 - yy0) ** 2 + (xx1 - xx0) ** 2) ** (0.5)
-        self.length *= plot_diag
-        self.width *= plot_diag
-        self.head_width *= plot_diag
+        length = self.length * plot_diag
+        width = self.width * plot_diag
+        head_width = self.head_width * plot_diag
+        head_length = None
         if self.head_length is not None:
-            self.head_length *= plot_diag
+            head_length = self.head_length * plot_diag
 
         if self.starting_pos is not None:
             start_x, start_y = self._sanitize_coord_system(
@@ -1819,8 +1820,8 @@ def __call__(self, plot):
             dx = x - start_x
             dy = y - start_y
         else:
-            dx = (xx1 - xx0) * 2 ** (0.5) * self.length
-            dy = (yy1 - yy0) * 2 ** (0.5) * self.length
+            dx = (xx1 - xx0) * 2 ** (0.5) * length
+            dy = (yy1 - yy0) * 2 ** (0.5) * length
         # If the arrow is 0 length
         if dx == dy == 0:
             warnings.warn("The arrow has zero length. Not annotating.", stacklevel=2)
@@ -1833,9 +1834,9 @@ def __call__(self, plot):
                 y - dy,
                 dx,
                 dy,
-                width=self.width,
-                head_width=self.head_width,
-                head_length=self.head_length,
+                width=width,
+                head_width=head_width,
+                head_length=head_length,
                 transform=self.transform,
                 length_includes_head=True,
                 **self.plot_args,
@@ -1847,9 +1848,9 @@ def __call__(self, plot):
                     y[i] - dy,
                     dx,
                     dy,
-                    width=self.width,
-                    head_width=self.head_width,
-                    head_length=self.head_length,
+                    width=width,
+                    head_width=head_width,
+                    head_length=head_length,
                     transform=self.transform,
                     length_includes_head=True,
                     **self.plot_args,
@@ -2032,7 +2033,8 @@ def __call__(self, plot):
 
         if is_sequence(self.radius):
             self.radius = plot.data.ds.quan(self.radius[0], self.radius[1])
-            self.radius = np.float64(self.radius.in_units(plot.xlim[0].units))
+            self.radius = self.radius.in_units(plot.xlim[0].units)
+
         if isinstance(self.radius, YTQuantity):
             if isinstance(self.center, YTArray):
                 units = self.center.units
@@ -2045,16 +2047,18 @@ def __call__(self, plot):
         # apply a different transform for a length in the same way
         # you can for a coordinate.
         if self.coord_system == "data" or self.coord_system == "plot":
-            self.radius = self.radius * self._pixel_scale(plot)[0]
+            scaled_radius = self.radius * self._pixel_scale(plot)[0]
         else:
-            self.radius /= (plot.xlim[1] - plot.xlim[0]).v
+            scaled_radius = self.radius / (plot.xlim[1] - plot.xlim[0])
 
         x, y = self._sanitize_coord_system(
             plot, self.center, coord_system=self.coord_system
         )
 
         x, y = self._sanitize_xy_order(plot, x, y)
-        cir = Circle((x, y), self.radius, transform=self.transform, **self.circle_args)
+        cir = Circle(
+            (x, y), scaled_radius.v, transform=self.transform, **self.circle_args
+        )
 
         plot._axes.add_patch(cir)
         if self.text is not None:


Test patch:
diff --git a/yt/visualization/tests/test_callbacks.py b/yt/visualization/tests/test_callbacks.py
--- a/yt/visualization/tests/test_callbacks.py
+++ b/yt/visualization/tests/test_callbacks.py
@@ -404,6 +404,23 @@ def test_sphere_callback():
         assert_fname(p.save(prefix)[0])
 
 
+def test_invalidated_annotations():
+    # check that annotate_sphere and annotate_arrow succeed on re-running after
+    # an operation that invalidates the plot (set_font_size), see
+    # https://github.com/yt-project/yt/issues/4698
+
+    ds = fake_amr_ds(fields=("density",), units=("g/cm**3",))
+    p = SlicePlot(ds, "z", ("gas", "density"))
+    p.annotate_sphere([0.5, 0.5, 0.5], 0.1)
+    p.set_font_size(24)
+    p.render()
+
+    p = SlicePlot(ds, "z", ("gas", "density"))
+    p.annotate_arrow([0.5, 0.5, 0.5])
+    p.set_font_size(24)
+    p.render()
+
+
 def test_text_callback():
     with _cleanup_fname() as prefix:
         ax = "z"