Repository: markkho/msdm

Issue:
Uniform distribution
The behavior for the `UniformDistribution` is inconsistent when it receives multiple copies of the same element. For instance:

```python
from msdm.core.distributions import UniformDistribution, DictDistribution

dist1 = UniformDistribution('abb')
dist2 = DictDistribution({'a': 1/3, 'b': 2/3})
dist1 == dist2 # False
dist1.prob('b') == dist2.prob('b') # False
```

I see two options and lean towards the 2nd:
1. Enforce that UniformDistribution receives unique elements.
2. Automatically bin equivalent elements when constructing a UniformDistribution and return a DictDistribution. This would end up making UniformDistribution as its own class somewhat superfluous.

Thoughts @cgc ?

Discussion:

Patch:
diff --git a/msdm/core/distributions/dictdistribution.py b/msdm/core/distributions/dictdistribution.py
index b271752..88fce1f 100644
--- a/msdm/core/distributions/dictdistribution.py
+++ b/msdm/core/distributions/dictdistribution.py
@@ -4,7 +4,11 @@ import random
 
 
 class UniformDistribution(FiniteDistribution):
-    def __init__(self, support):
+    def __init__(self, support, check_unique=True):
+        if check_unique:
+            assert len(support) == len(set(support)), (
+                f'Invalid support {support}: some event is duplicated.'
+            )
         self._support = support
     @property
     def support(self):
@@ -15,8 +19,6 @@ class UniformDistribution(FiniteDistribution):
         return 0
     def sample(self, *, rng=random):
         return rng.choice(self._support)
-    def __len__(self):
-        return len(self._support)
 
 class DeterministicDistribution(FiniteDistribution):
     def __init__(self, value):
@@ -32,8 +34,6 @@ class DeterministicDistribution(FiniteDistribution):
         return self.value
     def items(self):
         yield self.value, 1
-    def __len__(self):
-        return 1
 
 class DictDistribution(dict,FiniteDistribution):
     @classmethod
@@ -60,7 +60,6 @@ class DictDistribution(dict,FiniteDistribution):
 
     items = dict.items
     values = dict.values
-    __len__ = dict.__len__
     __or__ = FiniteDistribution.__or__
     __and__ = FiniteDistribution.__and__
     __mul__ = FiniteDistribution.__mul__
diff --git a/msdm/core/distributions/distributions.py b/msdm/core/distributions/distributions.py
index 38c6ff5..f6758ae 100644
--- a/msdm/core/distributions/distributions.py
+++ b/msdm/core/distributions/distributions.py
@@ -21,9 +21,8 @@ class FiniteDistribution(Distribution[Event]):
     def support(self) -> Sequence[Event]:
         pass
 
-    @abstractmethod
     def __len__(self):
-        pass
+        return len(self.support)
 
     def sample(self, *, rng=random, k=1) -> Event:
         support = self.support


Test patch:
diff --git a/msdm/tests/test_core_distributions.py b/msdm/tests/test_core_distributions.py
index 50d17ea..4befb84 100644
--- a/msdm/tests/test_core_distributions.py
+++ b/msdm/tests/test_core_distributions.py
@@ -6,6 +6,7 @@ import pandas as pd
 from scipy.special import softmax
 from msdm.core.distributions import DiscreteFactorTable, DictDistribution,\
     UniformDistribution, DeterministicDistribution, SoftmaxDistribution
+import pytest
 
 def toDF(p):
     df = pd.DataFrame(p.support)
@@ -95,9 +96,17 @@ class DistributionTestCase(unittest.TestCase):
             res = d & DictDistribution({'a': 0.5, 'b': 0.25, 'c': 0.25})
             assert res.isclose(DictDistribution({'a': 2/3, 'b': 1/3}))
 
-    def test_uniform_and_deterministic_dist(self):
-        assert DictDistribution(a=0.5, b=0.5).isclose(DictDistribution.uniform(['a', 'b']))
-        assert len(DictDistribution.uniform(['a', 'b'])) == 2
+    def test_uniform_dist(self):
+        d = DictDistribution.uniform(['a', 'b'])
+        assert DictDistribution(a=0.5, b=0.5).isclose(d)
+        assert len(d) == 2
+
+        # Handles duplicates by raising
+        with pytest.raises(AssertionError) as e:
+            DictDistribution.uniform('abb')
+        assert 'some event is duplicated' in str(e)
+
+    def test_deterministic_dist(self):
         assert DictDistribution(a=1).isclose(DictDistribution.deterministic('a'))
         assert len(DictDistribution.deterministic('a')) == 1