diff --git a/brainpy/base/collector.py b/brainpy/base/collector.py index f86ba372a..eb2ccbcbd 100644 --- a/brainpy/base/collector.py +++ b/brainpy/base/collector.py @@ -71,7 +71,7 @@ def __sub__(self, other): if not isinstance(other, dict): raise ValueError(f'Only support dict, but we got {type(other)}.') gather = type(self)() - for key, val in self.values(): + for key, val in self.items(): if key in other: if id(val) != id(other[key]): raise ValueError(f'Cannot remove {key}, because we got two different values: ' @@ -170,38 +170,3 @@ def dict(self): def data(self): """Get all data in each value.""" return [x.value for x in self.values()] - - # @contextmanager - # def replicate(self): - # """A context manager to use in a with statement that replicates - # the variables in this collection to multiple devices. - # - # Important: replicating also updates the random state in order - # to have a new one per device. - # """ - # global math - # if math is None: from brainpy import math - # - # replicated, saved_states = {}, {} - # x = jnp.zeros((jax.local_device_count(), 1), dtype=math.float_) - # sharded_x = jax.pmap(lambda x: x, axis_name='device')(x) - # devices = [b.device() for b in sharded_x.device_buffers] - # num_device = len(devices) - # for k, d in self.items(): - # if isinstance(d, math.random.RandomState): - # replicated[k] = jax.device_put_sharded([shard for shard in d.split(num_device)], devices) - # saved_states[k] = d.value - # else: - # replicated[k] = jax.device_put_replicated(d.value, devices) - # self.assign(replicated) - # yield - # visited = set() - # for k, d in self.items(): - # # Careful not to reduce twice in case of - # # a variable and a reference to it. - # if id(d) not in visited: - # if isinstance(d, math.random.RandomState): - # d.value = saved_states[k] - # else: - # d.value = reduce_func(d) - # visited.add(id(d)) diff --git a/brainpy/dyn/base.py b/brainpy/dyn/base.py index ff94899c8..333d6bd0c 100644 --- a/brainpy/dyn/base.py +++ b/brainpy/dyn/base.py @@ -2,7 +2,7 @@ import math as pm import warnings -from typing import Union, Dict, Callable, Sequence +from typing import Union, Dict, Callable, Sequence, List, Optional import jax.numpy as jnp import numpy as np @@ -55,12 +55,13 @@ class DynamicalSystem(Base): """Global delay variables. Useful when the same target variable is used in multiple mappings.""" global_delay_vars: Dict[str, bm.LengthDelay] = Collector() + global_delay_targets: Dict[str, bm.Variable] = Collector() def __init__(self, name=None): super(DynamicalSystem, self).__init__(name=name) # local delay variables - self.local_delay_vars: Dict[str, bm.LengthDelay] = Collector() + self.local_delay_vars: List[str] = [] def __repr__(self): return f'{self.__class__.__name__}(name={self.name})' @@ -99,10 +100,9 @@ def __call__(self, *args, **kwargs): def register_delay( self, name: str, - delay_step: Union[int, Tensor, Callable, Initializer], - delay_target: Union[bm.JaxArray, jnp.ndarray], + delay_step: Optional[Union[int, Tensor, Callable, Initializer]], + delay_target: bm.Variable, initial_delay_data: Union[Initializer, Callable, Tensor, float, int, bool] = None, - domain: str = 'global' ): """Register delay variable. @@ -110,14 +110,12 @@ def register_delay( ---------- name: str The delay variable name. - delay_step: int, JaxArray, ndarray, callable, Initializer + delay_step: Optional, int, JaxArray, ndarray, callable, Initializer The number of the steps of the delay. - delay_target: JaxArray, ndarray, Variable - The target for delay. + delay_target: Variable + The target variable for delay. initial_delay_data: float, int, JaxArray, ndarray, callable, Initializer The initializer for the delay data. - domain: str - The domain of the delay data to store. Returns ------- @@ -130,8 +128,11 @@ def register_delay( elif isinstance(delay_step, int): delay_type = 'homo' elif isinstance(delay_step, (bm.ndarray, jnp.ndarray, np.ndarray)): - delay_type = 'heter' - delay_step = bm.asarray(delay_step) + if delay_step.size == 1 and delay_step.ndim == 0: + delay_type = 'homo' + else: + delay_type = 'heter' + delay_step = bm.asarray(delay_step) elif callable(delay_step): delay_step = init_param(delay_step, delay_target.shape, allow_none=False) delay_type = 'heter' @@ -145,33 +146,29 @@ def register_delay( 'then provide us the number of delay steps.') if delay_target.shape[0] != delay_step.shape[0]: raise ValueError(f'Shape is mismatched: {delay_target.shape[0]} != {delay_step.shape[0]}') - max_delay_step = int(bm.max(delay_step)) + if delay_type != 'none': + max_delay_step = int(bm.max(delay_step)) - # delay domain - if domain not in ['global', 'local']: - raise ValueError('"domain" must be a string in ["global", "local"]. ' - f'Bug we got {domain}.') + # delay target + if not isinstance(delay_target, bm.Variable): + raise ValueError(f'"delay_target" must be an instance of Variable, but we got {type(delay_target)}') # delay variable - if domain == 'local': - self.local_delay_vars[name] = bm.LengthDelay(delay_target, max_delay_step, initial_delay_data) - self.register_implicit_nodes(self.local_delay_vars) - else: + self.global_delay_targets[name] = delay_target + if delay_type != 'none': if name not in self.global_delay_vars: self.global_delay_vars[name] = bm.LengthDelay(delay_target, max_delay_step, initial_delay_data) - # save into local delay vars when first seen "var", - # for later update current value! - self.local_delay_vars[name] = self.global_delay_vars[name] + self.local_delay_vars.append(name) else: if self.global_delay_vars[name].num_delay_step - 1 < max_delay_step: self.global_delay_vars[name].reset(delay_target, max_delay_step, initial_delay_data) - self.register_implicit_nodes(self.global_delay_vars) + self.register_implicit_nodes(self.global_delay_vars) return delay_step def get_delay_data( self, name: str, - delay_step: Union[int, bm.JaxArray, jnp.DeviceArray], + delay_step: Optional[Union[int, bm.JaxArray, jnp.DeviceArray]], *indices: Union[int, bm.JaxArray, jnp.DeviceArray], ): """Get delay data according to the provided delay steps. @@ -180,7 +177,7 @@ def get_delay_data( ---------- name: str The delay variable name. - delay_step: int, JaxArray, ndarray + delay_step: Optional, int, JaxArray, ndarray The delay length. indices: optional, int, JaxArray, ndarray The indices of the delay. @@ -190,6 +187,9 @@ def get_delay_data( delay_data: JaxArray, ndarray The delay data at the given time. """ + if delay_step is None: + return self.global_delay_targets[name] + if name in self.global_delay_vars: if isinstance(delay_step, int): return self.global_delay_vars[name](delay_step, *indices) @@ -197,6 +197,7 @@ def get_delay_data( if len(indices) == 0: indices = (jnp.arange(delay_step.size), ) return self.global_delay_vars[name](delay_step, *indices) + elif name in self.local_delay_vars: if isinstance(delay_step, int): return self.local_delay_vars[name](delay_step) @@ -204,40 +205,9 @@ def get_delay_data( if len(indices) == 0: indices = (jnp.arange(delay_step.size), ) return self.local_delay_vars[name](delay_step, *indices) - else: - raise ValueError(f'{name} is not defined in delay variables.') - - def update_delay( - self, - name: str, - delay_data: Union[float, bm.JaxArray, jnp.ndarray] - ): - """Update the delay according to the delay data. - - Parameters - ---------- - name: str - The name of the delay. - delay_data: float, JaxArray, ndarray - The delay data to update at the current time. - """ - if name in self.local_delay_vars: - return self.local_delay_vars[name].update(delay_data) - else: - if name not in self.global_delay_vars: - raise ValueError(f'{name} is not defined in delay variables.') - def reset_delay( - self, - name: str, - delay_target: Union[bm.JaxArray, jnp.DeviceArray] - ): - """Reset the delay variable.""" - if name in self.local_delay_vars: - return self.local_delay_vars[name].reset(delay_target) else: - if name not in self.global_delay_vars: - raise ValueError(f'{name} is not defined in delay variables.') + raise ValueError(f'{name} is not defined in delay variables.') def update(self, t, dt): """The function to specify the updating rule. @@ -297,7 +267,7 @@ def __repr__(self): return f'{cls_name}({split.join(children)})' def update(self, t, dt): - """Step function of a network. + """Update function of a container. In this update function, the update functions in children systems are iteratively called. @@ -321,16 +291,6 @@ def __getattr__(self, item): else: return super(Container, self).__getattribute__(item) - def reset(self): - nodes = self.nodes(level=1, include_self=False).subset(DynamicalSystem).unique() - neuron_groups = nodes.subset(NeuGroup) - synapse_groups = nodes.subset(TwoEndConn) - for node in neuron_groups.values(): - node.reset() - for node in synapse_groups.values(): - node.reset() - for node in (nodes - neuron_groups - synapse_groups).values(): - node.reset() @classmethod def has(cls, **children_cls): @@ -370,6 +330,59 @@ class Network(Container): def __init__(self, *ds_tuple, name=None, **ds_dict): super(Network, self).__init__(*ds_tuple, name=name, **ds_dict) + def update(self, t, dt): + """Step function of a network. + + In this update function, the update functions in children systems are + iteratively called. + """ + nodes = self.nodes(level=1, include_self=False) + nodes = nodes.subset(DynamicalSystem) + nodes = nodes.unique() + neuron_groups = nodes.subset(NeuGroup) + synapse_groups = nodes.subset(TwoEndConn) + other_nodes = nodes - neuron_groups - synapse_groups + + # reset synapse nodes + for node in synapse_groups.values(): + node.update(t, dt) + + # reset neuron nodes + for node in neuron_groups.values(): + node.update(t, dt) + + # reset other types of nodes + for node in other_nodes.values(): + node.update(t, dt) + + # reset delays + for node in nodes.values(): + for name in node.local_delay_vars: + self.global_delay_vars[name].update(self.global_delay_targets[name].value) + + def reset(self): + nodes = self.nodes(level=1, include_self=False).subset(DynamicalSystem).unique() + neuron_groups = nodes.subset(NeuGroup) + synapse_groups = nodes.subset(TwoEndConn) + + # reset neuron nodes + for node in neuron_groups.values(): + node.reset() + + # reset synapse nodes + for node in synapse_groups.values(): + node.reset() + + # reset other types of nodes + for node in (nodes - neuron_groups - synapse_groups).values(): + node.reset() + + # reset delays + for node in nodes: + for name in node.local_delay_vars: + self.global_delay_vars[name].reset(self.global_delay_targets[name]) + + class ConstantDelay(DynamicalSystem): """Class used to model constant delay variables. @@ -436,7 +449,7 @@ def __init__(self, size, delay, dtype=None, dt=None, **kwargs): f"be the same with the delay data size. But " f"we got {delay.shape[0]} != {self.size[0]}") delay = bm.around(delay / self.dt) - self.diag = bm.array(bm.arange(self.num), dtype=bm.int_) + self.diag = bm.array(bm.arange(self.num)) self.num_step = bm.array(delay, dtype=bm.uint32) + 1 self.in_idx = bm.Variable(self.num_step - 1) self.out_idx = bm.Variable(bm.zeros(self.num, dtype=bm.uint32)) diff --git a/brainpy/dyn/rates/couplings.py b/brainpy/dyn/rates/couplings.py index 10758b612..400848155 100644 --- a/brainpy/dyn/rates/couplings.py +++ b/brainpy/dyn/rates/couplings.py @@ -72,11 +72,10 @@ def __init__( if delay_steps is None: self.delay_steps = None self.delay_type = 'none' - num_delay_step = 0 + num_delay_step = None elif isinstance(delay_steps, int): self.delay_steps = delay_steps num_delay_step = delay_steps - check_integer(delay_steps, 'delay_steps', min_bound=0, allow_none=False) self.delay_type = 'int' elif callable(delay_steps): delay_steps = delay_steps(required_shape) @@ -84,7 +83,7 @@ def __init__( raise ValueError(f'"delay_steps" must be integer typed. But we got {delay_steps.dtype}') self.delay_steps = delay_steps self.delay_type = 'array' - num_delay_step = int(self.delay_steps.max()) + num_delay_step = self.delay_steps.max() elif isinstance(delay_steps, (bm.JaxArray, jnp.ndarray)): if delay_steps.dtype not in [bm.int32, bm.int64, bm.uint32, bm.uint64]: raise ValueError(f'"delay_steps" must be integer typed. But we got {delay_steps.dtype}') @@ -93,20 +92,18 @@ def __init__( f'While we got {delay_steps.shape}.') self.delay_steps = delay_steps self.delay_type = 'array' - num_delay_step = int(self.delay_steps.max()) + num_delay_step = self.delay_steps.max() else: raise ValueError(f'Unknown type of delay steps: {type(delay_steps)}') # delay variables - if self.delay_type != 'none': - self.register_delay(f'delay_{id(delay_var)}', - delay_step=num_delay_step, - delay_target=delay_var, - initial_delay_data=initial_delay_data) + self.delay_step = self.register_delay(f'delay_{id(delay_var)}', + delay_step=num_delay_step, + delay_target=delay_var, + initial_delay_data=initial_delay_data) def reset(self): - if self.delay_steps is not None: - self.reset_delay(f'delay_{id(self.delay_var)}', self.delay_var) + pass class DiffusiveCoupling(DelayCoupling): @@ -184,20 +181,18 @@ def __init__( self.coupling_var2 = coupling_var2 def update(self, t, dt): - # delay variable - if self.delay_type != 'none': - delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] - # delays - if self.delay_type == 'none': + if self.delay_steps is None: diffusive = bm.expand_dims(self.coupling_var1, axis=1) - self.coupling_var2 diffusive = (self.conn_mat * diffusive).sum(axis=0) elif self.delay_type == 'array': + delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] f = vmap(lambda i: delay_var(self.delay_steps[i], bm.arange(self.coupling_var1.size))) # (pre.num,) delays = f(bm.arange(self.coupling_var2.size).value) diffusive = delays.T - self.coupling_var2 # (post.num, pre.num) diffusive = (self.conn_mat * diffusive).sum(axis=0) elif self.delay_type == 'int': + delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] delayed_var = delay_var(self.delay_steps) diffusive = bm.expand_dims(delayed_var, axis=1) - self.coupling_var2 diffusive = (self.conn_mat * diffusive).sum(axis=0) @@ -208,10 +203,6 @@ def update(self, t, dt): for target in self.output_var: target.value += diffusive - # update - if self.delay_type != 'none': - delay_var.update(self.delay_var) - class AdditiveCoupling(DelayCoupling): """Additive coupling. @@ -266,20 +257,21 @@ def __init__( self.coupling_var = coupling_var def update(self, t, dt): - # delay variable - delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] - # delay function if self.delay_steps is None: additive = self.coupling_var @ self.conn_mat - else: + elif self.delay_type == 'array': + delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] f = vmap(lambda i: delay_var(self.delay_steps[i], bm.arange(self.coupling_var.size))) # (pre.num,) delays = f(bm.arange(self.coupling_var.size).value) # (post.num, pre.num) additive = (self.conn_mat * delays.T).sum(axis=0) + elif self.delay_type == 'int': + delay_var: bm.LengthDelay = self.global_delay_vars[f'delay_{id(self.delay_var)}'] + delayed_var = delay_var(self.delay_steps) + additive = (self.conn_mat * delayed_var).sum(axis=0) + else: + raise ValueError # output to target variable for target in self.output_var: target.value += additive - - # update - delay_var.update(self.delay_var) diff --git a/brainpy/dyn/synapses/abstract_models.py b/brainpy/dyn/synapses/abstract_models.py index aeb394244..d8d69652b 100644 --- a/brainpy/dyn/synapses/abstract_models.py +++ b/brainpy/dyn/synapses/abstract_models.py @@ -138,16 +138,11 @@ def __init__( delay_target=self.pre.spike) def reset(self): - if self.delay_step is not None: - self.reset_delay(f"{self.pre.name}.spike", self.pre.spike) + pass def update(self, t, dt): # delays - if self.delay_step is None: - pre_spike = self.pre.spike - else: - pre_spike = self.get_delay_data(f"{self.pre.name}.spike", delay_step=self.delay_step) - self.update_delay(f"{self.pre.name}.spike", delay_data=self.pre.spike) + pre_spike = self.get_delay_data(f"{self.pre.name}.spike", delay_step=self.delay_step) # post values assert self.weight_type in ['homo', 'heter'] @@ -334,23 +329,19 @@ def __init__( # variables self.g = bm.Variable(bm.zeros(self.post.num)) - self.delay_step = self.register_delay(f"{self.pre.name}.spike", delay_step, self.pre.spike) + self.delay_step = self.register_delay(f"{self.pre.name}.spike", + delay_step, + self.pre.spike) # function self.integral = odeint(lambda g, t: -g / self.tau, method=method) def reset(self): self.g.value = bm.zeros(self.post.num) - if self.delay_step is not None: - self.reset_delay(f"{self.pre.name}.spike", self.pre.spike) def update(self, t, dt): # delays - if self.delay_step is None: - pre_spike = self.pre.spike - else: - pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) - self.update_delay(f"{self.pre.name}.spike", self.pre.spike) + pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) # post values assert self.weight_type in ['homo', 'heter'] @@ -667,8 +658,6 @@ def __init__( def reset(self): self.h.value = bm.zeros(self.pre.num) self.g.value = bm.zeros(self.pre.num) - if self.delay_step is not None: - self.reset_delay(f"{self.pre.name}.spike", self.pre.spike) def dh(self, h, t): return -h / self.tau_rise @@ -678,11 +667,7 @@ def dg(self, g, t, h): def update(self, t, dt): # delays - if self.delay_step is None: - pre_spike = self.pre.spike - else: - pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) - self.update_delay(f"{self.pre.name}.spike", self.pre.spike) + pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) # update synaptic variables self.g.value, self.h.value = self.integral(self.g, self.h, t, dt) @@ -1258,11 +1243,7 @@ def dx(self, x, t): def update(self, t, dt): # delays - if self.delay_step is None: - delayed_pre_spike = self.pre.spike - else: - delayed_pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) - self.update_delay(f"{self.pre.name}.spike", self.pre.spike) + delayed_pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) # update synapse variables self.g.value, self.x.value = self.integral(self.g, self.x, t, dt=dt) diff --git a/brainpy/dyn/synapses/biological_models.py b/brainpy/dyn/synapses/biological_models.py index 0d8500a56..7babd50ea 100644 --- a/brainpy/dyn/synapses/biological_models.py +++ b/brainpy/dyn/synapses/biological_models.py @@ -200,8 +200,6 @@ def __init__( def reset(self): self.g.value = bm.zeros(self.pre.num) - if self.delay_step is not None: - self.reset_delay(f"{self.pre.name}.spike", self.pre.spike) def dg(self, g, t, TT): dg = self.alpha * TT * (1 - g) - self.beta * g @@ -209,11 +207,7 @@ def dg(self, g, t, TT): def update(self, t, dt): # delays - if self.delay_step is None: - pre_spike = self.pre.spike - else: - pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) - self.update_delay(f"{self.pre.name}.spike", self.pre.spike) + pre_spike = self.get_delay_data(f"{self.pre.name}.spike", self.delay_step) # spike arrival time self.spike_arrival_time.value = bm.where(pre_spike, t, self.spike_arrival_time) diff --git a/brainpy/integrators/runner.py b/brainpy/integrators/runner.py index 2f68c1971..efe73ba9a 100644 --- a/brainpy/integrators/runner.py +++ b/brainpy/integrators/runner.py @@ -80,32 +80,6 @@ class IntegratorRunner(Runner): >>> ax.set_xlabel('z') >>> plt.show() - Example to run an DDE integrator, - - .. plot:: - :include-source: True - - >>> import brainpy as bp - >>> import brainpy.math as bm - >>> import matplotlib.pyplot as plt - >>> - >>> # Mackey-Glass equation - >>> dt = 0.01; beta=2.; gamma=1.; tau=2.; n=9.65 - >>> mg_eq = lambda x, t, xdelay: (beta * xdelay(t - tau) / (1 + xdelay(t - tau) ** n) - >>> - gamma * x) - >>> xdelay = bm.TimeDelay(bm.asarray([1.2]), delay_len=tau, dt=dt, before_t0=lambda t: 1.2) - >>> integral = bp.ddeint(mg_eq, method='rk4', state_delays={'x': xdelay}) - >>> runner = bp.integrators.IntegratorRunner( - >>> integral, - >>> monitors=['x', ], - >>> fun_monitors={'x(tau)': (lambda t, _: xdelay(t - tau))}, - >>> inits=[1.2], # initialize all variable to 1. - >>> args={'xdelay': xdelay}, dt=dt, - >>> ) - >>> runner.run(100.) - >>> plt.plot(runner.mon['x'].flatten(), runner.mon['x(tau)'].flatten()) - >>> plt.show() - """ def __init__( @@ -221,7 +195,7 @@ def __init__( self.variables[k][:] = inits[k] self.dyn_vars.update(self.variables) if len(self._dyn_args) > 0: - self.idx = bm.Variable(bm.zeros(1, dtype=bm.int_)) + self.idx = bm.Variable(bm.zeros(1, dtype=jnp.int_)) self.dyn_vars['_idx'] = self.idx # build the update step diff --git a/brainpy/math/delayvars.py b/brainpy/math/delayvars.py index 70166868e..29939c594 100644 --- a/brainpy/math/delayvars.py +++ b/brainpy/math/delayvars.py @@ -82,15 +82,15 @@ class TimeDelay(AbstractDelay): The time precesion. before_t0: callable, bm.ndarray, jnp.ndarray, float, int The delay data before ::math`t_0`. - - when `before_t0` is a function, it should receive an time argument `t` + - when `before_t0` is a function, it should receive a time argument `t` - when `before_to` is a tensor, it should be a tensor with shape - of :math:`(num_delay, ...)`, where the longest delay data is aranged in + of :math:`(num\_delay, ...)`, where the longest delay data is aranged in the first index. name: str The delay instance name. interp_method: str The way to deal with the delay at the time which is not integer times of the time step. - For exameple, if the time step ``dt=0.1``, the time delay length ``delay_len=1.``, + For exameple, if the time step ``dt=0.1``, the time delay length ``delay\_len=1.``, when users require the delay data at ``t-0.53``, we can deal this situation with the following methods: @@ -311,6 +311,8 @@ def reset( # delay_len check_integer(delay_len, 'delay_len', allow_none=True, min_bound=0) if delay_len is None: + if self.num_delay_step is None: + raise ValueError('"delay_len" cannot be None.') delay_len = self.num_delay_step - 1 self.num_delay_step = delay_len + 1 diff --git a/brainpy/math/jit.py b/brainpy/math/jit.py index 3242fda07..9e22d7dd0 100644 --- a/brainpy/math/jit.py +++ b/brainpy/math/jit.py @@ -185,8 +185,8 @@ def jit(func, dyn_vars=None, static_argnames=None, device=None, auto_infer=True) Returns ------- - func : Any - A wrapped version of Base object or function, set up for just-in-time compilation. + func : callable + A callable jitted function, set up for just-in-time compilation. """ if callable(func): if dyn_vars is not None: diff --git a/brainpy/math/operators.py b/brainpy/math/operators.py index 13dc14b4a..d624b917b 100644 --- a/brainpy/math/operators.py +++ b/brainpy/math/operators.py @@ -11,7 +11,6 @@ from brainpy.math import setting from brainpy.math.jaxarray import JaxArray from brainpy.math.numpy_ops import as_device_array, _remove_jaxarray -from brainpy.types import Shape try: import brainpylib diff --git a/brainpy/math/random.py b/brainpy/math/random.py index 49a462925..dd1b3dc70 100644 --- a/brainpy/math/random.py +++ b/brainpy/math/random.py @@ -653,7 +653,7 @@ def _check_p(self, p): def bernoulli(self, p, size=None): p = _check_py_seq(_remove_jax_array(p)) - check_error_in_jit(jnp.any(jnp.logical_and(p < 0, p > 1)), self._check_p, p) + check_error_in_jit(jnp.any(jnp.logical_and(p < 0, p > 1)), self._check_p, p) if size is None: size = jnp.shape(p) return JaxArray(jr.bernoulli(self.split_key(), p=p, shape=_size2shape(size))) @@ -838,8 +838,7 @@ def wald(self, mean, scale, size=None): sampled_chi2 = jnp.square(self.randn(*size).value) sampled_uniform = self.uniform(size=size).value # Wikipedia defines an intermediate x with the formula - # x = loc + loc ** 2 * y / (2 * conc) - # - loc / (2 * conc) * sqrt(4 * loc * conc * y + loc ** 2 * y ** 2) + # x = loc + loc ** 2 * y / (2 * conc) - loc / (2 * conc) * sqrt(4 * loc * conc * y + loc ** 2 * y ** 2) # where y ~ N(0, 1)**2 (sampled_chi2 above) and conc is the concentration. # Let us write # w = loc * y / (2 * conc) @@ -981,14 +980,14 @@ def loggamma(self, a, size=None): a = _check_py_seq(_remove_jax_array(a)) if size is None: size = jnp.shape(a) - return JaxArray(jr.loggamma(self.split_key(), a, shape=size)) + return JaxArray(jr.loggamma(self.split_key(), a, shape=_size2shape(size))) - def categorical(self, logits, axis:int= -1, size=None): + def categorical(self, logits, axis: int = -1, size=None): logits = _check_py_seq(_remove_jax_array(logits)) if size is None: size = list(jnp.shape(logits)) size.pop(axis) - return JaxArray(jr.categorical(self.split_key(), logits, axis=axis, shape=size)) + return JaxArray(jr.categorical(self.split_key(), logits, axis=axis, shape=_size2shape(size))) # alias @@ -1366,5 +1365,5 @@ def loggamma(a, size=None): @wraps(jr.categorical) -def categorical(logits, axis:int= -1, size=None): +def categorical(logits, axis: int = -1, size=None): return DEFAULT.categorical(logits, axis, size) diff --git a/brainpy/math/setting.py b/brainpy/math/setting.py index 9a6427673..9c896005d 100644 --- a/brainpy/math/setting.py +++ b/brainpy/math/setting.py @@ -3,6 +3,7 @@ import os import re +from jax import dtypes import jax.config import jax.numpy as jnp @@ -46,7 +47,11 @@ def set_dt(dt): dt : float Numerical integration precision. """ - assert isinstance(dt, float), f'"dt" must a float, but we got {dt}' + _dt = jnp.asarray(dt) + if not dtypes.issubdtype(_dt.dtype, jnp.floating): + raise ValueError(f'"dt" must a float, but we got {dt}') + if _dt.ndim != 0: + raise ValueError(f'"dt" must be a scalar, but we got {dt}') global __dt __dt = dt diff --git a/brainpy/nn/nodes/RC/nvar.py b/brainpy/nn/nodes/RC/nvar.py index f05e93887..5a745841e 100644 --- a/brainpy/nn/nodes/RC/nvar.py +++ b/brainpy/nn/nodes/RC/nvar.py @@ -100,7 +100,7 @@ def __init__( self.nonlinear_dim = None # delay variables - self.idx = bm.Variable(jnp.asarray([0], dtype=bm.int_)) + self.idx = bm.Variable(jnp.asarray([0])) self.store = None def init_ff_conn(self): diff --git a/brainpy/tools/checking.py b/brainpy/tools/checking.py index 0d47feb1a..4d67c075f 100644 --- a/brainpy/tools/checking.py +++ b/brainpy/tools/checking.py @@ -3,6 +3,7 @@ from typing import Union, Sequence, Dict, Callable, Tuple, Type import jax.numpy as jnp +import numpy as np import numpy as onp import brainpy.connect as conn @@ -292,7 +293,10 @@ def check_integer(value: int, name=None, min_bound=None, max_bound=None, allow_n else: raise ValueError(f'{name} must be an int, but got None') if not isinstance(value, int): - if hasattr(value, 'dtype') and not jnp.issubdtype(value.dtype, jnp.integer): + if isinstance(value, (jnp.ndarray, np.ndarray)): + if not (jnp.issubdtype(value.dtype, jnp.integer) and value.ndim == 0 and value.size == 1): + raise ValueError(f'{name} must be an int, but got {value}') + else: raise ValueError(f'{name} must be an int, but got {value}') if min_bound is not None: if jnp.any(value < min_bound): diff --git a/docs/auto_generater.py b/docs/auto_generater.py index 4ad8ea76c..8a77051b7 100644 --- a/docs/auto_generater.py +++ b/docs/auto_generater.py @@ -5,14 +5,11 @@ import os from brainpy.math import (activations, autograd, controls, function, - jit, operators, parallels, setting, delayvars, + jit, parallels, setting, delayvars, compat) -block_list = ['test', 'register_pytree_node'] -for module in [jit, autograd, function, - controls, activations, - parallels, setting, - delayvars, compat]: +block_list = ['test', 'register_pytree_node', 'call', 'namedtuple', 'jit', 'wraps', 'index', 'function'] +for module in [jit, autograd, function, controls, activations, parallels, setting, delayvars, compat]: for k in dir(module): if (not k.startswith('_')) and (not inspect.ismodule(getattr(module, k))): block_list.append(k) diff --git a/docs/tutorial_math/compilation.ipynb b/docs/tutorial_math/compilation.ipynb index 918457e04..758a2895f 100644 --- a/docs/tutorial_math/compilation.ipynb +++ b/docs/tutorial_math/compilation.ipynb @@ -9,7 +9,7 @@ } }, "source": [ - "# Compilation" + "# JIT Compilation for Class Objects" ] }, { diff --git a/docs/tutorial_math/differentiation.ipynb b/docs/tutorial_math/differentiation.ipynb index 434e0bd77..820caaa3d 100644 --- a/docs/tutorial_math/differentiation.ipynb +++ b/docs/tutorial_math/differentiation.ipynb @@ -9,7 +9,7 @@ } }, "source": [ - "# Differentiation" + "# Autograd for Class Variables" ] }, { diff --git a/docs/tutorial_toolbox/illustration_joint_equations.py b/docs/tutorial_toolbox/illustration_joint_equations.py index 6df788567..f73f6cebb 100644 --- a/docs/tutorial_toolbox/illustration_joint_equations.py +++ b/docs/tutorial_toolbox/illustration_joint_equations.py @@ -13,7 +13,7 @@ def du(self, u, t, V): @property def derivative(self): - return bp.JointEq([self.dV, self.du]) + return bp.JointEq(self.dV, self.du) def __init__(self, size): super().__init__(size) @@ -32,7 +32,7 @@ def update(self, t, dt): self.input[:] = 0. -class IzhiSeparate(bp.NeuGroup): +class IzhiSeparate(bp.dyn.NeuGroup): def dV(self, V, t, u, Iext): return 0.04 * V * V + 5 * V + 140 - u + Iext diff --git a/docs/tutorial_toolbox/joint_equations.ipynb b/docs/tutorial_toolbox/joint_equations.ipynb index f4532fe08..5f827a9ad 100644 --- a/docs/tutorial_toolbox/joint_equations.ipynb +++ b/docs/tutorial_toolbox/joint_equations.ipynb @@ -3,7 +3,11 @@ { "cell_type": "markdown", "id": "1df2a482", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "# Joint Differential Equations" ] @@ -11,7 +15,11 @@ { "cell_type": "markdown", "id": "109f9b4e", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "@[Xiaoyu Chen](mailto:c-xy17@tsinghua.org.cn)" ] @@ -19,7 +27,11 @@ { "cell_type": "markdown", "id": "c9df7780", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "In a [dynamical system](../tutorial_building/dynamical_systems.ipynb), there may be multiple variables that change dynamically over time. Sometimes these variables are interconnected, and updating one variable requires others as the input. For example, in the widely known Hodgkin–Huxley model, the variables $V$, $m$, $h$, and $n$ are updated synchronously and interdependently (please refer to [Building Neuron Models](../tutorial_building/neuron_models.ipynb)for details). To achieve higher integral accuracy, it is recommended to use ``brainpy.JointEq`` to jointly solving interconnected differential equations." ] @@ -28,7 +40,11 @@ "cell_type": "code", "execution_count": 11, "id": "be08d171", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "import brainpy as bp" @@ -37,7 +53,11 @@ { "cell_type": "markdown", "id": "991cf807", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## ``brainpy.JointEq``" ] @@ -45,7 +65,11 @@ { "cell_type": "markdown", "id": "05d270a9", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "``brainpy.JointEq`` is used to merge individual but interconnected differential equations into a single joint equation. For example, below are the two differential equations of the Izhikevich model:" ] @@ -54,7 +78,11 @@ "cell_type": "code", "execution_count": 12, "id": "2921b856", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "a, b = 0.02, 0.20\n", @@ -65,7 +93,11 @@ { "cell_type": "markdown", "id": "44527a26", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Where updating $V$ requires $u$ as the input, and updating $u$ requires $V$ as the input. The joint equation can be defined as:" ] @@ -74,16 +106,24 @@ "cell_type": "code", "execution_count": 13, "id": "08ac3b75", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ - "joint_eq = bp.JointEq(eqs=(dV, du))" + "joint_eq = bp.JointEq(dV, du)" ] }, { "cell_type": "markdown", "id": "7dcfcc88", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "``brainpy.JointEq`` receives only one argument named `eqs`, which can be a list or tuple containing multiple differential equations. Then it can be packed into a numarical integrator that solves the equation with a specified method, just as what can be done to any individual differential equation." ] @@ -92,7 +132,11 @@ "cell_type": "code", "execution_count": 14, "id": "356cf60d", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "itg = bp.odeint(joint_eq, method='rk2')" @@ -101,7 +145,11 @@ { "cell_type": "markdown", "id": "1145f933", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "There are several requirements for defining a joint equation:\n", "1. Every individual differential equation should follow the format of defining a [ODE](ode_numerical_solvers.ipynb) or [SDE](sde_numerical_solvers.ipynb) funtion in BrainPy. For example, the arguments before `t` denote the dynamical variables and arguments after `t` denote the parameters.\n", @@ -113,7 +161,11 @@ { "cell_type": "markdown", "id": "e1f7c666", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Why use `brainpy.JointEq`?" ] @@ -121,7 +173,11 @@ { "cell_type": "markdown", "id": "a3996db1", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Users may be confused with the function of `brainpy.JointEq`, because multiple differential equations can be written in a single function:" ] @@ -130,7 +186,11 @@ "cell_type": "code", "execution_count": 15, "id": "4dec7537", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "def diff(V, u, t, Iext):\n", @@ -144,7 +204,11 @@ { "cell_type": "markdown", "id": "5943bc7a", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "or simply packed into interators separately:" ] @@ -153,7 +217,11 @@ "cell_type": "code", "execution_count": 16, "id": "12e5d88d", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "int_V = bp.odeint(dV, method='rk2')\n", @@ -163,7 +231,11 @@ { "cell_type": "markdown", "id": "50963fe1", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "To illusrate the difference between joint and separate differential equations, let's dive into the differential codes of these two types of equations. \n", "\n", @@ -174,7 +246,11 @@ "cell_type": "code", "execution_count": 10, "id": "38101bec", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", @@ -211,7 +287,11 @@ { "cell_type": "markdown", "id": "8500a6c7", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "As is shown in the output code, the variable $V$ is integrated twice by the RK2 method. For the second differential value `dV_k2`, the updated value of $V$ (`k2_V_arg`) and original $u$ are used to calculate the differential value. This will generate a tiny error, since the values of $V$ and $u$ are taken at different times.\n", "\n", @@ -222,7 +302,11 @@ "cell_type": "code", "execution_count": 19, "id": "32901ae6", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", @@ -261,7 +345,11 @@ { "cell_type": "markdown", "id": "6a1a9669", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "It is shown in this output code that second differential values of $v$ and $u$ are calculated by using the updated values (`k2_V_arg` and `k2_u_arg`) at the same time. This will result in a more accurate integral." ] @@ -269,7 +357,11 @@ { "cell_type": "markdown", "id": "73051bec", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "The figure below compares the simulation results of the Izhikevich model using joint and separate differential equations ($dt = 0.2 ms$). It is shown that as the simulation time increases, the integral error becomes greater.\n", "\n", @@ -280,7 +372,11 @@ "cell_type": "code", "execution_count": null, "id": "3b3aeeb3", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [] } @@ -306,4 +402,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/tutorial_toolbox/monitors.ipynb b/docs/tutorial_toolbox/monitors.ipynb index 0104f5096..1ba542475 100644 --- a/docs/tutorial_toolbox/monitors.ipynb +++ b/docs/tutorial_toolbox/monitors.ipynb @@ -3,7 +3,11 @@ { "cell_type": "markdown", "id": "f753f3ab", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "# Monitors" ] @@ -11,7 +15,11 @@ { "cell_type": "markdown", "id": "904397dd", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "@[Chaoming Wang](https://github.com/chaoming0625)\n", "@[Xiaoyu Chen](mailto:c-xy17@tsinghua.org.cn)" @@ -20,7 +28,11 @@ { "cell_type": "markdown", "id": "7717e918", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "BrainPy has a [systematic naming system](../tutorial_math/base.ipynb). Any model in BrainPy have a unique name. Thus, nodes, integrators, and variables can be easily accessed in a huge network. Based on this naming system, BrainPy provides a set of convenient monitoring supports. In this section, we are going to talk about this. " ] @@ -29,7 +41,11 @@ "cell_type": "code", "execution_count": 1, "id": "19ba3a79", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "import brainpy as bp\n", @@ -39,21 +55,14 @@ "bp.math.set_dt(0.02)" ] }, - { - "cell_type": "code", - "execution_count": 2, - "id": "8cfa1c32", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, { "cell_type": "markdown", "id": "596a3c54", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Initializing Monitors in a Runner" ] @@ -61,7 +70,11 @@ { "cell_type": "markdown", "id": "1143dc69", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "In BrainPy, any instance of ``brainpy.Runner`` has a built-in monitor. Users can set up a monitor when initializing a runner. \n", "\n", @@ -70,9 +83,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "243db637", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "HH = bp.dyn.HH\n", @@ -82,33 +99,38 @@ { "cell_type": "markdown", "id": "cedf74d0", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "After defining a HH neuron, we can add monitors while setting up the runner. When specifying the `monitors` parameter, a monitor, which is an instance of ``brainpy.Monitor``, will be initialized. The first method to initialize a monitor is through a list/tuple of strings:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "db284f81", "metadata": { - "scrolled": true + "scrolled": true, + "pycharm": { + "name": "#%%\n" + } }, "outputs": [ { "data": { - "text/plain": [ - "brainpy.running.monitor.Monitor" - ] + "text/plain": "brainpy.running.monitor.Monitor" }, - "execution_count": 4, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# set up a monitor using a list of str\n", - "runner1 = bp.StructRunner(model, \n", + "runner1 = bp.dyn.DSRunner(model,\n", " monitors=['V', 'spike'], \n", " inputs=('input', 10))\n", "\n", @@ -118,25 +140,30 @@ { "cell_type": "markdown", "id": "44336645", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "where the string `'V'` and `'spike'` corresponds to the name of the variables in the HH model:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "74426fa9", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { - "text/plain": [ - "(Variable(DeviceArray([0.], dtype=float32)),\n", - " Variable(DeviceArray([False], dtype=bool)))" - ] + "text/plain": "(Variable([-68.89985], dtype=float32), Variable([False], dtype=bool))" }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -148,16 +175,24 @@ { "cell_type": "markdown", "id": "b42f65b5", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Besides using a list/tuple of strings, users can also directly use the ``Monitor`` class to initialize a monitor:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "85524b4f", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# set up a monitor using brainpy.Monitor\n", @@ -167,37 +202,41 @@ { "cell_type": "markdown", "id": "fff55d35", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Once we call the runner with a given time duration, the monitor will automatically record the variable evolutions in the corresponding models. Afterwards, users can access these variable trajectories by using ``.mon.[variable_name]``. The default history times ``.mon.ts`` will also be generated after the model finishes its running. Let's see an example. " ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "43451236", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { + "text/plain": " 0%| | 0/5000 [00:00" - ] + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAEGCAYAAACAd+UpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAABHIUlEQVR4nO29eZxcVZn//36qqrcsZA9LWBIg7DsJi8iIgiOIiuOMCg6yKqDMiH7HBdT5iv5ccBln1NGfIlvYRFCUuACy4wakExKSkJWQfevsnXR3dVXd8/3j1q26XX2reqm6Xc+9fT6vV17VtaTuuafO+ZznfJ7liDEGCwsLC4t4IlHvBlhYWFhYhAdL8hYWFhYxhiV5CwsLixjDkryFhYVFjGFJ3sLCwiLGSNW7AX5MnDjRTJ06td7NsLCwsIgU5s6du80YMynoPVUkP3XqVFpbW+vdDAsLC4tIQUTWlHvPyjUWFhYWMYYleQsLC4sYw5K8hYWFRYxhSd7CwsIixrAkb2FhYRFjWJK3sLCwiDEsyVtYWFjEGLEh+e6sw/0vraGzO1fvplhYWFioQWxI/k+vb+bLv13EvX9fXe+mWFhYWKhBbEg+k3MAeG397jq3xMLCwkIPYkPyjsvxtKez9W2IhYWFhSLEhuS7sq4W32FJ3sLCwqKA+JB8xjXl91nHq4WFhUUBsSH5dN6S32cteQsLC4sCakbyIpIUkVdF5Pf55+NF5CkRWZF/HFerawUhlzMA7LUkb2FhYVFALS35m4Alvuc3A88YY6YDz+SfhwbH5XhryVtYWFj4UBOSF5GDgYuBO3wvXwLMyv89C3h/La5VDo5xWT6ddcjmwyktLCwshjtqZcn/D/B5wM+u+xtjNgHkHyfX6FqBML6/96Wt89XCwsICakDyIvIeYKsxZu4g//91ItIqIq1tbW2DbocxRZpvT2cG/T0WFhYWcUItLPlzgPeJyGrgIeAdInI/sEVEDgTIP24N+s/GmNuNMTOMMTMmTQo8h7ZfcHwkb52vFhYWFi6qJnljzC3GmIONMVOBS4FnjTGXA7OBK/MfuxJ4rNprVYLj02v2dlmSt7CwsIBw4+RvA94pIiuAd+afhwa/Jd9uSd7CwsICgFQtv8wY8zzwfP7v7cD5tfz+yhcv/mnr11hYWFi4iE3Ga09L3jpeLSwsLCBWJF/8O0oHh+zutAuShYVFeIgRyRtGNCYB6IgIyb++cQ8nf/VPPLFoU72b0i/84bVNPPTK2no3o1/Y3Znhm39cwp6I7Ooem7+BZ5ZsqXcz+oUte7r41uNL6MpEY57d/9IaXlq1vd7NqBtiQ/LGQCohNCYTkSH5pZv3APD716JB8jc+OI+bH11Y72b0C4/OW8/tL67i4Tnr6t2UfuGmh+Zz7azWejejX7jzL2/ysxdW8ecV2+rdlH7hy79dxKW3v1TvZtQNsSF5xxgSCaGlMRkZCyOVdLs/59eaIgAnAu31mrh+Z2d9GxJDbN/bnX9M17klfSNqcysMxIbkjYGECC0NSTq6oxFd05XfcURtIO6NQP8mxH3MOraOUa3R3ODSRhSMqX0RGKthIzYk7xhDQmBEYzIyco2XmeuPDIoColDpszNPQNmc/r71l+TIRKC4XkLcFTQKB/R02DpWcSJ5EImWXFMgoqhZ8hFINvPIPQoLfreP2KPQt55REoXF3r9omogZU7VCbEjeGINAXq7RP7GhSERRCvmEaNQG8iSwKCz4Xd1FIopCSK3Xt1EaB1A8InS4ITYk78o1riUfFZLPmegQkd8KikIpZ6+9nRHoW7/fIAoacqRI3jduo+KrqzViQ/Ku49W15KNAmhA1IopWlU9vckdhl+RX66Iwdr32RkFaMj1IXn/fhoHYkLynyUfJ8epZRFFob9RKOXtEFLW+jVJ7o7HrKP4dhb4NA7EheWMMiQS0NKYi82NGSa5xekwW/ZPbi+WPwi7JT/JR2HlEyTjxa/JRGLdhIDYkX9DkIyXXuI9RmCx+IopC/3qTOwoRIH4iitKiFIUFKWoLaBiIEcmD4MXJZyMRLpXzWZva25vrQfL6oxQ83ozCxPb/9FFor0ec6WwUxkG0pLAwEBuSN1CIrnFMtAagiUB7ja95UbI2O6KwgEbNks+PhWgsSMW/o+BDCAOxIXnHGCQfXQPRkBT8NWC0T5hcxLa9HsnnHNMj2UgjomZt5iIUFZaL0BwLC7EheZPX5KNUbrgHcSqfMH4iSmd1txWiNbmj5u9wIpRoFrUFNAzEhuQdpyjXQDR+UP9WUnt7o7TrgJ6TW3uNlagdeOPX5LVXJHUiJoWFgfiQfMTlGu3t7Zmwo1v+gJ4hn9r7tkeYn/K2AvhrvnUp39XlIrZLCgOxIXmDlwzlnk2u3TKGaG0loyQtQbQmdw+5Rvk4gFLjRPeCb3oYJ/r7NgzEh+TzpYZbGt1bikLiQy5CEStR2/Y6EQr5dCKWlelEaMG3BcpiRPJO4dAQ15KPwg/aM1FD96LUw/GqfGJDz0VJe3ujRJoQLad2lHZ0YSFGJO9Z8q4m35nRTZoQLbmmh3MwApMlqrqxdtKEaEkg/hyJLuW5KGGhapIXkUNE5DkRWSIii0Xkpvzr40XkKRFZkX8cV31zy8NxRfmC47WzW/8PmnMMo5qisfPwFqRkQtS3FaIm1/j6VvmCBNGyjnMRcsCHhVpY8lngP4wxxwJnATeKyHHAzcAzxpjpwDP556GhoMl7JB+BH9QYCnH92gegR0QjGpOR6FvHMTQmo3EWqbdLGtGYjIQln3NMZOaZt9g3pRLqx0FYqJrkjTGbjDHz8n+3A0uAKcAlwKz8x2YB76/2WpXb4WryzY3RmNjgTpaReUte/2RxH0c2pqIRAWIMI5q8BVS3Je9p3KOaUpGw5I0pjlvtfesZJyObUqSVtzUs1FSTF5GpwKnAy8D+xphN4C4EwOQy/+c6EWkVkda2trZBX9vT5BuTCRISDW3TMYamlNte7YuSR0QjmpKRIKKc4y5IoL9vPd3YteT1E1HOGEY2RcOS96SlloZojNswUDOSF5FRwK+BTxtj9vT3/xljbjfGzDDGzJg0adKgr+8mQ4l7mHdDRCQFY0gmhOYIlEf2tr0jG1NkcqbHAcka4RhTlMKUT26PiEY2pdSPA3BDPr18FO27usIOtEn/HAsLNSF5EWnAJfgHjDGP5l/eIiIH5t8/ENhai2uVg1dqGNwImyiQfM5xST4Ki5LjszZBv3XsGFOItFIvKfg0ee39Cm7fjozIAurJNS2NKfVzLCzUIrpGgDuBJcaY7/vemg1cmf/7SuCxaq9VEXlNHlyS125hQPHIwuYG/dv0okUUDS0257gF65pSCf1x8k5xl5R19O+SeviSlM+zXKFvk+rHbFhI1eA7zgE+CiwUkfn5174I3AY8LCLXAmuBD9bgWmXhGENDIk/yEbCMIS/XCDQ3JNRbRDknepZ81KSwEU1FH0JDUm8Ki18K0z7PijvQaEhhYaBqkjfG/IWiUlKK86v9/v7CO/4PokXyXuVM7TsP49PkIQIk70DCW0CVW3B+axNc4hzd3FDPJlWEYyCVTNCY1N+3BV9SU9JG10Qd/ozM5oak+gxSyEsKeU0+MpZ8hKIqEnkpTHvflkph2snI24E2NeiPPS/6O1J055weJRmGC2JD8sZvyUfFgVWwNvUnwfjj5EG/Jm88uSalfywUI5cisoDm/R0tEZDCSmXGKBx4U2vEh+SBPMe7co1y0oSeunGnctIs6sbRIqIoyDWlmrz2sevkd6BR8neMjEikVRiID8n7Qygjosn7JQX1ESAR0+RzBhIJoSkCRFSqyWtvr2MgGZF8lEI5jqZojNswEB+Sx02GAmiOilxTKI+c0D9Z8nJNS0SIyKtlFAVr0/h0Y4jALskYEomIOLV9OQigf9yGgfiQfKklr3zLC66VEZlkKCdilrxjSIrQnIoAETnFCBDQLyk4Xg5CBMat8YVQgv6+DQPxInm/Jp/J9aglrRGubhwNa7NUk9c+WZy8XBON6JpSItLfXs/xql1m7JXfoXwshIH4kDzg2fItjUkcA93KMwcdf5hfRvfJ97kSS167BecUFtAohPmVWvK62+uV44iGXBOtJL4wEB+SN6aHJQ/Qpb5UQDEZCiCt+OQaJ2LaZq5HxqvefoXiwRZR0eRNwZcUBbnGfYxKDkIYiA3JQ88CZaB/sjiGfCy3/hr4nrWZSgqNySg4ik2hLpDmfoWgyCXdRJSLkFPb24EWDD/l7Q0DsSH5Uk0eoEP74dhexmsEFqXC8X/52HPtFpHjc7yms45q/4zXt1EYB+CXa/Rb8r2qp1pNProwGCRvyzdH5Ggyv0UEutvrWUTRsY7dXVJTQwSkMN8Zr1HwIRifUzsKi70IkSk7HQbiQ/J+Sz4iurFbA0QKJK+5vZ4hnMzvPDQvSOAuSuJbQDX3rRfLnYzIAlo0ThLq68EUEg5T+sdBWIgPydNbrlFfo92hcJIV6B6A3kROCJGpB+NJS6DbgvOkpEQiGjkeXt9GYdx62bnFxV7vOAgL8SF5U5RronSSfDIRja2kp21GqR5MIiJEVFxAvYqkevvWGJPfNUdjB+r6vaApAsENYSE+JA+F8JqWRve2tJN8oYhWytt56G2vJ9ckIuJwyznFtoJuh5vjk8KalFvyuRL/AeieZ95in0gIjSn9h/OEgdiQPL6yBoWJrXiyQH4AJiQSi1LO+OSaCGQ6mvwuKQpyjbdLEoGWhoTqcrj+BSkKEkjOceUawI20UtzWsBAbknc1effHjEpSiacXNkXAKeQPoYxCEkyUHG6F6JrCeb+K2+pbkCIh1/iTJCNSuLDWiA/JG9OjQBnoJ3mvdk0UooGcHiGU+jX5wkHeESCinOmpyWset6WLPejuW+/MBohG8lYYiA/JU4yu8Zwsmi0i8Mk1EViUSrfp2ieLl3ofDbnGfUwkRH2ZbL+TOBpyjY/kU/pLXISB+JC8T5NP5J1CmkkTiiVbozJZwHdcofK+dSd3UVJQrXPnd3Sgn4icfNMSEXK8Fs6ZaLCO10jDf2gIRCPe2CuilUzorwdTCKGMSqajMT2ja5T3rWdttjTqzngtyjXRqAfj+ByvUTglLAzEh+R9ljxE4whAx5el29yQUL0olcbJa890LJRxTumXa3J+azOle9zmShZ70E3yXnYueJq83nEQFmJF8n6Wb45A6r1XRAvyYYmKt5IFTT4iDrfCyVARaKsxRWvTiwDRWlDNCdTk9fatt6MD8qeE6W1rWAid5EXkQhFZJiIrReTmUK9FT7kmEnHyvsmt2ZIvFiiLSugcPYq/abbgck5Pa1PzgTcFJ3FUnNpOcY65hpTetoaFUEleRJLAj4GLgOOAy0TkuDCu5T80BKIj13hWhvb2epZlMgLRQAVrM+/vaEiKaodbD2tT+YE3ucI4iMZin8tXI4Vo+OnCQNiW/BnASmPMKmNMN/AQcEkYFypRa9RXSnR80SrgOYV0Tmwonl7kxp7rtuD8sdyQ17kVT26/tVmQwpQuSn65piGZIJUQ3fPMp8m3NOo/7zcMhE3yU4B1vufr868VICLXiUiriLS2tbUN+kL+UsOgf9UuJaIW5SGfRcerfgvO7xwE1z+j2d+R65GwozvHw++AB/3OzFK5Rmu/homwSV4CXuvhUTLG3G6MmWGMmTFp0qRBX8h/aAjoT2EuJaIW5eFdXnp4FEojO75dB0RhwS/u6LRb8v4CZaA/Z8IpWUDTWaewGxkuCJvk1wOH+J4fDGwM40Kuvl18rl/jdh97WkR62+vVEAf9zkz/rgP0j4VSaxM0W/LuY8JPnIr7NucUa1ppX0DDQtgkPweYLiLTRKQRuBSYHcaFjL/WMO5k6VA6UaBnBinoJ6KcQy/dWGt7c6bE2mxM0ql0QYKeUVZRW0CbG3Tr3N6ZDeA7Q1cxL4SBVJhfbozJisi/AU8CSeAuY8zikK7WU5NXLtc4AUSkdWKDG13j7ZSKoXM6+9f0kmt0x0fnHF8EiPJidb19SdqlsN47UK3GSVgIleQBjDF/BP4Y/nV6Z7xmcoZMzqEhqS/ny9ON/ZmOmuP6c0GSgtLJkguQa7bv665jiyrDCVhA1fatrxopoL4iqXvWb4lco7Rvw4I+9hsk/FUoQf8P6q8BAm7NEq0TG/IRICUkr1WLLXUOak8081ub6sdtns+j6HhtUS6FhYX4kLzpGV3T3BgRa9NXBjXruDsPjTCmp7MN9E4WL3FLJBpEFKVdUlFmdJ+rDxhwepaMAL19GxbiQ/KUseSVZg6Wxhtr12JLU+9B72QpdbxGITy1V8ar0gU0F7CAai4VkPNlwmuPXAoL8SH5AE0e9BJRaSx3FIjTI00v01ErcfqLqUEEnIP+c0i1O7WDkvhU921AopnSvg0LMSL5nvXkRyjfmgVtewG1ddpL+1ezBOL4iqlBscSF1sqOfmuzMZkgIXpJPhdgnGgOocwFavJ62xsG4kPyJc+1b81KoxS07zy80r0eNKezB2Vlqq7s6LM2vYxi7ePWiwbS3FYoqQs0TOPkY0PylNauUa5xm1JJoVF7zZIiaYLuTMde/o4I+Gd69q3eXVKpXNOU1+S17pKCSkZo7duwEBuSd6tQ9qwnD6jNei1G17jPm1O6FyXHiU4pZ6ckckl7VEXO0EsKU7tLKo0Ky+vcWp2vPQ7ytiQfbQTVkwe9P2ivan7qiai3tal1QfJUmdLYc619654QVnzerDhDN+cE75L07kCLck1TKoEIqpMOw0B8SJ6e0TXNjbo96aacpKC0ve62t6dco9badIKd2lqJyG9tgu6SHMXCeu5jIeRTqfM1F+Dv6FK66wgL8SH5gHryoHfVDopSAL3x0Y4vTh5068blchA0t7fHAqr4MO/eTm3dvqScLwcB9DuKw0B8SJ7eIX6ge2JD0dpULylESq7pnQwFmndJPUlesyVf1qmt1DgxvXagehfQsBAfki9JhmpIJmhI6j2arFwIpdbJ7U+9B+UkH3AgCyi2Np2g6BqdpFlK8k1RkGtK/B1aOSEsxIfkodc5VJqP+yoNoWxSno3Xy9pUrMl7yVC9wlPV9i09JAXVC2hJgTL9smiJXNOou9prGIgNyWN6hlCCm/WqdrKUhFBq9/z3jpPXm+kYlAwFmkm+p79Dc/37cpnaWseCv8In6A79DQuxIXlTcmgI6P5BS7e9IuLWlFfq+c85pUSkd5eUi1jkUlA2sfZx668nD9CpNNEsWArT2bdhIT4kb3qfGq75CECnJN4YdNc9d0qiFLxMR42HIpfWPNeezt5LUlAs1wSdDAV6F1CnJNFMs3ESFuJD8tDbklcs1xQqJZaGd6ltb6njVW+mY65UUkjplmuML/UeihmvGksFlA391SzX+FiupVF3aeQwEB+SLzk0BHSv2sXomuJrTYq12FJJQbMFV7pLSiSEppTeqIqgbGLQuYAW+jZKiWbWko8HAi15xZZxaaEn0L5NL05s0G3BlTpeIW/BKY4GKo1cAp3E2esAesU7Osj3rdXk44FATb5R7w9aGssNuhel3kSk14IrdbyCbgsuKNEMdMpLpX3bmHSjwrT2bc5Ex6kdFmJD8kAvU76lQW9MrKfJ904wUmoR9SIivee8OgGWvObJnQvIeAWlUljJuC3Ug1HYVugdMNDSkKQ76xR2e8MBsSD5wsHNJa9rt4yht8NNr0XUM0qhKQLWZlRI3nF6LvZNih3F5catRtkOevetlxindVEKA1WRvIh8V0SWishrIvIbERnre+8WEVkpIstE5F1Vt7QCvCCEoOgajRMFemuboDwaqCQ9XLPjtbQcLuhPMCqNAAGdu6RAf0dDUm+cfGnfKjZOwkK1lvxTwAnGmJOA5cAtACJyHHApcDxwIfATEUlWea2y8DZeQdE1XRmlsdxBck1KOxFFJLqmzAKqdpdUxt+huW975kzoHbdBiWag14cQBqoieWPMn4wx2fzTl4CD839fAjxkjEkbY94EVgJnVHOtPtoBBFvyoNPzHxRCqXnnkXN6VvnUXL639NAQUC7dmdIIEL2SQmmmNuiNCiuGe0bD3xEWaqnJXwM8nv97CrDO9976/Gu9ICLXiUiriLS2tbUN6sJFS74nikcAZtEGE2RtKiYiY3qTJui0iEpjuUG3Jl8uB0Fje6O0gJZbkECnFBYW+iR5EXlaRBYF/LvE95kvAVngAe+lgK8K1EyMMbcbY2YYY2ZMmjRpMPdQPK0m0VuuAaWTJWAANmnOdDQmOE5ecd+WLqCaI62CQig1EpETsGvWugMtNw5AJyeEhVRfHzDGXFDpfRG5EngPcL4pstN64BDfxw4GNg62kX3BKUOKzYq3ZuUcWODKS95E14JecfKq5ZqARDOlRAS9D0nXHCdfLjy1rT1dryaVhVNSggF0R4WFhWqjay4EvgC8zxjT4XtrNnCpiDSJyDRgOvBKNdfqX3t6Pi9KCvosIo+IUj1IXrcW22Nip/RWHwxyDmqVFKB3wk5BN1a48yjUXIqAJl9awwj8nKBPwg0LfVryfeB/gSbgqbxT7iVjzA3GmMUi8jDwOq6Mc6MxJrRRUAihDIiuAZ2rdjYXbBGB296x9WhUBZQm7KSSCRqTOuvBBFnyzb5Iq1JZr94ot4BqJs7S85Q1j4PgRDN9xklYqIrkjTFHVnjvG8A3qvn+freDctE1ek8EyhYs+aKZobkkbi5neuw6wI0CUUlElaIqsjlGNFZr29QWjtMz0Syl+OhK70D30kgrjWPWVHC8auzbsBCTjFf3MaiePOjcmuXygmHKl2HkZTpqtDIyjunRVtA7uQPj5BU7M0sTdsAtj6yxrRnHIVXSWK3lOMoVqgOdhlRYiAfJ5x9LLXnPYtO4ameDNHnlzsxkQPSSzra6j0E6t7b2GmPIOabHjg70FtcL2tG1NCTpzjlkc7qIvlwRQNA3DsJEPEi+ULumjCav0DlYKbpGowSSzTm9iUgpyRcdr8XXtMb1By324EphaYV9m3UCSN6rB6Ms6dAJWOwbkkIyIerGQZiIB8nnH8tlvGpMhgrS5DVnOgZNbq0HpQeGUGol+bwDvlQC0bpLygbINVr7NpPrLYl6VTM19m1YiAfJl8kd0mwZV7LkNQ7ArGN6T26lmnxFLVZZ32Y930xkpLAgB7zOeeYZUg3J3u3V2LdhIRYk75nyUmLKN6YSpBKi8jDvggUXFEKpsr1OZIjIMW5ykUhA3yprb9GS79m3TUrLTmeCNHmtC6hnySdKjZOEyhyEsBALki+EUAa8p5WIco6DCL2OJgON2qbplXoPei2iTM7QUDqxlS6gBdkuQALRNg4gb8mXtHWE0oiVTC7YktfKCWEhHiRfpp486JUUMoEOLJ0hn16UQtBk0WgRZXNOYLgnaJQUguWaEY06+zYTsKNTu0tyyljyluSjh3JVKEFvzZKgkMRipqMuC66YnRugySvs23JOYlBIRAGyHbhE1JHRtdiDZ8lHI7M8U0YK02r4hYV4kHwh1bo3zbc0JNVq8qWSgtZSAZ5FFJVtbybn0JjqHe4JqBsLRbmmZMFv1HnaUiZnAhd70Fdrx9PkGyISuRQW4kHy+cdyco22LTq4mnwy2bvBzQ0JdVZGULQK9KwHowmZgJh+rZFWZZ2DWot+OU7gYg/6LPlyOQjWko8gypU1AHebrs16g2BJAXQuSplykoLSk7eyud6SgtYkmHJhfp61qe1sgWyZzGfQR/LFOPloJPGFhXiQPOU9ry1KQ9GCNHnQuZXMVYgAAYWT2zG9tuhak2Aq+TtyjikssFoQJDM2K42uyVaIrtFmSIWJWJA8FSz5lsaUuokNniXfu/ubFS5KnkUUFQsum+stKYBOCy4TUKgONId8OmXHgTbirBhdo6xfw0QsSL6iJq9Q4waXiAI4nhEKI1Zy5TIHlVpwQZo86AxLDDo8BhQnGAVE1zQk3aRDbW0tGyffqFMKCwvxIPkyh4aAW4lSY+2aTM7QWFpfFp1OoaxTRlJQasFlcibQktcYaZWp4HgFhSQfkPEKnnWszDfjlNfkHQPdyqpmhoV4kHyZQ0NAb63rdNahMdX7HFeNurFHRA1RkWsCimiBzvK9ZXdJauWa3iGU4PWtLmOqbMCA0r4NC/Eg+T6iazTWuu4OiOUGnbpxdz56prS9hZO3lE2W8pa8whyEgGMgQa9c053N0RQwbjXq3EXHa5kyDMr6NizEg+Tzj8GavM4ftDuboylIrlFYKsALkWwq2XloTWfP5JxeExt0RlWks257SvtWaz2YdNYpT/LK+jZbzqmttG/DQjxIvsyhIaD3B+3OBlvyGksFlLXklWryZXVjhf6OwgLaEA1Nvjvr9GoreHKNrt1ywfEacNgN6OvbsBATks//ESVLvoxco9EiKlqbwens2ogzkwvW5Fsa9IXTFndJ0SCidNYJDhho0Fe+NxtwaAjoNU7CQixI3kM5TR701SzpLjNZNJYK6MuS10hEHkn60dKoL5y2nBSmtR6Ma8lHI2AgXdaXpPdY0DAQC5IvlhoOSIBR6mRJV5BrvPe1oC9rU9sC2pWp4BzUNg7y7YnCAmqMIV3O8apQZuzK5EgmJLBAGejq2zARD5KvcGjICKXhUmU1eYUDsJwl35RKIKJv29uVyRXOy/VDYz2Ycgtoi8IFNJs/PKbcDlTbHOvKOIXy3X5olcLCQk1IXkQ+KyJGRCb6XrtFRFaKyDIReVctrlMOfR0aAtEjeU0JXOUiQESEEQondzrr0ByQg9DcmMSYqOyS8uGpioiou4yTGHRGLnVlc2VkO52H84SFVLVfICKHAO8E1vpeOw64FDgeOAh4WkSOMsaEMgoqhVAWNHllA7CsJq/wBKNy2ibo26YbY/KWfLBuDJR9vx7wFvtSqdErqKZxHJQu9qBTCutrHGgzTsJCLSz5/wY+T5FrAS4BHjLGpI0xbwIrgTNqcK1AVAqhLJybquwHTeeCQ9GK8pJ+axP0JW9lcq6kECTXaEyCKadxg76Qz3KyHeisB5POlJljhXGgZ46FiapIXkTeB2wwxiwoeWsKsM73fH3+taDvuE5EWkWkta2tbVDtqGzJu5sVTfKHMcaNUihTuwZ0EVFhckcgwagrLy0FWXAaHcXlkotAX62dcqG04PatNimsK5MLlO289muaY2GiT7lGRJ4GDgh460vAF4F/DPpvAa8FLvHGmNuB2wFmzJgxKDOgr+P/QNeq7U0ET5rxQ6NTKJ11S/cmIpBg5C045cL8QNc2PZ1xAuUPcHcjmhbQirKdQinM1eR7t1WjFBYm+iR5Y8wFQa+LyInANGBBnlwPBuaJyBm4lvshvo8fDGysurVl25hvU8B7BQeWIkves85GNvbufo1E1NmdLeyISqFNrklnyktLLQr9HV19yTWK2uqN2xEBxol/Bzp2KBtVAV2Z4HwJ0GechIlByzXGmIXGmMnGmKnGmKm4xH6aMWYzMBu4VESaRGQaMB14pSYtDmpL/jFIrtF4ItC+tLvgVJosmohobzrHqKZgknf7Vs8uyeu3ig43RX27L51lZJm+HdGQUkVE3rgNMk401tpJl4muAZ2O4rBQdXRNEIwxi0XkYeB1IAvcGFZkjXs99zHI8QoucWrSNguWfMDk1khEHd3ZwAUJ3PZu3t01xC0qD6/fWipo8pqIyCX5MnJNY5I9nZkhblF5FEg+YNxqlBk7u3OB4wDcHb6mcRAmakbyeWve//wbwDdq9f0Vr12hnjzoW7X3dVew5BUS0d4K1qY2SaG9y+3b0c0VrE1F7d2bzjFlbHPgey0NCbbs1tNWb9xWMk407UDbu7KB4wD0jdswEY+M1wqaPOjT3zrS5S355kZ9nv996WxZuUabJt/e5Vq+QZNbY2Jcpb7VZpzsLYzbSglGeqS7PV2Z8iSvMIkvLMSL5MuwvLZzUytZ8o3JBAnRRUQd3bmKco2mHIQ9eUt+v+aGXu9plMIqafLarM1Kmry2vs3kHLoyDqMDxgHoM07CRDxIngq1hnF/UE2afKXJotFRvLeStdmo67SlSnKNRt24Yt82pFQtoPvSWUSCjRNtfVtpHED+UHclbQ0b8SD5fljymn7QXR2upDCmJdjK0GbBVdQ2G5JkHVM4B7be2Juf3EHEWSiopoQ4MzmHdNapYMnrW0BHNaaC81GUlUYuynZl5pgyQypMxILkPZTV5JVZ8rs6ukkI7FeG5JsVSSDZnMPuzgzjRjYGvq/PgsswojEZeGiItl3Szn3dAGX71ltAu5Vkke7Y1834UeXbCprGQWVLXpufLkzEguQr1ZMHfan3Ozq6GdPS0OvwZg+aiGhXPoRvfDkiUmbBVXK2gS7/zI4Ol+QnRGQB3dnRzbgR0SB5L/S03FjQpsn/6JkVPNK6ru8PDgLxIPkK9eTBTXHvUpSws7MjU3aygC4iKlibEZnc2/Z2M3FUU9n3Nflnduzto2+VJcbt2NdddrEv1INR0rfb8uN2UpmxoC265qE56/j7qu2hfHc8SL4PTV5bDZBdHd1lt+ig6wCG7fsqW5vaSL6tPV2R5DXt6gp924cEomVR2rmvvCWfSIibYKSkb7e1pwHKjgVNviRjDG1702UXpGoRD5LPP5YneT0TG2D73vKTBXQ5Xnf0oRs3K4s937Y3zaTRFUhekRa7o5+7JA1j1xjD9n3dZRck0GUdt+1N05CUisENoMM4aU9n6c46Ffu2GsSC5Pffr4kb334Eh00YGfi+t2pnFazaABt3dXJQmSxHgOaUnkVp465OAA4cUy4rU89kcRzTJ8lr0mI37e6iISl9avIaxsLOjgzprMMB+5Uft5p8Sd6OLqhyKug6Z8IrC3LAmJZQvj+U2jVDjQPHtPC5dx1T9n2vEmVX1mFUQNTFUKK9K8OeriwHjS3/g7ryko4Faf3OTkY2JstbRIqIaNu+NJmcYf9KlnxDkl15h2e9sWFXJweNbSlLRN6BFxrGwvqdHQBMGVdh3CragW7a3cnkCguSphIXhb6twAnVIBaWfF/QZBFtyq/alUlejyW/YVcnU8a1lI1cGqEonX3tdneyHDYxeEcHniZf/7aCO7kPrkSa3rjN1n8sbNjp7ugqEZGm7Oc12zs4bPyIsu9r2oGuz/ftIRXGQjUYHiSf0qMbF1ftCnKNIpJfv7Oz4sTWFOa32iP5CpO7qSGhgjQB1u3oo29TeiQFj4gqLUpa5JrurMPGXZ0cNqH8ONB0Stj6nZ00phIVAwaqwbAgeW/bm1YwuZdv2QvAkZNGl/2MS0T1tzazOYc32vYyff/ybdXkwFqzfR8JgYPHVZjcSvwdO/d1s21vmiMmjSr7mUIIpYpx287EUY2MjUDAwPqdHTgGDu1jsQcdu/sVW9o5fOLIsrJdtRgWJF/UjetPnMs3t3PAfs2MGRGscYNLRN1ZB8ep76HIq7fvozvrcHQlklfkwFq8cQ9HTh4VeDydBy3+jqWb2wE45sD9yn6mWZEmv3RzO8ccUL6toCf0d/HGPQAcW7Fv3XGbVtC3Sza1V2xrtRgWJK9Jk1+yuZ2jDihPmuAbgHW25pdscono6Art1STXLNywmxOmjKn4GS1S2JJNeSKq1LcpHeM2k3NYvqW94jgAPXLNog27aUwlOKqCcaKlb3fu62bzni6O6aNvq8EwI/n6kuaergzLNu/h1EPGVvxcs5Kt5Ctv7mBEY7Li5E4mhMZk/ZNgNu3upK09zYl9kHxTQ5J01ikc/l4vzFm9gyljW/oM94T6j9uFG3aTzjqcdui4ip9rbkiosIznr9vFsQeM7nNHB/WXwuas3gHAyX1wQjUYJiSv4yCOuat34hg4c9r4ip/TYh2/tGo7M6eOp6GPsNMmBRnFf16xDYCzj5hQ8XPNBf9M/cjIcQwvrdrOWYdPKBu1BMVSAfXu27+/4abbn3V43+O23qS5L51l3tqdnHV4X+NAxwL6tze209yQ4NRDx4Z2jWFC8jq2Zs8t20pTKsGp/bCIoL7tXb+zgxVb9/KWPkgTPAmkvpPlhWVt7L9fU0X/AejYpr+2YTc7OzJ99m0iITSm6r+AvrCsjWMOGM2EPqI/NEhhf3tjO5mc4W1HTar4OQ2cYIzhxeVtzJw6nqZU8KE8tcCwIHkNCTs5x/D4os28/ejJhaiJcigSUf2I83cLNgHw7hMP7POzTakE6Tr2bXtXhmeXbuX8Y/evaBmDDgtu9vyNNCYTXHDc/n1+tt61djbu6uSV1Tu4uJ/joCtTXynsdws2MqalgdOn9teQqt84WLRhD6u27etX31aDYUHyTb6M13rhLyu30dae5uKT+v5B650EY4zh0XnrOfXQsRxSIQzNQ3Ne564X/vDaJjozOT54+sF9frbeu6R0NsfsBRs57+hJZbOI/ah3NNBvXt0AwHtPPqjPz3rjtrtO5UN2d2R4YvFm3n/KQX1axp4hVc+w6l/PW09DUrjoBEvyVaMYLlW/H/Tuv77JpNFN/OPxfVtv9d5KPr+sjRVb9/LRsw7r1+frWeXTcQx3/3U1R+0/ilP64byqd+TSb1/dwLa9aS7vd9/WT+fuyuS452+rOXf6RKZWyCL2UPQh1KdvH3hlDd1Zhw/NPKTPzybyAQP1auvOfd083LqO9550UMVw6lpgeJB8nTNeF23YzfPL2rj8zMP6pb0VnIN1GIDGGH707AoOGtPcL+sN8glGdSKiJxZvZtmWdm58+5F9SjVQX0u+O+vw0xdWcfxB+3Hu9In9+j/1TN56pHUdbe1pbnjbEf36fD2Nqb3pLD9/cRVvP3oSxx9UOcLKQz0DBu7+65t0dOe4vp99Ww2qJnkR+XcRWSYii0XkO77XbxGRlfn33lXtdapBQ1JISH3kD2MMX//D64wf2cjVb53ar/9TT0t+9oKNzFu7i0+dP73PqBoPTXWSFLoyOb7zxFKOmDSS95zUvwWpqY6O11l/W82b2/bxmQuO6teCBPWTa3Z1dPP9p5Zz5rTx/XK+Q339HT9+biU7OzLcdMFR/f4/rsw49ONg3Y4OfvbiKi4+6cA+cw9qgaqqUIrI24FLgJOMMWkRmZx//TjgUuB44CDgaRE5yhhTl2XTO9uzHoPv969t4qVVO/jaJcezX5lDhUtRL01+d2eGb/1xKSdOGcMHZ/S95fXQnEoWDicfSvz4uZWs3t7BAx87s+xRiqVorpN/ZvPuLn7wzArefvQkzj92cr//X1OdHK/feXIZuzsz3Pq+4/u9IHlyzVAT57LN7fz8xVX8y+kH90uy81CPBdQYw9d+/zoJEb707mOH5JrVWvKfAG4zxqQBjDFb869fAjxkjEkbY94EVgJnVHmtqlCP8K6t7V3852OLOOWQsXzkjEP7/f/q5fn/z98uom1vmq+//4R+kybUp28XrNvFT194g386dQrnHNk/6QPqY8k7juH/PDyfnGP4ynv7T5pQn+iaZ5du4cGX13LtW6cNKN2+HpZ8Opvjs48sYHRzii8OkDTrIYU90rqep17fwqcvmF6xEm0tUS3JHwWcKyIvi8gLIjIz//oUwH8q7fr8a70gIteJSKuItLa1tVXZnPIY6lhuxzHc/OuFdHbn+K8PnUxqAHXs6xHL/dtXNzB7wUY+ff70AWffNTUkhtSRuacrw7/9Yh6TRzfzlfceN6D/Ww8p7PY/r+Jvb2znK+89rl8OTD+G2tpsa0/z+V+9xjEHjOaz7zp6QP+3Hlmktz2+lIUbdnPbP59U9vzZchhq4+TNbfu49XeLOfvwCXz83MOH7Lp9yjUi8jRwQMBbX8r//3HAWcBM4GEROZzgM7UDg2eNMbcDtwPMmDEjtADboXay/OT5lTy7dCtffd/xFSsNBmGoLaKlm/fwxd8sZMZh4/jk248c8P9vSg3dAmqM4eZfv8bGXV08fP3ZFasiBmGondovrdrO955cxoXHH8CH+xH1UYqhjK7pzjrc+OA82ruyPPjxswacoDPUC+iTizdz919Xc/U5U3nX8UEUVRleXP9QYF86yyfun0tDMsH3P3xyaBUng9AnyRtjLij3noh8AnjUuNkPr4iIA0zEtdz9I/pgYGOVba0KQ7k1e27ZVv7rqeW8/5SDuOLs/oXK+TGU6ew79nXzsVmtjGpK8b8fOW1AMo0Ht2bJ0PTtT55/gz8u3MzNFx3D6YdVTngJwlD6O9Zu7+AT98/l0Akj+Pa/nDQgmcbDUI7br/5uMa+8uYMfXHpKxeJe5VDQ5IeAOJdtbuc/Hl7ASQeP4eaLyp8KVwnNDUn2dWdr3LLecBzDZ345n+Vb2rnrqpkcGNIxf+VQrVzzW+AdACJyFNAIbANmA5eKSJOITAOmA69Uea2q0NI4NBbRmu37uOkXr3LMAfvxrQ8MbmIX0tlDbm8m5/CJ++eytT3N7VfM4IAy57j2haGyNp9YtInvPrmM9518ENf/w+C2u0NlbbZ3ZfjYvXNwDNx55cx+JT4FYajkmvteWsMDL6/lE+cdwSWnBCqrfWKoFtBte9NcO2sOIxqT/Oyjpw+6JMBQ9e33n1rOn17fwpcvPo7zju6/071WqPaM17uAu0RkEdANXJm36heLyMPA60AWuLFekTUehuIH3ZvOcv19cxERfnb56X2WL6iE5lT4Ff1unb2Yl9/cwf98+JQBRSWUojmVJJMz5BwzqJ1Af7Bw/W4+/cv5nHLIWL4zSKsY3H6FcKWwnGP49EPzeaNtH7OuPoNpA9Th/RgK3fjvb2znq7MX845jJvPZfxyYDu/HUJTjSGdz3HDfXNra0zx8/dlVWcVNDcnQd6CPzd/A/z63kktnHsLV50wN9VrlUBXJG2O6gcvLvPcN4BvVfH8tEXaYX84xfOoXr7Ji617uuXomh1Y4eqw/CHtye5bbDW87gvefOjjLzYP/5K0RjbU/G37z7i4+du8cxo9o5PYrTi9YjINBKpkglZBQ+/Y7Ty7lmaVb+dolx/PWfiY9lYNXGtlxTCg67rodHXzygblMnTiSH1x6SlWLdHNDuCGUxhhu+fVCWtfs5McfOa3q8rxhS2Hz1+3ic796jTOmjedrl5wwaMOkWgyLjFfIn1oT4g/69T+8XnC0nju9cgW8/iBMkv/bym0Fy+1zA4ygCEKY1nFnd46P39tKe1eWO6+ayeTRg5OU/Agz0urXc9fzsxdW8a9nHtrvshCV0BJiGYY9XRmunTWHnGP4+RUzGN3PPI5yaArZkv/J82/w6Ksb+D/vPKpfNaD6QnOIx2xu2t3Jx+9tZfLoJn56+ekVa9uHjWFD8k0hHmhw30truPuvq7nmnGn9rknSF8KSl1Zu3csN989lWg0sNw9h6dxefPmijbv54aWn1uyItKaQ/B1z1+zklkcXcvbhEwaURFQJYZVhyOQcbnxgHqva9vHTy0+vSlLyEOa5qZ4/5pJTDuLf3zHwCLAghGVIdXbnuO7euXSks9x55cwBh3bWGsOG5MP6QV9c3satsxdz/jGT+dLFtctgC2PnsX1vmqvveYXGVIK7rppZteXmISyS//5Ty3l80WZuueiYfpXl7S/CGAvrdnRw3b2tHDi2mZ/862n9LgnRF8JwZhpj+Mrsxfx5xTa++YETecsAkskqoSmVQKT2tWsWbdjNZ365gFMPHcu3/3nw/phSeIX1alka2W+Y/ODSU4ekbEFfGDYkH0bm4Iot7dz4wDymTx7FDy47taZOx1oTUVcmx3X3zWXrnjQ/v2JGv0oI9xdhVB/8zavr+d/nVvLhGYfUPHGk1ru63R0Zrrr7FbKO4a6rZjKuhpZbGNnPd/z5TR58eS2fPO8IPjSA8hV9QUTcswVqKIFs3t3FtbPmMH5kI7d/dEZV/phSNKeSOAYyudqR/LefWMrjizbzpXcfW1PDpBoMG5L39Ldardrb96a5ZtYcmhqS3HnVTEY11dbh6IYl1mayOI7hs48sYO6anfz3h0/p82SqgaJYvrc2i9LcNTv4wq8Wcua08fx/76+9w6qWDrfurMMnHpjL2h0d/Oyjpw848a0v1Dr7+cnFm/nm40u4+MQDq4qkKYemGvat54/Z25XlzqtmVDwPdzCo9S7pgZfX8LMXV/HRsw7j2rdOq8l31gLDh+RTSXKOqcmqnc7muD5vFd9x5QymhFCDormGpy19/6nl/P61TXzhwmP6ddLTQNFUQ2vzzW37+NgsV/YIy2HlLvjV960xhi/9ZiF/e2M73/7nk/o8V3QwqKUU9tr6Xdz00KucfPBY/utD4WRd1sqXlHMM//6LV1m8cTc/vOxUjjmgNv4YP2qZ/fzC8jb+72OLOe/oSXzlvcfVLZImCMOH5Gu0ajuO4XOPvEZr3iquJr68Emol1zzSuq4ge9zwtnDqZdSqb7ftTXPV3a8gItxz9Rk1lT38qFV0zU+ef4NH5q7npvOn84HT+j6VajCo1aHuG3Z1cu2sViaOauLnV9RW9vCjFolxxhhunb2Yp5ds4db3Hc/5x4YjezTVaAFdsmkPNz4wj6P2H83/fuS0AdWpGgroak2IqFWUwjf/uITZCzaGZhV7qIVF9OcVbXzxNws558gJfP2fwovTLaazD75vO7tzXDurlc27u7jjyhk1ifYoh1rUEX9s/ga+++Qy/unUKXz6guk1allv1MLa3NXRzVV3vUJXd467r5pZc9nDj+ZUsmrL+GcvruK+l9Zw/T8czhVnT61NwwJQC5lxy54urr1nDiObktx11Yyay7a1gL4WhYTiqTWDH4B3/HkVd/zlTa56y9TQrGIP1VpEC9bt4vr75nLEpFH85F9Pr1m0RxCqLaiWcwyfeuhVXlu/i59efjqn1dhnUIpqF9AXlrfx2UcWcOa08dz2zyeGujWvVq7xFs812zu455qZTB9ETZqBoFop7LH5G7jt8aW856QD+cKFg6tJ019Um9+xq6ObK+58hd2dGX5ZZfZtmBh2JD/YyfLY/A18/Q9LePeJB/Cf7wlfc6tGrnmjbS9X3f0KE0Y1cu81Zwy6bkp/UY1F5DiGLz66kKde38JX33f8oKoJDhTVOF7nrtnJDffNZfrk0fz8yhmDrpvSX1QjhWVyblXJeWt38pOPnMZbjqhNqGQlVON4fX7ZVj77yALOmDae730w/EqN1XBCR3eWa+6Zw5vb9nH31TM5YUr/jhysB4aRXDN4bfOJRZv5j4ddy+37H6pNAlFfaM6XQR1oNND6nR1ccecrJBPCfdecyeT9qs8Q7QuDtYiMMXz1d4v5Zes6PvWOI7nyLVNDaF1vNA1Sk1+6eQ/X3DOH/fdrYtY1Z/T7pK9qMNgQypxj+MKvXuPZpVv5+vtP4KIQpUU/BnsU5N/f2M71983lqP1H8/Mah0qWw2BDf7uzDjfcP4/563bxw8tOGdChNfXAMCL5wf2gT72+hX97cB4nHTyGO64cmsEHRafQQGKO1+3o4NLbX6K9K8M9V58x4AMqBovBWETGGG57Yimz/r6Gj587jc+8s/9nc1aLwZRGfn3jHj7y85dpbkhw37Vnhqpr+zGYEMqcY/jcIwt49NUNfO5dR/OvZ9YmC7s/aM7X2hkI5q7ZybWz5nDo+BHcd+2ZjBkR/uIJgxu3XZkcN9w/lxeXt/GtD5zIhScMzeJZDaxcUwFPLNrMv/9iHsdPGcM915xRswzR/sDvQ+jPwrJ2eweX/fwl9qazPPCxs4Z0++hZRP3dJTmOa8HP+vsaLj/rUL747mOHNOSsKTUwf8fC9bu5/M6XGdGY5MGPn1XTRLK+4FUy7W/fZnMO//HIAh6bv5HP/uNR3DiIQ2CqQUtDko4B1Gj/84o2rr9vLpNHN/HAx84c0hIAA5XCOrtzXHdfK39esY1v/NMJfHhm/4/0rCeGDcm3DJDk7/37ar4yezGnHjKWu68emq25H/6j1MZQ+doL1u3i2lmtZB2HBz525pDrg6lkgtHNqX5V+ezOOnz2kQXMXrCRj587jVsuGlqCB7dvMzlDNuf0Ge724vI2bnxgHmNGNPCLISZ4GJiksDed5cYH5vHC8jY+f+HRfPK8oSV4gHEjGvpd7fWPCzdx00OvcsSkUdx7zRlDIi364XFCZ3ffnLB9b5rr7pvLvLU7+e6/nDSgg+7rjWFD8kXSrDxZMjmHbz++lDv+8iYXHLs/P7rs1Krqwg8W3gDs6GMAPrFoE5/+5XwmjW7i7qvO5MjJ9amVMX5kIzv2dVf8zNb2Lm58YB5zVu/kCxceww1vO7wuSSPj8kcG7urMMHFUednlvpfWcOvsxUyfPIq7rpo5ZAcv+1EoFdCHcbJhVycfm9XK8i3t3PaBE7l0AAfH1xLjRjayuzNTcQF1HMOPnl3Jfz+9nBmHjXMPVRkiicaPsSPda/a1KK3Y0s41s+awdU+aH3/ktFBDp8PAsCH5QhnUCqS5aXcn//7gq7Su2clVb5nKly8+tm6JDR75tLWnA2PG09kc3/rjUu7522pOOWQsd1w5oyJhhY1xIxrZ2VGe5F9etZ1PPfQquzsz/ODSUwZ9+lAt4OnpW/ekA/usvSvD/31sMb95dQPnHzOZH1x2al3jn8eOaKi4gD65eDOf/9Vr5PK1c952VPWlrgcLT24pt4Bu35vmC79+jaeXbOUDp07hmx84ccj8XKUY3ZSiKZWgbW868H1jDI+0rufW3y1mRGOKX15/dmjJj2Fi2JB8JW3TcQy/mLOW2x5fSs4x/PCyU3nfyQcNdRN74MD8UXyb93T1eq919Q6+/NtFLN3czjXnTOMLFx0deihfXxg/spEtAW3dl87y3SeXcc/fVnPYhBHcfdUZHHdQ7VPUB4LJeZIPmtwvLG/jy79dyMZdXXzmgqP4t3ccOSTRVJVw4JgWNu7u7PX69r1pvvX4Un41dz0nThnDjy47dcic7eXg7ZJ27uvuQfLGGP64cDP/97FF7OnKcOt7j+PKt0yta/q/iDBpdBPb2nuPg427Ovna717nicWbOfvwCXz/wyerjYPvC8OG5MeNaKQxmWDjruJkMcbwl5Xb+N6flrNg3S7OPnwC3/zAiaFmW/YX+3sk75vcq7ft44fPruDReRs4aEwzd145I7SU74HigDHNzF2zE2MMIkIm5/DLOev4wTMraGtPc9VbpvL5C48O5eSogcKz5LfsLi5KSzfv4XtPLufpJVuYNnEkD19/9qAOCg8DU8a2sGTTnsLzju4sD768lh89u5J96SyfPO8Ibrpget0XeigaJ2t3dBQSr15du5PbHl/Ky2/u4MQpY3jwg2epKMEL7ljY5BsHuzsz3PPX1fz0hTdwjOHmi47h4+ceXveFvhrUf8YNEZIJ4bAJI1ixdS+d3Tn+sHAT97+0hvnrdnHQmGa+98GT+efTpqgpLDS6KcXk0U38/Y3tHHPAfjzcuo4/LtxEQzLBDW87gk+df6QKwvRw3IH78eDLa3lu2VZe37iHB15ey6bdXcycOo6fXn66GsIEOHjcCEY3pXhxRRtjRzTw0Jx1PLt0KyMbk9x80TFcc860up7kU4ojJo3kicWbeWnVdv66chsPvLyWHfu6eeuRE/nKe48LPYt1IPDI+8XlbXR057j/pTW8/OYOJo5q5GuXHM9HzjhUVW2X4w7cj8fmb2Tumh08vnAzD81Zx950lotOOIAvXXwsB48bWkd7GJBaFsyvFjNmzDCtra2hff+tsxdzz99W05hK0J11OHziSK46ZyofnnmICiuoFF/93WLu/utqAEY3p7jsjEP52LnTanIEXq2xaXcn5333+UKM9LnTJ3LNOdM47+hJahZOPz73yAIembsegAkjG7nyLVO54uzDGDuivqf4BGHJpj2850d/IecYEgLnHT2ZT553BDOmjq930wJx9d2v8NyyNgAOHtfCR886jMvPOoyRCuu6/HXlNv71jpcB1xC86IQDuOFtR6jOYA2CiMw1xswIfG84kfzOfd38z9PLaUgmeOdx+3PGtPEqCchDOpvj9ws2sV9LA+dOn1g3B1V/8frGPSzauJszp43nsAn1l7wqoSuT4/FFm5gwsomzj5gQam2fWuDVtTtZ1baPc46cyAFj9C3yfuzpyvDkos0cMn4EM6eOVy91/P2N7Wxt7+Lc6ZPqflTfYGFJ3sLCwiLGqETyus0XCwsLC4uqYEnewsLCIsaoiuRF5BQReUlE5otIq4ic4XvvFhFZKSLLRORd1TfVwsLCwmKgqNbd/R3gq8aYx0Xk3fnn54nIccClwPHAQcDTInKUMaY2h5ZaWFhYWPQL1co1BvDSF8cAG/N/XwI8ZIxJG2PeBFYCZwT8fwsLCwuLEFGtJf9p4EkR+R7ugvGW/OtTgJd8n1uff60XROQ64DqAQw+NRulOCwsLi6igT5IXkaeBoDPZvgScD3zGGPNrEfkQcCdwARAUGBsYq2mMuR24HdwQyn6228LCwsKiH+iT5I0xF5R7T0TuBW7KP30EuCP/93rAX3D5YIpSjoWFhYXFEKFauWYj8DbgeeAdwIr867OBB0Xk+7iO1+nAK3192dy5c7eJyJpBtmUisG2Q/zeqsPc8PGDveXigmnsue8ZjtST/ceAHIpICushr68aYxSLyMPA6kAVu7E9kjTFm0IWwRaS1XMZXXGHveXjA3vPwQFj3XBXJG2P+Apxe5r1vAN+o5vstLCwsLKqDzXi1sLCwiDHiRPK317sBdYC95+EBe8/DA6Hcs6oqlBYWFhYWtUWcLHkLCwsLixJYkrewsLCIMWJB8iJyYb7a5UoRubne7QkDInKIiDwnIktEZLGI3JR/fbyIPCUiK/KPeg5TrQFEJCkir4rI7/PPY32/ACIyVkR+JSJL87/32XG+bxH5TH5MLxKRX4hIc9zuV0TuEpGtIrLI91rZe6xlFd/Ik7yIJIEfAxcBxwGX5atgxg1Z4D+MMccCZwE35u/zZuAZY8x04Jn88zjhJmCJ73nc7xfgB8ATxphjgJNx7z+W9y0iU4BPATOMMScASdwKtnG733uAC0teC7zHkiq+FwI/yfPcoBB5ksetbrnSGLPKGNMNPIRbBTNWMMZsMsbMy//djjvxp+De66z8x2YB769LA0OAiBwMXEyxXAbE+H4BRGQ/4B9w60BhjOk2xuwi3vedAlrySZUjcDPpY3W/xpgXgR0lL5e7x5pW8Y0DyU8B1vmel614GReIyFTgVOBlYH9jzCZwFwJgch2bVmv8D/B5wPG9Fuf7BTgcaAPuzstUd4jISGJ638aYDcD3gLXAJmC3MeZPxPR+S1DuHmvKaXEg+X5XvIwDRGQU8Gvg08aYPfVuT1gQkfcAW40xc+vdliFGCjgN+P+NMacC+4i+VFEWeR36EmAabp2rkSJyeX1bVXfUlNPiQPLDpuKliDTgEvwDxphH8y9vEZED8+8fCGytV/tqjHOA94nIalwJ7h0icj/xvV8P64H1xpiX889/hUv6cb3vC4A3jTFtxpgM8CjuuRRxvV8/yt1jTTktDiQ/B5guItNEpBHXYTG7zm2qOUREcHXaJcaY7/vemg1cmf/7SuCxoW5bGDDG3GKMOdgYMxX3N33WGHM5Mb1fD8aYzcA6ETk6/9L5uIX+4nrfa4GzRGREfoyfj+tviuv9+lHuHmcDl4pIk4hMo59VfMvCGBP5f8C7geXAG8CX6t2ekO7xrbhbtteA+fl/7wYm4HrmV+Qfx9e7rSHc+3nA7/N/D4f7PQVozf/WvwXGxfm+ga8CS4FFwH1AU9zuF/gFrs8hg2upX1vpHnEPZXoDWAZcVM21bVkDCwsLixgjDnKNhYWFhUUZWJK3sLCwiDEsyVtYWFjEGJbkLSwsLGIMS/IWFhYWMYYleYtYQEQmiMj8/L/NIrIh//deEflJSNf8tIhcUYPveUhEpteiTRYWpbAhlBaxg4jcCuw1xnwvxGukgHnAacaYbJXf9TbgcmPMx2vSOAsLH6wlbxFriMh5vlr0t4rILBH5k4isFpEPiMh3RGShiDyRLxuBiJwuIi+IyFwRedJLPS/BO4B5HsGLyPMi8t8i8mK+BvxMEXk0Xyv86/nPjBSRP4jIgnzt9A/nv+vPwAX5hcPCoqawJG8x3HAEbvniS4D7geeMMScCncDFeaL/EfAvxpjTgbuAbwR8zzlAafG0bmPMPwA/xU1RvxE4AbhKRCbg1gbfaIw52bi1058AMMY4uOVkT67pnVpYYEneYvjhceMWwlqIe0DFE/nXFwJTgaNxifkpEZkPfBm3QFQpDsQtCeyHVzNpIbDYuGcApIFVuAWnFuJa7N8WkXONMbt9/3crbhVGC4uawm4PLYYb0uBazyKSMUWnlIM7HwSXoM/u43s6geag785/V9r3ugOkjDHLReR03JpD3xKRPxljvpb/THP+Oy0sagpryVtY9MQyYJKInA1ueWcROT7gc0uAIwfyxSJyENBhjLkf96CM03xvHwUsHlyTLSzKw1ryFhY+GGO6ReRfgB+KyBjcOfI/9Cbgx3ErJg4EJwLfFREHtxrhJwBEZH+g0+RPCbKwqCVsCKWFxSAhIr8BPm+MWVHl93wG2GOMubM2LbOwKMLKNRYWg8fNuA7YarGL4oHOFhY1hbXkLSwsLGIMa8lbWFhYxBiW5C0sLCxiDEvyFhYWFjGGJXkLCwuLGMOSvIWFhUWM8f8AedsRBHr/DXwAAAAASUVORK5CYII=\n" }, "metadata": { "needs_background": "light" @@ -214,30 +253,30 @@ { "cell_type": "markdown", "id": "8389cad4", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "The monitor in ``runner1`` has recorded the evolution of `V`. Therefore, it can be accessed by ``runner1.mon.V`` or equivalently ``runner1.mon['V']``. Similarly, the recorded trajectory of variable `spike` can also be obtained through ``runner1.mon.spike``. " ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "4d08caa4", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { - "text/plain": [ - "array([[False],\n", - " [False],\n", - " [ True],\n", - " ...,\n", - " [False],\n", - " [False],\n", - " [False]])" - ] + "text/plain": "array([[False],\n [False],\n [False],\n ...,\n [False],\n [False],\n [False]])" }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -249,7 +288,11 @@ { "cell_type": "markdown", "id": "935cfe6d", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Where ``True`` indicates a spike is generated at this time step." ] @@ -257,7 +300,11 @@ { "cell_type": "markdown", "id": "8e46f299", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## The Mechanism of ``monitors``" ] @@ -265,7 +312,11 @@ { "cell_type": "markdown", "id": "f4cd5f06", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "No matter we use a list/tuple or instantiate a `Monitor` class to generate a monitor, we specify the target variables by strings of their names. How does ``brainpy.Monitor`` find the target variables through these strings?" ] @@ -273,24 +324,30 @@ { "cell_type": "markdown", "id": "2a95ada6", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "Actually, BrainPy first tries to find the target variables in the simulated model by [the relative path](../tutorial_math/base.ipynb). If the variables are not found, BrainPy checks whether they can be accessed by [the absolute path](../tutorial_math/base.ipynb). If they not found again, an error will be raised. " ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "1f2acef5", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { - "text/plain": [ - ".func(_t, _dt)>" - ] + "text/plain": ".func(_t, _dt)>" }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -307,7 +364,11 @@ { "cell_type": "markdown", "id": "1a290ec8", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "In the above ``net``, there are ``HH`` instances named as \"X\" and \"Y\". Therefore, trying to monitor \"X.V\" and \"Y.spike\" is successful. " ] @@ -315,22 +376,30 @@ { "cell_type": "markdown", "id": "a143e5ab", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "However, in the following example, the node named with \"Z\" is not accessible in the generated ``net``, and the monitoring setup fails. " ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "2730ace5", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "RunningError : Cannot find target Z.V in monitor of , please check.\n" + "RunningError : Cannot find target Z.V in monitor of Network(HH2=HH(name=HH2), HH3=HH(name=HH3)), please check.\n" ] } ], @@ -348,22 +417,30 @@ { "cell_type": "markdown", "id": "50ee199f", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "BrainPy only supports to monitor [Variables](../tutorial_math/variables.ipynb). Monitoring [Variables](../tutorial_math/variables.ipynb)' trajectory is meaningful for they are dynamically changed. What is not marked as Variable will be compiled as constants. " ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "8bacf930", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "RunningError : \"gNa\" in is not a dynamically changed Variable, its value will not change, we think there is no need to monitor its trajectory.\n" + "RunningError : \"gNa\" in HH(name=HH4) is not a dynamically changed Variable, its value will not change, we think there is no need to monitor its trajectory.\n" ] } ], @@ -377,27 +454,33 @@ { "cell_type": "markdown", "id": "a6732c5b", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "The monitors in BrainPy only record the flattened tensor values. This means if the target variable is a matrix with the shape of ``(N, M)``, the resulting trajectory value in the monitor after running ``T`` times will be a tensor with the shape of ``(T, N x M)``." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "id": "78583b04", - "metadata": {}, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { + "text/plain": " 0%| | 0/500 [00:00