Repository: yt-project/yt

Issue:
Cannot set linear scaling in certain PhasePlots
### Bug report

**Bug summary**

There appears to be a bug in PhasePlot in how it calculates extrema on subsequent passes, and it leads to failures when trying to changing PhasePlot parameters like which fields are set to log scaling or linear scaling.

**Code for reproduction**

This code creates a sphere object, then it attempts to create a PhasePlot of spherical theta vs spherical radius.  

```python
import yt
ds = yt.load_sample('FIRE_M12i_ref11')
_, c = ds.find_max(('gas', 'density'))
sp = ds.sphere(c, (150.0, 'kpc'))
p = yt.PhasePlot(sp, ('PartType0', 'particle_position_spherical_theta'), ('PartType0', 'particle_position_spherical_radius'), ('gas', 'mass'), weight_field=None)
p.set_log(('PartType0', 'particle_position_spherical_theta'), False)
p.save()
```

**Actual outcome**

The code fails on the line where we try to change the theta field to linear scaling (the second to last line).

```
Traceback (most recent call last):
  File "test_profile.py", line 7, in <module>
    p.save()
  File "/Users/chummels/src/yt/yt/visualization/plot_container.py", line 102, in newfunc
    args[0]._recreate_profile()
  File "/Users/chummels/src/yt/yt/visualization/profile_plotter.py", line 1611, in _recreate_profile
    self._profile = create_profile(
  File "/Users/chummels/src/yt/yt/data_objects/profiles.py", line 1443, in create_profile
    raise YTIllDefinedBounds(mi, ma)
yt.utilities.exceptions.YTIllDefinedBounds: The bounds -4.941e-324 and 5.233e+23 are ill-defined. Typically this happens when a log binning is specified and zero or negative values are given for the bounds.
```

**Expected outcome**

For it to change the theta field to linear scaling and then output a PhasePlot.

**Notes**

If one removes the second-to-last line: `p.set_log(('PartType0', 'particle_position_spherical_theta'), False)`, the code snippet above works perfectly fine.  In digging into this, the issue appears to in the extrema code [here](https://github.com/yt-project/yt/blob/main/yt/data_objects/profiles.py#L1350-L1411).  When yt initially generates the ProfilePlot, `extrema` is equal to None, so it enters the first conditional and correctly calculates that the extrema of the two fields (theta, radius): 

```
extrema = None
ex = unyt_array([0.00500881, 3.1364112 ], '(dimensionless)'), unyt_array([6.65419743e+19, 5.23294092e+23], 'cm')
```

And everything is fine.  But if the last line `p.set_log(('PartType0', 'particle_position_spherical_theta'), False)` is included in our script, yt goes back through all of the `ProfilePlot` code and runs it again (I'm not sure why).  This time, though, `extrema` is *not* None (but it's just composed of all Nones) so it enters the second conditional, which is a bunch of messy code that I cannot really follow.  The outcome is that the extrema minima are now negative, and yt fails:

```
extrema = {('PartType0', 'particle_position_spherical_theta'): (None, None), ('PartType0', 'particle_position_spherical_radius'): (None, None)}
ex = [[array(-5.e-324), array(3.1364112)], [array(-5.e-324), array(5.23294092e+23)]]
```

I'm submitting a PR that simply checks to see if `extrema` is composed of all Nones, and if so, it treats it as though it is None, so the same logic occurs.  But maybe there is a better way?

**Version Information**
dev tip:
```
commit aaaf4d00141b123c089f59978ac81bdfaf8de023 (HEAD -> main, upstream/main)
Merge: 4ecf7bcfb aa733c0f6
Author: Madicken Munk <madicken.munk@gmail.com>
Date:   Fri Jul 9 16:49:36 2021 -0500

    Merge pull request #3427 from yt-project/revert-3425-3269_pplot_zlabel

    Revert "Use a fancier repr for labels in Profile/PhasePlots. Fixes #3269"
```


Discussion:

Resolving patch:
diff --git a/yt/data_objects/profiles.py b/yt/data_objects/profiles.py
--- a/yt/data_objects/profiles.py
+++ b/yt/data_objects/profiles.py
@@ -1,4 +1,5 @@
 import numpy as np
+from more_itertools import collapse
 
 from yt.data_objects.field_data import YTFieldData
 from yt.fields.derived_field import DerivedField
@@ -1347,7 +1348,9 @@ def create_profile(
         else:
             logs_list.append(data_source.ds.field_info[bin_field].take_log)
     logs = logs_list
-    if extrema is None:
+
+    # Are the extrema all Nones? Then treat them as though extrema was set as None
+    if extrema is None or not any(collapse(extrema.values())):
         ex = [
             data_source.quantities["Extrema"](f, non_zero=l)
             for f, l in zip(bin_fields, logs)


Test patch:
diff --git a/yt/data_objects/tests/test_profiles.py b/yt/data_objects/tests/test_profiles.py
--- a/yt/data_objects/tests/test_profiles.py
+++ b/yt/data_objects/tests/test_profiles.py
@@ -642,6 +642,21 @@ def test_unequal_bin_field_profile(self):
                 ("gas", "mass"),
             )
 
+    def test_set_linear_scaling_for_none_extrema(self):
+        # See Issue #3431
+        # Ensures that extrema are calculated in the same way on subsequent passes
+        # through the PhasePlot machinery.
+        ds = fake_sph_orientation_ds()
+        p = yt.PhasePlot(
+            ds,
+            ("all", "particle_position_spherical_theta"),
+            ("all", "particle_position_spherical_radius"),
+            ("all", "particle_mass"),
+            weight_field=None,
+        )
+        p.set_log(("all", "particle_position_spherical_theta"), False)
+        p.save()
+
 
 def test_index_field_units():
     # see #1849