Repository: pyro-ppl/pyro

Issue:
Warn about duplicate pyro.sample sites
This mistake can arise in two common scenarios
```py
def model():
    x = pyro.sample("x", dist.normal, mu, sigma)
    x = pyro.sample("x", dist.normal, x, sigma)  # This should error

def model():
    for i in pyro.irange("data", 2):
        x = pyro.sample("x", dist.normal, mu, sigma)  # Should error on 2nd iteraion
```
The latter is subtle and will likely confuse beginners.

There are already xfailing tests for these examples in `tests/infer/test_valid_models.py`:
- [`test_variable_clash_in_model_error()`](https://github.com/uber/pyro/blob/cab2bd3/tests/infer/test_valid_models.py#L46)
- [`test_variable_clash_in_guide_error()`](https://github.com/uber/pyro/blob/cab2bd3/tests/infer/test_valid_models.py#L62)
- [`test_irange_variable_clash_error()`](https://github.com/uber/pyro/blob/cab2bd3/tests/infer/test_valid_models.py#L95)

One complicating issue is that `TracePoutine.sample()` currently [silently allows re-sampling](https://github.com/uber/pyro/blob/cab2bd308ae76cde5b5cbb0b81009d854b238df1/pyro/poutine/trace_poutine.py#L189), and this functionality is used by `iter_discrete_traces()` for replay. We'll need to modify other code before replacing this `xxx` with an error; @eb8680 is probably most knowledgable about the details.


Discussion:
observations dont _always_ require unique names so:
` x = pyro.sample("obs", dist.normal, mu, sigma, obs=y)`
should be ok (maybe with warning?)
That's if an `observe` name is the same as a `sample` name. Not two observe
names

On Oct 29, 2017 2:42 PM, "Fritz Obermeyer" <notifications@github.com> wrote:

> observations
>
> hmm, repeated observation seems like a separate issue. I believe
> duplicated observations currently *do* raise a RuntimeError (code
> <https://github.com/uber/pyro/blob/b71d91fd6d3339f1572b9fa31ae23cc0026c3651/pyro/poutine/trace_poutine.py#L187>
> ).
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/uber/pyro/issues/398#issuecomment-340302265>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AByHSXxRNkvElRqesMS7HFIrkv4QkGYnks5sxPE-gaJpZM4QKbfe>
> .
>


Resolving patch:
diff --git a/pyro/poutine/__init__.py b/pyro/poutine/__init__.py
--- a/pyro/poutine/__init__.py
+++ b/pyro/poutine/__init__.py
@@ -10,7 +10,7 @@
 from .lambda_poutine import LambdaPoutine  # noqa: F401
 from .escape_poutine import EscapePoutine
 
-# trace data structures
+import pyro
 from .trace import Trace  # noqa: F401
 from pyro import util
 
@@ -186,6 +186,8 @@ def _fn(*args, **kwargs):
                                    functools.partial(escape_fn, next_trace)))
                 return ftr(*args, **kwargs)
             except util.NonlocalExit as site_container:
+                for frame in pyro._PYRO_STACK:
+                    frame._reset()
                 for tr in extend_fn(ftr.trace.copy(), site_container.site,
                                     num_samples=num_samples):
                     queue.put(tr)
diff --git a/pyro/poutine/poutine.py b/pyro/poutine/poutine.py
--- a/pyro/poutine/poutine.py
+++ b/pyro/poutine/poutine.py
@@ -112,6 +112,14 @@ def __exit__(self, exc_type, exc_value, traceback):
                 for i in range(0, loc + 1):
                     pyro._PYRO_STACK.pop(0)
 
+    def _reset(self):
+        """
+        Resets the computation to the beginning, un-sampling all sample sites.
+
+        By default, does nothing, but overridden in derived classes.
+        """
+        pass
+
     def _prepare_site(self, msg):
         """
         :param msg: current message at a trace site
diff --git a/pyro/poutine/trace_poutine.py b/pyro/poutine/trace_poutine.py
--- a/pyro/poutine/trace_poutine.py
+++ b/pyro/poutine/trace_poutine.py
@@ -165,6 +165,15 @@ def get_trace(self, *args, **kwargs):
         self(*args, **kwargs)
         return self.trace.copy()
 
+    def _reset(self):
+        tr = Trace(graph_type=self.graph_type)
+        tr.add_node("_INPUT",
+                    name="_INPUT", type="input",
+                    args=self.trace.nodes["_INPUT"]["args"],
+                    kwargs=self.trace.nodes["_INPUT"]["kwargs"])
+        self.trace = tr
+        super(TracePoutine, self)._reset()
+
     def _pyro_sample(self, msg):
         """
         :param msg: current message at a trace site.
@@ -180,19 +189,13 @@ def _pyro_sample(self, msg):
         """
         name = msg["name"]
         if name in self.trace:
-            # Cannot repeat names between params and samples
-            if self.trace.nodes[name]['type'] == 'param':
+            site = self.trace.nodes[name]
+            if site['type'] == 'param':
+                # Cannot sample or observe after a param statement.
                 raise RuntimeError("{} is already in the trace as a param".format(name))
-            # observe has the same name as a sample
-            if msg['is_observed'] and not self.trace.nodes[name]['is_observed']:
-                raise RuntimeError("observe cannot have the same name as a sample")
-            # XXX temporary solution - otherwise, we reset self.trace to be empty
-            tr = Trace(graph_type=self.graph_type)
-            tr.add_node("_INPUT",
-                        name="_INPUT", type="input",
-                        args=self.trace.nodes["_INPUT"]["args"],
-                        kwargs=self.trace.nodes["_INPUT"]["kwargs"])
-            self.trace = tr
+            elif site['type'] == 'sample':
+                # Cannot sample after a previous sample statement.
+                raise RuntimeError("Multiple pyro.sample sites named '{}'".format(name))
 
         val = super(TracePoutine, self)._pyro_sample(msg)
         site = msg.copy()


Test patch:
diff --git a/tests/infer/test_valid_models.py b/tests/infer/test_valid_models.py
--- a/tests/infer/test_valid_models.py
+++ b/tests/infer/test_valid_models.py
@@ -26,7 +26,7 @@ def assert_error(model, guide, **kwargs):
     Assert that inference fails with an error.
     """
     inference = SVI(model,  guide, Adam({"lr": 1e-6}), "ELBO", **kwargs)
-    with pytest.raises((NotImplementedError, UserWarning, KeyError)):
+    with pytest.raises((NotImplementedError, UserWarning, KeyError, RuntimeError)):
         inference.step()
 
 
@@ -43,7 +43,6 @@ def assert_warning(model, guide, **kwargs):
             print(warning)
 
 
-@pytest.mark.xfail(reason="Did not raise error despite x being sampled twice")
 @pytest.mark.parametrize("trace_graph", [False, True], ids=["trace", "tracegraph"])
 def test_variable_clash_in_model_error(trace_graph):
 
@@ -59,7 +58,6 @@ def guide():
     assert_error(model, guide, trace_graph=trace_graph)
 
 
-@pytest.mark.xfail(reason="Did not raise error despite x being sampled twice")
 @pytest.mark.parametrize("trace_graph", [False, True], ids=["trace", "tracegraph"])
 def test_variable_clash_in_guide_error(trace_graph):
 
@@ -92,7 +90,6 @@ def guide():
     assert_ok(model, guide, trace_graph=trace_graph)
 
 
-@pytest.mark.xfail(reason="Did not raise error despite x being sampled twice")
 @pytest.mark.parametrize("trace_graph", [False, True], ids=["trace", "tracegraph"])
 def test_irange_variable_clash_error(trace_graph):