Repository: pgmpy/pgmpy

Issue:
KeyError in DynamicBayesianNetwork.active_trail_nodes()
### Subject of the issue
Invoking the `active_trail_nodes()` method of `DynamicBayesianNetwork` (inherited from `DAG`) raises a KeyError.

### Your environment
* pgmpy version 0.1.11dev
* Python version 3.8.5
* Operating System Manjaro x86_64

### Steps to reproduce
```
dyn_edges = [(('IC1', 0), ('IC1', 1)),
                       (('IC2', 0), ('IC2', 1)),
                       (('IC3', 0), ('IC3', 1)),
                       (('IC4', 0), ('IC4', 1)),
                       (('IC4', 0), ('IC1', 1)),
                       (('IC4', 0), ('IC7', 1)),
                       (('IC4', 0), ('IC13', 1)),
                       (('IC4', 0), ('IC15', 1))]
dyn_model = DynamicBayesianNetwork(dyn_edges)
active_trails = dyn_model.active_trail_nodes(('IC1', 0))
active_trails
```

### Expected behaviour
Should return the active trails of the node.

### Actual behaviour
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/.miniconda3/envs/bayes_net/lib/python3.8/site-packages/networkx/classes/digraph.py in predecessors(self, n)
    832         try:
--> 833             return iter(self._pred[n])
    834         except KeyError as e:

KeyError: 'IC1'

The above exception was the direct cause of the following exception:

NetworkXError                             Traceback (most recent call last)
<ipython-input-19-d8e71499ca22> in <module>
      8                        (('IC4', 0), ('IC15', 1))]
      9 dyn_model = DynamicBayesianNetwork(dyn_edges)
---> 10 active_trails = dyn_model.active_trail_nodes(('IC1', 0))
     11 active_trails

/mnt/Storage/Nextcloud/Documenti/Università/Artificial Intelligence/Year 1/Fundamentals of Artificial Intelligence and Knowledge Representation/Module 3/Project/src/pgmpy/base/DAG.py in active_trail_nodes(self, variables, observed)
    634                     traversed_list.add((node, direction))
    635                     if direction == "up" and node not in observed_list:
--> 636                         for parent in self.predecessors(node):
    637                             visit_list.add((parent, "up"))
    638                         for child in self.successors(node):

~/.miniconda3/envs/bayes_net/lib/python3.8/site-packages/networkx/classes/digraph.py in predecessors(self, n)
    833             return iter(self._pred[n])
    834         except KeyError as e:
--> 835             raise NetworkXError(f"The node {n} is not in the digraph.") from e
    836 
    837     @property

NetworkXError: The node IC1 is not in the digraph.
```

### Possible solution
I think the problem is in the line:
```
for start in variables if isinstance(variables, (list, tuple)) else [variables]:
```
of `active_trail_nodes()`. In particular, in the case of a `DynamicBayesianNetwork` each node is a tuple composed of the name of the node and the index of the time slice, i.e. ('IC1', 0). Therefore, when iterating over such tuple the `start` variable takes the value of 'IC1' and then 0, instead of the tuple ('IC1', 0). Thus, a possible solution would be to remove the `tuple` type from `isinstance`:
```
for start in variables if isinstance(variables, (list)) else [variables]:
```
However, that would force the user to pass a list instead of a tuple in the case of static networks.

Discussion:
ankurankan: @nihil21 The tests are failing because of formatting. You can run `black .` from the pgmpy dir to fix it.
nihil21: Thanks! I wasn't aware of the `black` tool for reformatting.
nihil21: I've looked into the failed tests, and as far as I can tell the problem is that the DBN takes in input tuples and gives in output `DynamicNode` objects due to the internal conversion, while the tests expect to receive a tuple. For instance, in the test `test_add_multiple_edges`:

- the DBN receives `[(("a", 0), ("b", 0)), (("a", 0), ("a", 1)), (("b", 0), ("b", 1))]` and converts each tuple into a `DynamicNode`;
- in output it gives `OutEdgeView([(a_0, b_0), (a_0, a_1), (b_0, b_1), (a_1, b_1)])`, where `a_0`, `b_0`, etc. are `DynamicNode` objects.

codecov[bot]: # [Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) Report
> Merging [#1438](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) (9eb872f) into [dev](https://codecov.io/gh/pgmpy/pgmpy/commit/aa75ff69b1de7d0de1062414dffb7da4c916b68a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) (aa75ff6) will **decrease** coverage by `1.83%`.
> The diff coverage is `66.66%`.

[![Impacted file tree graph](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/graphs/tree.svg?width=650&height=150&src=pr&token=UaJMCdHaEF&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy)](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy)

```diff
@@            Coverage Diff             @@
##              dev    #1438      +/-   ##
==========================================
- Coverage   94.41%   92.57%   -1.84%     
==========================================
  Files         138      138              
  Lines       15482    15539      +57     
