├── VERSION
├── tests
├── __init__.py
├── test_sgm.py
└── test_cgm.py
├── .gitignore
├── causalgraphicalmodels
├── __init__.py
├── examples.py
├── csm.py
└── cgm.py
├── LICENSE
├── setup.py
├── README.md
└── notebooks
└── cgm-examples.ipynb
/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.4
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
3 | __pycache__/
4 | .cache/
5 | .idea/
6 | .ipynb_checkpoints/
7 | .pytest_cache/
8 | dist/
9 | build/
10 | *.egg-info/
11 |
12 | *~
13 |
--------------------------------------------------------------------------------
/causalgraphicalmodels/__init__.py:
--------------------------------------------------------------------------------
1 | from causalgraphicalmodels.cgm import CausalGraphicalModel
2 | from causalgraphicalmodels.csm import StructuralCausalModel
3 | import causalgraphicalmodels.examples as examples
4 | import causalgraphicalmodels.csm as csm
5 |
--------------------------------------------------------------------------------
/tests/test_sgm.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from causalgraphicalmodels.examples import chain_csm
4 |
5 |
6 | class TestSGM(unittest.TestCase):
7 | def test_cgm(self):
8 | chain_cgm = chain_csm.cgm
9 | self.assertTrue(chain_cgm.is_d_separated("a", "c", {"b"}))
10 | self.assertFalse(chain_cgm.is_d_separated("a", "c", set()))
11 |
12 | def test_sample(self):
13 | sample = chain_csm.sample(n_samples=1)
14 |
15 | self.assertEqual(sample.c.squeeze(), 6)
16 |
17 | def test_do(self):
18 | sample = (
19 | chain_csm
20 | .do("a")
21 | .sample(n_samples=1, set_values={"a": np.array([5])})
22 | )
23 |
24 | self.assertEqual(sample.c.squeeze(), 14)
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Iain Barr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """Setup.py for CausalGraphicalModels"""
2 |
3 | from setuptools import setup, find_packages
4 | from codecs import open
5 | from os import path
6 |
7 | here = path.abspath(path.dirname(__file__))
8 |
9 | with open(path.join(here, "README.md"), encoding="utf-8") as f:
10 | long_description = f.read()
11 |
12 | with open(path.join(here, 'VERSION')) as version_file:
13 | version = version_file.read().strip()
14 |
15 | setup(
16 | name="causalgraphicalmodels",
17 | version=version,
18 | description="Causality Graphical Models in Python",
19 | long_description_content_type="text/markdown",
20 | long_description=long_description,
21 | url="https://github.com/ijmbarr/causalgraphicalmodels",
22 | author="Iain Barr",
23 | author_email="iain@degeneratestate.org",
24 | license="MIT",
25 | classifiers=[
26 | "Development Status :: 2 - Pre-Alpha",
27 | "Intended Audience :: Science/Research",
28 | "Topic :: Scientific/Engineering",
29 | "License :: OSI Approved :: MIT License",
30 | "Programming Language :: Python :: 3.5",
31 | ],
32 | keywords="causal inference causal graphical models causality",
33 | packages=find_packages(exclude=["notebook", "test"]),
34 | install_requires=["graphviz", "networkx", "numpy", "pandas"]
35 | )
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CausalGraphicalModels
2 |
3 | ## Introduction
4 |
5 | `causalgraphicalmodels` is a python module for describing and manipulating [Causal Graphical Models](https://en.wikipedia.org/wiki/Causal_graph) and [Structural Causal Models](https://en.wikipedia.org/wiki/Structural_equation_modeling). Behind the scenes it is a light wrapper around the python graph library [networkx](https://networkx.github.io/), together with some CGM specific tools.
6 |
7 | It is currently in a very early stage of development. All feedback is welcome.
8 |
9 |
10 | ## Example
11 |
12 | For a quick overview of `CausalGraphicalModel`, see [this example notebook](https://github.com/ijmbarr/causalgraphicalmodels/blob/master/notebooks/cgm-examples.ipynb).
13 |
14 | ## Install
15 |
16 | ```
17 | pip install causalgraphicalmodels
18 | ```
19 |
20 |
21 | ## Resources
22 | My understanding of Causality comes mainly from the reading of the follow work:
23 | - Causality, Pearl, 2009, 2nd Editing. (An overview available [here](http://ftp.cs.ucla.edu/pub/stat_ser/r350.pdf))
24 | - A fantastic blog post, [If correlation doesn’t imply causation, then what does?](http://www.michaelnielsen.org/ddi/if-correlation-doesnt-imply-causation-then-what-does/) from Michael Nielsen
25 | - [These lecture notes](http://www.math.ku.dk/~peters/jonas_files/scriptChapter1-4.pdf) from Jonas Peters
26 | - The draft of [Elements of Causal Inference](http://www.math.ku.dk/~peters/jonas_files/bookDRAFT5-online-2017-02-27.pdf)
27 | - http://mlss.tuebingen.mpg.de/2017/speaker_slides/Causality.pdf
28 |
29 | ## Related Packages
30 | - [Causality](https://github.com/akelleh/causality)
31 | - [CausalInference](https://github.com/laurencium/causalinference)
32 | - [DoWhy](https://github.com/Microsoft/dowhy)
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/tests/test_cgm.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from causalgraphicalmodels.examples import sprinkler, simple_confounded, \
3 | simple_confounded_hidden_confounder, front_door_example
4 |
5 |
6 | class TestCGM(unittest.TestCase):
7 | def test_is_d_separated(self):
8 | s = sprinkler
9 |
10 | self.assertTrue(s.is_d_separated("season", "slippery", {"wet"}))
11 | self.assertTrue(s.is_d_separated("season", "slippery",
12 | {"rain", "sprinkler"}))
13 | self.assertTrue(s.is_d_separated("season", "slippery",
14 | {"rain", "season", "wet"}))
15 |
16 | self.assertFalse(s.is_d_separated("rain", "sprinkler"))
17 | self.assertTrue(s.is_d_separated("rain", "sprinkler", {"season"}))
18 | self.assertFalse(s.is_d_separated("rain", "sprinkler", {"wet"}))
19 |
20 | def test_get_all_backdoor_adjustment_sets(self):
21 | s = simple_confounded.get_all_backdoor_adjustment_sets("x", "y")
22 | expected_results = frozenset([frozenset(["z"])])
23 | self.assertEqual(s, expected_results)
24 |
25 | def test_get_all_backdoor_adjustment_sets_hidden(self):
26 | self.assertFalse(
27 | simple_confounded_hidden_confounder
28 | .get_all_backdoor_adjustment_sets("x", "y"))
29 |
30 | def test_is_valid_adjustment_set(self):
31 | self.assertTrue(
32 | simple_confounded.is_valid_backdoor_adjustment_set("x", "y", {"z"}))
33 | self.assertFalse(
34 | simple_confounded.is_valid_backdoor_adjustment_set("x", "y", set()))
35 | self.assertFalse(
36 | simple_confounded_hidden_confounder
37 | .is_valid_backdoor_adjustment_set("x", "y", set()))
38 |
39 | def test_get_all_independence_relationships(self):
40 | self.assertFalse(
41 | simple_confounded_hidden_confounder
42 | .get_all_independence_relationships())
43 |
44 | def test_is_valid_frontdoor_adjustment_set(self):
45 | self.assertTrue(
46 | front_door_example.is_valid_frontdoor_adjustment_set("x", "y", "z"))
47 |
48 | self.assertFalse(
49 | simple_confounded.is_valid_frontdoor_adjustment_set("x", "y", "z"))
50 |
51 |
--------------------------------------------------------------------------------
/causalgraphicalmodels/examples.py:
--------------------------------------------------------------------------------
1 | """
2 | A set of example causal systems
3 | """
4 |
5 | import numpy as np
6 |
7 | from causalgraphicalmodels.cgm import CausalGraphicalModel
8 | from causalgraphicalmodels.csm import StructuralCausalModel, \
9 | linear_model, logistic_model
10 |
11 |
12 | chain = CausalGraphicalModel(
13 | nodes=["x1", "x2", "x3"],
14 | edges=[("x1", "x2"), ("x2", "x3")]
15 | )
16 |
17 | collider = CausalGraphicalModel(
18 | nodes=["x1", "x2", "x3"],
19 | edges=[("x1", "x2"), ("x3", "x2")]
20 | )
21 |
22 | fork = CausalGraphicalModel(
23 | nodes=["x1", "x2", "x3"],
24 | edges=[("x2", "x1"), ("x2", "x3")]
25 | )
26 |
27 | example_path_one = CausalGraphicalModel(
28 | nodes=["x1", "x2", "x3", "x4", "x5"],
29 | edges=[("x1", "x2"), ("x2", "x3"), ("x3", "x4"), ("x4", "x5")]
30 | )
31 |
32 | sprinkler = CausalGraphicalModel(
33 | nodes=["season", "rain", "sprinkler", "wet", "slippery"],
34 | edges=[("season", "rain"), ("season", "sprinkler"), ("rain", "wet"),
35 | ("sprinkler", "wet"), ("wet", "slippery")]
36 | )
37 |
38 | simple_confounded = CausalGraphicalModel(
39 | nodes=["x", "y", "z"],
40 | edges=[("z", "x"), ("z", "y"), ("x", "y")]
41 | )
42 |
43 | simple_confounded_potential_outcomes = CausalGraphicalModel(
44 | nodes=["x", "y_0", "y_1", "y", "z"],
45 | edges=[("z", "x"), ("z", "y_0"), ("z", "y_1"),
46 | ("y_0", "y"), ("y_1", "y"), ("x", "y")]
47 | )
48 |
49 |
50 | simple_confounded_hidden_confounder = CausalGraphicalModel(
51 | nodes=["x", "y"],
52 | edges=[("x", "y")],
53 | latent_edges=[("x", "y")]
54 |
55 | )
56 |
57 | front_door_example = CausalGraphicalModel(
58 | nodes=["x", "y", "z"],
59 | edges=[("x", "z"), ("z", "y")],
60 | latent_edges=[("x", "y")]
61 | )
62 |
63 |
64 | chain_csm = StructuralCausalModel({
65 | "a": lambda n_samples: np.ones(n_samples),
66 | "b": lambda a, n_samples: a + 2,
67 | "c": lambda b, n_samples: b * 2
68 | })
69 |
70 | big_csm = StructuralCausalModel({
71 | "a": lambda n_samples: np.random.normal(size=n_samples),
72 | "b": lambda n_samples: np.random.normal(size=n_samples),
73 | "x": logistic_model(["a", "b"], [-1, 1]),
74 | "c": linear_model(["x"], [1]),
75 | "y": linear_model(["c", "e"], [3,-1]),
76 | "d": linear_model(["b"], [-1]),
77 | "e": linear_model(["d"], [2]),
78 | "f": linear_model(["y"], [0.7]),
79 | "h": linear_model(["y", "a"], [1.3, 2.1])
80 | })
81 |
82 |
--------------------------------------------------------------------------------
/causalgraphicalmodels/csm.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import numpy as np
3 | import pandas as pd
4 | import networkx as nx
5 |
6 | from causalgraphicalmodels.cgm import CausalGraphicalModel
7 |
8 |
9 | class StructuralCausalModel:
10 | def __init__(self, assignment):
11 | """
12 | Creates StructuralCausalModel from assignment of the form
13 | { variable: Function(parents) }
14 | """
15 |
16 | self.assignment = assignment.copy()
17 | nodes = list(assignment.keys())
18 | set_nodes = []
19 | edges = []
20 |
21 | for node, model in assignment.items():
22 | if model is None:
23 | set_nodes.append(node)
24 |
25 | elif isinstance(model, CausalAssignmentModel):
26 | edges.extend([
27 | (parent, node)
28 | for parent in model.parents
29 | ])
30 |
31 | elif callable(model):
32 | sig = inspect.signature(model)
33 | parents = [
34 | parent
35 | for parent in sig.parameters.keys()
36 | if parent != "n_samples"
37 | ]
38 | self.assignment[node] = CausalAssignmentModel(model, parents)
39 | edges.extend([(p, node) for p in parents])
40 |
41 | else:
42 | raise ValueError("Model must be either callable or None. "
43 | "Instead got {} for node {}."
44 | .format(model, node))
45 |
46 | self.cgm = CausalGraphicalModel(
47 | nodes=nodes, edges=edges, set_nodes=set_nodes)
48 |
49 | def __repr__(self):
50 | variables = ", ".join(map(str, sorted(self.cgm.dag.nodes())))
51 | return ("{classname}({vars})"
52 | .format(classname=self.__class__.__name__,
53 | vars=variables))
54 |
55 | def sample(self, n_samples=100, set_values=None):
56 | """
57 | Sample from CSM
58 |
59 | Arguments
60 | ---------
61 | n_samples: int
62 | the number of samples to return
63 |
64 | set_values: dict[variable:str, set_value:np.array]
65 | the values of the interventional variable
66 |
67 | Returns
68 | -------
69 | samples: pd.DataFrame
70 | """
71 | samples = {}
72 |
73 | if set_values is None:
74 | set_values = dict()
75 |
76 | for node in nx.topological_sort(self.cgm.dag):
77 | c_model = self.assignment[node]
78 |
79 | if c_model is None:
80 | assert len(set_values[node]) == n_samples
81 | samples[node] = np.array(set_values[node])
82 | else:
83 | parent_samples = {
84 | parent: samples[parent]
85 | for parent in c_model.parents
86 | }
87 | parent_samples["n_samples"] = n_samples
88 | samples[node] = c_model(**parent_samples)
89 |
90 | return pd.DataFrame(samples)
91 |
92 | def do(self, node):
93 | """
94 | Returns a StructualCausalModel after an intervention on node
95 | """
96 | new_assignment = self.assignment.copy()
97 | new_assignment[node] = None
98 | return StructuralCausalModel(new_assignment)
99 |
100 |
101 | class CausalAssignmentModel:
102 | """
103 | Basically just a hack to allow me to provide information about the
104 | arguments of a dynamically generated function.
105 | """
106 | def __init__(self, model, parents):
107 | self.model = model
108 | self.parents = parents
109 |
110 | def __call__(self, *args, **kwargs):
111 | assert len(args) == 0
112 | return self.model(**kwargs)
113 |
114 | def __repr__(self):
115 | return "CausalAssignmentModel({})".format(",".join(self.parents))
116 |
117 |
118 | # Some Helper functions for defining models
119 |
120 | def _sigma(x):
121 | return 1 / (1 + np.exp(-x))
122 |
123 |
124 | def linear_model(parents, weights, offset=0, noise_scale=1):
125 | """
126 | Create CausalAssignmentModel for node y of the form
127 | \sum_{i} x_{i}w_{i} + a + \epsilon
128 |
129 | Arguments
130 | ---------
131 | parents: list
132 | variable names of parents
133 |
134 | weights: list
135 | weigths of each variable in the sum
136 |
137 | offset: float
138 | offset for sum
139 |
140 | noise_scale: float
141 | scale of the normal noise
142 |
143 | Returns
144 | -------
145 | model: CausalAssignmentModel
146 | """
147 | assert len(parents) == len(weights)
148 | assert len(parents) > 0
149 | def model(**kwargs):
150 | n_samples = kwargs["n_samples"]
151 | a = np.array([kwargs[p] * w for p, w in zip(parents, weights)], dtype=np.float)
152 | a = np.sum(a, axis=0)
153 | a += np.random.normal(loc=offset, scale=noise_scale, size=n_samples)
154 | return a
155 |
156 | return CausalAssignmentModel(model, parents)
157 |
158 |
159 | def logistic_model(parents, weights, offset=0):
160 | """
161 | Create CausalAssignmentModel for node y of the form
162 | z = \sum_{i} x_{i}w_{i} + a
163 | y ~ Binomial(\simga(z))
164 |
165 | Arguments
166 | ---------
167 | parents: list
168 | variable names of parents
169 |
170 | weights: list
171 | weigths of each variable in the sum
172 |
173 | offset: float
174 | offset for sum
175 |
176 | Returns
177 | -------
178 | model: CausalAssignmentModel
179 | """
180 | assert len(parents) == len(weights)
181 | assert len(parents) > 0
182 | def model(**kwargs):
183 | a = np.array([kwargs[p] * w for p, w in zip(parents, weights)])
184 | a = np.sum(a, axis=0) + offset
185 | a = _sigma(a)
186 | a = np.random.binomial(n=1, p=a)
187 | return a
188 |
189 | return CausalAssignmentModel(model, parents)
190 |
191 |
192 | def discrete_model(parents, lookup_table):
193 | """
194 | Create CausalAssignmentModel based on a lookup table.
195 |
196 | Lookup_table maps inputs values to weigths of the output values
197 | The actual output values are sampled from a discrete distribution
198 | of integers with probability proportional to the weights.
199 |
200 | Lookup_table for the form:
201 |
202 | Dict[Tuple(input_vales): (output_weights)]
203 |
204 | Arguments
205 | ---------
206 | parents: list
207 | variable names of parents
208 |
209 | lookup_table: dict
210 | lookup table
211 |
212 | Returns
213 | -------
214 | model: CausalAssignmentModel
215 | """
216 | assert len(parents) > 0
217 |
218 | # create input/output mapping
219 | inputs, weights = zip(*lookup_table.items())
220 |
221 | output_length = len(weights[0])
222 | assert all(len(w) == output_length for w in weights)
223 | outputs = np.arange(output_length)
224 |
225 | ps = [np.array(w) / sum(w) for w in weights]
226 |
227 | def model(**kwargs):
228 | n_samples = kwargs["n_samples"]
229 | a = np.vstack([kwargs[p] for p in parents]).T
230 |
231 | b = np.zeros(n_samples) * np.nan
232 | for m, p in zip(inputs, ps):
233 | b = np.where(
234 | (a == m).all(axis=1),
235 | np.random.choice(outputs, size=n_samples, p=p), b)
236 |
237 | if np.isnan(b).any():
238 | raise ValueError("It looks like an input was provided which doesn't have a lookup.")
239 |
240 | return b
241 |
242 | return CausalAssignmentModel(model, parents)
243 |
244 |
--------------------------------------------------------------------------------
/causalgraphicalmodels/cgm.py:
--------------------------------------------------------------------------------
1 | import networkx as nx
2 | import graphviz
3 | from itertools import combinations, chain
4 | from collections import Iterable
5 |
6 |
7 | class CausalGraphicalModel:
8 | """
9 | Causal Graphical Models
10 | """
11 |
12 | def __init__(self, nodes, edges, latent_edges=None, set_nodes=None):
13 | """
14 | Create CausalGraphicalModel
15 |
16 | Arguments
17 | ---------
18 | nodes: list[node:str]
19 |
20 | edges: list[tuple[node:str, node:str]]
21 |
22 | latent_edges: list[tuple[node:str, node:str]] or None
23 |
24 | set_nodes: list[node:str] or None
25 | """
26 | if set_nodes is None:
27 | self.set_nodes = frozenset()
28 | else:
29 | self.set_nodes = frozenset(set_nodes)
30 |
31 | if latent_edges is None:
32 | self.latent_edges = frozenset()
33 | else:
34 | self.latent_edges = frozenset(latent_edges)
35 |
36 | self.dag = nx.DiGraph()
37 | self.dag.add_nodes_from(nodes)
38 | self.dag.add_edges_from(edges)
39 |
40 | # Add latent connections to the graph
41 | self.observed_variables = frozenset(nodes)
42 | self.unobserved_variable_edges = dict()
43 | unobserved_variables = []
44 | unobserved_variable_counter = 0
45 | for n1, n2 in self.latent_edges:
46 | new_node = "Unobserved_{}".format(unobserved_variable_counter)
47 | unobserved_variable_counter += 1
48 | self.dag.add_node(new_node)
49 | self.dag.add_edge(new_node, n1)
50 | self.dag.add_edge(new_node, n2)
51 | unobserved_variables.append(new_node)
52 | self.unobserved_variable_edges[new_node] = (n1, n2)
53 | self.unobserved_variables = frozenset(unobserved_variables)
54 |
55 | assert nx.is_directed_acyclic_graph(self.dag)
56 |
57 | for set_node in self.set_nodes:
58 | # set nodes cannot have parents
59 | assert not nx.ancestors(self.dag, set_node)
60 |
61 | self.graph = self.dag.to_undirected()
62 |
63 | def __repr__(self):
64 | variables = ", ".join(map(str, sorted(self.observed_variables)))
65 | return ("{classname}({vars})"
66 | .format(classname=self.__class__.__name__,
67 | vars=variables))
68 |
69 | def draw(self):
70 | """
71 | dot file representation of the CGM.
72 | """
73 | dot = graphviz.Digraph()
74 |
75 | for node in self.observed_variables:
76 | if node in self.set_nodes:
77 | dot.node(node, node, {"shape": "ellipse", "peripheries": "2"})
78 | else:
79 | dot.node(node, node, {"shape": "ellipse"})
80 |
81 | for a, b in self.dag.edges():
82 | if a in self.observed_variables and b in self.observed_variables:
83 | dot.edge(a, b)
84 |
85 | for n, (a, b) in self.unobserved_variable_edges.items():
86 | dot.node(n, _attributes={"shape": "point"})
87 | dot.edge(n, a, _attributes={"style": "dashed"})
88 | dot.edge(n, b, _attributes={"style": "dashed"})
89 |
90 | return dot
91 |
92 | def get_distribution(self):
93 | """
94 | Returns a string representing the factorized distribution implied by
95 | the CGM.
96 | """
97 | products = []
98 | for node in nx.topological_sort(self.dag):
99 | if node in self.set_nodes:
100 | continue
101 |
102 | parents = list(self.dag.predecessors(node))
103 | if not parents:
104 | p = "P({})".format(node)
105 | else:
106 | parents = [
107 | "do({})".format(n) if n in self.set_nodes else str(n)
108 | for n in parents
109 | ]
110 | p = "P({}|{})".format(node, ",".join(parents))
111 | products.append(p)
112 | return "".join(products)
113 |
114 | def do(self, node):
115 | """
116 | Apply intervention on node to CGM
117 | """
118 | assert node in self.observed_variables
119 | set_nodes = self.set_nodes | frozenset([node])
120 | nodes = self.observed_variables
121 | edges = [
122 | (a, b)
123 | for a, b in self.dag.edges()
124 | if b != node
125 | and a in self.observed_variables
126 | and b in self.observed_variables
127 | ]
128 | latent_edges = [
129 | (a, b)
130 | for a, b in self.latent_edges
131 | if a not in set_nodes
132 | and b not in set_nodes
133 | ]
134 | return CausalGraphicalModel(
135 | nodes=nodes, edges=edges,
136 | latent_edges=latent_edges, set_nodes=set_nodes)
137 |
138 | def _check_d_separation(self, path, zs=None):
139 | """
140 | Check if a path is d-separated by set of variables zs.
141 | """
142 | zs = _variable_or_iterable_to_set(zs)
143 |
144 | if len(path) < 3:
145 | return False
146 |
147 | for a, b, c in zip(path[:-2], path[1:-1], path[2:]):
148 | structure = self._classify_three_structure(a, b, c)
149 |
150 | if structure in ("chain", "fork") and b in zs:
151 | return True
152 |
153 | if structure == "collider":
154 | descendants = (nx.descendants(self.dag, b) | {b})
155 | if not descendants & set(zs):
156 | return True
157 |
158 | return False
159 |
160 | def _classify_three_structure(self, a, b, c):
161 | """
162 | Classify three structure as a chain, fork or collider.
163 | """
164 | if self.dag.has_edge(a, b) and self.dag.has_edge(b, c):
165 | return "chain"
166 |
167 | if self.dag.has_edge(c, b) and self.dag.has_edge(b, a):
168 | return "chain"
169 |
170 | if self.dag.has_edge(a, b) and self.dag.has_edge(c, b):
171 | return "collider"
172 |
173 | if self.dag.has_edge(b, a) and self.dag.has_edge(b, c):
174 | return "fork"
175 |
176 | raise ValueError("Unsure how to classify ({},{},{})".format(a, b, c))
177 |
178 | def is_d_separated(self, x, y, zs=None):
179 | """
180 | Is x d-separated from y, conditioned on zs?
181 | """
182 | zs = _variable_or_iterable_to_set(zs)
183 | assert x in self.observed_variables
184 | assert y in self.observed_variables
185 | assert all([z in self.observed_variables for z in zs])
186 |
187 | paths = nx.all_simple_paths(self.graph, x, y)
188 | return all(self._check_d_separation(path, zs) for path in paths)
189 |
190 | def get_all_independence_relationships(self):
191 | """
192 | Returns a list of all pairwise conditional independence relationships
193 | implied by the graph structure.
194 | """
195 | conditional_independences = []
196 | for x, y in combinations(self.observed_variables, 2):
197 | remaining_variables = set(self.observed_variables) - {x, y}
198 | for cardinality in range(len(remaining_variables) + 1):
199 | for z in combinations(remaining_variables, cardinality):
200 | if self.is_d_separated(x, y, frozenset(z)):
201 | conditional_independences.append((x, y, set(z)))
202 |
203 | return conditional_independences
204 |
205 | def get_all_backdoor_paths(self, x, y):
206 | """
207 | Get all backdoor paths between x and y
208 | """
209 | return [
210 | path
211 | for path in nx.all_simple_paths(self.graph, x, y)
212 | if len(path) > 2
213 | and path[1] in self.dag.predecessors(x)
214 | ]
215 |
216 | def is_valid_backdoor_adjustment_set(self, x, y, z):
217 | """
218 | Test whether z is a valid backdoor adjustment set for
219 | estimating the causal impact of x on y via the backdoor
220 | adjustment formula:
221 |
222 | P(y|do(x)) = \sum_{z}P(y|x,z)P(z)
223 |
224 | Arguments
225 | ---------
226 | x: str
227 | Intervention Variable
228 |
229 | y: str
230 | Target Variable
231 |
232 | z: str or set[str]
233 | Adjustment variables
234 |
235 | Returns
236 | -------
237 | is_valid_adjustment_set: bool
238 | """
239 | z = _variable_or_iterable_to_set(z)
240 |
241 | assert x in self.observed_variables
242 | assert y in self.observed_variables
243 | assert x not in z
244 | assert y not in z
245 |
246 | if any([zz in nx.descendants(self.dag, x) for zz in z]):
247 | return False
248 |
249 | unblocked_backdoor_paths = [
250 | path
251 | for path in self.get_all_backdoor_paths(x, y)
252 | if not self._check_d_separation(path, z)
253 | ]
254 |
255 | if unblocked_backdoor_paths:
256 | return False
257 |
258 | return True
259 |
260 | def get_all_backdoor_adjustment_sets(self, x, y):
261 | """
262 | Get all sets of variables which are valid adjustment sets for
263 | estimating the causal impact of x on y via the back door
264 | adjustment formula:
265 |
266 | P(y|do(x)) = \sum_{z}P(y|x,z)P(z)
267 |
268 | Note that the empty set can be a valid adjustment set for some CGMs,
269 | in this case frozenset(frozenset(), ...) is returned. This is different
270 | from the case where there are no valid adjustment sets where the
271 | empty set is returned.
272 |
273 | Arguments
274 | ---------
275 | x: str
276 | Intervention Variable
277 | y: str
278 | Target Variable
279 |
280 | Returns
281 | -------
282 | condition set: frozenset[frozenset[variables]]
283 | """
284 | assert x in self.observed_variables
285 | assert y in self.observed_variables
286 |
287 | possible_adjustment_variables = (
288 | set(self.observed_variables)
289 | - {x} - {y}
290 | - set(nx.descendants(self.dag, x))
291 | )
292 |
293 | valid_adjustment_sets = frozenset([
294 | frozenset(s)
295 | for s in _powerset(possible_adjustment_variables)
296 | if self.is_valid_backdoor_adjustment_set(x, y, s)
297 | ])
298 |
299 | return valid_adjustment_sets
300 |
301 | def is_valid_frontdoor_adjustment_set(self, x, y, z):
302 | """
303 | Test whether z is a valid frontdoor adjustment set for
304 | estimating the causal impact of x on y via the frontdoor
305 | adjustment formula:
306 |
307 | P(y|do(x)) = \sum_{z}P(z|x)\sum_{x'}P(y|x',z)P(x')
308 |
309 | Arguments
310 | ---------
311 | x: str
312 | Intervention Variable
313 |
314 | y: str
315 | Target Variable
316 |
317 | z: set
318 | Adjustment variables
319 |
320 | Returns
321 | -------
322 | is_valid_adjustment_set: bool
323 | """
324 | z = _variable_or_iterable_to_set(z)
325 |
326 | # 1. does z block all directed paths from x to y?
327 | unblocked_directed_paths = [
328 | path for path in
329 | nx.all_simple_paths(self.dag, x, y)
330 | if not any(zz in path for zz in z)
331 | ]
332 |
333 | if unblocked_directed_paths:
334 | return False
335 |
336 | # 2. no unblocked backdoor paths between x and z
337 | unblocked_backdoor_paths_x_z = [
338 | path
339 | for zz in z
340 | for path in self.get_all_backdoor_paths(x, zz)
341 | if not self._check_d_separation(path, z - {zz})
342 | ]
343 |
344 | if unblocked_backdoor_paths_x_z:
345 | return False
346 |
347 | # 3. x is a valid backdoor adjustment set for z
348 | if not all(self.is_valid_backdoor_adjustment_set(zz, y, x) for zz in z):
349 | return False
350 |
351 | return True
352 |
353 | def get_all_frontdoor_adjustment_sets(self, x, y):
354 | """
355 | Get all sets of variables which are valid frontdoor adjustment sets for
356 | estimating the causal impact of x on y via the frontdoor adjustment
357 | formula:
358 |
359 | P(y|do(x)) = \sum_{z}P(z|x)\sum_{x'}P(y|x',z)P(x')
360 |
361 | Note that the empty set can be a valid adjustment set for some CGMs,
362 | in this case frozenset(frozenset(), ...) is returned. This is different
363 | from the case where there are no valid adjustment sets where the
364 | empty set is returned.
365 |
366 | Arguments
367 | ---------
368 | x: str
369 | Intervention Variable
370 | y: str
371 | Target Variable
372 |
373 | Returns
374 | -------
375 | condition set: frozenset[frozenset[variables]]
376 | """
377 | assert x in self.observed_variables
378 | assert y in self.observed_variables
379 |
380 | possible_adjustment_variables = (
381 | set(self.observed_variables)
382 | - {x} - {y}
383 | )
384 |
385 | valid_adjustment_sets = frozenset(
386 | [
387 | frozenset(s)
388 | for s in _powerset(possible_adjustment_variables)
389 | if self.is_valid_frontdoor_adjustment_set(x, y, s)
390 | ])
391 |
392 | return valid_adjustment_sets
393 |
394 |
395 | def _variable_or_iterable_to_set(x):
396 | """
397 | Convert variable or iterable x to a frozenset.
398 |
399 | If x is None, returns the empty set.
400 |
401 | Arguments
402 | ---------
403 | x: None, str or Iterable[str]
404 |
405 | Returns
406 | -------
407 | x: frozenset[str]
408 |
409 | """
410 | if x is None:
411 | return frozenset([])
412 |
413 | if isinstance(x, str):
414 | return frozenset([x])
415 |
416 | if not isinstance(x, Iterable) or not all(isinstance(xx, str) for xx in x):
417 | raise ValueError(
418 | "{} is expected to be either a string or an iterable of strings"
419 | .format(x))
420 |
421 | return frozenset(x)
422 |
423 |
424 | def _powerset(iterable):
425 | """
426 | https://docs.python.org/3/library/itertools.html#recipes
427 | powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)
428 | """
429 | s = list(iterable)
430 | return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
431 |
--------------------------------------------------------------------------------
/notebooks/cgm-examples.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# An Introduction to `CausalGraphicalModels`\n",
8 | "\n",
9 | "`CausalGraphicalModel` is a python module for describing and manipulating [Causal Graphical Models](https://en.wikipedia.org/wiki/Causal_graph) and [Structural Causal Models](https://en.wikipedia.org/wiki/Structural_equation_modeling). Behind the curtain, it is a light wrapper around the python graph library [networkx](https://networkx.github.io/).\n",
10 | "\n",
11 | "This notebook is designed to give a quick overview of the functionality of this package."
12 | ]
13 | },
14 | {
15 | "cell_type": "markdown",
16 | "metadata": {},
17 | "source": [
18 | "# CausalGraphicalModels"
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": 1,
24 | "metadata": {},
25 | "outputs": [],
26 | "source": [
27 | "from causalgraphicalmodels import CausalGraphicalModel"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": 2,
33 | "metadata": {},
34 | "outputs": [
35 | {
36 | "data": {
37 | "image/svg+xml": [
38 | "\n",
39 | "\n",
41 | "\n",
43 | "\n",
44 | "\n"
101 | ],
102 | "text/plain": [
103 | ""
104 | ]
105 | },
106 | "execution_count": 2,
107 | "metadata": {},
108 | "output_type": "execute_result"
109 | }
110 | ],
111 | "source": [
112 | "sprinkler = CausalGraphicalModel(\n",
113 | " nodes=[\"season\", \"rain\", \"sprinkler\", \"wet\", \"slippery\"],\n",
114 | " edges=[\n",
115 | " (\"season\", \"rain\"), \n",
116 | " (\"season\", \"sprinkler\"), \n",
117 | " (\"rain\", \"wet\"),\n",
118 | " (\"sprinkler\", \"wet\"), \n",
119 | " (\"wet\", \"slippery\")\n",
120 | " ]\n",
121 | ")\n",
122 | "\n",
123 | "# draw return a graphviz `dot` object, which jupyter can render\n",
124 | "sprinkler.draw()"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": 3,
130 | "metadata": {},
131 | "outputs": [
132 | {
133 | "name": "stdout",
134 | "output_type": "stream",
135 | "text": [
136 | "P(season)P(sprinkler|season)P(rain|season)P(wet|rain,sprinkler)P(slippery|wet)\n"
137 | ]
138 | }
139 | ],
140 | "source": [
141 | "# get the distribution implied by the graph\n",
142 | "print(sprinkler.get_distribution())"
143 | ]
144 | },
145 | {
146 | "cell_type": "code",
147 | "execution_count": 4,
148 | "metadata": {},
149 | "outputs": [
150 | {
151 | "data": {
152 | "text/plain": [
153 | "True"
154 | ]
155 | },
156 | "execution_count": 4,
157 | "metadata": {},
158 | "output_type": "execute_result"
159 | }
160 | ],
161 | "source": [
162 | "# check for d-seperation of two nodes\n",
163 | "sprinkler.is_d_separated(\"slippery\", \"season\", {\"wet\"})"
164 | ]
165 | },
166 | {
167 | "cell_type": "code",
168 | "execution_count": 5,
169 | "metadata": {},
170 | "outputs": [
171 | {
172 | "data": {
173 | "text/plain": [
174 | "[('sprinkler', 'rain', {'season'}),\n",
175 | " ('sprinkler', 'slippery', {'wet'}),\n",
176 | " ('sprinkler', 'slippery', {'rain', 'wet'}),\n",
177 | " ('sprinkler', 'slippery', {'season', 'wet'}),\n",
178 | " ('sprinkler', 'slippery', {'rain', 'season', 'wet'}),\n",
179 | " ('rain', 'slippery', {'wet'}),\n",
180 | " ('rain', 'slippery', {'sprinkler', 'wet'}),\n",
181 | " ('rain', 'slippery', {'season', 'wet'}),\n",
182 | " ('rain', 'slippery', {'season', 'sprinkler', 'wet'}),\n",
183 | " ('wet', 'season', {'rain', 'sprinkler'}),\n",
184 | " ('wet', 'season', {'rain', 'slippery', 'sprinkler'}),\n",
185 | " ('season', 'slippery', {'wet'}),\n",
186 | " ('season', 'slippery', {'rain', 'wet'}),\n",
187 | " ('season', 'slippery', {'sprinkler', 'wet'}),\n",
188 | " ('season', 'slippery', {'rain', 'sprinkler'}),\n",
189 | " ('season', 'slippery', {'rain', 'sprinkler', 'wet'})]"
190 | ]
191 | },
192 | "execution_count": 5,
193 | "metadata": {},
194 | "output_type": "execute_result"
195 | }
196 | ],
197 | "source": [
198 | "# get all the conditional independence relationships implied by a CGM\n",
199 | "sprinkler.get_all_independence_relationships()"
200 | ]
201 | },
202 | {
203 | "cell_type": "code",
204 | "execution_count": 6,
205 | "metadata": {},
206 | "outputs": [
207 | {
208 | "data": {
209 | "text/plain": [
210 | "False"
211 | ]
212 | },
213 | "execution_count": 6,
214 | "metadata": {},
215 | "output_type": "execute_result"
216 | }
217 | ],
218 | "source": [
219 | "# check backdoor adjustment set\n",
220 | "sprinkler.is_valid_backdoor_adjustment_set(\"rain\", \"slippery\", {\"wet\"})"
221 | ]
222 | },
223 | {
224 | "cell_type": "code",
225 | "execution_count": 7,
226 | "metadata": {},
227 | "outputs": [
228 | {
229 | "data": {
230 | "text/plain": [
231 | "frozenset({frozenset({'sprinkler'}),\n",
232 | " frozenset({'season'}),\n",
233 | " frozenset({'season', 'sprinkler'})})"
234 | ]
235 | },
236 | "execution_count": 7,
237 | "metadata": {},
238 | "output_type": "execute_result"
239 | }
240 | ],
241 | "source": [
242 | "# get all backdoor adjustment sets\n",
243 | "sprinkler.get_all_backdoor_adjustment_sets(\"rain\", \"slippery\")"
244 | ]
245 | },
246 | {
247 | "cell_type": "code",
248 | "execution_count": 8,
249 | "metadata": {},
250 | "outputs": [
251 | {
252 | "data": {
253 | "image/svg+xml": [
254 | "\n",
255 | "\n",
257 | "\n",
259 | "\n",
260 | "\n"
313 | ],
314 | "text/plain": [
315 | ""
316 | ]
317 | },
318 | "execution_count": 8,
319 | "metadata": {},
320 | "output_type": "execute_result"
321 | }
322 | ],
323 | "source": [
324 | "# get the graph created by intervening on node \"rain\"\n",
325 | "do_sprinkler = sprinkler.do(\"rain\")\n",
326 | "\n",
327 | "do_sprinkler.draw()"
328 | ]
329 | },
330 | {
331 | "cell_type": "markdown",
332 | "metadata": {},
333 | "source": [
334 | "# Latent Variables "
335 | ]
336 | },
337 | {
338 | "cell_type": "code",
339 | "execution_count": 9,
340 | "metadata": {},
341 | "outputs": [
342 | {
343 | "data": {
344 | "image/svg+xml": [
345 | "\n",
346 | "\n",
348 | "\n",
350 | "\n",
351 | "\n"
397 | ],
398 | "text/plain": [
399 | ""
400 | ]
401 | },
402 | "execution_count": 9,
403 | "metadata": {},
404 | "output_type": "execute_result"
405 | }
406 | ],
407 | "source": [
408 | "dag_with_latent_variables = CausalGraphicalModel(\n",
409 | " nodes=[\"x\", \"y\", \"z\"],\n",
410 | " edges=[\n",
411 | " (\"x\", \"z\"),\n",
412 | " (\"z\", \"y\"), \n",
413 | " ],\n",
414 | " latent_edges=[\n",
415 | " (\"x\", \"y\")\n",
416 | " ]\n",
417 | ")\n",
418 | "\n",
419 | "dag_with_latent_variables.draw()"
420 | ]
421 | },
422 | {
423 | "cell_type": "code",
424 | "execution_count": 10,
425 | "metadata": {},
426 | "outputs": [
427 | {
428 | "data": {
429 | "text/plain": [
430 | "frozenset()"
431 | ]
432 | },
433 | "execution_count": 10,
434 | "metadata": {},
435 | "output_type": "execute_result"
436 | }
437 | ],
438 | "source": [
439 | "# here there are no observed backdoor adjustment sets\n",
440 | "dag_with_latent_variables.get_all_backdoor_adjustment_sets(\"x\", \"y\")"
441 | ]
442 | },
443 | {
444 | "cell_type": "code",
445 | "execution_count": 11,
446 | "metadata": {},
447 | "outputs": [
448 | {
449 | "data": {
450 | "text/plain": [
451 | "frozenset({frozenset({'z'})})"
452 | ]
453 | },
454 | "execution_count": 11,
455 | "metadata": {},
456 | "output_type": "execute_result"
457 | }
458 | ],
459 | "source": [
460 | "# but there is a frontdoor adjustment set\n",
461 | "dag_with_latent_variables.get_all_frontdoor_adjustment_sets(\"x\", \"y\")"
462 | ]
463 | },
464 | {
465 | "cell_type": "markdown",
466 | "metadata": {},
467 | "source": [
468 | "# StructuralCausalModels\n",
469 | "\n",
470 | "For Structural Causal Models (SCM) we need to specify the functional form of each node:"
471 | ]
472 | },
473 | {
474 | "cell_type": "code",
475 | "execution_count": 12,
476 | "metadata": {},
477 | "outputs": [],
478 | "source": [
479 | "from causalgraphicalmodels import StructuralCausalModel\n",
480 | "import numpy as np\n",
481 | "\n",
482 | "scm = StructuralCausalModel({\n",
483 | " \"x1\": lambda n_samples: np.random.binomial(n=1,p=0.7,size=n_samples),\n",
484 | " \"x2\": lambda x1, n_samples: np.random.normal(loc=x1, scale=0.1),\n",
485 | " \"x3\": lambda x2, n_samples: x2 ** 2,\n",
486 | "})"
487 | ]
488 | },
489 | {
490 | "cell_type": "markdown",
491 | "metadata": {},
492 | "source": [
493 | "The only requirement on the functions are:\n",
494 | " - that variable names are consistent \n",
495 | " - each function accepts keyword variables in the form of `numpy` arrays and output numpy arrays of shape [n_samples] \n",
496 | " - that in addition to it's parents, each function takes a `n_samples` variables indicating how many samples to generate \n",
497 | " - that any function acts on each row independently. This ensure that the output samples are independent\n",
498 | " \n",
499 | "Wrapping these functions in the `StructuralCausalModel` object allows us to easily generate samples: "
500 | ]
501 | },
502 | {
503 | "cell_type": "code",
504 | "execution_count": 13,
505 | "metadata": {},
506 | "outputs": [
507 | {
508 | "data": {
509 | "text/html": [
510 | "\n",
511 | "\n",
524 | "
\n",
525 | " \n",
526 | " \n",
527 | " | \n",
528 | " x1 | \n",
529 | " x2 | \n",
530 | " x3 | \n",
531 | "
\n",
532 | " \n",
533 | " \n",
534 | " \n",
535 | " | 0 | \n",
536 | " 1 | \n",
537 | " 1.004020 | \n",
538 | " 1.008056 | \n",
539 | "
\n",
540 | " \n",
541 | " | 1 | \n",
542 | " 1 | \n",
543 | " 0.902045 | \n",
544 | " 0.813686 | \n",
545 | "
\n",
546 | " \n",
547 | " | 2 | \n",
548 | " 1 | \n",
549 | " 1.150323 | \n",
550 | " 1.323242 | \n",
551 | "
\n",
552 | " \n",
553 | " | 3 | \n",
554 | " 1 | \n",
555 | " 0.984634 | \n",
556 | " 0.969504 | \n",
557 | "
\n",
558 | " \n",
559 | " | 4 | \n",
560 | " 1 | \n",
561 | " 1.023118 | \n",
562 | " 1.046770 | \n",
563 | "
\n",
564 | " \n",
565 | "
\n",
566 | "
"
567 | ],
568 | "text/plain": [
569 | " x1 x2 x3\n",
570 | "0 1 1.004020 1.008056\n",
571 | "1 1 0.902045 0.813686\n",
572 | "2 1 1.150323 1.323242\n",
573 | "3 1 0.984634 0.969504\n",
574 | "4 1 1.023118 1.046770"
575 | ]
576 | },
577 | "execution_count": 13,
578 | "metadata": {},
579 | "output_type": "execute_result"
580 | }
581 | ],
582 | "source": [
583 | "ds = scm.sample(n_samples=100)\n",
584 | "\n",
585 | "ds.head()"
586 | ]
587 | },
588 | {
589 | "cell_type": "code",
590 | "execution_count": 14,
591 | "metadata": {},
592 | "outputs": [
593 | {
594 | "data": {
595 | "text/plain": [
596 | ""
597 | ]
598 | },
599 | "execution_count": 14,
600 | "metadata": {},
601 | "output_type": "execute_result"
602 | },
603 | {
604 | "data": {
605 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAEKCAYAAADuEgmxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAIABJREFUeJzs3Xd0VNXawOHfSTLJpPdeSEgBQodE\negcBASliwYKAXgH1+qko2Ati9yKWa+EqqCjiFZEqSO8lJJQQAiGF9F4myWQykynn+yNxwphCLJdQ\n9rMWCzhnz8w+yVrnnbP3ft8tybKMIAiCILSFVXt3QBAEQbh2iKAhCIIgtJkIGoIgCEKbiaAhCIIg\ntJkIGoIgCEKbiaAhCIIgtJkIGoIgCEKbiaAhCIIgtJkIGoIgCEKb2bR3B/5uXl5ecmhoaHt3QxAE\n4ZqSkJBQKsuy9+XaXXdBIzQ0lPj4+PbuhiAIwjVFkqSstrQTw1OCIAhCm4mgIQiCILSZCBqCIAhC\nm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqC\nIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILRZ\nuwUNSZKCJUnaI0lSsiRJZyVJ+r9m2kiSJH0oSVKaJEmJkiT1aY++CoIgCPXac49wA7BAluUTkiQ5\nAwmSJO2QZTn5kjbjgciGP/2ATxv+FgRBENpBuz1pyLJcIMvyiYZ/VwPngMDfNZsMfCPXOwq4SZLk\nf4W7KgiCIDS4KuY0JEkKBXoDx353KhDIueT/uTQNLEiS9JAkSfGSJMWXlJT8r7opCIJww2v3oCFJ\nkhPwE/C4LMtVf+Y9ZFleLstyjCzLMd7e3n9vBwVBEASzdg0akiQpqA8Y38myvK6ZJnlA8CX/D2o4\nJgiCILSD9lw9JQFfAudkWV7aQrONwMyGVVT9gUpZlguuWCcFQRAEC+25emoQcB9wRpKkUw3HngNC\nAGRZ/gz4BbgFSAM0wOx26KcgCILQoN2ChizLBwHpMm1k4JEr0yNBEAThctp9IlwQBEG4doigIQiC\nILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mg\nIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhCm4mgIQiCILSZCBqCIAhC\nm7Xndq+CIAhtotfrqampwWQyYTKZkGUZe3t7HBwcsLIS332vJBE0BEFod7W1taSlpZGamkpubi75\n+fnk5+dTVlZGVVUVtbW1zb5OkiScnJxwd3cnODiYoKAggoOD6datG506dUKhUFzhK7n+iaAhCMIV\npdVqOX/+PGfOnCExMZELFy6Qn5+PLMsA2NjYEBAQQEBAAB07dsTV1RVnZ2ecnJywtrZGkiQkSaK2\ntha1Wk11dTUlJSXk5uZy8uRJNBoNALa2tnTu3Jl+/foxcuRIIiIikCSpPS/9uiD99ou6XsTExMjx\n8fHt3Q1BEBrIskxGRgZHjhzh8OHDnDx5Er1eD0BgYCDR0dGEh4cTERFBZGQkfn5+WFtb/+nPKi4u\n5syZMyQlJZGYmEhSUhImk4ng4GBGjx7Nbbfdhp+f3995idcFSZISZFmOuWw7ETQEQfi7GQwGTp06\nxYEDB9i3bx+5ubkAdOzYkQEDBtC3b1+6deuGh4fH/7wv5eXl7Nu3j927dxMXF4csy4waNYqZM2fS\npUuX//nnXytE0BAE4YqSZZmUlBS2bNnCr7/+Snl5OQqFgtjYWIYPH87AgQPb/Rt+YWEhP/zwA+vW\nraOmpoZRo0bx+OOP4+/v3679uhqIoCEIwhWhUqnYtm0b69evJy0tDYVCwZAhQxg7diz9+/fH0dGx\nvbvYhFqt5vvvv+err74CYPbs2cyaNQsbmxt3mlcEDUEQ/mdkWeb06dP88MMP7N27F71eT5cuXZg8\neTJjxozB1dW1vbvYJoWFhSxdupTdu3cTHR3NW2+9RUBAQHt3q12IoCEIwt/OYDCwZ88evvvuO5KS\nknBxcWH8+PFMnjyZqKio9u7en7Zr1y5ee+01rKyseP311xkwYEB7d+mKE0FDEIS/jU6nY+PGjXz9\n9dcUFhYSFBTEPffcw6RJk1Aqle3dvb9FTk4OCxcuJCMjg+eff55bb721vbt0RbU1aNy4A3iCIFyW\nVqtl3bp1rFq1ipKSEnr06MHTTz/N4MGD//Sy2N8zGI2UqcopLiuhpKwUtaaGmloNtdpaTLKMrUKB\nwkaBvVKJt4cXvp7e+Hh64+Lk/Ld8/m+Cg4P54osvePrpp1m8eDE6nY7bb7/9b/2M60G7Bg1JklYA\nE4FiWZa7NXN+OLABuNhwaJ0sy4uvXA8F4cZUV1fHhg0bWLFiBSUlJfTt25fFixcTExPzlxLkispK\nOH0+idTMdDJyMsnIySS/uBCTyfSH38vbw5Mu4Z3oEt6J3l2607NLd2z+YiBzdHTkgw8+YNGiRbz9\n9tu4ubkxZsyYv/Se15t2HZ6SJGkooAa+aSVoPCXL8sS2vqcYnhKEP89kMvHLL7/w+eefU1BQQK9e\nvZg/fz59+/b9U+9XUani8Mk4jpw6zulzZygsLQbA2tqaEP8gwoNDCQkMxtfTBx9PL7w9vHBxdMLB\n3gEHpT2SlRV6fR11ej01tRpKykspLiulsLSI1Mx0zqVfICs/B1mWcXNxZfhNgxk9cBix3fv8pZpU\nOp2O+fPnk5KSwueff063bk1uT9eda2ZOQ5KkUGCzCBqC0L7i4+NZtmwZ58+fJzo6mocffph+/fr9\noScLWZZJz8lk56G9HDpxlJSLaciyjIebO3269KBH52707NyNyA4d/7a6UGpNDXGJCew+sp8D8UfQ\naGsJDQxh5pS7GD909J9eRltRUcGsWbPQarWsWbMGd3f3v6W/V6vrKWj8BOQC+dQHkLPNtHsIeAgg\nJCSkb1ZW1v+wx4JwfcnLy2Pp0qXs27cPX19fHn30UcaOHfuHvqnnFxeyec82dh7ex8XcLKysrOjR\nqSv9esYwqE8/OoVFXJFqtLq6OvYeO8DX69eQmpmOr5cP/7h9JpNGjvtTn5+amsrMmTMZNGgQ7777\n7nVdu+p6CRougEmWZbUkSbcAH8iyHNna+4knDUFom7q6OlatWsWKFSuwsrJizpw5zJgxo82roXR1\ndew5doDNe7YRl3gCgD7RPRk9cBgj+g/B0+1/XyKkJbIsc/hkHF/+uIozF5LpHd2DV//5DP4+fzwj\n/dtvv2XZsmUsXryYW2655X/Q26vDdRE0mmmbCcTIslzaUhsRNATh8uLj43njjTfIzs5m5MiRLFiw\nAF9f3za9tkxVzk+/bmTtto1UVKnw9/Zl4oix3DpyPH7ebXuP5hiMBjQ6LdZW1tja2GBjbfOXv9mb\nTCY27d7G+199ApLEsw89ztgho/7we8yZM4fCwkLWrVuHg4PDX+rT1eq6WHIrSZIfUCTLsixJ0k3U\n7zRY1s7dEoRrllqt5oMPPuDnn38mKCiIjz76qM2JbNn5uXyzYQ1b9+2gTq9ncN/+3DXhNmK7927T\n0I9GV0taQRYX8i6SW1ZIYUUJeeVFlFSVU6PVoNPXWbSXJAk3B2f8PXzwc/cmxCuA7qGd6BHaGVeH\nti23tbKyYvLoW4jp3puXP3yTF5a9TkZOJvNmzGlzQLKysuLJJ59kzpw5fPPNN8ybN69Nr7tetffq\nqe+B4YAXUAS8DCgAZFn+TJKkR4H5gAGoBZ6UZflwa+8pnjQEoXlHjx7ltddeo6SkhLvvvpt58+a1\naSjqQmY6X61bza4j+1DY2DBx+FjumngboYEhrb6uvFpFQnoSx1MTOXUxmaySxj0z7BS2+Lt7E+jh\nh4+bJ45KBxyVDjjYKjGajNQZ9Oj0dahqqsgvL6agopi8siKMJiMAHX2DGdG9PxNjRxLo2bYhJ4PR\nyNvLl7F+5xbum3wn/7zvoT/0JLNw4ULi4uLYsmXLVVlP66+6Zoan/m4iaAiCJa1WywcffMCPP/5I\naGgoL7/8Mt27d7/s67Lzc/n0+xXsPLwXR3sHbht7K3dPmt7iXIUsy2QW57IvKY7dZ45wPjcdAEel\nA73DoukSHEGnwDA6BXbEx9XzDw89afU6krPTOJ15jvi0M8SnnUGWZfqGd2P6wPGM7DHgsu8pyzLv\nfPEha7dt4L7Jd/LYzLlt/vykpCRmzZrFggULmDFjxh/q+7VABA1BEEhNTeX5558nIyODGTNm8Oij\nj2JnZ9fqa0oryvniv1+zftcvKGwU3D1pOvdMur3FDOwiVSm/JOzll/i9ZJXkAdAtJIohXWO5KbIH\nnQLD/3LSXUufuyV+D5uP7ya3rJCeoZ15YvIDRAdHtPq6SwPHs3OfZNrNbV7Rz+zZs6murubHH3+8\n7lZSiaAhCDcwWZb56aefWLp0Kc7Ozrz66qv079+/1dfo9XrW/LKOL39chbZOx7QxE5kz/T683Js+\nWRhNRg6dS+Cnw9s4duE0JtlEn45dGd1zEEO6xuLr5vW/urRm+7Lp+G4+2/odFTVVTIwZwf9Nmo2L\ng1PLrzEaefyN50hIOsWXb3xEl/C2FVtcv349S5YsYeXKlW16WruWiKAhCDcorVbL66+/ztatWxk4\ncCCvvPLKZXfIi0s8wbtffEhmXjaD+vTjydmPEBIQ1KSduraGjcd38eOhX8grK8LH1ZOJsSOZGDOC\nIK/mNzKSZZmC6lIulueTWZ5PlqqA0hoVqlo1ldpqNHod1pKEJFlhY2WNl6MrPk6e+Dl70tEjkN6B\nnfB39rrsN3u1VsOKnT/y/f5NBHr6suyBF1rsE4CqqpJ7nnoIB6U9q//1nzYlG9bU1DB27FgmTZrE\nokWLLtv+WiKChiDcgHJzc1m4cCGpqanMnTuXOXPmtLqyqbK6imVff8rmPb8S6OvPk7MfYWjswCbt\nqjRq1hzYxPcHNlOj1dAzrAt3Dp7A8G79mww9ybJMelkux3OTOZV/gdP5F6iorTKfd7d3xsfJAzd7\nZ1yVTtgr7EAGo2zCYDJQUqOiqLqcYnU5dcb6vcR9nTyICYpmfOeB9A3qgpXU8jWdzEhm4VdvAfDe\n7GfpGdbylq4H4o/w5JvP8+i9/+D+qW2bp1iwYAHnz59n8+bN19UQlQgagnCDSUhIYOHChZhMJpYs\nWcKgQYNabb/v+CHe/Ox9VFUqZk6ZwQO334edra1FG7VWw3d717Pm4BZqtBpGdO/PrFHT6RIUbtHO\naDJxIu8c+zNOciDzJAVV9alU/i5e9PKPood/JOGeQYS6B+Bq3/Kw0aVMsomMsjxO5qdwKi+FYzlJ\nVOs0BLv5ckePMdwaPRSlovn5mZzSAh7/4jWKVKW8/8ALxEb2aPFznnrrRY4lJrDx09W4u7pdtl+/\nDVGtWbOGiIjW50+uJSJoCMINZMOGDbz55psEBQWxbNkygoKaDi39pqZWw3tffsTmPb8SGRrOy48u\nolOY5c3PYDSyMW4nn29bTUVNFSO69+eBMXcQFRBm0S5bVciWcwf55fwhitXl2FkriA3uyuCwXgzs\n0ANfZ8+/7Rq1hjr2psfzY+JOkgrT8Xf24smh9zC0Y59m26tqqpj3yQsUV5ax8rF36OAT2Gy7zNxs\n7nh8NrOn3c38ux+4bD9yc3OZMmUKzzzzDNOnT/9L13Q1EUFDEG4AsizzySefsHLlSvr168dbb72F\ns3PLiW/n0i/w3NLF5BcXMmvqDB68fWaTsfyEtCTeW/8f0guz6RnWhSdunWOxIskkmziadYbvT/1K\nXM5ZrCSJ/iE9mNhlMINCe7b47b9Gr6VYU0GJRoXGoMNgMmIwGbCWrHG1c8Rd6YyH0hkPpctlh31O\n5J3n3b3fkFGex8iIWJ4dMRsXZdPcifzyYmZ/8DTODk58+8RSlLbN923hOy8Tn3SSLct/wF5p3+pn\ny7LM2LFjGTBgAK+++mqrba8l10VGuCAILTMYDCxZsoTNmzczdepUFi1a1GJFV1mW+XHbBpZ99Snu\nrm58vvh9enWxXP1Tqanmw81fsyluF/7uPrx539MWuQ8Go4Et5w+y+uQ2MisK8HZ0Z37/6UzoMhhv\nJ8sKsCqdmrOlmVwozyGlPId0VR5qfW2brstd6Uy0ZyhdPTsQ49eZYBefJm36BHZm1V2L+fbkVpYf\n+5kLJVl8OPlpAl0t2wZ4+LDk3gU88vnLLN++hscm3t/sZ86YeBt7jh1gz9ED3DL85lb7J0kSUVFR\nZGRktOl6rjciaAjCNUir1bJw4UIOHz7M3LlzefDBB1v8dq7V6Xjjs3+xdf9OBvftz8v/XISbs6tF\nm12nD/Puz8up1FQzc8RUHhxzp/lbudFkYkfqUf5z7GdyK4vp5N2BxTfPY1RELDbW9bcQWZa5WFnA\nsYJzxBWcI6U8BxkZG8maMDd/hgT1wN/JEx8HN3wc3HFUKLGxskFhZY3BZESlU1OhraaktpKU8myS\nSzM5lHeG5YmbifYMZXLEIAYFdsPaqnHS3cbahlkxk+gT2JkFm9/nH2uX8Nm05whxt8wQj43swZR+\nY1i9byM39xpM59/NxwD06tKdQF9/Nu/dftmgARAaGsqGDRuQZfm6mgxvCxE0BOEao9Vqeeqppzh2\n7BjPPfcc06ZNa7FtaUU5T7/9Ikmp55h712zm3HaPxWqqGm0t7/68nF8S9tI5KJwPH3rZYt7ieM5Z\nlh1YTVpZLlFeIfxr4hMMCu1pvlGqtGr2ZJ9ge1Y8mZWFAES5B3Nv9Bh6+0YS7haArfXll7L6O106\n91E/gV+iUbEv5xS/ZBzjzWPf4efowaO9p9HXzzKnood/JJ/f9jwPr3uTJzcv5cvbX8JVaTnZ/s+J\n97PnzFE+3/Y97z/4QpPPlySJMYNGsGr9GtQ1apwcW5+sDwwMpLa2lsrKStzcLj95fj0RQUMQriFa\nrZYnn3yS48eP8+KLL3Lrrbe22DY9+yKPv/4squoq3n76FUb2H2pxPjk7lRe+W0p+eTEPjrmDOaPv\nMC+fLVFXsOzg9+xMPUagizdLxj7MqMhY81LXDFU+P6bs5UBuIkbZRCf3YB7pPZVBgd1wVzY/p2KS\nZTQGHRq9FkmSUFrborRWYGNl3ey3dW8HN6Z3Gs7UqKEcy09mZdJWXjj4BTeHxjKv163Y2zTOT3T0\nCOTtWx7jkZ/f5tmtH/PhrU+Zn4IAnO0duWfYZD7Z+i3nc9Obfdro3yuWr9atJuHsaYbd1PrKMy+v\n+uTFsrIyETQEQbg6GQwGnnnmGY4fP87LL7/MxIktl784fT6JJ954DjtbO/6zZBmdO1p+O1935Ffe\nW/8FXs7ufPbwa/QKiwbqh5l+TtrDR4d+wGgy8o9+U7mvzy3Y2dQvxT1bmsma87uIL0zB3saWSeED\nGRd2Ex1cG4eEZFmmsKaCi1WF5FSXkqcuo6Cmghq9FpmmC2+U1raEuHgT6uJDBxdfunqGYHfJ04m1\nZMXAwG7E+HXiu+SdrE3ZS2pFLq8MmoWPQ+NcSs+AKJ4bOZtXd/6HH8/sYkavsRafM33gOFbuWsvP\nR7fz7PT5TfrRIyoaW4WCU+fOXDZo/BYoVCpVq+2uRyJoCMI1wGQy8eqrr3Lw4EGeeeaZVgPGoRPH\nWPTuK/h6evPRS+8QcMnGQ3qDnvfWf8HPR7czsHMfXr37cXOZ8dIaFUt2fcmRrERuCu7KohH3E+Ra\nvz9GbnUJK878wpH8s7jaOXJ/13FMCO+Ps2393hJGk5Hk8hzOlGZytiyLcq0aADtrGwKdvOjlHYaz\nrQNOCiWOCiUmZHSGOrRGPeXaarKrStiVfRqjbEJprSDWL4qhgd0Icm4sR2JrrWB29/H08O7IG0e/\n45l9y3l3+Dw87RvnZ8Z3HsT2C/XzLzdH9cfTofGck70jw7v1Y+fpQyyY8iC2NpbDZgqFgo7BoaRm\npV/292FvX7/CSqvVXrbt9UYEDUG4Bnz66ads3bqV+fPnt5obcCD+CAvffZnw4FA+fPFtPFwbv4mr\na2tY9M07HE9N5P6R05g37m7zxPKRrERe3bEcjV7HU8PuY3r3UUiShEav5bvkHWxIO4SttYL7u45j\nSuRglA1PHsUaFYfzz3Gk4DxVdRrsrBV0dg9iXGgMUe6BeNu7YtXGiWK9ycjFykIO55/jaMF5DuSd\nJdY3krs6DcPhkmW8ff068fqQB3nuwHKWHFnFu8PnY9NwHZIk8eTQe7lr9XOsStjC40PutviMsb2H\nsPXEPhLSzjCgc9P8jvCQjsQlXn7Jvm1DEqROp2vTtV1PRNAQhKvc2rVrWblyJVOnTmXOnDkttjuY\ncJRF775CZIdw/v3yuzhfMplbWlXO/33xGhmFObx812NMiBkB1OdcrDi+kf8c+5kIzyA+HfcIYR4B\nAJwousAHCT9RolExNiyWmV3HmucrsqqK2XLxOGdKM5GQ6ObVgUEB0XT1DDHfwP8ohZU1Ue6BRLkH\ncnvUYHZmn2J71gnSKwuZ03UM4W6NdaQ6e4bwf32n89ax1XyXvIP7u40znwtx92NURCybkg/wUL9p\nONg27hnSJ7x+BVZCelKzQcPf25fSinIMBkOLy5eh/skPuCL7nl9tRNAQhKtYXFwc77zzDoMHD2bR\nokUtLu88de4Mi959mYgOYXz80jsWAaO4soz5n75IaVUF7z/wAv079QJAZ6hj8Y7/sDMtjvGdBvLM\nyNkobWzRGfV8mbiFTemHCXL25r3h84n2CgUgt7qUjRnHOFOaiYONHRPCYhkcEI3bJauVZFlGpdNQ\noFFRrlNTrq2hsk6DSa6f0ZBlGWeFEi97Z7yUzgQ4uuHxu9VOjgolk8P7090rlBVJO1h64mdmdx1D\njG+kuc2w4F6cKErlh/N7GBTYnQj3xozv23uMZvuFo+xKi2NSdOMCAKWtHdHBEZy+eK7Zn6O3hxey\nLFNWWYGvp3eLvxeDwQCA9f+g5PvVTgQNQbhKlZaW8sILL9ChQwfeeOONFr/5Zufn8tTbL+Ln7cuH\nL75tse9FaVU5D3/2EuXVKj6e+wrdO3QCoFKr5unNH3C64AL/HHQn9/QejyRJ5KvLWHLkGy5WFjA1\ncgj3dxuHnbUCraGOTRlx7MlJRGmjYFLHfowI7oF9wzCVSTaRWVVKelUxF6tKqKzTACABrnYOuNk6\nYC1ZmYNeZV0t2SVlGOX6b+xBTh708upAlKsf1pd8e+/o6sfz/e7gk9NbWJG0AyeFks4ewebzD/Wc\nyOG8JFYlb+fVQbPNx7v7ReDj5MGhzNMWQQMgwr8DuxOPNPuzdGyYq6itbT0RUaOpv77rdb/w1oig\nIQhXIaPRyAsvvIBGo+Gzzz5r8eZUpa7miTefA2DZc29aJO1Vaqp55PNXKKks58N/vGQOGBW1VTz6\n89tkVRTy+riHGR3ZD4BTxWm8fmQVAIsHzSHWvzMASaVZrD6/F5VOzeDArkwO74+jon7Ip9ZQx+nS\nbE6VZlGt16KwsibEyZNYnzCCnDxxt3NocbjKJJuo0NWQXlnMqdJsNmeexNHGjls69CTUpfFbvr2N\nHY/0nMg78Wv5Imk7r/S/Gyfb+pu7o8KeqZFDWJW8nZyqYnP2uCRJDAjpzs60OIwmk0UgCvbyp1JT\nTZVG3WTPDaVd/XVp61qf4Far6yf6nZzaVnzxenLjDcgJwjVg5cqVxMfH8/TTT9OxY8dm2xiNRp5/\nfwn5xYW8u3Axwf6NwzNavY6nVrxBbmkB/5rznLk8eGWtmkd/foecymLev/VJc8DYlZXAiwe+xNPe\nhY9G/x+x/p0xmoysvXCQf5/ejNJGwVMxt3F35+E4KpRoDXr25CXzWdIuDhSk4G7nyOSwvjzafQzT\nwmPp7R2Kt71zq/MbVpIVnkpnbvIN5x/Rw5keHou9jS1r049zujTboq3SxpYHuo1Fo9ex5eJxi3Pj\nwm4C4GDeGYvj3fwjqKmrpaC6xOK4p3PDctmaKn7P1PDkI7VSeh3q8zMAPD3/voKM1wrxpCEIV5mT\nJ0+yfPlyxo0b12ry3ierv+ToqeM8P38BvaMbS3/Lsswr339AYlYKS+5ZQExEfY2pmrpa/rnhHbJV\nhbw38XFig7sCsCH1IJ+d3khP7wheGHAfTrb2VNVpWJ64jfTKAoYHdWda5CAUVtaYZJnTpVkcLLiA\n1qinq0cQsT4d8bZvuUhiW0iSRJiLDwGO7my6eJLtOWdQ67UM8m/MLwl08mRwYDT7884yIrgHPg71\nN38Pexc6uQdzND+ZGV1GmduHuddP6GeU5ZmXDgM4NRQ2VNfWNOmHXl+/f4eilUlwgOLiYqytrXF3\nd2+13fVIBA1BuIrodDpee+01/P39efbZZ1uc+E44e5pVG35g6piJTBk9weLc9wc2sTvxCP+cMJMx\nveqT1AwmI89v+4S00hzenfg4/UK6AbDt4jE+O72RgQHdeKb/3SisbCisqeDjU5upqtMwp+vNxPrV\nTz5X6jRszjpJfo2KECdPRgRF42PvYvHZJlmmXKehoLaayjodWqOeWqMBvcmIg40tTg1//Byc8FU6\nNbk+O2sF08Jj2JZ9hsOFqfg6uBJxyQ3/lrBYDuYlc6wghUnh/czHe/lE8OOFfehNBhRW9bc1v4ay\n7KWaSovP+G1uSG80NPm5qhvmKhwdmlbMvVROTg5+fn5iIlwQhPb11VdfkZ2dzccff4yjY/M3Lk1t\nLYs/fptAX3+emGWZ2ZyUdYGPNn/DsG79uHf4FPPxDw+u4UhWIotGzGJQaE8A9uec5sOEdcT4dmJR\nv/qAkVtdygcnNwAST/SZTFhDpndaZRG/ZJ1ClmFCh150cQ+wuOGrdLWkVJVSUFuN3mRCAlwUSuxt\nbHC1VaKwskZj0KPW11GsVZNaXYaLwo6ubj4EOVoWT7SSrLg5uBsltVXsyDlDiJMntg0lQdzsHIl0\nD+BEcToTO95k7kOIqy8m2UR+dak5O92xYd6jps5yUtvQECwUNk1rYpVXVgDg4dJ6aZDMzEzCwsJa\nbXO9EkFDEK4ShYWFfP3114wbN47+/fu32O6T1V9QUFLE8teWWez9UKvT8vL3y/Bx9eDFOx4131D3\npMXzw+nt3NnzZqZ1q8/PSCksEiVPAAAgAElEQVTP4b3jPxDt2YHnB9yHrbUNpbVVfHRqEzZW1jze\nZwq+Dm7Iskx8yUX25p3D196VW8P64GbXOClfrddxVlVMTk0lCisrAh1c8Ld3xkfphG0L38KNsomc\nmkpSKks5UpJDB001sV6BFkHIxsqakYHRrEk7ygVVId08GzeV6u4Vyk+ph6iuq8WloS/+jvVPFcUa\nlTlo/BZo9A1bxl76cwKa3fejuKwENxfXVvcLr6urIzMzk4EDm26LeyMQQUMQrhLLli1DkiQeffTR\nFtucPp/Ef7eu547xU5rsh/HRlq/JLSvk03mLzauCiqrLeGP3Crr4hPHPQXcC9XtdvH5kFe5KZ14c\neD9KG1uq62r56ORGDCYjT/WdZg4YBwtSOFqUTpSbH7d06IWiYWLbJMucVRWRUlmKlSTR2dWbTi5e\nLQaKS1lLVoQ6uRPi6EayqphzlSXYWdvQ08OypHmQkwfOCiUXVAUWQcOvod5Uca3KHDR+q1VVZ2oM\nEFpDHQBKG8vgUFpd/zTh7erRpG9Z+TmE+Le86yHAhQsXMBgMdO3a9bLXej0SQUMQrgIJCQns3LmT\nefPm4efn12wbk8nE2//5AF9Pbx6++0GLc2eyUlh7eBszhk6iT3j9fIUsy7y992v0JgOvjZ2PwtoG\nWZb5MOEnVDo1S0c8jKudIybZxJdJ26nQqfm/3pPxd6q/mZ4oyeRoUTo9PUMYE9zN/CRQZzRypCSb\nYm0NoU5udHfzRdnMUM/lWEkS3dx9qTMZuVBVSgcnV9xsG5+cJEkiwtWXM2U5FvtWeDZMulc01LeC\n+kAE9XM3v1E3DEs5KBozwgEKK0pRKuxwUlouY5ZlmYycTIbGtP4EkZiYCHDDBg2x5FYQ2pksy3z0\n0Ud4e3tz7733tthu24FdpGam8+i9/8DB3t7i9e9vXIGnsztzx84wH99/8SSHMk/zj5umEuxWP5l8\nIDeRI/lnua/rzUS413+j3pl9ipSKXO7sNNRcqiOjspg9eclEuPpaBAy1vo7dhRmUaDXEegYS6xX0\npwLGpbq5+WAlSaRXlTc5527niEE2UXvJEJOEZP7Xb6oakgldbBvngfKr6pfa+rs0Fj0ESC/MIsw3\nuMkkfF5RAaqqSqIjOrXa32PHjhESEtJicL/eiaAhCO3s0KFDJCUl8dBDD6FUKpttYzQaWf7DV3QK\ni2DMoBEW5/YmHSUp6wLzx9+Ng119MNEbDby//zsiPIO4s+cYAGoNOj47tYFI90CmRQ4BoEijYmP6\nMXp7hzPQvz6Xo7pOy+ask3jbuzChQ69LnjAM7C3MQGs0MMwvlFDnv2e5qa21DcEOrmTXVCLLlqXT\nfytUqNE3FgbUmxpKeFySsFeurc+5cLNrTLbLUdVvChXo2pgoKMsyqfmZRPiHNOlHYspZALpFdmmx\nr1qtlhMnTtCvX78W21zv2jVoSJK0QpKkYkmSklo4L0mS9KEkSWmSJCVKktS0wpggXONWr16Nj48P\nkyZNarHNvuOHyCsqYM70ey2K5MmyzFe71hHk6WcuQgiwKXk/BdWl/HPQXebNiNanHqRCp2Z+rynm\n6rY/px7GxsqauzoNNQeHXblJGE0mbg3rY55MlmWZ+LJ8tEYjQ31D8Va2viT1j3JR2GGQTZh+FzTq\nGlY62V2yoVJJbX2A8FI2Lve9qCrAWrIi8JJS6meLMnC2cyDgkuzyjMJsVDVV5mTHSx09dRw3F1ci\nQ5tu0GRuc/QotbW1DBs27A9e4fWjvZ80vgLGtXJ+PBDZ8Och4NMr0CdBuGLS0tKIi4vjjjvuaLWq\n6rcb/0ugrz/DYi03Bzp64RTnctOYOXKaORAYTEa+it9Ed78Icz5GjV7LTxf20d8/mi6eHQBIVxVw\nuvQiY0P7mCeUs6vLSK0sYoBfJO52jYEhp6aSPE0V3d198LCz5+/221ODviEj+zdqff1KJ4dLJrML\n1PXDWL6OjctiL1Tk0sHF12Jr2cSCVLr5hpt3GwQ4nlY/H9E33HIRgcFo5OipePr1jGm1cu3u3btx\ncXEhJibmD13f9aRdg4Ysy/uBpgOZjSYD38j1jgJukiT5t9JeEK4pmzZtwsbGhilTprTYJjkthTMp\nycyYeFuTZLIfD/6Ch7MbE/oONx87nHmaInU59/W5pXHZbfZJavRa7uoy0txuT04i9jZ2jAruaT6W\nUHIRBxtbYnwacxBkWSa5sgRXhZKo380P/F2q9DpsJCvsfld2JFddjpfS2WIo6lx5DsHO3uYVUxq9\nljOlGfTwbnxCyKss5mJ5vjlo/mZ34lHCfIMJ9PS1OH488QQVVSpG9hvSYh/1ej0HDhxgyJAhrQb4\n6117P2lcTiCQc8n/cxuOCcI1T6fTsWXLFoYMGdLqPtNrf92AvVLJhGE3WxwvUpVy+PwJJt802iJR\nbWPyPjwdXM1JfABbM44S7hZAlHt9hdhKXQ0nSzIYGNDZ/O28qq6W9MoiengGW9SMKqitplqvo7Or\nV4sZ6n+FLMuU1KrxUjpYvH+d0UBeTQWhlww5VdfVklFZSI+GUu0AxwvPYzAZGRjYGCD2pNdvpDQs\nvK/5WJGqlFMXk7m51+Amfdi6fwfOjk4M6ttyfkxcXBzV1dWMGjWqxTY3gqs9aLSJJEkPSZIUL0lS\nfElJyeVfIAhXgQMHDqBSqbjttttabKOrq2P3kf2MGTgCJ0fLiqq7Eg9jkk1MjG18etDUaTmalcTN\nUf3NcxkF6jIyKgsY1aGv+aZ8tiwbk2yiv19n82szq0qQgWgPy+9lRbVqrCWrJpnbf5dibQ3VhjoC\nHCxLkpwtz8Mom4h0a1yldCg/GRmZPj6NTxXbLsbhZe9q3vNDlmU2nt1PV99wi/mMDcd2IEkSY3tb\nPk2oa9TsPnqA0QOHY9ewI19zNm/ejIuLyw09CQ5Xf9DIA4Iv+X9QwzELsiwvl2U5RpblGG/vljdO\nEYSryZYtW/Dx8SE2NrbFNsdOx1NTq2H0wOFNzu1PiiPcL4Rgr8YR2/jcZPQmA4NDe5mPJRRdACD2\nkgCRUpGLk8KeAKfGKq15NRXY29jiYWcZnFR1WtxslW3etvWPkBuSBO2tbQh1anzaMsom4osz8HNw\nJdCxfpWWwWRkb+4ZungEm/udVVXEqeI0JnQcYM7VOJ6TTJaqgOk9Gp8IDEYjG+N20T+qF0FeliPc\n2w/tRVenY/Ko8S32s6Kigj179jBhwgTs7Jpmkt9IrvagsRGY2bCKqj9QKctyQXt3ShD+KrVazZEj\nRxg7dmyrRe8OnTiGo70Dsd17WxzX1uk4nXmeQV36Whw/mZeCnbWCngGN1WHPlWXhqXQh0KlxmCe7\nqoSOrr4WgaBcp8Zb6dxkCKrOZGwy1/B3SakspUxXS7Sbj/mmDxBXlI6qTsMAv0hzf/bmnqFSV8Po\nkMaAuDp5B3bWCsZ1rC+PLssyK45vwMPBlVERjcF45+lDFFeWMW3AWIvPN5lMrNnyE5Gh4URHdKYl\nP/74IwaDgalTp/4t130ta+8lt98DR4BOkiTlSpL0gCRJ8yRJmtfQ5BcgA0gD/gM83E5dFYS/1bFj\nxzAYDAwdOrTVdglJp+gd3aPJxGtyThpGk5GeoZZLR88VXyTSOwTFJUtUc6qLCXHx/V2BQTUeSsty\n5nqTybzE9lJOCluqG0py/J0y1RWcURUR7OhKmFNjzkexporDhal0dgswV7it0mnYknGcbp4diPas\nz7FIKc9mf24i06KGmvMzjmUncTI/hTmxt2L3266CJhMrd62lo28wQ6Itn+oOxB/hYm4WM6fc1eJ8\njUajYc2aNQwdOrTFvU1uJO26BECW5RmXOS8Dj1yh7gjCFXP06FGcnJzo3r17i21UVZVk5ec0O2xy\nPi8dgK4hkRbH08tyGR15k8WxoppyooIaJ8UNJiNaox5n26ZLZ02/W/IK4KZQkq+pprJOi6tt88mH\nf4Qsy1yoKiWxoggfpaNFsUKNoY4NFxNQWtsyumG/D5Mss+rcbgwmA9OjBpuv4eMTP+Nm58T0qPqc\nCYPRwIeH1uDv4sWUrsPNn7clfg8Xi3JYcs+TFstpjUYjn//wFYG+/s0O//3m+++/p6qqitmzZ7fY\n5kZytQ9PCcJ16dSpU/Tq1avVpZupWfWBISosssm53NICnJQOuDs1Tk7rDHVU6WrwcbIsxKcz6s17\neUN9nSYrSUJ/SZ0mAC+lEyW11U0+K8LFE4WVNafKC5pkbP9RepOR+LI8EiuKCHJwYbBPB/OwlN5k\nZH1GPGq9lqkd+5r7vD3rBEllWUyPGoxvw8ZLa87tIk2Vx6N9ppprS60+9SvpZbk8OeQe85NWdW0N\nH2/5hu4dOjG6p2WOy8bdW81lWWxaGCJUqVR8/fXXDB8+vNUAfyMRQUMQrrDq6mouXrxIjx49Wm2X\nnp0JQESHpvs25JcXE+jpZznk1HDDd//dxkh6k9FivkCSJOytbanRW+6D7evgSrVeS4XOckc7O2sb\nurn5UKyt4UxF0Z8KHEbZRGpVGVtzL5CpVtHF1Zv+3sHm/AuNXsePacfIq6nglg69CGiY/D5ZnM7G\n9GPE+EYytGFJ7aniNL4/v5uRIX0YFFh/I08vy+WLuPUMDevD0I6NhSM+2vw1Kk01T0/9h8VThqq6\nkk9Xr6BXl+6MGtBydve3336LRqNh3rx5Lba50dy4GSqC0E7S0+ufIDp1ar0wXmFpEXa2dni4Nq3x\nVF1bg6tD81us/n6Vk6utA5V1loEg0NmLrKpii2Nd3APYn3+ehOKLjA62TIoLd/agSq8jpaqUirpa\nurr54NWGUiJqvY6cmkoyqivQGPV4Kx3p4e6LxyV7cpTWVrMu4zg1eh2TQnvTyb1+ddO58hxWJG0n\nzNWXe7uMQJIk8tVlvHH0W4KcvXm4d31CpKZOy3NbP8bR1p5nRtxvft8jKSdZf2wH9w6fQucgy9Ig\nn3z3JVXqKhY++FiLcxkFBQWsXr2a8ePHExERcdlrvVG0GjQkSfIDkGW5UJIkb2AIkCLL8tkr0TlB\nuB6lpaUBEB7eco0jgOKyUnw8mk+o0+hq8XC2TAj8rYzIbwX9fuPl4EqxRmVxrKOrH9uzTlCj1+LY\nMLzjpFDS1T2QM2U59PUJsygjIkkSvT38cVHYkawqZk/hRdxslXjZOeBiq0RpZYOttTU6o4Eagx6N\noY5SnQZVXf3TjJedA329Aiy2eJVlmbPleezKPVtf/ypyAP4NpUFOlWSwImkHvo7uPNxzAnbWClRa\nNa8cWgnAywNn4ahQYpJNvL7rS7JVhXw0eSGeDa+vUFfy+n//TZhvsEXlX4BT586wfucWZkyc3mqd\nqd/2N3nkETGteqkWg4YkSXOBZ+r/Kb0NzAKSgDclSXpHluUvr0wXBeH6kp+fj0KhwMfHp9V2NZoa\nnJ2cmj1nZWWF/LtJazelE9aSFSXqCovj4W6BHMhNxCibzMNUfX0i2JaZwP7cJMaHNdZRGugfRWpl\nEeszErgzsj8Ol8yFSJJEhIsnoU7uXFRXkK1WkaVWNakXBWAjWeFmq6SHuy/Bjq4W7wNQUlvNztwk\nctXlBDi6MSm0Dy629siyzI7sk6xPO0IHFx/m95yAo0KJSqfm2f3LKdZUsGTwg+Y8jU+PrGVnWhyP\nDryDmOBooD4n47lV71FZU817s5/FTtH42VXqal5c9joBPn48dOf9tGT79u3s2rWLhx9++IYtgd6S\n1p40HgW6AvZAFhDR8MThDuwBRNAQhD+hqKgIHx+fVgvjQf22pHa2zSeSKRV2aOssl8HaWNvg5+xJ\nTqXlsFNPnwi2XYwjtTyXzg3LVYOcvejqGcLunNMMC+puLkHuYmvPpNA+/JxxnB9SjzAptA9e9pbD\nYDZWVkS6eBLp4olJNqEzGtEZDega8jkcbGxRWFk1+4RUoashofgip0qzsbO2YWxwd7p71u9toTXU\nsSZlP8cKU+jrE8HM6FHYWttQVFPBS4dWUKgu45VBs+nmXT/H82PiTr5J2MLUbiO4t88tQP3Tyweb\nVpKQnsQrM/7PYlhKlmVe//RflFSU8eXrH+Jo79CkfwClpaW8/fbbdO3alZkzZ7b4+7lRtRY09LIs\nawCNJEnpsiwXAsiyXCFJ0l9bQiEIN7CKigo8PT0v37AV7k6u5JY2zXON9ArhbFG6xU53fX2jsLWy\n4dfMOHPQAJjUsR/vxP/EqnO7eaj7OHP7UBcvbguPZWPmSb5JOchAv0j6+oSZt3q9lJVkhb2NFfat\nbMRkkk1kVpVysjSTjKoSrJDo7hnMkIBO5ieQ5LJsvju/hwqtmglhsdwSFouVJHG2NJPXjnyNwWRk\n8eA59PSpn1v47+kd/Gv/twwJ681Tw+4z9/27fRv44eAWZgydxC2XFHEEWPnTd+w+up/HZs6lawt7\nZsiyzOLFi9Fqtbz66qs3dGHClrT2E5ElSVLIsqwHJvx2UJIkJWLVlSD8aWq1utUChb+xs7Wjuqbp\nEliAIE8/jl04ZREcAGKCo9mbkUBuZbF5tz5nWwdGdejLzqwE7u86Djdl/ZBXBxcfpoT3Z13aYXbn\nnGbUJZnWIc5ezO48lJ25SRwoSCGuOJ1o90A6ufvj7+BmUdDw90yyTFVdLXk15VysKuFiVQlaox5H\nGzsG+kXS0ysEp4Z5lCqdhp/Tj3C04Dx+Du48FXMbHV39kGWZXy8e5+OT6/C2d+PVQbMJdvFBlmW+\nTtjMp0fWMjSsD2+Mf8Tcly3xe/hw89eM7jmI/5s4y6JPhxKO8tmalYwdMop7b72jxb6vW7eOw4cP\n8/TTTxMaGtryL+cG1lrQmAogSVK0LMvJlxz3AJ76n/ZKEK5jOp2uTfWLnB2dyCvMb/ZcmG8wOn0d\nWSV5hPoEmY8P6tCT91jFrymHebBfY8mLaVFD+DXzOF+e2cKC2DvNx0eH9CK9soC1qYfQGQ2MC+1j\n3n/CUWHH5LC+5KjLOF2aTWJZDidLs4D6FVludg44KZTIsoxJNmGQTah0Gip0NRgb5jkcbGyJcPUl\n3NWHcBdf8xLbSl0NO7JOsj/vLEbZxLjQvtwSFovCyhqVVs2HJ37iSP5ZenqH81z/e3Gxc6TOqOet\n3V+x5fxBxkYN4KXRDzZuMHVsB2+u/ZTYyB68MuP/LIb+ElPO8sy/FhMZGs4L8xe0uFrqzJkzvPfe\ne/Tr14/bb7/9sr+fG1WLQUOW5WwASZL+K0nSKuAdQAk8C8QAO65IDwXhBuXn5cO+uIOYTKYm8x99\nwuuzpRPSkiyCRoCrNwNCuvPz2b3MiplkvqkGOftwR6fhrDm/m5v8uzAkqD5HRJIkHug2llXJu9iU\ncYwLFbnMjB5lUWIk2MmTYCdP6owGctRlFNSoqNBpUNXVUK5VYyVJWElWWEtWuNjaE+rihaedEz4O\nrvjau1jcpPPVZRzIO8uh/GQMJhM3+UVxS1gMPg5uyLLMwdwzfHRiHRqDlge6T2Bq1BCsJSvKalQ8\ns/VjEgtS+cdNU3jgpinm911zYBNLN6xgQKfevD1rEbaXDJWlZV/kiTeew8vdgw+efwulXfMZ7cXF\nxTz11FP4+Pjw+uuvX3a+6UbWlgG7fsDbwGHAGfgOGNTqKwRBaJG1tTUGg+Gy7QJ8/KjT6ykuK8HP\n23LToCBPP/zcvDmQfJzbBlpufnlHzzE8sWkpPyXt5s6ejXtw3BM9hpPFabwXtwYnhT29feszzRVW\n1szuOobOHkGsSTnAK0dWMyK4B4MCuuDj0DiMZmttQ7irL+Guln25nHJtNYklFzlakEJWdTHWkhWx\nflGMD+1rfv+0ily+SPyF0yVpRLgF8lTsnXRwrV+1tC/jBG/sWkGtQccb4x5hVEOZFIPRyAebVvLD\nwS0M79aPJfcuaBIwHn55AXa2tnz80rt4uXs07Rz1taUef/xxamtr+fe//92mocMbWVuChh6opX4V\nlRK4KP9+rZ8gCG3m5ORETU3NZdt1Ca+vVJucltIkaEiSxPi+w/h69zqKK8vwcW2cWB/QoQf9Qrrx\n2ZGfGBkei3dDMUAbK2teGTSLZ/cv55VDK3l50Cz6+EaZ329gQDSd3IP4Oe0IO7JOsj3rBB1d/ejn\n14kIN398HdwtdtBrjkk2UVpbRZ66jFRVPufKcijU1C8BDnLy4vbIwcT6RZnrXuWry/gueQe7s0/g\nYuvAvJ63MiF8ADZW1tTqdbx/4Ds2nN1HlFcIr46dR8eGvT7UWg0vfPsvDp8/wYyhk3hs4v3mPBWA\n1Mx0Hn71KRQ2Nnz66lICfZvf8NNoNPL888+TlpbG+++/L5L42qAtQeM4sAGIBbyAzyRJuk2WZTHo\nJwh/gqurK1lZWZdtFxkajq1CwanzZxg5oGk13EmxI1m5ay1rD23l4VvuNR+XJImnh83k7tXPs2TX\nlyyd9KT5Zu9m58RbQ+fy7P7lvHxwJbd1GsZdnUeibFjF5GnvwoPdx6LSqjlWeIGjBef5PmUfUJ97\n4WHvgo+9K3bWCuysFVhJEhqDjhq9jhp9LUWaSnNyocLKmgi3AAYFRhN9yR4YsiyTVHqR9akHOJx3\nFoWVNXd2GsHtnYfjqKjP1diddpwPD66hsLqMmX0m8FD/aeZ6Uil5GTz/7b/IKyvi2enzmdrfckfD\nE2dP8/Q7L6G0tePTV5cSEhBEc0wmE2+88QYHDhxg4cKFDBokBlDaoi1B4wFZluMb/l0ATJYk6b7/\nYZ8E4brm7+/P4cOHm6x8+j1bhS039ejLvrhDPDHr4SZtg7z8ubn3EL4/sInpg8ZbPG0Eu/ny5NB7\neGvPV7y37xsWDr/f/HpXO0feHjaXz09v4ofzu9mXc4r5vSZzk3/jMlQ3pRNjQ/twc4feFNZUkF1d\nQp66jJLaSsq11eiMeuqMBoyyEQcbJQ4KO9yVznRyDyLAyZNAJ0/8HT0sSq2Xa6vZl3OK3VknSFPl\n4aSw545Ow5kUMRBP+/rCixlluSzd/x3Hc5OJ8Azi02nP0juwvtyKLMusObCZj7d8g6ujM/+e+wp9\nwi3Lnfx6YBevfvwOAT5+fPDCWy0+YZhMJt588002bNjAgw8+yB13tLyiSrB02aBxScC49Niq/013\nBOH6FxwcjE6no7i4GF/f1ucHRvQbwsGEo5xNPU+3qKa5BfPH3cPuxCN8tPkbXrvnCYtzU7uNIK+y\nmFUnfkGSrHhy6D3m5anOtg48FXsnYzr05d8n1/PyoZV08ejA6NC+DA3qiVPD8JEkSfg7eeDv1Px8\nQGtkWSarqoiTRanEFZzjdHEaJmTC3QJ4pPcURneIMT/hZKsK+er4JralHMbR1p6nht3H1G4jzP0t\nrCjh9R8/4diFUwyJjuXFOx/FzbGxMKPBaOTzNSv5at1qekf34N2Fi3F1dmm2X0ajkTfffJP169fz\nwAMPMHfu3D98bTcykbkiCFdYVFTDXEVy8mWDxsgBQ1n61Sd8v3ktrz/5YpPzgZ6+zBk1neXb19C/\nUy8mxIywOP/IwDuQgW9P/EKOqpDXxz2CyyWFBnv6RPDvMY+zOf0I2y7G8dGJdXx2aiN9fCPp4tmB\nTh4hRLoHmetTtcQom6jQVpNVWUi6Kp+MynySSi5Spq2q76eTF3d2HsnwkF6EuDRec3pZLt8kbGH7\nhSMorBXc3nM0s2Nuxa0hC91gNLB6/ya+2P4DAAunPsRtA8dZPHWVV1bw/PtLiD9zkimjJ7DwwcdQ\nKJpPNtTr9bz00kvs2LGDOXPmMG/evFaf9oSmRNAQhCusc+fO2NjYkJiYyIgRI1pt6+TgyNTRE/h+\n81rmF84hyC+wSZvZo6dzPC2Rd9YtJyogjMiAUPM5SZL456A76eDuz9t7vmLmmpf45+C7GBkeY75Z\nKqxsmBo5hCkRg0mtyGVnVgInii5wrOCc+X0cFUrc7JxwUtjj0FAo0Cib0Bn1lGurqdBWW2zg5OPg\nTrRXKL19IunjG4mvY+OTit5oYE96PD+d2cWp/AsobWy5q9dY7u1zC54OjfuDxKUmsnT9F2QU5TC0\n600smPwA/h6W9bqOnjrO4n+/U19T6pGnuXVky/t819TU8Oyzz3L48GEee+wxUSLkT5L+6qYqV5uY\nmBg5Pr7JiJogXFXmzp1LRUUF//3vfy/btqS8lGmPzqR/z768u+i1ZtsUqUp54KNn0Bv0fPbwEsJ8\ng5u0OVOYxpu7V5Jelkt3vwgeG3wXPfybbvD0m+o6DRfKc0hT5VFWW4VKp0ZdV4vWUGfOzbC1tsHD\n3gVPpQue9q6EuPgQ5uqPs61lXSeDycjJvPPsTI1jT3o8lVo1gS7eTO0+klu7DMXVvrEw49nsC3y6\ndTVxqafxd/dhwZQHGNrVcjfCWm0tH65aztptGwgL6sBrjz9Pp7CWVz4VFRXxxBNPkJ6ezqJFi5g2\nbVqLbW9UkiQlyLIcc9l2ImgIwpW3Zs0a3nvvPdauXdumchUr163mk+++YNlzbzCob/9m22SV5DH3\nkxcA+NfsZ+kaEtWkjdFkYsu5A3x+bB2lNSo6+4QyscsQRkfe1GTzpr+qtEZFXHYSx3LOciz7DBW1\n1dgr7Bgc2otbOg+if4fu5uxzgOTsVFbsWsv+s3G4Obowa9Rt3DZgnEWVWqjfN/2Nz5aSXZDL3ROn\nM//uB1C2kmGfnJzMggUL0Gg0vPnmmwwcOPBvvc7rhQgagnAVKykpYeLEicyYMYPHH3/8su3r9HXc\n+9RcKqurWPXuZ/h4ejfb7mJRDo9/8RrFleU8PP4e7hk2udns5lq9jk3J+9lwdi9pZbkAdPeLoId/\nJD39I+nsE4qX4+XzMqD+KaJYXU5eZTEpJVmcL87kfEkmOaoioH4nwdjgaEaExzCwQw+UisYbvCzL\nHD5/gm/3richPQknpQP3DJvMXUMm4ai03MO8TFXOB19/xtb9O/n/9u47PKoq4eP49yST3nslBAIJ\nTQgtAaRIB0UQRRdFVhfQFdauu1bW14Kia2FdyyplUVlExELvTTqEBAgpJCSEdNL7ZJKZOe8fCSwC\nmUwoCQnn8zx5mGTOvaxp5nMAACAASURBVHNugPnNuaf5e/sxd86L9Lutd4P1klKyevVqPvjgA9zd\n3VmwYIGah2GCCg1Fucm9+uqr7Nu3jw0bNuDg0PgueGcyz/LIS7Pp1L4jX735SYOdvWVVFbz74xfs\niD1AZGg4r06ZfVlfwHlSSk4XZrA7NZpD6bEknEu7MM/C0sISb0c3vB3csbOywcrSEitLK/QGA5W1\nWqpqqimpLievvOjCWlMAPo7udPEO5jbfTkQG9aCTZ7vftSgACsqKWHdkJ2uPbCejIAdvFw8eGjaR\nSRGjLwuLmtoaVm1ew8IfvkFXU8Mf75nKo/c+ZLJ1odVqef/991m3bh2RkZHMmzdPzfRuhAoNRbnJ\nxcXF8cgjj/DEE08wa9Yss47Zsm8nr338NiMHDuWd5+aisbzyarNSSn45uIVPVi/BII1MjBjJn0ZO\nwcfV0+T5dfoaEvPSSCnKIresgNzyQvIri9Hpa6g16Kkx6LGy1GBvZYuDtR3Otvb4OXni7+yFn7Mn\nnTzbNXibq0JbyZ74I2w7vp/9iUcxGI307tiNeyJHMzp88IV1ss4zGo1s3beTL5YvITsvhwG9+vHC\nzCcJDgi64vnPi4uLY+7cuWRkZDBr1ixmzZqFZQO/J+V/VGgoSivwwgsvEBUVxerVq83+JLx87Y98\nsvRLhvW/nXnPz8XG2rrBsudKCli6/SdWH96GAO7sdwcT+o2gZ3CXGz7UVEpJWl4mh5NPcDAxhkPJ\nx9Eb9Hi7eDC291AmRY4iyMv/suOMRiO7Du1l8U/LSDpzms7BITw9/XEGhPc3+Xp6vZ6lS5eycOFC\nPD09efPNN+nXr9H3QKWeCg1FaQVSU1OZOnUqEydO5PXXXzf7uJUbfuEfi/9F985dmP/CG5etTXWp\n7KI8lm5fxeaYPWhrqvF19WJIt34MCOvNbcFhv5sod7Vq9bUkZ6cRl5HMyfRkjp6OJa+0EKhbYPGO\nHpGM6DmIbu06XbGfpba2lq37d7H05+WcyTxLkF8gM6c8zLihoxpddTYxMZG33nqLpKQkxo4dy0sv\nvYSz8/Xt2G/rVGgoSivx6aef8u2337JgwQIGDx5s9nE7Dv7GW599gEaj4f+eepnBDYyquliVTsuu\nk4fYemwvMalxVOmqAejgHUhH3yACPHwI8PDFy9kdexs7HGztsLW2wWg0UmvQozcYqNBWUlRRSnFF\nKfmlhaQXZHM2L5vMwlwMRgMA7k6u9AruwoCw3kR07kWAR8Ohdq4wn1+2rOPXbespLCkiJKgDM+6b\nxsiBwxq9rVRVVcXChQtZvnw5rq6uvPTSS4wYMcLs36HyPyo0FKWVqKmpYfr06ZSUlPDtt982Okv8\nYmezM3j5H29yOj2VUQOH8dQf/4y/t69Zx1bX6kjISOH4mQROpCWSXpBNdlEeekPjy7afZ62xop2n\nH0Fe/gR5+RMW0JEeQaH4uHqavP2l1+vZF3OIdTs2sSfqAEYpub1PJFPGTWJgeP9GWxZSSrZu3cqC\nBQvIy8tj0qRJPPPMM6p1cQ1UaChKK5KSksKMGTMICAhg4cKFZo2mOq+mtoZvf/2BpT8vR0ojU++6\nj4funoKHa9PXizIYDeSXFlFQVkxVTTVVOi3VNdVYCAusNBo0FhocbO1xd3LBzdEFZztHs/tGDAYD\nsUnx7Dy4h017t1NUUoy7qxt3DRvDvWPuJtD38v6NK4mLi+Of//wn0dHRhIWF8be//Y1evXo1+VqV\n31OhoSitzIEDB3j22Wfp27cvH330EXZ2do0fdJFzhfl8tmwhm/dsx0qjYczgEdw/7h66hoS22PpK\nldoqok7GsO/oIXYf3kdRaTEajYbBfSK5e8R4BvWOQKMxbzWjjIwMvvjiC7Zu3YqrqytPPPEEkydP\nViOjrpNWERpCiHHAPwFLYJGUcv4lzz8K/APIqv/RZ1LKRabOqUJDac3Wr1/Pm2++SY8ePfjkk09w\ncXFp/KBLpGdnsnzdKjbs3oK2upp2vgEMi7idYRG3c1totxv6JltWUU5cciInkxOIOhnDiVNx6PV6\n7GxtGdx3IHdEDmZQ7wgc7c1vSWVmZrJ06VLWrl2LlZUV06ZNY/r06Tg6OjZ+sGK2mz40hBCWQBIw\nGsikbrOnB6WU8ReVeRToJ6V80tzzqtBQWrudO3fy6quv0q5dOxYsWIC/v3m3bS5VUVnBln072Xlo\nL1EnY9Dr9TjY2dO1Uxg9OnclNLgTHQKDCPDxw87W/FaNlJLyygpy8nLJyM3i9NkzpGScITU9jfSc\nutnlQgg6B4cwoFc/Bob3p2eX7lhbNTw0+EpSU1P5z3/+w+bNm9FoNEyaNImZM2fi6Wl6rolydVpD\naAwE/k9KObb++1cApJTvXVTmUVRoKLegqKgoXnjhBaBu5vjYsWOv6XwVVZXsjzlMTPwJ4pITSEpL\nwWAwXHjew9UddxdXnBydcLJ3xNbGhvPvDUajkQptFRWVFZRXVlBQXEilturCsRYWFrTzDaBDu/Z0\nCwmje+eudOsU1qTWxHlSSo4cOcKyZcvYv38/tra23H///UybNk2FxQ3WGkJjCjBOSjmr/vvpQOTF\nAVEfGu8B+dS1Sp6TUmaYOq8KDaWtyMrKYu7cuZw4cYIJEybw4osvXrdbMrqaGs5kppGenUlmbjZZ\n53IoKS+lvLKCisoKqnW6un4QAQKBo4MDTvaOONo74OHmgZ+XD35ePvj7+BIc0N7kkh7mqKioYOPG\njfz8888kJyfj7u7OAw88wJQpU9TyH82krYSGB1AhpdQJIf4M/EFKedkgbCHE48DjAEFBQX3N2X9Z\nUVoDvV7PokWLWLJkCa6ursyaNYvJkyc3uO5UayKlJC4ujjVr1rBx40a0Wi2hoaFMnTqVsWPHYnON\nQaQ0TWsIjUZvT11S3hIoklKa7BlULQ2lLYqPj2fBggVER0cTEBDA7NmzGTVqlNkjj24WUkpSUlLY\nvHkzW7ZsISsrCxsbG8aMGcN9991Hjx49Gj+JckO0htDQUHfLaSR1o6OOAA9JKeMuKuMnpcypfzwZ\neElKaXLaqwoNpa2SUrJ//34+//xzkpKS8PX1ZcqUKUyaNAk3N7eWrl6DpJQkJSWxfft2tm/fztmz\nZ7GwsKB///6MGTOG4cOHq0l5N4GbPjQAhBB3AguoG3K7REo5TwjxFhAlpVwjhHgPmAjogSJgtpQy\n0dQ5VWgobZ3RaGT37t38+OOPHD58GCsrKwYPHszw4cMZMmQITk5OLVo/KSXZ2dlER0dz+PBhDh8+\nTGFhIZaWlvTt25cRI0YwfPhwPDw8WrSeyu+1itC4EVRoKLeS1NRUfvrpJ7Zv305BQQGWlpb07NmT\nyMhI+vTpQ/fu3W9434BWqyU5OZnExERiYmKIiYmhoKAAAHd3d/r3709ERARDhw69qVtEtzoVGopy\nCzEajcTHx7Njxw6OHDlCYmIiUkqsrKzo0KEDISEhdOzYkXbt2uHl5YW3tzceHh5Ym1hW/Ty9Xk9R\nURGFhYUUFhaSk5NDZmYmmZmZpKWlkZ6efmF4rre3N3369CE8PJzw8HBCQkJabDa60jQqNBTlFlZW\nVkZMTAzHjh0jJSWFlJQUzp07d1k5Kysr7O3tcXBw+N1Mcb1eT1VVFVqtlpqamsuOs7GxISAggKCg\nIEJDQwkLCyMsLAwfHx8VEq2UuaHRuoZeKK1CRUUFeXl5lJSUUFZWRklJCVqtFp1Oh06no7a29kJZ\nIQTW1tbY2tpia2uLvb09rq6uuLq64ubmhpeXV5sYXtrcnJ2dGTZsGMOGDbvws4qKCnJycsjLyyM/\nP5/CwkKqqqqoqqqisrISo/F/W7ZaWlpib2+PnZ0d9vb2uLm54eHhgbu7O76+vnh6eja6Eq3SNqnQ\nUK5KaWkpycnJpKenk5GRQXp6OpmZmeTm5lJZWWnyWI1Gc+HTqNFo/N3M5EsJIfD09MTPz+/CJ9t2\n7doRHBxMhw4d1Fj+JnB0dKRz58507ty5pauitGIqNJRG6XQ64uPjiYmJITY2lqSkpN/d6rCysiIw\nMJB27drRt29ffH198fb2xs3NDRcXF1xcXHBwcMDGxgZra+vLbl8YDAZ0Oh3V1dVUVFRQWlpKSUkJ\nhYWFnDt3jtzcXHJzc4mOjmbjxo0XjrO0tCQ4OJjQ0FB69epFeHg4HTt2VJ+AFeUGUqGhXEZKSWpq\nKvv372ffvn2cOHHiwn3tDh060KdPHzp37kxoaCjBwcF4eXld08qp52+F2Nvb4+5ueg+I6upqsrKy\nOHPmDElJSSQlJXHkyJELYeLi4kLfvn0ZNGgQgwYNwtvb+6rrpSjK5VRHuALUBUVCQgJbtmxh27Zt\n5ObmAhASEsKAAQPo3bs34eHhN+U6QFJKsrKyiImJITo6moMHD5Kfnw9A586dGTVqFGPGjKFdu3Yt\nXFNFuXmp0VOKWfLy8li9ejXr168nMzMTjUbDwIEDGTZsGAMGDMDX17ytQ28mUkpOnz7NgQMH2L17\nN8ePHwegW7du3HXXXdx5550tPgFOUW42KjSUBkkpOXr0KCtXrmT37t0YDAYiIiIYO3Zsm1zSITc3\nl23btrFx40ZOnTqFra0t48eP54EHHlCdwopST4WGchkpJQcOHGDRokWcOHECFxcXJk6cyH333Udg\nYOB1fZ3CkiLyiwopKK77qqisQKurRltdTU1tTX1nuMDCQmBna4ejvQMOdg64Ojvj4+mNj4cX7i5u\n171TOyEhgVWrVrFp0yZ0Oh1Dhgxh1qxZdO/e/bq+jqK0Nio0lN+JjY3l448/JjY2Fh8fHx599FEm\nTpx4zUNW84sKSEhJIiEliZT0M2TkZJGRm4WuRnfF8jbWNthYWyOlREqJ0WhEq6vmSv8ONRoN7XwD\naB/Qjvb+7Qjr0JnbQrvi4+l9zRPIysrKWLlyJcuXL6esrIyhQ4fy7LPPEhQUdE3nVZTWSoWGAkBh\nYSGfffYZa9euxcvLi8cee4y77777qifMFZUWc/hENAePRXEkNpq8wroO5/O7t7XzCyDIL5BAvwC8\n3T3xdHPHw9UdJ0cnbK1trjjK6nxwVFRVUlJawrnCfM4V5pObf470nEzOZmWQnpN5YT6Hh6s7vbr0\nYFDvCAb27o+3h9dV/34qKyv54Ycf+Oabb6ipqWH69OnMmDEDW1vbqz6norRGKjRucVJK1qxZwyef\nfEJ1dTXTpk1j5syZ2NvbN/lcBcWFbNu/m637dnLiVN3K9S6OzvS/rTe9ut5G15BQQoNDmrTPdFPV\n1taSfDaVk8kJnEyK5+jJY+QV1S2KF9qhE6MH3cG4ISPx9fK5qvMXFBTw6aefsmHDBvz8/HjjjTfo\n16/R/z+K0mao0LiFlZeXM2/ePLZt20bfvn155ZVXCA4ObtI5jEYj+6IPsXLDLxw6cRQpJZ3bd2Tk\noGEMDI8grEOna5qbca2klKSkn2Ff9CF+O7KfE6fiEELQp1svJo4cz+hBd1xVayo6Opp33nmHzMxM\nHnvsMWbMmNGi16kozUWFxi0qPj6el156iby8PGbPns306dOb9KZXU1vDr1vX8/36n8jMzcbb3ZOJ\nI+9kzODhdAhsf9X1klKirammoroKXW0NFkJgISywsLDA2c4RW2uba+qnyMzNZtOe7WzYtYWM3Cw8\n3Tx4aMJ93Dd2EvZ2TWsBVVVVMX/+fDZs2ED//v2ZP38+Li4mN4xUlFZPhcYtaO/evbz88su4ubnx\n3nvvNWnrTCkl2/bv5rNlC8nOy6FnWHem3nUfwyMHm72lqJSSzMJcEjJOk5R9hqzCc+QU55FTlEdp\nVQVGaWzwWBuNNS4OTni7ehDk6U977wCCvQPoERSGl4vpWeIXMxqNHDwexbLVKzkSG42bsytPPDiD\nSSPHNyk8pZSsXbuW9957j3bt2vH555/j5XX1fSeKcrNToXGLWbNmDfPmzSM0NJQFCxY0aVe0hJQk\nPlj4T04mJ9C5fUee/uOfGRDe36xjs4vy2BsfxYFT0ZxIS6RcW7dYocZSg5+bF/7u3vi5eePh5IqD\nrT0OtvbYWFljNBoxSiMGo5FybSUllWWUVJSRW5JPen42eaWFF17Dz82LnsFdiQztxbAekTjZOZhV\ntxOn4vjsu6+JSYgltEMn/jbraXp1adoe1FFRUTz//PO4ubnx5Zdf4u/v36TjFaW1UKFxC9m2bRuv\nvPIKkZGRfPDBB2Z3dhuNRv679kc+/+8i3Jxdmf3QDO4aNqbRT+QV2ko2Rf/G6sPbOJWVCkA7Tz/6\ndbqNru060TUwhI4+7bDSXP2S5lU6LWfOZXAi7RTHzyRwLC2BovISNJYaBoSGM7bPUEbcNqDR1zjf\ngvrnt/8mrzCfmVOmM+v+pt2yO3nyJE8//TSenp4sWbIER0fHq74uRblZqdC4RRw7dow5c+bQpUsX\nvvjiC7OHihaVFvN//3qfAzGHGR45hNfnvIizo+mlNVJy01n+2xq2xuylulZHl4COjO09lMHd+9He\nK+B6XE6DpJTEZySz7fh+th3fx7mSAjyc3Jhy+3juGzgWVwfTs9grtVX8Y9G/WL9rMxE9+/L2s6/i\n7mL+1qNHjhzhySefZNCgQXz44Yeqc1xpc1Ro3AIKCgqYOnUqzs7OLFmyxOzFBLPzcpn9xgsUFBfw\n/J/+wr1j7jbZCV1cUcpXm7/n14NbsbGyZmzvIUweOJaugSHX61KaxGg0cij5OCt+W8uBUzE42Ngx\na8wf+MPgCWgaeTNfs30jHyz6J97uXnzx5kf4epq/Cu7KlSv54IMPeOaZZ5g+ffq1Xoai3FRUaLRx\nRqORp556imPHjrFs2TI6dOhg1nHp2ZnMefNFtNVaPn19Pt07dzX5Gj8d2MS/Ny2nSqdlyqDxzBz9\nQKOf6s8zGI2kl+SSXJDOuYoiSrTlFGvLqKqpRmNhicbCEmtLKzwcXPFz9sTfyZNgd388HcxfSTcl\nN53P1n3LvsSjhAV05JUps+nWrpPJY06ciuPpd17GxdGZr99ZgI+ZkwOllLz44oscOHCA5cuXN3kY\ns6LczFRotHE///wz7777Lq+++ir33nuvWccUlRYz/a9PUFNbw2d//wdhHRp+c9Xqqpm7/GN+iztC\nZGgvnps4k46+jS8tXlhVyo7kw+xKjSbuXAra2v8tJ2JtaYWrnRMO1rYYjEZqjXpq9LUUa8swXvTv\nMNjNn8igHgwI6kFEUA80FqZbD1JKdpw4wEerF1FUXsozdz/Kg0PvNnlM3OlE/vLmX/Hx8OKb97/A\n1sa823oFBQX84Q9/ICQkhK+//tqsYxSlNVCh0YZVV1czefJk/P39WbRokVnzGwwGA0+/8zLHEk6w\n+N1/0aVjaINlC8qKeWHJu5zKSuW5iX/igcF3mXwNg9HIrtSj/HpyJ1GZ8RilpKN7AP0Cu9HVO5hO\nnkEEuHhhb2V7xfPoDXryKorJLsvnVP5ZDmfEEZOViM5Qi7ejO5N73MGkbsPwaKQFUqGt5J2Vn7Mj\n9gCPjLiXOeMfNlnvQ8ejeOrtl7hn1F28+sTzJs99seXLl/Pxxx+zaNEiwsPDzT5OUW5mKjTasBUr\nVvDhhx/y1Vdf0bdvX7OO+eaX7/ls2UJem/0C94y6q8FyheUlzPzXyxSVlzDv4RcY0t300NuYrETe\n3/UNZ4qy8Xf2YmzoAMaEDqCjx7WtmqvT13DwbCw/ndzBofSTWFpY8kDPUcwZdD/Wlg2PmDIYDfzj\nl4X8fGAzDw69m+cmzjD5Ov/67mu+/XUFH78yjyH9BppVN61Wy4QJEwgPD+ejjz5q0nUpys3K3NBQ\n2722MlJKfvnlF3r06GF2YBSVFrNk1TKG9b+dSSPvbLCcwWjg9WUfUVhWzJez36ZH+4ZbIzp9Df/c\n+z0/xe7A39mLeePmMDykP5bXaSlzG401w0L6MiykL+kluSyL3sD3xzYTnZXIO+PmEOR65c2hLC0s\neeneP2NpYcn3v62lZ/sujOw1qMHXmf3gDHYd3suX3y9hcN8BZrXa7OzsmDBhAitXrqSyshIHB/Pm\njShKW3B9NytQbriTJ0+SkpLCpEmTzD5myapl6Gp0PDn9MZNviou3/sjRlJP87d7HTQZGha6K2T/P\n56fYHTwYPpblD81jVOfI6xYYlwpy9eXVETP44K5nyCkr4JEVb7DnTEyD5YUQPDfxT/QICuWdHz/n\nXElBg2U1Gg0z7nuY5LQUfovab3adhg4dSm1tLQcPHmzStShKa6dCo5XZu3cvFhYWjB492qzyNbU1\nrNu1hbFDRhIc0PBeEcUVpXy36xdGhw/m7oiRDZYzSiNvbPmKxPw05o9/imeHPISd1bXtyWGuYR37\nsOzBt2nv5sfrm77gbHFOg2U1lhrenvY81TU6/rt7jcnzjh0yEg9Xdzbt3mZ2XXr16oW1tTWxsbFm\nH6MobYEKjVYmJiaGsLAws2clHz4RTWVVJWMGjzBZ7oe966nR1zJr9AMmy/3nyFr2ph3jucEPMrxT\n8y8d7uPkwQd3PY21xoo3tnyF3qBvsGyAhw8jew1i7ZHtVFZrGyynsbRkcN8BHDgehV7f8Pl+d4xG\nQ/v27Tlz5kyTr0FRWrMWDQ0hxDghxCkhxGkhxMtXeN5GCPFD/fOHhBDBzV/Lm0tycnKTtiaNOhmD\njbU1Ebf1MVluS8weBob1poNPw8Nqy3WVLI1ay6hOEUzpOcrsOlxv3o7uvDD0YRLyznAw/aTJspMj\nx1BZXcXRFNMtgv639aayqpLUzLNm1yMwMJDs7GyzyytKW9BioSGEsAQ+B8YD3YAHhRDdLik2EyiW\nUnYCPgHeb95a3lxqamooLy/H09PT7GMyc7IJ8PE3ubdEaVU5mYW59O5oOox2p0ZTY6jlod7jrnm7\n1Ws1olN/7Kxs2Jd2zGS5sMCOAJw5l2Gy3PnNmwqKC02Wu5idnR063ZW3tVWUtqolWxoRwGkpZaqU\nsgZYAVzauzsJ+Kb+8SpgpGjpd6sWVF5eDoCzs3kzsgHyivLxaWSpjLRzmQB08jO9X0Z0ViJuds50\n8+lo9uvfKNaWVvT060xsborJco629rg6OJNdlGey3Pl1t8oqys2ug4WFxYUtaBXlVtGSoREAXPzx\nL7P+Z1csI6XUA6WA+Wt+tzHnFyNsyqdbK41Vo/fpz6/XZGq/CwCNhSUWQrR4K+O8Gn0tjtamN1iS\nUlJZXYWjrelhsefDwsXR/EAuKCho0hL0itIWtImOcCHE40KIKCFEVH5+fktX54axs7PDwsKCsrIy\ns4+xt7OnoqrCdBmbujfeskbKOVjbUVZdiU5fY/br3ygGo5HM0jw87E3vqFdUUUqtQY+7k+ly5wrq\nWiIebuZv+JSbm4u3t/kLHipKW9CSoZEFXNzrGlj/syuWEUJoABfgspvOUsqvpZT9pJT92vLuahYW\nFgQFBZGSYvqWzMVCgoJJST9DTW3Db/TtPP1xtnPk6GnTncq3B4dTa9SzPnGf2a9/o+xMOUJ+ZTEj\nOpmesb4n/ggA/TrdZrJc1MljONjZ09HMLW3Ly8s5e/YsYWFh5lVYUdqIlgyNI0BnIUQHIYQ1MBW4\ndED9GuCR+sdTgB2yra170kRdu3YlPj4ec38NPcO6U1NbS/zpUw2W0VhaMiAsnH0JUehMhEvfgC50\n9+nIt1HrKKuubHLdrxdtrY5Fh3+lvasfd4Q0POxXSsnGo7to5+lHqH/DqwAbDAb2RR+if88+Zm9t\ne/z4caSU9OzZs8n1V5TWrMVCo76P4klgM5AArJRSxgkh3hJCTKwvthjwEEKcBp4HLhuWe6uJiIig\noKCAuLg488rf1gc7W1vW7thkstzEyNEUV5axcu/6BssIIXh68IMUVJbwwrpPqG6B21TVtTr+tv6f\nnC3O4dkhD5qchb477jAxqfHcf/t4k/0wOw/tIa8wn3FDGp7UeKmtW7fi4OBAr169mlR/RWntWrRP\nQ0q5QUoZKqUMkVLOq//Z36WUa+ofV0sp75dSdpJSRkgpU1uyvjeD4cOHY21tzcaNG80q7+jgyJ1D\nR7N573aKS0saLBfRuSe3d+nLf7avoqCsqMFy4f6hvDnmz8TmnOaVDZ9RUdPwpLnrraCyhOfWfsyR\njHheGzmTQcENv2GXVpXz4S8L6eTXnimDxjdYzmg08u2vKwjyC+SOiMFm1aOyspIdO3YwZswYs3dK\nVJS2ok10hN9KHB0dGTZsGOvWraOiwnTH9XlT77oPvcHAp999ZbLcMxMfRW8w8Nf/zKe6tuERWiM7\nR/DS8Ec4mH6CPyx7mU2n9pt9u+xqGKWR9Ql7efC/rxKXm8KbY/7MhK5DGiyv1VXz/OJ5FFeU8tr9\nc9BYNnzLadWm1SSkJDFjysNmb+H63//+F61Wy+TJk5t8LYrS2qnQaIUefvhhKisr+fXXX80qHxwY\nxMMTH2Ddzs0ciY1uuJx3IG9Ne474zNO8vuxjavS1DZad3GM4i+//O94Obryx5Sue+PldorMSGx22\n2xS1Bj1bkg7y8PdzeWvbQoLd/Vj24DuMDWt4CXNdbQ2vfPcP4tKTeefh5+ke1PDCixk5Wfxr2UIG\n9o7gzmHmreVVVFTEsmXLGDFiBN26XToXVVHaPrWfRis1e/ZskpKSWLVqFW5ubo2Wr9bpmPbiY1RU\nVrJ0/uf4eV95aXGAlXs38OGvC+kZ3IX5f/wrns4ND0M1SiPr4vfw+f4fKakux8vBleGd+jMipD/d\nfTua3PviSsqqKzmSEceB9Fj2nomhWFtOkKsvj0VOZlTnCCxEw59zsovyeH3ZR5xMT+LVKbO5Z8CY\nBsuWlJUy67WnKS4rYfnHi8za8lVKyd/+9jf27NnDihUr1HavSpuiNmFq41JSUpg2bRpjxozhrbfe\nMuuYtMx0/vTKX/Dx9GbRvE9xtG94wtu24/t4a8W/cLJz4L0//pWewV1Mnltbq+O31Gi2nz7MgbOx\n1Bhq0VhYEuIRSBfvYNq5+OBq54SrnRNWlhoMRiM6fQ3aWh2ZpXmcLc7mbHEOqUVZGKXEycaeyHY9\nmNBtCJFBPUyG+fHyuQAAEKNJREFUBcDO2IO8s/IzjFLy9z88yfDbGm6NVGm1/OXNF0lKO83nb3xI\neFfTw3HP27hxI3PnzuWpp57ikUceafwARWlFVGjcAr788ksWL17M22+/zfjxDXf2XuzQ8aM8885L\ndAkJZcFr7+FqYtJbcnYaf106n5ziPO7qO5zZ46fh5dL45LfKGi2H0+OIz0slMS+NxLw0ynQND9G1\nEAJ/Z286uPvR2bM9A9vfRjefjo3uDQ6QlpfJFxuWsevkIboGduLd6S8Q4NFwKyq/qIDn33uNpLQU\n3n/x/7gj0rzO79OnTzNz5kxCQkJYuHCh2f0fitJaqNC4Bej1eubMmUNcXByLFy+mSxfTrYHzdh/e\nx6sfv4Wfly8LXnuXQN9LV2/5nwptJUu2r+KHPeuwtLBk+vDJTB0yASc783erk1KirdVRUl1Oibac\nGoMeSwsLbDXW2FnZ4O3o3uTbWOn52SzeupLNMXuwsbJmxqgpPDR0Ilaahs9zKjWZ5997jfKqCuY9\nN9fs7V0LCwt59NFHqa2tZenSpfj6NhxKitJaqdC4RRQVFTF9+nQAvvrqKwIDzdub+1hCLC/On4tE\n8trsFxgxYKjJ8lmFufxr/bfsOHEAWysbRoffzj0DxtAjKLTZ1qLS6qrZHXeYTdG7OXjqGNYaK+6/\n/U4evmMSbo4Nt5gMBgMrN/7K5/9dhIuTM5+8+i6hwSFmvWZJSQl/+ctfSEtLY+HCharzW2mzVGjc\nQpKSknjiiSewt7fn66+/xt/f36zjMnKyeO2Td0hIOcXEEeN59tHZODmY3twpKesMq/ZvZHPMHrQ1\n1fi5eTO4Wz8Gd+tH35AeWJv4pH81zpUUcCT5BAeTjrE3/ghVump8Xb0Y33cYDwy+Cw8nV5PHp2Wl\n8/bn/+DEqTgG9x3Aa7NfwNPNvEUGCwsLmTNnDhkZGXz44YcMGtTwXuOK0tqp0LjFJCYmMnv2bBwd\nHfn000/p0KHhZTMuVltby9crv+HbX1fg5uzK7AdnMGH42Ebv2VdWa9l2fB974g9zKOk4utoarCw1\nhPp3ICywI538ggn2DsDPzQtvFw+Tt42klJRpKygoKyazIIeU3HRSctNJzEwho6BuS1d3RxeGdO/P\n+D53EN6hKxaN7EdeWl7Gd6t/YMX6n7CxtuGFGU8yfugos1tFZ8+e5bnnniMvL4+PP/6YiIgIs45T\nlNZKhcYtKDExkaeffhqdTsf8+fMZONC8e/YAialJfLDoU2JPxRMS1IHHHvgjd0QMNqvDt7pWR1Ry\nLNEpJ0nITCExK5UqnfbChD8hBE62DtjZ2GJvbYuVxopagx69QY+utoaSyrLL5oT4u/vQyS+IPiE9\niOjcixDfILPe8CsqK/h+/U/8d+0qqrRVjBk8gmcfecLs1gXU7cP+2muvYWVlxYcffkh4eLjZxypK\na6VC4xaVk5PD888/T0pKCk899RTTpk1r9FP5eVJKdh7cw+fLF5GenUmQXyBTJ9zHXcPGYG9net+K\nixmNRnJLCsgqzCW3OJ+c4nzKtBVodVqqdNXU6GuxstRgpbHCylKDq6Mzns5ueDm74+vmRUefIBxs\nzX89gNSMNH7eso51OzdRqa1ieOQQHp/6KJ2CzGtxQd3OiF9//TVLly4lLCyMDz/8ED8/vybVQ1Fa\nKxUat7CqqireeOMNdu7cSWRkJHPnzm3SiB+DwcCuw3v59tcVxJ8+ha2NLcMibmf8kJFE9upn9kqw\nN9q5wnx2HdrD1n27OJ54EiuNFSMGDuXhiffTpWPDM8GvJDExkTfeeIOUlBTuueceXnzxRbWulHJL\nUaFxi5NS8vPPP7NgwQIsLCx4+umnmTx5stmtjvPnOJmcwLqdm9m2fxdlFeW4ODkzoFc/BvaOILJX\nPzybsGnRtaqtrSXudCJHYmPYH32Ik8kJAHQIbM/dw8cyYfg43FxMd4xfqqKigsWLF7N8+XLc3Nx4\n/fXXGTzYvLkbitKWqNBQAMjKyuLtt98mKiqKbt268eyzz9KnT58mn6e2tpb9xw6z48BvHDwWRVFp\nMQBBfoF07RRG905dCOvQiSD/QDxc3a95GK6upobM3CySz6ZyKjWZU2dOE5sUT7WuGiEEXTqGMjxy\nMMMjhxAcGNTk8xsMBlavXs2///1vioqKmDhxIs8++2yT9l9XlLZEhYZygZSSTZs28emnn5Kfn8+g\nQYOYM2eO2ZMBL2U0GklOS+HIyRiOJ8QSf/oUeUUFF563t7UjwNcfTzcP3F1ccXN2xdHBAWsra2ys\nbdBYatDXd4Tr9XrKKysoqyintLyU/KJCsvNyKSj+3waN1lZWhAR1oGdYd/r2CKdPt164OF3dm7te\nr2fr1q0sXryYtLQ0wsPDee655+jevftVnU9R2goVGsplqqurWblyJUuXLqWsrIzbb7+dhx56iIiI\niGtuGeQXFXD67BkycjJJz8ki61w2hSXFFJcWU1RaTE1twyvmWlpY4OzojIuTM+6ubvh7++Lv7UeA\njx+hwSEEBwRdcz9KZWUlGzdu5LvvviMrK4tOnTrx+OOPM3z48GabnKgoNzMVGkqDysvLWbFiBT/+\n+CNFRUWEhIQwdepURo8ejaOj6cl9V0NKicFgQFdbQ01NDbX6WjQaDRrLui87W9sb8sYtpSQhIYFf\nfvmFTZs2odVq6d69O3/6058YOnRok/p3FKWtU6GhNEqn07FlyxaWL19OcnIyNjY2DB8+nAkTJtCv\n380zSqqpMjMz2bx5M5s3byY1NRUbGxvGjBnDvffeS48ePVTLQlGuQIWGYjYpJSdPnmTdunVs2bKF\n8vJynJycGDRoEEOGDGHAgAG4ujZtVFJzqq2tJTY2lgMHDnDw4EESEupGVYWHhzNu3DjGjRt3Q1pQ\nitKWqNBQropOp2P//v389ttv7N27l+LiulFSnTt3pl+/fvTr14/u3bvj6enZYnWsrq4mMTGRo0eP\nEh0dzYkTJ9BqtVhaWtKjRw+GDBnC2LFj1cQ8RWkCFRrKNTMYDMTHx3P48GGOHj3K8ePH0enq9g73\n9vamS5cuhISE0L59e4KCgggICMDNze269BVIKamoqCArK4vMzEwyMzM5ffo0SUlJpKWlYTTWbSsb\nEhJCnz59iIiIoH///qpFoShXSYWGct3pdDoSEhKIj48nISGBhIQEMjIyMBgMF8pYWlri6emJh4cH\nLi4uODs74+joiKOjI9bW1mg0GiwtLTEYDOj1dUNuq6qqKC8vp7y8nNLSUgoLCyksLKS6uvp3r+/j\n40NoaCihoaF07dqV8PDwm/q2maK0Jio0lGah1+vJzs4mPT2drKwsCgoKKCgoID8/n7KyMsrLy6mo\nqKCiooLaKwy7tbCwwMHBAScnJxwdHXFxccHDwwMPDw88PT3x9/cnMDCQgIAA1YpQlBvI3NBoncNj\nlJuGRqMhKCiIoKDGZ2VLKdHr9RgMBjQaDRYWFmrYq6K0Mio0lGYjhMDKygorq+u7UZOiKM1HfcxT\n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\nB4Zpnck22NlhAAAAAElFTkSuQmCC\n",
606 | "text/plain": [
607 | ""
608 | ]
609 | },
610 | "metadata": {},
611 | "output_type": "display_data"
612 | }
613 | ],
614 | "source": [
615 | "# and visualise the samples\n",
616 | "import seaborn as sns\n",
617 | "\n",
618 | "%matplotlib inline\n",
619 | "\n",
620 | "sns.kdeplot(\n",
621 | " data=ds.x2,\n",
622 | " data2=ds.x3,\n",
623 | ")"
624 | ]
625 | },
626 | {
627 | "cell_type": "markdown",
628 | "metadata": {},
629 | "source": [
630 | "And to access the implied CGM\""
631 | ]
632 | },
633 | {
634 | "cell_type": "code",
635 | "execution_count": 15,
636 | "metadata": {},
637 | "outputs": [
638 | {
639 | "data": {
640 | "image/svg+xml": [
641 | "\n",
642 | "\n",
644 | "\n",
646 | "\n",
647 | "\n"
679 | ],
680 | "text/plain": [
681 | ""
682 | ]
683 | },
684 | "execution_count": 15,
685 | "metadata": {},
686 | "output_type": "execute_result"
687 | }
688 | ],
689 | "source": [
690 | "scm.cgm.draw()"
691 | ]
692 | },
693 | {
694 | "cell_type": "markdown",
695 | "metadata": {},
696 | "source": [
697 | "And to apply an intervention:"
698 | ]
699 | },
700 | {
701 | "cell_type": "code",
702 | "execution_count": 16,
703 | "metadata": {},
704 | "outputs": [
705 | {
706 | "data": {
707 | "image/svg+xml": [
708 | "\n",
709 | "\n",
711 | "\n",
713 | "\n",
714 | "\n"
747 | ],
748 | "text/plain": [
749 | ""
750 | ]
751 | },
752 | "execution_count": 16,
753 | "metadata": {},
754 | "output_type": "execute_result"
755 | }
756 | ],
757 | "source": [
758 | "scm_do = scm.do(\"x1\")\n",
759 | "\n",
760 | "scm_do.cgm.draw()"
761 | ]
762 | },
763 | {
764 | "cell_type": "markdown",
765 | "metadata": {},
766 | "source": [
767 | "And sample from the distribution implied by this intervention:"
768 | ]
769 | },
770 | {
771 | "cell_type": "code",
772 | "execution_count": 17,
773 | "metadata": {},
774 | "outputs": [
775 | {
776 | "data": {
777 | "text/html": [
778 | "\n",
779 | "\n",
792 | "
\n",
793 | " \n",
794 | " \n",
795 | " | \n",
796 | " x1 | \n",
797 | " x2 | \n",
798 | " x3 | \n",
799 | "
\n",
800 | " \n",
801 | " \n",
802 | " \n",
803 | " | 0 | \n",
804 | " 0 | \n",
805 | " 0.258736 | \n",
806 | " 0.066944 | \n",
807 | "
\n",
808 | " \n",
809 | " | 1 | \n",
810 | " 1 | \n",
811 | " 1.142529 | \n",
812 | " 1.305371 | \n",
813 | "
\n",
814 | " \n",
815 | " | 2 | \n",
816 | " 2 | \n",
817 | " 1.927665 | \n",
818 | " 3.715894 | \n",
819 | "
\n",
820 | " \n",
821 | " | 3 | \n",
822 | " 3 | \n",
823 | " 2.878977 | \n",
824 | " 8.288507 | \n",
825 | "
\n",
826 | " \n",
827 | " | 4 | \n",
828 | " 4 | \n",
829 | " 4.098250 | \n",
830 | " 16.795653 | \n",
831 | "
\n",
832 | " \n",
833 | "
\n",
834 | "
"
835 | ],
836 | "text/plain": [
837 | " x1 x2 x3\n",
838 | "0 0 0.258736 0.066944\n",
839 | "1 1 1.142529 1.305371\n",
840 | "2 2 1.927665 3.715894\n",
841 | "3 3 2.878977 8.288507\n",
842 | "4 4 4.098250 16.795653"
843 | ]
844 | },
845 | "execution_count": 17,
846 | "metadata": {},
847 | "output_type": "execute_result"
848 | }
849 | ],
850 | "source": [
851 | "scm_do.sample(n_samples=5, set_values={\"x1\": np.arange(5)})"
852 | ]
853 | }
854 | ],
855 | "metadata": {
856 | "kernelspec": {
857 | "display_name": "Python 3",
858 | "language": "python",
859 | "name": "python3"
860 | },
861 | "language_info": {
862 | "codemirror_mode": {
863 | "name": "ipython",
864 | "version": 3
865 | },
866 | "file_extension": ".py",
867 | "mimetype": "text/x-python",
868 | "name": "python",
869 | "nbconvert_exporter": "python",
870 | "pygments_lexer": "ipython3",
871 | "version": "3.6.3"
872 | }
873 | },
874 | "nbformat": 4,
875 | "nbformat_minor": 2
876 | }
877 |
--------------------------------------------------------------------------------