Repository: Pyomo/pyomo

Issue:
ContinuousSet collocation points have type numpy.float64
## Summary

I was serializing components in a discretized model and unexpectedly saw pickles of Numpy float objects in the ComponentUID indices. The unexpected part was that members of the ContinuousSet had type numpy.float64. Not necessarily a bug, but some mildly unexpected behavior.

### Steps to reproduce the issue

```python
import pyomo.environ as pyo
import pyomo.dae as dae
m = pyo.ConcreteModel()
m.s = dae.ContinuousSet(initialize=[0, 2.0])
disc = pyo.TransformationFactory('dae.collocation')
disc.apply_to(m, wrt=m.s, nfe=2, ncp=3, scheme='LAGRANGE-RADAU')

# Bounds are whatever type the user provided
print(m.s.first(), type(m.s.first()))
print(m.s.last(), type(m.s.last()))

# Finite element points are `float`
fep = m.s.get_finite_elements()[1]
print(fep, type(fep))

# Collocation points are `numpy.float64`
cp = m.s[2]
print(cp, type(fp))
```
Output:
```console
0 <class 'int'>
2.0 <class 'float'>
1.0 <class 'float'>
0.155051 <class 'numpy.float64'>
```


Discussion:

Resolving patch:
diff --git a/pyomo/dae/plugins/colloc.py b/pyomo/dae/plugins/colloc.py
--- a/pyomo/dae/plugins/colloc.py
+++ b/pyomo/dae/plugins/colloc.py
@@ -159,7 +159,7 @@ def calc_cp(alpha, beta, k):
             poly = [sum(pair) for pair in zip(poly, prod)]
 
     cp = numpy.roots(poly)
-    return cp
+    return numpy.sort(cp).tolist()
 
 # BLN: This is a legacy function that was used to calculate the collocation
 # constants for an alternative form of the collocation equations described
@@ -229,7 +229,7 @@ def calc_afinal(cp):
         p = [1]
         for j in range(len(cp) - 1):
             p = conv(p, ptmp[j])
-        afinal.append(numpy.polyval(p, 1.0))
+        afinal.append(float(numpy.polyval(p, 1.0)))
     return afinal
 
 
@@ -301,7 +301,7 @@ def _get_radau_constants(self, currentds):
             alpha = 1
             beta = 0
             k = self._ncp[currentds] - 1
-            cp = sorted(list(calc_cp(alpha, beta, k)))
+            cp = calc_cp(alpha, beta, k)
             cp.insert(0, 0.0)
             cp.append(1.0)
             adot = calc_adot(cp, 1)
@@ -336,7 +336,7 @@ def _get_legendre_constants(self, currentds):
             alpha = 0
             beta = 0
             k = self._ncp[currentds]
-            cp = sorted(list(calc_cp(alpha, beta, k)))
+            cp = calc_cp(alpha, beta, k)
             cp.insert(0, 0.0)
             adot = calc_adot(cp, 1)
             adotdot = calc_adot(cp, 2)


Test patch:
diff --git a/pyomo/dae/tests/test_colloc.py b/pyomo/dae/tests/test_colloc.py
--- a/pyomo/dae/tests/test_colloc.py
+++ b/pyomo/dae/tests/test_colloc.py
@@ -75,6 +75,7 @@ def test_disc_single_index_radau(self):
 
         for idx, val in enumerate(list(m.t)):
             self.assertAlmostEqual(val, expected_disc_points[idx])
+            self.assertTrue(type(val) in [float, int])
 
         self.assertTrue(hasattr(m, '_pyomo_dae_reclassified_derivativevars'))
         self.assertTrue(m._pyomo_dae_reclassified_derivativevars[0] is m.dv1)
@@ -174,6 +175,7 @@ def test_disc_single_index_legendre(self):
 
         for idx, val in enumerate(list(m.t)):
             self.assertAlmostEqual(val, expected_disc_points[idx])
+            self.assertTrue(type(val) in [float, int])
 
         self.assertTrue(hasattr(m, '_pyomo_dae_reclassified_derivativevars'))
         self.assertTrue(m.dv1 in m._pyomo_dae_reclassified_derivativevars)