Repository: PlasmaControl/DESC

Issue:
I/O warning for profiles without `params` attribute
The `_Profile` class defines the I/O attribute `"_params"` that all other profiles inherit. But `PowerProfile`, `ProductProfile`, `ScaledProfile`, and `SumProfile` do not have a `self._params` attribute, they only define a `self.params` "getter" method. This leads to a `RuntimeWarning` when saving and/or loading these profiles. 

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b9f33067..348e970cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,8 @@ New Feature
 
 Bug Fixes
 
-- Small bug fix to use the correct normalization length ``a`` in the BallooningStability objective
+- Small bug fix to use the correct normalization length ``a`` in the BallooningStability objective.
+- Fixed I/O bug when saving/loading ``_Profile`` classes that do not have a ``_params`` attribute.
 
 v0.13.0
 -------
diff --git a/desc/profiles.py b/desc/profiles.py
index ae1684abb..faaa7e3ab 100644
--- a/desc/profiles.py
+++ b/desc/profiles.py
@@ -35,7 +35,7 @@ class _Profile(IOAble, ABC):
     Subclasses must also implement getter and setter methods for params
     """
 
-    _io_attrs_ = ["_name", "_params"]
+    _io_attrs_ = ["_name"]
 
     def __init__(self, name=""):
         self.name = name
@@ -673,7 +673,7 @@ class PowerSeriesProfile(_Profile):
 
     """
 
-    _io_attrs_ = _Profile._io_attrs_ + ["_basis"]
+    _io_attrs_ = _Profile._io_attrs_ + ["_params", "_basis"]
 
     def __init__(self, params=None, modes=None, sym="auto", name=""):
         super().__init__(name)
@@ -848,7 +848,7 @@ class TwoPowerProfile(_Profile):
 
     """
 
-    _io_attrs_ = _Profile._io_attrs_
+    _io_attrs_ = _Profile._io_attrs_ + ["_params"]
 
     def __init__(self, params=None, name=""):
         super().__init__(name)
@@ -954,7 +954,7 @@ class SplineProfile(_Profile):
 
     """
 
-    _io_attrs_ = _Profile._io_attrs_ + ["_knots", "_method"]
+    _io_attrs_ = _Profile._io_attrs_ + ["_params", "_knots", "_method"]
 
     def __init__(self, values=None, knots=None, method="cubic2", name=""):
         super().__init__(name)
@@ -1048,7 +1048,7 @@ class HermiteSplineProfile(_Profile):
 
     """
 
-    _io_attrs_ = _Profile._io_attrs_ + ["_knots", "_params"]
+    _io_attrs_ = _Profile._io_attrs_ + ["_params", "_knots"]
 
     def __init__(self, f, df, knots=None, name=""):
         super().__init__(name)
@@ -1149,6 +1149,8 @@ class MTanhProfile(_Profile):
 
     """
 
+    _io_attrs_ = _Profile._io_attrs_ + ["_params"]
+
     def __init__(self, params=None, name=""):
         super().__init__(name)
 
@@ -1383,7 +1385,7 @@ class FourierZernikeProfile(_Profile):
 
     """
 
-    _io_attrs_ = _Profile._io_attrs_ + ["_basis"]
+    _io_attrs_ = _Profile._io_attrs_ + ["_params", "_basis"]
 
     def __init__(self, params=None, modes=None, sym="auto", NFP=1, name=""):
         super().__init__(name)


Test patch:
diff --git a/tests/test_input_output.py b/tests/test_input_output.py
index 6ac1d4433..9ad533723 100644
--- a/tests/test_input_output.py
+++ b/tests/test_input_output.py
@@ -19,6 +19,14 @@ from desc.magnetic_fields import (
     SplineMagneticField,
     ToroidalMagneticField,
 )
+from desc.profiles import (
+    PowerProfile,
+    PowerSeriesProfile,
+    ProductProfile,
+    ScaledProfile,
+    SumProfile,
+    TwoPowerProfile,
+)
 from desc.transform import Transform
 from desc.utils import equals
 
@@ -666,6 +674,26 @@ def test_save_after_load(tmpdir_factory):
     assert eq2.equiv(eq)
 
 
+@pytest.mark.unit
+def test_io_profiles(tmpdir_factory):
+    """Test saving/loading profiles. Test for GH issue #1448."""
+    p0 = SumProfile(
+        TwoPowerProfile(params=[0.6, 2, 1.5]), PowerSeriesProfile(params=[0.4, 0, -0.4])
+    )
+    n = ScaledProfile(2, PowerProfile(1 / 3, p0))
+    T = ScaledProfile(0.5, PowerProfile(2 / 3, p0))
+    p1 = ProductProfile(n, T)
+
+    tmpdir = tmpdir_factory.mktemp("profiles")
+    tmp_path = tmpdir.join("p0.h5")
+    p1.save(tmp_path)
+    p2 = load(tmp_path)
+
+    x = np.linspace(0.1, 0.9, 9)
+    np.testing.assert_allclose(p0(x), p1(x))
+    np.testing.assert_allclose(p1(x), p2(x))
+
+
 @pytest.mark.unit
 def test_io_OmnigenousField(tmpdir_factory):
     """Test saving/loading an OmnigenousField works (tests dict saving)."""