==========================================
- Hits        14617    14386     -231     
- Misses        865     1153     +288     
```


| [Impacted Files](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) | Coverage Δ | |
|---|---|---|
| [pgmpy/models/DynamicBayesianNetwork.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvbW9kZWxzL0R5bmFtaWNCYXllc2lhbk5ldHdvcmsucHk=) | `82.70% <62.50%> (-11.00%)` | :arrow_down: |
| [pgmpy/base/DAG.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvYmFzZS9EQUcucHk=) | `94.42% <100.00%> (ø)` | |
| [pgmpy/independencies/Independencies.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvaW5kZXBlbmRlbmNpZXMvSW5kZXBlbmRlbmNpZXMucHk=) | `87.80% <100.00%> (ø)` | |
| [...y/tests/test\_independencies/test\_Independencies.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9pbmRlcGVuZGVuY2llcy90ZXN0X0luZGVwZW5kZW5jaWVzLnB5) | `99.23% <100.00%> (ø)` | |
| [pgmpy/sampling/NUTS.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvc2FtcGxpbmcvTlVUUy5weQ==) | `19.74% <0.00%> (-78.35%)` | :arrow_down: |
| [pgmpy/readwrite/XMLBeliefNetwork.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvcmVhZHdyaXRlL1hNTEJlbGllZk5ldHdvcmsucHk=) | `53.62% <0.00%> (-39.14%)` | :arrow_down: |
| [...gmpy/tests/test\_readwrite/test\_XMLBeliefNetwork.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9yZWFkd3JpdGUvdGVzdF9YTUxCZWxpZWZOZXR3b3JrLnB5) | `75.40% <0.00%> (-18.86%)` | :arrow_down: |
| [...py/tests/test\_sampling/test\_continuous\_sampling.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9zYW1wbGluZy90ZXN0X2NvbnRpbnVvdXNfc2FtcGxpbmcucHk=) | `83.47% <0.00%> (-16.53%)` | :arrow_down: |
| [pgmpy/readwrite/PomdpX.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvcmVhZHdyaXRlL1BvbWRwWC5weQ==) | `86.62% <0.00%> (-10.37%)` | :arrow_down: |
| [pgmpy/tests/test\_readwrite/test\_PomdpX.py](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy#diff-cGdtcHkvdGVzdHMvdGVzdF9yZWFkd3JpdGUvdGVzdF9Qb21kcFgucHk=) | `83.68% <0.00%> (-9.22%)` | :arrow_down: |
| ... and [1 more](https://codecov.io/gh/pgmpy/pgmpy/pull/1438/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy). Last update [aa75ff6...9eb872f](https://codecov.io/gh/pgmpy/pgmpy/pull/1438?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pgmpy).

ankurankan: @nihil21 Thanks a lot for the PR. This should fix a lot of issues. I have also added a commit to this PR fixing the failing tests. Could you maybe add some tests for the two new methods `get_markov_blanket` and `active_trail_nodes`? 

And just a reminder, make sure to pull your origin/dev before making any changes (as I pushed my commit to your dev) to avoid any conflicts. 
nihil21: You're welcome! 

I have pulled from my origin/dev, but there are still conflicts. Apart from that, I've corrected get_markov_blanket() such that it returns also the children's parents, and added the two test.
ankurankan: @nihil21 Thanks for adding the tests. You were getting the merge conflict because pgmpy/pgmpy:dev had new commits, so you needed to either rebase or merge that into your fork. I have now rebased pgmpy/pgmpy:dev on nihil21/pgmpy:dev to fix the conflict, and also did a minor fix for one of the failing tests. The PR looks good to me now. 

Also, you will probably need to do a hard reset on your local dev branch because it would have diverged from your origin/dev (because of rebase and force push). Something like:
```
git fetch origin
git reset --hard origin/dev
```

Patch:
diff --git a/pgmpy/base/DAG.py b/pgmpy/base/DAG.py
index 7dedca39..e0f6f489 100644
--- a/pgmpy/base/DAG.py
+++ b/pgmpy/base/DAG.py
@@ -667,7 +667,7 @@ class DAG(nx.DiGraph):
         for child_node in children:
             blanket_nodes.extend(self.get_parents(child_node))
         blanket_nodes = set(blanket_nodes)
-        blanket_nodes.remove(node)
+        blanket_nodes.discard(node)
         return list(blanket_nodes)
 
     def active_trail_nodes(self, variables, observed=None, include_latents=False):
diff --git a/pgmpy/independencies/Independencies.py b/pgmpy/independencies/Independencies.py
index b1bd91c5..2fb10ea9 100644
--- a/pgmpy/independencies/Independencies.py
+++ b/pgmpy/independencies/Independencies.py
@@ -416,21 +416,22 @@ class IndependenceAssertion(object):
                 "event1" if not event1 else "event2" + " needs to be specified"
             )
 
-        self.event1 = frozenset(self._return_list_if_str(event1))
-        self.event2 = frozenset(self._return_list_if_str(event2))
-        self.event3 = frozenset(self._return_list_if_str(event3))
+        self.event1 = frozenset(self._return_list_if_not_collection(event1))
+        self.event2 = frozenset(self._return_list_if_not_collection(event2))
+        self.event3 = frozenset(self._return_list_if_not_collection(event3))
         self.all_vars = frozenset().union(self.event1, self.event2, self.event3)
 
     def __str__(self):
         if self.event3:
             return "({event1} \u27C2 {event2} | {event3})".format(
-                event1=", ".join(self.event1),
-                event2=", ".join(self.event2),
-                event3=", ".join(self.event3),
+                event1=", ".join([str(e) for e in self.event1]),
+                event2=", ".join([str(e) for e in self.event2]),
+                event3=", ".join([str(e) for e in self.event3]),
             )
         else:
             return "({event1} \u27C2 {event2})".format(
-                event1=", ".join(self.event1), event2=", ".join(self.event2)
+                event1=", ".join([str(e) for e in self.event1]),
+                event2=", ".join([str(e) for e in self.event2]),
             )
 
     __repr__ = __str__
@@ -451,7 +452,7 @@ class IndependenceAssertion(object):
         return hash((frozenset((self.event1, self.event2)), self.event3))
 
     @staticmethod
-    def _return_list_if_str(event):
+    def _return_list_if_not_collection(event):
         """
         If variable is a string returns a list containing variable.
         Else returns variable itself.
@@ -476,11 +477,12 @@ class IndependenceAssertion(object):
     def latex_string(self):
         if len(self.event3) == 0:
             return r"{event1} \perp {event2}".format(
-                event1=", ".join(self.event1), event2=", ".join(self.event2)
+                event1=", ".join([str(e) for e in self.event1]),
+                event2=", ".join([str(e) for e in self.event2]),
             )
         else:
             return r"{event1} \perp {event2} \mid {event3}".format(
-                event1=", ".join(self.event1),
-                event2=", ".join(self.event2),
-                event3=", ".join(self.event3),
+                event1=", ".join([str(e) for e in self.event1]),
+                event2=", ".join([str(e) for e in self.event2]),
+                event3=", ".join([str(e) for e in self.event3]),
             )
diff --git a/pgmpy/models/DynamicBayesianNetwork.py b/pgmpy/models/DynamicBayesianNetwork.py
index 291890f2..3cacc633 100644
--- a/pgmpy/models/DynamicBayesianNetwork.py
+++ b/pgmpy/models/DynamicBayesianNetwork.py
@@ -1,5 +1,7 @@
-from itertools import combinations
+from itertools import combinations, chain
 from collections import defaultdict
+from dataclasses import dataclass
+import typing
 
 import numpy as np
 import pandas as pd
@@ -9,6 +11,59 @@ from pgmpy.factors.discrete import TabularCPD
 from pgmpy.base import DAG
 
 
+@dataclass(eq=True, frozen=True)
+class DynamicNode:
+    """
+    Class for representing the nodes of Dynamic Bayesian Networks.
+    """
+
+    node: str
+    time_slice: int
+
+    def __getitem__(self, idx: int) -> typing.Union[str, int]:
+        if idx == 0:
+            return self.node
+        elif idx == 1:
+            return self.time_slice
+        else:
+            raise IndexError(f"Index {idx} out of bounds.")
+
+    def __str__(self) -> str:
+        return f"{self.node}_{self.time_slice}"
+
+    __repr__ = __str__
+
+    def __lt__(self, other) -> bool:
+        if self.node < other.node:
+            return True
+        elif self.node > other.node:
+            return False
+        else:
+            return self.time_slice < other.time_slice
+
+    def __le__(self, other) -> bool:
+        if self.node <= other.node:
+            return True
+        elif self.node > other.node:
+            return False
+        else:
+            return self.time_slice <= other.time_slice
+
+    def __eq__(self, other) -> bool:
+        if isinstance(other, DynamicNode):
+            return (self.node, self.time_slice) == (other.node, other.time_slice)
+        elif isinstance(other, (list, tuple)):
+            return (self.node, self.time_slice) == tuple(other)
+        else:
+            return False
+
+    def to_tuple(self) -> tuple:
+        """
+        Returns a tuple representation as (node, time_slice) for DynamicNode object.
+        """
+        return (self.node, self.time_slice)
+
+
 class DynamicBayesianNetwork(DAG):
     def __init__(self, ebunch=None):
         """
@@ -98,7 +153,7 @@ class DynamicBayesianNetwork(DAG):
         >>> dbn.add_node('A')
         ['A']
         """
-        super(DynamicBayesianNetwork, self).add_node((node, 0), **attr)
+        super(DynamicBayesianNetwork, self).add_node(DynamicNode(node, 0), **attr)
 
     def add_nodes_from(self, nodes, **attr):
         """
@@ -190,6 +245,9 @@ class DynamicBayesianNetwork(DAG):
         except TypeError:
             raise ValueError("Nodes must be of type (node, time_slice).")
 
+        start = DynamicNode(*start)
+        end = DynamicNode(*end)
+
         if start == end:
             raise ValueError("Self Loops are not allowed")
         elif (
@@ -205,10 +263,12 @@ class DynamicBayesianNetwork(DAG):
 
         if start[1] == end[1]:
             super(DynamicBayesianNetwork, self).add_edge(
-                (start[0], 1 - start[1]), (end[0], 1 - end[1])
+                DynamicNode(start[0], 1 - start[1]), DynamicNode(end[0], 1 - end[1])
             )
         else:
-            super(DynamicBayesianNetwork, self).add_node((end[0], 1 - end[1]))
+            super(DynamicBayesianNetwork, self).add_node(
+                DynamicNode(end[0], 1 - end[1])
+            )
 
     def add_edges_from(self, ebunch, **kwargs):
         """
@@ -267,7 +327,7 @@ class DynamicBayesianNetwork(DAG):
             )
 
         return [
-            tuple((x[0], time_slice) for x in edge)
+            tuple(DynamicNode(x[0], time_slice) for x in edge)
             for edge in self.edges()
             if edge[0][1] == edge[1][1] == 0
         ]
@@ -316,7 +376,7 @@ class DynamicBayesianNetwork(DAG):
                 "The timeslice should be a positive value greater than or equal to zero"
             )
 
-        return [(edge[0][0], time_slice) for edge in self.get_inter_edges()]
+        return [DynamicNode(edge[0][0], time_slice) for edge in self.get_inter_edges()]
 
     def get_slice_nodes(self, time_slice=0):
         """
@@ -340,7 +400,7 @@ class DynamicBayesianNetwork(DAG):
                 "The timeslice should be a positive value greater than or equal to zero"
             )
 
-        return [(node, time_slice) for node in self._nodes()]
+        return [DynamicNode(node, time_slice) for node in self._nodes()]
 
     def add_cpds(self, *cpds):
         """
@@ -542,7 +602,7 @@ class DynamicBayesianNetwork(DAG):
         >>> student.initialize_initial_state()
         """
         for cpd in self.cpds:
-            temp_var = (cpd.variable[0], 1 - cpd.variable[1])
+            temp_var = DynamicNode(cpd.variable[0], 1 - cpd.variable[1])
             parents = self.get_parents(temp_var)
             if not any(x.variable == temp_var for x in self.cpds):
                 if all(x[1] == parents[0][1] for x in parents):
@@ -638,11 +698,52 @@ class DynamicBayesianNetwork(DAG):
         """
         dbn = DynamicBayesianNetwork()
         dbn.add_nodes_from(self._nodes())
-        dbn.add_edges_from(self.edges())
+        edges = [(u.to_tuple(), v.to_tuple()) for (u, v) in self.edges()]
+        dbn.add_edges_from(edges)
         cpd_copy = [cpd.copy() for cpd in self.get_cpds()]
         dbn.add_cpds(*cpd_copy)
         return dbn
 
+    def get_markov_blanket(self, node):
+        # Wrap node into DynamicNode
+        if not isinstance(node, DynamicNode):
+            node = DynamicNode(*node)
+        # Get standard Markov blanket
+        markov_blanket = set(
+            super(DynamicBayesianNetwork, self).get_markov_blanket(node)
+        )
+
+        # Augment Markov blanket:
+        # if node is in the last time slice, unroll and add children nodes from next time slice
+        max_ts = max([n.time_slice for n in self.nodes()])
+        if node.time_slice == max_ts:
+            # Move node to previous time slice and get children
+            temp_children = self.get_children(
+                DynamicNode(node.node, node.time_slice - 1)
+            )
+            # Move children to next time slice
+            next_children = {
+                DynamicNode(child.node, child.time_slice + 1) for child in temp_children
+            }
+            # Get children parents
+            next_parents = set(
+                chain(*[self.get_parents(child) for child in temp_children])
+            )
+            # Get children's parents
+            temp_parents = {
+                parent for child in temp_children for parent in self.get_parents(child)
+            }
+            # Move children's parents to next time slice
+            next_parents = {
+                DynamicNode(parent.node, parent.time_slice + 1)
+                for parent in temp_parents
+            }
+            # Add them to Markov blanket
+            markov_blanket = markov_blanket | next_children
+            markov_blanket = markov_blanket | next_parents
+
+        return sorted(markov_blanket)
+
     def get_constant_bn(self):
         """
         Returns a normal bayesian network object which has nodes from the first two
@@ -741,3 +842,25 @@ class DynamicBayesianNetwork(DAG):
             )
 
         self.add_cpds(*cpds)
+
+    def active_trail_nodes(self, variables, observed=None, include_latents=False):
+        if not isinstance(variables, DynamicNode):
+            # Wrap variables in DynamicNode objects
+            if len(variables) == 2 and isinstance(variables[1], int):
+                variables = DynamicNode(*variables)
+            else:
+                variables = [DynamicNode(*v) for v in variables]
+        if (
+            observed is not None
+            and len(observed) > 0
+            and any([not isinstance(o, DynamicNode) for o in observed])
+        ):
+            # Wrap observed in DynamicNode objects
+            if len(observed) == 2 and isinstance(observed[1], int):
+                observed = DynamicNode(*observed)
+            else:
+                observed = [DynamicNode(*o) for o in observed]
+        # Call super method
+        return super(DynamicBayesianNetwork, self).active_trail_nodes(
+            variables, observed, include_latents
+        )


Test patch:
diff --git a/pgmpy/tests/test_independencies/test_Independencies.py b/pgmpy/tests/test_independencies/test_Independencies.py
index 765624f4..0288d6db 100644
--- a/pgmpy/tests/test_independencies/test_Independencies.py
+++ b/pgmpy/tests/test_independencies/test_Independencies.py
@@ -7,9 +7,11 @@ class TestIndependenceAssertion(unittest.TestCase):
     def setUp(self):
         self.assertion = IndependenceAssertion()
 
-    def test_return_list_if_str(self):
-        self.assertListEqual(self.assertion._return_list_if_str("U"), ["U"])
-        self.assertListEqual(self.assertion._return_list_if_str(["U", "V"]), ["U", "V"])
+    def test_return_list_if_not_collection(self):
+        self.assertListEqual(self.assertion._return_list_if_not_collection("U"), ["U"])
+        self.assertListEqual(
+            self.assertion._return_list_if_not_collection(["U", "V"]), ["U", "V"]
+        )
 
     def test_get_assertion(self):
         self.assertTupleEqual(
diff --git a/pgmpy/tests/test_models/test_DynamicBayesianNetwork.py b/pgmpy/tests/test_models/test_DynamicBayesianNetwork.py
index ec66a141..8ae22510 100644
--- a/pgmpy/tests/test_models/test_DynamicBayesianNetwork.py
+++ b/pgmpy/tests/test_models/test_DynamicBayesianNetwork.py
@@ -341,6 +341,38 @@ class TestDynamicBayesianNetworkMethods(unittest.TestCase):
         df.columns = wrong_colnames
         self.assertRaises(ValueError, model.fit, df)
 
+    def test_get_markov_blanket(self):
+        self.network.add_edges_from(
+            [(("a", 0), ("a", 1)), (("a", 0), ("b", 1)), (("b", 0), ("b", 1))]
+        )
+
+        markov_blanket = self.network.get_markov_blanket(("a", 1))
+        self.assertListEqual(
+            markov_blanket,
+            [
+                ("a", 0),  # parent
+                ("a", 1),  # child 1's parent
+                ("a", 2),  # child 1
+                ("b", 1),  # child 2's parent
+                ("b", 2),  # child 2
+            ],
+        )
+
+    def test_active_trail_nodes(self):
+        self.network.add_edges_from(
+            [(("a", 0), ("a", 1)), (("a", 0), ("b", 1)), (("b", 0), ("b", 1))]
+        )
+
+        active_trail = self.network.active_trail_nodes(("a", 0))
+        self.assertListEqual(
+            sorted(active_trail.get(("a", 0))), [("a", 0), ("a", 1), ("b", 1)]
+        )
+
+        active_trail = self.network.active_trail_nodes(("a", 0), observed=[("b", 1)])
+        self.assertListEqual(
+            sorted(active_trail.get(("a", 0))), [("a", 0), ("a", 1), ("b", 0)]
+        )
+
     def tearDown(self):
         del self.network