From e2f3e74c93779d0ee9bdd151dfbe70bb850a2886 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 7 Mar 2024 16:58:35 +0800 Subject: [PATCH 01/68] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20added=20`Identity`.?= =?UTF-8?q?=20Renamed=20&=20exposed=20`SynConnType`=20to=20pb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide-of-PAIBox.md | 18 +++++++++++------- paibox/__init__.py | 3 ++- paibox/synapses/__init__.py | 4 ++-- paibox/synapses/synapses.py | 29 +++++++++-------------------- paibox/synapses/transforms.py | 16 ++++++++-------- 5 files changed, 32 insertions(+), 38 deletions(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 2e631565..96eddcf1 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -178,7 +178,7 @@ print(output) PAIBox中,突触用于连接不同神经元组,并包含了连接关系以及权重信息。以全连接类型的突触为实例: ```python -s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.synapses.ConnType.All2All, name='s1') +s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.SynConnType.All2All, name='s1') ``` 其中: @@ -210,7 +210,7 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.synaps 两组神经元之间依次单对单连接,这要求**前向与后向神经元数目相同**。其权重 `weights` 主要有以下几种输入类型: -- 标量:默认为1。这表示前层的各个神经元输出线性地输入到后层神经元。这种情况等同于 `ConnType.BYPASS` 旁路连接。 +- 标量:默认为1。这表示前层的各个神经元输出线性地输入到后层神经元,即 $\lambda\cdot\mathbf{I}$ ```python n1 = pb.neuron.IF(shape=5,threshold=1) @@ -242,6 +242,10 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.synaps 其权重实际上为 `N*N` 矩阵,其中 `N` 为前向/后向神经元组数目。 +#### Identity 恒等映射 + +具有缩放因子的单对单连接,即 `One2One` 中权重项为标量的特殊情况。 + #### MatConn 一般连接 普通的神经元连接类型,仅可以通过矩阵设置其权重 `weights`。 @@ -473,8 +477,8 @@ class fcnet(pb.Network): self.i1 = pb.InputProj(input=pe, shape_out=(784,)) self.n1 = pb.neuron.IF(128, threshold=128, reset_v=0, tick_wait_start=1) self.n2 = pb.neuron.IF(10, threshold=128, reset_v=0, tick_wait_start=2) - self.s1 = pb.synapses.NoDecay(self.i1, self.n1, weights=weight1, conn_type=pb.synapses.ConnType.All2All) - self.s2 = pb.synapses.NoDecay(self.n1, self.n2, weights=weight2, conn_type=pb.synapses.ConnType.All2All) + self.s1 = pb.synapses.NoDecay(self.i1, self.n1, weights=weight1, conn_type=pb.SynConnType.All2All) + self.s2 = pb.synapses.NoDecay(self.n1, self.n2, weights=weight2, conn_type=pb.SynConnType.All2All) ``` #### 容器类型 @@ -507,7 +511,7 @@ class ReusedStructure(pb.Network): self.pre_n = pb.LIF((10,), 10) self.post_n = pb.LIF((10,), 10) self.syn = pb.NoDecay( - self.pre_n, self.post_n, conn_type=pb.synapses.ConnType.All2All, weights=weight + self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=weight ) class Net(pb.Network): @@ -518,12 +522,12 @@ class Net(pb.Network): self.s1 = pb.NoDecay( self.inp1, subnet1.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s2 = pb.NoDecay( subnet1.post_n, subnet2.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) super().__init__(subnet1, subnet2) # Necessary! diff --git a/paibox/__init__.py b/paibox/__init__.py index 863ef168..e66df694 100644 --- a/paibox/__init__.py +++ b/paibox/__init__.py @@ -10,7 +10,7 @@ from .projection import InputProj as InputProj from .simulator import Probe as Probe from .simulator import Simulator as Simulator -from .synapses import NoDecay as NoDecay +from .synapses import NoDecay as NoDecay, SynConnType as SynConnType __all__ = [ "Mapper", @@ -20,6 +20,7 @@ "NodeList", "InputProj", "Simulator", + "SynConnType", "Probe", "BACKEND_CONFIG", "FRONTEND_ENV", diff --git a/paibox/synapses/__init__.py b/paibox/synapses/__init__.py index d7518cb4..53355470 100644 --- a/paibox/synapses/__init__.py +++ b/paibox/synapses/__init__.py @@ -1,6 +1,6 @@ from .synapses import RIGISTER_MASTER_KEY_FORMAT from .synapses import NoDecay as NoDecay from .synapses import SynSys as SynSys -from .transforms import ConnType as ConnType +from .transforms import ConnType as SynConnType -__all__ = ["NoDecay", "SynSys"] +__all__ = ["NoDecay", "SynSys", "SynConnType"] diff --git a/paibox/synapses/synapses.py b/paibox/synapses/synapses.py index 56be7a4d..f6c054a6 100644 --- a/paibox/synapses/synapses.py +++ b/paibox/synapses/synapses.py @@ -18,18 +18,10 @@ class Synapses: - """A map connected between neurons of the previous `Node`, \ - and axons of the following `Node`. - - User can use connectivity matrix or COO to represent the \ - connectivity of synapses. - """ - def __init__( self, source: Union[NeuDyn, InputProj], dest: NeuDyn, - /, conn_type: ConnType, ) -> None: """ @@ -43,7 +35,7 @@ def __init__( self._check(conn_type) def _check(self, conn_type: ConnType) -> None: - if conn_type is ConnType.One2One or conn_type is ConnType.BYPASS: + if conn_type is ConnType.One2One or conn_type is ConnType.Identity: if self.num_in != self.num_out: raise ShapeError( f"The number of source & destination neurons must " @@ -124,8 +116,13 @@ def __init__( if conn_type is ConnType.One2One: self.comm = OneToOne(self.num_in, weights) - elif conn_type is ConnType.BYPASS: - self.comm = ByPass(self.num_in) + elif conn_type is ConnType.Identity: + if not isinstance(weights, (int, np.integer)): + raise TypeError( + f"Expected type int, np.integer, but got type {type(weights)}" + ) + + self.comm = Identity(self.num_in, weights) elif conn_type is ConnType.All2All: self.comm = AllToAll((self.num_in, self.num_out), weights) else: # MatConn @@ -161,7 +158,7 @@ def update( def reset_state(self, *args, **kwargs) -> None: # TODO Add other initialization methods in the future. - self.reset() # Call reset of `StatusMemory`. + self.reset_memory() # Call reset of `StatusMemory`. @property def output(self) -> NDArray[np.int32]: @@ -179,11 +176,3 @@ def weight_precision(self) -> WP: def connectivity(self): """The connectivity matrix in `np.ndarray` format.""" return self.comm.connectivity - - def __repr__(self) -> str: - name = self.__class__.__name__ - return ( - f"{name}(name={self.name}, \n" - f'{" " * len(name)} source={self.source}, \n' - f'{" " * len(name)} dest={self.dest})' - ) diff --git a/paibox/synapses/transforms.py b/paibox/synapses/transforms.py index 34577e12..800b9108 100644 --- a/paibox/synapses/transforms.py +++ b/paibox/synapses/transforms.py @@ -6,10 +6,10 @@ from paicorelib import WeightPrecision as WP from paibox.exceptions import ShapeError -from paibox.types import DataArrayType, WeightType +from paibox.types import DataArrayType, IntScalarType, WeightType from paibox.utils import is_shape -__all__ = ["ConnType", "OneToOne", "ByPass", "AllToAll", "MaskedLinear"] +__all__ = ["ConnType", "OneToOne", "AllToAll", "Identity", "MaskedLinear"] MAX_INT1 = np.int8(1) @@ -30,7 +30,8 @@ class ConnType(Enum): One2One = auto() """One-to-one connection.""" - BYPASS = auto() + Identity = auto() + """Identity connection with scaling factor.""" All2All = auto() """All-to-all connection.""" @@ -126,15 +127,14 @@ def connectivity(self): ) -class ByPass(OneToOne): - def __init__(self, num: int) -> None: +class Identity(OneToOne): + def __init__(self, num: int, scaling_factor: IntScalarType = 1) -> None: """ Arguments: - num: number of neurons. - - NOTE: The weights are always 1. + - scaling_factor: scaling factor. """ - super().__init__(num, 1) + super().__init__(num, scaling_factor) class AllToAll(Transform): From 6a561320403bae2ae2a4807e8d248941c4d40205 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 7 Mar 2024 16:59:32 +0800 Subject: [PATCH 02/68] =?UTF-8?q?=E2=9C=85=20sync=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/mnist/example1/model.py | 8 ++-- tests/backend/conftest.py | 80 +++++++++++++++---------------- tests/conftest.py | 24 +++++----- tests/neuron/conftest.py | 14 +++--- tests/simulator/test_simulator.py | 18 +++---- tests/synapses/test_synapses.py | 32 ++++++------- tests/test_network.py | 26 +++++----- 7 files changed, 101 insertions(+), 101 deletions(-) diff --git a/examples/mnist/example1/model.py b/examples/mnist/example1/model.py index 566ec65c..ab711ddb 100644 --- a/examples/mnist/example1/model.py +++ b/examples/mnist/example1/model.py @@ -13,13 +13,13 @@ def __init__(self, weight1, Vthr1, weight2, Vthr2): self.i1, self.n1, weights=weight1[:392], - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) self.s2 = pb.synapses.NoDecay( self.i2, self.n1, weights=weight1[392:], - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) # tick_wait_start = 2 for second layer @@ -33,13 +33,13 @@ def __init__(self, weight1, Vthr1, weight2, Vthr2): self.n1, self.n2, weights=weight2[:, :5], - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) self.s4 = pb.synapses.NoDecay( self.n1, self.n3, weights=weight2[:, 5:], - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) self.probe1 = pb.simulator.Probe(target=self.n2, attr="spike") diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 629ac717..030ab0ae 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -98,13 +98,13 @@ def __init__(self): self.n2 = pb.TonicSpiking(1200, 3, name="n2_1", tick_wait_start=2) self.n3 = pb.TonicSpiking(800, 4, name="n3_1", tick_wait_start=3) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1_1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1_1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2_1" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2_1" ) self.s3 = pb.NoDecay( - self.n2, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s3_1" + self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s3_1" ) @@ -121,13 +121,13 @@ def __init__(self): self.n1 = pb.TonicSpiking(30, 3, name="n1_2", tick_wait_start=1) self.n2 = pb.TonicSpiking(20, 3, name="n2_2", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1_2" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1_2" ) self.s2 = pb.NoDecay( - self.inp2, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s2_2" + self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s2_2" ) self.s3 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s3_2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s3_2" ) @@ -146,19 +146,19 @@ def __init__(self): self.n4 = pb.TonicSpiking(300, 4, name="n4", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp, self.n1, conn_type=pb.synapses.ConnType.One2One, name="s1" + self.inp, self.n1, conn_type=pb.SynConnType.One2One, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.n2, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) self.s4 = pb.NoDecay( - self.n1, self.n4, conn_type=pb.synapses.ConnType.All2All, name="s4" + self.n1, self.n4, conn_type=pb.SynConnType.All2All, name="s4" ) self.s5 = pb.NoDecay( - self.n4, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s5" + self.n4, self.n2, conn_type=pb.SynConnType.All2All, name="s5" ) @@ -176,19 +176,19 @@ def __init__(self): self.n3 = pb.TonicSpiking(400, 4, name="n3", tick_wait_start=2) self.n4 = pb.TonicSpiking(400, 4, name="n4", tick_wait_start=3) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.n1, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) self.s4 = pb.NoDecay( - self.n2, self.n4, conn_type=pb.synapses.ConnType.One2One, name="s4" + self.n2, self.n4, conn_type=pb.SynConnType.One2One, name="s4" ) self.s5 = pb.NoDecay( - self.n3, self.n4, conn_type=pb.synapses.ConnType.One2One, name="s5" + self.n3, self.n4, conn_type=pb.SynConnType.One2One, name="s5" ) @@ -206,13 +206,13 @@ def __init__(self): self.n2 = pb.TonicSpiking(20, 3, name="n2", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.inp2, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.inp2, self.n2, conn_type=pb.SynConnType.All2All, name="s3" ) @@ -230,19 +230,19 @@ def __init__(self, connect_n4: bool = False): self.n3 = pb.TonicSpiking(30, 4, name="n3", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.n1, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) if connect_n4: self.n4 = pb.TonicSpiking(50, 4, name="n4", tick_wait_start=3) self.s4 = pb.NoDecay( - self.n3, self.n4, conn_type=pb.synapses.ConnType.All2All, name="s4" + self.n3, self.n4, conn_type=pb.SynConnType.All2All, name="s4" ) @@ -261,16 +261,16 @@ def __init__(self): self.n3 = pb.TonicSpiking(30, 3, name="n3", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.inp2, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s3" ) self.s4 = pb.NoDecay( - self.n1, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s4" + self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s4" ) @@ -293,7 +293,7 @@ def __init__(self, n_onodes: int): pb.NoDecay( self.inp1, self.n_list[i], - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, name=f"s_{i}", ) ) @@ -321,35 +321,35 @@ def __init__(self, seed: int): self.inp1, self.n1, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s1", ) self.s2 = pb.NoDecay( self.n1, self.n2, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s2", ) self.s3 = pb.NoDecay( self.n1, self.n3, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s3", ) self.s4 = pb.NoDecay( self.n2, self.n4, weights=rng.randint(-8, 8, size=(10, 4), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s4", ) self.s5 = pb.NoDecay( self.n3, self.n4, weights=rng.randint(-8, 8, size=(10, 4), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s5", ) @@ -373,35 +373,35 @@ def __init__(self, seed: int) -> None: self.inp1, self.n1, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s1", ) self.s2 = pb.NoDecay( self.n1, self.n2, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s2", ) self.s3 = pb.NoDecay( self.n1, self.n3, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s3", ) self.s4 = pb.NoDecay( self.n2, self.n4, weights=rng.randint(-128, 128, size=(10, 4), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s4", ) self.s5 = pb.NoDecay( self.n3, self.n4, weights=rng.randint(-128, 128, size=(10, 4), dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, name="s5", ) @@ -425,10 +425,10 @@ def __init__(self): self.n_list = n_list self.s1 = pb.synapses.NoDecay( - n_list[0], n_list[1], conn_type=pb.synapses.ConnType.All2All + n_list[0], n_list[1], conn_type=pb.SynConnType.All2All ) self.s2 = pb.synapses.NoDecay( - n_list[1], n_list[2], conn_type=pb.synapses.ConnType.All2All + n_list[1], n_list[2], conn_type=pb.SynConnType.All2All ) self.probe1 = pb.Probe(self.n_list[1], "output", name="n2_out") diff --git a/tests/conftest.py b/tests/conftest.py index 3706630b..fa715e11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ def __init__(self): self.inp1 = pb.InputProj(1, shape_out=(1,)) self.n1 = pb.TonicSpiking(1, 3, tick_wait_start=2, delay=1) self.s1 = pb.NoDecay( - self.inp1, self.n1, weights=1, conn_type=pb.synapses.ConnType.One2One + self.inp1, self.n1, weights=1, conn_type=pb.SynConnType.One2One ) self.probe1 = pb.Probe(self.s1, "output", name="s2_out") @@ -34,10 +34,10 @@ def __init__(self): self.n2 = pb.TonicSpiking(1, 2, tick_wait_start=3) self.s1 = pb.NoDecay( - self.inp1, self.n1, weights=1, conn_type=pb.synapses.ConnType.One2One + self.inp1, self.n1, weights=1, conn_type=pb.SynConnType.One2One ) self.s2 = pb.NoDecay( - self.n1, self.n2, weights=1, conn_type=pb.synapses.ConnType.All2All + self.n1, self.n2, weights=1, conn_type=pb.SynConnType.All2All ) self.probe1 = pb.Probe(self.s2, "output", name="s2_out") @@ -70,10 +70,10 @@ def __init__(self): self.n_list = n_list self.s1 = pb.synapses.NoDecay( - n_list[0], n_list[1], conn_type=pb.synapses.ConnType.All2All + n_list[0], n_list[1], conn_type=pb.SynConnType.All2All ) self.s2 = pb.synapses.NoDecay( - n_list[1], n_list[2], conn_type=pb.synapses.ConnType.All2All + n_list[1], n_list[2], conn_type=pb.SynConnType.All2All ) self.probe1 = pb.Probe(self.n_list[1], "output", name="n2_out") @@ -91,15 +91,15 @@ def __init__(self): self.n1 = pb.neuron.TonicSpiking(2, 3) self.n2 = pb.neuron.TonicSpiking(2, 3) self.s1 = pb.synapses.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All + self.n1, self.n2, conn_type=pb.SynConnType.All2All ) self.n3 = pb.neuron.TonicSpiking(2, 4) self.n4 = pb.neuron.TonicSpiking(2, 3) self.s2 = pb.synapses.NoDecay( - self.n2, self.n4, conn_type=pb.synapses.ConnType.All2All + self.n2, self.n4, conn_type=pb.SynConnType.All2All ) self.s3 = pb.synapses.NoDecay( - self.n3, self.n4, conn_type=pb.synapses.ConnType.All2All + self.n3, self.n4, conn_type=pb.SynConnType.All2All ) @@ -118,16 +118,16 @@ def __init__(self): self.n3 = pb.TonicSpiking(30, 3, name="n3", tick_wait_start=2) self.s1 = pb.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s1" + self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) self.s2 = pb.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All, name="s2" + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) self.s3 = pb.NoDecay( - self.inp2, self.n1, conn_type=pb.synapses.ConnType.All2All, name="s3" + self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s3" ) self.s4 = pb.NoDecay( - self.n1, self.n3, conn_type=pb.synapses.ConnType.All2All, name="s4" + self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s4" ) diff --git a/tests/neuron/conftest.py b/tests/neuron/conftest.py index 047f86b4..14317c5e 100644 --- a/tests/neuron/conftest.py +++ b/tests/neuron/conftest.py @@ -46,7 +46,7 @@ def __init__(self): self.inp1 = pb.InputProj(fakeout, shape_out=(2,)) self.n1 = pb.neuron.IF((2,), 3) self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.One2One + self.inp1, self.n1, conn_type=pb.SynConnType.One2One ) self.probe1 = pb.simulator.Probe(self.inp1, "output") @@ -66,13 +66,13 @@ def __init__(self): self.inp1 = pb.InputProj(1, shape_out=(2, 2)) self.n1 = pb.neuron.LIF((2, 2), 600, reset_v=1, leaky_v=-1) self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, weights=127, conn_type=pb.synapses.ConnType.All2All + self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) self.s2 = pb.synapses.NoDecay( - self.inp1, self.n1, weights=127, conn_type=pb.synapses.ConnType.All2All + self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) self.s3 = pb.synapses.NoDecay( - self.inp1, self.n1, weights=127, conn_type=pb.synapses.ConnType.All2All + self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) self.probe1 = pb.simulator.Probe(self.inp1, "output") @@ -90,10 +90,10 @@ def __init__(self): self.n1 = pb.neuron.LIF((2, 2), 100, reset_v=1, leaky_v=-1) self.n2 = pb.neuron.LIF((2, 2), 100, reset_v=1, leaky_v=-1) self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, weights=10, conn_type=pb.synapses.ConnType.All2All + self.inp1, self.n1, weights=10, conn_type=pb.SynConnType.All2All ) self.s2 = pb.synapses.NoDecay( - self.n1, self.n2, weights=10, conn_type=pb.synapses.ConnType.All2All + self.n1, self.n2, weights=10, conn_type=pb.SynConnType.All2All ) self.probe1 = pb.simulator.Probe(self.n1, "voltage", name="n1_v") @@ -108,7 +108,7 @@ def __init__(self): self.inp1 = pb.InputProj(fakeout, shape_out=(2,)) self.n1 = pb.neuron.TonicSpiking((2,), 3) self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, conn_type=pb.synapses.ConnType.One2One + self.inp1, self.n1, conn_type=pb.SynConnType.One2One ) self.probe1 = pb.simulator.Probe(self.s1, "output") diff --git a/tests/simulator/test_simulator.py b/tests/simulator/test_simulator.py index 53aa4ea0..267e2f64 100644 --- a/tests/simulator/test_simulator.py +++ b/tests/simulator/test_simulator.py @@ -18,7 +18,7 @@ def __init__(self, n_neuron: int): self.inp, self.n1, weights=np.random.randint(-128, 128, size=(n_neuron,), dtype=np.int8), - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s1 = pb.NoDecay( self.n1, @@ -26,7 +26,7 @@ def __init__(self, n_neuron: int): weights=np.random.randint( -128, 128, size=(n_neuron, n_neuron), dtype=np.int8 ), - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) # Probes inside @@ -54,13 +54,13 @@ def __init__(self, n: int): self.inp1, self.n1, weights=np.ones((n,), dtype=np.int8), - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s1 = pb.NoDecay( self.inp2, self.n1, weights=np.ones((n,), dtype=np.int8), - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) # Probes inside @@ -83,13 +83,13 @@ def __init__(self, n: int): self.inp1, self.n1, weights=np.ones((n,), dtype=np.int8), - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s1 = pb.NoDecay( self.inp2, self.n1, weights=np.ones((n,), dtype=np.int8), - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) # Probes inside @@ -109,7 +109,7 @@ def __init__(self): w = np.ones((10, 10), dtype=np.int8) self.syn = pb.NoDecay( - self.pre_n, self.post_n, conn_type=pb.synapses.ConnType.All2All, weights=w + self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w ) self.probe_in_subnet = pb.Probe(self.pre_n, "spike") @@ -127,12 +127,12 @@ def __init__(self): self.s1 = pb.NoDecay( self.inp1, self.n1, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s2 = pb.NoDecay( self.n1, subnet.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.probe1 = pb.Probe(self.inp1, "spike") diff --git a/tests/synapses/test_synapses.py b/tests/synapses/test_synapses.py index 92e315af..eb3fc159 100644 --- a/tests/synapses/test_synapses.py +++ b/tests/synapses/test_synapses.py @@ -13,7 +13,7 @@ def test_SynSys_Attrs(): n1, n2, weights=np.array([[1, 1, 0], [0, 1, 1], [0, 1, 1]], dtype=np.int8), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, ) assert np.array_equal(s1.n_axon_each, np.array([1, 3, 2])) @@ -68,7 +68,7 @@ class TestNoDecay: ) def test_NoDecay_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): s1 = pb.synapses.NoDecay( - n1, n2, scalar_weight, conn_type=pb.synapses.ConnType.One2One + n1, n2, scalar_weight, conn_type=pb.SynConnType.One2One ) assert np.array_equal(s1.weights, scalar_weight) @@ -102,7 +102,7 @@ def test_NoDecay_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): ) def test_NoDecay_One2One_scalar_illegal(self, n1, n2): with pytest.raises(ShapeError): - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.synapses.ConnType.One2One) + s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.One2One) def test_NoDecay_One2One_matrix(self): weight = np.array([2, 3, 4], np.int8) @@ -110,7 +110,7 @@ def test_NoDecay_One2One_matrix(self): pb.neuron.TonicSpiking((3,), 3), pb.neuron.TonicSpiking((3,), 3), weight, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) assert (s1.num_in, s1.num_out) == (3, 3) @@ -125,7 +125,7 @@ def test_NoDecay_One2One_matrix(self): pb.neuron.TonicSpiking((2, 2), 3), pb.neuron.TonicSpiking((2, 2), 3), weight, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) assert (s2.num_in, s2.num_out) == (4, 4) @@ -161,7 +161,7 @@ def test_NoDecay_One2One_matrix(self): ], ) def test_NoDecay_All2All(self, n1, n2): - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.synapses.ConnType.All2All) + s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) assert (s1.num_in, s1.num_out) == (n1.num_out, n2.num_in) assert np.array_equal(s1.weights, 1) @@ -173,14 +173,14 @@ def test_NoDecay_All2All_with_weights(self): """1. Single weight.""" weight = 2 - s1 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.synapses.ConnType.All2All) + s1 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.All2All) assert np.array_equal(s1.weights, weight) assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT """2. Weights matrix.""" weight = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - s2 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.synapses.ConnType.All2All) + s2 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.All2All) assert np.array_equal(s2.weights, weight) assert np.array_equal(s2.connectivity, weight) @@ -188,7 +188,7 @@ def test_NoDecay_All2All_with_weights(self): # Wrong shape with pytest.raises(ShapeError): s3 = pb.synapses.NoDecay( - n1, n2, np.array([1, 2, 3]), conn_type=pb.synapses.ConnType.All2All + n1, n2, np.array([1, 2, 3]), conn_type=pb.SynConnType.All2All ) with pytest.raises(ShapeError): @@ -196,7 +196,7 @@ def test_NoDecay_All2All_with_weights(self): n1, n2, np.array([[1, 2, 3], [4, 5, 6]]), - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) with pytest.raises(ShapeError): @@ -204,7 +204,7 @@ def test_NoDecay_All2All_with_weights(self): n1, n2, np.array([[1, 2], [4, 5], [6, 7]]), - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) with pytest.raises(ShapeError): @@ -212,7 +212,7 @@ def test_NoDecay_All2All_with_weights(self): n1, n2, np.array([[1, 2, 3], [4, 5, 6], [6, 7, 8], [1, 2, 3]]), - conn_type=pb.synapses.ConnType.All2All, + conn_type=pb.SynConnType.All2All, ) @pytest.mark.parametrize( @@ -234,7 +234,7 @@ def test_NoDecay_MatConn(self, n1, n2): -128, 128, size=(n1.num_out, n2.num_in), dtype=np.int8 ) - s = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.synapses.ConnType.MatConn) + s = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.MatConn) assert np.array_equal(s.weights, weight) assert (s.num_in, s.num_out) == (n1.num_out, n2.num_in) @@ -242,12 +242,12 @@ def test_NoDecay_MatConn(self, n1, n2): # Wrong weight type with pytest.raises(TypeError): - s = pb.synapses.NoDecay(n1, n2, 1, conn_type=pb.synapses.ConnType.MatConn) + s = pb.synapses.NoDecay(n1, n2, 1, conn_type=pb.SynConnType.MatConn) # Wrong shape with pytest.raises(ShapeError): s = pb.synapses.NoDecay( - n1, n2, np.array([1, 2, 3]), conn_type=pb.synapses.ConnType.MatConn + n1, n2, np.array([1, 2, 3]), conn_type=pb.SynConnType.MatConn ) # Wrong shape @@ -256,5 +256,5 @@ def test_NoDecay_MatConn(self, n1, n2): n1, n2, np.array([[1, 2, 3], [4, 5, 6]]), - conn_type=pb.synapses.ConnType.MatConn, + conn_type=pb.SynConnType.MatConn, ) diff --git a/tests/test_network.py b/tests/test_network.py index 009d9771..67755fec 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -18,7 +18,7 @@ def __init__(self): w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) self.syn = pb.NoDecay( - self.pre_n, self.post_n, conn_type=pb.synapses.ConnType.All2All, weights=w + self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w ) @@ -114,12 +114,12 @@ def __init__(self): self.s1 = pb.NoDecay( self.inp1, subnet1.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) self.s2 = pb.NoDecay( subnet1.post_n, subnet2.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) super().__init__(subnet1, subnet2) @@ -159,7 +159,7 @@ def __init__(self, n: pb.neuron.Neuron): self.s1 = pb.NoDecay( n, subnet.pre_n, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) super().__init__(subnet) @@ -175,7 +175,7 @@ def __init__(self): self.s1 = pb.NoDecay( self.inp1, self.n1, - conn_type=pb.synapses.ConnType.One2One, + conn_type=pb.SynConnType.One2One, ) super().__init__(net_level2) @@ -241,8 +241,8 @@ def test_Collector_operations(self): def test_add_components(self, build_NotNested_Net_Exp): net: pb.Network = build_NotNested_Net_Exp n3 = pb.LIF((3,), 10) - s1 = pb.synapses.NoDecay(net.n1, n3, conn_type=pb.synapses.ConnType.All2All) - s2 = pb.synapses.NoDecay(net.n2, n3, conn_type=pb.synapses.ConnType.All2All) + s1 = pb.synapses.NoDecay(net.n1, n3, conn_type=pb.SynConnType.All2All) + s2 = pb.synapses.NoDecay(net.n2, n3, conn_type=pb.SynConnType.All2All) with pytest.raises(ValueError): net.diconnect_neudyn_succ(n3) @@ -307,10 +307,10 @@ def test_insert_neudyn(self, build_Network_with_container): # Insert n3 between n_list[0] & n_list[1] n_insert = pb.LIF((3,), 10) s_insert1 = pb.synapses.NoDecay( - net.n_list[0], n_insert, conn_type=pb.synapses.ConnType.All2All + net.n_list[0], n_insert, conn_type=pb.SynConnType.All2All ) s_insert2 = pb.synapses.NoDecay( - n_insert, net.n_list[1], conn_type=pb.synapses.ConnType.All2All + n_insert, net.n_list[1], conn_type=pb.SynConnType.All2All ) # Replace s1 with s_insert1->n_insert->s_insert2 @@ -367,7 +367,7 @@ def test_Subnets(self, build_Network_with_subnet): def test_Sequential_build(): n1 = pb.neuron.TonicSpiking(10, fire_step=3) n2 = pb.neuron.TonicSpiking(10, fire_step=5) - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.synapses.ConnType.All2All) + s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) sequential = pb.network.Sequential(n1, s1, n2) assert isinstance(sequential, pb.network.Sequential) @@ -381,7 +381,7 @@ def __init__(self): self.n1 = pb.neuron.TonicSpiking(5, fire_step=3) self.n2 = pb.neuron.TonicSpiking(5, fire_step=5) self.s1 = pb.synapses.NoDecay( - self.n1, self.n2, conn_type=pb.synapses.ConnType.All2All + self.n1, self.n2, conn_type=pb.SynConnType.All2All ) seq = Seq() @@ -393,9 +393,9 @@ def __init__(self): def test_Sequential_getitem(): n1 = pb.neuron.TonicSpiking(10, fire_step=3, name="n1") n2 = pb.neuron.TonicSpiking(10, fire_step=5, name="n2") - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.synapses.ConnType.All2All) + s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) n3 = pb.neuron.TonicSpiking(10, fire_step=5, name="n3") - s2 = pb.synapses.NoDecay(n2, n3, conn_type=pb.synapses.ConnType.All2All) + s2 = pb.synapses.NoDecay(n2, n3, conn_type=pb.SynConnType.All2All) sequential = pb.network.Sequential(n1, s1, n2, s2, n3, name="Sequential_2") assert isinstance(sequential.children, NodeDict) From 1890ee264b99563ef46c3e60e41c40fcf0c35a72 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 8 Mar 2024 15:54:07 +0800 Subject: [PATCH 03/68] =?UTF-8?q?=F0=9F=90=9B=20fixed=20the=20situation=20?= =?UTF-8?q?when=20there=20are=20no=20input=20nodes=20in=20the=20graph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index fc2269fd..367a8dea 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -203,6 +203,7 @@ def group_edges(self) -> List[FrozenSet[EdgeType]]: seen_edges: Set[EdgeType] = set() # Check if all edges are traversed for node in self.ordered_nodes: + """Process the predecessor nodes of nodes first, then process the successor nodes.""" if self.degree_of_nodes[node].in_degree > 1: edge_group = self._find_pred_edges(self.succ_dg, node) # Get the edges traversed for the first time @@ -468,7 +469,7 @@ def get_longest_path( Return: A tuple containing the longest path in the graph and its distance. """ - distances: Dict[_NT, int] = defaultdict(int) # init value = 0 + distances: Dict[_NT, int] = {node: 0 for node in ordered_nodes} pred_nodes: Dict[_NT, _NT] = dict() for node in ordered_nodes: @@ -487,6 +488,7 @@ def get_longest_path( distance = distances[node] path = [node] + # Construct the longest path by following the predecessors while path[-1] in pred_nodes: path.append(pred_nodes[path[-1]]) @@ -515,9 +517,13 @@ def get_shortest_path( distances: Dict[_NT, int] = defaultdict(lambda: MAX_DISTANCE) pred_nodes: Dict[_NT, _NT] = dict() - # Set initial value for all inputs nodes. - for inode in input_nodes: - distances[inode] = 0 + # Set initial value for all inputs nodes. If there is no input node, + # the first node after topological sorting will be used as the starting node. + if input_nodes: + for inode in input_nodes: + distances[inode] = 0 + else: + distances[ordered_nodes[0]] = 0 for node in ordered_nodes: for neighbor, edge_attr in edges_with_d[node].items(): @@ -535,6 +541,7 @@ def get_shortest_path( distance = distances[node] path = [node] + # Construct the shortest path by following the predecessors while path[-1] in pred_nodes: path.append(pred_nodes[path[-1]]) From 1dd83f018db64aff1d44db9f01ec0dc669cece94 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 8 Mar 2024 16:00:51 +0800 Subject: [PATCH 04/68] =?UTF-8?q?=F0=9F=9A=9A=20renamed=20arguments=20due?= =?UTF-8?q?=20to=20paicorelib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/neuron/base.py | 69 +++++++------ paibox/neuron/neurons.py | 190 +++++++++++------------------------ tests/neuron/conftest.py | 6 +- tests/neuron/test_neurons.py | 2 +- 4 files changed, 96 insertions(+), 171 deletions(-) diff --git a/paibox/neuron/base.py b/paibox/neuron/base.py index f3cc51cc..6f47110e 100644 --- a/paibox/neuron/base.py +++ b/paibox/neuron/base.py @@ -36,13 +36,13 @@ def __init__( shape: Shape, reset_mode: RM, reset_v: int, - leaking_comparison: LCM, + leak_comparison: LCM, threshold_mask_bits: int, neg_thres_mode: NTM, neg_threshold: int, pos_threshold: int, - leaking_direction: LDM, - leaking_integration_mode: LIM, + leak_direction: LDM, + leak_integration_mode: LIM, leak_v: int, synaptic_integration_mode: SIM, bit_truncation: int, @@ -58,13 +58,13 @@ def __init__( # They will be exported to the parameter verification model. self.reset_mode: RM = reset_mode self.reset_v: int = reset_v # Signed 30-bit - self.leaking_comparison: LCM = leaking_comparison + self.leak_comparison: LCM = leak_comparison self.threshold_mask_bits: int = threshold_mask_bits self.neg_thres_mode: NTM = neg_thres_mode self.neg_threshold: int = neg_threshold # Unsigned 29-bit self.pos_threshold: int = pos_threshold # Unsigned 29-bit - self.leaking_direction: LDM = leaking_direction - self.leaking_integration_mode: LIM = leaking_integration_mode + self.leak_direction: LDM = leak_direction + self.leak_integration_mode: LIM = leak_integration_mode self.leak_v: int = leak_v # Signed 30-bit self.synaptic_integration_mode: SIM = synaptic_integration_mode self.bit_truncation: int = bit_truncation # Unsigned 5-bit @@ -110,13 +110,13 @@ def _neuronal_charge( return v_charged def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: - r"""2. Leaking integration. + r"""2. Leak integration. - 2.1 Leaking direction, forward or reversal. - If leaking direction is `MODE_FORWARD`, the `_ld` is 1, else is \sgn{`vjt`}. + 2.1 Leak direction, forward or reversal. + If leak direction is `MODE_FORWARD`, the `_ld` is 1, else is \sgn{`vjt`}. - 2.2 Random leaking. - If leaking integration is `MODE_DETERMINISTIC`, then + 2.2 Random leak. + If leak integration is `MODE_DETERMINISTIC`, then `vjt` = `vjt` + `_ld` * `leak_v` else (`MODE_STOCHASTIC`) if abs(`leak_v`) >= `_rho_j_lambda`, then @@ -126,14 +126,14 @@ def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: `vjt` = `vjt` + \sgn{`leak_v`}* `_ld` * `_F` """ - _rho_j_lambda = 2 # Random leaking, unsigned 29-bit. + _rho_j_lambda = 2 # Random leak, unsigned 29-bit. - if self.leaking_direction is LDM.MODE_FORWARD: + if self.leak_direction is LDM.MODE_FORWARD: _ld = np.ones(self.varshape, dtype=np.bool_) else: _ld = np.sign(vjt) - if self.leaking_integration_mode is LIM.MODE_DETERMINISTIC: + if self.leak_integration_mode is LIM.MODE_DETERMINISTIC: v_leaked = np.add(vjt, _ld * self.leak_v).astype(np.int32) else: raise NotImplementedError( @@ -329,7 +329,7 @@ def update( v_charged = self._neuronal_charge(incoming_v, vjt_pre) # 2. Leak & fire - if self.leaking_comparison is LCM.LEAK_BEFORE_COMP: + if self.leak_comparison is LCM.LEAK_BEFORE_COMP: v_leaked = self._neuronal_leak(v_charged) spike = self._neuronal_fire(v_leaked) else: @@ -375,18 +375,18 @@ class Neuron(MetaNeuron, NeuDyn): def __init__( self, shape: Shape, - reset_mode: RM, - reset_v: int, - leaking_comparison: LCM, - threshold_mask_bits: int, - neg_thres_mode: NTM, - neg_threshold: int, - pos_threshold: int, - leaking_direction: LDM, - leaking_integration_mode: LIM, - leak_v: int, - synaptic_integration_mode: SIM, - bit_truncation: int, + reset_mode: RM = RM.MODE_NORMAL, + reset_v: int = 0, + leak_comparison: LCM = LCM.LEAK_AFTER_COMP, + threshold_mask_bits: int = 0, + neg_thres_mode: NTM = NTM.MODE_RESET, + neg_threshold: int = -1, + pos_threshold: int = 1, + leak_direction: LDM = LDM.MODE_FORWARD, + leak_integration_mode: LIM = LIM.MODE_DETERMINISTIC, + leak_v: int = 0, + synaptic_integration_mode: SIM = SIM.MODE_DETERMINISTIC, + bit_truncation: int = 0, *, delay: int = 1, tick_wait_start: int = 1, @@ -432,13 +432,13 @@ def __init__( shape, reset_mode, reset_v, - leaking_comparison, + leak_comparison, threshold_mask_bits, neg_thres_mode, (-neg_threshold), # In `MetaNeuron`, it is unsgined. pos_threshold, - leaking_direction, - leaking_integration_mode, + leak_direction, + leak_integration_mode, leak_v, synaptic_integration_mode, bit_truncation, @@ -520,8 +520,7 @@ def update( return self._inner_spike def reset_state(self, *args, **kwargs) -> None: - """Initialization, not the neuronal reset.""" - self.reset() # Call reset of `StatusMemory`. + self.reset_memory() # Call reset of `StatusMemory`. def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" @@ -539,13 +538,13 @@ def __deepcopy__(self) -> "Neuron": self._shape, self.reset_mode, self.reset_v, - self.leaking_comparison, + self.leak_comparison, self.threshold_mask_bits, self.neg_thres_mode, self.neg_threshold, self.pos_threshold, - self.leaking_direction, - self.leaking_integration_mode, + self.leak_direction, + self.leak_integration_mode, self.leak_v, self.synaptic_integration_mode, self.bit_truncation, diff --git a/paibox/neuron/neurons.py b/paibox/neuron/neurons.py index 6340fefd..2a1cfb73 100644 --- a/paibox/neuron/neurons.py +++ b/paibox/neuron/neurons.py @@ -1,6 +1,6 @@ from typing import Optional -from paicorelib import LCM, LDM, LIM, NTM, RM, SIM +from paicorelib import LCM, LDM, NTM, RM from paibox.types import Shape @@ -16,51 +16,36 @@ def __init__( threshold: int, reset_v: int = 0, *, - delay: int = 1, - tick_wait_start: int = 1, - tick_wait_end: int = 0, - unrolling_factor: int = 1, keep_shape: bool = False, name: Optional[str] = None, + **kwargs, ) -> None: """ Arguments: - - shape : the shape of the neuron(s). It can be an integer, tuple or list. - - Threshold: When the membrane potential exceeds the threshold, neurons will fire - - reset_v : Membrane potential after firing - - Description: - IF neuron : intergration + firing + - shape: shape of neurons. + - threshold: when the membrane potential exceeds the threshold, neurons will fire. + - reset_v: reset membrane potential after firing + - delay: delay between neurons. Default is 1. + - tick_wait_start: set the neuron group to start at the `N`-th timestep. 0 means not to \ + start. Default is 1. + - tick_wait_end: set the neuron group to continue working for `M` timesteps, 0 means working\ + forever. Default is 0. + - unrolling_factor: the argument is related to the backend. It means that neurons will be \ + unrolled & deployed to more physical cores to reduce latency and increase throughput. \ + Default is 1. + - keep_shape: whether to maintain size information when recording data in the simulation. \ + Default is `False`. + - name: name of the object. """ - _sim = SIM.MODE_DETERMINISTIC - _lim = LIM.MODE_DETERMINISTIC - _ld = LDM.MODE_FORWARD - _lc = LCM.LEAK_AFTER_COMP - _pos_thres = threshold - _reset_v = reset_v - _ntm = NTM.MODE_SATURATION - _reset_mode = RM.MODE_NORMAL - super().__init__( shape, - _reset_mode, - _reset_v, - _lc, - 0, - _ntm, - 0, - _pos_thres, - _ld, - _lim, - 0, - _sim, - 0, - delay=delay, - tick_wait_start=tick_wait_start, - tick_wait_end=tick_wait_end, - unrolling_factor=unrolling_factor, + reset_v=reset_v, + neg_thres_mode=NTM.MODE_SATURATION, + neg_threshold=0, + pos_threshold=threshold, keep_shape=keep_shape, name=name, + **kwargs, ) @@ -72,57 +57,32 @@ def __init__( shape: Shape, threshold: int, reset_v: int = 0, - leaky_v: int = 0, + leak_v: int = 0, *, - delay: int = 1, - tick_wait_start: int = 1, - tick_wait_end: int = 0, - unrolling_factor: int = 1, keep_shape: bool = False, name: Optional[str] = None, + **kwargs, ) -> None: """ Arguments: - - shape: the shape of the neuron(s). It can be an integer, tuple or list. - - threshold: When the membrane potential exceeds the threshold, neurons will fire - - reset_v: Membrane potential after firing - - leaky_v: The leakage value will be directly added to the membrane potential. - If it is positive, the membrane potential will increase. - If is is negative, the membrane potential will decrease. - - Description: - LIF: leaky + intergration + firing + - shape: shape of neurons. + - threshold: when the membrane potential exceeds the threshold, neurons will fire. + - reset_v: reset membrane potential after firing + - leak_v: the signed leak voltage will be added directly to the membrane potential. + - If it is positive, the membrane potential will increase. + - If is is negative, the membrane potential will decrease. """ - _sim = SIM.MODE_DETERMINISTIC - _lim = LIM.MODE_DETERMINISTIC - _ld = LDM.MODE_FORWARD - _lc = LCM.LEAK_AFTER_COMP - _leak_v = leaky_v - _pos_thres = threshold - _reset_v = reset_v - _ntm = NTM.MODE_SATURATION - _reset_mode = RM.MODE_NORMAL - super().__init__( shape, - _reset_mode, - _reset_v, - _lc, - 0, - _ntm, - 0, - _pos_thres, - _ld, - _lim, - _leak_v, - _sim, - 0, - delay=delay, - tick_wait_start=tick_wait_start, - tick_wait_end=tick_wait_end, - unrolling_factor=unrolling_factor, + reset_mode=RM.MODE_NORMAL, + reset_v=reset_v, + neg_thres_mode=NTM.MODE_SATURATION, + neg_threshold=0, + pos_threshold=threshold, + leak_v=leak_v, keep_shape=keep_shape, name=name, + **kwargs, ) @@ -134,42 +94,25 @@ def __init__( shape: Shape, fire_step: int, *, - delay: int = 1, - tick_wait_start: int = 1, - tick_wait_end: int = 0, - unrolling_factor: int = 1, keep_shape: bool = False, name: Optional[str] = None, + **kwargs, ) -> None: """ Arguments: - - shape: the shape of the neuron(s). It can be an integer, tuple or list. + - shape: shape of neurons. - fire_step: every `N` spike, the neuron will fire positively. - Description: - The neuron receives `N` spikes and fires, then resets to 0. - `N` stands for firing steps. + NOTE: The neuron receives `N` spikes and fires, then it will reset to 0. """ super().__init__( shape, - RM.MODE_NORMAL, - 0, - LCM.LEAK_AFTER_COMP, - 0, - NTM.MODE_SATURATION, - 0, - fire_step, - LDM.MODE_FORWARD, - LIM.MODE_DETERMINISTIC, - 0, - SIM.MODE_DETERMINISTIC, - 0, - delay=delay, - tick_wait_start=tick_wait_start, - tick_wait_end=tick_wait_end, - unrolling_factor=unrolling_factor, + neg_thres_mode=NTM.MODE_SATURATION, + neg_threshold=0, + pos_threshold=fire_step, keep_shape=keep_shape, name=name, + **kwargs, ) @@ -182,48 +125,31 @@ def __init__( time_to_fire: int, neg_floor: int = -10, *, - delay: int = 1, - tick_wait_start: int = 1, - tick_wait_end: int = 0, - unrolling_factor: int = 1, keep_shape: bool = False, name: Optional[str] = None, + **kwargs, ) -> None: """ Arguments: - - shape: the shape of the neuron(s). It can be an integer, tuple or list. - - time_to_fire: after `time_to_fire` spikes, the neuron will fire positively. - - neg_floor: the negative floor that the neuron stays once firing. Default is -10. + - shape: shape of neurons. + - time_to_fire: after `N` spikes, the neuron will fire positively. + - neg_floor: once fired, the neurons will remain at this negative membrane potential. \ + Default is -10. - Description: - The neuron receives `N` spikes and fires, then resets the membrane potential to 0, - and never fires again. - - `N` stands for `time_to_fire`. + NOTE: Once the neuron receives `N` spikes and fires, it will reset to the negative floor & \ + never fires again. `N` stands for `time_to_fire`. """ leak_v = 1 - pos_thres = (1 + leak_v) * time_to_fire - _neg_thres = neg_floor - reset_v = -1 - _neg_thres - super().__init__( shape, - RM.MODE_NORMAL, - reset_v, - LCM.LEAK_BEFORE_COMP, - 0, - NTM.MODE_SATURATION, - neg_floor, - pos_thres, - LDM.MODE_REVERSAL, - LIM.MODE_DETERMINISTIC, - leak_v, - SIM.MODE_DETERMINISTIC, - 0, - delay=delay, - tick_wait_start=tick_wait_start, - tick_wait_end=tick_wait_end, - unrolling_factor=unrolling_factor, + reset_v=(-1 - neg_floor), + leak_comparison=LCM.LEAK_BEFORE_COMP, + neg_thres_mode=NTM.MODE_SATURATION, + neg_threshold=neg_floor, + pos_threshold=(1 + leak_v) * time_to_fire, + leak_direction=LDM.MODE_REVERSAL, + leak_v=leak_v, keep_shape=keep_shape, name=name, + **kwargs, ) diff --git a/tests/neuron/conftest.py b/tests/neuron/conftest.py index 14317c5e..f5990dfc 100644 --- a/tests/neuron/conftest.py +++ b/tests/neuron/conftest.py @@ -64,7 +64,7 @@ class Net2(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(1, shape_out=(2, 2)) - self.n1 = pb.neuron.LIF((2, 2), 600, reset_v=1, leaky_v=-1) + self.n1 = pb.neuron.LIF((2, 2), 600, reset_v=1, leak_v=-1) self.s1 = pb.synapses.NoDecay( self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) @@ -87,8 +87,8 @@ class Net3(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(1, shape_out=(2, 2)) - self.n1 = pb.neuron.LIF((2, 2), 100, reset_v=1, leaky_v=-1) - self.n2 = pb.neuron.LIF((2, 2), 100, reset_v=1, leaky_v=-1) + self.n1 = pb.neuron.LIF((2, 2), 100, reset_v=1, leak_v=-1) + self.n2 = pb.neuron.LIF((2, 2), 100, reset_v=1, leak_v=-1) self.s1 = pb.synapses.NoDecay( self.inp1, self.n1, weights=10, conn_type=pb.SynConnType.All2All ) diff --git a/tests/neuron/test_neurons.py b/tests/neuron/test_neurons.py index d1a819ef..c8a9361b 100644 --- a/tests/neuron/test_neurons.py +++ b/tests/neuron/test_neurons.py @@ -339,7 +339,7 @@ def test_IF_simple_sim(self): print(output) def test_LIF_simple_sim(self): - n1 = pb.neuron.LIF(shape=1, threshold=5, reset_v=2, leaky_v=1) # leak + 1 + n1 = pb.neuron.LIF(shape=1, threshold=5, reset_v=2, leak_v=1) # leak + 1 # [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] inp_data = np.concatenate((np.zeros((2,), np.bool_), np.ones((10,), np.bool_))) # inp_data = np.ones((12,), dtype=np.bool_) From 66db0c07ccdaf57d9551ecaaaa194369400e0781 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 8 Mar 2024 19:33:22 +0800 Subject: [PATCH 05/68] =?UTF-8?q?=F0=9F=92=AC=20updated=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 8 ++++---- paibox/backend/placement.py | 5 ++--- paibox/base.py | 2 +- paibox/mixin.py | 2 +- paibox/projection.py | 10 +++++----- paibox/types.py | 4 ++-- tests/test_projection.py | 4 ++-- 7 files changed, 17 insertions(+), 18 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index a5e6490b..7c881254 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -154,8 +154,8 @@ def encapsulate( - addr_offset: offset of the RAM address. - axon_segs: the destination axon segments. - dest_core_coords: coordinates of the core of the destination axons. - - dest_chip_coord: coordinate of the chip of the destination axons. \ - The default is `output_chip_addr` in the backend context. + - dest_chip_coord: coordinate of the chip of the destination axons. Default is \ + `output_chip_addr` in the backend context. """ attrs = NeuronAttrs.model_validate(neuron.export_params(), strict=True) dest_rid = get_replication_id(dest_core_coords) @@ -271,7 +271,7 @@ def gen_config_frames_by_coreconf( write_to_file: bool, fp: Path, split_by_coord: bool, - format: Literal["txt", "bin", "npy"] = "bin", + format: Literal["txt", "bin", "npy"], ) -> Dict[Coord, FrameArrayType]: """Generate configuration frames by given the `CorePlacementConfig`. @@ -281,7 +281,7 @@ def gen_config_frames_by_coreconf( - write_to_file: whether to write frames to file. - fp: If `write_to_file` is `True`, specify the path. - split_by_coord: whether to split the generated frames file by the core coordinates. - - format: it can be `txt`, `bin`, or `npy`. `bin` & `npy` are recommended. + - format: `txt`, `bin`, or `npy`. `bin` & `npy` are recommended. """ def _write_to_f(name: str, array: np.ndarray) -> None: diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index b2908d97..85ca673a 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -281,13 +281,12 @@ def n_neuron_of_plm(self) -> List[int]: # Get #N of neurons on each `CorePlacement` according to the # maximum address required of neuron segments on each `CorePlacement`. - assert [] not in self.neuron_segs_of_cb # FIXME if it never happens, remove it. + assert [] not in self.neuron_segs_of_cb # TODO if it never happens, remove it. - n = [ + return [ sum(seg.n_neuron for seg in neuron_segs) for neuron_segs in self.neuron_segs_of_cb ] - return n @cached_property def raw_weight_of_dest(self) -> List[WeightType]: diff --git a/paibox/base.py b/paibox/base.py index 3af72fd2..7ae8d428 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -240,7 +240,7 @@ def export_params(self) -> Dict[str, Any]: if sys.version_info >= (3, 9): params.update({k.removeprefix("_"): v}) else: - params.update({k.lstrip("_"): v}) + params.update({k.lstrip("_"): v}) # compatible for py3.8 return params diff --git a/paibox/mixin.py b/paibox/mixin.py index 508e245e..4759a70d 100644 --- a/paibox/mixin.py +++ b/paibox/mixin.py @@ -194,7 +194,7 @@ def set_memory(self, name: str, value: Any) -> None: self._memories[name] = value self.set_reset_value(name, value) - def reset(self, name: Optional[str] = None) -> None: + def reset_memory(self, name: Optional[str] = None) -> None: if isinstance(name, str): if name in self._memories: self._memories[name] = deepcopy(self._memories_rv[name]) diff --git a/paibox/projection.py b/paibox/projection.py index c23efe36..7e1cf62b 100644 --- a/paibox/projection.py +++ b/paibox/projection.py @@ -68,7 +68,7 @@ def __init__( def update(self, **kwargs) -> SpikeType: _spike = self._get_neumeric_input(**kwargs) - if isinstance(_spike, (int, np.integer)): + if isinstance(_spike, (int, np.bool_, np.integer)): self._inner_spike = np.full((self.num_out,), _spike, dtype=np.bool_) elif isinstance(_spike, np.ndarray): try: @@ -80,14 +80,14 @@ def update(self, **kwargs) -> SpikeType: else: # Should be never raise TypeError( - f"Excepted type int, np.integer or np.ndarray, " + f"Excepted type int, np.bool_, np.integer or np.ndarray, " f"but got {_spike}, type {type(_spike)}." ) return self._inner_spike def reset_state(self) -> None: - self.reset() # Call reset of `StatusMemory`. + self.reset_memory() # Call reset of `StatusMemory`. def _get_neumeric_input(self, **kwargs): # If `_func_input` is `None` while `input` is numeric, use `input` as input to the projection. @@ -130,9 +130,9 @@ def input(self): @input.setter def input(self, value: DataType) -> None: """Set the input at the beginning of running the simulation.""" - if not isinstance(value, (int, np.integer, np.ndarray)): + if not isinstance(value, (int, np.bool_, np.integer, np.ndarray)): raise TypeError( - f"Excepted type int, np.integer or np.ndarray, " + f"Excepted type int, np.bool_, np.integer or np.ndarray, " f"but got {value}, type {type(value)}" ) diff --git a/paibox/types.py b/paibox/types.py index 794c3392..f5607ac0 100644 --- a/paibox/types.py +++ b/paibox/types.py @@ -25,9 +25,9 @@ ArrayType = TypeVar("ArrayType", List[int], Tuple[int, ...], np.ndarray) Scalar = TypeVar("Scalar", int, float, np.generic) IntScalarType = TypeVar("IntScalarType", int, np.integer) -DataType = TypeVar("DataType", int, np.integer, np.ndarray) +DataType = TypeVar("DataType", int, np.bool_, np.integer, np.ndarray) DataArrayType = TypeVar( - "DataArrayType", int, np.integer, List[int], Tuple[int, ...], np.ndarray + "DataArrayType", int, np.bool_, np.integer, List[int], Tuple[int, ...], np.ndarray ) SpikeType: TypeAlias = NDArray[np.bool_] WeightType: TypeAlias = NDArray[np.int8] # raw int8 weights diff --git a/tests/test_projection.py b/tests/test_projection.py index 7fc285e7..9fca4a27 100644 --- a/tests/test_projection.py +++ b/tests/test_projection.py @@ -164,12 +164,12 @@ def test_input_PeriodicEncoder(self): assert len(sim.data[prob]) == 10 def test_illegal_input(self): - def fakeout_with_t(t): + def fakeout_with_t(t, **kwargs): return np.ones((10, 10), dtype=np.int8) * t inp1 = pb.InputProj(None, shape_out=(4, 4), keep_shape=True) with pytest.raises(TypeError): - inp1.input = fakeout_with_t + inp1.input = fakeout_with_t # type: ignore sim = pb.Simulator(inp1) From 2e362545bb5e771fa1879a6c2c7c7ede82716847 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 8 Mar 2024 19:36:47 +0800 Subject: [PATCH 06/68] =?UTF-8?q?=E2=9C=85=20refactored=20parameterized=20?= =?UTF-8?q?test=20data=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/__init__.py | 0 tests/backend/conftest.py | 253 +++++++++++++++++++++++++++++++++++ tests/backend/test_graphs.py | 228 ++++--------------------------- tests/conftest.py | 9 ++ 4 files changed, 286 insertions(+), 204 deletions(-) create mode 100644 tests/backend/__init__.py diff --git a/tests/backend/__init__.py b/tests/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 030ab0ae..0164b867 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -26,6 +26,8 @@ from paibox.generic import clear_name_cache from paibox.node import NodeList +from tests.conftest import ParametrizedTestData + @pytest.fixture(scope="module") def ensure_dump_dir(): @@ -596,3 +598,254 @@ def packbits2(): @pytest.fixture def packbits1(): return partial(packbits_ref, count=1) + + +class TestData: + + toposort_data = ParametrizedTestData( + args="nodes", + data=[ + ( + { + "inp1": {"n1"}, + "n1": {"n2", "n4"}, + "n2": {"n3"}, + "n3": {}, + "n4": {"n2"}, + } + ), + ( + { + "inp1": {"n1"}, + "n1": {"n2", "n5"}, + "n2": {"n3"}, + "n3": {"n4", "n6"}, + "n4": {}, + "n5": {"n3", "n6"}, + "n6": {"n7"}, + "n7": {"n4"}, + } + ), + ( + { + "inp1": {"n1"}, + "inp2": {"n4"}, + "n1": {"n2"}, + "n2": {"n3"}, + "n3": {}, + "n4": {"n5"}, + "n5": {"n3"}, + } + ), + ( + { + "inp1": {"n1"}, + "n1": {"n2", "n3"}, + "n2": {"n4"}, + "n3": {"n4"}, + "n4": {}, + } + ), + ( + { + "inp1": {"n1"}, + "n1": {"n2"}, + "n2": {"n4"}, + "n3": {"n2"}, # Headless neuron N3 + "n4": {}, + } + ), + ], + ids=[ + "one_input_1", + "one_input_2", + "multi_inputs_1", + "one_input_3", + "headless_neuron_1", + ], + ) + + get_longest_path_data = ParametrizedTestData( + args="edges, expected_path, expected_distance", + data=[ + ( + # inp1 -> n1 -> n4 -> n2 -> n3, 1+1+1+1=4 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n4": 1}, + "n2": {"n3": 1}, + "n3": {}, + "n4": {"n2": 1}, + }, + ["inp1", "n1", "n4", "n2", "n3"], + 4, + ), + ( + # inp1 -> n1 -> n3 -> n4, 1+2+5=8 + { + "inp1": {"n1": 1}, + "n1": {"n2": 3, "n3": 2}, + "n2": {"n4": 2}, + "n3": {"n4": 5}, + "n4": {}, + }, + ["inp1", "n1", "n3", "n4"], + 8, + ), + ( + # inp1 -> n1 -> n2 -> n3, 1+2+1=4 + { + "inp1": {"n1": 1}, + "inp2": {"n2": 1}, + "n1": {"n2": 2}, + "n2": {"n3": 1}, + "n3": {}, + }, + ["inp1", "n1", "n2", "n3"], + 4, + ), + ( + # inp1 -> n1 -> n3 -> n5, 1+2+1=4 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n3": 2}, + "n2": {"n4": 1, "n5": 1}, + "n3": {"n4": 1}, + "n4": {}, + "n5": {}, + }, + ["inp1", "n1", "n3", "n4"], + 4, + ), + ( + # inp2 -> n5 -> n4, 4+1=5 + { + "inp1": {"n1": 1}, + "inp2": {"n5": 4}, + "n1": {"n2": 1, "n3": 1}, + "n2": {"n5": 1}, + "n3": {"n4": 1}, + "n4": {}, + "n5": {"n4": 1}, + }, + ["inp2", "n5", "n4"], + 5, + ), + ( + {"n1": {"n2": 1}, "n2": {}}, + ["n1", "n2"], + 1, + ), + ( + {"n1": {}}, + ["n1"], + 0, + ), + ], + ids=[ + "one_input_1", + "one_input_2", + "multi_inputs_1", + "multi_outputs_1", + "multi_inputs_outputs_1", + "headless_neuron_1", + "headless_neuron_2", + ], + ) + + get_shortest_path_data = ParametrizedTestData( + args="edges, inodes, expected_path, expected_distance", + data=[ + ( + # inp1 -> n1 -> n2 -> n3, 1+1+1=3 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n4": 1}, + "n2": {"n3": 1}, + "n3": {}, + "n4": {"n2": 1}, + }, + ["inp1"], + ["inp1", "n1", "n2", "n3"], + 3, + ), + ( + # inp1 -> n1 -> n2 -> n3 -> n6 -> n7 -> n4 = + # 1+1+3+2+2+3=12 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n5": 5}, + "n2": {"n3": 3}, + "n3": {"n4": 10, "n6": 2}, + "n4": {}, + "n5": {"n3": 5, "n6": 7}, + "n6": {"n7": 2}, + "n7": {"n4": 3}, + }, + ["inp1"], + ["inp1", "n1", "n2", "n3", "n6", "n7", "n4"], + 12, + ), + ( + # inp2 -> n2 -> n3, 1+1=2 + { + "inp1": {"n1": 1}, + "inp2": {"n2": 1}, + "n1": {"n2": 2}, + "n2": {"n3": 1}, + "n3": {}, + }, + ["inp1", "inp2"], + ["inp2", "n2", "n3"], + 2, + ), + ( + # inp1 -> n1 -> n2 -> n4, 1+1+1=3 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n3": 2}, + "n2": {"n4": 1}, + "n3": {"n4": 1}, + "n4": {}, + }, + ["inp1"], + ["inp1", "n1", "n2", "n4"], + 3, + ), + ( + # inp1 -> n1 -> n2 -> n4, 1+1+1=3 + { + "inp1": {"n1": 1}, + "n1": {"n2": 1, "n3": 1}, + "n2": {"n4": 2}, + "n3": {"n5": 1}, + "n4": {}, + "n5": {}, + }, + ["inp1"], + ["inp1", "n1", "n3", "n5"], + 3, + ), + ( + {"n1": {"n2": 1}, "n2": {}}, + [], + ["n1", "n2"], + 1, + ), + ( + {"n1": {}}, + [], + ["n1"], + 0, + ), + ], + ids=[ + "one_input_1", + "one_input_2", + "multi_inputs_1", + "multi_outputs_1", + "multi_outputs_2", + "headless_neuron_1", + "headless_neuron_2", + ], + ) diff --git a/tests/backend/test_graphs.py b/tests/backend/test_graphs.py index d4005bb0..e8ebdc2b 100644 --- a/tests/backend/test_graphs.py +++ b/tests/backend/test_graphs.py @@ -7,56 +7,16 @@ from paibox.backend.graphs import _degree_check from paibox.exceptions import NotSupportedError +from .conftest import TestData + class TestTopoSort: @pytest.mark.parametrize( - "edges", - [ - ( - { - "inp1": {"n1"}, - "n1": {"n2", "n4"}, - "n2": {"n3"}, - "n3": {}, - "n4": {"n2"}, - } - ), - ( - { - "inp1": {"n1"}, - "n1": {"n2", "n5"}, - "n2": {"n3"}, - "n3": {"n4", "n6"}, - "n4": {}, - "n5": {"n3", "n6"}, - "n6": {"n7"}, - "n7": {"n4"}, - } - ), - ( - { - "inp1": {"n1"}, - "inp2": {"n4"}, - "n1": {"n2"}, - "n2": {"n3"}, - "n3": {}, - "n4": {"n5"}, - "n5": {"n3"}, - } - ), - ( - { - "inp1": {"n1"}, - "n1": {"n2", "n3"}, - "n2": {"n4"}, - "n3": {"n4"}, - "n4": {}, - } - ), - ], - ids=["one_input_1", "one_input_2", "multi_inputs_1", "one_input_3"], + TestData.toposort_data["args"], + TestData.toposort_data["data"], + ids=TestData.toposort_data["ids"], ) - def test_toposort(self, edges): + def test_toposort(self, nodes): """ Test #1: one input 1 INP1 -> N1 -> N2 -> N3 @@ -77,9 +37,13 @@ def test_toposort(self, edges): Test #4: one input 3 INP1 -> N1 -> N2 -> N4 N1 -> N3 -> N4 + + Test #5: headless neuron 1 + INP1 -> N1 -> N2 -> N4 + N3 -> N2 """ - ordered = toposort(edges) - assert len(ordered) == len(edges) + ordered = toposort(nodes) + assert len(ordered) == len(nodes) @pytest.mark.parametrize( "edges", @@ -481,7 +445,7 @@ def get_longest_path_proto( Return: the longest distance in the graph. """ - distances: Dict[NodeName, int] = defaultdict(int) # init value = 0 + distances: Dict[NodeName, int] = {node: 0 for node in ordered_nodes} pred_nodes: Dict[NodeName, NodeName] = defaultdict() for node in ordered_nodes: @@ -508,79 +472,9 @@ def get_longest_path_proto( return path, distance @pytest.mark.parametrize( - "edges, expected_path, expected_distance", - [ - ( - # inp1 -> n1 -> n4 -> n2 -> n3, 1+1+1+1=4 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n4": 1}, - "n2": {"n3": 1}, - "n3": {}, - "n4": {"n2": 1}, - }, - ["inp1", "n1", "n4", "n2", "n3"], - 4, - ), - ( - # inp1 -> n1 -> n3 -> n4, 1+2+5=8 - { - "inp1": {"n1": 1}, - "n1": {"n2": 3, "n3": 2}, - "n2": {"n4": 2}, - "n3": {"n4": 5}, - "n4": {}, - }, - ["inp1", "n1", "n3", "n4"], - 8, - ), - ( - # inp1 -> n1 -> n2 -> n3, 1+2+1=4 - { - "inp1": {"n1": 1}, - "inp2": {"n2": 1}, - "n1": {"n2": 2}, - "n2": {"n3": 1}, - "n3": {}, - }, - ["inp1", "n1", "n2", "n3"], - 4, - ), - ( - # inp1 -> n1 -> n3 -> n5, 1+2+1=4 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n3": 2}, - "n2": {"n4": 1, "n5": 1}, - "n3": {"n4": 1}, - "n4": {}, - "n5": {}, - }, - ["inp1", "n1", "n3", "n4"], - 4, - ), - ( - # inp2 -> n5 -> n4, 4+1=5 - { - "inp1": {"n1": 1}, - "inp2": {"n5": 4}, - "n1": {"n2": 1, "n3": 1}, - "n2": {"n5": 1}, - "n3": {"n4": 1}, - "n4": {}, - "n5": {"n4": 1}, - }, - ["inp2", "n5", "n4"], - 5, - ), - ], - ids=[ - "one_input_1", - "one_input_2", - "multi_inputs_1", - "multi_outputs_1", - "multi_inputs_outputs_1", - ], + TestData.get_longest_path_data["args"], + TestData.get_longest_path_data["data"], + ids=TestData.get_longest_path_data["ids"], ) def test_get_longest_path_proto(self, edges, expected_path, expected_distance): ordered = toposort(edges) @@ -608,8 +502,11 @@ def get_shortest_path_proto( pred_nodes: Dict[NodeName, NodeName] = defaultdict() # Set initial value for all inputs nodes. - for inode in input_nodes: - distances[inode] = 0 + if input_nodes: + for inode in input_nodes: + distances[inode] = 0 + else: + distances[ordered_nodes[0]] = 0 for node in ordered_nodes: for neighbor in edges_with_d[node]: @@ -635,86 +532,9 @@ def get_shortest_path_proto( return path, distance @pytest.mark.parametrize( - "edges, inodes, expected_path, expected_distance", - [ - ( - # inp1 -> n1 -> n2 -> n3, 1+1+1=3 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n4": 1}, - "n2": {"n3": 1}, - "n3": {}, - "n4": {"n2": 1}, - }, - ["inp1"], - ["inp1", "n1", "n2", "n3"], - 3, - ), - ( - # inp1 -> n1 -> n2 -> n3 -> n6 -> n7 -> n4 = - # 1+1+3+2+2+3=12 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n5": 5}, - "n2": {"n3": 3}, - "n3": {"n4": 10, "n6": 2}, - "n4": {}, - "n5": {"n3": 5, "n6": 7}, - "n6": {"n7": 2}, - "n7": {"n4": 3}, - }, - ["inp1"], - ["inp1", "n1", "n2", "n3", "n6", "n7", "n4"], - 12, - ), - ( - # inp2 -> n2 -> n3, 1+1=2 - { - "inp1": {"n1": 1}, - "inp2": {"n2": 1}, - "n1": {"n2": 2}, - "n2": {"n3": 1}, - "n3": {}, - }, - ["inp1", "inp2"], - ["inp2", "n2", "n3"], - 2, - ), - ( - # inp1 -> n1 -> n2 -> n4, 1+1+1=3 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n3": 2}, - "n2": {"n4": 1}, - "n3": {"n4": 1}, - "n4": {}, - }, - ["inp1"], - ["inp1", "n1", "n2", "n4"], - 3, - ), - ( - # inp1 -> n1 -> n2 -> n4, 1+1+1=3 - { - "inp1": {"n1": 1}, - "n1": {"n2": 1, "n3": 1}, - "n2": {"n4": 2}, - "n3": {"n5": 1}, - "n4": {}, - "n5": {}, - }, - ["inp1"], - ["inp1", "n1", "n3", "n5"], - 3, - ), - ], - ids=[ - "one_input_1", - "one_input_2", - "multi_inputs_1", - "multi_outputs_1", - "multi_outputs_2", - ], + TestData.get_shortest_path_data["args"], + TestData.get_shortest_path_data["data"], + ids=TestData.get_shortest_path_data["ids"], ) def test_get_shortest_path_proto( self, edges, inodes, expected_path, expected_distance diff --git a/tests/conftest.py b/tests/conftest.py index fa715e11..c5a17f29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,17 @@ +from typing import Any, List, NotRequired, TypedDict import pytest import paibox as pb +class ParametrizedTestData(TypedDict): + """Parametrized test data in dictionary format.""" + + args: str + data: List[Any] + ids: NotRequired[List[str]] + + class Input_to_N1(pb.DynSysGroup): """Not nested network inp1 -> n1 -> s1 -> n2, n3 From 68eca96e60a1550f0d6a452b18465b741fa5e669 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 8 Mar 2024 19:52:08 +0800 Subject: [PATCH 07/68] =?UTF-8?q?=F0=9F=93=9D=20updated=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 - README.md | 2 +- docs/Guide-of-PAIBox.md | 12 ++++++------ poetry.lock | 8 ++++---- pyproject.toml | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67f8991b..0b05799e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,6 @@ repos: rev: 1.7.1 hooks: - id: poetry-check - - id: poetry-lock - id: poetry-export args: ["-f", "requirements.txt"] - id: poetry-install diff --git a/README.md b/README.md index 935fc03c..c9dd7e93 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - + diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 96eddcf1..4d8b4bfe 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -18,7 +18,7 @@ poetry install python = "^3.8" pydantic = "^2.0" numpy = "^1.23.0" -paicorelib = "0.0.12" +paicorelib = "0.0.13" ``` 通过pip安装PAIBox: @@ -72,8 +72,9 @@ n1 = pb.neuron.IF(shape=10, threshold=127, reset_v=0, keep_shape=False, delay=1, - `reset_v`:神经元的重置膜电位。 - `keep_shape`:是否在仿真记录数据时保持尺寸信息,默认为 `False`。实际进行运算的尺寸仍视为一维。 - `delay`:设定该神经元组输出的延迟。默认为1,即本时间步的计算结果,**下一时间步**传递至后继神经元。 -- `tick_wait_start`: 设定该神经元组在第 `N` 个时间步时启动,0表示不启动。默认为1。 -- `tick_wait_end`: 设定该神经元组持续工作 `M` 个时间步,0表示**永远持续工作**。默认为0。 +- `tick_wait_start`:设定该神经元组在第 `N` 个时间步时启动,0表示不启动。默认为1。 +- `tick_wait_end`:设定该神经元组持续工作 `M` 个时间步,0表示**永远持续工作**。默认为0。 +- `unrolling_factor`:该参数与后端相关。展开因子表示神经元将被展开,部署至更多的物理核上,以降低延迟,并提高吞吐率。 - `name`:可选,为该对象命名。 #### LIF神经元 @@ -81,10 +82,10 @@ n1 = pb.neuron.IF(shape=10, threshold=127, reset_v=0, keep_shape=False, delay=1, LIF神经元实现了“泄露-积分-发射”神经元模型,其调用方式及参数如下: ```python -n1 = pb.neuron.LIF(shape=128, threshold=127, reset_v=0, leaky_v=-1, keep_shape=False, name='n1') +n1 = pb.neuron.LIF(shape=128, threshold=127, reset_v=0, leak_v=-1, keep_shape=False, name='n1') ``` -- `leaky_v`:LIF神经元的泄露值(有符号)。其他参数含义与IF神经元相同。 +- `leak_v`:LIF神经元的泄露值(有符号)。其他参数含义与IF神经元相同。 #### Tonic Spiking神经元 @@ -223,7 +224,6 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.SynCon ``` 其权重以标量的形式储存。由于在运算时标量会随着矩阵进行广播,因此计算正确且节省了存储开销。 - - 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1, 2, 3, 4, 5]`, ```python diff --git a/poetry.lock b/poetry.lock index 0d56ac62..0e708efa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -130,13 +130,13 @@ reference = "tsinghua" [[package]] name = "paicorelib" -version = "0.0.12" +version = "0.0.13" description = "Library of PAICORE 2.0 in Python." optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "paicorelib-0.0.12-py3-none-any.whl", hash = "sha256:182de7dd543fdbffb2b59cec61982028210d7cc4a7caf7d932085055ddaf7505"}, - {file = "paicorelib-0.0.12.tar.gz", hash = "sha256:d84910710e3c634d4316c50f071224dfa1763f8438d49b2ce986601add83bf07"}, + {file = "paicorelib-0.0.13-py3-none-any.whl", hash = "sha256:0e6f56b55d59cffe0ced7920396c096e4024aa9774b71fab57a6ec02ebc25f8a"}, + {file = "paicorelib-0.0.13.tar.gz", hash = "sha256:0112a5097ecd3899409f6d51930d88071922a576bd780038f84fa30120d90286"}, ] [package.dependencies] @@ -350,4 +350,4 @@ reference = "tsinghua" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e4c053aefe29f7d01e3bda3d670b36a95b8834fa54e999ac239a04d0e2dbaf65" +content-hash = "295f008b50237c89eb3f089a3ee5300773b140e93e4d879f87ba469b20e6ac36" diff --git a/pyproject.toml b/pyproject.toml index ca9a6e57..cf43b98f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "paibox" -version = "1.0.0a3" +version = "1.0.0a4" description = "New toolbox of PAICORE 2.0." authors = ["Ziru Pan "] maintainers = ["Ziru Pan "] @@ -21,7 +21,7 @@ exclude = ["paibox/backend/experimental"] python = "^3.8" pydantic = "^2.0" numpy = "^1.23.0" -paicorelib = "0.0.12" +paicorelib = "0.0.13" [tool.poetry.group.test.dependencies] From 6ecb5d1d2927822bce7ba5cf785cec8e9a10a604 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:03:50 +0000 Subject: [PATCH 08/68] :rotating_light: auto fix by pre-commit hooks --- docs/Guide-of-PAIBox.md | 1 + paibox/__init__.py | 3 ++- tests/backend/conftest.py | 1 - tests/conftest.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 4d8b4bfe..ee7ac977 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -224,6 +224,7 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.SynCon ``` 其权重以标量的形式储存。由于在运算时标量会随着矩阵进行广播,因此计算正确且节省了存储开销。 + - 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1, 2, 3, 4, 5]`, ```python diff --git a/paibox/__init__.py b/paibox/__init__.py index e66df694..60c75a9b 100644 --- a/paibox/__init__.py +++ b/paibox/__init__.py @@ -10,7 +10,8 @@ from .projection import InputProj as InputProj from .simulator import Probe as Probe from .simulator import Simulator as Simulator -from .synapses import NoDecay as NoDecay, SynConnType as SynConnType +from .synapses import NoDecay as NoDecay +from .synapses import SynConnType as SynConnType __all__ = [ "Mapper", diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 0164b867..97a12417 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -25,7 +25,6 @@ from paibox.backend.routing import RoutingCluster from paibox.generic import clear_name_cache from paibox.node import NodeList - from tests.conftest import ParametrizedTestData diff --git a/tests/conftest.py b/tests/conftest.py index c5a17f29..6b582148 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ from typing import Any, List, NotRequired, TypedDict + import pytest import paibox as pb From 78ff5af4cd4bcf47bbef4cdf9fb79f7a8778604e Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 11 Mar 2024 15:10:24 +0800 Subject: [PATCH 09/68] =?UTF-8?q?=F0=9F=90=9B=20bugfix:=20added=20MAX=5FNE?= =?UTF-8?q?STED=5FLEVEL=20to=20implement=20parsing=20of=20nested=20structu?= =?UTF-8?q?res?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 4 +++- paibox/network.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 367a8dea..f2edec9f 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -98,7 +98,9 @@ def build( _edges: Collector[EdgeName, EdgeType] = Collector() for network in networks: - sub_nodes = network.nodes(level=1, include_self=False) + # FIXME MAX_NESTED_LEVEL is set manually in DynSysGroup. + # Do we need to take precautions in advance? + sub_nodes = network.nodes(level=network.MAX_NESTED_LEVEL, include_self=False) _nodes += sub_nodes.include(InputProj, NeuDyn).unique() _edges += sub_nodes.subset(SynSys).unique() diff --git a/paibox/network.py b/paibox/network.py index f3ae7bb5..fa478559 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -1,5 +1,5 @@ import warnings -from typing import Callable, List, Optional, Tuple, Type, Union +from typing import Callable, ClassVar, List, Optional, Tuple, Type, Union import numpy as np from typing_extensions import TypeAlias @@ -18,6 +18,8 @@ class DynSysGroup(DynamicSys, Container): + MAX_NESTED_LEVEL: ClassVar[int] = 9 + def __init__( self, *components_as_tuple, @@ -37,6 +39,9 @@ def update(self, **kwargs) -> None: TODO Prove that the operation sequence I->S->N can be divided into Ix->Sx->Nx->Iy->Sy->Ny & it has \ nothing to do with the network topology. + + The above expression cannot be completely established, and the condition needs to be met: the \ + dependent synapses of the neuron are in the same subgraph. """ nodes = self.nodes(level=1, include_self=False).subset(DynamicSys).unique() From f7afe1354aa08e9342c90f8a4e9b3672cb9ec006 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 11 Mar 2024 15:14:03 +0800 Subject: [PATCH 10/68] =?UTF-8?q?=E2=9C=85=20added=20tests=20for=20parsing?= =?UTF-8?q?=20of=20nested=20structures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/conftest.py | 64 ++++++++++++++++++++++++++++++++++++ tests/backend/test_mapper.py | 51 +++++++++++++++++++--------- tests/test_network.py | 10 ++++-- 3 files changed, 106 insertions(+), 19 deletions(-) diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 97a12417..a529079e 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -3,6 +3,7 @@ import tempfile from functools import partial from pathlib import Path +from typing import Optional import numpy as np import pytest @@ -435,6 +436,59 @@ def __init__(self): self.probe1 = pb.Probe(self.n_list[1], "output", name="n2_out") +class ReusedStruct(pb.Network): + """Reused structure: pre_n -> syn -> post_n, 8-bit""" + + def __init__(self, tws: int = 1, name: Optional[str] = None): + super().__init__(name=name) + + self.pre_n = pb.LIF((10,), 10, 2, tick_wait_start=tws) + self.post_n = pb.LIF((10,), 10, 2, tick_wait_start=tws + 1) + + w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) + self.syn = pb.NoDecay( + self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w + ) + + +class Nested_Net_level_2(pb.DynSysGroup): + """Level 2 nested network: inp1 -> s1 -> ReusedStruct -> s2 -> ReusedStruct""" + + def __init__(self, tws: int = 1, name: Optional[str] = None): + self.inp1 = pb.InputProj(1, shape_out=(10,)) + subnet1 = ReusedStruct(tws=tws, name="Named_Reused_0") + subnet2 = ReusedStruct(tws=tws + 2, name="Named_Reused_1") + + self.s1 = pb.NoDecay( + self.inp1, + subnet1.pre_n, + conn_type=pb.SynConnType.One2One, + ) + self.s2 = pb.NoDecay( + subnet1.post_n, + subnet2.pre_n, + conn_type=pb.SynConnType.One2One, + ) + + super().__init__(subnet1, subnet2, name=name) + + +class Nested_Net_level_3(pb.DynSysGroup): + """Level 3 nested network: inp1 -> s1 -> Nested_Net_level_2""" + + def __init__(self): + self.inp1 = pb.InputProj(1, shape_out=(10,)) + subnet1 = Nested_Net_level_2(name="Named_Nested_Net_level_2") + + self.s1 = pb.NoDecay( + self.inp1, + subnet1["Named_Reused_0"].pre_n, + conn_type=pb.SynConnType.One2One, + ) + + super().__init__(subnet1) + + @pytest.fixture(scope="class") def build_example_net1(): return NetForTest1() @@ -500,6 +554,16 @@ def build_Network_with_container(): return Network_with_container() +@pytest.fixture(scope="class") +def build_Nested_Net_level_2(): + return Nested_Net_level_2() + + +@pytest.fixture(scope="class") +def build_Nested_Net_level_3(): + return Nested_Net_level_3() + + @pytest.fixture(scope="class") def get_mapper() -> pb.Mapper: return pb.Mapper() diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index 539fb99a..a0b3d028 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -37,7 +37,6 @@ def test_multi_inputproj( ): net = build_multi_inputproj_net mapper: pb.Mapper = get_mapper - mapper.build(net) mapper.compile() mapper.export( @@ -54,7 +53,6 @@ def test_multi_inputproj2( ): net = build_multi_inputproj_net2 mapper: pb.Mapper = get_mapper - mapper.build(net) mapper.compile() mapper.export( @@ -71,9 +69,9 @@ def test_multi_output_nodes( ): net = build_multi_onodes_net mapper: pb.Mapper = get_mapper - mapper.build(net) mapper.compile() + assert len(mapper.graph_info["output"]) == 2 mapper.export( @@ -88,7 +86,6 @@ def test_multi_output_nodes2( ): net = build_multi_onodes_net2 mapper: pb.Mapper = get_mapper - mapper.build(net) mapper.compile() @@ -106,12 +103,32 @@ def test_multi_inodes_onodes( ): net = build_multi_inodes_onodes mapper: pb.Mapper = get_mapper - mapper.build(net) mapper.compile() + assert len(mapper.graph_info["input"]) == 2 assert len(mapper.graph_info["output"]) == 2 + def test_nested_net_L2_compile(self, get_mapper, build_Nested_Net_level_2): + net = build_Nested_Net_level_2 + mapper: pb.Mapper = get_mapper + mapper.build(net) + mapper.compile() + + assert len(mapper.graph.nodes.keys()) == 5 + assert len(mapper.graph_info["input"]) == 1 + assert len(mapper.graph_info["output"]) == 1 + + def test_nested_net_L3_compile(self, get_mapper, build_Nested_Net_level_3): + net2 = build_Nested_Net_level_3 + mapper: pb.Mapper = get_mapper + mapper.build(net2) + mapper.compile() + + assert len(mapper.graph.edges.keys()) == 5 + assert len(mapper.graph_info["input"]) == 2 + assert len(mapper.graph_info["output"]) == 1 + class TestMapperDebug: def test_build_graph(self, get_mapper, build_example_net1, build_example_net2): @@ -120,22 +137,23 @@ def test_build_graph(self, get_mapper, build_example_net1, build_example_net2): net2 = build_example_net2 mapper: pb.Mapper = get_mapper - mapper.clear() mapper.build(net1, net2) + mapper.compile() - assert mapper.graph.has_built == True + assert len(mapper.graph.nodes.keys()) == 8 + assert len(mapper.graph_info["input"]) == 3 + assert len(mapper.graph_info["output"]) == 2 @pytest.fixture - def test_simple_net(self, get_mapper, build_example_net1): - """Go throught the backend""" + def compile_simple_net(self, get_mapper, build_example_net1): + """Reused fixture.""" net = build_example_net1 mapper: pb.Mapper = get_mapper - mapper.clear() mapper.build(net) mapper.compile() - @pytest.mark.usefixtures("test_simple_net") + @pytest.mark.usefixtures("compile_simple_net") def test_export_config_json(self, ensure_dump_dir, get_mapper): """Export all the configs into json""" mapper: pb.Mapper = get_mapper @@ -149,7 +167,7 @@ def test_export_config_json(self, ensure_dump_dir, get_mapper): ) print() - @pytest.mark.usefixtures("test_simple_net") + @pytest.mark.usefixtures("compile_simple_net") def test_find_neuron(self, get_mapper, build_example_net1): net: pb.Network = build_example_net1 mapper: pb.Mapper = get_mapper @@ -159,7 +177,7 @@ def test_find_neuron(self, get_mapper, build_example_net1): print() - @pytest.mark.usefixtures("test_simple_net") + @pytest.mark.usefixtures("compile_simple_net") def test_find_axon(self, get_mapper, build_example_net1): net: pb.Network = build_example_net1 mapper: pb.Mapper = get_mapper @@ -171,13 +189,14 @@ def test_find_axon(self, get_mapper, build_example_net1): def test_network_with_container(self, get_mapper, build_Network_with_container): net: pb.Network = build_Network_with_container - mapper: pb.Mapper = get_mapper - mapper.clear() mapper.build(net) mapper.compile() - print() + assert len(mapper.graph.nodes.keys()) == 4 + # Input projectioon is discnnected! + assert len(mapper.graph_info["input"]) == 0 + assert len(mapper.graph_info["output"]) == 1 class TestMapper_Export: diff --git a/tests/test_network.py b/tests/test_network.py index 67755fec..a9ed0a20 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,3 +1,4 @@ +from typing import Optional import numpy as np import pytest @@ -10,8 +11,8 @@ class Nested_Net_level_1(pb.DynSysGroup): """Level 1 nested network: pre_n -> syn -> post_n""" - def __init__(self): - super().__init__() + def __init__(self, name: Optional[str] = None): + super().__init__(name=name) self.pre_n = pb.LIF((10,), 10) self.post_n = pb.LIF((10,), 10) @@ -110,7 +111,7 @@ class Nested_Net_level_2(pb.DynSysGroup): def __init__(self): self.inp1 = pb.InputProj(1, shape_out=(10,)) subnet1 = Nested_Net_level_1() - subnet2 = Nested_Net_level_1() + subnet2 = Nested_Net_level_1(name="Named_SubNet") self.s1 = pb.NoDecay( self.inp1, subnet1.pre_n, @@ -144,6 +145,9 @@ def __init__(self): .unique() .not_subset(pb.DynSysGroup) ) + + assert isinstance(net[f"{Nested_Net_level_1.__name__}_0"], pb.Network) + assert isinstance(net["Named_SubNet"], pb.Network) assert len(nodes) == 5 assert len(nodes_excluded) == 3 From 6daa8e5ece99068c4df3d6bd4d72c85b2ecb8ef7 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 11 Mar 2024 15:26:01 +0800 Subject: [PATCH 11/68] =?UTF-8?q?=F0=9F=93=9D=20updated=20building=20a=20n?= =?UTF-8?q?ested=20network=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide-of-PAIBox.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index ee7ac977..77ca3efc 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -224,7 +224,6 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.SynCon ``` 其权重以标量的形式储存。由于在运算时标量会随着矩阵进行广播,因此计算正确且节省了存储开销。 - - 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1, 2, 3, 4, 5]`, ```python @@ -504,13 +503,15 @@ for i in range(5): 有时网络中会重复出现类似的结构,这时先构建子网络,再多次例化复用是个不错的选择。 ```python +froom typing import Optional import paibox as pb class ReusedStructure(pb.Network): - def __init__(self, weight): - super().__init__() - self.pre_n = pb.LIF((10,), 10) - self.post_n = pb.LIF((10,), 10) + def __init__(self, weight, tws, name: Optional[str] = None): + super().__init__(name=name) + + self.pre_n = pb.LIF((10,), 10, tick_wait_start=tws) + self.post_n = pb.LIF((10,), 10, tick_wait_start=tws+1) self.syn = pb.NoDecay( self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=weight ) @@ -518,8 +519,8 @@ class ReusedStructure(pb.Network): class Net(pb.Network): def __init__(self, w1, w2): self.inp1 = pb.InputProj(1, shape_out=(10,)) - subnet1 = ReusedStructure(w1) - subnet2 = ReusedStructure(w2) + subnet1 = ReusedStructure(w1, tws=1, name="Reused_Struct_0") + subnet2 = ReusedStructure(w2, tws=3, name="Reused_Struct_1") self.s1 = pb.NoDecay( self.inp1, subnet1.pre_n, @@ -538,7 +539,7 @@ w2 = ... net = Net(w1, w2) ``` -上述示例代码中,我们先创建需复用的子网络 `ReusedStructure`,其结构为 `pre_n` -> `syn` -> `post_n`。而后,在父网络 `Net` 中实例化两个子网络 `subnet1`、 `subnet2`,并与父网络其他部分连接,此时网络结构为:`inp1` -> `s1` -> `subnet1` -> `s2` -> `subnet2`。最后,在为 `pb.Network` 初始化时,传入子网络 `subnet1`、 `subnet2`。由此,父网络 `Net` 才能发现子网络组件。 +上述示例代码中,我们先创建需复用的子网络 `ReusedStructure`,其结构为 `pre_n` -> `syn` -> `post_n`。而后,在父网络 `Net` 中实例化两个子网络 `subnet1`、 `subnet2`,并与父网络其他部分连接,此时网络结构为:`inp1` -> `s1` -> `subnet1` -> `s2` -> `subnet2`。最后,在为 `pb.Network` 初始化时,传入子网络 `subnet1`、 `subnet2`。由此,父网络 `Net` 才能发现子网络组件。如果想取到 `Net` 内的 `subnet1` 对象,可通过索引其名字 `Net["Reused_Struct_0"]` 取到。 上述示例为一个二级嵌套网络,对于三级嵌套网络或更高(不推荐使用),可参考上述方式构建。 From 7ad909473933da8109a6c985003df4dadf5295a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:26:29 +0000 Subject: [PATCH 12/68] :rotating_light: auto fix by pre-commit hooks --- docs/Guide-of-PAIBox.md | 1 + paibox/backend/graphs.py | 4 +++- paibox/network.py | 2 +- tests/test_network.py | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 77ca3efc..7bbc06be 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -224,6 +224,7 @@ s1= pb.synapses.NoDecay(source=n1, dest=n2, weights=weight1, conn_type=pb.SynCon ``` 其权重以标量的形式储存。由于在运算时标量会随着矩阵进行广播,因此计算正确且节省了存储开销。 + - 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1, 2, 3, 4, 5]`, ```python diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index f2edec9f..c488c0d7 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -100,7 +100,9 @@ def build( for network in networks: # FIXME MAX_NESTED_LEVEL is set manually in DynSysGroup. # Do we need to take precautions in advance? - sub_nodes = network.nodes(level=network.MAX_NESTED_LEVEL, include_self=False) + sub_nodes = network.nodes( + level=network.MAX_NESTED_LEVEL, include_self=False + ) _nodes += sub_nodes.include(InputProj, NeuDyn).unique() _edges += sub_nodes.subset(SynSys).unique() diff --git a/paibox/network.py b/paibox/network.py index fa478559..fe9d5fd4 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -39,7 +39,7 @@ def update(self, **kwargs) -> None: TODO Prove that the operation sequence I->S->N can be divided into Ix->Sx->Nx->Iy->Sy->Ny & it has \ nothing to do with the network topology. - + The above expression cannot be completely established, and the condition needs to be met: the \ dependent synapses of the neuron are in the same subgraph. """ diff --git a/tests/test_network.py b/tests/test_network.py index a9ed0a20..2a742a32 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,4 +1,5 @@ from typing import Optional + import numpy as np import pytest @@ -145,7 +146,7 @@ def __init__(self): .unique() .not_subset(pb.DynSysGroup) ) - + assert isinstance(net[f"{Nested_Net_level_1.__name__}_0"], pb.Network) assert isinstance(net["Named_SubNet"], pb.Network) From 7868e394204035e2b952b64df580deb96b72d8e6 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 12 Mar 2024 14:19:44 +0800 Subject: [PATCH 13/68] =?UTF-8?q?=E2=9C=A8=20implemented=20recursive=20sea?= =?UTF-8?q?rch=20for=20all=20child=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 6 +---- paibox/base.py | 58 +++++++++++++++++++++++++++++++--------- paibox/network.py | 25 ++++++++--------- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index c488c0d7..0b663997 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -98,11 +98,7 @@ def build( _edges: Collector[EdgeName, EdgeType] = Collector() for network in networks: - # FIXME MAX_NESTED_LEVEL is set manually in DynSysGroup. - # Do we need to take precautions in advance? - sub_nodes = network.nodes( - level=network.MAX_NESTED_LEVEL, include_self=False - ) + sub_nodes = network.nodes(include_self=False, find_recursive=True) _nodes += sub_nodes.include(InputProj, NeuDyn).unique() _edges += sub_nodes.subset(SynSys).unique() diff --git a/paibox/base.py b/paibox/base.py index 7ae8d428..898c5998 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -23,7 +23,12 @@ class PAIBoxObject: def __init__(self, name: Optional[str] = None) -> None: self._name: str = self.unique_name(name) - def __eq__(self, other) -> bool: + def __eq__(self, other: "PAIBoxObject") -> bool: + if not isinstance(other, PAIBoxObject): + raise TypeError( + f"Cannot compare {type(self).__name__} with {type(other).__name__}." + ) + if self is other: return True @@ -59,44 +64,66 @@ def nodes( method: Literal["absolute", "relative"] = "absolute", level: int = -1, include_self: bool = True, + find_recursive: bool = False, ) -> Collector[str, "PAIBoxObject"]: - """Collect all the children nodes.""" - return self._find_nodes(method, level, include_self) + """Collect all child nodes. + + Args: + - method: the method to find the nodes. + - "absolute": the name of the node it is looking for will be `v.name`. + - "relative": the name will be its attribute name, `x` in `self.x = v`. + - level: the level at which the search ends. + - include_self: whether to include the current node itself. + - find_recursive: whether to search for nodes recursively until they are not found. + """ + return self._find_nodes(method, level, include_self, find_recursive) def _find_nodes( self, method: Literal["absolute", "relative"] = "absolute", level: int = -1, include_self: bool = True, - lid: int = 0, + find_recursive: bool = False, + _lid: int = 0, _paths: Optional[Set[_IdPathType]] = None, + _iter_termination: bool = False, ) -> Collector[str, "PAIBoxObject"]: if _paths is None: _paths = set() gather = Collector() + if include_self: if method == "absolute": gather[self.name] = self else: gather[""] = self - if (level > -1) and (lid >= level): - return gather + if find_recursive: + if _iter_termination: + return gather + else: + if (level > -1) and (_lid >= level): + return gather + + iter_termi = True # iteration termination flag def _find_nodes_absolute() -> None: - nonlocal gather, nodes + nonlocal gather, nodes, iter_termi for v in self.__dict__.values(): if isinstance(v, PAIBoxObject): + iter_termi = False _add_node2(self, v, _paths, gather, nodes) elif isinstance(v, NodeList): for v2 in v: if isinstance(v2, PAIBoxObject): + iter_termi = False _add_node2(self, v2, _paths, gather, nodes) elif isinstance(v, NodeDict): for v2 in v.values(): if isinstance(v2, PAIBoxObject): + iter_termi = False _add_node2(self, v2, _paths, gather, nodes) # finding nodes recursively @@ -106,24 +133,29 @@ def _find_nodes_absolute() -> None: method=method, level=level, include_self=include_self, - lid=lid + 1, + find_recursive=find_recursive, + _lid=_lid + 1, _paths=_paths, + _iter_termination=iter_termi, ) ) def _find_nodes_relative() -> None: - nonlocal gather, nodes + nonlocal gather, nodes, iter_termi for k, v in self.__dict__.items(): if isinstance(v, PAIBoxObject): + iter_termi = False _add_node1(self, k, v, _paths, gather, nodes) elif isinstance(v, NodeList): for i, v2 in enumerate(v): if isinstance(v2, PAIBoxObject): + iter_termi = False _add_node1(self, f"{k}-{str(i)}", v2, _paths, gather, nodes) elif isinstance(v, NodeDict): for k2, v2 in v.items(): if isinstance(v2, PAIBoxObject): + iter_termi = False _add_node1(self, f"{k}.{k2}", v2, _paths, gather, nodes) # finding nodes recursively @@ -132,8 +164,10 @@ def _find_nodes_relative() -> None: method=method, level=level, include_self=include_self, - lid=lid + 1, + find_recursive=find_recursive, + _lid=_lid + 1, _paths=_paths, + _iter_termination=iter_termi, ).items(): if k2: gather[f"{k1}.{k2}"] = v2 @@ -149,7 +183,7 @@ def _find_nodes_relative() -> None: def _add_node1( - obj: object, + obj: Any, k: str, v: PAIBoxObject, _paths: Set[_IdPathType], @@ -165,7 +199,7 @@ def _add_node1( def _add_node2( - obj: object, + obj: Any, v: PAIBoxObject, _paths: Set[_IdPathType], gather: Collector[str, PAIBoxObject], diff --git a/paibox/network.py b/paibox/network.py index fe9d5fd4..ab314151 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -1,5 +1,5 @@ import warnings -from typing import Callable, ClassVar, List, Optional, Tuple, Type, Union +from typing import Callable, List, Optional, Tuple, Type, Union import numpy as np from typing_extensions import TypeAlias @@ -18,7 +18,6 @@ class DynSysGroup(DynamicSys, Container): - MAX_NESTED_LEVEL: ClassVar[int] = 9 def __init__( self, @@ -43,7 +42,11 @@ def update(self, **kwargs) -> None: The above expression cannot be completely established, and the condition needs to be met: the \ dependent synapses of the neuron are in the same subgraph. """ - nodes = self.nodes(level=1, include_self=False).subset(DynamicSys).unique() + nodes = ( + self.nodes(include_self=False, find_recursive=True) + .subset(DynamicSys) + .unique() + ) for node in nodes.subset(Projection).values(): node(**kwargs) @@ -54,13 +57,12 @@ def update(self, **kwargs) -> None: for node in nodes.subset(NeuDyn).values(): node() - for node in ( - nodes.not_subset(Projection).not_subset(SynSys).not_subset(NeuDyn).values() - ): - node() - def reset_state(self) -> None: - nodes = self.nodes(level=1, include_self=False).subset(DynamicSys).unique() + nodes = ( + self.nodes(include_self=False, find_recursive=True) + .subset(DynamicSys) + .unique() + ) for node in nodes.subset(Projection).values(): node.reset_state() @@ -71,11 +73,6 @@ def reset_state(self) -> None: for node in nodes.subset(NeuDyn).values(): node.reset_state() - for node in ( - nodes.not_subset(Projection).not_subset(SynSys).not_subset(NeuDyn).values() - ): - node.reset_state() - def __call__(self, **kwargs) -> None: return self.update(**kwargs) From 868702e23c4a5cb55c81151a7165cbbd8dfcff18 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 12 Mar 2024 14:26:59 +0800 Subject: [PATCH 14/68] =?UTF-8?q?=E2=9C=85=20added=20tests=20for=20recursi?= =?UTF-8?q?ve=20search=20for=20nodes=20&=20removed=20useless=20test=20obje?= =?UTF-8?q?cts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 98 ++++++++++++++++------- tests/simulator/test_simulator.py | 52 +------------ tests/test_network.py | 125 ++++++------------------------ 3 files changed, 95 insertions(+), 180 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6b582148..14b3e65d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ -from typing import Any, List, NotRequired, TypedDict - +from typing import Any, List, Optional, TypedDict +from typing_extensions import NotRequired +import numpy as np import pytest import paibox as pb @@ -89,30 +90,6 @@ def __init__(self): self.probe1 = pb.Probe(self.n_list[1], "output", name="n2_out") -class MoreInput_Net(pb.DynSysGroup): - """Nested network, level 1. - n1 -> s1 -> n2 -> s2 -> n4 - - n3 -> s3 -> n4 - """ - - def __init__(self): - super().__init__() - self.n1 = pb.neuron.TonicSpiking(2, 3) - self.n2 = pb.neuron.TonicSpiking(2, 3) - self.s1 = pb.synapses.NoDecay( - self.n1, self.n2, conn_type=pb.SynConnType.All2All - ) - self.n3 = pb.neuron.TonicSpiking(2, 4) - self.n4 = pb.neuron.TonicSpiking(2, 3) - self.s2 = pb.synapses.NoDecay( - self.n2, self.n4, conn_type=pb.SynConnType.All2All - ) - self.s3 = pb.synapses.NoDecay( - self.n3, self.n4, conn_type=pb.SynConnType.All2All - ) - - class Network_with_multi_inodes_onodes(pb.Network): """ INP1 -> S1 -> N1 -> S2 -> N2 @@ -141,6 +118,66 @@ def __init__(self): ) +class Nested_Net_L1(pb.DynSysGroup): + """Level 1 nested network: pre_n -> syn -> post_n""" + + def __init__(self, name: Optional[str] = None): + super().__init__(name=name) + + self.pre_n = pb.LIF((10,), 10) + self.post_n = pb.LIF((10,), 10) + + w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) + self.syn = pb.NoDecay( + self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w + ) + + +class Nested_Net_L2(pb.DynSysGroup): + """Level 2 nested network: inp1 -> s1 -> Nested_Net_L1 -> s2 -> Nested_Net_L1""" + + def __init__(self, name: Optional[str] = None): + self.inp1 = pb.InputProj(1, shape_out=(10,)) + subnet1 = Nested_Net_L1() + subnet2 = Nested_Net_L1(name="Named_SubNet_L1_1") + self.s1 = pb.NoDecay( + self.inp1, + subnet1.pre_n, + conn_type=pb.SynConnType.One2One, + ) + self.s2 = pb.NoDecay( + subnet1.post_n, + subnet2.pre_n, + conn_type=pb.SynConnType.One2One, + ) + + super().__init__(subnet1, subnet2, name=name) + self.probe1 = pb.Probe(self.inp1, "spike") # won't be discovered in level 3 + + +class Nested_Net_L3(pb.DynSysGroup): + """Level 3 nested network: inp1 -> s1 -> Named_Nested_Net_L2""" + + def __init__(self): + self.inp1 = pb.InputProj(1, shape_out=(10,)) + subnet1 = Nested_Net_L2(name="Named_Nested_Net_L2") + + subnet1_of_subnet1 = subnet1[f"{Nested_Net_L1.__name__}_0"] + + self.s1 = pb.NoDecay( + self.inp1, + subnet1_of_subnet1.pre_n, + conn_type=pb.SynConnType.One2One, + ) + + super().__init__(subnet1) + + self.probe1 = pb.Probe(self.inp1, "spike") + self.probe2 = pb.Probe(subnet1_of_subnet1.pre_n, "spike") + self.probe3 = pb.Probe(subnet1_of_subnet1.pre_n, "voltage") + self.probe4 = pb.Probe(subnet1.s1, "output") + + @pytest.fixture(scope="class") def build_Input_to_N1(): return Input_to_N1() @@ -167,5 +204,10 @@ def build_multi_inodes_onodes(): @pytest.fixture(scope="class") -def build_MoreInput_Net(): - return MoreInput_Net() +def build_Nested_Net_L2(): + return Nested_Net_L2() + + +@pytest.fixture(scope="class") +def build_Nested_Net_L3(): + return Nested_Net_L3() diff --git a/tests/simulator/test_simulator.py b/tests/simulator/test_simulator.py index 267e2f64..935184ef 100644 --- a/tests/simulator/test_simulator.py +++ b/tests/simulator/test_simulator.py @@ -98,52 +98,6 @@ def __init__(self, n: int): self.n1_output = pb.Probe(self.n1, "spike") -class Nested_Net_level_1(pb.DynSysGroup): - """Level 1 nested network: pre_n -> syn -> post_n""" - - def __init__(self): - super().__init__() - - self.pre_n = pb.LIF((10,), 2, tick_wait_start=2) - self.post_n = pb.LIF((10,), 10, tick_wait_start=3) - - w = np.ones((10, 10), dtype=np.int8) - self.syn = pb.NoDecay( - self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w - ) - - self.probe_in_subnet = pb.Probe(self.pre_n, "spike") - - -class Nested_Net_level_2(pb.DynSysGroup): - """Level 2 nested network: -> s1 -> Nested_Net_level_1""" - - def __init__(self): - self.inp1 = pb.InputProj(None, shape_out=(10,)) - self.n1 = pb.LIF((10,), 2, tick_wait_start=1) - - subnet = Nested_Net_level_1() - - self.s1 = pb.NoDecay( - self.inp1, - self.n1, - conn_type=pb.SynConnType.One2One, - ) - self.s2 = pb.NoDecay( - self.n1, - subnet.pre_n, - conn_type=pb.SynConnType.One2One, - ) - - self.probe1 = pb.Probe(self.inp1, "spike") - self.probe2 = pb.Probe(self.n1, "spike") - self.probe3 = pb.Probe(self.n1, "voltage") - self.probe4 = pb.Probe(subnet.pre_n, "spike") - self.probe5 = pb.Probe(subnet.post_n, "spike") - - super().__init__(subnet) - - class TestSimulator: def test_probe(self): net = Net1(100) @@ -237,12 +191,12 @@ def test_sim_specify_inputs_2(self): sim.reset() - def test_sim_nested_net(self): - net = Nested_Net_level_2() + def test_sim_nested_net(self, build_Nested_Net_L3): + net = build_Nested_Net_L3 sim = pb.Simulator(net, start_time_zero=False) # The probes defined in the subnets cannot be discovered. - assert len(sim.probes) == 5 + assert len(sim.probes) == 4 net.inp1.input = np.ones((10,), dtype=np.int8) sim.run(20) diff --git a/tests/test_network.py b/tests/test_network.py index 2a742a32..ff8962a8 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np import pytest @@ -9,41 +7,7 @@ from paibox.node import NodeDict -class Nested_Net_level_1(pb.DynSysGroup): - """Level 1 nested network: pre_n -> syn -> post_n""" - - def __init__(self, name: Optional[str] = None): - super().__init__(name=name) - - self.pre_n = pb.LIF((10,), 10) - self.post_n = pb.LIF((10,), 10) - - w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) - self.syn = pb.NoDecay( - self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w - ) - - class TestNetwork_Components_Discover: - def test_flatten_hzynet(self, build_MoreInput_Net): - net = build_MoreInput_Net - - nodes1 = net.nodes(method="relative", level=1, include_self=True) - assert nodes1[""] == net - assert len(nodes1) == 8 - - # 2. Relative + include_self == False - nodes2 = net.nodes(method="relative", level=1, include_self=False) - assert len(nodes2) == 7 - - # 3. Absolute + include_self == True - nodes3 = net.nodes(method="absolute", level=1, include_self=True) - assert len(nodes3) == 8 - - # 4. Absolute + include_self == False - nodes4 = net.nodes(method="absolute", level=1, include_self=False) - assert len(nodes4) == 7 - def test_flatten_nodes(self, build_NotNested_Net): net = build_NotNested_Net @@ -72,7 +36,7 @@ def test_flatten_nodes(self, build_NotNested_Net): ) assert len(nodes4) == 3 - def test_nested_net_level_1(self, build_Network_with_container): + def test_nested_net_L1(self, build_Network_with_container): net = build_Network_with_container # 1. Relative + include_self == True @@ -105,28 +69,9 @@ def test_nested_net_level_1(self, build_Network_with_container): sim.run(10) sim.reset() - def test_nested_net_level_2(self): - class Nested_Net_level_2(pb.DynSysGroup): - """Level 2 nested network: inp1 -> s1 -> Nested_Net_level_1 -> s2 -> Nested_Net_level_1""" - - def __init__(self): - self.inp1 = pb.InputProj(1, shape_out=(10,)) - subnet1 = Nested_Net_level_1() - subnet2 = Nested_Net_level_1(name="Named_SubNet") - self.s1 = pb.NoDecay( - self.inp1, - subnet1.pre_n, - conn_type=pb.SynConnType.One2One, - ) - self.s2 = pb.NoDecay( - subnet1.post_n, - subnet2.pre_n, - conn_type=pb.SynConnType.One2One, - ) - - super().__init__(subnet1, subnet2) - - net = Nested_Net_level_2() + def test_nested_net_L2(self, build_Nested_Net_L2): + net: pb.Network = build_Nested_Net_L2 + nodes = net.nodes(level=1, include_self=False).subset(DynamicSys).unique() nodes_excluded = ( net.nodes(level=1, include_self=False) @@ -147,67 +92,41 @@ def __init__(self): .not_subset(pb.DynSysGroup) ) - assert isinstance(net[f"{Nested_Net_level_1.__name__}_0"], pb.Network) - assert isinstance(net["Named_SubNet"], pb.Network) + from .conftest import Nested_Net_L1 + + assert isinstance(net[f"{Nested_Net_L1.__name__}_0"], pb.Network) + assert isinstance(net["Named_SubNet_L1_1"], pb.Network) assert len(nodes) == 5 assert len(nodes_excluded) == 3 assert len(nodes2) == 3 + 3 * 2 assert len(nodes9) == len(nodes2) - def test_nested_net_level_3(self): - class Nested_Net_level_2(pb.DynSysGroup): - """Level 2 nested network: -> s1 -> Nested_Net_level_1""" + del Nested_Net_L1 - def __init__(self, n: pb.neuron.Neuron): - subnet = Nested_Net_level_1() - self.s1 = pb.NoDecay( - n, - subnet.pre_n, - conn_type=pb.SynConnType.One2One, - ) + def test_nested_net_L2_find_nodes_recursively(self, build_Nested_Net_L2): + net: pb.Network = build_Nested_Net_L2 - super().__init__(subnet) - - class Nested_Net_level_3(pb.DynSysGroup): - """Level 3 nested network: inp1 -> s1 -> n1 -> Nested_Net_level_2 -> s1 -> Nested_Net_level_1""" - - def __init__(self): - self.inp1 = pb.InputProj(1, shape_out=(10,)) - self.n1 = pb.LIF((10,), 10) - - net_level2 = Nested_Net_level_2(self.n1) - self.s1 = pb.NoDecay( - self.inp1, - self.n1, - conn_type=pb.SynConnType.One2One, - ) - - super().__init__(net_level2) - - net = Nested_Net_level_3() - nodes_excluded = ( - net.nodes(level=1, include_self=False) - .subset(DynamicSys) - .unique() - .not_subset(pb.DynSysGroup) - ) - nodes2 = ( - net.nodes(level=2, include_self=False) + nodes = ( + net.nodes(level=-1, include_self=False, find_recursive=True) .subset(DynamicSys) .unique() .not_subset(pb.DynSysGroup) ) - nodes3 = ( - net.nodes(level=3, include_self=False) + + assert len(nodes) == 3 + 3 * 2 + + def test_nested_net_L3_find_nodes_recursively(self, build_Nested_Net_L3): + net: pb.Network = build_Nested_Net_L3 + + nodes = ( + net.nodes(level=-1, include_self=False, find_recursive=True) .subset(DynamicSys) .unique() .not_subset(pb.DynSysGroup) ) - assert len(nodes_excluded) == 3 - assert len(nodes2) == 3 + 1 - assert len(nodes3) == 3 + 1 + 3 + assert len(nodes) == 2 + 3 + 2 * 3 class TestNetwork_Components_Oprations: From 0d646b4444c7bf62265fa6d36852e16a769f504e Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:09:03 +0800 Subject: [PATCH 15/68] =?UTF-8?q?=E2=9C=A8=20supported=202d=20convolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/base.py | 35 +++++- paibox/synapses/__init__.py | 10 +- paibox/synapses/base.py | 201 +++++++++++++++++++++++++++++++ paibox/synapses/conv_utils.py | 171 ++++++++++++++++++++++++++ paibox/synapses/synapses.py | 221 +++++++++++----------------------- paibox/synapses/transforms.py | 175 ++++++++++++++++++--------- 6 files changed, 601 insertions(+), 212 deletions(-) create mode 100644 paibox/synapses/base.py create mode 100644 paibox/synapses/conv_utils.py diff --git a/paibox/base.py b/paibox/base.py index 898c5998..52dcecf5 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,5 +1,7 @@ import sys -from typing import Any, Dict, List, Literal, Optional, Set, Tuple +import numpy as np +from typing import Any, ClassVar, Dict, List, Literal, Optional, Set, Tuple, Union +from numpy.typing import NDArray if sys.version_info >= (3, 10): from typing import TypeAlias @@ -10,6 +12,8 @@ from .generic import get_unique_name, is_name_unique from .mixin import ReceiveInputProj, StatusMemory, TimeRelatedNode from .node import NodeDict, NodeList +from .types import WeightType + __all__ = [] @@ -306,3 +310,32 @@ def unrolling_factor(self, factor: int) -> None: raise ValueError(f"'unrolling_factor' must be positive, but got {factor}.") self._unrolling_factor = factor + + +class SynSys(DynamicSys): + CFLAG_ENABLE_WP_OPTIMIZATION: ClassVar[bool] = True + """Compilation flag for weight precision optimization.""" + + @property + def weights(self) -> WeightType: + raise NotImplementedError + + @property + def weight_precision(self): + raise NotImplementedError + + @property + def connectivity(self) -> NDArray[Union[np.bool_, np.int8]]: + raise NotImplementedError + + @property + def n_axon_each(self) -> np.ndarray: + return np.sum(self.connectivity, axis=0) + + @property + def num_axon(self) -> int: + return np.count_nonzero(np.any(self.connectivity, axis=1)) + + @property + def num_dendrite(self) -> int: + return np.count_nonzero(np.any(self.connectivity, axis=0)) diff --git a/paibox/synapses/__init__.py b/paibox/synapses/__init__.py index 53355470..9b94bd66 100644 --- a/paibox/synapses/__init__.py +++ b/paibox/synapses/__init__.py @@ -1,6 +1,8 @@ -from .synapses import RIGISTER_MASTER_KEY_FORMAT +from .base import RIGISTER_MASTER_KEY_FORMAT +from .base import SynSys +from .synapses import FullConn as FullConn from .synapses import NoDecay as NoDecay -from .synapses import SynSys as SynSys -from .transforms import ConnType as SynConnType +from .synapses import Conv2d as Conv2d +from .transforms import GeneralConnType as GeneralConnType -__all__ = ["NoDecay", "SynSys", "SynConnType"] +__all__ = ["Conv2d", "FullConn", "NoDecay", "GeneralConnType"] diff --git a/paibox/synapses/base.py b/paibox/synapses/base.py new file mode 100644 index 00000000..81bff250 --- /dev/null +++ b/paibox/synapses/base.py @@ -0,0 +1,201 @@ +import numpy as np + +from typing import ClassVar, Literal, Optional, Tuple, Union +from paicorelib import HwConfig, WeightPrecision as WP + +from paibox.base import NeuDyn, SynSys +from paibox.exceptions import ShapeError +from paibox.neuron import Neuron +from paibox.projection import InputProj +from paibox.types import DataArrayType, SynOutType, WeightType + +from .conv_utils import _fm_ndim2_check +from .transforms import ( + Conv2dForward, + GeneralConnType as GConnType, + OneToOne, + AllToAll, + Identity, + MaskedLinear, + Transform, +) + +RIGISTER_MASTER_KEY_FORMAT = "{0}.output" + + +def _check_equal(num_in: int, num_out: int) -> int: + if num_in != num_out: + raise ShapeError( + f"The number of source & destination neurons must " + f"be equal, but {num_in} != {num_out}." + ) + + return num_in + + +class Synapses: + def __init__( + self, + source: Union[NeuDyn, InputProj], + dest: NeuDyn, + ) -> None: + self.source = source + self.dest = dest + + @property + def shape_in(self) -> Tuple[int, ...]: + return self.source.shape_out + + @property + def shape_out(self) -> Tuple[int, ...]: + return self.dest.shape_in + + @property + def num_in(self) -> int: + return self.source.num_out + + @property + def num_out(self) -> int: + return self.dest.num_in + + +class FullConnectedSyn(Synapses, SynSys): + def __init__( + self, + source: Union[NeuDyn, InputProj], + dest: NeuDyn, + name: Optional[str] = None, + ) -> None: + super().__init__(source, dest) + super(Synapses, self).__init__(name) + + self.set_memory("_synout", np.zeros((self.num_out,), dtype=np.int32)) + + # Register `self` for the destination `NeuDyn`. + dest.register_master(RIGISTER_MASTER_KEY_FORMAT.format(self.name), self) + + def __call__(self, *args, **kwargs) -> SynOutType: + return self.update(*args, **kwargs) + + def update(self, spike: Optional[np.ndarray] = None, *args, **kwargs) -> SynOutType: + # Retrieve the spike at index `timestamp` of the dest neurons + if self.dest.is_working: + if isinstance(self.source, InputProj): + synin = self.source.output.copy() if spike is None else spike + else: + idx = self.dest.timestamp % HwConfig.N_TIMESLOT_MAX + synin = self.source.output[idx].copy() if spike is None else spike + else: + # Retrieve 0 to the dest neurons if it is not working + synin = np.zeros_like(self.source.spike, dtype=np.bool_) + + self._synout = self.comm(synin).astype(np.int32) + return self._synout + + def reset_state(self, *args, **kwargs) -> None: + # TODO Add other initialization methods in the future. + self.reset_memory() # Call reset of `StatusMemory`. + + def _set_comm(self, comm: Transform) -> None: + self.comm = comm + + @property + def output(self) -> SynOutType: + return self._synout + + @property + def weights(self) -> WeightType: + return self.comm.weights + + @property + def weight_precision(self) -> WP: + return self.comm._get_wp(self.CFLAG_ENABLE_WP_OPTIMIZATION) + + @property + def connectivity(self): + """The connectivity matrix in `np.ndarray` format.""" + return self.comm.connectivity + + +class FullConnSyn(FullConnectedSyn): + def __init__( + self, + source: Union[NeuDyn, InputProj], + dest: NeuDyn, + weights: DataArrayType, + conn_type: GConnType, + name: Optional[str] = None, + ) -> None: + super().__init__(source, dest, name) + + if conn_type is GConnType.One2One: + comm = OneToOne(_check_equal(self.num_in, self.num_out), weights) + elif conn_type is GConnType.Identity: + if not isinstance(weights, (int, np.bool_, np.integer)): + raise TypeError( + f"Expected type int, np.bool_, np.integer, but got type {type(weights)}" + ) + comm = Identity(_check_equal(self.num_in, self.num_out), weights) + elif conn_type is GConnType.All2All: + comm = AllToAll((self.num_in, self.num_out), weights) + else: # MatConn + if not isinstance(weights, np.ndarray): + raise TypeError( + f"Expected type np.ndarray, but got type {type(weights)}" + ) + comm = MaskedLinear((self.num_in, self.num_out), weights) + + self._set_comm(comm) + + +class Conv2dSyn(FullConnectedSyn): + _spatial_ndim: ClassVar[int] = 2 + + def __init__( + self, + source: Union[Neuron, InputProj], + dest: Neuron, + kernel: np.ndarray, + stride: Tuple[int, int], + # padding: Tuple[int, int], + fm_order: Literal["CHW", "HWC"], + order: Literal["OIHW", "IOHW"], + name: Optional[str] = None, + ) -> None: + super().__init__(source, dest, name) + + if kernel.ndim != self._spatial_ndim + 2: + raise ShapeError( + f"The convolution kernel dimension must be {self._spatial_ndim + 2}, but got {kernel.ndim}." + ) + + if order == "IOHW": + _kernel = kernel.transpose(1, 0, 2, 3) + else: + _kernel = kernel.copy() + + # O,I,H,W + out_channels, in_channels, kernel_h, kernel_w = _kernel.shape + + # C,H,W + in_ch, in_h, in_w = _fm_ndim2_check(source._shape, fm_order) + out_ch, out_h, out_w = _fm_ndim2_check(dest._shape, fm_order) + + if in_ch != in_channels: + raise ShapeError(f"Input channels mismatch: {in_ch} != {in_channels}") + + if out_ch != out_channels: + raise ShapeError(f"Output channels mismatch: {out_ch} != {out_channels}") + + # If padding is considered, the implementation of convolution unrolling + # is extremely complex, so fix it. + padding = (0, 0) + + assert (in_h + 2 * padding[0] - kernel_h) // stride[0] + 1 == out_h + assert (in_w + 2 * padding[1] - kernel_w) // stride[1] + 1 == out_w + + comm = Conv2dForward( + (in_h, in_w), (out_h, out_w), _kernel, stride, padding, fm_order + ) + + self._set_comm(comm) diff --git a/paibox/synapses/conv_utils.py b/paibox/synapses/conv_utils.py new file mode 100644 index 00000000..1f2f9668 --- /dev/null +++ b/paibox/synapses/conv_utils.py @@ -0,0 +1,171 @@ +from itertools import repeat +from functools import partial +from typing import Any, Iterable, Literal, Tuple, TypeVar, Union + +import numpy as np +from numpy.typing import NDArray +from paibox.exceptions import ShapeError +from paibox.types import SynOutType, WeightType + + +T = TypeVar("T") + +_TupleAnyType = Union[T, Tuple[T, ...]] +_Tuple1Type = Union[T, Tuple[T]] +_Tuple2Type = Union[T, Tuple[T, T]] +_Tuple3Type = Union[T, Tuple[T, T, T]] +_Tuple4Type = Union[T, Tuple[T, T, T, T]] + +_SizeAnyType = _TupleAnyType[int] +_Size1Type = _Tuple1Type[int] +_Size2Type = _Tuple2Type[int] +_Size3Type = _Tuple3Type[int] +_Size4Type = _Tuple4Type[int] + +Size2Type = Tuple[int, int] + + +def _ntuple(x, n: int) -> Tuple[Any, ...]: + if isinstance(x, Iterable): + return tuple(x) + + return tuple(repeat(x, n)) + + +_single = partial(_ntuple, n=1) +_pair = partial(_ntuple, n=2) +_triple = partial(_ntuple, n=3) +_quadruple = partial(_ntuple, n=4) + + +def _fm_ndim1_check( + fm_shape: _TupleAnyType, fm_order: Literal["CHW", "HWC"] +) -> Size2Type: + if len(fm_shape) < 1 or len(fm_shape) > 2: + raise ShapeError() + + if len(fm_shape) == 1: + channels, l = (1, *fm_shape) + else: + if fm_order is "CHW": + channels, l = fm_shape + else: + l, channels = fm_shape + + return channels, l + + +def _fm_ndim2_check( + fm_shape: _TupleAnyType, fm_order: Literal["CHW", "HWC"] +) -> Tuple[int, int, int]: + if len(fm_shape) < 2 or len(fm_shape) > 3: + raise ShapeError() + + if len(fm_shape) == 2: + channels, h, w = (1, *fm_shape) + else: + if fm_order is "CHW": + channels, h, w = fm_shape + else: + h, w, channels = fm_shape + + return channels, h, w + + +def _conv2d_unroll( + in_shape: Size2Type, + out_shape: Size2Type, + kernel: WeightType, + stride: Size2Type, + # padding: Size2Type, +) -> WeightType: + """Unroll the convolution kernel of 2d convolution into a matrix.""" + cout, cin, kh, kw = kernel.shape + + ih, iw = in_shape + oh, ow = out_shape + in_size = ih * iw + out_size = oh * ow + + w_unrolled = np.zeros((cin * in_size, cout * out_size), dtype=kernel.dtype) + zeros_image = np.zeros((cin * ih, iw * cout, out_size), dtype=kernel.dtype) + + for i in range(oh): + for j in range(ow): + for ch_idx in np.ndindex(kernel.shape[:2]): + # [0] -> o_ch, [1] -> i_ch + zeros_image[ + i * stride[0] + + ch_idx[1] * ih : i * stride[0] + + ch_idx[1] * ih + + kh, + j * stride[1] + + ch_idx[0] * iw : j * stride[1] + + ch_idx[0] * iw + + kw, + i * ow + j, + ] = kernel[ch_idx[0], ch_idx[1], :, :] + + t = ( + zeros_image[:, :, i * ow + j] + .reshape(cin * ih, cout, iw) + .transpose(1, 0, 2) + ) + for o_ch in range(cout): + w_unrolled[:, i * ow + j + o_ch * out_size] = t[o_ch].flatten() + + return w_unrolled + + +def _conv2d_faster( + x_chw: np.ndarray, + out_shape: Size2Type, + kernel: WeightType, + stride: Size2Type, + padding: Size2Type, +) -> SynOutType: + xc, xh, xw = x_chw.shape + + # (O, I, H, W) + cout, cin, kh, kw = kernel.shape + assert xc == cin + + x_padded = np.pad( + x_chw, + ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), + mode="constant", + ) + + assert (xh + padding[0] * 2 - kh) // stride[0] + 1 == out_shape[0] + assert (xw + padding[1] * 2 - kw) // stride[1] + 1 == out_shape[1] + + # kernel: (cout, cin, kh, kw) -> (cin*kh*kw, cout) + col_kernel = kernel.transpose(1, 2, 3, 0).reshape(-1, cout) + + # padded: (cin, xh+2*p[0]-kh, xw+2*p[1]-kw) -> (oh*ow, cin*kh*kw) + col_fm = _im2col(x_padded, out_shape[0], out_shape[1], kh, kw, stride) + + # out = np.zeros((cout,) + out_shape, dtype=np.int64) + # (oh*ow, cin*kh*kw) * (cin*kh*kw, cout) = (oh*ow, cout) + out = col_fm @ col_kernel # + self.bias + + # (oh*ow, cout) -> (oh, ow, cout) -> (cout, oh, ow) + out = out.reshape(out_shape + (cout,)).transpose(2, 0, 1) + + return out.astype(np.int32) + + +def _im2col( + x_padded: np.ndarray, oh: int, ow: int, kh: int, kw: int, stride: Size2Type +) -> NDArray[np.int64]: + cols = np.zeros((oh * ow, x_padded.shape[0] * kh * kw), dtype=np.int64) + + _, ph, pw = x_padded.shape + + idx = 0 + for i in range(0, ph - kh + 1, stride[0]): + for j in range(0, pw - kw + 1, stride[1]): + cols[idx] = x_padded[:, i : i + kh, j : j + kw].reshape(-1) + idx += 1 + + return cols diff --git a/paibox/synapses/synapses.py b/paibox/synapses/synapses.py index f6c054a6..67718c5e 100644 --- a/paibox/synapses/synapses.py +++ b/paibox/synapses/synapses.py @@ -1,106 +1,28 @@ -from typing import ClassVar, Optional, Tuple, Union - +import warnings import numpy as np -from numpy.typing import NDArray -from paicorelib import HwConfig -from paicorelib import WeightPrecision as WP - -from paibox.base import DynamicSys, NeuDyn -from paibox.exceptions import ShapeError -from paibox.projection import InputProj -from paibox.types import DataArrayType, WeightType - -from .transforms import * - -__all__ = ["NoDecay"] - -RIGISTER_MASTER_KEY_FORMAT = "{0}.output" - - -class Synapses: - def __init__( - self, - source: Union[NeuDyn, InputProj], - dest: NeuDyn, - conn_type: ConnType, - ) -> None: - """ - Args: - - source: the source group of neurons. - - dest: the destination group of neurons. - - conn_type: the type of connection. - """ - self.source = source - self.dest = dest - self._check(conn_type) - - def _check(self, conn_type: ConnType) -> None: - if conn_type is ConnType.One2One or conn_type is ConnType.Identity: - if self.num_in != self.num_out: - raise ShapeError( - f"The number of source & destination neurons must " - f"be equal, but {self.num_in} != {self.num_out}." - ) - - @property - def shape_in(self) -> Tuple[int, ...]: - return self.source.shape_out - - @property - def shape_out(self) -> Tuple[int, ...]: - return self.dest.shape_in - - @property - def num_in(self) -> int: - return self.source.num_out - - @property - def num_out(self) -> int: - return self.dest.num_in - - -class SynSys(Synapses, DynamicSys): - CFLAG_ENABLE_WP_OPTIMIZATION: ClassVar[bool] = True - """Compilation flag for weight precision optimization.""" - - def __call__(self, *args, **kwargs) -> NDArray[np.int32]: - return self.update(*args, **kwargs) - - @property - def weights(self) -> WeightType: - raise NotImplementedError - @property - def weight_precision(self) -> WP: - raise NotImplementedError +from typing import Literal, Optional, Union - @property - def connectivity(self) -> NDArray[Union[np.bool_, np.int8]]: - raise NotImplementedError - - @property - def n_axon_each(self) -> np.ndarray: - return np.sum(self.connectivity, axis=0) - - @property - def num_axon(self) -> int: - return np.count_nonzero(np.any(self.connectivity, axis=1)) +from paibox.base import NeuDyn +from paibox.neuron import Neuron +from paibox.projection import InputProj +from paibox.types import DataArrayType - @property - def num_dendrite(self) -> int: - return np.count_nonzero(np.any(self.connectivity, axis=0)) +from .base import Conv2dSyn, FullConnSyn +from .conv_utils import _pair, _Size2Type +from .transforms import GeneralConnType as GConnType +__all__ = ["FullConn", "Conv2d"] -class NoDecay(SynSys): - """Synapses model with no decay.""" +class FullConn(FullConnSyn): def __init__( self, source: Union[NeuDyn, InputProj], dest: NeuDyn, weights: DataArrayType = 1, *, - conn_type: ConnType = ConnType.MatConn, + conn_type: GConnType = GConnType.MatConn, name: Optional[str] = None, ) -> None: """ @@ -111,68 +33,69 @@ def __init__( - conn_type: the type of connection. - name: name of this synapses. Optional. """ - super().__init__(source, dest, conn_type) - super(Synapses, self).__init__(name) - - if conn_type is ConnType.One2One: - self.comm = OneToOne(self.num_in, weights) - elif conn_type is ConnType.Identity: - if not isinstance(weights, (int, np.integer)): - raise TypeError( - f"Expected type int, np.integer, but got type {type(weights)}" - ) - - self.comm = Identity(self.num_in, weights) - elif conn_type is ConnType.All2All: - self.comm = AllToAll((self.num_in, self.num_out), weights) - else: # MatConn - if not isinstance(weights, np.ndarray): - raise TypeError( - f"Expected type int, np.integer or np.ndarray, but got type {type(weights)}" - ) - - self.comm = MaskedLinear((self.num_in, self.num_out), weights) - - self.weights.setflags(write=False) - self.set_memory("_synout", np.zeros((self.num_out,), dtype=np.int32)) + super().__init__(source, dest, weights, conn_type, name) - # Register `self` for the destination `NeuDyn`. - dest.register_master(RIGISTER_MASTER_KEY_FORMAT.format(self.name), self) - def update( - self, spike: Optional[np.ndarray] = None, *args, **kwargs - ) -> NDArray[np.int32]: - # Retrieve the spike at index `timestamp` of the dest neurons - if self.dest.is_working: - if isinstance(self.source, InputProj): - synin = self.source.output.copy() if spike is None else spike - else: - idx = self.dest.timestamp % HwConfig.N_TIMESLOT_MAX - synin = self.source.output[idx].copy() if spike is None else spike - else: - # Retrieve 0 to the dest neurons if it is not working - synin = np.zeros_like(self.source.spike, dtype=np.bool_) - - self._synout = self.comm(synin).astype(np.int32) - return self._synout - - def reset_state(self, *args, **kwargs) -> None: - # TODO Add other initialization methods in the future. - self.reset_memory() # Call reset of `StatusMemory`. +class NoDecay(FullConn): + def __init__( + self, + source: Union[NeuDyn, InputProj], + dest: NeuDyn, + weights: DataArrayType = 1, + *, + conn_type: GConnType = GConnType.MatConn, + name: Optional[str] = None, + ) -> None: + warnings.warn( + "The `NoDecay` class will be deprecated in future versions, " + "use `FullConn` instead.", + DeprecationWarning, + ) - @property - def output(self) -> NDArray[np.int32]: - return self._synout + super().__init__(source, dest, weights, conn_type=conn_type, name=name) - @property - def weights(self): - return self.comm.weights - @property - def weight_precision(self) -> WP: - return self.comm._get_wp(self.CFLAG_ENABLE_WP_OPTIMIZATION) +class Conv2d(Conv2dSyn): + def __init__( + self, + source: Union[Neuron, InputProj], + dest: Neuron, + kernel: np.ndarray, + *, + stride: _Size2Type = 1, + # padding: _Size2Type = 0, + fm_order: Literal["CHW", "HWC"] = "CHW", + kernel_order: Literal["OIHW", "IOHW"] = "OIHW", + name: Optional[str] = None, + ) -> None: + """2d convolution synapses in fully-unrolled format. - @property - def connectivity(self): - """The connectivity matrix in `np.ndarray` format.""" - return self.comm.connectivity + Arguments: + - source: source neuron(s). The dimensions need to be expressed explicitly as (C, H, W) or \ + (H, W, C). The feature map dimension order is specified by `fm_order`. + - dest: destination neuron(s). + - kernel: convolution kernel. Its dimension order must be (O,I,H,W) or (I,O,H,W), depending \ + on the argument `kernel_order`. + - stride: the step size of the kernel sliding. It can be a scalar or a tuple of 2 integers. + - fm_order: dimension order of feature map. The order of input & output feature maps must be\ + consistent, (C, H, W) or (H, W, C). + - kernel_order: dimension order of kernel, (O, I, H, W) or (I, O, H, W). (O, I, H, W) stands\ + for (output channels, input channels, height, width). + - name: name of the 2d convolution. Optional. + """ + if fm_order not in ("CHW", "HWC"): + raise ValueError(f"Unknown feature map order '{fm_order}'.") + + if kernel_order not in ("OIHW", "IOHW"): + raise ValueError(f"Unknown kernel order '{kernel_order}'.") + + super().__init__( + source, + dest, + kernel, + _pair(stride), + # _pair(padding), + fm_order, + kernel_order, + name=name, + ) diff --git a/paibox/synapses/transforms.py b/paibox/synapses/transforms.py index 800b9108..94d22c7c 100644 --- a/paibox/synapses/transforms.py +++ b/paibox/synapses/transforms.py @@ -1,15 +1,23 @@ from enum import Enum, auto, unique -from typing import Tuple, Type, Union +from typing import Literal, Type, Union import numpy as np from numpy.typing import NDArray from paicorelib import WeightPrecision as WP +from .conv_utils import _conv2d_faster, _conv2d_unroll, Size2Type from paibox.exceptions import ShapeError -from paibox.types import DataArrayType, IntScalarType, WeightType +from paibox.types import DataArrayType, IntScalarType, SynOutType, WeightType from paibox.utils import is_shape -__all__ = ["ConnType", "OneToOne", "AllToAll", "Identity", "MaskedLinear"] +__all__ = [ + "GeneralConnType", + "OneToOne", + "AllToAll", + "Identity", + "MaskedLinear", + "Conv2dForward", +] MAX_INT1 = np.int8(1) @@ -22,8 +30,14 @@ MIN_INT8 = np.iinfo(np.int8).min -@unique class ConnType(Enum): + """Basic connection enum type.""" + + pass + + +@unique +class GeneralConnType(ConnType): MatConn = auto() """General matrix connection.""" @@ -59,10 +73,13 @@ def _get_weight_precision(weight: np.ndarray, enable_wp_opt: bool) -> WP: class Transform: - weights: WeightType - """The actual weights in synapse. Must stored in `np.int8` format.""" + def __init__(self, weights: WeightType) -> None: + self.weights = weights + """The actual weights in synapse. Must stored in `np.int8` format.""" + self.weights.setflags(write=False) - def __call__(self, *args, **kwargs) -> NDArray[np.int32]: + def __call__(self, *args, **kwargs) -> SynOutType: + """Ensure that in all subclasses, the output dimensions are (M,).""" raise NotImplementedError def _get_wp(self, enable_wp_opt: bool) -> WP: @@ -89,31 +106,33 @@ def __init__(self, num: int, weights: DataArrayType) -> None: Arguments: - num: number of neurons. - weights: synaptic weights. The shape must be a scalar or array (num,). - If `weights` is a scalar(ndim = 0), the connectivity matrix will be: - [[x, 0, 0] - [0, x, 0] - [0, 0, x]] - - Or `weights` is an array(ndim = 1), [x, y, z] corresponding to the weights of \ - the post-neurons respectively. The connectivity matrix will be: - [[x, 0, 0] - [0, y, 0] - [0, 0, z]] + - weights is a scalar(ndim = 0), the connectivity matrix will be: + [[x, 0, 0] + [0, x, 0] + [0, 0, x]] + - weights is an array(ndim = 1), [x, y, z] corresponding to the weights \ + of the post-neurons respectively. The connectivity matrix will be: + [[x, 0, 0] + [0, y, 0] + [0, 0, z]] """ self.num = num if isinstance(weights, np.ndarray) and not is_shape(weights, (num,)): raise ShapeError( - f"Excepted shape is ({num},), but we got shape {weights.shape}" + f"Expected shape is ({num},), but we got shape {weights.shape}" ) # The ndim of weights = 0 or 1. - self.weights = np.asarray(weights, dtype=np.int8) + _w = np.asarray(weights, dtype=np.int8) + + if _w.ndim not in (0, 1): + raise ShapeError(f"The ndim of weights must be 0 or 1, but got {_w.ndim}.") - if not self.weights.ndim in (0, 1): - raise ShapeError(f"The ndim of weights must be 0 or 1.") + super().__init__(_w) - def __call__(self, x: np.ndarray, *args, **kwargs) -> NDArray[np.int32]: + def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: + # (N,) * (N,) -> (N,) output = x * self.weights.copy() return output.astype(np.int32) @@ -138,48 +157,48 @@ def __init__(self, num: int, scaling_factor: IntScalarType = 1) -> None: class AllToAll(Transform): - def __init__(self, conn_size: Tuple[int, int], weights: DataArrayType) -> None: + def __init__(self, conn_size: Size2Type, weights: DataArrayType) -> None: """ Arguments: - - num_in: number of source neurons. - - num_out: number of destination neurons. + - conn_size: size of connections. - weights: synaptic weights. The shape must be a scalar or a matrix. - If `weights` is a scalar(ndim = 0), the connectivity matrix will be: - [[x, x, x] - [x, x, x] - [x, x, x]] - - Or `weights` is a matrix(ndim = 2), then the connectivity matrix will be: - [[a, b, c] - [d, e, f] - [g, h, i]] + - when weights is a scalar(ndim = 0), the connectivity matrix will be: \ + x * I + - when weights is a matrix(ndim = 2), the connectivity matrix will be: \ + [[a, b, c] + [d, e, f] + [g, h, i]] """ self.conn_size = conn_size if isinstance(weights, np.ndarray) and not is_shape(weights, conn_size): raise ShapeError( - f"Excepted shape is {conn_size}, but we got shape {weights.shape}" + f"Expected shape is {conn_size}, but we got shape {weights.shape}" ) - self.weights = np.asarray(weights, dtype=np.int8) + _w = np.asarray(weights, dtype=np.int8) - if not self.weights.ndim in (0, 2): - raise ShapeError(f"The ndim of weights must be 0 or 2.") + if _w.ndim not in (0, 2): + raise ShapeError(f"The ndim of weights must be 0 or 2, but got {_w.ndim}.") - def __call__(self, x: np.ndarray, *args, **kwargs) -> NDArray[np.int32]: + super().__init__(_w) + + def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: """ - - When weights is a scalar, the output is a scalar. (Risky, DO NOT USE) - - When weights is a matrix, the output is the dot product of `x` & `weights`. + NOTE: + - When weights is a scalar, the output is a scalar (sum * w) & repeated \ + `conn_size[1]` times. + - When weights is a matrix, the output is the dot product of `x` & weights. """ if self.weights.ndim == 0: sum_x = np.sum(x, axis=None, dtype=np.int32) - output = self.weights * np.full((self.conn_size[1],), sum_x, dtype=np.int32) - # Risky - # output = self.weights * sum_x + # (M,) + output = np.full((self.conn_size[1],), self.weights * sum_x, dtype=np.int32) else: + # (N,) @ (N, M) -> (M,) output = x @ self.weights.copy().astype(np.int32) - return output.astype(np.int32) + return output @property def connectivity(self): @@ -195,25 +214,19 @@ def connectivity(self): class MaskedLinear(Transform): def __init__( self, - conn_size: Tuple[int, int], + conn_size: Size2Type, weights: np.ndarray, ) -> None: - """ - Arguments: - - conn: connector. Only support `MatConn`. - - weights: unmasked weights. - """ - self.conn_size = conn_size - - if not is_shape(weights, self.conn_size): + if not is_shape(weights, conn_size): raise ShapeError( - f"Excepted shape is {conn_size}, but we got {weights.shape}" + f"Expected shape is {conn_size}, but we got {weights.shape}" ) - # Element-wise Multiplication - self.weights = np.asarray(weights, dtype=np.int8) + _w = np.asarray(weights, dtype=np.int8) + super().__init__(_w) - def __call__(self, x: np.ndarray, *args, **kwargs) -> NDArray[np.int32]: + def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: + # (N,) @ (N, M) -> (M,) output = x @ self.weights.copy().astype(np.int32) return output.astype(np.int32) @@ -221,3 +234,49 @@ def __call__(self, x: np.ndarray, *args, **kwargs) -> NDArray[np.int32]: @property def connectivity(self): return self.weights.astype(self.conn_dtype) + + +class Conv2dForward(Transform): + def __init__( + self, + in_shape: Size2Type, + out_shape: Size2Type, + kernel: np.ndarray, + stride: Size2Type, + padding: Size2Type, + fm_order: Literal["CHW", "HWC"], + ) -> None: + self.in_shape = in_shape + self.out_shape = out_shape + self.stride = stride + self.padding = padding + self.fm_order = fm_order + + _w = kernel.astype(np.int8) + super().__init__(_w) + + def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: + cin = self.weights.shape[1] + + if self.fm_order == "HWC": + # (N,) -> (H, W, C) -> (C, H, W) + _x = x.reshape(self.in_shape + (cin,)) + _x = _x.transpose(2, 0, 1) + else: + _x = x.reshape((cin,) + self.in_shape) + + o_conv2d = _conv2d_faster( + _x, + self.out_shape, + self.weights, + self.stride, + self.padding, + ) + + return o_conv2d.flatten() + + @property + def connectivity(self): + return _conv2d_unroll( + self.in_shape, self.out_shape, self.weights, self.stride + ).astype(self.conn_dtype) From 19f290faff164b16f35806d27c9bc8de1fb3e906 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:10:07 +0800 Subject: [PATCH 16/68] =?UTF-8?q?=E2=9C=A8=20improved=20typing=20&=20libra?= =?UTF-8?q?ries=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/__init__.py | 43 +++++++++++++++++++++------------ paibox/backend/conf_template.py | 16 ++++++++++-- paibox/backend/mapper.py | 2 +- paibox/collector.py | 4 +-- paibox/neuron/__init__.py | 2 +- paibox/neuron/base.py | 2 +- paibox/projection.py | 21 +++++++--------- paibox/simulator/encoder.py | 6 ++--- paibox/tools.py | 13 ++++++++++ paibox/types.py | 13 +++------- paibox/utils.py | 5 +--- 11 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 paibox/tools.py diff --git a/paibox/__init__.py b/paibox/__init__.py index 60c75a9b..ffe52dc7 100644 --- a/paibox/__init__.py +++ b/paibox/__init__.py @@ -10,22 +10,10 @@ from .projection import InputProj as InputProj from .simulator import Probe as Probe from .simulator import Simulator as Simulator +from .synapses import Conv2d as Conv2d +from .synapses import FullConn as FullConn from .synapses import NoDecay as NoDecay -from .synapses import SynConnType as SynConnType - -__all__ = [ - "Mapper", - "DynSysGroup", - "Network", - "NodeDict", - "NodeList", - "InputProj", - "Simulator", - "SynConnType", - "Probe", - "BACKEND_CONFIG", - "FRONTEND_ENV", -] +from .synapses import GeneralConnType as SynConnType from importlib.metadata import version @@ -33,3 +21,28 @@ __version__ = version("paibox") except Exception: __version__ = None + +from paibox import tools + +# Minimum required version of paicorelib +__plib_minimum_version__ = "0.0.13" + +try: + import paicorelib as plib + + if hasattr(plib, "get_version"): # For plib <= 0.0.12 + raise ImportError( + tools.PLIB_UPDATE_INTRO.format( + __plib_minimum_version__, ".".join(map(str, plib.get_version())) + ) + ) from None + + if plib.__version__ < __plib_minimum_version__: # For plib > 0.0.12 + raise ImportError( + tools.PLIB_UPDATE_INTRO.format(__plib_minimum_version__, plib.__version__) + ) from None + + del tools, plib + +except ModuleNotFoundError: + raise ModuleNotFoundError(tools.PLIB_INSTALL_INTRO) from None diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index 7c881254..7b6e3709 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -23,8 +23,9 @@ WeightPrecision, get_replication_id, ) + +from paicorelib.framelib import types from paicorelib.framelib.frame_gen import OfflineFrameGen -from paicorelib.framelib.types import FRAME_DTYPE, FrameArrayType from paicorelib.framelib.utils import np2bin, np2npy, np2txt from typing_extensions import NotRequired, TypeAlias @@ -33,6 +34,17 @@ from .context import _BACKEND_CONTEXT from .graphs_types import NodeName +# Prevent import errors caused by changes in type definitions in paicorelib. +if hasattr(types, "FRAME_DTYPE"): + FRAME_DTYPE = types.FRAME_DTYPE +else: + FRAME_DTYPE = np.uint64 + +if hasattr(types, "FrameArrayType"): + FrameArrayType = types.FrameArrayType +else: + FrameArrayType = NDArray[FRAME_DTYPE] + class CoreConfig(NamedTuple): """Configurations of core.""" @@ -284,7 +296,7 @@ def gen_config_frames_by_coreconf( - format: `txt`, `bin`, or `npy`. `bin` & `npy` are recommended. """ - def _write_to_f(name: str, array: np.ndarray) -> None: + def _write_to_f(name: str, array: FrameArrayType) -> None: nonlocal fp, format _fp = fp / (name + f".{format}") diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index de171248..6700801d 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -132,7 +132,7 @@ def compile( occupied cores, or both. The default is specified by the corresponding compilation option in the\ backend configuration item (`both` by default). - Return: compiled network information in dictionary form. + Return: network information after compilation in dictionary format. """ if weight_bit_optimization is not None: set_cflag(enable_wp_opt=weight_bit_optimization) diff --git a/paibox/collector.py b/paibox/collector.py index bf15c24e..b8f41359 100644 --- a/paibox/collector.py +++ b/paibox/collector.py @@ -30,7 +30,7 @@ def update( ) -> Union["Collector[_KT, _VT]", "Collector[_KT, _T]"]: if not isinstance(other, (dict, list, tuple)): raise TypeError( - f"Excepted a dict, list or sequence, but we got {other}, type {type(other)}" + f"Expected a dict, list or sequence, but we got {other}, type {type(other)}" ) if isinstance(other, dict): @@ -75,7 +75,7 @@ def __sub__( ) -> Union["Collector[_KT, _VT]", "Collector[str, _T]"]: if not isinstance(other, (dict, list, tuple)): raise TypeError( - f"Excepted a dict, list or sequence, but we got {other}, type {type(other)}" + f"Expected a dict, list or sequence, but we got {other}, type {type(other)}" ) gather = type(self)(self) diff --git a/paibox/neuron/__init__.py b/paibox/neuron/__init__.py index 3b2a4223..59b21939 100644 --- a/paibox/neuron/__init__.py +++ b/paibox/neuron/__init__.py @@ -1,4 +1,4 @@ -from .base import Neuron as Neuron +from .base import Neuron from .neurons import IF as IF from .neurons import LIF as LIF from .neurons import PhasicSpiking as PhasicSpiking diff --git a/paibox/neuron/base.py b/paibox/neuron/base.py index 6f47110e..741ea50e 100644 --- a/paibox/neuron/base.py +++ b/paibox/neuron/base.py @@ -97,7 +97,7 @@ def _neuronal_charge( if self.synaptic_integration_mode is SIM.MODE_STOCHASTIC: raise NotImplementedError( - f"Mode {SIM.MODE_STOCHASTIC.name} not implemented." + f"Mode {SIM.MODE_STOCHASTIC.name} is not implemented." ) else: if incoming_v.ndim == 2: diff --git a/paibox/projection.py b/paibox/projection.py index 7e1cf62b..ca0d4b1f 100644 --- a/paibox/projection.py +++ b/paibox/projection.py @@ -60,7 +60,7 @@ def __init__( self._num_input = input self._func_input = _func_bypass - self._shape_out = as_shape(shape_out) + self._shape = as_shape(shape_out) self.keep_shape = keep_shape self.set_memory("_inner_spike", np.zeros((self.num_out,), dtype=np.bool_)) @@ -71,16 +71,15 @@ def update(self, **kwargs) -> SpikeType: if isinstance(_spike, (int, np.bool_, np.integer)): self._inner_spike = np.full((self.num_out,), _spike, dtype=np.bool_) elif isinstance(_spike, np.ndarray): - try: - self._inner_spike = _spike.reshape((self.num_out,)).astype(np.bool_) - except ValueError: + if shape2num(_spike.shape) != self.num_out: raise ShapeError( - f"Cannot reshape input value from {_spike.shape} to ({self.num_out},)." + f"Cannot reshape output value from {_spike.shape} to ({self.num_out},)." ) + self._inner_spike = _spike.flatten().astype(np.bool_) else: - # Should be never + # should never be reached raise TypeError( - f"Excepted type int, np.bool_, np.integer or np.ndarray, " + f"Expected type int, np.bool_, np.integer or np.ndarray, " f"but got {_spike}, type {type(_spike)}." ) @@ -113,7 +112,7 @@ def num_in(self) -> int: @property def num_out(self) -> int: - return shape2num(self._shape_out) + return shape2num(self._shape) @property def shape_in(self) -> Tuple[int, ...]: @@ -121,7 +120,7 @@ def shape_in(self) -> Tuple[int, ...]: @property def shape_out(self) -> Tuple[int, ...]: - return self._shape_out + return self._shape @property def input(self): @@ -132,7 +131,7 @@ def input(self, value: DataType) -> None: """Set the input at the beginning of running the simulation.""" if not isinstance(value, (int, np.bool_, np.integer, np.ndarray)): raise TypeError( - f"Excepted type int, np.bool_, np.integer or np.ndarray, " + f"Expected type int, np.bool_, np.integer or np.ndarray, " f"but got {value}, type {type(value)}" ) @@ -167,8 +166,6 @@ def _call_with_ctx(f: Callable[..., DataType], *args, **kwargs) -> DataType: try: ctx = _FRONTEND_CONTEXT.get_ctx() bound = inspect.signature(f).bind(*args, **ctx, **kwargs) - # warnings.warn(_input_deprecate_msg, UserWarning) return f(*bound.args, **bound.kwargs) - except TypeError: return f(*args, **kwargs) diff --git a/paibox/simulator/encoder.py b/paibox/simulator/encoder.py index 3f5d37d5..669bdd85 100644 --- a/paibox/simulator/encoder.py +++ b/paibox/simulator/encoder.py @@ -21,7 +21,7 @@ def __init__( seed: Optional[int] = None, name: Optional[str] = None, ) -> None: - self._shape_out = as_shape(shape_out) + self._shape = as_shape(shape_out) super().__init__(name) self.rng = self._get_rng(seed) @@ -32,7 +32,7 @@ def _get_rng(self, seed: Optional[int] = None) -> np.random.RandomState: @property def num_out(self) -> int: - return shape2num(self._shape_out) + return shape2num(self._shape) @property def shape_in(self) -> Tuple[int, ...]: @@ -40,7 +40,7 @@ def shape_in(self) -> Tuple[int, ...]: @property def shape_out(self) -> Tuple[int, ...]: - return self._shape_out + return self._shape class StatelessEncoder(Encoder): diff --git a/paibox/tools.py b/paibox/tools.py new file mode 100644 index 00000000..12fa09e6 --- /dev/null +++ b/paibox/tools.py @@ -0,0 +1,13 @@ +_PLIB_BASE_INTRO = """ +To install or update to the latest version, pip install paicorelib. + +To use the development version, pip install --pre paicorelib.""" + +PLIB_INSTALL_INTRO = ( + "\nPAIBox requires paicorelib, please install it.\n" + _PLIB_BASE_INTRO +) + +PLIB_UPDATE_INTRO = ( + "\nThe minimum required version of paicorelib is {0}, but the current version is {1}.\n" + + _PLIB_BASE_INTRO +) diff --git a/paibox/types.py b/paibox/types.py index f5607ac0..c3fdd844 100644 --- a/paibox/types.py +++ b/paibox/types.py @@ -1,4 +1,3 @@ -import sys from typing import ( AbstractSet, Any, @@ -16,21 +15,17 @@ import numpy as np from numpy.typing import NDArray -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - Shape = TypeVar("Shape", int, Tuple[int, ...], List[int]) ArrayType = TypeVar("ArrayType", List[int], Tuple[int, ...], np.ndarray) Scalar = TypeVar("Scalar", int, float, np.generic) -IntScalarType = TypeVar("IntScalarType", int, np.integer) +IntScalarType = TypeVar("IntScalarType", int, np.bool_, np.integer) DataType = TypeVar("DataType", int, np.bool_, np.integer, np.ndarray) DataArrayType = TypeVar( "DataArrayType", int, np.bool_, np.integer, List[int], Tuple[int, ...], np.ndarray ) -SpikeType: TypeAlias = NDArray[np.bool_] -WeightType: TypeAlias = NDArray[np.int8] # raw int8 weights +SpikeType = NDArray[np.bool_] +SynOutType = NDArray[np.int32] +WeightType = NDArray[np.int8] # raw int8 weights _T = TypeVar("_T") diff --git a/paibox/utils.py b/paibox/utils.py index fdf5f327..8bb86b91 100644 --- a/paibox/utils.py +++ b/paibox/utils.py @@ -60,16 +60,13 @@ def shape2num(shape: Shape) -> int: """Convert a shape to a number""" if isinstance(shape, int): return shape - - if isinstance(shape, (list, tuple)): + else: a = 1 for b in shape: a *= b return a - raise TypeError(f"Unsupported type: {type(shape)}") - def as_shape(shape, min_dim: int = 0) -> Tuple[int, ...]: """Convert a shape to a tuple, like (1,), (10,), or (10, 20)""" From 913778799df9a3ededb610bd8a1daed2abdbbc01 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:12:48 +0800 Subject: [PATCH 17/68] =?UTF-8?q?=F0=9F=90=9B=20bugfix:=20wrong=20shape=20?= =?UTF-8?q?of=20stateful=20variables=20in=20neurons=20when=20keep=5Fshape?= =?UTF-8?q?=20is=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/neuron/base.py | 56 +++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/paibox/neuron/base.py b/paibox/neuron/base.py index 741ea50e..f41315f3 100644 --- a/paibox/neuron/base.py +++ b/paibox/neuron/base.py @@ -23,9 +23,6 @@ __all__ = ["Neuron"] VoltageType: TypeAlias = NDArray[np.int32] -VJT_MAX_LIMIT: int = 2**29 - 1 -VJT_MIN_LIMIT: int = -(2**29) -VJT_LIMIT: int = 2**30 class MetaNeuron: @@ -129,7 +126,7 @@ def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: _rho_j_lambda = 2 # Random leak, unsigned 29-bit. if self.leak_direction is LDM.MODE_FORWARD: - _ld = np.ones(self.varshape, dtype=np.bool_) + _ld = np.ones((self._n_neuron,), dtype=np.bool_) else: _ld = np.sign(vjt) @@ -213,7 +210,7 @@ def _neuronal_reset(self, vjt: VoltageType) -> VoltageType: def _when_exceed_pos() -> VoltageType: if self.reset_mode is RM.MODE_NORMAL: - return np.full(self.varshape, self.reset_v, dtype=np.int32) + return np.full((self._n_neuron,), self.reset_v, dtype=np.int32) elif self.reset_mode is RM.MODE_LINEAR: return np.subtract( @@ -225,7 +222,7 @@ def _when_exceed_pos() -> VoltageType: def _when_exceed_neg() -> VoltageType: if self.neg_thres_mode is NTM.MODE_RESET: if self.reset_mode is RM.MODE_NORMAL: - return np.full(self.varshape, -self.reset_v, dtype=np.int32) + return np.full((self._n_neuron,), -self.reset_v, dtype=np.int32) elif self.reset_mode is RM.MODE_LINEAR: return np.add( vjt, @@ -236,7 +233,7 @@ def _when_exceed_neg() -> VoltageType: return vjt else: - return np.full(self.varshape, -self.neg_threshold, dtype=np.int32) + return np.full((self._n_neuron,), -self.neg_threshold, dtype=np.int32) # USE "=="! v_reset = np.where( @@ -281,11 +278,11 @@ def _relu(self, vj: VoltageType) -> VoltageType: def _when_exceed_pos() -> VoltageType: if self._spike_width_format is SpikeWidthFormat.WIDTH_1BIT: - return np.ones(self.varshape, dtype=np.int32) + return np.ones((self._n_neuron,), dtype=np.int32) if self.bit_truncation >= 8: return np.full( - self.varshape, + (self._n_neuron,), ((vj >> self.bit_truncation) - 8) & ((1 << 8) - 1), dtype=np.int32, ) @@ -293,17 +290,17 @@ def _when_exceed_pos() -> VoltageType: _mask = (1 << self.bit_truncation) - 1 _truncated_vj = vj & _mask return np.full( - self.varshape, + (self._n_neuron,), _truncated_vj << (8 - self.bit_truncation), dtype=np.int32, ) else: - return np.zeros(self.varshape, dtype=np.int32) + return np.zeros((self._n_neuron,), dtype=np.int32) y = np.where( vj >= self.pos_threshold, _when_exceed_pos(), - np.zeros(self.varshape, dtype=np.int32), + np.zeros((self._n_neuron,), dtype=np.int32), ).astype(np.int32) return y @@ -498,17 +495,7 @@ def update( if x is None: x = self.sum_inputs() - # If the incoming membrane potential (30-bit signed) overflows, the chip will automatically handle it. - # This behavior needs to be implemented during simulation. - incoming_v = np.where( - x > VJT_MAX_LIMIT, - x - VJT_LIMIT, - np.where( - x < VJT_MIN_LIMIT, - x + VJT_LIMIT, - x, - ), - ).astype(np.int32) + incoming_v = _vjt_overflow(x) self._inner_spike, self._vjt, self._debug_thres_mode = super().update( incoming_v, self._vjt @@ -590,3 +577,26 @@ def feature_map(self) -> SpikeType: @property def voltage(self) -> VoltageType: return self._vjt.reshape(self.varshape) + + +VJT_MAX_LIMIT: int = 2**29 - 1 +VJT_MIN_LIMIT: int = -(2**29) +VJT_LIMIT: int = 2**30 + + +def _vjt_overflow(x: np.ndarray) -> VoltageType: + """Handle the overflow of the membrane potential. + + NOTE: If the incoming membrane potential (30-bit signed) overflows, the chip\ + will automatically handle it. This behavior needs to be implemented in \ + simulation. + """ + return np.where( + x > VJT_MAX_LIMIT, + x - VJT_LIMIT, + np.where( + x < VJT_MIN_LIMIT, + x + VJT_LIMIT, + x, + ), + ).astype(np.int32) From 160ea3a0fa9a85cecc05fd76989643b3c0b5719a Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:22:36 +0800 Subject: [PATCH 18/68] =?UTF-8?q?=E2=9C=85=20added=20tests=20for=202d=20co?= =?UTF-8?q?nv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/simulator/test_simulator.py | 33 +++ tests/synapses/test_synapses.py | 160 +++++----- tests/synapses/test_transforms.py | 473 ++++++++++++++++++------------ 3 files changed, 415 insertions(+), 251 deletions(-) diff --git a/tests/simulator/test_simulator.py b/tests/simulator/test_simulator.py index 935184ef..9edd50c1 100644 --- a/tests/simulator/test_simulator.py +++ b/tests/simulator/test_simulator.py @@ -98,6 +98,30 @@ def __init__(self, n: int): self.n1_output = pb.Probe(self.n1, "spike") +class Conv2d_Net(pb.Network): + def __init__(self): + super().__init__() + + pe1 = pb.simulator.PoissonEncoder() + + self.inp1 = pb.InputProj(pe1, shape_out=(8, 24, 24)) + self.n1 = pb.IF((16, 22, 22), threshold=10, reset_v=0, keep_shape=True) + + kernel = np.random.randint(-128, 128, size=(8, 16, 3, 3), dtype=np.int8) + stride = 1 + + self.conv1 = pb.Conv2d( + self.inp1, + self.n1, + kernel, + stride=stride, + kernel_order="IOHW", + ) + + self.prob1 = pb.Probe(self.n1, "spike") + self.prob2 = pb.Probe(self.n1, "feature_map") + + class TestSimulator: def test_probe(self): net = Net1(100) @@ -202,3 +226,12 @@ def test_sim_nested_net(self, build_Nested_Net_L3): sim.run(20) sim.reset() + + def test_sim_conv2d_net(self): + net = Conv2d_Net() + sim = pb.Simulator(net, start_time_zero=False) + + net.inp1.input = np.random.rand(8, 24, 24) + sim.run(10) + + sim.reset() diff --git a/tests/synapses/test_synapses.py b/tests/synapses/test_synapses.py index eb3fc159..c851dd3c 100644 --- a/tests/synapses/test_synapses.py +++ b/tests/synapses/test_synapses.py @@ -4,12 +4,13 @@ import paibox as pb from paibox.exceptions import ShapeError +from paibox.utils import shape2num def test_SynSys_Attrs(): - n1 = pb.neuron.TonicSpiking(3, 3) - n2 = pb.neuron.TonicSpiking(3, 3) - s1 = pb.synapses.NoDecay( + n1 = pb.TonicSpiking(3, 3) + n2 = pb.TonicSpiking(3, 3) + s1 = pb.FullConn( n1, n2, weights=np.array([[1, 1, 0], [0, 1, 1], [0, 1, 1]], dtype=np.int8), @@ -23,53 +24,51 @@ def test_SynSys_Attrs(): assert s1.weights.dtype == np.int8 -class TestNoDecay: +class TestFullConn: @pytest.mark.parametrize( "n1, n2, scalar_weight, expected_wp", [ ( - pb.neuron.TonicSpiking(10, 3), - pb.neuron.TonicSpiking(10, 3), + pb.TonicSpiking(10, 3), + pb.TonicSpiking(10, 3), 1, WP.WEIGHT_WIDTH_1BIT, ), ( - pb.neuron.TonicSpiking((3, 3), 3), - pb.neuron.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), 4, WP.WEIGHT_WIDTH_4BIT, ), ( - pb.neuron.TonicSpiking((5,), 3), - pb.neuron.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), -1, WP.WEIGHT_WIDTH_2BIT, ), # TODO 3-dimension shape is correct for data flow? ( - pb.neuron.TonicSpiking((10, 2, 3), 3), - pb.neuron.TonicSpiking((10, 2, 3), 3), + pb.TonicSpiking((10, 2, 3), 3), + pb.TonicSpiking((10, 2, 3), 3), 16, WP.WEIGHT_WIDTH_8BIT, ), ( - pb.neuron.TonicSpiking((10, 2), 3), - pb.neuron.TonicSpiking((4, 5), 3), + pb.TonicSpiking((10, 2), 3), + pb.TonicSpiking((4, 5), 3), -100, WP.WEIGHT_WIDTH_8BIT, ), ( - pb.neuron.TonicSpiking(10, 3), - pb.neuron.TonicSpiking((2, 5), 3), + pb.TonicSpiking(10, 3), + pb.TonicSpiking((2, 5), 3), 7, WP.WEIGHT_WIDTH_4BIT, ), ], ) - def test_NoDecay_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): - s1 = pb.synapses.NoDecay( - n1, n2, scalar_weight, conn_type=pb.SynConnType.One2One - ) + def test_FullConn_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): + s1 = pb.FullConn(n1, n2, scalar_weight, conn_type=pb.SynConnType.One2One) assert np.array_equal(s1.weights, scalar_weight) assert (s1.num_in, s1.num_out) == (n1.num_out, n2.num_in) @@ -83,32 +82,32 @@ def test_NoDecay_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): "n1, n2", [ ( - pb.neuron.TonicSpiking(10, 3), - pb.neuron.TonicSpiking(100, 4), + pb.TonicSpiking(10, 3), + pb.TonicSpiking(100, 4), ), ( - pb.neuron.TonicSpiking((10, 10), 3), - pb.neuron.TonicSpiking((5, 10), 4), + pb.TonicSpiking((10, 10), 3), + pb.TonicSpiking((5, 10), 4), ), ( - pb.neuron.IF((10,), 3), - pb.neuron.TonicSpiking((5,), 4), + pb.IF((10,), 3), + pb.TonicSpiking((5,), 4), ), ( - pb.neuron.TonicSpiking(10, 3), - pb.neuron.TonicSpiking((5, 10), 4), + pb.TonicSpiking(10, 3), + pb.TonicSpiking((5, 10), 4), ), ], ) - def test_NoDecay_One2One_scalar_illegal(self, n1, n2): + def test_FullConn_One2One_scalar_illegal(self, n1, n2): with pytest.raises(ShapeError): - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.One2One) + s1 = pb.FullConn(n1, n2, conn_type=pb.SynConnType.One2One) - def test_NoDecay_One2One_matrix(self): + def test_FullConn_One2One_matrix(self): weight = np.array([2, 3, 4], np.int8) - s1 = pb.synapses.NoDecay( - pb.neuron.TonicSpiking((3,), 3), - pb.neuron.TonicSpiking((3,), 3), + s1 = pb.FullConn( + pb.TonicSpiking((3,), 3), + pb.TonicSpiking((3,), 3), weight, conn_type=pb.SynConnType.One2One, ) @@ -121,9 +120,9 @@ def test_NoDecay_One2One_matrix(self): assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT weight = np.array([1, 0, 1, 0], np.int8) - s2 = pb.synapses.NoDecay( - pb.neuron.TonicSpiking((2, 2), 3), - pb.neuron.TonicSpiking((2, 2), 3), + s2 = pb.FullConn( + pb.TonicSpiking((2, 2), 3), + pb.TonicSpiking((2, 2), 3), weight, conn_type=pb.SynConnType.One2One, ) @@ -141,58 +140,58 @@ def test_NoDecay_One2One_matrix(self): @pytest.mark.parametrize( "n1, n2", [ - (pb.neuron.TonicSpiking(10, 3), pb.neuron.TonicSpiking(10, 3)), + (pb.TonicSpiking(10, 3), pb.TonicSpiking(10, 3)), ( - pb.neuron.TonicSpiking((3, 3), 3), - pb.neuron.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), ), ( - pb.neuron.TonicSpiking((5,), 3), - pb.neuron.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), ), ( - pb.neuron.TonicSpiking(10, 3), - pb.neuron.TonicSpiking(100, 3), + pb.TonicSpiking(10, 3), + pb.TonicSpiking(100, 3), ), ( - pb.neuron.TonicSpiking((10, 10), 3), - pb.neuron.TonicSpiking((5, 5), 3), + pb.TonicSpiking((10, 10), 3), + pb.TonicSpiking((5, 5), 3), ), ], ) - def test_NoDecay_All2All(self, n1, n2): - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) + def test_FullConn_All2All(self, n1, n2): + s1 = pb.FullConn(n1, n2, conn_type=pb.SynConnType.All2All) assert (s1.num_in, s1.num_out) == (n1.num_out, n2.num_in) assert np.array_equal(s1.weights, 1) assert np.array_equal(s1.connectivity, np.ones((n1.num_out, n2.num_in))) - def test_NoDecay_All2All_with_weights(self): - n1 = pb.neuron.TonicSpiking(3, 3) - n2 = pb.neuron.TonicSpiking(3, 3) + def test_FullConn_All2All_with_weights(self): + n1 = pb.TonicSpiking(3, 3) + n2 = pb.TonicSpiking(3, 3) """1. Single weight.""" weight = 2 - s1 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.All2All) + s1 = pb.FullConn(n1, n2, weight, conn_type=pb.SynConnType.All2All) assert np.array_equal(s1.weights, weight) assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT """2. Weights matrix.""" weight = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - s2 = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.All2All) + s2 = pb.FullConn(n1, n2, weight, conn_type=pb.SynConnType.All2All) assert np.array_equal(s2.weights, weight) assert np.array_equal(s2.connectivity, weight) # Wrong shape with pytest.raises(ShapeError): - s3 = pb.synapses.NoDecay( + s3 = pb.FullConn( n1, n2, np.array([1, 2, 3]), conn_type=pb.SynConnType.All2All ) with pytest.raises(ShapeError): - s3 = pb.synapses.NoDecay( + s3 = pb.FullConn( n1, n2, np.array([[1, 2, 3], [4, 5, 6]]), @@ -200,7 +199,7 @@ def test_NoDecay_All2All_with_weights(self): ) with pytest.raises(ShapeError): - s3 = pb.synapses.NoDecay( + s3 = pb.FullConn( n1, n2, np.array([[1, 2], [4, 5], [6, 7]]), @@ -208,7 +207,7 @@ def test_NoDecay_All2All_with_weights(self): ) with pytest.raises(ShapeError): - s3 = pb.synapses.NoDecay( + s3 = pb.FullConn( n1, n2, np.array([[1, 2, 3], [4, 5, 6], [6, 7, 8], [1, 2, 3]]), @@ -218,23 +217,23 @@ def test_NoDecay_All2All_with_weights(self): @pytest.mark.parametrize( "n1, n2", [ - (pb.neuron.TonicSpiking(10, 3), pb.neuron.TonicSpiking(10, 3)), + (pb.TonicSpiking(10, 3), pb.TonicSpiking(10, 3)), ( - pb.neuron.TonicSpiking((3, 3), 3), - pb.neuron.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), + pb.TonicSpiking((3, 3), 3), ), ( - pb.neuron.TonicSpiking((5,), 3), - pb.neuron.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), + pb.TonicSpiking((5,), 3), ), ], ) - def test_NoDecay_MatConn(self, n1, n2): + def test_FullConn_MatConn(self, n1, n2): weight = np.random.randint( -128, 128, size=(n1.num_out, n2.num_in), dtype=np.int8 ) - s = pb.synapses.NoDecay(n1, n2, weight, conn_type=pb.SynConnType.MatConn) + s = pb.FullConn(n1, n2, weight, conn_type=pb.SynConnType.MatConn) assert np.array_equal(s.weights, weight) assert (s.num_in, s.num_out) == (n1.num_out, n2.num_in) @@ -242,19 +241,46 @@ def test_NoDecay_MatConn(self, n1, n2): # Wrong weight type with pytest.raises(TypeError): - s = pb.synapses.NoDecay(n1, n2, 1, conn_type=pb.SynConnType.MatConn) + s = pb.FullConn(n1, n2, 1, conn_type=pb.SynConnType.MatConn) # Wrong shape with pytest.raises(ShapeError): - s = pb.synapses.NoDecay( + s = pb.FullConn( n1, n2, np.array([1, 2, 3]), conn_type=pb.SynConnType.MatConn ) # Wrong shape with pytest.raises(ShapeError): - s = pb.synapses.NoDecay( + s = pb.FullConn( n1, n2, np.array([[1, 2, 3], [4, 5, 6]]), conn_type=pb.SynConnType.MatConn, ) + + +class TestConv2d: + def test_Conv2d_instance(self): + in_shape = (32, 32) + kernel_size = (5, 5) + stride = 2 + out_shape = ((32 - 5) // 2 + 1, (32 - 5) // 2 + 1) + in_channels = 8 + out_channels = 16 + korder = "IOHW" + + n1 = pb.IF((in_channels,) + in_shape, 3) # CHW + n2 = pb.IF((out_channels,) + out_shape, 3) + + weight = np.random.randint( + -128, 128, size=(in_channels, out_channels) + kernel_size, dtype=np.int8 + ) + s1 = pb.Conv2d( + n1, n2, weight, stride=stride, fm_order="CHW", kernel_order=korder + ) + + assert s1.num_in == in_channels * shape2num(in_shape) + assert s1.connectivity.shape == ( + in_channels * shape2num(in_shape), + out_channels * shape2num(out_shape), + ) diff --git a/tests/synapses/test_transforms.py b/tests/synapses/test_transforms.py index 59ead01a..a2451328 100644 --- a/tests/synapses/test_transforms.py +++ b/tests/synapses/test_transforms.py @@ -1,187 +1,292 @@ import numpy as np import pytest -from paibox.synapses.transforms import AllToAll, MaskedLinear, OneToOne - - -@pytest.mark.parametrize( - "weight", - [ - (np.array([1, 2, 3], dtype=np.int8)), - (np.array([1, 0, 1], dtype=np.bool_)), - (np.array([1, 0, 1], dtype=np.int8)), - (10), - (np.int8(-1)), - (np.array([127, 0, 1], dtype=np.int8)), - (np.array([-128, 1, 127], dtype=np.int8)), - ], - ids=[ - "array_1", - "array_2", - "array_3", - "scalar_pos", - "scalar_neg", - "array_int8_1", - "array_int8_2", - ], -) -def test_OneToOne_dtype(weight): - num = 3 - f = OneToOne(num, weight) - x = np.array([1, 0, 1], dtype=np.bool_) - y = f(x) - expected = x * weight - - assert y.dtype == np.int32 - assert y.shape == (num,) - assert np.array_equal(y, expected) - assert f.connectivity.shape == (num, num) - - -def test_OneToOne(): - weight = np.array([1, 2, 3, 4], dtype=np.int8) - f = OneToOne(4, weight) - assert f.connectivity.shape == (4, 4) - - # The last spike is an array. - x1 = np.array([1, 2, 3, 4], dtype=np.int8) - y = f(x1) - assert y.shape == (4,) - - # The last spike is a scalar. - x2 = np.array(2, dtype=np.int8) - y = f(x2) - assert y.shape == (4,) - - -@pytest.mark.parametrize( - "weight, expected_dtype", - [ - (1, np.bool_), - (-1, np.int8), - (10, np.int8), - (-100, np.int8), - (-128, np.int8), - (127, np.int8), - ], - ids=[ - "scalar_1", - "scalar_-1", - "scalar_10", - "scalar_-100", - "scalar_-128", - "scalar_-127", - ], -) -def test_AllToAll_weight_scalar(weight, expected_dtype): - """Test `AllToAll` when weight is a scalar""" - - num_in, num_out = 10, 20 - x = np.random.randint(2, size=(10,)) - f = AllToAll((num_in, num_out), weight) - y = f(x) - expected = np.full((num_out,), np.sum(x, axis=None), dtype=np.int32) * weight - - assert f.conn_dtype == expected_dtype - assert y.dtype == np.int32 - assert y.shape == (num_out,) - assert y.ndim == 1 - assert np.array_equal(y, expected) - assert f.connectivity.shape == (num_in, num_out) - - -@pytest.mark.parametrize( - "shape, x, weights, expected_dtype", - [ - ( - (3, 4), - np.random.randint(2, size=(3,), dtype=np.bool_), - np.random.randint(2, size=(3, 4), dtype=np.bool_), - np.bool_, - ), - ( - (10, 20), - np.random.randint(2, size=(10,), dtype=np.bool_), - np.random.randint(127, size=(10, 20), dtype=np.int8), - np.int8, - ), - ( - (20, 10), - np.random.randint(2, size=(20,), dtype=np.bool_), - np.random.randint(2, size=(20, 10), dtype=np.int8), - np.bool_, - ), - ( - (2, 2), - np.array([1, 1], dtype=np.bool_), - np.array([[1, 2], [3, 4]], dtype=np.int8), - np.int8, - ), - ( - (2, 2), - np.array([1, 1], dtype=np.bool_), - np.array([[127, 0], [3, -128]], dtype=np.int8), - np.int8, - ), - ], - ids=[ - "weights_bool_1", - "weights_int8_1", - "weights_int8_2", - "weights_int8_3", - "weights_int8_4", - ], -) -def test_AllToAll_array(shape, x, weights, expected_dtype): - """Test `AllToAll` when weights is an array""" - - f = AllToAll(shape, weights) - y = f(x) - expected = x @ weights.copy().astype(np.int32) - - assert f.conn_dtype == expected_dtype - assert np.array_equal(y, expected) - assert f.connectivity.shape == shape - - -@pytest.mark.parametrize( - "shape, x, weights, expected_dtype", - [ - ( - (3, 4), - np.array([1, 1, 1], dtype=np.bool_), - np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], dtype=np.int8), - np.int8, - ), - ( - (10, 20), - np.random.randint(2, size=(10,), dtype=np.bool_), - np.random.randint(-10, 10, size=(10, 20), dtype=np.int8), - np.int8, - ), - ( - (20, 10), - np.ones((20,), dtype=np.bool_), - np.random.randint(2, size=(20, 10), dtype=np.int8), - np.bool_, - ), - ( - (2, 2), - np.array([1, 1], dtype=np.bool_), - np.array([[127, 0], [3, -128]], dtype=np.int8), - np.int8, - ), - ], - ids=["weights_int8_1", "weights_int8_2", "weights_bool", "weights_int8_3"], -) -def test_MaskedLinear_conn(shape, x, weights, expected_dtype): - f = MaskedLinear(shape, weights) - y = f(x) - expected = x @ weights.copy().astype(np.int32) - - assert f.conn_dtype == expected_dtype - assert f.connectivity.dtype == expected_dtype - assert y.shape == (shape[1],) - assert y.dtype == np.int32 - assert np.array_equal(y, expected) - assert f.connectivity.shape == shape +from typing import Tuple + +from paibox.synapses.transforms import * +from paibox.utils import shape2num + + +class TestTransforms: + @pytest.mark.parametrize( + "weight", + [ + (np.array([1, 2, 3], dtype=np.int8)), + (np.array([1, 0, 1], dtype=np.bool_)), + (np.array([1, 0, 1], dtype=np.int8)), + (10), + (np.int8(-1)), + (np.array([127, 0, 1], dtype=np.int8)), + (np.array([-128, 1, 127], dtype=np.int8)), + ], + ids=[ + "array_1", + "array_2", + "array_3", + "scalar_pos", + "scalar_neg", + "array_int8_1", + "array_int8_2", + ], + ) + def test_OneToOne_dtype(self, weight): + num = 3 + f = OneToOne(num, weight) + x = np.array([1, 0, 1], dtype=np.bool_) + y = f(x) + expected = x * weight + + assert y.dtype == np.int32 + assert y.shape == (num,) + assert np.array_equal(y, expected) + assert f.connectivity.shape == (num, num) + + def test_OneToOne(self): + weight = np.array([1, 2, 3, 4], dtype=np.int8) + f = OneToOne(4, weight) + assert f.connectivity.shape == (4, 4) + + # The last spike is an array. + x1 = np.array([1, 2, 3, 4], dtype=np.int8) + y = f(x1) + assert y.shape == (4,) + + # The last spike is a scalar. + x2 = np.array(2, dtype=np.int8) + y = f(x2) + assert y.shape == (4,) + + @pytest.mark.parametrize( + "weight, expected_dtype", + [ + (1, np.bool_), + (-1, np.int8), + (10, np.int8), + (-100, np.int8), + (-128, np.int8), + (127, np.int8), + ], + ids=[ + "scalar_1", + "scalar_-1", + "scalar_10", + "scalar_-100", + "scalar_-128", + "scalar_-127", + ], + ) + def test_AllToAll_weight_scalar(self, weight, expected_dtype): + """Test `AllToAll` when weight is a scalar""" + + num_in, num_out = 10, 20 + x = np.random.randint(2, size=(10,)) + f = AllToAll((num_in, num_out), weight) + y = f(x) + expected = np.full((num_out,), np.sum(x, axis=None), dtype=np.int32) * weight + + assert f.conn_dtype == expected_dtype + assert y.dtype == np.int32 + assert y.shape == (num_out,) + assert y.ndim == 1 + assert np.array_equal(y, expected) + assert f.connectivity.shape == (num_in, num_out) + + @pytest.mark.parametrize( + "shape, x, weights, expected_dtype", + [ + ( + (3, 4), + np.random.randint(2, size=(3,), dtype=np.bool_), + np.random.randint(2, size=(3, 4), dtype=np.bool_), + np.bool_, + ), + ( + (10, 20), + np.random.randint(2, size=(10,), dtype=np.bool_), + np.random.randint(127, size=(10, 20), dtype=np.int8), + np.int8, + ), + ( + (20, 10), + np.random.randint(2, size=(20,), dtype=np.bool_), + np.random.randint(2, size=(20, 10), dtype=np.int8), + np.bool_, + ), + ( + (2, 2), + np.array([1, 1], dtype=np.bool_), + np.array([[1, 2], [3, 4]], dtype=np.int8), + np.int8, + ), + ( + (2, 2), + np.array([1, 1], dtype=np.bool_), + np.array([[127, 0], [3, -128]], dtype=np.int8), + np.int8, + ), + ], + ids=[ + "weights_bool_1", + "weights_int8_1", + "weights_int8_2", + "weights_int8_3", + "weights_int8_4", + ], + ) + def test_AllToAll_array(self, shape, x, weights, expected_dtype): + """Test `AllToAll` when weights is an array""" + + f = AllToAll(shape, weights) + y = f(x) + expected = x @ weights.copy().astype(np.int32) + + assert f.conn_dtype == expected_dtype + assert np.array_equal(y, expected) + assert f.connectivity.shape == shape + + @pytest.mark.parametrize( + "shape, x, weights, expected_dtype", + [ + ( + (3, 4), + np.array([1, 1, 1], dtype=np.bool_), + np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], dtype=np.int8), + np.int8, + ), + ( + (10, 20), + np.random.randint(2, size=(10,), dtype=np.bool_), + np.random.randint(-10, 10, size=(10, 20), dtype=np.int8), + np.int8, + ), + ( + (20, 10), + np.ones((20,), dtype=np.bool_), + np.random.randint(2, size=(20, 10), dtype=np.int8), + np.bool_, + ), + ( + (2, 2), + np.array([1, 1], dtype=np.bool_), + np.array([[127, 0], [3, -128]], dtype=np.int8), + np.int8, + ), + ], + ids=["weights_int8_1", "weights_int8_2", "weights_bool", "weights_int8_3"], + ) + def test_MaskedLinear_conn(self, shape, x, weights, expected_dtype): + f = MaskedLinear(shape, weights) + y = f(x) + expected = x @ weights.copy().astype(np.int32) + + assert f.conn_dtype == expected_dtype + assert f.connectivity.dtype == expected_dtype + assert y.shape == (shape[1],) + assert y.dtype == np.int32 + assert np.array_equal(y, expected) + assert f.connectivity.shape == shape + + @staticmethod + def _conv2d_golden( + x: np.ndarray, + out_shape: Tuple[int, int], + kernel: np.ndarray, + stride: Tuple[int, int], + padding: Tuple[int, int], + fm_order: str, + ): + cout, cin, kh, kw = kernel.shape + + if fm_order == "HWC": + _x = x.transpose(2, 0, 1) + else: + _x = x.copy() + + xcin, ih, iw = _x.shape + + assert cin == xcin + + oh = (ih - kh + 2 * padding[0]) // stride[0] + 1 + ow = (iw - kw + 2 * padding[1]) // stride[1] + 1 + + assert oh, ow == out_shape + + out = np.zeros((cout,) + out_shape, dtype=np.int64) + + x_padded = np.pad( + _x, + ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), + mode="constant", + ) + + for o in range(cout): + for i in range(cin): + conv_result = np.zeros((oh, ow), dtype=np.int64) + for h in range(oh): + for w in range(ow): + window = x_padded[ + i, + h * stride[0] : h * stride[0] + kh, + w * stride[1] : w * stride[1] + kw, + ] + conv_result[h, w] = np.sum(window * kernel[o, i, :, :]) + + out[o] += conv_result + + return out + + @pytest.mark.parametrize( + "in_shape, in_channels, out_channels, kernel_size, stride, padding, fm_order", + # Padding is fixed at (0, 0) + [ + ((28, 28), 16, 8, (3, 3), (1, 1), (0, 0), "CHW"), + ((28, 28), 24, 12, (3, 3), (2, 2), (0, 0), "CHW"), + ((28, 28), 24, 12, (5, 5), (2, 1), (0, 0), "CHW"), + ((16, 16), 8, 16, (3, 3), (2, 2), (0, 0), "CHW"), + ((28, 28), 16, 8, (3, 3), (1, 1), (0, 0), "HWC"), + ((24, 32), 8, 8, (3, 4), (2, 1), (0, 0), "HWC"), + ((24, 24), 8, 16, (7, 7), (2, 2), (0, 0), "HWC"), + ((32, 16), 4, 12, (5, 7), (1, 2), (0, 0), "HWC"), + ], + ) + def test_Conv2dForward( + self, + in_shape, + in_channels, + out_channels, + kernel_size, + stride, + padding, + fm_order, + ): + kernel = np.random.randint( + -128, 127, size=(out_channels, in_channels) + kernel_size, dtype=np.int8 + ) + + out_shape = ( + (in_shape[0] + 2 * padding[0] - kernel_size[0]) // stride[0] + 1, + (in_shape[1] + 2 * padding[1] - kernel_size[1]) // stride[1] + 1, + ) + + f = Conv2dForward(in_shape, out_shape, kernel, stride, padding, fm_order) + + if fm_order == "CHW": + fm_shape = (in_channels,) + in_shape + else: + fm_shape = in_shape + (in_channels,) + + x = np.random.randint(0, 2, size=fm_shape, dtype=np.bool_) + xf = x.copy().flatten() + + y = f(xf) + expected = self._conv2d_golden(x, out_shape, kernel, stride, padding, fm_order) + + # Flattened output + y = y.reshape((out_channels,) + out_shape) + + assert y.shape == expected.shape + assert np.array_equal(y, expected) + assert f.connectivity.shape == ( + shape2num((kernel.shape[1],) + in_shape), + shape2num((kernel.shape[0],) + out_shape), + ) From f7a2cf330c24afbc83b23509586a1e99a61595be Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:27:00 +0800 Subject: [PATCH 19/68] =?UTF-8?q?=E2=9C=85=20sync=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/conftest.py | 98 ++++++++++++++--------------- tests/backend/test_mapper.py | 5 +- tests/backend/test_placement.py | 4 +- tests/backend/test_segment_utils.py | 12 ++-- tests/conftest.py | 36 +++++------ tests/neuron/conftest.py | 58 ++++++++--------- tests/neuron/test_neurons.py | 24 +++---- tests/simulator/test_simulator.py | 12 ++-- tests/test_network.py | 32 +++++----- 9 files changed, 137 insertions(+), 144 deletions(-) diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index a529079e..e1b64fe2 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -99,13 +99,13 @@ def __init__(self): self.n1 = pb.TonicSpiking(2000, 3, name="n1_1", tick_wait_start=1) self.n2 = pb.TonicSpiking(1200, 3, name="n2_1", tick_wait_start=2) self.n3 = pb.TonicSpiking(800, 4, name="n3_1", tick_wait_start=3) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1_1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2_1" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s3_1" ) @@ -122,13 +122,13 @@ def __init__(self): self.inp2 = pb.InputProj(input=1, shape_out=(50,), name="inp2_2") self.n1 = pb.TonicSpiking(30, 3, name="n1_2", tick_wait_start=1) self.n2 = pb.TonicSpiking(20, 3, name="n2_2", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1_2" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s2_2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s3_2" ) @@ -147,19 +147,19 @@ def __init__(self): self.n3 = pb.TonicSpiking(400, 4, name="n3", tick_wait_start=4) self.n4 = pb.TonicSpiking(300, 4, name="n4", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp, self.n1, conn_type=pb.SynConnType.One2One, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n1, self.n4, conn_type=pb.SynConnType.All2All, name="s4" ) - self.s5 = pb.NoDecay( + self.s5 = pb.FullConn( self.n4, self.n2, conn_type=pb.SynConnType.All2All, name="s5" ) @@ -177,19 +177,19 @@ def __init__(self): self.n2 = pb.TonicSpiking(400, 4, name="n2", tick_wait_start=2) self.n3 = pb.TonicSpiking(400, 4, name="n3", tick_wait_start=2) self.n4 = pb.TonicSpiking(400, 4, name="n4", tick_wait_start=3) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n2, self.n4, conn_type=pb.SynConnType.One2One, name="s4" ) - self.s5 = pb.NoDecay( + self.s5 = pb.FullConn( self.n3, self.n4, conn_type=pb.SynConnType.One2One, name="s5" ) @@ -207,13 +207,13 @@ def __init__(self): self.n1 = pb.TonicSpiking(80, 2, name="n1", tick_wait_start=1) self.n2 = pb.TonicSpiking(20, 3, name="n2", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.inp2, self.n2, conn_type=pb.SynConnType.All2All, name="s3" ) @@ -231,19 +231,19 @@ def __init__(self, connect_n4: bool = False): self.n2 = pb.TonicSpiking(20, 3, name="n2", tick_wait_start=2) self.n3 = pb.TonicSpiking(30, 4, name="n3", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s3" ) if connect_n4: self.n4 = pb.TonicSpiking(50, 4, name="n4", tick_wait_start=3) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n3, self.n4, conn_type=pb.SynConnType.All2All, name="s4" ) @@ -262,16 +262,16 @@ def __init__(self): self.n2 = pb.TonicSpiking(20, 3, name="n2", tick_wait_start=2) self.n3 = pb.TonicSpiking(30, 3, name="n3", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s3" ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s4" ) @@ -292,7 +292,7 @@ def __init__(self, n_onodes: int): for i in range(n_onodes): self.s_list.append( - pb.NoDecay( + pb.FullConn( self.inp1, self.n_list[i], conn_type=pb.SynConnType.All2All, @@ -319,35 +319,35 @@ def __init__(self, seed: int): self.n2 = pb.TonicSpiking(10, 4, name="n2", tick_wait_start=2) self.n3 = pb.TonicSpiking(10, 4, name="n3", tick_wait_start=2) self.n4 = pb.TonicSpiking(4, 4, name="n4", tick_wait_start=3) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s1", ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s2", ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n1, self.n3, weights=rng.randint(-8, 8, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s3", ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n2, self.n4, weights=rng.randint(-8, 8, size=(10, 4), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s4", ) - self.s5 = pb.NoDecay( + self.s5 = pb.FullConn( self.n3, self.n4, weights=rng.randint(-8, 8, size=(10, 4), dtype=np.int8), @@ -371,35 +371,35 @@ def __init__(self, seed: int) -> None: self.n2 = pb.TonicSpiking(10, 4, name="n2") self.n3 = pb.TonicSpiking(10, 4, name="n3") self.n4 = pb.TonicSpiking(4, 4, name="n4") - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s1", ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s2", ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.n1, self.n3, weights=rng.randint(-128, 128, size=(10, 10), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s3", ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n2, self.n4, weights=rng.randint(-128, 128, size=(10, 4), dtype=np.int8), conn_type=pb.SynConnType.MatConn, name="s4", ) - self.s5 = pb.NoDecay( + self.s5 = pb.FullConn( self.n3, self.n4, weights=rng.randint(-128, 128, size=(10, 4), dtype=np.int8), @@ -416,9 +416,9 @@ def __init__(self): self.inp = pb.InputProj(1, shape_out=(3,)) - n1 = pb.neuron.TonicSpiking((3,), 2) - n2 = pb.neuron.TonicSpiking((3,), 3) - n3 = pb.neuron.TonicSpiking((3,), 4) + n1 = pb.TonicSpiking((3,), 2) + n2 = pb.TonicSpiking((3,), 3) + n3 = pb.TonicSpiking((3,), 4) n_list = pb.NodeList() n_list.append(n1) @@ -426,10 +426,10 @@ def __init__(self): n_list.append(n3) self.n_list = n_list - self.s1 = pb.synapses.NoDecay( + self.s1 = pb.FullConn( n_list[0], n_list[1], conn_type=pb.SynConnType.All2All ) - self.s2 = pb.synapses.NoDecay( + self.s2 = pb.FullConn( n_list[1], n_list[2], conn_type=pb.SynConnType.All2All ) @@ -446,7 +446,7 @@ def __init__(self, tws: int = 1, name: Optional[str] = None): self.post_n = pb.LIF((10,), 10, 2, tick_wait_start=tws + 1) w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) - self.syn = pb.NoDecay( + self.syn = pb.FullConn( self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w ) @@ -459,12 +459,12 @@ def __init__(self, tws: int = 1, name: Optional[str] = None): subnet1 = ReusedStruct(tws=tws, name="Named_Reused_0") subnet2 = ReusedStruct(tws=tws + 2, name="Named_Reused_1") - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, subnet1.pre_n, conn_type=pb.SynConnType.One2One, ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( subnet1.post_n, subnet2.pre_n, conn_type=pb.SynConnType.One2One, @@ -480,7 +480,7 @@ def __init__(self): self.inp1 = pb.InputProj(1, shape_out=(10,)) subnet1 = Nested_Net_level_2(name="Named_Nested_Net_level_2") - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, subnet1["Named_Reused_0"].pre_n, conn_type=pb.SynConnType.One2One, @@ -605,7 +605,7 @@ def MockNeuronConfig() -> NeuronConfig: offset = random.randint(1, 100) interval = random.randint(1, 2) - neuron = pb.neuron.IF((n,), 3, reset_v=-1) + neuron = pb.IF((n,), 3, reset_v=-1) ns = NeuronSegment(slice(0, 0 + n, 1), offset, interval) axon_coords = [AxonCoord(0, i) for i in range(0, n)] @@ -618,7 +618,7 @@ def MockNeuronConfig() -> NeuronConfig: @pytest.fixture def MockCorePlacementConfig(MockCoreConfigDict, MockNeuronConfig): - neuron = pb.neuron.IF((100,), 3, reset_v=-1) + neuron = pb.IF((100,), 3, reset_v=-1) cpc = CorePlacementConfig.encapsulate( random.randint(1, 200), diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index a0b3d028..0f276527 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -200,11 +200,14 @@ def test_network_with_container(self, get_mapper, build_Network_with_container): class TestMapper_Export: - def test_export_multi_nodes_more_than_32(self, build_Network_with_N_onodes): + def test_export_multi_nodes_more_than_32( + self, build_Network_with_N_onodes, ensure_dump_dir + ): net = build_Network_with_N_onodes mapper = pb.Mapper() mapper.build(net) mapper.compile() + mapper.export(fp=ensure_dump_dir) assert len(mapper.graph_info["output"].keys()) == net.n_onodes diff --git a/tests/backend/test_placement.py b/tests/backend/test_placement.py index 8e948c58..8a639044 100644 --- a/tests/backend/test_placement.py +++ b/tests/backend/test_placement.py @@ -28,8 +28,8 @@ def test_get_raw_weight_ref(): w_of_neurons = [w1, w2] - n1 = pb.neuron.LIF((20,), 1) - n2 = pb.neuron.LIF((30,), 1) + n1 = pb.LIF((20,), 1) + n2 = pb.LIF((30,), 1) dest = [n1, n2] diff --git a/tests/backend/test_segment_utils.py b/tests/backend/test_segment_utils.py index 289c741e..2de467aa 100644 --- a/tests/backend/test_segment_utils.py +++ b/tests/backend/test_segment_utils.py @@ -342,10 +342,10 @@ def test_get_neu_segments_both(self, neurons, capacity, wp, lcn_ex, expected): @pytest.mark.parametrize( "axons", [ - [pb.neuron.LIF(600, 2), pb.neuron.LIF(800, 2), pb.neuron.LIF(256, 2)], - [pb.neuron.LIF(384, 3), pb.neuron.LIF(383, 3), pb.neuron.LIF(385, 3)], - [pb.neuron.LIF(1153, 2)], - [pb.neuron.LIF(2222, 1), pb.neuron.LIF(2378, 1)], + [pb.LIF(600, 2), pb.LIF(800, 2), pb.LIF(256, 2)], + [pb.LIF(384, 3), pb.LIF(383, 3), pb.LIF(385, 3)], + [pb.LIF(1153, 2)], + [pb.LIF(2222, 1), pb.LIF(2378, 1)], ], ) def test_get_axon_segments(axons): @@ -362,8 +362,8 @@ def test_get_axon_segments(axons): @pytest.mark.parametrize( "axons", [ - [pb.neuron.LIF(1151, 2), pb.neuron.LIF(1153, 2)], - [pb.neuron.LIF(1151 * 2, 2), pb.neuron.LIF(1153 * 2, 2)], + [pb.LIF(1151, 2), pb.LIF(1153, 2)], + [pb.LIF(1151 * 2, 2), pb.LIF(1153 * 2, 2)], ], ) def test_get_axon_segments_boundary(axons): diff --git a/tests/conftest.py b/tests/conftest.py index 14b3e65d..609e9d2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def __init__(self): super().__init__() self.inp1 = pb.InputProj(1, shape_out=(1,)) self.n1 = pb.TonicSpiking(1, 3, tick_wait_start=2, delay=1) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, weights=1, conn_type=pb.SynConnType.One2One ) @@ -44,10 +44,10 @@ def __init__(self): self.n1 = pb.TonicSpiking(1, 2, tick_wait_start=2, delay=3) self.n2 = pb.TonicSpiking(1, 2, tick_wait_start=3) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, weights=1, conn_type=pb.SynConnType.One2One ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, weights=1, conn_type=pb.SynConnType.All2All ) @@ -70,9 +70,9 @@ def __init__(self): self.inp = pb.InputProj(1, shape_out=(3,)) - n1 = pb.neuron.TonicSpiking((3,), 2) - n2 = pb.neuron.TonicSpiking((3,), 3) - n3 = pb.neuron.TonicSpiking((3,), 4) + n1 = pb.TonicSpiking((3,), 2) + n2 = pb.TonicSpiking((3,), 3) + n3 = pb.TonicSpiking((3,), 4) n_list: pb.NodeList[pb.neuron.Neuron] = pb.NodeList() n_list.append(n1) @@ -80,12 +80,8 @@ def __init__(self): n_list.append(n3) self.n_list = n_list - self.s1 = pb.synapses.NoDecay( - n_list[0], n_list[1], conn_type=pb.SynConnType.All2All - ) - self.s2 = pb.synapses.NoDecay( - n_list[1], n_list[2], conn_type=pb.SynConnType.All2All - ) + self.s1 = pb.FullConn(n_list[0], n_list[1], conn_type=pb.SynConnType.All2All) + self.s2 = pb.FullConn(n_list[1], n_list[2], conn_type=pb.SynConnType.All2All) self.probe1 = pb.Probe(self.n_list[1], "output", name="n2_out") @@ -104,16 +100,16 @@ def __init__(self): self.n2 = pb.TonicSpiking(20, 3, name="n2", tick_wait_start=2) self.n3 = pb.TonicSpiking(30, 3, name="n3", tick_wait_start=2) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, self.n1, conn_type=pb.SynConnType.All2All, name="s1" ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s2" ) - self.s3 = pb.NoDecay( + self.s3 = pb.FullConn( self.inp2, self.n1, conn_type=pb.SynConnType.All2All, name="s3" ) - self.s4 = pb.NoDecay( + self.s4 = pb.FullConn( self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s4" ) @@ -128,7 +124,7 @@ def __init__(self, name: Optional[str] = None): self.post_n = pb.LIF((10,), 10) w = np.random.randint(-128, 127, (10, 10), dtype=np.int8) - self.syn = pb.NoDecay( + self.syn = pb.FullConn( self.pre_n, self.post_n, conn_type=pb.SynConnType.All2All, weights=w ) @@ -140,12 +136,12 @@ def __init__(self, name: Optional[str] = None): self.inp1 = pb.InputProj(1, shape_out=(10,)) subnet1 = Nested_Net_L1() subnet2 = Nested_Net_L1(name="Named_SubNet_L1_1") - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, subnet1.pre_n, conn_type=pb.SynConnType.One2One, ) - self.s2 = pb.NoDecay( + self.s2 = pb.FullConn( subnet1.post_n, subnet2.pre_n, conn_type=pb.SynConnType.One2One, @@ -164,7 +160,7 @@ def __init__(self): subnet1_of_subnet1 = subnet1[f"{Nested_Net_L1.__name__}_0"] - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp1, subnet1_of_subnet1.pre_n, conn_type=pb.SynConnType.One2One, diff --git a/tests/neuron/conftest.py b/tests/neuron/conftest.py index f5990dfc..d218677d 100644 --- a/tests/neuron/conftest.py +++ b/tests/neuron/conftest.py @@ -44,15 +44,13 @@ class Net1(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(fakeout, shape_out=(2,)) - self.n1 = pb.neuron.IF((2,), 3) - self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, conn_type=pb.SynConnType.One2One - ) + self.n1 = pb.IF((2,), 3) + self.s1 = pb.FullConn(self.inp1, self.n1, conn_type=pb.SynConnType.One2One) - self.probe1 = pb.simulator.Probe(self.inp1, "output") - self.probe2 = pb.simulator.Probe(self.s1, "output") - self.probe3 = pb.simulator.Probe(self.n1, "output") - self.probe4 = pb.simulator.Probe(self.n1, "voltage") + self.probe1 = pb.Probe(self.inp1, "output") + self.probe2 = pb.Probe(self.s1, "output") + self.probe3 = pb.Probe(self.n1, "output") + self.probe4 = pb.Probe(self.n1, "voltage") class Net2(pb.Network): @@ -64,21 +62,21 @@ class Net2(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(1, shape_out=(2, 2)) - self.n1 = pb.neuron.LIF((2, 2), 600, reset_v=1, leak_v=-1) - self.s1 = pb.synapses.NoDecay( + self.n1 = pb.LIF((2, 2), 600, reset_v=1, leak_v=-1) + self.s1 = pb.FullConn( self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) - self.s2 = pb.synapses.NoDecay( + self.s2 = pb.FullConn( self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) - self.s3 = pb.synapses.NoDecay( + self.s3 = pb.FullConn( self.inp1, self.n1, weights=127, conn_type=pb.SynConnType.All2All ) - self.probe1 = pb.simulator.Probe(self.inp1, "output") - self.probe2 = pb.simulator.Probe(self.s1, "output") - self.probe3 = pb.simulator.Probe(self.n1, "output") - self.probe4 = pb.simulator.Probe(self.n1, "voltage") + self.probe1 = pb.Probe(self.inp1, "output") + self.probe2 = pb.Probe(self.s1, "output") + self.probe3 = pb.Probe(self.n1, "output") + self.probe4 = pb.Probe(self.n1, "voltage") class Net3(pb.Network): @@ -87,33 +85,31 @@ class Net3(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(1, shape_out=(2, 2)) - self.n1 = pb.neuron.LIF((2, 2), 100, reset_v=1, leak_v=-1) - self.n2 = pb.neuron.LIF((2, 2), 100, reset_v=1, leak_v=-1) - self.s1 = pb.synapses.NoDecay( + self.n1 = pb.LIF((2, 2), 100, reset_v=1, leak_v=-1) + self.n2 = pb.LIF((2, 2), 100, reset_v=1, leak_v=-1) + self.s1 = pb.FullConn( self.inp1, self.n1, weights=10, conn_type=pb.SynConnType.All2All ) - self.s2 = pb.synapses.NoDecay( + self.s2 = pb.FullConn( self.n1, self.n2, weights=10, conn_type=pb.SynConnType.All2All ) - self.probe1 = pb.simulator.Probe(self.n1, "voltage", name="n1_v") - self.probe2 = pb.simulator.Probe(self.n2, "voltage", name="n2_v") - self.probe3 = pb.simulator.Probe(self.n1, "output", name="n1_out") - self.probe4 = pb.simulator.Probe(self.n2, "output", name="n2_out") + self.probe1 = pb.Probe(self.n1, "voltage", name="n1_v") + self.probe2 = pb.Probe(self.n2, "voltage", name="n2_v") + self.probe3 = pb.Probe(self.n1, "output", name="n1_out") + self.probe4 = pb.Probe(self.n2, "output", name="n2_out") class TonicSpikingNet(pb.Network): def __init__(self): super().__init__() self.inp1 = pb.InputProj(fakeout, shape_out=(2,)) - self.n1 = pb.neuron.TonicSpiking((2,), 3) - self.s1 = pb.synapses.NoDecay( - self.inp1, self.n1, conn_type=pb.SynConnType.One2One - ) + self.n1 = pb.TonicSpiking((2,), 3) + self.s1 = pb.FullConn(self.inp1, self.n1, conn_type=pb.SynConnType.One2One) - self.probe1 = pb.simulator.Probe(self.s1, "output") - self.probe2 = pb.simulator.Probe(self.n1, "output") - self.probe3 = pb.simulator.Probe(self.n1, "voltage") + self.probe1 = pb.Probe(self.s1, "output") + self.probe2 = pb.Probe(self.n1, "output") + self.probe3 = pb.Probe(self.n1, "voltage") @pytest.fixture(scope="class") diff --git a/tests/neuron/test_neurons.py b/tests/neuron/test_neurons.py index c8a9361b..7942c274 100644 --- a/tests/neuron/test_neurons.py +++ b/tests/neuron/test_neurons.py @@ -10,7 +10,7 @@ def test_NeuronParams_instance(ensure_dump_dir): - n1 = pb.neuron.LIF((100,), 3) + n1 = pb.LIF((100,), 3) attrs = NeuronAttrs.model_validate(n1.export_params(), strict=True) @@ -22,13 +22,13 @@ def test_NeuronParams_instance(ensure_dump_dir): def test_NeuronParams_check(): with pytest.raises(ValueError): - n1 = pb.neuron.LIF((100,), threshold=-1) + n1 = pb.LIF((100,), threshold=-1) with pytest.raises(ValueError): - n2 = pb.neuron.IF((100,), 1, delay=-1) + n2 = pb.IF((100,), 1, delay=-1) with pytest.raises(ValueError): - n3 = pb.neuron.IF((100,), 1, delay=1, tick_wait_start=-1, tick_wait_end=100) + n3 = pb.IF((100,), 1, delay=1, tick_wait_start=-1, tick_wait_end=100) class TestNeuronBehavior: @@ -245,14 +245,14 @@ def test_vjt_overflow(self, incoming_v, expected_v, expected_spike): ) def test_neuron_instance(shape): # keep_shape = True - n1 = pb.neuron.TonicSpiking(shape, 5, keep_shape=True) + n1 = pb.TonicSpiking(shape, 5, keep_shape=True) assert n1.shape_in == as_shape(shape) assert n1.shape_out == as_shape(shape) assert len(n1) == shape2num(shape) # keep_shape = False - n2 = pb.neuron.TonicSpiking(shape, 5) + n2 = pb.TonicSpiking(shape, 5) assert n2.shape_in == as_shape(shape2num(shape)) assert n2.shape_out == as_shape(shape2num(shape)) @@ -260,8 +260,8 @@ def test_neuron_instance(shape): def test_neuron_keep_shape(): - n1 = pb.neuron.TonicSpiking((4, 4), 5, keep_shape=True) - n2 = pb.neuron.TonicSpiking((4, 4), 5, keep_shape=False) + n1 = pb.TonicSpiking((4, 4), 5, keep_shape=True) + n2 = pb.TonicSpiking((4, 4), 5, keep_shape=False) assert n1.spike.shape == (16,) assert n1.voltage.shape == (4, 4) @@ -300,7 +300,7 @@ def test_neuron_copy(): class TestNeuronSim: def test_TonicSpiking_simple_sim(self): - n1 = pb.neuron.TonicSpiking(shape=1, fire_step=3) + n1 = pb.TonicSpiking(shape=1, fire_step=3) inp_data = np.ones((10,), dtype=np.bool_) output = np.full((10, 1), 0, dtype=np.bool_) voltage = np.full((10, 1), 0, dtype=np.int32) @@ -312,7 +312,7 @@ def test_TonicSpiking_simple_sim(self): print(output) def test_PhasicSpiking_simple_sim(self): - n1 = pb.neuron.PhasicSpiking(shape=1, time_to_fire=3) + n1 = pb.PhasicSpiking(shape=1, time_to_fire=3) # [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] inp_data = np.concatenate((np.zeros((2,), np.bool_), np.ones((10,), np.bool_))) output = np.full((12, 1), 0, dtype=np.bool_) @@ -325,7 +325,7 @@ def test_PhasicSpiking_simple_sim(self): print(output) def test_IF_simple_sim(self): - n1 = pb.neuron.IF(shape=1, threshold=5, reset_v=2) + n1 = pb.IF(shape=1, threshold=5, reset_v=2) # [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] inp_data = np.concatenate((np.zeros((2,), np.bool_), np.ones((10,), np.bool_))) # inp_data = np.ones((12,), dtype=np.bool_) @@ -339,7 +339,7 @@ def test_IF_simple_sim(self): print(output) def test_LIF_simple_sim(self): - n1 = pb.neuron.LIF(shape=1, threshold=5, reset_v=2, leak_v=1) # leak + 1 + n1 = pb.LIF(shape=1, threshold=5, reset_v=2, leak_v=1) # leak + 1 # [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] inp_data = np.concatenate((np.zeros((2,), np.bool_), np.ones((10,), np.bool_))) # inp_data = np.ones((12,), dtype=np.bool_) diff --git a/tests/simulator/test_simulator.py b/tests/simulator/test_simulator.py index 9edd50c1..a0461a38 100644 --- a/tests/simulator/test_simulator.py +++ b/tests/simulator/test_simulator.py @@ -14,13 +14,13 @@ def __init__(self, n_neuron: int): self.inp = pb.InputProj(pe, shape_out=(n_neuron,), keep_shape=True) self.n1 = pb.LIF(n_neuron, threshold=3, reset_v=0, tick_wait_start=1) self.n2 = pb.IF(n_neuron, threshold=3, reset_v=1, tick_wait_start=2) - self.s0 = pb.NoDecay( + self.s0 = pb.FullConn( self.inp, self.n1, weights=np.random.randint(-128, 128, size=(n_neuron,), dtype=np.int8), conn_type=pb.SynConnType.One2One, ) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.n1, self.n2, weights=np.random.randint( @@ -50,13 +50,13 @@ def __init__(self, n: int): self.inp1 = pb.InputProj(fake_out_1, shape_out=(n,), keep_shape=True) self.inp2 = pb.InputProj(fake_out_2, shape_out=(n,), keep_shape=True) self.n1 = pb.LIF(n, threshold=3, reset_v=0, tick_wait_start=1) - self.s0 = pb.NoDecay( + self.s0 = pb.FullConn( self.inp1, self.n1, weights=np.ones((n,), dtype=np.int8), conn_type=pb.SynConnType.One2One, ) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp2, self.n1, weights=np.ones((n,), dtype=np.int8), @@ -79,13 +79,13 @@ def __init__(self, n: int): self.inp1 = pb.InputProj(pe1, shape_out=(n,), keep_shape=True) self.inp2 = pb.InputProj(pe2, shape_out=(n,), keep_shape=True) self.n1 = pb.LIF(n, threshold=3, reset_v=0, tick_wait_start=1) - self.s0 = pb.NoDecay( + self.s0 = pb.FullConn( self.inp1, self.n1, weights=np.ones((n,), dtype=np.int8), conn_type=pb.SynConnType.One2One, ) - self.s1 = pb.NoDecay( + self.s1 = pb.FullConn( self.inp2, self.n1, weights=np.ones((n,), dtype=np.int8), diff --git a/tests/test_network.py b/tests/test_network.py index ff8962a8..c760886b 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -165,8 +165,8 @@ def test_Collector_operations(self): def test_add_components(self, build_NotNested_Net_Exp): net: pb.Network = build_NotNested_Net_Exp n3 = pb.LIF((3,), 10) - s1 = pb.synapses.NoDecay(net.n1, n3, conn_type=pb.SynConnType.All2All) - s2 = pb.synapses.NoDecay(net.n2, n3, conn_type=pb.SynConnType.All2All) + s1 = pb.FullConn(net.n1, n3, conn_type=pb.SynConnType.All2All) + s2 = pb.FullConn(net.n2, n3, conn_type=pb.SynConnType.All2All) with pytest.raises(ValueError): net.diconnect_neudyn_succ(n3) @@ -230,10 +230,10 @@ def test_insert_neudyn(self, build_Network_with_container): # Insert n3 between n_list[0] & n_list[1] n_insert = pb.LIF((3,), 10) - s_insert1 = pb.synapses.NoDecay( + s_insert1 = pb.FullConn( net.n_list[0], n_insert, conn_type=pb.SynConnType.All2All ) - s_insert2 = pb.synapses.NoDecay( + s_insert2 = pb.FullConn( n_insert, net.n_list[1], conn_type=pb.SynConnType.All2All ) @@ -289,9 +289,9 @@ def test_Subnets(self, build_Network_with_subnet): @pytest.mark.skip(reason="'Sequential is not used'") def test_Sequential_build(): - n1 = pb.neuron.TonicSpiking(10, fire_step=3) - n2 = pb.neuron.TonicSpiking(10, fire_step=5) - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) + n1 = pb.TonicSpiking(10, fire_step=3) + n2 = pb.TonicSpiking(10, fire_step=5) + s1 = pb.FullConn(n1, n2, conn_type=pb.SynConnType.All2All) sequential = pb.network.Sequential(n1, s1, n2) assert isinstance(sequential, pb.network.Sequential) @@ -302,11 +302,9 @@ def test_Sequential_build(): class Seq(pb.network.Sequential): def __init__(self): super().__init__() - self.n1 = pb.neuron.TonicSpiking(5, fire_step=3) - self.n2 = pb.neuron.TonicSpiking(5, fire_step=5) - self.s1 = pb.synapses.NoDecay( - self.n1, self.n2, conn_type=pb.SynConnType.All2All - ) + self.n1 = pb.TonicSpiking(5, fire_step=3) + self.n2 = pb.TonicSpiking(5, fire_step=5) + self.s1 = pb.FullConn(self.n1, self.n2, conn_type=pb.SynConnType.All2All) seq = Seq() nodes2 = seq.nodes(method="absolute", level=1, include_self=False) @@ -315,11 +313,11 @@ def __init__(self): @pytest.mark.skip(reason="'Sequential is not used'") def test_Sequential_getitem(): - n1 = pb.neuron.TonicSpiking(10, fire_step=3, name="n1") - n2 = pb.neuron.TonicSpiking(10, fire_step=5, name="n2") - s1 = pb.synapses.NoDecay(n1, n2, conn_type=pb.SynConnType.All2All) - n3 = pb.neuron.TonicSpiking(10, fire_step=5, name="n3") - s2 = pb.synapses.NoDecay(n2, n3, conn_type=pb.SynConnType.All2All) + n1 = pb.TonicSpiking(10, fire_step=3, name="n1") + n2 = pb.TonicSpiking(10, fire_step=5, name="n2") + s1 = pb.FullConn(n1, n2, conn_type=pb.SynConnType.All2All) + n3 = pb.TonicSpiking(10, fire_step=5, name="n3") + s2 = pb.FullConn(n2, n3, conn_type=pb.SynConnType.All2All) sequential = pb.network.Sequential(n1, s1, n2, s2, n3, name="Sequential_2") assert isinstance(sequential.children, NodeDict) From e0e971746c12f7ad5e017cc6ac189a8bf8d52274 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 15 Mar 2024 16:28:10 +0800 Subject: [PATCH 20/68] =?UTF-8?q?=F0=9F=93=A6=20added=20an=20example=20for?= =?UTF-8?q?=20conv2d=20network?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/mnist/example2/README.md | 8 ++ .../example2/examples-mnist-ex2-network.png | Bin 0 -> 60375 bytes examples/mnist/example2/main.py | 117 ++++++++++++++++++ .../mnist/example2/weights/Vthr_conv1.npy | Bin 0 -> 136 bytes .../mnist/example2/weights/Vthr_conv2.npy | Bin 0 -> 136 bytes examples/mnist/example2/weights/Vthr_fc1.npy | Bin 0 -> 136 bytes .../mnist/example2/weights/weight_conv1.npy | Bin 0 -> 200 bytes .../mnist/example2/weights/weight_conv2.npy | Bin 0 -> 416 bytes .../mnist/example2/weights/weight_fc1.npy | Bin 0 -> 92288 bytes 9 files changed, 125 insertions(+) create mode 100644 examples/mnist/example2/README.md create mode 100644 examples/mnist/example2/examples-mnist-ex2-network.png create mode 100644 examples/mnist/example2/main.py create mode 100644 examples/mnist/example2/weights/Vthr_conv1.npy create mode 100644 examples/mnist/example2/weights/Vthr_conv2.npy create mode 100644 examples/mnist/example2/weights/Vthr_fc1.npy create mode 100644 examples/mnist/example2/weights/weight_conv1.npy create mode 100644 examples/mnist/example2/weights/weight_conv2.npy create mode 100644 examples/mnist/example2/weights/weight_fc1.npy diff --git a/examples/mnist/example2/README.md b/examples/mnist/example2/README.md new file mode 100644 index 00000000..f02cb7a0 --- /dev/null +++ b/examples/mnist/example2/README.md @@ -0,0 +1,8 @@ +该示例构建了常规的卷积网络,由两层卷积与一层全连接组成。 + +- 权重及阈值:`./weights` +- 测试数据:`../data`,数字7 +- 网络结构: +

+ MNIST-ex2 网络结构 +

diff --git a/examples/mnist/example2/examples-mnist-ex2-network.png b/examples/mnist/example2/examples-mnist-ex2-network.png new file mode 100644 index 0000000000000000000000000000000000000000..59b6432347869691c7e150edf2c0575648145da4 GIT binary patch literal 60375 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 R=LMej3$$i7@$}zZ`3KQeWYPct literal 0 HcmV?d00001 diff --git a/examples/mnist/example2/weights/weight_fc1.npy b/examples/mnist/example2/weights/weight_fc1.npy new file mode 100644 index 0000000000000000000000000000000000000000..b5a8e71e06811964d177738f24e9f20f8b7c092c GIT binary patch literal 92288 zcmbTfL8xR&mZoO|ZxM)G_#zg$h{at5R0v|>l422vgg(~^L@q+Y7at(^Mv96NT-e~ulqYuDe4$gKb2Kl{)B)8l{fPtt#z{=1*N|IO|MB#b zum0()pS+xY@=t&EXMgc$fAU}dmp}Wn_kXJWKlzhy|NKv}|M@q6^56U^;}`$I{^j%^ z{zv;)|NhJAzn}hpeg3_aa!n}(@{c+yLpKG!n=W|dC()#6=OWhpCRN%ZlEcouw-LI9g>(}48TkXH= zIn4bQIIq80fWGpsNOJ^8(Q61~QHNtY56&4|YUj$x%-A*CzczMGp62t1Kl~ouv8dRz z%VQr!2~1!=_r1&Lt@fIlZzawe?Zt{MMnCnje(RYAPDARq2EL$xhnf1o1oXEKefPle zb{6FG99y&Yp3n96JuK0X||98N%VQu>Fk?n)N5?g_}R>tz%n`zk1nWM*=L%ZW6KKA8%@E(}! z5lo#iDkFR&p-vIaR)1^O;fq%x=x>n=~ zc?a4v$X?q|>o54pt)B>B4aUmhdH!4QYv5gHPvtPqJ=SN%Hvwadz-E6|d|hW|&Yl#c zIhIksvA6jBGw{QH;S(U?SDD`<^RQjfjnV#w7<)TuV}3#RvqnGH6}vfI=ksw7mx?cb zD{_Z4);z{!q+?_9g?Zw`+A4& z^V8Ny-h@wy?rW@e<0nunu0TD2AM>Q>{Kn;(afO5V*Y}K(Z9UAHXUBX-zoq~Gh<*gk zzxMvgFJCea*C+sW!uCX9J7iZx8QowQSx18p)H!HG$EvJ3a zW}R|9GH&O8*yXeDa$ms_m`_w--l%;)DssAD$96=z9;NdMblLCWkJvvs2du$y!Dr8N z<-4}|=Dud@xy5z{Ct%#C;pf=>T!~Eq_{g(H?~ygwE9aF3dqPh$WBR*26OLPaU>D4_ z7^MCc7~kX=%k7-lqswi73gbX;oMR8B!N1j@&lcNwh!1Ql$iVmon{}v%^FYqZyfNm~ zX9qrjYm=Y*>wJ4pP z8}J60r{Lq+Is$XJ-dpql#%ACh_#POO;MZrFdrZCjlk^w&qVt*SEqENYPV(Tp66fC7 zi%Pt3PW}@xhjk?E>j0xxgMD9_lh;1kvxK}NcOV1#3NT*Y6ZjgRp9Aml55OGxkNS6S zId;~l4fvhh_m%bgzM9nWxdRdK0>|=uHXhVj_fPTrEAZD~)fd>yGu-~H;GRAMdGK40 z>wO9Af5IojS;2%}hICzLdMp1OFh|9<=nt$x`2=3qU_I`SwSPO_D_3mxNxu?E&l5=K z)7GJ#D|Dy}R+5d!OYD@Eq8izZ&asFK)qm@Rc=?!(O=-*KmuT#u>2h?%~JL zlU4`kdX3I2*EhhuwHC);j&=PO-S_+x@C7i(9t5%GO2`7Q-)GHhf7&@>${rbKPlP$_ zy?NfScYlF2pL23Q;xD)I2r}H>SMs$Ujy#pI-1@mLd-D=}4UWJ(;wiBI^1lGq-ag0@ zpL0ZY4ZUabx-Op+`JWKC1J*3hYj6uZ)9--2RhoY#F3Cfzobv1#PauFfJm>CJ3D??n zbLeY*-=hB({1W^Md=KnJ#ose=0_yVlaW7BkYxMIhhdKAaxlhVUzh?Nsm9gjOZ|VO} zM=f&tPJVLeJl44qb4Hp=?jyReR%M=hj*6dm_?7Vs;$6G@)z*e?&Sgk-c?&S^1}<~m zf&@-L{z-j?mNU9X&-ktZFE;Idlu?`7zK5l-C;gea60`Bi@1E-G{q3D#AJ4#ASL_wo zH*;94zI&vxflcJ!^}Ral$9w_$HC;dSqw>D5$KFpfaY#8`OGQ3E^j?37bPcX4oFDP| z()Y-51=e7H%-dyO!=AC6>)i9~X6)z*-U;l$OJGjdnL!EWK~>`4&SAyG*nS*=yj#y! z?B3VyXEWv=fqSU$(?i!?kNI3j%fou(LJnZf?fniwChv?K_q=%TeI-Q~f^9_y!+qc>q4c@_Yg0e-3PILC${9kF!r6?F)9- zX6^DOY%5SYU%`IvYxsb)A5nXZYw+h}&xy7bY5mq}p9}Ep>eqY&KTv=?n>4mCK8@Za zbZZJQq08aCJ$?z4mANyha5Zk`3%Va=sf5w1gj^YC<3RwQKW)tq>AEV?J(d3sc?E0j zy)`Z~pTHhn4r_Ak$Gy_mxd~LkxJ2LI>#)0~SNHggXC&hzcRK^fjDJF&fZVNq{5>1? z)43xshjI_j;KiYHnDaQ!yXVxqWC8LEV*;Peo-VzYU&Eh7kQ1MscBK6F=4SA>dG7G- znF{9QT|uu1}V%n;4mnStfCXgc z-6Q$G0$0}UIn*xC3iNwu{9})qcaN_6jod=54-6dlKLg+yUo3!%;i$4!ynq8EfJZ0MhWInCu73ib)v-U;3p>e%!(wnX=_mIq)R_IAbofqB=E&{OT4 zE8`<@J_|VIcHUfp&Dx!h+Q-_>YYzDyk*yANxvZmc#W)+$?aqaB@K-qc`RTT}P#?G( z(s;Rr@5zka8g7Tp`Gns1YG1eG1-}W{*X_H`iLt#{L0i-24@Wz{*!y1eJ3zgH3{LR; z9_bpjPt@Ww?74meK4J6OxB=Vye1+e>7x*)JMkdDVHpUi=g>%+t?HL4fw~qbPUT>R6 z_fsnW2!LF{#;Z-PimecDZ!2SK{vcno_p|0&JQF{DzrJR?_1QI9zwz=EV%@VqPNT2# z9`z$`dq3ERm3hCby#vp|d$8Rb-rMFhHjBZz!n=}d!4^Q?GkArseYNNECGH>h;to83 zH0A>7o#ei_2K}9L?AkNjNsPF4#L8pM=52n+TD#wbZV&Xy*wjCawaaaO&qn)Q2J4VR zn>@A6^>rHOItv^%dJkJIj0U(tbbK9)_(<#h0mz`EBZ9| zKAIpijZdydr$aSg=EVUAWiy1BMzb(0k?b)V^kk84c${h*^-pYKw6ovR?D zPwLV02D)(0bKxf=jalY?f8@Y81MKFMBLV#;_*?zxSFnIJm}Y(jN5(1lzP5V|+k`Fq zv)#;|0oTx;J!A>^hD|QtN#^mK1k(9{x#t;BE7TVk9Tw>}fI0M{P)n!|UHcVlMU-(%gbzx5=1tMSacUTbp=+u1DG z?Xl7T;l$vD$!KErwWqK50Yx{QG;m{}HSgY=IdR z=A7U8OyTQVjI$5t!M{V7e+_CvnqvX3y#Uww2Hpa`1FpGXbKjHiBJ~RJwVoYtPJZu| z9O&38@`P;tL_Rq%$2vaY^BXvS0gk|Nt*P-9Vyw@4EBZ8dWORSVH1|5}&TE%{I`r?7 zh3C?H^Y4JaAM?3!Ju7}k{NxtySHjpKgl_C*Osvxx1c{C{MtQh?}2xuF$JF$ zUwdHPa=XqIyRq^w*ca+|e=Fk&dqL{wxl0H3lpO6@!l!@-Fo6WDSv|lBBx22L@2lhJ zzi%z*nYlgojIO`yz5@HCOvL%I4t1q|^7{N_Y_7qzuIR!1gnhd2=eosV+ha?>@fqJe z2;h3;+5u%@UO(3({{mdM_0$->!?y7Ux;)lloYH<-yE)921sq##ttQ-!l{3|jKwr_t z!#MxyNBs10Ud|P1Jx9>aDe?-;FJ8yjZ^W(u^wEqs^zc3d_KjzTi z*i^eNr#$w>^W)v^9r=h{w1JqJiot`F!?>-M6@5?c_FU%F>qAT)kPS-6a^4-+67pSX~aE zC;On>ett2}jyXFVdIjvKwVEr}4*iW2x1eILa$@%^ zZ09quB{=-#0Mx(zd4Ttq>%9`+c$sVKtbP4^?A9pEZ+$Bqb*4rjUj=ug&q3VM^uC`9 z+}{D$ZSAft1J`Ch?2EtG^IowpJ7k4#nmfPw7hrDZwsqQP<9Eb*Uz+1<;QW_Jxtv$O z8Pc35Ah&#pS`%`ccWuGAVAt>E;QogA4E(!Wz8{{WyQkJrS*x|Tx{!OY1Mb+8+1%^>H8V_YZ&#e#dxa&OG*KPuv3BvlBimux{&b_ZN20z!%^X z{$=c+Ipp!~RxXUqEnH{C=FbYABV*0C%)KUaRN(n_ZGKiN^uB)&=QqH(M_?VsZS_xc@6TQves92a_-xPI#CV0zn*H9V zl*jsC0rz%g>^{r$5uAZF<*~Qs*VnUni=IJ&Q~!)#MZeDdOs?oFHqYb(^Kzenb(`Z6 zxdX=SXP@`ZslRq(6Lvps57ndlW^`=@zf!yZf*zm(`Q5MVJTYQIH-E)n?rBbquQF#o zWA{K^8Q3#^=Gmln`Jdkt5A2csJ3UL#D6|Cs%;CGK*i#1!*)<5}D<$w4Zn}uUw|Jr<@^Jz9>^$U>Z zVH?soxl?Sp(V25xK~dJkKR|Rj;jb-*pJV6U!-&qKZ2rg=&oEd2(dW$>jt89kfNcf# zurl^+tM5T!T%(`AJnTQL6T7*$vA!X&8!xAPH}~Ex{aydnkUqBoDn1GMF#NaPuf#PD zWQxw?x(j~Rbppm(yV85kv*Ee>2Ke_fegpnFxCQ0D|Gfos`iy!Gyzk6wjmu0v5ag0u zyYED6e+Bfp#b&MEl@*T@XM20pvi;eDenU+WRlTIF>8&RN?8%$LVG)%E~B zQuDt>x`y8Z>orE+>yNnPE7TWaaD0o+x~=CcU|#F6{s8|o_!q#x5Ahv%1MG`&FTgtP zdi5P~E8H0wKqT8bk%;B0A@C`7(e`nL*vl;UUtl9NybH4F&mXsfvH_sVZ zleOKU`+R5MS=Ohtwf1LQPJf5t_b+4JyDQT81-Zu9U-&cQ55PXUw&(b~1bcMre{$%( zdV0$Tt%CeS`X{6KBBb^q&FXXQQQcb_w;wdb;yXYg20!DbGBN9kF1 zZiO$fuOQ7mHlgc(23tM;eWv!FKlFE4#m8sU+VqiQK{{@Ij6Sb@yFZ%Wu$!X{n9IAf z?bnPQ$Q7)|erJKNJxO34an|d-T?1d_1Wsd*ti$<)^eo*1`>tKGN&zHjWG+-r1cjoZ&-_T8TT<+wB5r*9A3 zV+VNVehwGrZ$N2royQ)mzDEL-Mb& zd^dL=?LN=mgYs{GCZWw*?X~g7R$_PP)_WedJIA%(<9l?u^>eMw7g@lNzcKda z3XJiyKiDgJgU$Mm7uP43$7@Je+x*tLF8D~zP*YJJ?+zpzZH~$oBO^O@>F6zAzxs750sv{Gd9oY2_#?+vErZR9%nw+rnIL%2e*v1eZc1v zuwNe;dmhYd&NTPlo|ro_HpV<9I+yX{3?^bcJ1;ro-nR=_@w4~a8O@CK&lRxtTjq{n z4=Qukd}7{O{0?G2JUgC6@0|eFpr31XZ!35NTVLgl=<9mG?imSCu-{$!8ub-PUu+M+ zH9rRxTicfyoJf6K&mL^^t;U&k1O1(|&K=l%Cv0WheJ`^+Lz-j(62zq<9WKa%E=-?&%6`55!Z!SRUn ztXaE$D`Ra%uAr0|J4opEVF7YvptL8h-QVeW|LIr2%AEB-j2_9kaZl(OANTg@fmmP; zxr-oH-5MW&Ia)qq0;JJ{1!+#_f4q*sw#=*@-TJoOv71y`DD4Z{K|TTI{8Ht=+z?Gj{yt z)$TqeWEz~oTt?q=`OFt$wliWKo=L}^)3;zkm;W^|hj4urxW1F~U_9ge+UtUs_lKlHoD{k8wzOJ4(}d0IYs`Q+$5FlR!K>RNlv=1ABxxPlfxGY`_C z-^>2q!rzbn5?DjQZ#yH#o2$UNM(>k-+W<_?frV36NA1!{ltPz6!fUwcSV07*819WaNqm7 zFb?Lf_}i!UeH%Rk<0p0G3LN{<)}IwP(9M;`d&z#8!~M7-t=0JUzCoVw6)Vub0{H?d z_7`mRbId2^Dlq;Na~rp7`2g&}#$oT&ucOZ~cOHGz<*#rb(HG#ebp@W+3~t!xpV4>k z#SHA7XME3G0T)IYBRB!qE#Gru zyg$7s<#Vp$FV7ul`Iy`8M)#)Gn)^9=&7A(LG2=U@aqgG*k$;ZlxrjF3{U$i9XNP@7 z7I4HSJRi=>>31F9A?nUe=pR9X*SftQy_cFTlfU+LokpDTes>ITYaH`6dJXQ;wqE0$ zTkvtcZ{U1&-+w3d4Z43m?;0M!8R+L-b8Tn7&&h9J^4xRCr*9qh%GmYFVXXDP0=Fa2 zr%~&+X1R>_K5%cX^$7d~Qm!Z1t#_^D0U7AC0`DsO{|oRnFki+#fi(AL_l(rnm^%
5F(I%S6QD4cfq9*K1Gcj_VKe^afqgslJ(KqXFeZRL)@C2h-~>vrZ!0egtidJp*$XoF9J=A;){TkKhZi%zY2-pWMzb zAQN9>)QukMAcK$07i#9q`_t4!w5y9k0yWzgNK8CSY6;UxBrH7mA90%iN0G;8}PLVKKue$>mGRCgnAwN<2XC(OdLHjj~v$i3-C2KG8V?(fdc0b z{EX%L1fJmQygBvJfBW~m?Mnjg?WzyHZ$Y3hphoA+#93cL`YGrSK;H?hwfpU`wRh2s z5Ab}Rkn$(|?7MNsJAOo-K;zDRpDW`l@SME>XLRef2H|I#DFxgdI)B2aw?LVE58-n+&XYN`XE2dNU4HE)M$O!}zI!S$K0gHn^Lynb^)t;l1NrrN zb)fFyU$L*AF?{8=KSwe5ec!$xuJ~J?jCMBO-htBu}jA?P`-}&A{n#=r)a_IJ?abhpP zm=(EWUfc~o=kzl+A=@~*zt5PzAM|IB_C1ezdt-g(NptVf$~ZaplbOTr*cz;Dtv#nX z?6Y|SxS#*H?J48NJu@FbnFoeXCf2$|t-aO~ohu{TIbhEC33&u^nzygF_v?YNYxAAw z`{}Fu{=52b(LLW+=2l>igtXS}Q_xqCu}{tc#}R$MzbF2?#z!vnU(i2)kD>iN@J#rw z^!x4^_*`Y;D=0wDt!L)22i_@f!8@@1nWf`5z~2G5Hz(j4eQpwVxwXkxv;lL-=~%y8 zG55Idh_M&e=f9`mxeBoD=Njns^V*Nht&FXCL+0z z_jKvw1=|Nu!58rBtDoO5efO@>_dq}SvT`FRqbHM+yWd2NTT9Hzm1b|KRxuH)qEyaeC3hb8eE4w`A2i|wOZ(hdHn40kAWNbj_4<#-F$ur^I1KE zoBRG=^ZWi6!1vor@QJyPz-MU%=8(VDgj#}s3~c-4bLn2Xwp=@!KYjr!DA<$G$C~+A zpS5XAm(JbdnXk3S1kyQuiaKaE)a;ng!kl^oeb0<5&~Cj2>8Die7bMWvxeQkA>egip zWA|?|4tbDfZR&= zPyPVfeAeZ$XB&?k-kl-fw05QUlIN(6*&FXn&zEQTOJMCQHuY!VIjD?30p&-qT>3lj z>3v_rp7Da+9<}~4Hm^Tx`P^BTap};{g1veKSJ3l}GeZ1?ZMxtK_gCOK@SGKDKU4dU ze}9?qwJ!Oe9C{9Wk%4vFyVoG8V+)YN9`v6hmwU!Qe)l8Qezq&p`jg1?ct)namJ|9J zn5P6lW@N7Ytk?^mj|zhKpYLb$$yv0439Q7d*yKq-o~NTfk7Hk4mws&?xmo?R}Kjo@VeJ zI1SygXXZv5KK9jqOsvPd<=uVfyhZvw;Vm#vVb0of^X+G8Kli)vc;GwLp`VinY@V?< zBlZbW{?g7x|Gp)+|4!8h@D-3#AA6*J1PS=_je9cz_rYGPd)Ivf{9U5oyPkm?uzk1t z8vQ-6hKhe7)_z#8<0tq1JK%|VW3^fH8c2MCvGq7-pO4@ZV|%Tx-M>$wOmKU?7xvKS z=!D-MyKD5nOXPP#|DAq&y8!*KNPD^i&%rvzuhBF54*&N2Hbap?V(|0#a@ zey2Zh&)g?%{%+ZxdHzHN0TOe@PGI{v`~AV+&-LGPZGZp5=iWZstBuPV-;SQWM7I|^ zAiv!0u0WX6Robp8|c1neeaC&rBY&zDcusJxjtKotQJ1Jo3x^9y|kY!4W(ky3hJT%pSN`D}3%lK?b}DwC^R% znJYU6JA5*TLx*R^cg7ctt$Pp5Q}m-Q_br12%rk+2uL60L))1gXKMx6cW$pyrXZ@Y4 z=8fKtEmJ3iqc!UmqD;$0PDJ(!1a*u>Fiy zuvg-YO<*^7)S#ctEx7@k+`j*^ z-_B+EfHv1@UyjH%y4@Uptk1QT`nirX<2`8c$gTdC6F<-7GUEd>aIIzN+U@fMjF+d? zL;l2A{R&nf?+NH5l9=H`=IFIrvw8u!!pYbi?YYor{9Z;M;{xpN$DNCwu)Fpq(QEBo z%ErxFoO2CS!8o+B~n z`Of&P*d5p5e{Iicf6wpXdJBAB7vNZ3=r5;hkfZI5`$|4*+w3dco{w8>*7S;4x!!;@ zAYWn5dMaEeq-)OdZuP@cfb+rJ-g(gaG57BY0nfL3FMR8+}5pb?gCEk6>@SMy)W|JjeELO{Hc3oPM&9n z{yj(@Ij*3Bt!{m^KN)!)-vasNvF{bg>slNuoi}bG&bkZvv>AT_+wW--x_rX*6j1R^ za0JMp^;(^NRpnUYCwx~Rhx`lDnkHoXxdG|AoZo@HI)1xL_X~9SSLEiSUi3fo?>`Ud z)?4sfknO)Oq73Um4y+aZ(${Xi6L1eBI=^dc{!PxmLvBq1gHM?JrNY9V_?u#+<81I>A=L}gu#paz51Ad?IXM=>@HOjpo zQeDo7UjGjL?$W>ie|hisWq;pHJo-~Gfg51H41E62zA2z-AUYaX_e*1wh;IcObB=k>6*R}xf z+*hpEpKr`@1=f;5i4HkMyH|!jF)x-mwY4wkTRyoT87uA6b0DvE9FcMrr1j^y_l!|H zgVyf;6tL}E!InW1(f8JIz+wNz4pisKksTuw(wgKNZr8KdvhZSq>b{<%U6dS2FmUE3{l+uu*N9(z!*8}AvHLqF}tM|6%ocDan#HzQA= z;^RH&T2_4-d$#;u5R9E)f%7|Kkp(${#>w17{KlV&5sqD>xx#pXF4{99Kl5@fK#oG3 zQvWpk6LWz*V^5&vcMR46bw5qwU&r@CpO@Aj<7($|&fh2Of%AT^aJ`;)#|1x8fw@}! z*j?`l*uRLxKSn?BTY)_9z;fUJe%E*Cx8T>{+jckg=juwl&sW{(e)3Tjy?b?d|y<_ow%Xv1k2q zKX(OxIjrLdPCy^|wO6F}4D5q_JI{UoicL7T2d=AtI`?;x?+kt0vz!MHe1Wfd&2fir zPtMvv8nFv{FmHa(+ZxbpVC=cxfo8);K4E>v*eA#4R!^V;c|OjVIja@@=741Ez1Gge zka7p}%J!-JGdlKTKQcCRtU&(uvn&1+oQ@atGw}CNKI1j|9blZF30nZiN2IxObie5` z@++A)xBmJ(gWnkIO^m-d^nU;NJ(`#|pZ?}Bz7AU+&ro5!f{h!Ev%`A_|K)-?I*0Mr zXMTM*ea|>oY<$cke~#Ycg49N%D^5dnSTm9YZ zZH@YeHc*Ciyn;-8txO>o;P?tWkGn(9;U1bx&K3AEZT6|(ISJEI|w_N)9XnCR8uwJ?32xNPvqxYg)g4|$O?z+bKPt0{MOrSfy7691t;vn`V-vV^H0GG;JRKN@SXep=9ygsKRy|od(iq= zd;KMPJ&AD!m9aK+c~`gwd-NXo@B8;P^gAu!n?T?%yY#3$YNKdW%Bn?G|E1XwHc_?Xwda*gg?0Wo@! z=FVH;Hf90ZHoLW$BP%O)GMB+R_Q;xFGCu)-7xgp7uB-j`#PFLyj^0n}TN&Hi-Gw>e z&sxTRLeI?W8_ZY6592If(T%mA+jj^#wsWg)zZ~ngpu65>Kz`?~E0FfXT;{O;#a}Nt23Ui&toXa8iZr*qRM!^BX-@64AlK-*eC~XHcL!|PefJc61KSnYKcA21U;_R; z;`47^M`XrVf4R4~jPBj-oOQm$_w|8ZU^7qK>*&4mY=vB4g4@4GW-j;8y0`sK#LDNm z66>GedtbP>`g^}kV2@2$%YvMkmq*y6D>#8O*nw8_ym5AEjxY!Fui&7-t|p=QUpz@&<7r;3=Rz&&U+LkDd$p6L>(k=l0(7DYq!-j$MOt2jsHm z6`3!+Ue|9P_x$X$io61I=u`D&pK_$OPsVDq2L)Mzc#zQ3+_^VfU^8}me;T8&^RCkv z^$L`JT8wki?at|^y?x+VqtD4pYLAgC2VjjWvI1@T1*l+U?Ed(70sJ%Y8{qz!@2DLgW9+#fy|G_k z9EfGS0`)J!(;rEFt!a1v`R{Ywq5l?~!M_5(1_7)g1J`f81ti9mcsaJ8Utb=24$o-~ zVyMHt^uGJKJm7x<=C}pgJ)3*>)!*;F1FyhK5W{AzZy0|IlyaNHGjj%V6;MSm z@2BE(`D_p7j4AAa@3S+=V6Q!JXU==40?&}+i8*tm=ruZ@wIk(nUE)h%4Yxr5JTMJ= zJHHol0c%pOa3*Ae$J$n;dWoKQ#pciTJ_if9f+IMC2k;Ht&w+ls3$e%}(z>!3XQqwO z+d0Ip-=uFt-|t6s`L(;APe}j%muF!G5xpPz($As%JN!JiW*W@~KB@c2%T4UaR_co&wYv1?4wdUx4j9N{u zD}ecpt>CO)dv5cqJMW$uf1I%)ci8ODm%!&so)!C^dEr{L1-iajy%OW64nAdoF`n=_ z0?$vY8{TJNt22$WV$bdE4qSn8*4grq*E(FU2z2MH$2F8OHqRZf_KLg${g#>Bb86>k zQUdeHY3yEG?c>d+U)G+nGv_C&|L8v#etSFXUg9 ztfBQ*`=#F%ao^v+o+o{84n2oF_QW`Ina6q8o{(b8Ywi=6fN{picXQu=H}e8{Vr+h6 zvhm6~B;(9{0p@hAaxI`RC%18OSH{}q`U2i}V7>IS*Lpbo3qBL}?f0xV+#}v6){*cj zz&@@>?+p7Qf6IwX!1L+v_dHLH2b(eWLRjlZaOKSTTs{KplB0qN?1nUtIWsViYq$6B zf$KJZWlpTjU9}B3U$B`^ZtHy*(mE1)AR9(4-fQ20H^3S*^Bw&j+3wetBjCuy!()DP zSa(7%Ac)_C6L3#o(-XhfnZtKi=vTXsJxIX)Z`|0OPh#%-xT2d&o9i~V;=kdD?mNc1 zAHdxJ5&d~(EPo{~khdVACuQ^;Z9V9@vL5XAPG4gx_U_;Jtza`pnfso#yJGZFZtJ*$ z3AhGhjoE5Y7a^FJe~)Z?ip`#RmR4-O!`pLs>G8%rpgX_SR~VmxeMrE)$Z(o_0?KAX z8t*;lUG#~uIn2Ai^m?rS6L5X@R6e=4yDJfYn(>X!y^f#tTb@IY^BJ?o=Zrmf`X<4r zVm|?UVcpgoLoP^bEiukKx;--A9XJBx{h6S>=!#LKVL*8K2a z!1n)l7U<^H$GgY=*oUWwo;NdB#yZ#N&s!iXNCR@G+HLl8s|jn{WBu);caUq|gGYFM zHVbpEO{nW@&L-i~wgS)Sc21RBjJfo+=idF>dhOXqV66HN>qJl3vKX~mdjRd*oN{;R zSS#jnC(5t?mxtcRGkmw;hH*i*+L7inf2+^<3-d^OYyI}jKCHZJ-B0^okrf}YhtC{x z+#LL_*x#2AjLngv!#o*(*Dw6ttbA7>zxlS`p|od=xE23`Ow0u`gVixGx8sBq&5o?m zb?2K8^&iJ`AH(*8KfWy&c5Uh_QjUe38NDEV4jd=!N9{9u?srG6-DkyK(9ilYpQGnD zCe_1e#}HC5oe`ifod6)1yoMJ9Ey z%w2l-$bSMi!2LF-&u*>64nOrhc6s~vq23qA0ZNQ~=&sdu6m)BnSAO61>OSMfc}{BJ z%O?l_!1oGv_;^;$XD{|(WqxAZV$lmW;haCC_-q$Z`@JS#1#%iwkrTZBKFRNJsS-C+ zJ8-S?ERKn}Lrx%%u|1O8y{^RVuv?ql{(nWt_Yxm-oWTj`BY%c(;>@{UEBf~56$#z_ z^lp9&yywhgUGnb0mQz2^mN{4EBgXz9^)Eo1az$o!eBa#n@6NwKdOqb`nX|qv$1QrT z!~kvP*ldj#dz%k(j5z(Q$!EpiTU!5f@DZE+bS~p>9=R6Bz%?inoM}e)XGO;HYtyeF z1K9hCv36rBu>R}Z;r(BkGnaicw^HBCyTg8aXC}vw)-qkir&`;e0B%s zv2ABSUZ06iNPFnBegjr`0^7ExRx><)+rWu# z)9&jT-_bvL6R6C2e_GG;1M{F_FTiIfGoO(YaNWr5|4wOL zo}VxA0IDx`^dwv5xurf~Af7o04m!jKOAT_Q;2ZEBdI@& zj_TAA*aN?5PK_ra-GA?}FAn{_JfZ7v9?!`U_%SXEc+4{axvjDNeFo$wa0Sl$jI8*} ze+CJD;rFWcj;oy`Gbd*X)&UZ+dmzX5UA&^Zk9+WlZvUNMKsJWw*!%P5(7&(QYtNQ_ zu#SRHyUVdz@9n@1Joa4a9()PR;Tc`A2S{+N#MhbJp!EZ3O>!TRqJZr_X>-nN<2?D^ zx906TKrk-YJ$om8;CD~#m3y=T*V*bvx>j@Sf$Q3}*dFhCK7x1fn?t_@7ND>CNy*0? z`ucN+--E0}Ki91^uQ`Q!j-K-qbM_}j&AZ{N&$edsBz$~Mt-<=O=Q?}~zMd&jf|}-j z=Jv>2e5|$kAXnxmq3MRmrUSf4dt|LYoJq!V8-JXU zz})igf#*SK9R=uf3j$r=6WHP==Ptd+0f+t-SeO1g=Cvy){3^PA%E%npu*VHj}p51Yn$&eJ9JT* zGe-c=p6B@md*S(&KjCu%@~j{OKil^Tb36AD?41J>2v7olo+EAL^7(muKzDou?&nss zwFGc|e&jv_kkx7>99maq1?2j*6= zyYD}HS@#kA9LQO~zhv(JME(~1bMSZIYv8j~iM;}I6gZDFeBrqF;*8x|CZ1>SDgV26 z{{a3U@V|h61NXm0{=dk7jr9Ky?5D^#;1jX-OAh(>AmFYb$hFn3kKdL3Z$SU+!Slb9 z@BhH|_hW3%3j7Z2-vg4{`U_IH4mm5_u49_%6T0`0&y>D@4<3MjXXw|U0DGan0Q=@% zIQ9%oV1f4uGRX_vhrnh|&x|=;oBVIVYmk98ZvWkt-=Mz)^84rc{@GE0N8;RL`;ve> z-W%0-D!OOC-J#g-7|XGGo{@h|U+nV>VEPovJS;4#NDpj};mxhq`u`iySeo;lwO)~rv3FT6YFl+uBCr1xWcZ!-S~j48+y zkS`;BCl_FR+ehvfrG9rn8NfBI=9sAu{skC&2HNGawu)}<8|K9mpx=*ukNBn9^9KHY zH`oKYkHEM$;4ARR_zJdr-r62#){T70yt(9FK!Gb=I)^?z||(wtUz!@MGKp&eZIm*Sps9kSF90d;sMF zYTxVqeLruWuRV72S)W|4)#qZtp3twO7dz&yI|93NNONVRb>4#Q>^`~g&w~5wGqD5a zkWc@WabkQ{V)OS?o~y!~c57Xco>6ld@6Xbn#Q-<(pOE@J{HSN1XLEWEkNS*te#ExD zcenf2JQ;Ye@0qv$3Z4_^_c-_A7HQ4}JSxX?^mBZ?S8jpN=E|J)=IHyAXZ8zQVO+4+ zF?YoF0F-B7t$F0I-ZbF)+#7vP`oOdO`9sWoAKVZ3IWgxRYd5F;b}bpWZaM6u9PPeC zde-meeqKD^2`-<9=imj{b1waRvpygG-MRM6SNuQ@1n4yX1b%ymQ-kf~YDiM?#SO4#7eGdAEp zyQULT{ROf9Hv_gm2XvpTE9SoL0$<}~Y@Kqr*9%C@IdYD`r10_ zvvp-qi94wiTaeoxJ)pZM2~0}-cF0hv;-d4Z%RL>&IY6(> zS(i1KtBui*=mA`V{G0TweQ*qdIpda@F+3-vb$iCnNY8a!>!tTn{=m=uF{kUOz`eK* z*#Af18C#GUe{<`)-J#2XEjEb+C;10`uv@h zcelEAPQd!CA&}avrSS)`K#l-A<~-{GHeNjXXYMi9TmugJ=J^Zswe~#Tan`wFbFbQ( zoCgbIYw`P{KGr9Pc?w93x1aGc^99K7Ug=|PBBO^0>*@RBUZsQn{f<53Cw~B?b*#1X z$Yl?|yvGiX=n0%ajeb6lb3Y57=TE@%@DvpI+wXMnTj;ZAqo8NOS`xD5y(=F(-5>8W_t9E{SmW=|cOX@5y_WStPN2`i+!_ekxoe?O%8D$wsZ)@ptFtjHBE_w^B6 zK>%&mAeTI@H8Eb0<_aL+C*bp=-)SHr<(I?%#*BNk0QdL{@Iznm@#B7Od2+=Ny{-p% zu47Hx?}N7I@)kbzjI{0tFb!^Xd3MO6oO?a$?R-VIU9p?@cFZTw@1^(JK8R1iTyk2w z9NKrl+#-PhdvFKZ9DeSdeX<|Uttb>|kKpWi=RPsU!!scu|GR_Ljoudn(tBhf4RY4i%q5Ao&;=Iw`TQokZ^?r{RzT$}kmfUV!g9fAJx8{dFl zK>_wO0)ehgDUW`4pa63zmm#|{N{r`^o zDf-vIoW66myVbF4JAu4`p04m#_&q~D-`4XLaYwNIz5ZXK-+>R{6?hA_KesZs>#4+< zBf+;IkD%Ro>}7^Ofju_sx9=;``pxre@N=*S=1#y`y(7Knt;^V`0G@)(prV^EF{giF z+e>sk)pYsn{D@AGujqWGOx5&Kd#?#jOWnj(b~=7 z?_$)?An-}i&qF~skDT_Z{hjui{$bO{dhNr8&W~^HHu;|2_n$W=@+9om6WANah|SuA z`MJ+OVmGI`xU(*jDtT z`-Gmbd1lO~pU-JxULSLWXLIg8`WE!_+|Tt1-81TYDVWQGxF*r%)yMj@Tgw?JAEX6 zdCe`4_7$7CC+%Zyd;jR0^u3`2a}wqkUJP zX8R@Q8Ek*o;1=C@V~lSx^R~8LyM5hbbN$=?uF>l_4}Lkt5m-wG@=gN_@`ts~=ouj8 z$@AwsX~GuRcKX-eOY5(Wu^U$^GRE0fXRe(?Y=JK4GSdqnmp?xR$e<2=H}>dsA^yJi zt*xK=qz`dFjPYsv?)k!AoYM!WXQVmWJw5E!@ApZcUwa<-*+Zr0LfQB$@$g!QJz5#p zfy76@%y@e*JUjG!57h6!+v#@>`R&1qZy7K~uE{y1-235YZTc(qslhp(*+1|l{1dRA zoBPk_tKw%53Nn}z8GR)-As-oAM+Oz#spFrN`1l;Zh4=dd-(`@SpK%KaI0}A#NzQ)LHe2U&nea&qS z-&N0n;|$z~GWhN1-LUVGiFo-=K>iA}t8aN)59ZX?r>@TqANNQ<*Y3|H*5sM+4)$GX z9?yHh=Qwn^SH{Mi}tTtKNKZu<^FOaq%KV@2Uh9c=oJOjta8lOYcjG?vuxJW-e_z zGdA^{68l6L*1eDeyzua=}>0A4bj-IgXE`6@` z2-7G0&)@`%k$3YewR2VM1(-vh04q>$JlO5omix$f)3-mf&DhNCIhl~w7C@W4LYtfg z1emlTQ|)VY-X7>v(Sv#K7^V3#DA z54=YPcny9z@I7+HZvjD!{jmNI;N5+HkLDqNg$!VS(hLs$&wjr}Zuk!U=ir}#zXN{_ zz5*rsxqWs|J>&N-tp|+XkJ?|MPr#lA{7(=4zky=i|6f&S4cLm@ZEuZJFq8BQX|rSeLwQ0F<-{Lex422(fWW4 z@OakF$T#@={@{4W*nZy)Snuq~-!t~#O8L*=PvAAErT0HP_C3D4K#R`3U*O9TJ@OU4 zFTh)H0vXt6IsZ83zaf8RUH^x5{O_SJ@a=(p*x>QJDl@#EzXGiJJ@^6G=U>4;gMR_v z0?(8+`Lmbbi&@7%lEXjyx3?RJ@ZC7aE;6_RV|`vM*zAjIw%5XabsgV>MEr-x{tjd= zZLYap&)DMNldBHc^E=S|@RjqP`7KhL`K{kRnZx;MT%(?KIjz0GZ=Kp*n=x_-bA64} zRtD^sdD}gg*fvII?2AYIF1_cukOKs;;lBX)&wct&;Ab%VZGX&T-A9mOe@^T!{frnN zKt>sul7v*k+D1*QeXY_x2A}` zJ=4&qwd*#ddmjXPKk@QAzd{|(iT9j(&NFcSiTZqA`Kht@+&U7vqdveI9Gj=$qum;< zFEO?c>doKwk#R=X)`2}R4@kKZW9#2JMjE#;j-YWcm*0Agm*)u>AF;I_FxJPO)S(ZI z6Ju*w7*D%Dd$*sH(8*)W@v)!1_^7V0KdA=`x|iTRxB>dN&-&%8$?eZE@&?B8sI(2oW>E2uK)tbY zDkF1|8Bb_)4`;53&AKbdAQa}<_dw}+mdkkOvp3tN>+G31-oS?>qs|O-9D? zv*VGt4cr0q7;jB-e*g)1Ka}75lXbYxJwDdh`fNW_*OkbSe#qrnO~4$VfM>)$==;3( z_oE}+-ir%*eFAYYB0&ZVes#zb;~mhp;M=YVWCb^m=z;e+=MkUVgWUc7?f%lQi-f*_ zR6@HB+F4*2*Dw1pznee9V_o(}?j!gK{002T_3iiX_Q18?GM3YQv^VYgWFEo1W&(QG zquegIP?PsX^E?k}kADOI6Fh+Lfqjujeq%#qOwUi~gFu(j$=&y(uk|A~^VoOwdt@%` zC!GE+`k#U8um-ualD#`3dwr}Qy=&2K?`!PqUGR}V3^>_5{fK$gRUpR|+=J7Q+9I$9$F6gSG*65jX&vLz z^SG9yI{c0xGjH`E<#_@H*az*Axc!iAADB<>Ld?_^^m%0t)CXfgV6ML9#K-rHwl~-o zkkB<>r2Z-Pen(;gx)#USdyDS_Pe}i4FE{^rAMQGHwV2DOJ@)#&*R`{Y$moRgJns9} z>;2jL!!6h|PH?#Hg|T+I)t^8Z^Qp7%BBIydk99Y{;<#V0XLQ1zRrIc3$ZxDQw0l2e zxf>Vb1hgwF&{mLBXPhWNo!kMryYw|w#^&6xo4+vMBkfBXHtp?$&e&4EP8SD%2=ybEv~K_GUnePgVjJ||#JmAQ5~^-svgkFNEZxqjM|8Ck$w zubdU16Vfy7K3e;%A>eDQF*8SIEO)?n+S)q#xy^Tv2tw&x0bl*h7ts}9e7nBsqk8S? zwd=1vIbO#ej%SrV^WL0@v1acx6>05$BJv3KAcHc-zdv`ij9f_XjmGQeb?h}gzn8X- zg?YelLt2wkzsxuywcmpPT+58DZQT8>&038~v17L)?cumqn;v_>uAjLbw>~L}-#`R6 zk9~deI(M!1ztx0q0_x>RE3wD|?595Fj?6tn73p5;2}jKRZvvS-0HAyFP2M zCf6zavlE|71z5{GGmG;mcfUt}2lp+A=-b%)r`mR$C!5TDT{pseG1PS|EI&y3?^|N9O9hCG9R0=FPwdjR%E&IL5d+}gCY zeRe0W*mn+;&1(F0$#cO7?ZKzgGImSx_htR z!1-tJ&){3$vFv}VW5jICTMtne#^$l_6}x;n_8MHXzKOYYw9oK!FTMi{2!);)hm^~A zJooF$I0AL{O%7#3o~It^-2F_}uZ+ymC2R}WjdQkRTtM3k_&j3&%pdG zFZMHIYl&b3@H476E?F$F2V-0BkWBZr6sIl{sC zI`lq|j=u7 z@Mq~)!0!b8bBiR%;aXi+TO&I8bL?k9-pDwJ(#O{1I^92Wy@hiJUV}HF4t`@p?EBQt z1>Qd8J`3y zSimP>jPW0k-+}LWR{6cgJMbRd0qw0G#yL_tD9pBV(>(=kji#kV9YNurT9J_VdC-)>VKC1(s-*$${sh_!b7x;iY@}AJQ9K){PM^M2tcu5Xp&fsVG zBhqXB2->7hoSs>0{59 z+%Fz`KK16!_}PzT4G7yFmJ%|GHK6P@xnAQku+Qq9TWdlV;M`mp+0Lu_ zi~HKIF?;m-vFA834%m%90^?2~0riao8OL~c>3QT`81I37_qqzmJ9Uh$-+g#uegv`f z9Oe=3qidPco=kvz~3AJoPqVbu1}1wpc3aky+mj1J;;qqeXnQ!d|t81Wvz0x-0<%hS88y6KVsz9 zKj8bIFEAzzo%-MRRA2jI54>mUSKyXcS&)tc<0M$S`dAo?(OdIuBVJp^#~6Kc>Hf}} zKXavA)6W>!81bp#IJoSM&%Gz){Co1|b+0{F3EvC~h@h5{(=m`U;O9C$NA|?Nx>kF2 zi|%I-fX^-0ABcNidwrg-`EFIvdma+fJl5};KMol09{&jLz@NZ9@Voc*UAhPmrG zfeenTT>buPy-lYc%y(LQ5W#!!2k>X`7w{W6fqxJF2(A8@Y(zp zyaTSsxiz>>>ng;Rm22-nyb`E8!{NT|fPL5hihPCiciOGhet!>sdF;Q(^ey9;z;nLG z*Y(by8SQVTU`A2n12O4hyMcZ!PD4d?WeWB-@HEf9Toj#K7Xc) zjIH+ytg#^7BYU+0*QTxxz1$i6Bk-Ks*N2t5K>=qF;sCMv()ZVRYt-NR(;f(yMcng@VfDAxR>sW zoEiW2JVIw|yT6TC`znw9HpdJ2D?ZlkK31gT_I`#wt@y6k4(NdAapH!t>oa~nhx)kp z+5TFO&&Lx|Zr5a;+S>OuD|-X(hm_N6bYot@0^+4}q}bQ*+T5EY7+=SDLq7ekNA5?V zHt&UZz#KL9d1QPXI&JdULwW6sT-JF6+MH{%M;YlkQ1=WLkjN8&zUGJ^g9NNYyS-kh z&ugXtc`Ev__I?>}j(7OU{|0;mb|B3A$cVG2)^-B+G$D;~f2}9OrB8{SyUgA(-yPsf z*jD^N0s;NPUi%zuzQ_f9k zUVIzSMSQGF?xGF&aa}d`bu43i9qW|aJk}Jz)1{xI@aP))xwYqe^aT#N0y31oPV;2P zoFDfs;#XtuQ9?F7Ha}-fk4|xwN@O*l{ zUjxtG0_65P9J%k>xYr*dIpC2mqE|Xkj5E@;Zj9x8)zCQ{d(V`+VE4Sb9_Pv+BH$BC z=apL&5aX9?(b#vyy$AN9GPhpWV~zlhjLq|w@q%=|v1guhYuqtUrS~UC#`JzeH~T4{ zyq*=;ufH7T*6##v;g;XAXXZHg3w(auJI{sZ-SZhQoudrd;uz~M@__sS*t09LqBq_g zays@L$$bKzNqdwpy?62~j5E5(I3kVBARKzE>+=3$|2(%9Bz%SQjLfrNrT1w^Ohvz7 zZ{wkN?i%HI1`*g>&y#y1mv!2AYnCGdW6r?d2kl_yRJT)?dc53UA4Z*w-G}#&eUC#N z>qENM0E{)}2{?~Pb8XItuEst;F}Lmv^g9CkW9{y*oa!U~iLu-ZFg_riZ(#n}M8!wD zx&r*O7N4cA_apH0bm9Nt-hqZ7(E%KZ|R#yQ2nfWsI$6lFd89tsX&uj&s zK=UWgx;#JD>lqSzU|xCKK1Al|?uX6(%R7A&cDZlIbII$_Gvs*)*yN}_bBM{zqdIa{ zWV0h(lYZuwJ5A_U`u)KB$Ni<>JABTaFFmFZpYgHA6PVZBu8n&>c`JVM>hCxM_tbl` z_cZyeIS(Y{2JA!Bf9+?-{W4yDb#l*Z&^k`ckHC+71zbUjeQqAls{j7i{MkxBIjv8= z9aum>C$y=v_o;k2i|(cMMv(A1A?0>I?#I}C3G6{6R_s{wzhl0D4alj!Fb<#sIiA54 z_;F3ObRKQuJbGszpN7uyJy<}&-Y~rk*>l$bugGC&tg9fO&Sy$~j@~dyu}ZANa;` zwiCK+JlxvS1(bfB>wg8F9sTw|{!F|$GxojYJ#egV0Z-)gKCu8}3j6`-x*W?@W4|_) zk-6*2jxS$oHsx#%zq-*@4LQv?B5d|+qmZl@IB)v zw=zWjhi+Lr%x`@ec+GmB^BMmVX)X37uITVdAdjDGa_RGgjKIG7UbDezyuRsQ zJs16KWH_v~fCTjaU<~&8XWMd3zCe5+CL`r^y~Y*h?b<8YL+7?W&wqOlX#MHmjktz{ zzxRW7rpG-bz;i+;LIDXx1oL*r8QV9@pOrl0>OD8_fIX}Dwrh%ekap+t+<^tS zX4lm8@Xhr_bPG1moxW%A1YB#uW)7vb$KTW% z>$Jzx;rVQF%!6ZmQtWVIIN4*!s04 zpx%#quS4Pa{Dd^0b=I-J^SkzT9jpYEUcdQ6?CVTQ^yajet|v0jNY6uNY_2Q#X1t#y z#u4~&4er$wuvcCq?f%NRans8ZLBY+QZr+3JjrM2YdXB)o)~~GCV$W%g03!1Q{OD_4^17G%F%Pl# zXJLKAQM*WUReIny|cHJ}co5%j>uTB4r@jZ~=XOKO) zf@ffF&On+cRm7jLrB2%;mLN;k9>>vFj_p8wba+ z+YA2o!ng-u{`)aDr?x9Fx3zp??6o528R>oO8K^73=U4&(M1h}cHD9>&Iug3V{HPt7 zL9=0Z-PSL!KOcKvk+*^?^Y$!eY^)ruk7Mk8LA}!RVT}o&g?VLckK89~+yir*#@IEe zYn;d`H}VN&@Z!+xI5JMyqITBhSig$gUqX9F)L(9W^w0WGPf!B&!gYQEfoJz$k*^QE zr)S0$yYD+885f7eopEx z{bvx@E5|)ZW%yu|FGZZe!rb%dc~RGL4V#?F_bY5C5SY(D`+9l6w)QiVux&uDLab{p z_`Zbq1bzXYy@0I%`)ECx@d@9|cxNn_=l3cHFlO=}u?L_$gV&G!&o+NTddB4`_-||P ziFL?7`)k~Oz)Q-e(YqOR?vewaH!3ElBgV_gH*0z9J60`JCS&6EIH!a-Tp18J>2(#ji4U zKI0SPGd`|)1MXYIPak>ZmrLFR3fO}P!V2(lEWf@17&qmK`5QQ0^9}e0)Y$Fbm)rHA z4!;f959JXwetc^o4KVK+Oy5kr`xA*5_Ex`y&V7{Y7TuzL=oVxKUS}b8ct*@;jDDX$ z1@DM^^Vom>^1c&*eM{Kn_FNldf0PS20qae`9P+Ba0@rUZo*0{>^&0-aMQ4q7=&Yq6 z^$E_w4&>OS{1NQI0<=AY4XCrv6=^Q75%XE6eb6VA&ddL|`^&bD^BQ|!V(EVJo`CC6 zSEe1^@A<7e0;T>1-=M_5{ayPNf8c%C-Uaj-)B-nBc;*8*0_XRazNRa(0IwC#jrWz#;DY6xPhc!(duB4eCB8;fYtJj! zGg#1X;5%>xFTi}Zw=)Vy1@_E!*t46*p2NJZQ`^Hz9I_(cz#+F>FM<5ufDb^MHMpLy z!5w4I(f&gGWADw5XOQ>EI&@cLiP+P)rb>Sf&AOF8gIT||oIxhW`mD?EGw072_Q-pL z*Vwn+|fo zzCBzU_znD5@JC=TN(}A#dwJ+**d?=vIpl5kQaI%>*1XnjKMMNFI5PJ4FnuRb%I$f+1q-^}+IaU{@ zUPtn8K-&@g1Na3PXRcnKwfFkppmRS0aJ_!#<+`lf{g*2NYfoz*r-(oE9eVxD^}3T-8X3gI+{sy>aZT8IG+*5=5as(eig3~3>t--F;Im$|IN zJl1XB-Xm{88F_N)=RP3is~|=sQ0IJphu`Xvf9TB>85fX&de@&~=V&_Af%)g}AIE*F zmcG8{R!3iRB=(89>yXEI`=D?8K9g}`?0I)z7w7Fedt~gog56l*nfL{`&u>6M_iBti ztM=Ks=f=Hp&o^{(C?`*aI}Tl7yw?YuK?ZW$>v>&X(Rn`gna^8ma8D9C_uBpN=WA*)b9k&&VgsEBE5fZ1Ky*|q1=PydXNPfft(r6 z3Ib?){=TSCD}`i{rvN@C!g*pR?BQI+a)CUy%iv&wlKYNf1*@ zKL^XD`+Y*M-Q4ooXZI|Rd3#Smx4(pTkGSF^cfqdj#&`kkI=!$K;5qgB*d0c0_|%(Q zc*f@E%GmqNdmvw3`x%foF;@nl%~}$5d!5(F{SW>Vwv0`F_onD4u%DR!4DT<%JjNeK zpX7Ai1sp-f-?=ezMdq2gF`n%`2qLjAq-$`U-Y2~WoEeu9^LuXhbpzUxD53GhIVUay zYfl5tAHdl?LWaj4SBbe_`#!mDYrleac9a+Wyf=uRCzyxve)R35KH#Js-sdrnYwxMO z5E;lDO7BfZUN206@llD+{{RtqxX6V}eXN9Neap0SpPxixLT`;PZSYjQ5f z0(|DU=k`SY>7N+;v39w3pho=5$csILd+;s1o-yy)+VyMa0oxhK`3dN2o{W#VtljI# zUg#f)bA8t5GuQpOh10qh;QhJalR(7gd!~Ca{auIoo!bL>E55?GcD-V=ChL(;-tU0t z?1aufCVcIa^H_Sld&Y@z0M=TOo4(}HUt0unS75y7S8ks%=1J(;3LvlVeGOpUa=$HKRFZ7mP#M1%fnt78|yREUS5Iy zH`n~0)EogH*B%b?_xrBnBjXa`MQVQ-()(Zr`b1>K$2ACh5+3_|>Ad#`?AFnGr5$8+ z&YR9Lc5B(d6G%g+-uMVAW9Lm08yO3ALLc`pFg8bj0AN3#Dl&%k{t&|S+5me?`9UK|7x`^_i?uxucnnQkVEgp_#g$H;(e4oBK^!jt@K62cFh%KQL0hsq*J-l+>AoZQx z+ADtZXP4HK6(9D&YfirfL=ZrYV~vc>r@a7q?aPUAz$XE1x%60b6m-^C7;Ez$6o@fa zsMjYO2h>@61qsN#2mjw1neT>Pn;h2VyO7^=+1L5AYr>{azxS~Z*vxT7r|pEy#;@V? zeQJm93<7up`uj}y0r>Yv&GC%hyk4iDfW3YX?1$^Kc6pWh?UC(%7LkmPK<*0U`s%TN z&*XjE`sLLBPRU%U&A1fN6(lf+H7p>52!cR=LKbije8;lJx4`wN4{`L_>yNSfe_Fdt z%;l)S_;W*l*EDBleg!^1pO6dEI$s0F>b%di?-e}%D>WS8LT#>tk-$RG3E*dwm7z+_lbLw0lDEgVmjLmDFmXo-tlOutvI&d1$u76U32-Kwl#~Ruh zXmfP!JfIJZBT~ISn||<_L#bZQ9plE$*tKQktXmFat2Xvg1je3MeNyc3Z2LTn*xD!d zIJpXAy<+4g2dzxLimY}Rgm?_Y83U5gvF%i~9#=gNDF=fIpT zchuVJifgaiHRhtS=XPv-i9J^vGY`z=QMwk#-X}d{=2F-Cq<#22!EJBskv`rp?~$nx zKlI9q{~rAU0%Q9dOFw(3dB&ORTaYE<GN8d8<&WeD-JA3dur_`5S%N? zTalY%KJ&dvjB5#GcnRCgw#QI1gXz zmj7EIpSGkN_R8F6qR*4Y8~c7o^acFBo{CM~9^Vsi&u>9w`~j&hU>BZ$`}h0nAV&CB z>`2k(uD769$4CB!IM-?)J*&M;j|aMxW7Fr25{|&Q;+# zgEx;I^E;>=I(r=jF}pP|=p4#6N9UP3feem7n>oX!qZVZBH3an5p)H}SAUa2{)OU|< z0cFh9PvqEr3exy??o#;7uP&641J26a{ZrS^Xy4}qzmPX;0|i)zGRE%TdOGx39bD7j z{&@XcZw1EPBb^^X1OZ+9J1LACuk$qQjIGxk@;cUk_IzF+nONV47i1mw{o3R7HMeKO zapOap`*S{GO2md#ubh9!;Pqqwv(sDRJ!{q;*Y01C3ETspCE?KbcX#PIgE~IB^mS+S zH}LqZ^voW?ABUX(p3NtGPQV<2xDo*}B6r>kv02}Sv>rL-pYN;zKJOFnz|Y_(a1Xrp zz5(XG1<%Aifh({##(OQ=NA1>egM2``o`-QSS{R>!*V&I?0};F%@ACzl^|)uA_5Lo~ z*Kf_&k;l3h?5@FmF{U!lF|;$LPA=DziE-T(pLsUyZ9i+>w#ROr?F=XD?|RpALhr|V ztf7J<(Eo1j`1;_x08XjL&7V8XKhNh5`<|Fs`u+xAov2FMWxt_td2lg3!2keo1_yB$e zFM##FJg{&0T90wAJHq9f0#bas^qwy0_TYw?d+kWa37?-H`*ULc%woQR&pYfdfPVH; zv|Pm4vw+{5$9|>)^Cx2FcVKd8|A75B@D5nZtnHfv9N<0p9^3-Io149|f5w-w=kiB& zeC(ll&q)97VFLD3pW`YQ-vhw6fnSGo9cOHEe#EcsZR~w}`RMxlcLztt{+nHY8gqLS z!R({!llvENci?*gq&=x}!&ialwUoY}6EcJ+PgDzDFnQ0aWJp(zV|q zJ$v)dN^j7a;}ueF?f2j(d$IO4Sd%^2fr_v9wTx~4esPFhXIyRQ{`TA2w13C=EjVGf zZ`PWywdZ*v@5&gzBl4?*++LsXAU9zBVW1!vV9j%EzpP(B$L90=o4>`co%u9eX58u;vHP)i^ZO#}G4@;P_!W5l&41VHmT~JN zJVBndbGCjkZW5b$v=<=P4%>pf8++n4=K1zo@Vv}-0nei`)}a0#DUY=mIl<)C=Ofr@ zA8Sb1w=q7UKZ6J=dSm520%PUXCqz&>&%!)`E3jwA{eEo;-vT1A$I39qsdWE{H1E!M zZ1-!R{(jw9uetP9F4*Mo{J2K14P%^dAc8w%k=7x%_QYIYV>0svY3&>ElOq3zu7Eh; zUOCo3A=`Dx*fU&^xda>lAJ--_wi>%ld+P;u_fxxTFjih=%YoF_c|dO;3iD;eB=l#- zd92I)86RUZwxl0p^#SR5Gnd@P%IQa4yii~1u}AcqK0toQi@2ZzF^(Ag3(zlW2Nl~M zpMW&q#`pws=`rSY?ZyOLGZ)4u zUi!Mu%?1zh2o|sb>rl47S`%aQh+6Qy^lQ02$NmO?bTvYb-k1pP@x6i*Fh0(6kH5W= zV*&dyE=aFq-GKTQ{if-A?!?$S(~2p zMc%82qk^=4x$j%QbJvuy$$LXgL3)2XAp=rQQSnn(@U`|UxW#Ty`|m&XJ+VKY3wsmr zZNN7*^d2mXLjism%bk(tmrrhcbOd*6@5hTnuQxOQ1UwJ>ONVDwU(vW?*Kha)5aD+Z ztig95>o#BZJpsG*J^*W;+?g2p12Ve2nYkhtp6&Df<)|N6zLm`c}sD`m?=s zKjT|0GM0WGW9;?FQ5gqpa_C!Pw^heLy*`ogks5X&Gq<15;0*kE-E(pR_SAmJ-Qq{A z{>CMcORs4^{FUYi=>7O^FW)Ed{UZwCJFM^d>h3@Sa^3^^oV$LpFt!$L#u{@3QQ#ZK zSiSc7PUm`z%fNTa`L1qVY3*^=D%WY$cxF8NuHLhkV%J5awG`szPM|V=LUs<<+UE&< zi2V#{Q}6GWdH=}hwcFFS?<>Bk>y2r7(CJrzx?K8RwVp6He)0x5PhjQ@*j$gf628te zh`{k4z4HWO3=BHtKCNA+E#v2TX*u1C*yj;j5OG|m*b4Ive|_dz$>`+Q7U8j%1+==+ zHD2c4-|xXr8>qx2?8XUe$jFZu=D@mKgV4`99P4MiH7v}hPTpENBKpYK8r22nN1*h+ z>%GyrXD!B&fA}qHUq@k{f!_(+WBs&cum_2G5ZFTLeJPARGglz@2INuNha8bBX9on4 zMqQ76-(8m|_(h<-7G&)473p|)K>}?V_-X5p$b-*5zjNw);X1tz&2Js;gI~MDa^GQ=h~ASzNIRb6A75)Hpbi<~2tuea)G%eem9~JocXfjW@@(_CA{9HMSdY zW*k8P)@_c~6Ls*rjveFqX9*kQ6ENQA$US4@%^z3%k?In#<}1@+w~NCZgV8` zwS@MO%TJSx&0S;f-NN{mobqNM-@MiwM-V{5f1W3GSv@*yl%uVOGva*uWyaQRPb$(i zw%)GzfExR`us;EteoFZ_kbt#B{POTs`dyRnROWO~GAJeVdZ?lKGq;vIY`$yje?|I? zv4+#y{R2K?k6!6Hx8~jO^|@qh!FD7jMf^ZU?{&QZbIg0G^|zl#kgm~st<$y3VQ-X$ z`4uGKJCHv5*Vy?czcp?8V6!K!25QXc61ISh$Tp9i&w2`w{|x-QhptsyfeWa6SR zKq)FPm)EHCdEYtWZ*F6sK^{^bpV6+tz1To4Jyw1{@>ru>qA$|RgEu3EWBnrbSe!j2& z2=tfNeERQM$NckV>n-CNsf+`@uE)3!!2Njwu0N5(pJDwzSRd;(M$QOa<7wqO1pXU% zKAY5S!1EM{&zH{m!|2@}9@h~{mj%1MIGcyDYg)i=MGkJix0v6vde-kjB;JpCP8a*y z|J-l(HlcIh+#`EoK5N+=uf#Er$Q(PLe?DQJd3{-jyb0_?>T@P0Y_vd6vw_rkH~IbFEe({nYp#fvmo_JpaOjg z2-q4w*Y4c^iq7+8p9*7RtWB==Nxd&IcAkn1*qmEu7}BwMl@ZzeQa>Z|+xOjt`S8ic zQFlUm&$z>8?UDHdKF{>O{d~tV#LppZt5!kmR$Y;Fk zE5O_ZxaO=xw*g}#bNP+6_JBO17jm?2U(pn4#*7V&tvoZ67Nf1!=LcUk@X`BGJxcKWem#)CtagFqKjA!lM z7bmy%U&orfMqKl5?fKjb_p=U};FDK=?{oISdCN&2^F28S_QyQFcX-d7_X_RucnunF zzg$B`Czt(jol1Efw;1FVoInEdJ2xhSd&lU!p1p5)ZTk%IIzNpXF@ zKYpB>vw)dH9`zAun^IouHQrt|i9Hnhv?5bq=Uh{cUvk<9c^c5mYn-_X(y{f~`;TA) z8C-$7q)+Vo;#@@L#_t@L@R`qfWnPBtXTQf(bT1$AZ|AY^aa)W1_-yOB8z(yT=Kee{ z{jA===8LiS(Cev!09^_Yp!!u`ppFvv- z(pYt6#5uQ4&qs}%&9$Eo>v4Urz$eCUkyoT+>-z?r8Gk@pi)-5<AruygKl{ec)^je$S1*#(xj~0Dc7K zx&g0&=R)oWVC|*!URt*~cJ#n^@Y@5v@c%8`&Lh0WJm~|6{rMSqCNj7i^Y_RrK5_?r z*WM3%lHgF6&{^vTV4leQh~65M1-yOiKj-=1Hv1#6=jNQ(uxr_`J*M@Wm<+$)e|WwN zcy*u`;A!}6_?Y7gJSW!w7w{{vXCHv|yBF5sdgaaoUMKec(}3IyuzuHB^t<%><$DKy z0H4sw^@i~+xYq~YGjh-T8JRhg-kY}7{`7tQj^6*f1GO|=KM(r(^Y2UWEjSTlzsxJ- zPe^lkeR%yY!2Pg>68jl(Ew00J)b<>kc6(!8awUA_e*#{IUTXn<`Q*{ZJkRalx$1TK zJDJwE0DE9P#<)(;jeVETp2+hN?0`Ar7`J_0yX}hZh8S~s7Vd%fxu1a7C2phu=S;B!=c!45(M7X)K* zV(fbDqiZg}nkKilSwqI(vAV#x#=(P3%;nQ(G3H;toR=4m{rO|>?4#1PyQVu3K?VU- z;tL3|^UEWru)Y&8{(H`m-+64{1Gob}0iU1t{~v&R=l)Nhc|YCYSb+IL>{4#eqGv~c z<1#w)9f7s_^Oa}Znp62wyL{#{b`Oen;5w|oGIrh8=fCGPpGk65ePZa(*R}7XdnwMu z?Z6(mAIARzJZ~SFiz{&MHDFF-tS`z7D%|I_-=n>rGl=L~Ui=bc{XI|nu}_}s7eKDJ zNV#3p3GDR+k+@KhYv-TuQ&;8-DAt23h`~i7HT3-Yi$hUygSVxKIxMt+u2F&YRcs{J<1Om9f^!tjvvVQ9*=-s@_7n4j{~5b}#s>Tx7sg8KZv5JS`lv5x zoXk@R&9-rVeAZgKy`N{^78{8IBNguvG0L=1$ZsE5BDQ~V(j_SE{_~-ACZn1um{H>y+1yI2r?)`CgMY+ z*JG~TeSZV&opJ6_)ejkv#yBrvhwcj8Q~PorFwS+>*nRGp3%Tu`+}E+z$(67NI3sYK z_Ec{3$ae(R=lr;iv%=c#zvs#Ok!MO?YuSSxnD;B^a>!Lc2KPXn@UyL+hkk7rWRCa( zV?F`r5m=||vz7{+&+j@;_%&T5KOCMhulaCb{SDO1++2&GUh7YAB(^A-61!22ZfJ$M+g(OAK}Mx>w1mo>!L^#vJ#b9c@K*A>P15o;1 zqkVk_?wvluyh^`YiOAMh_yS|=Q|EXAM{ouOWbo~y=zn(ejX%kx< z0r+Wrg~713Tq``Ff)YEg{@V7S;_rDmj| z3$Xqj^AgF6Jt&6 zlt`~r`!cWHGye9uAdN5jfiu2GV61DevD+dxb2Ps+)`LBvS7*)EFYgm@J@(4$;mTMa zYqORFGB8g-zq=qo0!thBYizF$yM6bp*D1&QTV^h|`4;T+XJm85wby%x&2{E+9c;{9 zSH{O&-Vf&e&E9){v_+(2^WFl#*Y@ugeFt2p>r>9Z3z~*bPU|^pi=C^M-eY9}8NF*e z0XeME{V|Wc#_BV#UG3BVf$ZyEU}WhFriEy?f%C+;hkFKjCW~EeAg5 zfr^yNXPmz;=e^5Vd16Grk{4VXde7{+>ya}tq!5-+-_F=_# Nz!&M6D Date: Fri, 15 Mar 2024 16:31:43 +0800 Subject: [PATCH 21/68] =?UTF-8?q?=F0=9F=93=9D=20updated=20example1=20of=20?= =?UTF-8?q?MNIST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{example1 => }/data/mnist_input_data7.npy | Bin examples/mnist/example1/README.md | 4 +- .../example1/examples-mnist-ex1-network.png | Bin 55501 -> 79037 bytes examples/mnist/example1/main.py | 63 +++++++++++++++--- examples/mnist/example1/model.py | 46 ------------- 5 files changed, 57 insertions(+), 56 deletions(-) rename examples/mnist/{example1 => }/data/mnist_input_data7.npy (100%) delete mode 100644 examples/mnist/example1/model.py diff --git a/examples/mnist/example1/data/mnist_input_data7.npy b/examples/mnist/data/mnist_input_data7.npy similarity index 100% rename from examples/mnist/example1/data/mnist_input_data7.npy rename to examples/mnist/data/mnist_input_data7.npy diff --git a/examples/mnist/example1/README.md b/examples/mnist/example1/README.md index 80ad3aac..2a19f296 100644 --- a/examples/mnist/example1/README.md +++ b/examples/mnist/example1/README.md @@ -1,7 +1,7 @@ -该示例构建了双输入节点的两层全连接网络,进行仿真测试与后端部署,可实现MNIST手写字体识别。 +该示例构建了双输入节点的两层全连接网络。 - 权重及阈值:`./weights` -- 测试数据:`./data`,数字7 +- 测试数据:`../data`,数字7 - 网络结构:

MNIST-ex1 网络结构 diff --git a/examples/mnist/example1/examples-mnist-ex1-network.png b/examples/mnist/example1/examples-mnist-ex1-network.png index 46009cb557d38d1c87015761747a99037d304765..4a164107c6e91bb8b48d6d8cae35a63c37bb05a5 100644 GIT binary patch literal 79037 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 z zUp8v7V9w7}1@C2WH@LIf(m7u)B7F6OqYhUr%d3nLiLO!1pbY+4fT6rvVDFU{B}X zI6!?~a64@9SFE*Iw@FaGi+~rId6OXd`|asULE?vbEtiu%9~|b*Usp#`@o%Vr{e^HG znWWDtPBOzy7?yZ|s&wwgLAYnmi&bl{eK3qS)%2oB=aHwcbUlfR6w=ug+xQ^$Eigs$hBKgTZqt1_rM& z!rU*s;KrNN)oKF9HLEYWdY58HQP)0!OBZ{@p63b#QeAp8l! z*SHTGFe_yZaO&DSl#|6C{Sp!;zWnuepZVTk(sg(O`CehA@4X_=O~XRx4RhQ3);RIb z3Pd)Qy8xFXgW*2C^QF@%)Y$H<*>*4Xvft(q-)4#f^bL#1EE@q#DNJEN90-HCA$zrGQnb4?N zQ4IPnf>8uB{xg=t2kRE#dLsyvyzbpIy~FUu44zO7m~hGklDU?VCoZS){?fgS&D_I*e8V_ zqx-)1xuu5EL|S-CbIHF+e+Aaj21{B*;sV)F1X$Jm{IbruGbBS8Np+F5IZAD75e9QH z4vsH=R`GgIp;2S}z0ivwz@!z}JiAkYzI-snG|DxgbTa02Dr6)`kSuh(X20SkH;|3t zao5#?rLsj}Q_ST59!_DcZ0F}v~ewF@p!K8p9MGdv!f$!winZjiU1Z9j57 zZL7tUE~ZJ3I4d1NaZtWst4HiyBM%* zIIb!Roj!Rtgv$;e_OVAnNW^q~eFg74XhxIQO|DbzcIcQ{>RDo6T+%|?+|9U8BZW?iJW;FW2P z`{-N@YkVF&Gy-1?X;*(V5t|o_^{T~(a^2y8*q)C%RW@8>dQpm*1<^Azg```os+_`* zeiXpo1oN@ZV`7h|lKqA@zlp&zo;e^l-`k^*(W71n+A&pIEn_(xd^u{mDymO;7Qo!p z+=&|-=pTGuNIdq%IJzo{g(lz2aZDfBaBtoGgKzPs?G}~tA`*P`C^E3e4>)-B9T?eZ z*-`DTIfTU;cX*9OaVY>g<&=no%=}($s-d)gta+qG&I}nA!#DjshH12{Lp4j`M@kCF zW*bf#YtXyZtK%S>f=~$M)LM8_O(jv|-JeS1KeE7XTD>>k-sHG}PQ2V1Etkr^(Hu9P zqaM1)&wCDhYle$Q4KV7JA-7bmuaW_CmQ{p$-e0p`(1L>Asq1*t=lpgPD zzgVvNsCnyth}()a%cN&HqG{*)te<9VzcctHy2U$?x*Xp!?7?;g9tQ8kvN!I3@C)T; z?q)Dv{jFy`*%QwbN}u-)$G5qPQ*T4TZ_*-tlAka*31Tkvw)a!qCN^}OxHFB^<2WIy z2Be$|rP+PnWqQIqG<;e;EFO~`-^$vu!;3v^|B0)on0b^~h+(8TX($MTSD9^0L5Yg{ z@(4pZ{6?D&Z#-t}sr9L2;0__OUiVyC&dJweR&Naz&=>k7gMxnz=QJnM>EW02_4R*N z7K(Rv_ngP@vATmIZ}FVp5&htO2a_+Kj9v816It?eO@C_~yg@;R$H@59>=YP(*`7Uyq-@Bi|vuD;?GaG0>#frALfx47}!g&}`ZFd*n?wYga z;r#Jfv)(WFF_#0Vmf^KIM8E^dKGZ_1yHa1!&iQW>nMr;@zP};{ys!pBT?yIpGg?BIURSxfO5QyiTgb03`0nq??Cby3=)o$xtD4W7^;06yUp%@}ICjAqX2#~?N zRAP7cZbv9&B<_{<5z2&MS&|AQ4QJy8!j~@Hfn!yqF^V+Bt+U$IJ7HBAu3tUiM&_Kg z(=MDF-+jHFU1$x)1o0J(JW-;4tSfp!HmIkUn)S8+@q&I8(#V8y1atI!e^%fbj6Far zP2NivqZO3Z;=ByHA3v~1<^PM>HKA#?RGh2$kkOl|r4m0K9SQZ}lMmVKaxMu9i9mm1 ziwv{ti|SAu>|QFTKQUr17r5!yWaM%pQQ?8}(anWx1CVO8O~39D-Abn#oTYLbpi^NK z1|*t;59ebvmAX76(*)UxgT0F*84v-lzU0FsAeLsLj3LTurEx9FxxLYzN>KAf@dK29?IY(kP}N& zDNTV=i7?T4l&r7>>sRcLTGbhF1DC&FmY_HBs~~ZJp8rX2_KN$-S$Ii~7d(cUM1geU zSK$V0;PvB6^Wyb(jE?23f|9-(!OK=11i*|f{Fe3zEY*mVvwR@RIoA%WqP&IUOZB8w zTxPpg+8I~vyZ^s~E-*4}a&E9IAOqg|6h^UuEZ#`1Dzz;q~qwr$T7 zXu$u^7eZeUSl+z+hOQ6C%2)@Y{oe@%BMdZNBL#oIAj2J5+ivYX&V&{L|0bz;LP!uK zs~8;>e32e-`Cr<+%U1u!e$-c^l=|u1<_MgtOu0y+ey4adV}L4yl|&$#_r46m%%L84;EA08PplMw`PG<10{rbmXxG*Ly0-FhyjDai zVF}%?L7&G)#~1XW`!qp+w@3x#!4*zhPH2qk7bmQ=eq2jFM97SvU~g{lksn~}w{EyV zMQjlG<%^erq=YM;G}vww>fFogzczc( z1>`R!Gb%untPz2f6mb5r?Tdd~=ni!xC>rwW^`n}2_^MhRaGo}C!9Bu+3Q8q#DdO}& zjwC9D+}ZDF#)N0H#~{_Q$8fFd_@8I49cVLK()L+?RN+&uuN>MRPWn7J;7brsW&(|) z0aRpw#6H)OsSy|Wff2{~UvRjyB|1a>@h$%E#77#8!2o(vpmYA|V08wEjKZiLUR;r0 z%3!c;)PqUmCEA9cH{OtOpWK5j+QO>7Ey}#6b2dP9gTOXEee-zlgCVMy!U!B2xfXrX zF=he8ts7*{=3{;F3SpY6(Qke}iiN$vjrZgg`VDFOkrB}J$S@gbIqi@YIHgv|VC4)) z&i%L#5lb!V>{vG0JULfmh)~DS+&a>O1 zSTpd%6D@t~rMWa5*<&;h(!cx@nO?WpCOmCZWswfc;`G=GS;OFY%4@x*h$oc^_74%P z1;b-GB&Qzyfh1r8O!?ErO=O0LC=w*8NxkViSo|~E)vlu?TS7p52WEo8cU&|Hv6BRU zX;MTqE3tI6MiaEE*>1T2uPZ`~-iG$t-tcVAigc%sHAY zbX#1Y=A=7~GbICip=zB*;Ykb|Y*=I=@iH)L7#_`wIoQPakPH= zX3fFo4u#h;fl&o3qHWd+Ecg}CxGsIeoQNFgUtOQF3>USHjulz)&Zr8c{L4OOZrOs}1YYKJqiPQWV2zVH>)i>L_Z$)QXcR-izbgOW!hv z6nF|u7)PT6&ge7D8v$5gU@~UekN-WyYT4SlngZH4l7q+_7h(sx;F|iinkV?d^V^FOnZw zoPX@XVI;l;F}uI4L;aJDz?1>Hfu)POEFO{YC=dPB#H!5|ZL$hU>5$Vvk7u@Tp(GQCq5~U=zK{?4Cf(Mv?D%EhyYNWeV`wP}whKb3%S*k{*0hNIn z*_}Fx8Z5!*w)14tZe_XD?D0)%-2*SIC8C^0B!R!e3e|*zmPFccO?ra5{Dk|mU8*b@ z3r!OI&1>c>*kU<&0;$I zeN*s{VD`~O$m3$qTp^(e|AYb)K;42i8l&De((px{scog9fYc=7?5UB~;&P&b>wcpv zz0rd|hgNCX_II^9-^-+U<%%0+XD0DAP)SQC>`IY_e+-eW7!O4yt&+Gc_^Io6vcWN1 zea@#FLVPyHHIq+REW6}z?ZH$nqaFrj>7>o;CDaHB+z?+xT=z;x0588CDqZ8Kbb=ZN z%k@ui@Y04_EwL2j3t`d&B=n!ZHd$_;8xZQ$J{`EN7&w+2P8vO@JzI)>Evd6TFMAmA ziUx+bh-K!<1M@zbO2*ND!!;C6u0pyudgwqbK{b#zSRG6A>3tJXTl~God!Og&+melD z?|HS`)6RY!&mFZLU>Rlh9{$$xWHS8p$D_EppK|Uax`r6DMk}j}!eK%{dWm@zY^RW7 z-_cMK5~U@IL_&o+<9=y0X(?B)^_QSH^N%KeNk(7zlk$w5_%zGMeoqxV7*~)hh4UkV zsjYJ0*y3p~C^~#{y*0;jy+pSm!CD*G-q+>CKbjL3A0~sPK6g40;m9^>hz$z5d^C#N z-D;7!5J&NhhEUm4=!vSacvk9J;EfN8u7M+1L0JS|%vQdig*b(?O)o{b6&vdBuH)IW z9VuYQy?%XHjA|;_Z=^cs^|BB=lGLPzn9VdDW~|r|`OImrI|y?=|0f~%`Qj8sMyb(@ ze!Ouq`xPhVNWG_g{Fx8_<$3h3fXhQ9%S~;@y_mGX6HhZ9G6)z_!Rx9V-j^m-)Zj_t zLc9{W{z02kISQRbdM(_?@n90gMbN+W7fD9(S2`E$V$&Xyw&8v38a7l8qZ*+}CjkVS zd8J&<0zrqB1QKBcj(&AC%{6t~cl*J4B8yjfpGwsTEs8qppt%#mQmArWhP^qeNo;xf zoU1jgB!9m{E$4ydAIblIS*cWe4TYD^i$us2xN_}X>do@hbDqHAp_}iy7UlKFCt$p6 z;PbSZrV2}Uim|fII+ldDz{EjnA+Ix*@-|%C5uw$J5gr?cw^3-M=++yegG4qTjS#B) zg}p1DR=8ZZFptoR~T8n=}xV;_oS&7o?p?Byyb81gi9!?ehF8Z7ljDtIds1kBa6&Uv?8#6dg@twv9x-41)R#-@ zrPOt=dpT9_MtrB8iBH4jb@mGaz|x#q&!e)ET1vAzQ4~U*SGyNe44%dV&{L_`=~4Pc z@GTN)?m6B#5f_)DOf;;%-d|Z3;U1gF&!L&~6}4CAXHB>oDxDjS?l`~*FCbRWwb2uH z;-J0^WqzlT8{tIXJylfR045K?FUh@ zBF%E+j>d%J%;}IBh8})=p6F3D+r|Gb0{aF?F!0&!!wFkXO15HRV#GJ>eCupBoiM*@ z!#b-^2)EvJj$uh>T3K7*&t;wZLf(9~p*W z<2?-JZ2PIPg3HCQsV--+A1_L&p?a0rb)!?O7AuZekeNMCCe#N&vG#{EDN!AOuH2^+ z68%_sl<_!`O8gAZTRdFJ9E&b!Y&RfmosPP6xYut%2# zGh8N@&z3_PD~r^MbMbgjnr|nI>-8A0B!|*0BGnE;d0;p&QzLEF0P7L24d^TN->6cf zIc3N73xDrMl5R`AlP8wq%xCLqDR0lF-+vaF@_>OzFY`G2h8A)Cliwl9yCKn9#%kc} zWAB}7O-5}c)w*20KNVP`BsN`;Y%QPQ-gtUkPws%h(zgV1DEuay=-&yp2^49&^xXh? zWN3IUy!db}k<4I#k6|t+G$5NZaC2|LjiCzUg#bQ}IkMH-<{ z;aN~I7=5gmkon_y^?i4IA8rOaU%jI#D~Ryb)Ib)W(eYrrYSX)l`NsM*0zb=bs}FpC zGF$j~z}0cfz9r+h({wBuO6NXjdPfaZlV;s?X1wQgoxISDxyDKk&>i;+A?#A5|2|M< z7r32;grrcM3_4`5gbJxG7Km_RS%9KO3BO$!L}7XM{H%x9%F{Hz6G%|guAtM_ca{{mqrRh!po~(H>a_Cwm&G; zj#L>M594~}k!Ramch<>=*yC73=_HPV;-IG1I*QR};lGno7%-rLW@%|FkOCu*xzpnw za%DJ5!OcvKQw&=Z8;6%E4{AK7!-uoQq#C?7WZF;IS&;&~nlA9cB2+zB27RHuukHQ- z_kNinDk$ji5m2<3o+MJsMAl=Cko))e#G6k=biMb)((~rc98Oi*vbr1+MI@KaLV|i4 zVm=H(5AOGtv?LxIp#{M&3q$VCLazt3&IO@2J#p9Q>muV>R0N$5SJtzhuS7q{`wApc zE@yr&;a+roW8t5 z3YYe_5L1mUIEq~FIf?-q0xAX=cir=&nYhl zScJu!LcrS9YQ{N9u(4nib||LFTQi$^Sim`NJNsi26z*4@jw~cnF&!fkcv2`7w{s7y ziQ2|hiR(Jb%A+A(Xy1Hnt-d-nk{AqBUf$~R*&0Ch3x~4Xoy%w8N@l|-$HNWmhw-_0 zp0Gk9m@@?FO4gb~!Q%)uY8XgV+*QTX#`XV0mY=zJft?5p8z==Ff+3q-U<`e{Ws;u{ zn$`_3)$FI~@xDL%DJ%vI>p765F=RP6Zxq#G(&UKTWwlH^7W{eBjW{PKV%U z8)=0ecXAWY-*1-t;Q#P3Sn#3!b@>R8@P!Br){Ku>P z5&V1G9S_G((FwF*0xI*rME}h$HKeyYpcON|!U5zrQwmTez0w1?-m`29_U^)M16ZYR zGF7R$KNx|}e1ldW;2|IOu8Oppt$TX^e?dwIpPY8bLvYH~MT*B_Ss5R#_yTP-wgaC& z_ZPLP#b~7wGG@AFrPYJcQOLA)9R#9+#{Q)6hKezwBk{iVSot&Dt)7aD`N4EMhtf%o zekx4cVV3qGFz_BIEx;lh(jCKADy@DVMNee!K>s{2Jq7{v6;|TymoM>b^WJV}JplUe zcz?!EsgSUm{>bQ@I`&1mGJHJXx-&EG^Qo1OpT}Qu<0=BoeZfEyZnHk?sbje6hE92v@yc68Y^JW&@L3`>rKb6CkSn zvw@B`x9soMI~XFXwO%!41S&w0s{RfD!vR1Y!@~z#6gyz zD?$TL-kb1OZl@AI50sq7@g~;d4u`CS_ps*LqUkwY~}L4B>#Q36l@HIV7%4- znErc_C7l`=P&$W%XIVEd@6U-JE!Ijga1;+uFj#xF13=~2)f%E!hrHjeu4Pd%va)_e z{-|zdL{sXi{;?g zX!qyjkrtrkAp8MX955MgCC|VF99}eIN(=k$=pfndspldSV0{R1Yx}5CVh`e&{Sgx? z+ilO>Lw~~tQI?eAGZrHS9A-*dY1J}=4HtsWOsS|S<3l~BG>a!48ZdAwr0!^?0;@7T zu6@po3G$V7+Y>ha_n2(^f>cJ3h&Ym zSkn67K@N}^^uv&!%ZkJUq^ux93pXsxzg!RL+8)QQ!Gb9R*|h5n%_!Otfb_j9mlCl8jgi*7hu?R2{IdWg`3)N&+I@?A zX33#447HKYqDb4QEFCMZ7GrGP_480!?TEDqKRKMr+so*kC7^bs6}Ne zmcv4!eZ0Mw?0c79wveetkb<}Ihq}u~_{ZPi7(OUV!@2k1e>VZCU;OmCo$E{=Ap&cg zoJa>ZovU8()8NbLbFs2xLWQq_x1(*^oj5Q(uUsvyL`h~URg_2tuT{T#ojbY8>OPUh z`<%TywmNz3b(@%Re&w^V!8&ozx39Ij95N8U38vX8fWhLm$omSt@;OzQ8e=8Cl^dhk zzfc}8CQBLoF+`;kW{jmNy?~cf^4JVsN1En_66NhRarXh#%C9?|ZlF5V*yxXs2TDZL z?Sq2-#s7#&_p-ZCj{=I9dYdB)mc7bowG_hxt^OhyUZ4Wz@I}&k?V+rtmKDlOs*Z$* zm)l+NKpx);(ul7+T@;uyxSS1@3_`19>!r0DL>A=L*KtElc|sb6y}kQ7e}6{M{O+h3 zv|>m1jL=`0AUSn+QY}n(qcnVRr5# zmJgTvxuZ-&q)9KdBG4H-tgbWREJMwfoQX9uK`>S!3?UovUha$6h5b4yUzVv$`bhf} zi)!c@u?1!Y3FXRAHyY|OY5S&M{fNLiwO;$?V~u3l)I&xrufPJV(bHMi8BD;Km>Uh6 z-DOKNqLc?&#+Z6;*41${U*AuMW0D%*lG?dJL0@TyM5(nb)|lmk?or&oFw$?4Gg-Je z{zyE{!!gRpTB(3In%fh+MQ_5;Y_NmK@pV^^&-7Gt_?O;8tUmQ|8oP}@bn`WRmL(={ z3Nj<=q<}{_RsO%zKq1e<>wWDaLM(OP6B?EXM!as({Z}RA($%EPIa)(aF8j~BdhYTo z7)38)Z~tx+cV~gStRz@x^Bv|$2id%&D4I-)28X2u>+F=FXbt|`up+x(TKF%LJNd3W zT?>{W%An~E->(ku4(C4$wKTyetYnC^&m#I(J~xCLdTqX6&4}DwU1SR%6?#u5DLxn$ z!^1((v)``dG9QO+oFdRyRPVeiLTAkC!KCU(y? z;QSncGNq2M4BRcOS68@8nZEU)%Q&tx2k-ojotk{fnk44Cy0j!g2SSR}AX1VJvN6|KZBNFO;L>8SU+rD2*8%D$DT_(Gk?2B~+$cqI=6om7gMEG;&yFH7A zg@uq9T@UCRW4Unz3>}c3o<5Ek@fjPFgBs3zhNJUUi{5ek4mN2Mi)Cg%@8t{+bN#m} zTAb@%YPxDIul+3)n;4LJ@}QQ99Kg!C_F`?Jx?7P5e0W&mM!N_&fA%oA)~!^x{TvmB zF-lu*G$Eq4X_I?@4P!L(joL_M94zm8msrboALyyl$rl|>WkQ6&Vjp_xG=?gk%n07C zcT&du}7Dx_RZ1S=A@>g%nK@87}Bj@E-h0n?bF{nOz;0&Bd#?P4g9 z&lHWQ4~d1KGh8qf&V5*h3e((M%`5%|YUj)Ba@x>?J&&I%Rd^}Z|K>=6##6^N2G`$n z+>H6^;1#9d9a{Ea?~>~k!>VU;@WvK!ZF6ctSeCX`E>l4C==<>GP=o7IMJIRie8AhM z!yRI*)y4QCb04RvXlnlheqFHMeKSS#`lWX(el6V#IRQp6JBBI&ohVfKp{!j%as32>`!Pf$1t!*KemZO$7V)2S4LrX;!cBXD;c-=@IBg& z20xBd;1Tzj)8@vtc-q1nj@Ami>V5^Mpr#Ew_^gF9KfcJl*e9|Lc;Jn<2a|7qj^@gO zKbdDokE{rWZDCVdQ;f z%UxfhWEld@*!`)gK%Tv19W5r7;sUaY#-q>EWPRgq+WGfXHb@u0jAdx5B1A2=^OHj* zXR^6U(8$#|T8y&ns!T-V2&QvWtq4{hpdPYP%eU`^`?`NH&=icbBrw|eRR$}+e^nTT z2fBHExTO5f=-2lk$!Q9Soyl3 z-aeHuwov0Gj|*CVe{t6J=#wkeZiIiz_WIiTy78WGcbe@>m1gjCA=>_S`Pl{KswyKi zm=^}AKmHnmG5A66a)I^YprR&dAi#Dd7y=j}xXJ)c+0Lj1>@nY@Gu2@*evN$f7MQ8H1+90a>&xO3k- zJm92cT<_m8e4K4o+u>$9e*}l=d$o*x8(==9zg{jCVOz3os^@-NsfV)nVZn>*e8TU1 z;P~RxI!1ckv#;W)-81F%JSJ?`amAv2I(l(>f{F*KH_1hxQ-(Pb8#a~fq9iKCYQqGaC_UmPR?5`j5l2R@?tmCSSu=R>>vmt5I* z_{sL`SjOc2$9utxkjF*S*M%9!-H!Y1%LRjb{>uBaWh`$FtM7suRlOMY?S}}gR|xwC z59@7c{G7NsE~};*5546)jg~R)16Yr}z9%BV(rFEoe^$i~%p2Bg-^-l7cSpVg2krnkKxB1iI*CC=3?$VtU=v;@t6*1fkI zN^(WAv!xAS*9G2$Wph~<%uzF6<}^(8d0%3QSrj&$age#M>DcW5Vp`8v-Qt~|Ts_WU z_;@GMlLyQGK2bIHIugppZL&E`DTHt>ohov#y85#eM6TskFM^u1_+(1`eyA}2PaW^D zz;>Jc-s>M-b@zj9RpLC`Pr^@z;)Eu3?M>H;gPgSQOCmZR6rC&s%v!`NIYVjdEL{Wt z`5(5vtHao7a|HwM%it52Xv#xqd>DF{4hKre%eW-^M(6Qcf{8|`3Aa?M{|-kXq3&7$ z+8+ueii`8733ls$y%@Y?1Ei6Jlv@*2(h&rbw`0@a?|$rFjl8@Jl2|M~_vftvn>FzSCgbZk zb>r6c4z&nT@lB8gUdQsL(#+2~2ufVfiLc=E2gCP&c6Opt9r0i9;`k0^D~i~s@e;#V zdpw?2Fcz3LToDtxQG8?TLqhth$9NKjcRf;W`-n)&w)H&;@4EB;P%e0aI0oN%Pli!q zXU%-d(1+_bDsPh<4>Zwn=at(NW@Lak4gdLO%yMQQ^m6M?s$Rt) z%bCbcBohLifzPXY@3}Tb8xGz2oa=#(!uN=Ipym8?Fc>FngT&b)o!|{FUU&ehEuM$S zB~PA6hch~>|D&Lm=1gu;IVtk)T0NV8l7Gum9q01LZf%dL3 zYSTazT^5K0kAQho0=#xtl-&O;X%{?FH-|*1(q7U+By+0IqPNT;Ucw>gWK*=%k%f1i zYP?Li!ixk^tOO-DfP4j+qH|TlQh~&BLu!`Jxwt*Rj0anHLZ4m<`$Ws z@5^lQ>D%-6nxi;bhyCLHUY&Snm{%85*Kl~nDdQnHt1jzLRFfR<_4^*VwA<@f9QpM9 zo&Lb!piaNDPH*4*Z*H5*%ehY4TAjG~B^EB>l+M@;{rUS@IGDe$pAHhZ-e7YOfxkkab?)MOC+7E% z3DM^nEtX|iuwUQ$bKvomVR~AwRuy?iuVZ|L5eG#DrT={1rFpqv#b&=uSrN^wn~unW z#&})%4qUF48@=HO->yCi9Wd7WhM)2LoA}Ols+5RBone*wAb(otfN^KCoT!xH6^qr) z3f5TjYs*+J#DzZ|$6=L;(`rzznocCBVPH&V=P~`*=Gu zff3V%dGqUm+f^{jLIwi)8iu8kdGdqJxCJ3HtT}SmF z%?j(jNc}u1>tjWY;|tcS@A2cm{L8#)%0#AfnVGXes9k>}5gman-lD@g{#s*KQbcd9 zOVDfuM(5Xr83(7J5?X!Y9#-kvsULd*`k#~WfG?We%PYQNn0tHK5_q9|! zi_K!1J{J-)Z*N-L0OKYiOHL=w^H&q1nIcx|)j!hd2}KUQGq}uw;$+~x)&bleC%uJZ z<8k{@{mMpfc{K(;J!vjn8e-5Hx=X`T$5VkkP4}F`Fjk?=Q@p)?{Lg{LF= zn}+I%)s%axMiV$)lq?(_+PKv!INedqxF#0A4B;f#A5a>;qS-bg(g%7R2FN#7LUz0@ z6mfoPrVYJqAjjHJtg1t&SaF_yXk+C_Hi2Q0oyLhi??bkl+`n7jny}B4HDKu08Y=uP z>F?~qiNk&m)uov2J5l?!fFbz7d3Dv6gXlA3y4L#AD9Na-kyz5!-FS06+l<$%5V5ph zgh|sPbEn?;y`M)Epa1Ba(qM|KUt9WrounxG+ou-XkE|JF=196@nbvXm+sp-BoB5RZTjD^XU=lPMFS?D?mwJ$ZA)l^$PgOB{ z=cum#L(z6AcGx3Z1f{T8zb4a_nIU9Vzp=5Xf(c0^Ma>va&8gmqkE1U`1g`JqZT=&H zN-$S}1kwZ&sLR30zZsVyZpj6+6WwY9eTtPP-MJMAjAi3mXh3mP-YY1L+9@z2g**Il(rvb{->=(^$Tw(MrK7M3*YhC+ z{7w>@UESOaJd5xh+%*wXgEv*)R@aX6_$8Q|oh}4yAU~d&#oSC;OarF^;i-Dm`(# z_uVMtQgg@7{>rH9n4|A^dQE5!cZyOvX8gJc9se1Qzgp2ZK8snCAaHX$o2#3cBH&G9 zAW!pAEc_zW(euk^9LuMRPKEQEJY4!TR}tczGs+CcpfizLwQ{-Y-4KNS2>$S^w7V~j z8I6h-iKX)Q%tg2m?tOHc{zZzAeCjWr{J0wMM1!ArpacH;^LKF1>jNVZ1&k6!^CNj9 z(*jNj7<51j^DNG}7KF2ovZIpE93qjT8N#ieZvBT*z9StoZP@YEX)+}^SUOZP^D}mE zG=B+l(orW{kT^>sMaBCgjJFHI&PswXHMcIi^b_oXP`1E)Mmml72U_&0l4aYUwFkZT zjk4*|jy0)-MQr$S>X_7xZ>9RB{^&j%Y)$%8g3 z#63lwqe!*yB*d9m`$@@dX0l6minXR`@kG$k^ zh#vk-YrN1B;i7M{r%9<#EUiwIzkOdkxsq*&3*$!K=YfoHZkTwU$aX2AOCdx!^wIFh zMc|i>vDco@%{K?ABC%frC#%<&<362*v?}|0#&VIay090xR{!>3s?uk!Sy;kR};Ecx+_OdTM7;=mAmGX0H=X;lT4?w^87d z^5ur0qd_KwsuiK8g~tpV;;fw@u8VjoI7i}!>q@3gMV_{zx_|PF7B{G8KL=yxp$-?M zsTZM^`bAc|{bgpWP>lz$S+T7$1l)PJ>M<;{(hTPfYhEjd z7CY7SJn(Zb&$!iQj)UvaJqWoLL#(JHKsdiv&unD~6=+$coK~dzj6hv$MsD3zEB2eQ zDf*10#bXI_K6wjB!oJbExm_G=g~r6rw5pS6;?t>8Kk=EAUEESkdh&eyj<3NNA}flp@(ET=Uz&jnaQm-kB_!-H{Ba{o!dDqI|*skp5>X(i_^ z1SR*d9^=3#THO(_pGH)kixvWWeE8^5Gv{q4`I+BOIZg0J76vjX%(}JAjr->`Qf}S% z(&04z^3Hw-0t0&87lqiDU9s4E=y5;L(`pNi#d0-I0DrEAWL6G3!{bYN6#)R7ZV%{5 z<;YudDQetJQJI~-|0R%DSsPJNzU;RhUU}2SjUd)w^Qse3rn9&5@^ZeaZKsssVcnP* z(+!C4#!o~m2j!*?XoNDTT~ntEEExpp-p4_@QAf2FZoVamvlSi%^bWrw;tIDgWu9u0 zbi~?FanVp-oS9$O?cC1^v!*jK5@<}(SC8;(S;o`Q(Dc~pwHcju1v;&B>dm9PFseUV zO%O8an(vJ=rXDAL4Myy%#I@VSnIsGQ7aT^e-UI(PCSS^2&zHW_@A9#JGK~KL6##MS z>bqvH_exOKX*Gy>32{n{vl<3TH1H8 z%|ADdRRFNE7*G37v^2*Ui82{^Zam*sR|M^1M3%ZY{Mm^rNS8df;|WU18aAR*(GUja z;nUuo%HG?82NJ&w#HK2>z~NhI^S~h4i%|~~^%!Hn%v&Xw;yOcy0Bo@8Px0-DZ(3lZ z#m1$IMfWzu3=!3#o<8Y>Ex}EcPfdhk_ih8XG0s5gJ3u| zkaFaIElDTJ<#`MkkLjsSF#jqo?Z(jIHvS<3*e(CmsHchB(>0ZB$u zZ;`X~`rx?wk7)_(zP<~$<3G|VmGE9d20{NDGOPJgrTNR8LE^#FZ4yNM1E4G8Dwf$V z4dz8+@t1o=O;sVUDkxXg}5*K=p(O<46O;aSr zA|U^nzJmKt&H;lX(b_U=M!xhN`_ri5`SCc}HD}k_6u z7c#_~T7|_rR}4Zg)B)3F;@GoU>SRpiOztN z=~6Ok{-^Ls3p6QW&s{ad)_r|h?>DVqf&t<%K~!qc8Hxi*#ck)Yj5!$r&~0MX<&QfA z_FIv>XE#)>;(;Yy+*|_ykl{xj~fZR7t=O{-rdz5dO_-iHdZj0SvFer1u^M=YRF=h0L z)r~x$!PX8B>r}NujP&H2ECwuC(d=2i+wYH{(*ghtV&NGPMa(_Qy-2*-F6<3~{%T&U zkso{?FmHPF_csksqtPc3DRH>5^d`)iJtp>Sy|G6f>F5IDmXSXAP)C`0e9&*Zl~4$; z+yFs!$c1M+%`W0dyXH@0fTnPUjrlAD$Cr#n()}#DDN1AmLg$9(Qx^jQF6_KNKha#d zIub$}&&hMw_Gst1kD`vOCyPs6U^UL|pPTda83s}!?cefdjs&@A$5*yJ$4B<_>M?x>{i`(bU`GK%`8= z_KB#}`FZ5qbzp0K7{jG;>+Fx?c!o8$k*RM~3KtfNNr^ zk9B>(9L#@>o=pby61o>{0CE_5IW@I~>D76|-AZt`i}&C-Jn3;;Ak~RJfjGPw z%q^o3mbk{hR%>?JT3;g)vb^{_-4;AR*2QEqlC%G3-EE>bgzM@Bp>8R4HEm;t)M*{A z!*O*p%qT82rn_90wd$=J%|1&zpQc;28isGI0Dkrg@qy0h5ugJz-|z|M6v#M{iT9b7^1b*so)3H=_WhXg zWR3qH_rMD$qU{J3P-v&QHN0rM8`+^xEC zPUTz|$Zyh0fzu4KHuYgKiSy+888(Ik7n?-8ri@jNnHmRFJ5Gn+^(8aUNm`ItVKg{v zFSaRURWo1%uwX{THf190-G-Q`43h#+`3JBGF@_2fyD_zF<2SvsrEE=%P|I2quE}z? z$Gvt?a$mRw+|~!T=(HAZ^W0@|&n~DYB=hr^it> zFNHejXJE->cTYNtiNq4>DY8Xi(?BeZhHp3${Aa!x0*N#)*bsO%rz7UgswT(B+AAKC z`_zzMp0VAj9@9wB@A7Y2&^<=-Ho#Ey2bN4NgS(RuG6(Hv!e)>Er&CA^C2o_9Tl8wF#ji1 zHC6Oiu4t5YP1=$U7F4Hj8>&%#8C4Z2?Q;BD<3!)FYNbL{j)`6an9q04^xs>;ijcE| zD@3TjPi2Beh*V#OYKLs{0q5H|a#FW@g;ssXese`^2IhNs4}bkSY2R+G@+4A$gh|^1 zD@CH}@MvFf)9UwY1z3-U#TEOfbHUo9sfP25A|s*L)4zYxK&evp*&-0VA9&E%xt1dn zDHKjtoK8(MA+M@=xTDK)j*um92Zx#3)hR?efnqV$Q0O(+w@}XT)LlTeg9x@3*%VL_0VZkkdbi< zDa+7KFGHw7F!F3mM;9K?2bt!eHFrgwz81ftp*5iX`FGL53~~9+hU1zbi`!|5=@#Oe z;NN-CD1p=K1sWGN;SUPd^cSpEJe*K7-?2-(}4;FLvU~4;|3K zN2JQ!M*HSnnuy4<G`5W-g!cY z#06KW*+ERAU=TiM&B@crqK?1lW+mR2d=k7D4a|m?cD=n3Up^Ty|#h`v0duWMDi4#`D)|@*)$UpIkSu7fg|I~9^ z$AdX(K;a=;q%I8nl5<%`>)b-8(+JP2S`q4dw+aEUjBxmA!(P@WwGi^BewC9#7;*6s z&OmhiIy&!C@-ER{w6CG&s^9Jrm?V~>`Gf~@dX|nCrUr1zV3;Gte3{_N?@cX8dKDOf zH)ya}<;lM{GR{a2R}N&yl6#&dsZ%SsX}{&MP1ii4qw4xb^ttgCaxb4OGzYQnsNerj zQ=3oDG4|-G7R~xKv6~aKEQ@t$XMQ(tE?fu%4#e>t)=vmmyL0UuC4xsrn#&fQlaN0csfngzz%*(DH}cn2>z^sU#lf#u}94YMJPtb z;eSh5{d-XaYrdc?_xICNPssiH4iC-7q10QLKD+!dpOu}x9YfK?hN+(nd}Z8VI9-$!`0la zS3{%Pzg)12C!U~=Iqi_=YJoDEl}XJI5GG-1#n6IVk^)*7d{T1<~h5hK)`>#XGPp5OJ~ZGv8i(xb>S|hv05lkD5yZu zpZ`G1PumRY{-vR&_#v5LFf)>9Or0jE_5yB7abf=rTbLdID2t%8HaAVVw5rxM(Np zAFE)-ncZXOX~s|OMNBedicwEL`-R>f$a0p-^y?5=%BU+Z#YsZ|J4%u1;vmuoMprmDmc8=_8`Ezmed9(DRE0$p4`Ok31jZXlzS;y(RKQC@BJ&kr0z64RXn zX%*pLsp9hdQW-R|RUPj##|Dw7=h7jIin6is#81-)yB?OxS58n4F2k!zU%#tKm{)O$ z^8#?b&$i=K;8N6p9}@#xZMcX^^?s8{sSlcdz-?5X{_MI%IksrO(v(tn-UqhWVg0u}S6 zt5~%p^nvQGa}_LY4FHVYsC4w+b)wuyDuM}&{w*s?`M+{yvQ}7TjCgw!eS*Q6>(EpJ=W!H6Rm(067h%kjo`1 zk7ApLb7Ed50tz7GlJ4!a3YV7bR?C<6d-@>kTn~By1P7w#dj$bkvBzZMDXtY*Fa@4j zc=1xsZQcFZ?G&h~6|Z8V-%3j6{RO6rPA8bEt0uPHq%ub}Xxej*$@qkH@;qZ*A4ro9 zh)e^>uOB}Q|NqGP3ZN?AVDCeSGzcOkE!{^DkQ9*a?k;KR4v|jj?(UKrMZ})BM|56IT?0!%v@_mdW1Xx+Yn_F=j*w{Wp4E$3e%xom* zJiHI8HL|4g{ZF9;qW1Zg<~t`Zz> zoVj4og!MM0$VNS9g#u*4G|bv2ZpkRiYAZk1p8#IT$oKciIL!5Bx;UHu;&=Fnvfm38 zWm+vICyF#NB~Y7eOYri)>)THh%Mt+Dnh)Pi6=lAQ#Z(^6(Fr$OTmvxv27;to^PcWO z^>W?uSH-`|XX8Q~2OC0FZij`;7i+jv2`(pC2J?DQ_Ra*kQpP50l0Q&rVww@YK=)q5 zWdA$KR`vGO`ABxHGhhhDUW!gE7+!oib}77W{d~6%Jw&6$&aHGsJtS60OVl>Ln+J6E z#k!6R9*8r64+Tf)Q+mrM%eP*G6bFntHz@H%I$x}=J$!WU%wyN+`-&{@mf@8D zyk25czLx@WyDPMeonsny_xT#jwA-f7Lm4aqK}tr&%mW$GI_fcPbhMc{J8$&s!?|wM z%lF8Y7({$N{wu=vA|MJ-QJ`Am%#>uDE3c!Q4tLZnDVCPZnpXG+MdM@%>rq^SjvjGF7d7Mi3( zQSjV%Qw6)7uUBRB<+%S_WV!sHl!ypYl%J6)l=S+GwfcN5eQ%HPuyBAN9~`bG5oFJY zpHX;rZkQS<6BHfY>ahBY=9szzxV9#SNRnL9-wh@$=cOhs$}kFLN@^8z^p%FY@93`f z80oGIv0@qSDVx;bB;xDJ4CwzBc48&+`_eQcrPr#T$-4x2!2>@AlmO8yZGUBdhx`Wo zWJmG61s%U^fL+6u`iq8B>r1PsndPEbJy*-Dwl|M{YC0{pNl4B$ooJ8Gbe|U!e?M+x zzQnJtE~5G@z7`A14nOTcaCTlcLu|IuaZCeNkPlWv61a`BRd_V!Bh{d$(eo!r@c)-F z;gmDZ!(-3y`fD=p=D4gdT)6QjjtMu;>0)mm^*!&J z>-|&)`!Vn^09ZO0yx}eP(8uWj;1>@@@{=ruZ4{LPwe26)DhN3g5CKD}5M(h$3Sk_` zX4J4Wid%B`-@5jWBwj+X4~P!`d^=Jvi3wCiVTHm{bK@r+fg~2Q`;uHdxlumSRxk$4 zy7(i*T4XNEHos}Np`1~sx zop;r5O90~*w%x>wH~DM!S^i>Pd(hxVYcSV7Ht6Wi`Chqkl5TeCt#}9+Km)~R z#~TilKdF!ih`W~!TLkw{afy9gMgh5-(=NdQvB5LT92vY^1;fnox4HVe6znJIxt5Ca zlP>O;i)Hbbw{m%5Nyz5=D@O<9zBKh$eUUbhRU;Z9zW-wLWZx6>5^KU%(%2QSc$Xap zzBQJJtC1Wnkc_fp?(cOr8c+}VWb0vHJgAbO+{HE573cxPJyFV8ugD=9?k^>=PssJT z2wum7#wLeiGWprWm!7mG$=TwOND+c?W~Ya|mnXcLHQ8=u9fhc@)V2K1BF8oOXfh zVH@f(5mL}?{#$j1d@E}na3TJC1gdil)89%~3B?KYC?Tbo{+P~px?1lbQeF`r85xVH zD2AYfLi2D#bTr_Is~I^pPj5^NU4brQv^B;Fa#4*!g<_(4_3V$uRXCkx-1C7ift9w> zx9?d$tDQUKYn6yKbriV2hOIGQ3{&Pw3vx1YDP6AH`A5~y5O116!<6nnD#HNYv-m#D7OsG{YZMujo9VSqHJkrH%;zv~0LNVb zLajRI%o65gDIA_)iB!N;sc_-eN>mv-JNhW=y?LN8Oki|V3rg0xwxso%0=+U6A(Eth zSX)LE5MOywHm&|wPonS=v+i}9PZ$Rr3=U>b6SAV|C$r(_+dQ97gdbZ**ygY89)o4} zRNaZ1FR&Z*T4BA~oPP)&8%AmsthbyRX%(oo^h_Ba_J{yO z9Pl2SgCye&N`;ShN}{UPs$N>)^?c69Vu5^3341k|zP-gI%SJFxiT19XMqOqh^E3$1TFU^zD)dy?hBu-;|qd)3`C63ilvy zcx8UE{4LK;?+>IWgP_cu9@g@{0Emyt`}XudeH;+Z$CV+DOuK^VT6ZLjtg5#qr?Ty7 zb|~d+Uk=NcaGKM#gfjYD#+Rn6)}r7O_hJ7tz)p!>FBgeTWzJsAbKmD02?2KXqYi8F zqrUHgXgX$(9s2(`DFw0v5fuBo_6Uic{*kEiNNp82G_Tzoln^c$?32F%bKn0chGo%~ zyec3mx9l*Ia5APCzWg({pZde(nntbAq2~yP(Csc7x2b!!gGWk9XEhl0TOMv4b=pEW+0?u%i`dHgGdoD z2H%G|I2fT<;Vcpw+xMw~Zj4+Nn8+zk8uk{66bBRPZ@*yNBvJ^4;_u)_3Vw8^AA~9Q zu^0d(Piplwp4gMI#h^(4E!w^{A^Y^ZAh8tmHRyt{Ev&`^5I+PRCNEj$kn>zX_Cz!0 zPFq{+K5MbkRRQk!ULE7y-&R-TjzZA}gaOq$WO<{zm+@0xs+Cxc;jP_OT%rxY%ZL&K zu`q^5g;#ufX@2$X?ciQR%>~3-T*_L&IM(>g;CYm z?Lwj3YZlwWKnaKi-J-mb>h@2cqzuhq?#^Fbuttm*Flp``UsMb$3-jB?%fHb0BPzP| ze4DBLBA?&*ur%PVCG!>grv(jXzxYq+1k&@Okq^4_BnMr*IN8^x&Gc4 z3!N6$t*s6H>>rn<{LuYrTS-oDwPHF?|2Wpd@HipY`LHfyF*V-{8d0$j#AW|zJVl70 zQsl;Z)ZPX@Su4Xgd{+L=^km2=%s1hlR?mrLr2}d}8*{oqu1gxqI{Ll}mHxbQWsf7K za}PGxQ#tr<~TlNDk(bmB<%2mu8mEw zl3h&_KIB^WoqNsp-Jd`9C5FDuPRy13+IB7;`#Wko*jzL8^5Zj2^&aL};E-N9xsL|z zFOcyI;XM1G_V182FY-;D`xbUil?vjF)>U)14ThZ80hq_Ugqv9!(3jSb?!sC~DN5me ze>We(rsab7yk`IF)N{XLuG9lN^%~0#fielC9qmcJYgqvJQGobOt0a|vExUDZu?80r#LUS1sD(C^*%{*KRN`C5JC^e+yD zT)rG~#Dw63Wd5Hp_NNf^bCnqLFe#rS&+BHJ?e(TF)U~gnf5$J6(wiG#m}6H@%K(t?{QPi8U2R zR19i~F(rB6;$=BNRo2<*XU9ItssXk4JroX9x?#|1+3fw)Dh*&smUxDJDINIt)L2&& z=U609&Z;}wiCWQi9Pi^PM>kK7Gs9RnyysRUA^a))NdQRVyoBqP`yz_o8{L2TM zf(v=)L&E($g+hrEz3DFnRw@9vGGE5Mua1eLBqbX2!D!KM&}pXx_rrKhan}$eM~81z z0x0y!kLjNuql7VjAe-?o;s^X(nF4r)ilhF8*!COn^Y!rL%I6Zt?!2>*!bEcFb4u8& z+#-ga{$ZY16LCv;uJ3f23x0y_``(;zM>*tsb(%&To;&S)8?C#J9pmbHi+$reXAhQb zLY%3wrBo^dIm3vRH^a^6C`x6EIPt?wVT^cVr#?)a z=`uMc*m?iBl@*XUjb?5+sV5a(naD>#+lt(8K*`zvHxLFfm=m{6(u4mIpkkL8pe?ju zPew@Ssx8w~-+C8J;1KZF;;q#XUGl8&RE z(G3DlH=K(!%~v<|Sbi^V^E+kov^3EQ%Rl&aG{u+PpM=dRTZr6j^j0MKr~cG*@F990C1B$TkQoRXE^`O@U*W&Mk9=OAX!VJ%VS z>B(qVp*G^`q1*PbdKs(4JDPdeHyiT$F=`cx)Sf9PdziHAEb2sL98F{yr;U%Aw;Ie8 zV|2fm_@JmB@tsi znU!=53>^<#Ji`kw0!NPoBa1Ko}DXU?7}?LmKkQIg2o&YW@{uG=soX%K-BZh7;wk!hzzecUMG(&>tS zK)bR%H-aNQKYiW_WCgQjajl2s^$zrkyyd749z5(nP+=`j$e||3*da>>d4ta#|;;H{Nzc=MUV9#(`U z8m@6p&LI9&>;23iB@;{*@^$L6fWdZklUEb1jKLq|2m^hI>0-1YIvw&E0n!*JpEJoBg!Ho(D>IH38sV@ zP@r+ziuq(nWO-{vM%c2n`Q|iDQBgEQ>`+6;^fAy}R)g%|V3r;K-wyE5_DyMvQ}^DY z${UIZI66neT(6y-1BuKcEh2y(io|@i5;cy~`}rnZ^_r1VbO188wBb ze68-yWY5t)qT(1Y!lSC;_(*&hB`&2q@-NlL2U{5knRGzTG+1dn&{l=&ulvTMShVJY z(SSbVN}X2TSHXs>vKXi1bz>Q);Xy=I#1B7NBtzUubRu-4GNFeJK*iN>{;Ahwl8B35 za%1$)X7rZ7b9j!7_5r?LGu`~67LSr2F$jlyf5v^?$&TEpTW<4{z0?@j-~{Hu^HIjM zK7N~&WRFx281GJxlD7^i)OU<<)+We{XBBgMmftm@yr4#d^|tWMqEJDopz`NllAu^< zD!6iMRyIP(TIJp92{Wqes3^lWKAuk)RKz0yJzEeIJ}M#-Xm%Ji+5GDrhP z#~Io-xy^?GRLT*3gS<=#l7^m%ZM*o^Q2p!x&ucPvg*K);?M+eX$|Hs3B)kyxuUt3l zP}&$QrFZayKln9Cu{XdFy6xEG8kl2;M_%P##AUBMHL6g4!_V&|K%`>79SdPfYQc~>JS<<;WVE7k7S@>m@T+vqM|Wa z6dW^9^@)cAxmQ&Q;msW#h^U+W`C?-bosT@7CeJhLhII~1@oSZNCRMU&LB=7&YD>{U9eIp`*4d754XtMdeKuwl@_iJb;10BU~jQ<$5{?8;U}j@+WIFO|7mnv z@#tWm?(ogxgVgU@`ahxjRYp6_bl69(7IG8l=M@+k}v4IFd zyuMrF!?<|iRO%q6tmQ`LfkRlYU_BFKD=<-;ZtQ2Oq0$#gRbkX9qQWXm@7?fe9{D)m z4HhT1KknIjWPPgpo_zjv{wLgTk7^38Pp2oCCPmc!_ay!J-nd1CDp1PfI=^Ex?ANMV zRp#;}N_*ZXI2j9emo@PK%6NVJ#HpLQl$b`wn^k&rNAJ8a^*`b0ClxzG_~|BW$=o@; zX=gCWMlCwj3RR~jrqTB{6;7w@Q}4cW`k%iz{+9+@vz6R_NWp&iXL%%>BA_4Z&%ZoR zZ&cRaCBySK>AYlZf4!+fqH10V3*vT+58glnal)|Uv>8q{4e6Tv*(UJ0JAAjCD zAYlo2AM`KX^c^;y(g?>3YDLL<%u9AgUwdguY(3lZxn5bfJ*HMUGI(V)XP)?Y{>by7 zd#&CO@%H);`n;;^2V*cDv6M_v8c zqc4MwuZx`^_dF>DEBNV(Np$ve(X*HeV&I9#H`N+{y#YHbS;j>Xz>FfjiTvz4xLwbW z_Np%%-H#y85`_*XEaC8 z!<(@9@J(?La$Z9l*sg0_+v3rNRtAFvJU)HZbsJ##rm+8ciQZ%>b+cJ+8$d>(&gAK~ z7sY&_;A*hLH}JS|Cg({M68c2q;k3vF5=sMV9sg>dTHrurP#lKZibHUvXT0`Oyo z=@;P=9-C>C^veQQJ_>NRsw{Ood<0-4GwY>VkViug<1$no~{Z_Fo(8^Y(WCsxBV$9Wf^>{OyHxxL6coO&#Wpl-&*Jky)8eezRWCf zH@5nlP^MQ`xc&X8rG(&=lS`0SDLk_x4>B-`gFUwSN29NhU>WKWzNoMq>6vNQu7HL#3kG5*jw#z$y0^J?3h& z#Sv(GDn4LQM*KW3w^b67V#=_>2948}%J~XgpB`r;gMVE2N6ko13Ev!-B3gM~Lfuq2 z-C1Sgs;DRiBKX`^qJdh-gD6hgFXvxqbqZLuJ+HHaCDa=V9sT(`ZT!=l)y{>4ShL8(w z;WIZO0k)g#Q@m(&jKI0DF#Gm%-{+>3=LTy6!x_eD3LN3xRh2=t_AY2Bjhz& z&r#pD_Y)0vAGJ=!xtqn}EcR$l#*oYp(Dtl>0YnF`I|`5-m+3`NIB|}DffDlWa%Q*O zx;R*+;S8c$+Y~E{)&nb|PDfj&enq(x%%Xr<{FsDMFi-t3ul~%rm*cC~Tj-7Ms#~!~ z-rDsElr>MhJLgq^m#ePWM$z}Q?>Z}B`FXGx$J_Yp{Vrb(I5F}{>%q}$i-FY}!o(E4 zZa_ItwEJr>9Y=5mG(LZ!m8#{m>aIOAdw$x5&-j+oldmQInA5@H=e(r>b?ONr11Do0 zd#Axj_^ z#NO$P{L^n2TSW1k&XL(9gUcob6a_`r=;mXS5Mro@^pve&uyW42(`&9G~9mci1 zlf>jkHS^Vug%xfIaa~n>?dq16?-EwGE5yYW>wE}f)0cp~nkw70ZrqC8v}Pyac79sv zo9y_R$Fe1u<1UKPv+P%Z@e)FG(*|x8A9{a!Xyl9Eebl9T)$9kh_!KH@ z&kD)6c?0S=gNs=U(PUEu7?FSyF@(9S=d)$>uLh_kC)$yT1Ee>s{CI@uVbv zci>;~8D7n|?8y*Qyq6{BptVy{`Qjb;QGlz5Lr8f`&c@p8A;vtw6zK-&fFc$6CAwY& zH{)AipLnSoV0>~Ys8AA##05Qa;2Bo`Mmgf>j#sx4<}9eK2j+5Uuy2!>a9}A1tvK;`3p{k zvKAP;cS{6$Dem8ff7;x}#OYjN%a^-MBAPEXQR`{r8R4;*hX{o__Lvbhkksn3ya(BK zb6s~j6{}yf)mWi@MdffE!;!DGv&0MRsjX5C+HA4q zsVN2@aVB90tpiK^mE9Y_^WYS>4<_5C1F z{d!C^t&jF#bg;LWh$y?P`bE|@S*fZrEHV~0L|3MawgKnokV?-S?pSQfDGfiwQ`?d$ zZ;Ubh5)-^9I+FsQpzqgZXnwzy7hUIz<9M2<7XOWPq)4~O9^!x3vZs%LMu+2R~zEAs{e zZISVQkKuSeleYn2v9cwafo6KE!6HmIqPl}lseH{MAz$nas4;1sf5|<}xX0w)S0ve7 z&Iv9>O|g5ePeOp`B$GxIZt8B}pQ?O%ui_A~&6(T|Yna!4VXiK_^}x4qI;vmUEE<6x zBwlCpk8CNk$=g9)NF%B7aosQRSmOobMP12yOO_a|Y*}=gZiJbsHmmJCHuD=^cA&2( zj`WENHKk8Gb>unW=t;%d!8d>YGmE!f(DD8pTx*>}Igs#%EC&O7BOI?}xw`UV54XyHI`_g{`qR^q6?q2RMx&F^jhX58Ab{!5lKr z{aA1}HQiKu+|4%nG&i7XM1P^AXEyT~0MZUCbVy zZMuSxi-L>>DvTpHe%kGBB=rZun>q2Apo~oKe(o@z-NsOA1-j1}9i3+vqf2{P!yn>e zJ`w2C5-w(s;a)o(8rL6857}^uB{a7&^Wm#4`!@o!X>qSm&UffjZ{aVq9Xa4I_u_CnM|zt{Z*Y$NV6x3Djc}aQ%v$W?OcdsF?o(38e?%7BTcTWg8ds<<#w`I2P&Q${x2Ye_G*Jwev=BakEV~ z)eQMs^!1aTV@)k$Y>Pm9`oRb-(CZvg2samww;D(ejPZ%1S;WeE23WZ_-}q=!{mBY8 zC5Q)V4%b9WevWCLa{)sP3%oOOER?qoKMaQ(AWHjUtJLc!`{BCuRKt}swSPr+TwNL8 zUgT%n<7|S+8RG{dY-RyjSZUV zXCG>0gx`nKBE#KcLTRmHB9xrDug#_EV z)oxX{6Yr)9LsNrxy)73RVRT-ZHXg{h+D9W4LE%{8s*|!BYGJ$du-Jw4zUjK5)6i=& z*C%rl%3@DMT7&Lnd!zw@((-9Ov~4TrIa*h*1G*HC#QmXWzmAZ7 z>^#HE8N4-uKGB(rI{Z>%@|)P_MTkEM!i1UY|D4lcIx?iRZKh@Ik&%%l5Td;FLW$ap z;%u>ZM?k+{!O8>6Po3K=2KBm|dBaQzZWkvH!Bof!-(}^%u3zAyBM>?AE02`8sj6Jh zn~)NcN6KT6$v{RvlJNYpJe0n^=WJ%hOd_vSj_BD;AMLUbtMSo_GPJQQL(NxC+T;$L zM<`aUg&J<*q!k%#qG7il&_Sc6>LNqb4Dyeidmsq4w;eCqb^Z9beYOQ=80>*_5L=i1 za8%49FqM+jSZ_fA-@qv}<=L|He3wue7M|%Umdk=QS_0It7F-gEN;UhOuN4br7pM2+ z9X0LRj)xV;ff?p>g;@P#*_xN4*|3RBFg2tKXyG-oS39Y_m-qNVh~Rr|r=V4_*k< zNtzQyt9Pne6bSTle|EYyz><^@>t zI(ZCHv+s|CsRN)+|Byx$Z0gF*O^W%h)QX-QzADYg)2I1&kwGI=Y*)z>yEiX0g;9^o z4@yXL-biSk2{H;%AR38Uw7RX+qb+j$_~EDK44qmL0#0D@xUikMh>Of8q5bf3hvFgG z8}SCZI(p=qM0OvS;zdnYEeJ+P;;$JFoaQ%oIRKWZVPj(q0Sw9-_pTBFop5LEUqnf* z@N*v~J2DH&8B2+YNpPKA*_kh4P_chX-01r4=b(T1Met*WcwnSFFK~%I5n{~Y4v!(E zrZ7ahKuL~QAf%pbIgN9!gp|DKHOo;o(02;~9@Cg&slRmV!oLxIY7YhyGN44VkTe_N zxEi0=iB53(VTEWx9eAHhzM+r()@Ms8dK=Ke6!@E19jQf1pPV-E@f{pq=)`a>Dc|tx zdlnlh%;Gnp>E0l4JVvUMnyKbm$^`BG@+?O;Mi#wz)hin$5 zx~tLH`%K2G)^A|pHqd?xfCfGan2{e(7^q#;{2@)kD6EX$CK$bWh);Q;Hb`AZM)zEH zQ*j_u|3IjStR-*jtx{4i+7lFl23tD=f z;$39#S!VD2QlTfMpXtQ?-D@{bE*V!Twu5ouPKac<^yv+W_1^ zWXX&>1e`7^C!8TJ3SFQJoZ2Q4a%zN0?Ue#_ki22G8bDHNALQi|sssdLg0gXyg8Vf5 zE^@LgJNy22r*hT}q5j+8-9TZJ30+Y$KiP^*Oxwjy8akrXVgZGGj_rz$9XXHs!QE94 z*W$i{l*gy8PgS%uKA=th z$C7L5BCQ`uCDBJ!M537lFWzkA|1)81K{_Af6Z<2G1{jM1Cy|Ai7(%dyKvNqH`vwS= zp#@QZ0s@fk1s`%rq`N6`_qzNT!rpswWnh>P{?Z78f7ndoN4nXcoKj>Nk_XT$on~3l zvbVHiVEC^TQQ+xsjS+;G-%wSlRq-D8%(w-Rp>~*Uae>fO$qRb?OeBXB)SJ37)jl|n z;Qc+dOL!G-1KNTT6!_Lk`ifS556%nW^;6Q|F9kZaLT^F+xc%HEH|Eg1c6PM7GmxNn z>|jZV>!uV+^+s}W6a)&>3z7wqCbj?IN0I66q35r#ochk_dZ}JEL>r{z0XCL65B|+> z=XkgDDl)_^WIMWv!Pi?#z*Y@TK<)Oxdk`M^D4c%h?#7k^UTJ% zh6ASn1Jp#3oQ!#xivhFfncgE4-7UF~ot7cGPNUmX7l$FDWv?2nCZ8SXAp^zGk1B=H zis)O59b&@jP)s8Cj6Izio93YFIi9@dXs01g=?)uMY0ZoJ~GooOU-c`X)hBASaJabpAZ z>IUP4$563XjRoJcar4*9?QXLmU(lad9KH-W@W4QI02?a?jpa)XvVaI6~jRAK_K6oh8T6&y92?x%5OG`)YprmdzI0Ea5MLIP z^G%aP>RPlXh~-&1LwB##A>oqt_wR;x1hG_cPwbn-N0%WsgmrM&ZJjIAUasedEgXFV zjKYBOIbQ%@KBDBn_>$EmC+>*wkF%k2ngyn> zFBmYzhKOc;$$ayLAJ^b!G2YV`EBwNkp#R`UGV{r+jL8UWV^aP&aD&t6+uU!4s!gL{ z;*UoI2&~ve3J1?Si9|lGv-tn$bv_iCl6Or5oP5oM?XjBYUQR{Aiguwp zk61@^B5$KuNB#Ulv2Q}F;?NX0pJRV)wBDapkeULy6Rs}Aj??e@Uw+xTH~_s(cU#jJ zud=4F$jS|O9k|Yyiqi#KxX^&AHZbmhAaf+gPG;&gU@>Mzh(|YdOJladIEF98OhPiw z$$B=c-eO)X?YrMK^?=XtmHuG?uFcQ2L(*-o9&=!Ag(yD7t(9*mH4F4#G^jS~kZ++( zr`AiGcsWm*;)kz%t=BY>G;iNqxK~V z0K%<~@aBqZ;B+j(Tj*7*k53d-me>Vp>ZQ={lHBNVNOBsM0Sk|7OJLXYSjg&?`qh0v zYf+(6f%yi85zVxhKiJKo7n;N+Jd|~M;Jb8&!$NwlDvMM2&N%!+{e$4 z%4pmOLuV>6ZyKrDH4_1t5h6UGoXBE%7-~lM*J8AL@*HBaiJ=R~B1G~d<&!S882e*u z(6(3jus7q$fnkBVOp;1abWxW6EW$~*e@45R(enHdFyd>SI4C>^3pd^|Kfv=0$L{Td z_~XVz)olSl8^FjxTxhl_-;0}y-F81nE59mVdHY&BmkHa1OA0~ClH(c@>l`S(JrBOu zu`XGcijm~aG#`y|hT2-UshB?A48)19AxgywcG5(4f66oy+rna?DB73~e7!zH*N**( z8mCV2#}t51Zz}>g8ABVPa0e5G*39j3@N@PgBdrH~@Xvrf;>%3Pa4@L^bO0QvxpOHu zsLR~<)ihBaV)DRZ#OkU3wi^&`THZ}rVy<3ht!>gbS;FZ?hx#z{uoATa-J3HZv1mc7CgUR;gY!>tbH!}gp}^1 z#AF;`pMPsLKntXMcfQ+MfA92Cg|@@t?UWx9!`wUwdme>k z41P!1#YL~}(($QK-C1m_mqe5hs>M5VG#D!~evmWq1h#D~n(o_Nmn&5oZi&wmq~|A< zx4~DMS?o4{V_;+tjCG}A7t`Nos8q=d!(NBw&H7c`yIKDqHvC~s4}nB{Tqy0PMcbT!1VtI1%y%7cq9$u3i4SE^Z&2kcVJQr*1`2oV0&3+Z zuBfUG4!Eb2;Ol+a90ypJ>N=mxWyU=935dLcbVlyaa%o_s_&fK+4>ZGgSz!{q0TLIz zus(%UgfnhzPVYxI$VA_Hui^$XJIZnh&tw8FYrkaU2Yz3Miu?|KHY!R2;K2lmeP9=^ zxRPxl^6|B`Z@-)d8AU;dxe!GEsyvlQgG$IkEQ%?hD_fS&$l!MZg}@?pVP5k9(;PvV=I1nbKlm3&=L=ijz(=eB)fUDO>5Xiyz9XLxl?GL1+^XXm(u8qVGj~7qTDSnQXMbvg?8}C=Zq08}fWELK ziP+v;d9c4gX}yWSD-TqBq;$p6kMH@nrk1m!r2V$U)aAH;0cIH%bP{pdeoG@R@Q5v; z9knFFN1)I%W(Wg%Mu1w>zo8rCeUPCW`zrry=tc*+buFU6iV0nzu6)F^XBX(ik5yh! z<+?NbZYq!|54_z}oW!q{nl{5j3JYpZQtSSZK2w*9i(Mv07E~7s2F%X$3Rg?>)`o|f7l6tsq6Nkxvndg@lGK_ykX$6 zkUAy?0V1wW8T?!GEiBNzhf;~LV!TKyj(MBN2=!w!t}fzHzvCG%Cdw5Y{OgL!W!l_S zWFE5q4E@VQ1&~C;J$nL+ihsHDDoE=No#)`crVhZ2?+V25e_*5!3^fLX>i8O%G1w_f za9(smB7rP>Ww#-Hv9gJ^{vt8HuB)@2fzEJMa0@IpckZKDJbZ2Xx|Zta{L`<@CTFr;4zjZo3sMG2ofo$NK;k| zuT8l>0#oU{IVxoMGQ>G5zo|W>FzQl(URTRsB-@Xmd!S>8G(tW@Y-Qv@Ju-#(>trsd z7C;&h*3)zIHT$*5XEl?eB`l^aW}V3^mR&*x!R?=lUkWwn2x^m!Vz< zGyBVfc6kp#rfl5(zwFYPwU&Gy6$VGTxV+l7{?y;w$ZCWADnk>-JSti;7iL-!ApymzoQU z?(?g13l>>xU&pn8KF{GUTZQNf#*gsuEY=XY3ON;u86%|7v zr5PE+IfEnQ^+E?W84ZfEnV&r^%Sku{MAin+1RdgqjLvlP>~ubS4sY5}CuvaPAQA5m z%y*Z%if2@z1%loZRFRZfgHw)QSCQBVS59L5bH}1UF{UA&X<}rrb(09D*Cxn0zYK~q zLjx*4QAzM&JUh`01|k!tBW2EycjRtf2lB3f$w zusBfHBE5AmSpPdAS#gh4F9(nZBB`fk?{hZ8^%1eFlLai}fxeruTVPw}a1}ZdU0D zxskg1N5b`@x%|agDY+yF(3#q91LF}YOpx;!x=4+Wl^!-Je-172a|Xsx%je-NS-ud35%(5xqFuYb2i{k|~-L zH$E+C8i^FGFsB1f1ol66csvm+bm5{TUcLqJRcvDjsflIuZ;dEa_h7zjDjm*F`QE$N zAdMZV7zi#p+g#t+6v`oMg{F@CAOhT)_#w30tAP($Vb;2E2T@^Wc{NjuGb`?z9G_uL zq7dt_e)+f~L5t(q^&9Xl)+H0d7qsZ;a;~j~M@1U#wgM%6sLPc35ri^1P!rUOd~&Q2 z=roFg8LZ#0k`OP7;g(_`+;OZ@B%3Wv4=*ATaT z4Y74jWSkgUm$q&EHtU;61~tGj@MW_(4-V_LRB2L9KyxtiCoWQKxxp3`mV)c={0Yvy z)6cxdy@G^AANkI{>AQJ!mewj7Z3_Q^PICnQiOMOB|FlT0gdtBZA8*tGx#Ke61py_^2>O~#pQBvr=()r(v*m+%MJsLbaim|*6f2v#QO$`4)$xU+Tq z7y=jk^avqAvZPr^-$~`bFMq4N@Us!Ag^yTc6P!xJ#Z4iqS)kaBMSxC@ zq%9%KLAy?zoOkm14u)1DA)Xfj10Q`p%G0ChJ-LxV_At-4hqoSe*|mPl7L0&v3x8-U#0RWQ zLZ-hdqJyWMPY#mprZI5Ih}~n12Ro1_y>fj)41G-MCHNlw4@jnGG~(cL#RP4}!95|B zsAiGH%n=@%FzU-#W2d|`U&&vJT*8nfGd%-|-5}(GC^F5RY>@=I3duhnw|~h}Tb#ms z7?)N@t7MvxZ>fw@Hf0JnzWNuyK*QC^XXvQHV}dKKVeo&ztM~(ipIXU{R&~oz3d$?4 zlJ~E5pV|d@C}}t_WtF7+a=NrUy65;EOBHp{AE=9O^gXK;e?;;HaLV@O=QWy*4CE zBMReR;@R!{b5lN?>x|V%e2{f;gU-iI^`4YM{OaY;6nYe!`)Itw-yiLYV}spLuKkkk zTiOO2hduHo1H!*cKSx2vB!jH~NUoQtpGL!s}+jPEu7M=Z45LqrePJC&CXRhA7|65 z-WRJ!HfQAhmFOAz)p6Q5oXH@$7(cONS=B^4T$Q5<+zvx?Y7l+~2iVmdx!h;+*mv#+ zYWd+2YKx3*VPxoS|BgN@G^+F?AMrj<1Nf*kYJaGoF`&in5WGNiW8!$3HThAG2Aj7h%X#e=6*n4uhJ*n zIZ!3ot3y8S;>P-~?U9uCaROsy^@o*N)dqJr@A#A79cINxyagz=*=87g}Kd%+%eFS+X-# zF+bN7Q3*ISLaKL_6KX9o>Ot|9-JA#fg9T!JE{Zd1hNR0Q6XZO+(U)TZ-{-tH{Z%lX zlS9zAIJom=E*wws_!Qeliu-bGvt&w7z?D;nxf%_pu-=qbc+7(CUF2aq=eW%ajG~`% z^biu{T5H>OYpo8~x{%zi?-uDO4u5wEv|k*4qw%E$=ZXe@skh107)_JC^}H9LQC6-x z-eVbJ5FZ2yGK?Qq*y}!|J}wv{Mr;e|*PsbdZ&~FuK>r&ayYzZj*U_ zYCLYg{Qd$3(5)uvBBv6*R+2_8${a7Nut<$*VAm_-QC>^!U#%tcc+x}efjigcNuC{-G)Oa*dK?RGrG}s`(yx3YpmH%wGQ#v5I7N zi)Q7V{Vw1KZ*}z)X9M-H{pl$0H7g0;I{fVXN(6_g@EWzooFUV*0{f4NK4ev6gUDXW z3np4SxHf1(QXL9AKuWf^x*WX6r*}QqK%H8(m~(mfuF#*KJH4@XG zHbP@m@9(S%WjE})o5v34a{qU`sGbN`2C8o+H=YI|=LG!WYLAHQ-mdi(MMje|4rCNH z&sckLFA9%g;IzF_t;5+YqQ}#w#`x!N0@jVq&5-AlC6LL@F=lfFQ7R{pn4oj~>biqO z_ROluwV1*tWrA9AJ+*KKyN}3@aO2`OnQJikaLy~*278zN+E73QnLUQ(?U+%G9Vaf8 zp`IlYp2Q90ruXf+jnT9H@U%DrKF^fOMmM>L=QMYS%FWjkzt{L&=*4l&!=p95 zeLYcG|31K(0>7jEyIaGGH2%5P(+AzRC5}l|Oj=AaUr%OxmQdQ=sD?O}-|A{!|E7<> zZ%xC$cPEvwUhTNv7YqoI4n;5K+Nstcs+Ewu-o-eXBu~){uRsO#Pn@{j}7VudZfYC$R$q^2Fv4^)^44~{;&*6yz`F`8X`o_L^(cW6se6*Tc4BI z?s7{R&0|lFSXsH$LhW0kXwEh8E%4*k@k}Mq=^<_oMtw%No22%fWn)U=q|#(Lv5`#X zAiX+y@H`gq!Ui}Pd{j&-y&lc!Wu1H{m-8#;W&eN3COrYtKz2%E*MMw49xz%er(%|J zm<*2a=a=mn|Edz5d`)EUe4Y($d$Wb4QMaL4B!Yp%=&kt7bq8nS+ha-aVtaoqv{S8N zPL8IFN7~$u2#tHZKX1!@rSU(UX3F5KwXMP+^1V7HnLS-#-7nLuNM7BVb6PWhx!7vE zT+cTRynsUtbs42ogbIA+FFXxHA=$gB1$NZ{W_s#97y2tw%_h@YrJ&72Mu;uuP`miC zNT_cD+XtI1IQ3Eqn#z10-HQm5WpQi+HaILFr}NMCUo;`~^}r6MN8@g$$#yk6Wk>d%OUGU?MYe5$P#MiKjs7)7!viqw&-fzxKs0l#y^ z?CJF9-o98J#mv=myB~nrRvWaa&0|q;gem`I7AkdjSj(|B>&N*I#iKl|c%NhUL_}Dq z4q+1S$W7e!4UOe;dz`-eZQT7PG_IyCl_M_90)keQ{uby*M%SgMeAm&MT_5Iv7DQR= z)2U8`=N>t-8$8#NDQh>(?>bZ8dd08=fA~ zsJ6R(#2@}9dK0o4lx^}KxWMQ+Q!gL3q<}Pf(Pq!1aRSV5zjoQ_a`BtY8%xGnhe{TxYM@<`O zu>vuFAnXmfVD$O9b2s-cG6@Y;h37$ZqC%=#tGaaT!A4luLIXb${mJgqE*k8mmG|NN1`Oj8}Zg@Obgs?#pG~tO0r1-HPMJ+;Q zbH5;><>?%0Gu^tX(M3{W5jm|6EH@w33v#J;?t#UKzDJ|rA%I6zb9#N~6NLglEHg>L z9z=&Hf`rL#58$Ky!1Q6Cc799{j9`lyzvIru(MvFsfXtpo)w}rR$8(1^Xqy(k!i!&v zM~>vQc@g4@>Z$Kw|B#<@y|hrBHh#?X=~ujY>>~ zRW>V`IP=FP&*L{WtKJnhHS%!{uU!X!YlN;6^zrhnmua?$glSQl7N5ZN)3y9K=gHvq z<26~?LUks#zl0h^=iVre`e*c==^fUxl@<8>vo*8?Tzc7{YmKLKgLIRESP|Fj3URL@ zSA{U(@DAlq*)&=F#sTC10XwbufQ)SRj`bjgJ{)WJNZ*9r@Ey6Op{mJp2fOUz77^|z zG>_a2te(eG5gRy`xq=emku^1<76@O;mAZY>-kPVWJrFw1Gc~czVT&ZQw0J%nJ|-LI zu6z)-WJ28?rvK;gBcwMJN)d+*nkus=m_FVOPRJF_53url%;M+7rB@8^^K~Ug;K)+%0z>QOAXl;^{uK%I)U)$`pVE) z2O;_SEuRT1-7GHHNz70Ddeq3}W|3E<_1a;M(88DWOj}xwvbV_=MgAyXpSyIf8mB-_ z5vBNv=a|0eFRDb*?EyB@%nIGPfwKc~~l90l*E7H5RM@AI{E-V}FYYj9h`s6-i;ILfHjVLDP1)9=$PE*8SQc)okvQ!IdbR@ybSw z_{ZxFz1fSf;QLXj9~SgqKb}!6+-`m62hIK|C=b>}j7Dcz>3SjW#5{6E3tF67>-;3{ zw)<0FB3!?S4c71fXaS5oNx4z4OpJguK=}NZ#HztV@s`^ntb?0Q33y3H1IpI#j-H*q z_w#j5y&mMnC*=Aa1D4tN@^6P94YW@F?IV*I;YzoLM{^fOHvKZHjK|Kms0#lLVgULl-6{59x~KY&n<| zKr;XJ)c+(tz^hsnsc-&wlA*1RsI;aaxRfG}37hPXk0pzxs>74sqGD`UK|jJ3`~sL9 zZ|j!~|A%{i-)m{NklM8{S>PiGFaiCU^?Up7hP@ApbIE%f`A{6LMG=}0gOLKfAjzNJ zmwQlVDcd5sbP1+&c^q#1?tHlfkRnrtxzugV{%lpcoS3?g#zuHtGp}(6q?jf}*jvG73n9o^D)TKi$KK;bI`B<7xDp@G0i=E3$)TdNb+WP5x1XVhm_ zo%}U?lM{jH`1X2>PSyE&RURC%j9g!y7m5J*dzsER3yRGiJQC%0(tjh*aIHMxF^PfQ zgBNez`r?=0LOYMCEC1i_3HC*F(a}=lyzZV_X#~{iw18Ax+Q0Su4wPhLSpflhuaZv7 z##qoR6r}8<{`2-*4C;rY6sf;-C?s4Y)aL;C#kSyqdUzh0Q5jOEx%Kp*Noqd)u9U$N z;_I{ieX3OAVg*ShhvsTUI3Hf6fle+_$O<)?Nfl<=z0~g7Uc#neKQt1K2A$`V%M>KP zxtW-Gu7S5qITjLmp2vi*@ukj7?xYY1zKWR1>CRdY$BpIodcRGtuXdWHwT3vTNLUBU z%)^sUrT@%!ha-_maHC-|1l{V!t$wAcnytjzO&09dstNn3xy3TvM=t8Gt#lfoD@tsv zInLPcOV_LJ(M3OV<5#FiBBt}2b4WxEl0aCWp62`>YjwHRT7J1>14Qk0ERP3pR7uHq zDx`n>7jZMc9EoVDDPg(G6?kEe!G&Mc)2)GKfzpdUJ{c&{n>QBKe?+Vith57%{LAtu zgH&5&wUb%sPCX}e(Xz7nDL);E#v6^>aTSK>6CmkX-?#;u{>BG(Yye`Nc8McBwOyr0 zmXISIPSq{yAytM#3qe0hRb~HHiu4(XX&J7@%WR=^5$I9Pp zXxZDMC98whJgybq7Z=-PIZvT32Fjz_n+}(UeVp2_@ z5vL8+mk$)8pfR@|z_veH?plNCzK}`bB%pDNmHYCg_`Okr>~D8HQejWZ{e=917&0PF zyi%u5z6nCg+26F2`ZhCGO!@XXgA8DUlUJp0>;??0$tNJf&eV8dOoTN|1%UCwq!xBY ziQ<^fs@N8%`aAjBHd?-L%BFv+#5>q8_WQdCQ&zUhgL9---yiN55GgMIU6KxH<6^~+ z->z7wtNNsRc@>dSXp~zj|l*Ow_o=w*ygaE;@`RN;1OD2bA;m@ZlZ@jtw#R7+6?XLjc<10xlUf?)7(=7LERr z&g;lT`m8ND?7E_?c~z%cV<^z<(Tto-8(y{0jr1V`eTK}biVPPIjDrpE_E`B3yQMY1}59V5mtPdU-e2Lc}6PIwPxy#nep*X=5ZC^^()O;JPAt{{ z91WCa?3vMV5A6z78l>pmKbAUVUfd(t0-jGG%$)drp-0Iapm{Fko&FITi)`46fVgqn zb5G%?T%{kCdl`K+F1eDeo-&gV9#xZ9MUMmS*wt`wsHg^ie1=>0y@KwKmg$~-aj__* zx3b;L^gQG8U-N)$37ZD%k&Q}%FnpIv^hoD-r~NrOVng8H~Ep7^>_)rpOJ zkupl7P*r^O{fk1iV=D;;I4aT|9Y4ZdSn2OTvCFxrx z|89#eb?p0B&bDc8uMEp)zf*!B51cA>?|k7rhvaiIOkwhwB%{qL3&wPP&dSbJc`qKu@(vf4;`{ z_t4m~Io~&Ry{iR7qn8?tH$NXNR zV)YPheeD4ogH$Lc3JgFV;mV~e$9%s7O;ObAO}v&ye8plxUW@3xvDm*q3pk~}M-IaW z_uHJU|JLf>l}CE)USU1Z2gns<6aVLXf&&)K$s_COd^Je~^eL5U?&8CR5gn*{bH)v~ z^sH)V95aprG^^V~Y2RK-jdL(m-!CEkEzwkJEw*|R&t?h@;wjKI&}i~B0J#kxnhr1h zmpI`*|7HUKvHhq0GFK@DO|0_0ld{gT$o6h0HT>2WxfT;$B>}@J<=wSQokZ10(k*sp z7eiEO%VWaB&J2Lm1xWM*NV?x8flYbkGe7RU5;jt%Ba*LUsdZH92-t%Hg+5&}M7$Nk zqkG3A3FcUR;Qv6JLJ>-Iy#r+Hz?F6Ua3ZBh1gJ0ZF&#%O6&mE_Ul{9KnY(fbkh=i= zz({NEMCJi5Qkc!vr&1=__WA)9THOCtAikI$9B_u%oZl<-bD8YS3mpCuDn>dz_Ur@Bw;{*N%t) zo089h!7r6~_ zS(Cx037KZnman2sZrt(&EJe-1$m#wv2^)`t*Au4^vgs!JO#@+M{Gl#Qx%@*3hkr=c zj~KC~Nu#JZDQle8qS5V|+{+Ye-d9&eIf9roI632M4b9xxfSgyy1+uu=D%}T$Nlc6W z)^V+yucHlXwH?^lAU5ngzL@}+0Rpn*Rmm5sO9dbIZe|tPu;&ROkgB)5vFb3o*OUIkAFNW4Wh4wWjkr!E0yIM{ zWA!-sucW)B1czN=OlU!mj1JqJ!V~wdDb1(IanQkB4g26tk!Il3mo`hg579U{{4b|# z4sKl1P@bbDEdRx(!GEjW1v7FJBKSLnNxly^K?Qz15~1ibQ42i&r1sDHqWVUfwBhzY z6Sx1Qu%kvv8Wjh8oR;F0508FPrVyak@5cZBwOF3F3l)Q4@WXX(u>H<3Np_>(l7}0q zp!`EDO+3?ICZU7fw{N84?JQ;b*dDcaiF^YY=#3IkNaQxAnOI@ho0NDA$}~?yf5%nw z*SqYz$rAX4i*+U@H9yBy^e2(fDuDfS3WWiU%JVYe>3qjVu2|srbUYj9mr?&N41eG) zn!ggcT_MSA8gO!7hWU%;{sWeJw?5}=kUll!fhQd1x41zEP8*IED-5|Gj7SeFQpkWg z?j^0zKa*~+Fs&F`QZ0NAZDfLgsahmEvbm@!`;9x0GIPS0KTj%K(W@GtqXRUys%jiM zc-dqAUF_!)rKC|~-|rGg#8VA^e(*fsxnwL-{6^~imk5$%UMywRr`vEdSLc-0c$&7F z+{S{RIXm=3{xoifoWAk)iQ9X^ygQB*608fCRbQ|3+#dId-5_}K6A!q+mmmFL`B%|l z48K!>Vob_EN=lAIA^@OCPY#q!g<%MT>E)7 z2mjK%oB*Zbv>qCdTtJDTL|vLx2_ZM>_ z)f|UOedJmborN4xq!W{Zut2@+D-O!j zB)7YGpxcL8o?)Sc91+Runic2ERezAX{aIH#^eekV(~WBWYD3_@r#N_~SXL66_@n7# zd7b;NBIwUhnyUR&NiTeuZBAbp8iG1RhbI*MDb~KlG3^iu5IQkUb60pxt+~8hSbz`@ zj_;cpevz%nl1_ddo*^JBRpQmh+g&f8vYoHw7xlXjeIa0FkQ%}!v66o`6yI+`#&1uh zaY$U8D9w0EA+$N)ez^z=yg7{O`LHB=A24q~f(Pb%pJm+j@Zw&T?c}y(>r7Teg}B+=BZ_cRLDa(Mxw9-;YQ|S zuSyoSBh7<6AzP3Y-XJY79GT#_yy-{cxjaQW)GT7f|H&PA#gxBaQ?ri?zH+VDn=IU+ z+Mh4OX|RfxouSzbEr^&sUZs6!as}K+^jQ4~U)T+W#77oka-1W3qDWt>NHK?8Q#gFG z`Cc(Ac1ov)+O@-No^n0GZ=uksoiE_K2{FG@PfA~bEoF27hzwk;fOaU}d~w!WZ+S@w zir!CHZ02%>uG{`1ZYb3D#OiuDSL;k@bJ;{*$+6doBvqC_uvTdO2w%*0fI71R+LUQ+ ztEWE;CLaXGPH#EUAJ1ru4Wcp@V{o_|*Q{Ad)!6tY!bKG*GTfw~6WE$n`Z%y7R|d;d zy?B(>?_AE$BAxK%nB;c`;wgJ;su{+~=#yxw#fE%EX>G3gF5 z+%za8fZPG`U)5I}XL2}R*neA6_B3oeR%vIgmteYIRzLOjRgd({Kt(K+r!a zb3+Jk#Uv&zELGKsbh@Ubn~k2-Wpf4^9`ING9Ci@>jB`l6Cg-smq~WohU96h2R;0uT zGq~*Ikp^Ry^2u~+T z98P0D%du}r>0legVh*Eso}^}GNhDku&7C*op*@m=^5(-!Sh@9HG0$iJHu7Z&xfY+g z|F6v9{k6PXClZNdta|8Vkzy3-&WlY;e3b4$`DoPGal3A(4Zm?%53f+QX-!sM%O60B z0gLmdWP2bC%~sfrm{Qcggq{O^Xwr{H&SNY)&QPxbIROKOWJNF|nU*G$X}QAs?yrP= zLOC*pqy(feh9-zfefKE>E6=S$3I#XM=<_}C&H+vsHMqGKc(i72eU8T>K8m&OZKtH) zm5-|ECT z`_w=Jfcrm&@7KP(15Z-)*9^oiBa_HukS=f*czk94v~^uMXZvV?_s;)OlQ@;hg@(M_ z1L0z8D0M&&KWeJ48?^sJv0QcLNL%LGhc~fRoN_=V5hej>?9NwBLF#+`4_H19lmB*> zQVL}+0Ir>mdsKq{e}V&BhjJ8|2?n+UUwuRP9%+W*+G-mDg>F1zUmAyoh2=|Ei#cC! zYIScYLL=XgF%LpFpJilylrU;gnuQ9cP+<7y25j{VIPuhY*8A_>`xkf6t_C%lAiHc6 zf%VFGm`G=m5xWDlGSRvckhc~WT?fSc1-0jM8gnMp?UnwSV>k!(W?_{7) z%`Z`+-M-l7`H)k$<6XJC8F>2gw!@!B;f*)ukH*q5ZpTM1&2oC9(B}+#hmE)6Na>B% zgH4DUNxgL!oawou0dL0ma@06xqz0=N%X77@6sRDj1_&SvHm#_N#-j)ZL3{Wv4nV;? zfDb!Ib!`=ymDD6(3Pg@z7L|Dx?@t?4LPR#g(KMu6O50LENcl#jlw|AtfeLhRK*R$< zM4!sj7w~-3mRgXfH}dm#EI&X%;t3a8ba4}2^I(dkO#`7JL(SdYB48$f!@!Of!HkM^bw+(JiTUXeWu81XQ?8& zEfyv0dm(%WkJ@m9jD17+{4Mn<`#)D4GflxCB3fu{l-6tEaOwNM1>g4C_o>K&jN zD?;YKGFp%OA3Wibc&%pDHOs2Privac}3FqzfB9E$k%(fJiExQ#RPev-Bvw_+6 z>&l|O16Og&Ebqwdk4>c*wF*Z$HB~ARXY}mAvnqa%`A{?4iJJDZyB2cG*Nvbz2pQ`j zf%T>l-w1t%DT8s2l~N9kB_G9dHN_2H%Tt%^@_L42ZDB`pzqqueSRnpSXe+eh8nOC< zATh`psjpKqLA})R5v54cx`TRvQ^!M%Anp%xB1Djqe(H-g{r+b$D z0J$(ZUBxbjS)0$nyI5lJ_c1u?l>u)RGg*}^ua7*b>K&KtjC4e9Z#`5-LSv_%9=?-B zD*;+93sXy$Z$HskGATt)hVe8!o%30aS&822%42<4tD{)r za~xX!=E|mSQ0iy2-0m(TT&hqzy3gB|DlO#5EkG&}uJO$gR^Z4IAvqXkCiURigmHR; zyPT62hyhF>2IhP{Ph&)uZjf!@vvH7aD1dDzw;g?2&cE%Yd>aC;>_xd%dB6EH?(^xN zVZFFEa%2*4&T-jF9?7TeXzo2nUcz0593dCt!0Fl|idm<50k;cM(~lYZdyR9H((|on zix(Ab|DsSxCeAO!A>skA%OlEFeXa@c_P!iZA3!%Mv6%iQryQzSi`iWlXk+;+D_h2s z+9AGbH!0D1pnR(|Q9#ZdpUJR~rrP37-1ozu4o$8?x5u%ol9mck)x#fv0L6{39Yb6s zhVRekR~qpES=FCJNG39`xvd8{IIpLDq8mMP&T(+uxL>tFQI2h~P1P3mcWCUSnCO@J zv$}`ez2x-@7NTq8KOB>#%Hh^C(fZxY?bu*#r@nYRcN7x)sqU9~UdwytOiRM56<Rxp5_p81}9&qWC5tZ^#P=nt@Crg#Wd75_b+(LE%B z(+^h$7HtDA-*@wrWOKQ@*QJ15`j>-Msl5L*&^RENGDzN_uU9xqf@O}w=&cZ)WC?nn zTY>e5jOdC-&e@g;c)vOiJE^D4X*&(teZxHLFQ>NL5>P)Nm&o{r%>f$dxYsjvvxOF7 zyYyS|OQPEF9k+u#3|t99p4nwMv9OZZ+jJBXy+F)kyDUlIf%Uwv%>Qwg5jR-Wk9**Z z50YF--{n}Mu^Ucc&wlwg(OdRc!MMmxAx_I62nps8q(6nN$bTK|tOxW{LB zr_@Ql2wQC+mKG&l?MY?}XrdOoa|B)9aOU<-KX?1>{FO}>NTMW^R(Oo{SXq+(9&vSb zu#I8QEMKHhN|~*?J7~fXUMa&3 zFe~?N(h0#LRXPkCinO7zH6HR>EXeCAp$ZKwfLq2H6G8oP|9Ea`+JjsU%Czwagv&eq zIfpn!J+MXp^R-@hPG9DbyeT0O#U7k8r}m* z4r$KSEsAi1TJy{9J>E2L8B|NzK|7ZbjsX{xr0fad?e8Q?) z#`NuAl8C`F>k{5#r{*}|$6_H?aKDYa|J-ZU%k|1)V4&|E+8w>1&~rj#{3P? zg5uR@!W)d4!17?mgdA#vQO9}WHdqu~-N5{MH-FbHB|u6m6W&v^qe<@y^Vby!Q86J(yti=C#_3lPND( z+vyD?5b1=b4@Sr;L3Pa|$!RU)$>$RezCt!P@2046e=S0Nr2CvozzugJ(}xVQ2&DCrHlW6qy@vH`agREFi;?G2p3|bMkgTkr zky7?q{d$u3dB+$h`pTqcJ`h*RtKvZ9 zKKgjiySZA5BZv#mcQW3%=tuIp-v-qfzKbzhn%x3AAIJr}VH4f#$vYOkw~^=0ypK9Z zKpwG>7(CR;WCX*-%K+|~p@+T{LZ&;iQN*`IriZyx2wr80XU|g)za^x%|6F_?3N!x! zFu!E9;Bfn)X=W3md?$%Nki9hC-w^m%Y;abEC2f@+J1zDSw(6(QV)jw23fkFvJH{*l zKRELO&h*5n54ptNCvzj5vSWFa?88H44@J|Vzhe;{R11|Y3iH%R$9J1Y*7*b4jx|!; z_?;h}u}dBW5r{^^r55Je$}cC1qVX!glM_Vxvc2e`-noq9}0f)iT`~XRYVM!YdDhzhM`=NJEGN~}j zYl*({^z`7VNGVK=X7g|$!gA$`;<$|GQW{sz1;W!n@D`<|e*0<;N|!8pYl9Rt@k(oG zJ*lfh?>O5>)}K0W;b;gdHO2e>!W@iq7ur39pAhd0A`5=n!xpk7_Hb{=1X&Y;C7`-g z32doK^%MXwp+-YU2DDZF0jg$qySxU}lDp^-14CdFVsujCBE`kqUcB!RDUw`=)!lHw zq%!J<>c8HV*B^~bFL>&K{qB6+KXMdt+esCxgSHo4={)1SQ5P%s6{=M1B2%9nxN`RPTV#rk z+xLe&2g$omZjNeb5g|ABC&u-H$i>5tq+9;ZOGm_#P*h@A?^o%$(UVz>lZ*#EaZOYt z_i~&%=QPk4OZ*})Zd%G$9oyoM2iY|g@wv|LjhT$+yC4~Mdl9(m{CR)y@~s7E1evWR zAS`rxSU{6}erI+MVE=G2DsHL1|LXMmNRa+RwfcxYJz#vTpINH2Q8THGOI{cth_SJ_ z%A?e;Y(dJE$?-$9aNGfWt@Y-6b`&BNtcKJ4gLMsXkUp1Jtgmy-{LMC5iuLp^|H#Yj zJ{jnhbC|56)KhuD-lLY&_Nr4f|Cr9iO!ED#_xxUNC`1;4w~F;io@a^?XxTqmd&HB! z0a|k;&N__PAXl2cJ(=x0m)!x9u7K-B8W|JOKoZx|KU1US=LU#8b_5Gr404lnOqUKM zF>dmLX=7N-6Fik8|1BoZl;wMAeU;hHkAKYF*VhVwhwW&K2_Ey!DWp{F5Lv90lTgLj z_l?e6atbRr?c(e!|2)2z+HENkFumlisseBh`D+!z@I^iOM-+5jY8G0K{~(Lx+awih zts~!~%3r;RuESwGAr#3`6Ig9Py=ZoOwk&g|>|b|rhA}aU0!Swkb0q8h`!QqF4?lJzi?X!iDjZ0BtcF`>1Q-uc--;vV%HTmv6WL=}s1$ym86?93NRvkhe?_Z@HRxgp;ek^mTm7f(y*!=h6_0sM@!xev=+z8BV-po_ zU5aO-Q=*qdlj zR_NYK4;T)e>C~b9W^e_{m8y$97RJqlgG>ZX z!qgfj?!}6__!0Z7aN|rE;OH(qQ~$2}ND?q2 zA{X!{t6^4`Ge3CV;&i(04sDhvUYF?Xo$DBcgK{45aZmrXvf$ zM1=w87eFi~HqZkC2q@{{@*%JJxRMcu(Xl_e_Cx*?C-+@HAvXDz## zb)KI>+b%ZKwOlWjnR#mRO*riivk1yo#52{bv?C_pywceAIxNw^z>8SWj6uKUh+~0p z_EaQ-k#p7Vy1ZX&1}IDD#@c`vi0akm7~fvJ7LV{;hJ!~-1BoX(3_rzi**a8WU)RJM zG<+{fBceZj4{y@rH{pu!ZeV}z>kks5HyU)4xF7%7NQ4*?wo50Q2$LDYM;h06_bfs# z>5K8b<$5WJ&tIR%ag};8HApt6u8ynA_kr^Exjm+_bTpX8a?#B$E?5-)!!w+8E5Ns5 zhUF63AY_rkzP6`=Vr(t~K*;9pBwJAUyc}`UGBw_3s&=X-dbw=n`8Iyih8L-`L2}gZ z)O31ILc>aA_~6qh^!esQd~WvodN3|yJwa^I{jIPLCcJUCUwryke{9lwQhNDR$->=} zM5d&JcrBh9t0(z3mlNx}PTwx^QB*k`P=%wZ$6jw+&;TkS zPNU~%l=QTX((?t(-WQb6-_v<3s*=8-hLWjCpn>7CI>vU??G}huP;W2(elAv;?{fly zXr%u5(PKVxPe9B&j%Lz(C?kX?H_?5I-E_K-GD&O0O=ro|!k};(ZB9S3UdkqjU4w~K zY;7bINGdh2inF$6;F;oQpeDptUN_X6;EMsIiiivXd+dLY*Q~QAtDhGfW;cHWb|=?I zCfvb;Q%W`$ zlx=#SA>yFu!QSEdK*X!Wcjca z;i5{eXhDrHw;#~NkL8LI3dutlFSZ!P7q3Vrt`ctGeu3LO8Og$0Dofh7@(_pwgT#lk7A&$ zBEmXO5dIrh@<%mVhIwn&BPNgSutn_$-|I~RWpcr&Qn(3tOK28sZ!%|Ga(Tg7mnEw& zd3h=diA=$m!&|t~I2b^AdtxFg=D2I3xBO!331~4LB&t`-7s`PP6q>4xepr)vdi-qt zX!!kG%-;F&0j=NNBZ=G>2FYQ~@{JTBc2pu(6dj$VHA$Ugfw0l}dEcjgp(VAEK zH2MJRp?1*Mc`1l^J`lY5szXscnPXT<;(DTrz^oFy^gpvosg|uf&V-)-#+7vWJDr;( zm@?}p5o5Z5K6ottT$S$i0+<{Wl8DYBZ*(ufwicfw226PyU!}Wj%WZN_u0YW^b6~J6 z8p4P17yd*gen`bECpDmYOV^2JQg(vbZbSsQ@{Klje-a1lhW)oqAAxP!G+9`F;pqel zYC+_2bVjcZM>bj5r}VQ}OFTDSyI5?%IEMnKa!8;px(@i$qLRTNJX%}t24)0cL@8Y| z+V%X1ldxTDi4~5Rv0)}mpZtf-lzSCy z4GE%^j;`?ID&wM3V@5Q4?3Qn#sS|`VVn8bc3Z*iYFtRA?92xkZGI0Z&!RP%HANkpXGZ0D z)>!1ALOLgw-?KDvq+5e4zCcYGcLF}J4N1y?TMf{&OeOIE(}$$99xpBS2(Y03b`u!D zB)rt?1LQtas%@Y0&Jl-wWp^(%2+J; z&lE)NfwL9LE=BS=bT>hH>)Ad3Th_P~tU!*RGcoc6sEAQLs?-%ZSDQ#`9$hn0NXUd@ zfsYVY_!++Y>>0k2@;~a)23Eq*&X;9z3Qv{= z3*mC@PSB$@XX^?a4KJ0bFOp4-=r~37Ne!UTmc{l?yMFCpL_ExGz#v*eW~=xuv}W)V z!>oOeYK(OB5OA|pQ1NTu#B4rHI<^HnJB9p~sH*vZbT>I7qO#s}%f;{cUP%~uy{l)|OzU5Xm3CzvNrvMk zesVx|-%9nauTiHqk;x3sQa%&&g;g8>V0roXMu|TqC1t#bO#J56H&)`wc05Bv!>iq# z4nIUj4nrW$xGYDKQM5)<-cJrhNgwf(djX1`tTrzkz`EI2I%bjxSXv*mI60$tM#Ay!gJPjqtW`hyIMRK1T#9m^Pe|?S7viP)I%4y=f{af$xVH+i$?{G{xVq%&s1m^P4cV74SL!5?vU-vlAFD`A`ik+naFNs>0IPB}qNKH?cdxQQD zh$hWW^Wwq@=L@97bo0gyrte=xX13X%NC26-;X5-jVIU0240j5?aZZ1_Kv?>YEI{@8 zAahmRJzsD3c$k|%1&F8|SlL2v?R+Ur{6Cq@H?aA66uF(x*hnfe)-f<%r)MWQv(9LM z9Q0k_HL3+*^Q)i`e&1_L_H}LeF(hfFGvCc@Dl9P)Td&C*jqu|#9X1BtnWVBUN>IPe zXddAD$?EhlazddI@auXv(Jp5ZWBe84;l^U}M;LG&O8A7gSg}e1Aet-G*n+_Q0zsIR zY&T5;%@j1U*fx@=|Ks=*XW2?r^ay^I*p?!o0EW=M;w;P4$v9g6{8yi1f zh<<;=n?k~n8oR)&lGj@#^n=;j1$sV*x!A=NKx&o4o>A!3*&<6U)+6^&yV?_nNXBRW z^M=Cf@{|IQsD?@^5TR{FIUaHOg&G^2*8Isp^^A_1zxm)3c0 zh`8QsRbUI$vuDqxN}(xvyfA+A)8FVTgVd5wadIy=)gWa57r{>vZ+`mmVr7LS zE_m-_FJh2{w*i0`a7mRt0-RaWyY%r--hWP4=G$JJ0D_#|g{#E|>&k9Q9}QCkO^77q z?r82aG{|YK_o$l`@aqFgrdLlZlh0r>8_6wWK&{%!apg4&4?g=F^nu_+4eH6_zy}X! zP9xG^Um3oOy*$S-$D{<^6elebu$bNrT*xL<2g|KACA>N{LlFZ}d~FSzql0Afm)phF zXk^B+Bi6pq>w}|^b6T#C%m&xrM^bU-=w;f$m>qCiSBX1skK|AkcE3|?cGNQyP0|EJ z?TAT=QupIyObybsyNkve_$o`F{<^L8_nObeJd4wnj8zvGmyG1Xfv~Q#RWn~Ki@p|E z@hM(Prl6BFIr!b(6J$PP4gmfK=l?Fa0F;|X5txPLo@-12CEo$-qI7VUpp+RX;yrNt z!gsWm#$e9rmVRx_3?TjeU-}}#^x8G4T-rzw| zF8sPJ;7U8tziF|gl!`n|EXlzj*i#%FDxm@|iSPodj#$#TFJY)=*=P7g?g|7yrKKDn zS6juHvgK%>B_oj480iA3#DcAsT2RX00ajt$Nk6w8z}~ViVut&?*rxW+s{3QHJT!}g z;?>|EmdW?OiaN`+?dna76CBo^DGin|@s@8300eV}gNYSg{~BqekWC|UV)frhHGIzBtpa$n=WpMJ z(k%t`Rj$=z*}lRepf+MyvdqY0!=;xeGK(saOENmEJy7u>C!-RJ<5mZ`_4ShQ;Jo)r zR#|2|FWK=EU2yx8tl{?CX3@dnz_@`m~GlE)Jts7({2pYaOP z>P1f8Y5zU6Ef!fi7|JP-L&#!~0O9~jpZd-=^Zzia1w~1buh2LU$kH}u2jU=mN5DkT zJFu`+ZNFks1*(v(^kEE@&vhWDG;PBRhu3bQMK~3bM4|&gN!(>Io)jj9%&DzM@L3GlMilG9HzUmoyEE( zxxYV9o5*rnS0>$6+jQ}AR%ndwmw|C;PL4syjIyTJC6F@2FTikS?Jywiz9i4kYbU&Q z5|3J+pcNg#;Sn!ZvzmvcN~&G0Lx2mCCRN4#a!_r^gTtRPRnS|)RhHM=EIO{g;C&OC z*|d+Gq5=O3EeNZ?TO~Y^F=rqhjPYip_h^2Js#UkXD@bU`H?3AUtImcGNg<=~5ro5m zg4gpp*N%ZH6uC(T?X}T)Zg2KL>fOnSmY`Yu{>*#62VI~BBW*DRkpS5lWV4f<0%Yei zxaAIHUV3PDE;8V11|U<7kJeUh|EJ4aWk{)(sLoZ(dG`wsJLQCbd&--%*@10}pJ1kx zcYvd&s9C?6b`(Yej{{#6>>|m4SYpszH`&xfGzpz~&M~aY$!F5kXPYgQ&J8eX%! zz!)VHnl`v8(5F#b>#6q@&bvZCyX=l6U=FtEZgRzWO2#d9`q(a|)Y{KXrwC{{oP)14 z${kg0TZyQPLY+euSyvkTfwAhNf@7O>Ap(fcBi9gTshDb^gd9}$rxhVMsa` zqr35Jw)Qr+%m7CII`HUK%9=xdX6ZS>iR8;TgIx@&HOg`{pMz1Crr7qr#Y3!I1iWuQ z-fu~X`b+OvH8ICgIer{(#htrT49iW*{d_)KQben0_h+F{^SQ?XPQq5ra#>OLENSWs z1DMVP_)F%ICIpF08*N9UEhH6tqnftIpIi)DpsT%>uMWoQ2-^9~O_MwTe5SMY(KD~Y zOw5T9mHzVChrnvyvIn1P>vvAx1aX<{B8S%bo>ooWJ&QK#8m-)A-y}$c%iTFRoMW$2 zUFx2ONxJ$bWY&qufZm~ET!`gdKRS|~7|4I zOD8Jy;vj0AL+3M1R%)<&mh z?R!T4V5^)K4?HIPCa$+8#m#EbZy>doj>XyU2?z;k)O3qf*dEpDvV(ndY$mfH(~K4L z2CE+KH=LWj)KfvZ!9B=E;04PbA}TRnyx;5uCMqEDQk=%ZH~`78e#YTVO<#D#?*T{> zqp$pI=l9Scwtrn}UE7pFOYkj1H0v!u_S$%%INffnJaN^7*xE&~M7ZSKK8>E=#)2Xe z^Vo|5vX)`|)2_2j=8d{pJFS#_Plw`MR3ppFw7}mETa-&NQ3#q8_7azjwMx#cgX3v) zM`+-{^$TTu;J#S3_XJ(RQ_MYBmt8#*f*#oA`F6lCq1 zTH%E2%c!;m=r7?yy2yXCaLf+8zYF=;%^)EkiEqC&VzX>5E0~K+fy;!yQg3@vVo^9( z;7mi^=o~yynpG-3cRp$B$;C*ZVS00wF3~!A4XnL>X>1O`?0L zTzw|x*|x&tAk!?-vrvM3{rfrM3Tf~+erA;r+9>(MqbLAU@)J{(rp88eK)~}BRl8KO za9lZjv{!+|#Kx)f4&(1Qpoh9tj<9QoeUv!5Bn4sm+D6s}Qb#dtO4CS?@XWeu=4L5e zoCNAu=!8$PeT&UGk)1IM3koL`=EHugT|@(DFB`*N4HCh&DGx93;U)u2E))?{0_ZC6 zUslokvI^@#Xfc5azyVLv*UDZ?@YkCr7H|C0p#EQEj?{EHo43EahDT(l}jJn$%b-at_E91)1fFd_{r6&+T zD+2q(A5s)~pyouNtVRXev_RL5univQGJuQH7OG#!h8@I!{vF-%e_2Xj1c^!7>C|}4 zSARRX6W_6ap(U8v5c%sTbfTj5Ms=3~SVJVY43V&q-RWYmT4g0+ry`nV(Ia-nsyTiIrS2_= z$Tq_6i1idLGif>BnvUL`4iUPXsJx}Z0}5gE1SXmbZ&T2zkzTXkQ)mL-YgWH07sU!a zGPCdsU@x!9b!Lu&LW1<1a*qQFEJzCIAp64D^qN&OPb(Hzq>*XuBKL0LEw>i)(G17G z6gG^dLzSH|Bo;06Gc}cf0d}?f>x&ZAt+7nK+^KLtx(*0NJ4D(`bRJ7SeczheRUnMB z7MFFgT0zEO(liH~Kbo_ESg&e2wzeaHC%JP(!uSq&lK2eOn9zp%H)wb4Q6EU?D^Cz} zpot^;0oMIsj3pfezaqC&ElD=c9w_o3s?Om@dwYFzgKH1}2Ozt}K1O+5c8-aK)apX@ zhnQ@-0n}f6)DtTgN8xXsOnG$ddo_aO0nz4}0kL*gaO4^v2qwXok>#+!{&4j*KCGC_ zCr)tB$6i;mi(MrOPHeq2E`ov@pP0;SX)4^(jV~6bc5fAvl|gO~0qyVk4eK&1k82)O z3T%jL?l4eNUWZ^&f8Olz_&4^cH5_`FdMuZjE!7foY|(z?T;FeWDqboULk|m(|8%rAojXmnl z5N%|hC9zYkF3q>_%fom<&ZH@SpU*i4?^Bwnj?;wa%YDSayGH9i4F%FU7Pt~1g5ot8 zKoLH5f9r>Nys@=ITyZ-CP9I1I&`d_%ob3!IDY5K(-af2u2*6U^uzE*?+LZgzg)^am z@WPkA+8XA|VQ=oFa`;E~%wP3Gc;%9dF9}2cy(%qB+&iGeegRlCSonqA4b3~K_kMj` zG0=if1eX5Oehu0v6!0otI|WtfGTmSi!@z$X zt{LJf-QCW4f$3dqT!U&Vi&34WJu@HHfOQG9sOYMhHu&hQ_U!9&Krjg8HR{lGcJrF& zQgxzj9*mRrA4n2KL_>h$`Rw^>On|@iE_`ck4IPDw3Km^_4j)0BhGFPS2LwJ;Ehc;x z9Frg+pc5*>seHjumfNp`5o?%#MT&np6V3ph)j&ilN2?Aa%EcCW!(~c^VjYU|q20qE z_5E;KtoH8~vy=PjukH8E*G>j4NuECKp8aDNC(U4m=#oW8r}2~1E2D)~47~5fjc31I ztREFqChVP42O4eDl8SpjSQuVy%Zv_PXH2M_oq+bgpF9f(wpaR~X^%fFD$Skxevstr z%%39(mCSlJ8p|6Z_{maL-ZKFU|1ft?Xj*(dmSmsad_pip5ji<$ZDdzXdJ)nn_ac3UwJHH|bc|*~}y|cL2`)^h7{(9>!?7Pa* z>qsO6lHrB;%M0aoWq8*B+T}o4UT1KH9LNvqP+bL>f7x7 z>ZE?jD(HbPD3t}`lYF$_xB8wy&N`qqnD$iX0JgP#{sO&JyU2?u0mi(@uX_AVDqL$} zvrA^aZPTFG^+e^zo#}dZSuoLB>Y;$bOS9g@$mTiID8xi%V{{mRzIe1eQ%8%Qnr63s zALAZhe^fC$uj?% zNW)^7yxKZ3hz2dK-rgRU{Vd)`D=F!S$2(MiK>L!3TzAd^EPY%WUqxOvSJ6L1|KQT* zu`V)UTG*Ln)?rs~a+E2kRei{XpOhGY`oUSY;4c;Hmff4Yhx#KVy@GOUFU#7i%kL)= zGL6?^MY|6&S9}KgVcZqEWo=iBng)!)F^x ztre1YKKL)tcKq#LYTLUpvVE59YI>Em?2z|!aqSiM9%Bs2Ba#x22>hDIf$Qpd*B=#% zS8S7>!3Ss@rkDzcUu_OJ8hrRtCRa@A7tb~y8k60Z8>io8o@U+NT1?F3%s<~|CMA=E zU}>g~e4N%@3qeKRG9T@WZVlEKHd9IsMVv0ob@6H`m9_UeV=L&`;6u_qmaqzFHtrNq z&RhN`=v=z{gou`=*gA2~39($d`6~;XFD45jWnk?af>{4@JW9%z9GEHE$w9eJSE@Y+ zIpzp)JLeWO7P-Zc54pVOZ&h@Qo!+Y5cuklk5mwRTm`E2s_{ez?~JTJ{^i%8G#3!|m(RU}FBkze$UWL_ao!V_I9PQKK2JH;Sq`+RMbF#`33G_4r-j z2y_agD2gIepO#bWRW!6q(h*cV_fu7Lte9Ne9~0-dbNk#%c+aKBK0e$}D?K_Fsq6H6IBBy(^c=`QCX_psR+0%&RoHx!=t$87@l7jka^Lf1TTuFr39`G z@{v>lnZ~}I`w4=oekZ5a%Jh=ODJoh--zK-}O{EXQpJLf-uE(lC^fP-Frn@}6IovhZ zT5fcPHb4wU9%nf~wr+4(I~TOq9K1_r->hYFlpN!-TP^f9yyxO`HAFD_ti2;+v(QA5 z&~NRf5%$Hu#x2-WVNkW$43#5FlB}Y#j~?WC46b6mc|_*aICdVww2gIGQ08944Cy@m$Qo!G?TIHgw_%1BeKI zq}Nn}N8=AQwQQG9q#iD(Nc|Hz`d>=4?ZHQL4b~S?vi@|*A-u{;#7r>iYML|!wzzi} z(*2i#uNdh)Z$49YAQD^qqNFS9F1Np2Y{>8?zQgom2_9It^YmjK6YG8s`6k(ZuF&Cm zhr7sYY-wMq0q+5dYtf$1nD~L zmRa6e;`4BH;LX)N$1|JcW;cJBCz#B3b%lNak@yMT433Tt_}O@%D#TJJ@G6`%olh{H z6&@%?-2x7Y9!Lr6n5e3P(G*5-ksCYHPU&A^3h$|<73Di1nwR2h7mDmMMa2zpSttCF z-?<=GS8N8E$H_SaM63gr5{Ww;{tVdb0rL2iM-7PdGyGjKU32NmQm`EMoe0K10J~U& zGPgn?k7W=StX&%q5#(QRxLs~lK7!NlAHof&fbmpoE_ABro&z1Pzn8whPJa&N8;Guo zZ_m80mTJi_sQwf?8P7Uw4)+FcJs`5r&xLRt1u}*?;qIwe%!M?vnIeLYw+^JUJ*S7C zb-sB+uz=O9Xl zZ`JY*g(wK-wJG{0a@5RvkPgjHB%EOsh$v~N)q7YpT;Quki#(EhfDHW+J3wuAS#9SrRwys> z4k3GKk%y{{fWyO0Qu37ZQ*3;GQmW{&uFEDTDpq3NnIWUq(HN4BF6=Qmdx*4Q{b=X3W7m5MB)v-l1#oSd@=A0R;LmdRt zy@l;AUU6WbXLpdXoM%6-xWAl7v$>^nP*T12+vUQBBnUxR_ z0)qz)E?)$wc@6S(uGjyi6(Up7Az3|@6}o+527O$n-wgO6uI#kFosaMq5+-E~7Qo#+ z4vnbEOIZEQsh`%TlI7BcYoob-i4F^dsC2~KRsgRa45yTOxC zG7MfQGO&%}bXvZl0#3GDOmYnTPAN6*%Pf7On~EalMAHl^$o}=V+>QJ^!zzaIWF)nl z0g-X7-A(NNlu*}|HbnBx09+Tu&~@nepX>I#2fp&xju-S|PosT50YrNeK9AhMWM_K% zvyd1`g)&UiJ*00F#jTNY~l;Jn#5k z-|zfCf6f>;gF&r(&AF~Q=QXdn2vvF`g@O7E6#xJX8EJ790Dyx404N$65%wLUUK9z~ zCy=v>lqgU>Lb40{0>wdE%NYO!0v|s>lcg0d06+oAh>NIt=4 z&=)A{_LB2K%l6WZw_xT)uKP%}Zzjhnf7_{F#)AO}4<#D--=_-%qQ&g#qm2E(ei9AP z`(NSyzx)LP2z8}C*$gulUfTbE9&qyE|Gkc|E_N0W8E&~nA@je#4-b0<=KueSe?9R3 zv7`3r{lfw7?jjTv&^HVeb#+-Vjqv|@^igI!vFx^6gEkVr#Q|^NkA#2=aQy(p!rJ<( z*vj()xCBAS5eJ?P|3HTX0V8k@BoK8#3&12UgGjaV0_E@ytiYGx7Qh#=a_U24i&_;r zL=zwf`p}H{N>l!V(OSB2)-@e&^V%Eo83}+3bj0Al4|rLL{+vg1VDRSxYR+RRjbohHksy|E&mMy<_=wRz3fO>GZmXbr0!l`u z(774ImaVdNoYo@!D-^^r0)&5m_EnMtadL6XzOY&}XRoNUaV)^vqww$k#fJ)+kIWra zysDBPfR93q-Z0NmQ|O9BISML7U*Q030TTG54|GHih)3kTqhw%*RP_S>HUbF$N87&R z4>&0hE|qsU{&p#*Gt$1fp0d^^xJ*sRcTt}S-wEPUfd2bwg9nduT@TD5WU+`Jasrhj z_V%~tAxu8lmY~~4GZ~+8%J-RP99&edpIh_>Vexcrdnu4iIUqOo%#EI@^~pK5tS2A8 ze{en+a*PzaWJPqS>f~+25Ps?ZnQS1;RI{t`YaGxBTy_W(lY{H$g#~>Qmw}qGE)RbR zA2YV>JE!jxrQ;t!0$!H;jT+Qz*OG8|Se)TCO$*1$A~PsI zlfY&ESO1emPEg=;>=&v+k8QKv5PpQXS)#4*h0pc|35LW2iMe%7ds-wK{Q^2TF|Dy7 zZyntx$9wNq;P>t9%8KhSu?M6_|rMVfd9N=dSAa8T^k6#4M(#ed-|pq>W1u+=e`#!Cv_t(sKX$gUF9 zo;-!LN(iCy>Yl7!m?elB-6jsXnn(869eQoG_ORgi^p;|*UTOT@H=L3*=RAzW$7 z>|f_bk&gnOYrhcCC!^@-PfMxdR|6{hXCrHSzbtK4HkXsc*xSpiFUpJWP_&P)nK5g1 zW!BK_Uyrd>y(D+wD`$T3v70AJuwP(f9wW?3gU4N&RVc}zTnXi8J3u!1Ea~VCu31>< zUx`H;!Ud|nmV9G4usc&hir8jt3(seECc=&2ATj(yQzdnVckZFiE8`5T*;TOcg0NS8 zDVD^KQ1M5(OSv1@taKaAR;6Huz5Q@|LP_b-nakH%gGJ;0gq!hed|42)`uU``$%92? z)%i7)CNu7@_#wfo6kQJCGI#V~y6`QqCN!)9lZUcTCr=`+;zPuMuhw`B#&RPA!Bj1W z$R;io>Qx>|gls>TV!20nH;*J)9vW5j*jA(XCn?>X&&WzQFTXRrshq6}O|o{(qGgDe z%F?^TKb$bWOZ**17&p5|q?z%T*~SU}T0YdDIA0Vt6quwC-6KOjAa{hz_+Ybwn78{j zN9-MK!L*SFVwB19#~ICPr$hovMnYHe zDTfm`BilrUJWvi;KE%#5agAegw>yh0DD~25;#s?Y;}l49-Q&2eo~C*SEigtgahYHl zC2dS@xxYRu&i&l$`emcf)yFRgoj=KBrpe{%*7MtIrMzWk*6IE72;65@Y0D}@|5dnP zQFtr`DQ&gP;>miI(@Qf^ywIOe__DR!Oku-?80^ph6FFhh|nP)6TvK*LC?6O+agHZENi z&=6nY`fK{o20;bOnx4L=Fa8XIB|`-bxw3=MT=t&dT5%zH`DC*2iw4X2PkUqUzpF*wbMUduay~HdRd4VNdacy`RBNLoiaqwpfqk!|jpG2VyHjUO*BrA!wyq5GIBrtJ z|AgTRXMoZ#;wL5z>I`0zrCkTR48zidkyjx+KG>L`TQGp4upz77RUb<|AV7K%LVHkN zL9?c?5ElOGr%o~0FmB%Q<9A>qxmM`7^df@pY!oy$zL83w9WQC^y0BhJAAV8iKzlsG zLQVin7@SROuY*glY=Zxah&_1<{Og{14#ry`ci@<)oZ6Qy0+M_Qr_P5$K zo6~p2kPtX5Cppn9U4uJIrcy3A8=gZMLcLF@-tlrsr5$>PMw;COTVrGB^C37 z7V7MTa7U?Tlyz%jB3U+V4tPgSqrwMoIsU4r07@DiHmc8hcv({?OFwbQG8M-uq4PBb z30M<;(a&X~JBJ}rfhjH!tx>M&2Fe=PeLX*hlj;633UTwY20q)ZZYhjw_X#Q3x5y3A z!1&2>7K#Z@$@O}NiqG@9^`(HwV_KH`1G0Vkvjbr!_Adr&M9VoP)bYnD3T_GAe`Avh zz9ZXy0qH!_?7j2d`*3Pwxia<$!L8pgG*$+l#@v*abLxhh5`!j*IgZ?RlF; zg?<&ZQvs5eYf~EP4M|K>%d&Q^WYfNcHha1_yST_YTN}K9RyMFvFoLxp_(pSck{2vy z6!+g`k_HE4hq3D!dZIdLXF$o;*`J<&+yNyyVfR~PBFi0tgC3#(|A(q$^@En ze6*L8W2uSZ?qDoRz;V1OTlXy`S-w|w7HC~48!1YWdi`-2e}Hc4UDeBQsB;4xKV7mc zGyVF#sm#_Ja;kw3bpI0(;;#Z6pu|r~gMsEe>SZ|pYEQIEs zJhIko;u%TY+U>1uYnpPR%nd&jBOOFE;I(Ff;5@BJ3pshChF%hxs@=wqd(UJm$uU2O zw;TWEUqf$j%orCdJ(Tp5AW96)t~Rj7&GoN)k0^od9kWdVmOy3t=d!;Nn;@+AKaJbV z43$3`6C_rjdxbkyP8l3mrhyF0*Jo|2fBdrl6roc%UwLwQk$BS5GfAmDVz0OKmpHY}o;=geGyG z?_;5+zHaDSX7|ud=;sBFNSD|#MRzE#2-{dp22QQwe^n7fE^y2bu8vi2Km$Aj5*F?J z#QRUMx<$n$6a}H7p1l(&3u2FT+ZYVeea4kagC2{l&U3?NS+G?v%P)o$%ZgAbTumcK zvi>muAJa&`O45CDQV0#AKxq@D_IJ6i!3l|P#d*VuH6aG-QyWk!NdAemiVZm+{FzDG zo|_u%mCva-An@I-^e9?g^=8c}RDJu!{$)VK^Jp3>ZBdf`Dw|_&7WahDbSTdW24*0} zF@%TH<($&tWi~Z_LsI``lQ6jI4FwDpE+H0JJUm_?e_JDf^q)jf#Tah5RS;3jwB={y0KDX z5~&I3E^6qRMkz%8FK7sZ3sA&Gm!+F>1b8hG8DFRdQUBve!UE+RmxD&roPkf5Glv40 z+X}cfS6{c%wbp=C8CJz%mEf*B++#it2x_W@{@mB*g$IHFJDPryMbL^Ki}<($R#4Z= z+fuR80S!t=U0VG-KX#P{)+o(-iR31JxA5&fM&KM#6RY}fUo$^aqY2l&LsKj^-pxXg zCaVwixcLTZo&~Bw=rsf_-2vIq(hCRR`LriXHK9(FY|;N?=valnxP{1RN&f)*29;m} zA7UJg%v_{p(@SL2tMj_$GGTpSvE~}CNWHYy>3tTue<)&CMaUQ~z-T#TecZ)-9A5&S zIh?ADaL%KSP$69$?G|OZo6uRhJFClHzdwAYN5_-D*keE``24?nvjA=yL&@cZ{2<|k zwo^!&ipLG<;<-pr#3xR^T|& z*^mL6!us#Dj{BG&5~m`;6F*9`PY5LRGYn%%ETgFrjjzf|eJnj?@%CDoM>FPd+ZoyF zn;~X#U7S;Bsv17Vy9uxPX;HW~q1LgjLrg%%m@kDCY@aS7%XVneT9at9c1 zJ?J`HY?g&A=n=Hv6eV(DXEVAxZ$f1;_A&R>w5gYQnG4B5$u?Ay6=~Z^xkF}gn$F#* z2fKMfQhbffyPsLjl5oh7qGVw(>Kn((gLx_ur%!^ia}iE>xK;96h>I zYtp_XQH%|OFR9j+HYR~NE6l8}HHML*uY%M`;}~v28?#*{wVELkm*MW*jw!TN?Onx8 zQ`UYm2hRrCzAOkNMxksI*XyiQy!jt6YSO{Lh@4dYN=WAHJ2Z9;qH(@a!U?BAAt~TK zGJ&MO)a;<`pNBSTy%e_DM*ef?s_Iq_r>N>|`haa2T502Fk%i||W3n5oc0k5&mxW&a zmk1}^j2A-aBj%IUQUvP^jqeiJp5a=nm$sdJ7i)E4*OC)1h2U~&rWx+PRQk?MXpL?t z037GAxML*$k0AK|pJlKqLCnLl$X^dhd!1Kay)QR#(vUt zb+h%wU)LjvsBg{n`epEQXs53*0jP3rm3t4|ySN+>>5XiU&HNYiqJ<&wd&TyJSPS_t zRlu5!$2nWs9-&)7JX|Z&5eUEGH#8fVNMbczKa=$w-YleI78pd~KL4Lp#^Cqz zVXN@Vo6;aux=3~W=0{mE=}lj0dd1cw(&p%>t;GL2T&#)}6Kj^5b97YlKus=UGJsj_(fC&|Ew+me#J58Xi_k;$udr43eOX!)LJW^3<>ed< z{m|_u(!x;MY(7dHP6SrGfA2A9_|%O05wuOX(b#qm&bkFSqZj3x2LZp;)=HQ2uHHmD zc$mE<9WMAV!35pYA8zZ=t|qYFG;Ln1R4T_#n4XJndsurg%G>(co|Au45_9Bl0(zg- zol@86JN2htRf$vlfO#LX1@qH1JcJv#A;}1_pIp1?`N0;T*P|keR_}ucQ~`eGNs(3L zkL3oN2BakGx3x zRpo1<)1@2HiYE0>&eha{Q0uF@Z-j$-fk*T#Ln}CVCXQ^3#@@x}*=mBEnzUOidsHP< zk0$Hjz@UofehtASG@MRNffrtT+mE=`R_o(e&tQCWsy0j zh?jJUaL@Qth;<~Mpc-(yT8UV|J$E&10f%^!Fg1tI)Fl2iI!@}}QoDpA{;@Tb&&TjN zXwS=a#EttM@4)icXl#o4XhYLHn1666jvBXG^hqN5tt)-1vIM_|8sIn|Fld5qv$uLX z47t)~#gl8tawspEp1)Z18w=b&1hj9kN4G(mzqreFCs&t^>fG?XtJ@I49;}HUJOOR1 zsRI#Eys0|OBe)?NV7H}@^9}Zto(3>cSL~(IzgjLf2YlD{71*C4GPk}6QHqvO6~|79 z=ip{Qd7~T?&CIs$TRZX!*&L2d(J!{)wh!+WmO;YsbzPtK5V8`2ePz+z8VWYqd@!=g zw&+zNC+>a~2)hG#-}%u1-Svt9Aak%j(fa=RJ63(vO8)0d*9VyO3 z6B4RfAH4y4kO^QT3#Ay-mVbzlIv%!^yi>CV{E|p;rS z7wQx{$w_}0KZOcxtU)c2vH#CZ1zuEGyh2l$*qSVpwkLZwxy>n}?h6Wtd8wd#HJ(KA zbxPMwtDtI}xKQCn?h$AD&Rus|TkG5en&GiLX;@R^W{t_RQ>NEvw}p9W60h9S&X78Z>M;cMrE42SVyE z=twp*9Hdxsi#-Eg;}-8$j&zQfE{O_^IS$=^7DnY@`!~x4rUD2K^`X@Q ze_RIqnS>W&>7#0)JcpdP-zrkT=jXe(80h4qgbvE1F| zG=!_HzRz&bs+mTb0-NLb28=B+C9F1fMy<+>8w^;`FFxf^PRab6>(ya{qS_Bs(0@;j zHh>ayTWwbxj;eNpwUVHJQ*TyHg91Dwe?ukP9gs-l2UET-BF^?hkH`*k;nrUb*1U`H zZj4IVEe-g#b~m@Fd_%xOR~=wHB>M0At1y?5_-@(6{Ba?K8`NOlxVUQCP_MpW%aT}e z{H%F9A`o~3P=L8*pi{cRCKZ^1h}8Ot-8!x;dKvt~SMeSRZi;I(a;06dT}Cs0pj&GM zW^&h^bqaDo2@Vp2UDyBX-v*n49*hqWF*2e2$8YR7_2dvuJ)OE=#tSL?qBQVF4UzSb zCKu8}IK!IE0O`W7JY*;DSGet%H}x)pb%5lGAn#ag2`+Aa;cyBJSErzSr|)W{_wt*(0wY*45nU7S*h#n`ej|1cAtJnHoBmZ>Ke4}jqIl4??&YYO|05YjyQIa zCgwudLR0=D{@6(PgJ;x#wvYTnHFl8e`TXZfoonAA6gt4be$H%y-G0gC-zY+&Oc<O(_}-+;q`%|h;nBt&5G}p z)iBc1;(Fn82s%=ZHjzP~$jFdTg2r+8;-Xa{Z#X=Mi9?hzx-zAc<@Lj>2WazuN+;}` zDqwMo(M;mpSTG@yF~>>CmJPz4X!KXXQyLBW#qUJt$e_2%+=bJRL*IuOzv$nH;5m@# ziQ?HtDRPz9`E}?sVu}eNF1!3nke0xvG<}=|d`62S^8=lB*N+@4vnEgJ#M~CD`%+n$ z=GzV2=4(|BBYW~--Zv9kNyFRcAQ7#TNcDR2o}_(4jSui@1;&(<>>FXav#_#RC_Jc~ zd`e~e+K5GQ3h}p(xk&oLLSTMa@oQ;eQ9U&Y1$w6s(gI~d_zHf7Z=^z`09s;3Hzw5D z4a!4BHDzBp`Jo$AkG9$gayvGBE^jj5&aGu9=d%Uf&1-l9TTl(WQg@Dl`0;H64I363 zZg{ctyh2*laE_OjLK^N0V$WRYHe!!<2H^zV2jw)rhFDGcZYL`>)B?a)2CvK;-~?6j zg^0w1qd{4u{!g{vFBbvNybI+b$>pTKI`DK{lR#J+eKns}@F1%)uSEgVSbl+E?~yZV zcKsnAx&T2jLN$$e-3GxQ>H)?@_e$BUjut5h#VOqG6aEi9E%#Z#)KVLp9aQu6u6PWY zw3148aaqSncoBVgOo7=(nyXa@M&>Z4RAE>pdidwSD1eK7>f6@8+a<@?Y%OKNm~JN4 zQTkpT=76&D3Gf|1*w^0<8BQhW0vIjsT8JPT+9A|ezt{?*Hmo|fyWt^43?stjoa95f5Y8uPhvq#PrD?(f4_3m;SS#fc`sB%ZHkg97{ z4*gS}?pV5rgP$*1&!iU6Rw|m@?J`o6rsdSq(cHg!6o!4k!o$Pu(dDq5`{r?#x6M6_ z2lFt$n9n`AZt;Ka0$?maGzWivnCtQ?YTJHRIQ`B~4<rt;y9creI3|Ou%r@ z8>d-AFRNDcAvR$|A{2E~xmjKdpG6Hs-SmJU*f1o?E5rHree^s> z)xExz(cU|IF^}QSOY!8NiXzc*WWS7cg@$lo)@n}>!LP{VI+ZLO-lVU2w7#+8ap@5rX^bzC`e+!Jzu z_x={eoq46e`0`X0{pp#slkQGbJst!NmMnO_gp0_wn{}-7DK~n##`Cpd4osGRR#6*0 zy;&macktN8ylt_>yT7-l#8NgI27qQ^I{ z=Zc*Gzl~8-b}fMAK%(sX_H>4siks-gr2(*PVM0iz2HHDqhqz8_W=b4vFPTwtDXKNp zd3RAxY>vE)Q$TUJW9NDSv;H`|?uXCvRe5BJvx=ZnqoA3WxKiF4PoY{KbFl_PxbvXS z>2nwNb0eF1S0L4$6jJWv6w+sYtt!LFYvIvG_+p=%sT}_3cH&Kxn3{od3yCYMaw|V0 zmsNx;1~G40bGE!O=~9y$%Xz8RXOr1kvQ(9K)nDlqYX_5A4W_ z+0gT_K-^N(Bi5B2Fjjluk;JpY&o7bYCwOd<`4WGj%A z45mlm{R~Fo_TCo4S8CdJqn$dj*c~4OH=6^j+w&XL{@jhR?x_>`Z*k$8O%FLTdaKf zv&VK;^ScpJ?O}{*kC;k)w`ak`eJ&zhZ`yr`Cx~C@Ro-*eW@!H}rrtDb-C6J^bPD4^ zXFI!lPw1g5k-W82h%8bnvlcPey(wa-s&EzeWc@MQrEe|$<_ezTUNGm4!=lbx-pI9Zpn6WqFVjZ`0M&LOgcrp z8L+{jfWWM9`D2CGE>bY-UJ*UYp{UyEn6}eUgJl1qz>EE;XM5iDiqwt~_dT z3X4;wz2AAICjTIR!Q#K2myiB2?EZjFSmk)FFfJ{EQ6c524_8Nu{qz9AOmad{+B#VT zBI=S|X3Uk6CSO;NjhsI*k@H(v$w|k1N6L^_EmLk zH1^815hl|#mh-J~gUj3)^Rb5Gr+a$!8su^-6|~R4e&ycG26v?hxv$?Oq#hskzs=x1 zI}iE&E*Bc*gSeUZ`~`!*-@$Bk-#wi7bMKQQ-6Ca^iKZ=Wd4o?5TY;aMd0S+pQTcK2%qfX0&P#I!S zbvuy9h|{J6x3htPDU~q_)GvrV)q;O!DU@{6MT$P9qxHN{LNlDJm;a;b$NpN7uz;RO z)qNO2a(_O2y>D_V(VufN-pw%aWmWmzx7we<&u^gFgsc6UkGy*{_AupKp;s}jBbLC= zXfeOV+)YJdt3Nic@$^e@sHh-k^8&a4yCo^m*sYA)Lzmos0u>dBCZ^tG%D zzr_Qn@@db@Cd?fN`~BK06{!l5P4_xj>dsO3uI?w&?tZSeUspUpcwCazWuvB6 zF*cg);eJ2*nM;dsgnpr|23K(RLW^Vch_H-)Ac`^4^(!*wMZ`>j0L;)~1lajq1{KwK z2-@XY%LUG%hR-GPRXLeYCwwT@1+l}Cy#2TkS#N0M zIP(V+Ngz}3y!p`Deb9b3<@;FHTl0Bx&+`kF!I+)pe8tWHmQ*xzwfJrrPMbgVX5ZmN z3~xWC4~;*E`gRwI#Mm*uyo~RAeWO`w(wXAHj{1b!1OlW6U`&h_=U@qOXM)Msxq3LW zXs=e8jkO|0!nThOrb?E;zdUa0BuEIpWkiWyYyUXpw z0#57i`2$;$Kd_ax+^*U-ks28zpJYsGKhrR1%I-Q}0 zz{)Vs=ASa0Ksfm8wH|#|45nO>2lwkZSvZdo9OE6MI$0W*u`n$Cwo{z~ZBG}AS&GCBq%Gzv z4C1;bQW}*Q9%ErA)n8G`N}n>C1^Bw{HMHhU*vh`VoQ*@yci)*wwQ8eYL+oLr%6$5# z_(tS)-#7}DwS4C-PzrEcmD}&DZkxfQTHA20TgY|gbBkNvP#&ftwjf_PmE&{5(r6aA zky``g1Gid$cjyhkMr!y$1k!~&xjc=L%YhR z&K(Iz@(^-;s5JbFxrM^-l0tG$dsl1oUR{x!({}`_YNgjU9Z!3+2@qO{o^#VZY{6_)dFJ2bTq z+REXSbgeliD1AD(YqC;ey}NLyHPYM~A|1lJH3Tu4ry~W4%QVHmYi6nKU)FJNK0_5Q zZB%?(D1Pwr5T3#Z7i)&90}kckT^5QN3c}+~3!NTm#he`9`mHF#SD}p==YrRtO?pnC zBEE*#9h2rH!Rp!PVYS@t$}Ilo1^JvZDpjJBp2!5j8VZN?Our6quWvdj+&}m|eQ>1g z)kMwOqPLiOLH(BeX52vSJy|zA>>#*2VPUiWWa|g+TlHoFIo#2{kDUv?&79Axn7YH5 z)VHuO&>N$~0tc&HWGeXhjf5UG`5FJmppJX2uWc5ri{=jMae-6=sD)Ook+{JcMbmot zXYRgt!{G}kBbO)B9g$V|62p9XuZdr=^H}|sQbk+wEJ`PA;A?5z@N<_ct26gwOTTeG z#%y`NAQj|SN*_LNEpT_-v?=k&+ipzW_%5FbJysU0pcw4ktOflmy-<9-@rxgDJ zFKky6s4G`R>0}yeXKHs}B}~$mg$;NwZZp>4nZi!Qn<940>toDP4Lyk;OUJ^-AHD@I zE0;_aYRlwkzL`;^$@ry|nqoC#gZiU~1L5KPy^2(C`Y*N`>S-r~YjZz@%LTzd83XMx zV@O!<)?{lr4dg}NhxBl_nO%8@;@x(I{N5-WBUfhdf8wH86c-mR^jj*E>6KLkBG*+I zm62QVzRrE!7miBz;a|6y1IjyJ#V4%!7@Tu`yoWQiL>`F;e#0ZH70vU8BP!!5YLq_3 zCe1GIQmtOYwyxTm)INAZe$LTI1-x5rfH+>bQ}}Zkhe?v4e}V=#$oy$ZI1>+UM%-N| z7T)^pLyzL!%{rNFYvq7q8p1NJW&^Z&XjCCS#JMRyLIXe8QBtUwq znNq5fPkGrzK#!ZI>en-#YvvCZIK%V5bCne-Fp*#&Tlo4XV4ums*!454^go>Ep#C^E zx$OdKTfQN9bg4CXd?&+cKV9PDY9YaAu z(J?cvfAJX|GdzpnLh^9#>Y4}5KzX0NW! zGMh4?oHxLzoWd7O0)}!F?gB4Y&Zc$NpKVfFa!EN4))gl9=gT{eVp7dleE@wJ4}Zy& zJtQa7PqK}y?#n@D3~CqC?`+$7)NhP^HkmO`6F|kEBe*_zZjj{!J`42-3&I!*#e2?f z@&ldIY7QCWSumFy85cW_U(m4IiZ9Ul3Ws?qgqD>x&JoOI^`?7+TC}#^*xjl_Lr}2U zv=Y5~Hd>gooIDtltmA|{$>e5=l>7Lwjb5jm`PW?m*)uNNh`_+WN|mi^9NdN4S%Zcs zR$J*Q7kY&2T-k2tXs`Wl$^b)&JRL6kE;gZukUMX8v7oRM0>om#x}C zdN=nBJGEg$IB>DHWkN#?{7h!_{=BUROAVfH?3Pq~VtN+sM6McL;R-j0N}s>U5HEwy ztwV$m;x5|JM)`sq*mvtb`>l4nFH3B7Fsl}6;`j}x;>o`PCH3E*DwbyOS1C1o?Ft+$ z|H5gj1xcv#X9GJeC<=C?d%B?$MQ@W|8Zq`@5{I`mU94SEj^VFmtBV5ceYw)q!tkVAm{pS z7pSc^h{##*@J^rIk8sJ;1l!ku`W}5$e$r|}^3XRbdywTp%vdFsR&Ng0ikSIU=PT)j zq70bAWRJPpYwF_1Za4lyZSraA*q6uNkWOHToMXAxo9mZrbRiak<%)p&dQc4#oY#x{ znQsJaQKaGhr)}c;`We+Wld=gNbpClQ33xs0D}wP6Q?nkwsa?9{yYXN4Y-Tu~|PHq+|4c7CZN^cd=wH+eHs z-LgC1fEs<0P-$l{rpd(E_~pBy=Ru!RlH)^omWL(3r%c{bvY(}aXYd6Lf^4hWoLnmp z$4WWRYAR678Fh*Sf1XyS{}?LDT#Z5M7JyS~*}AIV9Jz6n2!E$4SjQMiCJ^zeMBQ-h zxk=Stuc%;i+ErY|e#nb;bIq3|$H+b_4grWN{;i!q<7|tBa-@I)H*I(|+s%~hxd+?N zDJdH5Pe#Xc->jy_C-0PQg)KNv9#terpo>B&4c zB<{_66S(zi^5;)`QX%~CBQP|TrEvU!nCNb1Gu&tQWnQ`vuE@e_apX*ut&}ezZ06U> zP@4KU>_Nq=ytzJJ;yYaZ7(>3ckM(Kwq=fB)*C#WAG-^82r)BK4HR?>_FxWfDWq`q7 zER1XIhG}}|$0uf8c!~aLat(b|ykX)=Tl$L9OgSxkw%|aw>0(TDC?@8@syDO_QZ&;vg zoiMfZ_;@H2&2;cBo>j!NBZK+fs6uRrGjG9fd1;3pVwe0MlO?%D2-#7$3h&rQvB(8J zg|VCp;Y)}Ea&3!ZRGSPtla$oXHtZ(GPcz{&z)FTE7a3}SN6rADcK^>2Vj{nF68;^G z_qXNZ20ZB&pmV8+(Z_ikgct0lFSOA$rnr0`2(_yp8S{Vc_wwl|)_Y2!!!xeB-c*w% zPSw{l^7xSUSPos*oExUy&5vSF&Q-HEpjdpL1Y>hYy)X7Up1+MiqHWmhNz|;`3%(7d z*J!2k)omI)>be}*A;XmknzMfAMf{IVr;YjY;IUzZ1=ik7ffitGr=Tk@X;Uby3&+Cp zHo{~KvT^?QWs}Z{TQ#l#=t`EspfB>1XzdV!`8PxdqcSd#V zqd$7BE1I$GBX8lKa50UM-J6+>MD%+UPTK~3D_e9Mf{t7Io$Y2ek$+Urena+UsmJY_ zS{uHSo49xl{J#I;K(eTK8{s9n(Xh^K>=yRuk60>Nf2_lWfX#KmA5q>KHw<6YI#H>7 zpLsungo7|Om^(&O&z_ME6DN<0Nt$#KBS@WDf-sLTjNKlGoa`cxyEg#C?gb6s8DWl; zkj?o}AykGYje3lS>{omZhgBrh-N)WwW#J?*;zF>CN5Z-gSY;-}ZEf>y`RDSc^a7QHUYJg(Jv1c>LvK_uF(+WR>324}Fr z8|ka(lm>qIGO}H8i{+?;S)Jq3oxf-)Fl%VEJmIx}5er;-rwuST)a21@PLeKFl$HGs zzC&>MHLQoHO51t%GQLO)Pr+*eC)V9$Hsuu&pD82Z18m~BOd51rg0L;R%nJ6{3_Rwp z&w~)_WaQ+#^(8CQOFKvl_n7C6S`jW9N|DE7W1b;OwzA&WUb~m-H6S8kk%*4xNGMe~ zNlX;#!A)ALx%Zf+o$TVfw0`H1O~BkMp+|72tUw+7QzJaVIFGRP!vqo7hFrr~2@W6wQ;8 z!hoUsqyMcn9g7n_VR$8LWWvQS(S`u6^ShVLw;N2CCX3`s_}x!CHTaz~Cej%l&h`dP zPvHdZBtbS?uZ*<*Kw0V$%6EP4hYO$2lMJH1Z4MgWMktDsJxou&MyPDKVDtZhGixBg z?Xp_^7>eT?9j9Z(vsWOOCJN~Fvika|Vvk`@JPNnh278L08v)%2epBym{EJmMbYgzw zn{#|OaW&|zmxt{P(#WlPzK^$iZnp%2WUENUXNc_s)K&{GT;nz<0gBq8Tdb5x3=3TK z>&|;H_p433!alC6skTW!nzkt`ZhTrT*5;*ldsb7 zp1I;IfxC(3Wt(o8w(GAO+n*Mny3w{*Tj%W@%-HnTY~D9HG0CRCowIOkyHbCsLem;N zQH&A`cZ;cgSrZ>;_-tXzX;BTz__gA#6rFz6apD;sq zWa7{WLJ>gk4S&*=UcVP@ug5oayouPYMVH>UZ_U|g@c2L&HD*=@xn2#cW=26~d&r0p ztqPcJ@Of@$;Yj_?;WgFim4_8SKE1C-mJPUTT}z8=icl9^5Wm~4{K8_3z*MPrB$G~UyQxO4(^9wI!_Bh-iOY?C zysNAE+9sS_p$xmHN899w;VA2pd{F4(1Ikw+oI-MqWxd%(D8(dZ&=nJvK3druCz|W0?vx~97(>uejGpHvf+7tkrd99C#BEQ~#3`01PUviw z8)1y}*Q}Y(AJ7e*>8R4Nh;*?obo(g(_% z71q4auoP?4!h)e^{XBA5sr>h>2!{8-Hu-9Q@Zl)i{7#M;z1Nf$x9cx_ebRH#FN2v% zrIuxu>p6xy9@NQh!y@f2 zsMgpaVVvOYU1sdlYqj2mKUYQ2N2ny^!nrq6)o%YSTR&X?{tuUNi$;=kFtE|2kU%E^c6zb-wNEaj% zu0*e$sd~a3F&>&x_@BD~{}j!eCf$HFa{?5ru@a5@2-f`yde!r-a)Go9Z@joz{;AnA z6SPuY{gqzxjo`Nqw!!gPbJI_*Z`WUV98@8t3wgQD-0V0FKFCk+>bD=`6&x*1i1xNt zOMQJlFY71%5xAdzA$7%fq#h*@87Mx=7XIa_jqCKHW@95nmmCeu&BlckZs5m4K3LSu zzrbyq|5TpYnjDM<;=kx9%?OgO(Mv%hL>u|(Fvn>dF3@z&O@8K6%H}1sFbFM-A`$#R zh*92?c4+<+CDCUI<2>+@jo;!T2LYIy*N!_YlzU56#NKX)?T01PM)##xnr&_^y^gP~ zejsO#UNO^4b$jql~n%t+cPFTg*f$R&Ht!|VX3uba9!gqP&6#l0qNz11o06Cj{G>OChoj}z)&wvI&LU?vng5~CG zCqY~S$l2SMA0@h+Ueg>sdcn&y2sm(3&?wcZ9?`0(Up{;g7g|`{CZqAX>U_9`JQ*{G zz>OS92hgN?1rRFY?+T0T9Siq#HU9|{7I;xg|IY8e^+XT9_Z2;$n?{;<;RmhR=SSUj z`r_L!+tK8QJcC^>_9hwB&dw>3Dc`Y9}*Hd@~H zv7}GN3p*W9_0Di&A!il9(V?pXbDv7295Yui_ zFE^Jn81aC8d=3Tsv>Q3|;n|~vb&kMubz!r(gpZfB(DbzdbsNKP7pBy-4R~GSA>PG$ z1%wAOO;5PoT0jzE@a|7~)CxW$RF;I-AkJUhnU6kplE|vlq4UoG8r4twdtUrrL}6Hk ztJ*^R3clBOaB00cdNPqo_mv0U@r-(mG>52vNPD)*_C2GmMPf4hX@y}554gp9+O5V) zo5{~c627tv-IsH}{Pral|8E*HU;cWfMyKCe2Nd>~J{bB${HS#cT9?1GCdWMC=fF9q z9UJgWgb3{EhFV8pM16_EbWUl*?QMWNiYagnId!6G^+IUuqMWlJ*j!O2Eqr0Q?YWA# zi-bzd%E5+I0Fapd^BKwIAt3kIY{+Fr(pi`yv4A!1T~QXY)RdIM}h9sk$M*H zo;RYH-{|~!o?@_B8SdLyd>=xZJ?_FN>fNNJ=L8%BSLBQGa!mZkL#O&5`O7s5zGw~sNh^2MK?GbyVKQFuhQQQU44 zTZHfSPxi@GPor=;do=+K=Asj@-w!(chMIQxK8*)6N#D_o*_$a@1<(8%?$=5uPAyrgAH$!Z&!nZF%g6###uKB#{N}S(yzQ=&*b3 zSPW&nM)LGriz|2FZQ7Xiu>YZ{`@X*$G#sRWP)Y9@N~*qNm-d=n`y*V6ks0i2G(Z7* zEwK<5O~A&zZTZj$Z~s;F)(u~~tiu08*IP$b^#$+4m+qF3R2l?Cx;vynx!Vcx#u5zG0%{ z8^@t^ucAM9vRfKI%4yhDYSd)9yBkYE?)M6bue@q_nRbmu>JUk?%*?3+o9KRv-s(CB z#=IYpN%jT~C7vIfO;>TxI`(04V({U@dhTFFIUO>S{XSYVA90lKjl{<)JM4O)bKlQm%KV0hwOHh(ut0nL z^D!(%aa2s@bTLca#-DfBz{igDbhTEVNeBFRxBJG{Py<_nr0hiRwshiCtEdC-@z-z4 ziOXD|Ed(ImLe=%C(}K`sK6b%1oqwp-dn-I zKZkq9o+frIU^7yaid@dwqi8lNAW6i6z3;XAwC*5lIjLo|?SRP24kV2Z-TKE9s0&V~ zJAM}k7;hnU2&!;Ycp~u&`@2X*u|-9kT;35Fsa%6E?+qGl)?P5{j5`!3dQ2gw)6Ml)KL7+eD1 zh5%mr$|n(C1^B_`#P@|Epn2K#TxJDdLy1N+2VY}5eTit8DyZ_&j??~dfxv7qso#RV!>R9%Gwou3(N4y8 z#R|>8_d+nt6&U!-Cs+J0vCH~E_qq{sEN?QFXL&uaBW$lx1iY-=5^40p7dkWph{WVR zE6~(STF^@Q0#2>`Vrb*x3y0gma*#U@8Sq=$LL5w67u^3-i#225O{SR1w=v}Emj3PC zW~~QCC$0)L*i7D;+cW%gYu64)>Ls}VJ9K7;J@Ao75$)&9k3C9I?mIB&xIG83Z<_?i zE>VVtzJ>sYY{<-Drx;MD6c7Ch4S47H(zXFpmzfA|lNrHxE^U$txW^)m--3u;p<{Uf z*@xh*0c`g09isXuUh#3?hiT2)ch1{#U)e5^J|g)3C4MS4{2U(5{ologeT1st=`);W zbl|&BmnnCL_K9VT{?z4i`#yqThh*l#O2>^!bUmVi+6t3lQ~^v47DIB~4!+Zo0yn`+ zy@}OK{wkn5d=u6>uk;f=iTkHAR{z1dFvdzRh`$*UZ+LL!Ph!+8HYZ*mIM?7sviZP@ zgwJ?=F*?2;O0IQ%%4m_@vOI+`+Hs)EvBxibGOR*$ZXRgyZ?`HkC|sNid*}5a>qpiq z1*FA_0TNN5EcjOe7J9dx6)Ls8Q$@4`?T&-|hd*jKZB7I37K^i25)cFXWBI-Bcz!in zrWz`h*2){AQ+)5tegV(=SHr>}5+j0FP{mOE($#)hGNiee2n-Ja) z%aR~yi1aUF==91fuUaCMILJ$Q>bMqPtuJn(0#ad&_xabHvxFFX{mXMmth+JsJg(am z9IfRHe2e2Ky^(~czc#bQA6G`w4}VbXJ&5#=kiGMq+{<12YUmKRCp~bBFODJM8bCuz zLE7S&083*Spy#Ecz~NxJP_~;Eh0$5rh4EA2q_}8Ia+r00FzAhG)~4-lkCW$G?(GT1 zqFaElz`4om{{8DwjS8(uABTN#04AA4f8v3EuY-X-ODu(O=)ZfPYLlJI*?Jd}gvb|N z9^V>rZFyZ{cTy_mA}RctoBT_~G6ewC+6U+h$Sb_141#!S(N4pzJG)b3XPA3jzj&LQ zOO`HlNUx-)!zOm}Qw$&#WPjIfjjZ;wNt|RFf1I|l+r0i2GJuxbxOC6<@R4+$Rwz}m zxJ}?}mz7zyfL@uC79+Xp{tX9($_@KK5QDLUANMK z;bHM_4Hc~3Vn%deik3rT-}4YXKcP(Tg+Fjg9e2SniPL89ngaoRZz(lWpvFFg*Sq}G zEA}RDcK3`3+;tQoVnVWlhVt3rXGr#= z*$F`ZclCCP-TsxSowuRreGCTIN< z3RAB`*q<83sAq|&^)mx!TtH=2N{#w0lMnlZ@Z`UTOA*BwyG_)SK}%&E3Kj$$w`oTG09?G?^a;$IIK-` zixR2prCKA{QC~Rm@ZsLt4a~?mh5WNI*(j;y=|AzCrg^WCy99aPNNg|(@+03_uR9=M z)|vF$y4>SMJDO*&G=5yVSG3Tk5+Vm~Jh7#gxxQxKTL1Z=^jgzHTH?UA{hc?fPJ`8s ziX%xW|8D^TTO&&h%olv8z+m|mrQ>Tb4vVxpK`yRK^j!ttewC;k&>C0(a`@505r&AU zw{}6hoMo?-hP%^}Y^vR-bizdliZx3;3Uq>p0yqiEDIQ27mPv7z4N-2OqX|mNCqT`* z)xPaTq1;O>hB}8)mIWVO>2lp$o7_3ym0Y^9!*~n9zD@n+JGzDCbtzK5D-ZwFo*oHw zl2hLAd$SCNJOP3ZwB);9ZxpixjBgaYw(eyt1H=hc&q?{5NhwB|$^%a(gNgGI?`DAF zJR^M7P+!}mVpI+Rd;ymax=!$+WM+%e<->j4$HAq9@aY-ygTpD}JD_Ja;vGxMzj>Ek z={hoJAs&2wiPx9z-F1gAw0>2a$7G-VZcziKLvRLKKdvH5u?&ZQVRCT#)GL!*K`H|OBf<+Pbo@}v$-m(4jlAlr`Ibqb zuqS=dVOy{)3<4^Dp6nGA;wC1r#0Cqlq#`!_uu@NC z;;H9mwQ=((^;z`o-3eOT)ui}r{l_;aCjy6F)Lag|ztoSWTk#xKCN{UdZWZFzhmanY z4&8+0lKGH;17M8GdeQ3I{1(q=wsZwtS{bHs$tZ8k0INFx%k@#^DQG#Yy6_>K9A<%r z1_L#;l{Q!6`lP9RX2)ntJ_Ln{jGef@rWVb=B+uctH1$L>^(P-}&3L2>{N;+Z^Q!KF zGky!$PQNz91~mlaUhfF>)`0Fk9)IKEk0pH{$G=A^89;^f1fj$K6WZ^yaawE@(&3+L z(4aSV$5wSnZ6pkP4{F`y`tyzcBN{Qu^N%W^zdoGHwH0UOp~+ zue*q;xHlrpt3d1IsZZV6RPk}H;ESZh)W&7Aamx1+&%g`<6K9;+r{+BK3v0Py z2Jf;vV2(yeL^c^W?pCdG$2yT}gwl88yTj}UdTv%@5(#(`)G=t-K^|d9rwt@}^-9C= z*!^QK&etLkYO|@^F<`?31(dNHy6|DePeJ8K6d?>rx5KOJ1j(;TSf*HEobbT+c zDJ!qc;&Yoj-7rO=LIXzFZJSKoQI21OwRL-6VCj#e8U0A~xGhE@Dr$ZkJ)}4$K%0J{ zADeh@64s#tN>2}AwKMY7{`82z3KB7TS1^Q;UI^^vf082Q9cvP!UsOZnL2D9+>Y%`$ zao=BIU!=CS0iLQ4%8xq#uY0CvNe#mfzsU6Gu z+1fyPVpoIqnYw>BHtD>xmh~Y^1OP)Bn&(4K9gGEK(zJ+oM zxL+-kU7ExF60a|8luEN(vs&_;Zt)3bxqa-}UPKSLISPb%z$x^{iN{3S#zI)IN-aP9(# z2na_(2Mlh`&c|V8r^x!iGyzAG%y%<@D`>Zg{!hP_FJJ{Vhn6PK;EzmdG(YirB^35vjvP?%DlDv3?Fhr8qs2Uy%+Q{rx5e0&Ljegi8S&U{uI0qwo{$ z@5{`?-I8Xy$KO7|9aZi$dLD)Nt1X`nGx|E`hBi`;aTDL&Q+_wp@TD|%g7E-e(wkP{g~7% z;_*kUYjbmHal`P9w+ zL^K<`*$UHGE}x8ca*zw!&xc>$%kH2p%ZxKEQ z+(U`3!c>O5wsp{CazPF7q1hy`@V83Oz{f$OEt(e}Xx@5Bk`6A%XCm(95nATwwEe9G zaMqcciJU(0d1^>SLqz@5O)d2UO`P|l463dx{01V&rrV z3{kWLgR#TInHb!xCKv>C+F1hDzlm+_TeQ>Qf5-qJfjeK4A11XZC_u8G((~HxfacT% zcW%H^GU4-{^sBm&Asb-MX#uRo_X-vZQ-4-9@WcB6wQqWene!a6X4io`Xqq2yvp$7r zl`8wh$UESv-%!!MSOXBuSb5A)m;`@!MmqodBCL06KFw(w!PPMXl7;1& zq5mu9gwkF6$BLd^{te*r1t?lp=To001^A1-i88eOMlT632HHsh#IKA{0RH`CvI9mg z09dYwFQzHn%95n}H85by=ycdd4em&|?BDWum7U-21Kb0ZVqp_8&>;K}lv!H;N~Hd7 zh#{!8pf8q2OTF6MI+X4>eNKpKVj8A6e^%-U?A2D5VAMb1Dqv2dSj(BTMYMQCC4f%r;O>FTZ~kR})@qZhL9T7D4GS56 zgEBTtcW3hu(bN+Q*iWS@u(Xma-*myqMRgdE$Qm}c=4SVHpMl^=36lhZ--RB#C(SyE z`b(k<5D3g<7cMgTPMDCv``^IJ8}V3i!+`UU=e0)e-4}tiBm**-R8G>W+gW0sWvjz) z(&t5>G5=PkWBCrdU+b~?xTxeDlX)pCAe*x`uKUf)$~=1EGM?#bzrMRpjQeCZ)gc@x zy*p6~no|5H7bENg5N`fj*Gb0sL?Za|f|^N49oKGfSZl-(@+^U&M&;xSxs|2d(_i@g z+O>5+#4{bSHgmXK_;mz2^|2FEgApxkN<0be#r5L=WOZcfk%)TeG)OJRJ+ah`uliG-jloj8AId4v?+@baL^E{*&Da@>MeR- zUgiTf0Jp(XJI}5&GEQxjEW%UKanrH;5hnXLso_gZpT9zu0IA*J6TQCh5Jp;|AFd;L zn2@t)&26R;gRAkrtmWK4-#-))$yB3q8h0UX=@(hN8u+Pna>=^{75!P~aFH@F|A=$w z4u798m3lf&$|y=D(GJ^Qe`C^Ej?va0;AM%&UBGP@{Np@AImG}>0>poQN?bRKvH+aa zg7vFcHj^P|9Dv7uueWS;Q>EnVYyJ8&^_j}=h&}1w+ZhP?o`97c0V_eicvv#1m0RNI zO%)IjPYKRAki`$Y(pWy!wD0Sr6ExV1k#!hd;_0gcC5y^s7?pTxo*V580jJB!wi<+A zGZiEem;a)Pb9{2eL>>CB>wDGaitwNVNRdRdkO6Z&lNdJps%Fi= zLKz4};CpSq#oKyx5p@tQ%l_9+#ar)@^KNuOR3n!&lg>JJ#j1oO7PWKq3Uy!()^e!| z(OR9^1`RvigB(&*uUl=iENpQht)iwryhmo|{Lw ze~p3i{#6h7oB84PGx1kamm4Y;_$nKXtzvg?(z2UKI-9D$I1Xb;B%D%(p%}oFwq@rp zQejV!&&>DUEY5GEM+g6QD`Bf|7@>2z4>}V5?6W2NPjW_@*ut%k5;G~*Jg`Ye1NQ!o zJkVd53*z%8s$Wp48Sk!DsaiX;4jLQkIB~jXm44R;$jb_q7szEki>nwMn_?sfL60IK z^>6b0DyU5d`m4)oRzMjwJ{d=wVbs@Y3MV?SzKZ~N{lpaNButio zWNT+vgk)5nS`s6@_Ksd+iSm*a8H-LCb!0LRNrVNyq?lMkZ)vE7PqYi9l$E!LMj#_Av2jA9l4~oI z$gI8q$fTWz97*^F)hqey*A=FO^z<-Q-e#VMLm5(DcT|%NLB}he3ksef!8z%S(K}lF z)c5awU4SHJO)Q|N0_5I~YzXk41i0T;rj)H^A%}*1z(=5v>2f`>YSpw@h* zj*A+}=aVDW5l0S_Xj?K{K|h-i)+VejdCmOp-C@e={e0iq%+>Fapw;=0OS|9by3V_g ze2nQoMpsaT_6^B*;cW~5OW$nvvVKe7)W5UIg}Za6vIX3y@V{;@Fa=ASB;40sKNHvK z$Kg<9%eXDHlRb1*2&j~X2YB};g8>e@ALQgl+jt0?c>(m1G3e=hx`S582FiVPll<9E zE~1MdD+f|%vi46wb&FW|t1{Ko3{+??ha6!DIA4TK^~6l2!1A~{ctn4v$c5`m%Y-K< zsxPn|@8lI!Q&$Mdt3yR2P}eV~mz7eiE3M}^B=sHL;P6?NH)F&2i(o9HiNe@|&oh9dLR>MI!hoD57`~n*XA*wWC3o!% zhs#oWHiyhuH4cNm6~+ziBvJSj_rza3=tK^QuZ~S5T^sf_a5D}v^P|_g5%k3MI0JFS zBsXS{{uTRj{UH(L0l2V|YyqZ!Ok94md~*)d{Y;6m1Y*nz3fP|)EQ;Obl(*0q@X zK;vkb#NPiXBJZb( zCUC%g|0)q!L0l%q4SFS|h%5Gy8#M<8R|^~Zu`77t1;45x-^)%uje*W%?646WSee4$=b*86gT+6g5N%e}e$O@!e$_Yx+NiB`=G)|otX5O1IjwD=DNw!K3 zLbc8+q%TSWSYbh?8~WMqfG4C_anYx z@Kw~IfyAQl;UJtibQo!$PY!S;pcOPwHczCir|Azp(E8cl+ns|@F8})9Rf=ZPoD~D! zUq%;u9tz|<6O!!cm)lCh!N&=fz_Wvc}v*f5kL@Di_& zD8X|eZ{n)t)BtMlczVtqVg?lOoW%|>z6V;U5*3tEh}Ci)7`|8Wj?P6MJqIp=xGj1M z$q!B!OgDAXPgSa|z8pZvXc_RA`zlE>j z&#~dCpETv`Phk`T0vyj#1KWLNEHA%(P-+?Q956Bu9Pr!`*2qaD;C;fu`^d8sB&5hj z-wZT$V2mzV;rl!S`-HxvKuUd`;Sgcto z(cyIcJ+GgFr)&kDiw#2uFe7GBkdj)SwMA`cbcY=~nBkXOe7DI>IJ|Io#Iquf+c?9D3GY zim91wr6R|%t*3=3SN_7k2k~EnR4)N*zk;R2W!=Dt1yyN@__4$ITISptfxf}dGS^sM z{R`%~=kM&8^8KCf2j{3*BYPrHWp*f&(V1K}7$ogG*#kDxt*+12Ke?I5W=W-e(hoZf zo%VdDS9poT(_%kyh|&+eTsVKTy*@;&Jfiv>Nzs$^B7tx%D13IBJ(uAc3??Jzfe<{3*Jp6j^65^Pj zOb6Y*|AIfs>Qh1=JsS`Hx@2@t0trpaKK@7Ks$ZQYiJG3eQtY@M3*O?~dxhqSKVDz` z@9-{-*vFlY$G!jEB1DU{|4r?6(byN*1oIw>QP~92PRla9Tx^zBqIidVq`(0 zvWo?ELdXMr&k>@JQ0wFEti(hL91)|`<&Uj@OZgOMjCJYRw)uj(zk(&hV&}dWyYaHT zvkrQ+NuIB9@&~j_#F|P6mGT8QppK|N;i>@N9FJv)Q)S3t%z{O2F9 zXkj)n%=^`Sh*;B5AX)|62cud1^CnW_4~W0#qkR!;qfWkq0q6Ng6+VnyTUXx_h-%KK&6Ug2ljZc*IDV|lU8oA zS^G)uV&b{MG4+mR4}u1&`Y73%*hrY7yT0m-iBj)VXp16cHc$B*9Vl7M5fCx`vCMuY zf0>DK(KTns`6%>Q_8@QABR zaO&|n|L4{D3Z(gaeFA(OBLYLT*Z3RfX8rTpIdzt3b|2iIP4NoW9Oo5`EdlMXCtQQV z4gD_?TPsd~P%MGErP`T+LrO7k=}NBlsds||Xn9vCdD<-H?lVQ<$Ln#gE8}C*0^v!V zg0dSs8t|y0Lhoj~&RZ3y6&Jat8zxknY+954pL=g8<=G^mPVfpg84p;Dm!0t`lD&dm z!!}x4;RU^b9omOkF;u&L7muQ$gGG91&1Admj zs}^RmvCT3Umq|;t?H&JWt;ar0zngNvQj3#9ou!s|uyrqYe0qA+P;@{2hPsZ~-FQgt z_WUMIW~40x7>m}h%dlT7ZvU?5G(rWBzk{}_`0qJBa$F=H)Iq|*o&at~^(Qbrd1*M< z3ReDz?XwBpg!b@A*9($U)?ESfLD{h3B~|?{hDu9a?^Y$3M1~FH1}ABYe@C*+3-ZlH zR-w!YN1voqudLQzXR^HUl8o7osMN0yg7fmL082?e=}-dF04($;`K|U7=wJs5!2VkV zZl`I(f=nSQ3&FF>Egx(T=u`1H%1u79G!}o>YW>t&2Z>mJoRklYTZBDV3yqi^}v{f`1@SzbYU z=z@CxqF6!_UX3$fd}wd*z^#>JpcMz?BE&+*!Dj$bS{mG*ponilF9Y)_#jmGZoDuOb#H;i3y5$Qd9%1Ay`e0X`b zH>suE%0@2yBRR&47OCx>TEB*(JJ-eGV#vfJnfT8H@}r7dTmw5oP>9&T8znFm{LxKj zzvI3}7fFvS=Ib#$*lA+0w1J^0dnD?APSy^Xr-9q4>@!rWD;e;!=^ac1>e?u-pUK*n z*!Y}hI50_VzA#=-1(1`abr`9iDrO7B8fr3ih1P#Nx>DgwYq0$^MS{oKV4}&~y^xx{ zHd66|?6%&ZB&u?YMRlW*i&qRpN8SuBP@Uma*z8g=BpQpqZ^~5!^c~B8$@ROk__{xn zQ0I>YqxAEZmVjtLH4@1*NAm=lSg;ozTQ->L?*Ez(z78zD@jMcrM1a+q6wPl*+k?>V z3;5l@>vH#$1s`^XJxu?sB!CQ23B0pKcDG+Poy^*DDa7tv2U%ZFvuhXUqQ_!G`bUdi zts05OnPV>eLcn=&*l7HWz!_E0+M$6#hyRouV8b8*u<~yOCTI7_Mr?Etfexk{Hnapa zB*-bq7iMzk(GUUY+(GZ^-{O7OG1=rf5(0-<4C5w$3E}81>g8c^uI!ZcmHjHyLdVZF zs|^}jaLV_E@LV{!c;7QtoyNoPs4|jzNprnQRU4PI&Qvqh=}BLM zD+?B+eSaJWU7YBdOiDZ&U;h9imPQQq{|Lwcgc7C@l^m|8U6qj{65$&-BvGUD!samr z#e$H;Rd~OBbp0^N_d7b?dKpKPg=*rP(+qX>iu-eE`Ak+l)du7BBFDEXuyb>BSJnq? zliwa|Vi)~<9+V?Kx~=hG2GHt@hRAOEPE`GD3c?E{qoc^1B64nkJj+zg`UI>`=>pfkHHDM z&fT}2sGo7|lhaf-gtJ_c@8Jxm82)%bUQOK_5NcjbI<3B#uVqfae9XT#TE(&%E0m7w zJ?9q#SqKgHdF-La`U!j8VJ`=aYj*Ct&djWh6+qnt>b#afwnQcft0w#`(^pDJTaUHq zP+R}al4-vEVwb8$o^6OV<0MK)=;jeYs#?}ZdMJJ0ct6kMewU`|mCkMf z1;V_tp$4;I@F?x)F<%b*@nRyf_!$DZ1S)t|ETgWsKJ{rosTlL9ZXdM)PXIK}7M_^$ z41?6-kok77DW3?&>nA+~nkePu!NBMZ7Vo0952}|(*gGiTF@YWQI$AnV)DY!5)$tdb2p&=?pW!SMl9X_}&7KpG)te2<@z3V7ICMHUJez|f?(_UD;Im$w;bR3UA`D)rK zS)#z7&knG^Hv?5!RGdvnf$$h!B{LVjo1=dAnQCZkN~fQDI&Qpp3%hi^mW-lJK(l*& z_gE&_v_Te3Md^ES@XFB_$qgt1#VF^a`uv(cQComNN=m#9SK=`MMQ-8d)81ggVWZ?U z!UUaBCu~ea$^w!>12`Ns`vtj{B|N#O1ibg(s60ZNNxBOTrNwvgt(r$GVI_Zh^LV(+ zbbHIp3AkZ=?`%bbQ)f>8No29OYTwil z@A)_z%JhN9RxW#?&)5t8uKM7N9Fc`xH4PGgN}K;b((*(C(n`k{dq08ch};Sd$OXpL zO@xpH=DqoOY-;C3%BYIv$Z<<^i;C4~+brs#?=aawCY|cU?kmYXO-P zI&t&BiqxsEl>Uc1*f8->6TSr8`q`owFh0vz2B<~_T#rWs!6YCK!T|Bs>)e{pDZS#Z z_w#1(95e8VQWFFf)!q|Jl>6JAaCZ+)4B=l-ntR=_Fb~4v0%)v%*)4m(*8ACcY}}Yz z^O-czSv9iLFj7J?EEfT7dAgQDGf%*8XVal(z$?CaGDB|^_zj3Qx(Ln)C*LlKd{a8R zBVWG@NSthV9CLRAY!;hnbvVk6ok&LRQlbrD{L`EX2^L)cpA7c#_D_3yesoIMoceNeQjoF3MNcVSzw57PVqyG`!e0>y zXMT7K?Hmac36*e@FLV|&tgo#FHOu*{N6h2}*S3haC_R(DH1$dpWM40=M=S{rC z$8VTfO&lpm1)V{_nT?q2@`FL)v9)f1#9Qu#ztH%f;Yx%>UVzW*A)81a0ZchknxAHS zc6c+ZY^FXOTO9eCO!mZ9qHvIB2~YPeVVH~7L?g`NYH{K}r{*w}Ie?g~!^qD*guPjoeJ!e z_`n3-=b!Wmpwk<+<2_2)VhRvCd%q%3aJ-!V-zU(z*}vMIr$pZuy>k|q%~Wt;5RA)v zsYI8Ij{&@)NBJgw8#Hv z;8gk;q=j8j3Ho|F&17@f4?c*`}wLQ0;a*o zrWCBKva~`6qJ<4lCR4U%eMXM=$$-_Yx4MKS zkdRG({oE697 z+zNM>{gT-Zb;XeVr_w(^+PLu{DH?J4db$0Y6jjV3fIP5fEKMS#A!B*TUNJ4ELpe?7UW zq3<9Zk{HMmE3>42i`PH&#qSJE8q|EM$?X}?tpcD4I=b{RO=zC26T?u`L7RU8aRTA7mHDt;H(F_MbJa0hG65!G2CMIN`s}qzKwgPKX%aBad#*TO6J> zuqJr%fnRPCv(YX8cXF>gh1QU}$VdWR#;4-90-N;vo%W~w$RvQWDR8=G;S5WMJ7slX zMN2TFH+kvxiHwI;jT34H5k>e6*NBt}Y-Qc2_+3Nws%S>D^^~c`B^-u$Xzw#amaq~rre%5dVozsI(nSbk+q$GTFii`ySJ6{6x%%2QeAPWGl znsn&=)4><$D(ZUr)%)<`LBbGQ1d$mqHJ1nqf@1B&g991%@ zv&*RGS(v{vlRIgPPwP!hC*IJ(o&Ei(TJFcum8Kr1(GQs^v9{k&k*k=sqikCZ9^UzS zb8DGanZ`cla6>@a%7423VfkrS{Xl`*_?2cS5TAZg#3l4P&|5bz49?U>mXR7uB@~ZPtYzwB1l-v=7{^*LATfU;kvyj359PiixCl zpYtzT_zkh|=T zfI+=oc}X87$w+q#RKXuGAawO8Rh3P2~yEIaFbLt0g}v{QvGM)>I7+DV=9?OQ48hwOk&3bA>!R#zh4yl+0Oa#V?3 z_cFNdKwrA9ulrMUQcK}7t2=dOU&Zj@j2G%ZIYdC=QZE`Yy+s!KW~^e6F#sUUdtqnK z{#~7-GDK_44#5kDkZ_OwZa13luul4wBh$XMs=L_#dRPz$gVLjV{q?$7=rf~E5x;Ex z_09^w)L|UIkCOl&J^m=-PWBe%f4Bh5z;a;Z_t3mp1_OyR6#Aw>>qDMY0(RRfXL{&v zeZ)l4ldJI~)Akgj3;UlphCqlkbzDdZZR;w>STe{eF>Hn1ohk#f70#-g|0zWJ%;vN? zPL|Q)iDb1SmGPI<^XMxvKF(47z7FM^!J@V&YG=a|=}iK}&9VA2&wOuuhp{XrlhK#@3Nqb=i5%Vi8EzzJ>|47J zJ3-ZUql5m*k$+%L5Tgdalt1k}Q=mOt#S|7ptN_(xRT}t3dxwTc9}YsdIZd6a<5x@i z4|V=N3wrv>KZ{@d@<|D5ObPqNulH*LgPdluOUT7Zr6lt2CS!c&m#85+ZO_DbjLi)N zxzV1*I_ajQoA>9bKYOUiQB$;`#eDYEMo)`Z4vxp<0lR{L6?-!PowCzHk^@CMlus(@ z6C)9okK91tX(5`3SnfODUVtur*|kCBf#QsiMfj^hW#_F#)D0Q>rU0Mvh+@O_;S~k# zA>gve3FM`_g~W*$fmZD)e7DOICpT$n@C3d>_6G+9&9eT~JZ)&?v8nuC(wPB{DvolS zF_#uFJ@l#4IVBW9S%0&^RhP#Br#dVjSagIXU{f86h{`F}V5w|qQGkxh1UI+lF>3!Q z#tY@iRj*0-uwS6;V&Nv=`z|V_0I(dw=XBDrL0S6IV^WXtUdijjpz6%`0o!BlLqT@V z17zvOw|joywlC=z=3+cZv*e2+9u8G4}#TNjAqv6ATt zCvITqR2&&wuQKM-uAluEea%YQEKm(9FQw}PYMGuq;QPeSC>KP$t{3Y5A% z(q34_aIv}vGaIP)o*+@`2=}Cm3?+Q|PO&CNX%m9{8>=KX^+m4%=Gxg}Y zK15V{7L>IUilaf*IpBEiCXmu z;>6AC116W6b8$T$(+X^u7P0kN85LWEquAg6c1dj~a7V~&obW6M2S;Te5DFPJLM^-~ zJQL4DTQwP)e1wova1WH}_0(t+f9d@d8Wz*&`)FDyEVg;iVY;BuoBxM9J}V(;fyrHB zSz--)^i%qxAlHGCs`icxWlm?{&aw%UXf+tp^6AvgnIE7`wsjB;!TpkjuS0%8;=o!# z@nf?cuae>iM0tl2C3sZE7R~Gc=>vLR$=`SUH=n>Jxx$f|pe0_`0q!@1+0L|C)%2?E ze81XPqMYk(y1(t7=J{q$IOz4aM-e(5{_qlL(m6Z!A-|eiM96l}gJ|bfd1z1pe!x|u z*cAA>va*Unv@imk2A=TwWOa|tHJ9mlkBt~CODF@*8v8O0&!7S8>GS@`^yn_7Ek1uF zOGv0>vI>W>$aP!VsE)od2?al^FzbtuozQC*ZU_(Jp%tM~Jh0%ArXtJInn^!%FEQ6hc5nW~M z7=#hcQ2F-H*NNMJA4MpDh|yrRwl1Wsh-IDYNA|UjcMcGMP5b#vwVq;?KQf$1VH7nh zP@+eUeTskMI?z*nREkZGhWei@E>4edmc{qoU3i*t&RfQ-l%)NqZ726Y)gV(gfTtd` zM1addDA2d_x#(LMk}n*ra&Kj6vrG5)>kkRBE52f|&TQ-$@)AR6(J>SbGl(i?2{a-k zM8@J&>lo->ejylwfOONij&cVe<%8J!lm@4XuIF(e>qCzX#NBSQw zy`eG6jV^>uUfz5r_Nn?7?ox;&56ArWbQIjwLFS%z?u55*4B2F6C{7!rSBUQN|7Y7FSc3xjn36JjfrhmLxcVb@atc zziMf$pWc2kek5PHug(bE7p31i=~2evkP@}tXBBkHO!B1T0(NF?WBUV%o6($gRJEdf z#RcQz^N9rb#KjZ}l&WIGFS!?1|MxuzjcO~SFig_^%~n?uM~N_BL-`x+%F607puwq+ zG(tQ{k$U%K#SAB~W|~5g2Aq1evC^Enb*))19WCkMU+O_Imc?FkshJ{}eYRW_6Ot$C zLVC&lIXa6vXkUyJJ$DcG;TUI==4&Oz_ zaY-C>n=-XDetUaOHCn{?GgiXk{`AH7#TLW&0j6HbyG%3dzZI##jUn*8JImM1hLz;R z7r^&z=yqsFsl4==4zhUjxlwQ^h)5`Gt#K!|Mx?<|MX9Zk5S`qhk{B>5!2eT#068`+ z!~`ZL4P`|fZUYHU%dAviN0^DGDBW-kaHbIzt6Bic6~iYwPxo|(kp$}%8bBL$}k25uz2^RkP6jUzvnyTK7oS)T+fEF$X)jo!xH zvH)u5qeKr2H?LafycPorLk^%YqN<;;#?@dgCf4M+h-s`^m6t>GW9Q;p#U*u;@aU30 z=4*`>NbdT}*W;TKaFj%GX29nN(2Ood-j05ZxZwEMpOxOmJK!bId9(aFtAET;1D_9b z*ojEy5T1XTA|uaR0@&^A%v~7rJmB}B&>s@^1HvcrhkzE`p;{1y5G)zF&oqX__bFMt zCD9WTKyou?9h2FsW%Z=3)}x&0JPQ97XN<0x?In zi@i%}wT}#D)aDH7f0KKHx(?v`4|ss_lc;r1Gp=P0?+;+xv#@f6qlUN@!5h)aw03s2 zhOsOK1-|=HZP)GY!GOtoiI=op{j54wqlSx?ZjKwa|;Jp};t?EW9TA8CO1W6grD zBRoW7#*-}pM3&5)aSa^g?Aq422ju-tx<8hq$zn!hxgQ($tQIXqz4B?V2BiW9G(Zkm zC|N_NcbdT0eEL27nKNB)t})v2hMX1>?W>&((drd83YDI;G0^qwoI-#m{z?JP`4!=3 zi)Df8TNoZIsjzg|tC?uu9N-Ri*}hXA&wK1?PewaVC@}v7xt@iQE??IZi};ID9l|R` zL?Xw9Jj4|~ryF#H7WJQnG;L>*F>cQA`Lb%50-V?iN{RJMFv;rIhptxL6n? zVCOsI22Y#+rGl}=ETBB$0Wy(UXGVMGz~`FL9CY>Pno(_~+p!$~17D0P;oNZaT~FYa z-ohRW(I1VITS9Hrl?u2Z4QCMAZKdpYa&k*f=fsKSTh5Cqlgq$I?HXj>RNMU_Ie{aW z%lc#S$mN-8&-1VFRaM9Ckf%H#G#bD@i`Pb_+&t(IF?N1oY)}-DzG>ch_lw*-VGjrp z$hy>YSfTn#`aMoTv$)7w;K5$}|4{apL3K4-xA4Y-Lx5nxLU0KZ+#$FILI^IwU4pv> z3l72Eg1hU6;1=B7-CZ`^Me@AoZq@y>irQ3lub!)W_UJjsm<_%%Lm_NLZ~8L! znYXuZ^4BR4Df3#t2-t@`o{U&_ARIcHpwuJ`=ihrTCM=$TjU?Jw!n}8d_|O#%)%&ys zlx38k{pmmWsedw^Adcy2fQ%Ne@vPMx_l6!5n6^KnQ-GxunltKy_;paqI9DEUy)S?& z{kN}BiTyS;W<_PUUztEid7zr!kW-`a`i#TK0~eUF5}np82c=jqgSy-ysFt~l?de*4 z%zM5;%baH?K(Lzd=IBLO8kcY5)lqwv2;#>Qd14F5y?o9)xV8h9Zcs21WO;YOr-*PB zMsLmFMzER-Vy}~G2twm2^N^kRGT)lLAl<$KMkCwklI86k`05Cdj^wZ4SWPBj8A=!& zI!$w*(7^$B!AE?I2qGxkrnV(KXP9Xfqukvk#qlBP?u)FvNWO<78}h?}F)DBOhPK+& zQdugWlu7iYOCg^wic0XP5!O@ON5lIe??VRby9Uihz4Pn1W3UAq)yD zR!e+KwS6{jyJgL5SRIC{j4Rp?oH|He=*J`NVO>9$hhW^lXgvz+Aj?G^NQgFKdQ5_w zZr0;)N@e^ySZGHBRH-6xay*y+&Y(EUpmI?5E2!jwwaRB>2QMg#?9$se z-p6QE7N0zct_bnCUMBUq_qCzhh^5_2J|Y{bR#sN*gmt$JxDb}V(^pe@dq-#Cv9@_= z8Cke)e=hzfxUK#rK02JZiz7|;r@-A6p1HcOm`D8SjCqwQnO5tjG{)J`^{{}09~=`?blS7gN=^N z$G(!+Thq*yEuhb(=p6$=7}oaXDRFcr6H@cw0XcyUim>(y%kq<>gbBeueFi9Wkn%LT zDdsgJ`Po8zfa}fYB#Ru*JM4FZiM4TK@9U_x3g1yZ~takz{HJtoi|M%u=P^_ z;k1!Y9LY#PjBvRKHvY+*Vd`a-?I)1+Y~M1*U9=~AhvD&K%yC#u=~Do{gH3WXa^OKK z>30@E5STOoOuw}^rRVZ0FO2;>E1ngZ6s@Q~H;p(o)S?uhgd$Q7v<6{Sv7L(H!}Ma z0*4LcWCVo)reIl4{Iu9+UdFpGDgpyL=J$zCZoG$&b`?<|_JCylGYMVKoF6Q-E~T~o zW62+nf$JxX?w%bNACl2{fjf)4I?6~3upG2<`qOKy_^!W~1`l>4FZ!%L_Yr1{k#>Tet-B*@kuQRw{npQ%P zc89S&IAuGxT!L3sRn-wszH6Q#DYZZJdRcAewU7jMx_r&vZP8`5$6ojVb|_G(f6acd z5D28*%-P38a9RP%^^8R1=hyP|z&>L#dG6$ z9!@q_cOSx7EhM4fM#}xD2pl%q*}oLkTJ>T^ehp2YF92L7DC~3PwZ)T{G3hZ#!Zuf9 z+jGz3`~aPZ?nVS`C*{VoRCsvPgE~CZ3#(;}4*^PGi^-e$J+AlnwKgz)u5+7*tsCy1 ztE;ICaP|OX)-g|Z3V;y3(~OFdgF4QMjD_6nFC~aO<-Sp8u!p*D>p?HGZ)dOCn?}?A zbw}bxi}%oZek>5ce3@cXm*$`iKUH@8Iq({c$SzEs38E}59TW>f!SdFX^lHZ9CGa}d zvjicFMPCIse~P-j{cJjJYa8QKc=Vi{i2IucbZi-avZAcF zS@vs*#v^6^*`Ss?7svS)XBcT^i+2-SsI{S7_+4lufka7bcC!zA{(Q!h1;x}5u6Q>% zPa?WHf5g=HA&JupQMbZ4&`wW*turpdM%#SJ;-6yeSw$AG+4-u+D@79YVL@%Nq9evj zMfv&rP7q(oz@=(ws-=6HXy(`YUcD8Q`aMl){+^lkX~^puH!l}HSq&p$ScDqTW{G>% zQ!+wP?-GW{`jvg>V4|O`^o#~o?F>It5O~9czrw{ugD-sBlyizxsCe^jBQl~28OiTC zeBg;Qoey9iMu;j9s}+>_O}&()!U(M(qPbJg!TKuH2Z+<$wK?ftj#OrjG+Vu*RVohI z2#$>gvBM|xo^rZ*b~62w*tS3Ko^wyklA+nmjuqqnq9Y*IJcdr-K>TilcH*1)IHc+$ zY*n6bclapV4L^h%$*Se7g*wIgfAZDRfAZC`Uw)fN8k~-S1Q(kZ6q~O_R2q1E9$B4r zFxL#fh>F(LP3E6uu)63Q;8d>6SmxTkI}VmMD!LWBL&{hEG4LzjWkcC9&^>9xO=r;8lV;VP=beu8o;`BV&aD{+q}oN(P{YI?KZ~|d0nnSvg$R= zW?!kA1Q3$eri@Q3bX2UG_(qE=Hi650F@xX*zI+xD@7+OHMOe0g@tKzJD`?|6y*>+_ zt5xbw8?u@!aM_!hXWi^CJzki#PuwqMHwjq~c+j!52|mN-eumlsTM|lQTuw@>^9^(t z69uWr@x+~0;kuL{<$3w$+w<7dXNI>OI|Yp``*@Z0JBJ62w58>jERsvbH)s}GW)>xg z!KbP;UcGx}7zTYZz|f!AtOoDp%19(EoQp!6&w5ftM`5!gPj3~*Lq1=*0~tL6Wh))$ z`EX#~5>RuzRh9Zc?_pxtJT!HCF7zB?%mwg$6NXERG#avJX$U2za z2|on1Un&>X0&)fJTe7Mo5Xi_{~Rn*mJP^4tiT`C88_gn?b zbI~$|fgfA-mM(`qp~L7g0P-SE2bH1TQs>+5-m!Okx#y5}B0VM-w5*o486|ZLu+6qU z_l`?-cHM&7UIp=@LM7{5^w~hBPv+2MoEV`)li0h42$W5Af$FOctuhtwW6&EbPn8I-X0%T?aK2>xNO$|To+9-6tOvu+ zqjb4#=P{HZILHcY5QG?fMp>*DbQg!5Sx&>npDn)sEJfVs4Z9Rv2u6JD(MWY{gwng| zNdi2F7dPuCkJhhg@UO;*TNf5kv1yg$i7ZOA3c2U!=gTg)s<*Gi1e{_pwv?JOGsMs;`xk4U z`rg3az>{%s;O;%B6sq}-2f>zQ{fO4wweNYLHJAGdf zphri=LvVL>_;cqmEJI%s{d&?Jv9t#HIU54?-C@&YQ4OuGoqUhZfQI~YPC5DRbEN+v zoZS~j2}_)R1xfCp_aZM_Kzwt(*JczHlnN#@v3Kn-j>N|T!b$WlVvDam7F-BZ;%&t=Zw=acLHn+k?j%Yk#-cF5vm#%edtk)RN(D*8?!N2Sp+Sl>+ul z0T-Is_2fdqw?kRNW4J}{zTB?c+&AbVvKMLQd}|$m{#3l!b5HC{<1q1dAmxVUa9($= z$EPEV+LO;QVFs#1hAk}U=!9%hu#mlUmo0QZw+?ur+v!X zHCfvV&=hd9DhTos0G}LTB+oSDw4w#Xc$c5l*z+J;Ru`d`34Gs=lVXlieK-Ay0PoV_ zNc?lOd|y8@Mw&>vGB}lbM*(wxp;Ip){I!ci)h}j)a@ganb8+LjbfPa6wjWXAIz7Ub z=fJej-utqDd)`Vu)4TPLf^2g>v_?)nmp8Bg4#-mhF7T6r}i52fV$`k+-O?p#pNJ+`hy) z)$O}!9O&z$9hP$4UZn45CtZtJ`xBJyL5S=82Y1V}F?*Sa%gje1oh25|j3W40OS(R6 zue>UI_OJ_Lo&JXlKu7tx0${*+Awuu-iBd+)JdS6o+<|2{8>hW=XIY?%&i145etX#7 z=v;(@m*1qf7+1S5oZl1UOObn7FbQWHjh5UM9``KdLIJb)*_HXXF9tratuQ`^(8mgj<~;n+mZ_~cvG}-(AoK)Y>~bE5)`dY- z_^Er^-G$XmL46~Aoh1#2Z;H_$V5Yj_aEXZV$0>D!wP~sk_7Fa+BP>H{zB^tzdHm>| ziw5o1&jgN{0#-dmAssnGK@=$Z%iFd|+?roaBq#h`GP)-Ss8pD(pJbux zu`Bi-XnEfXiF=|PJ<{?@sZimBQxUy;6kOQTaEC|vuyAB=WANS^S1yBZ?)8qf5Fv?! zdA)S_tIDqZZVKCS!;Pyt>%)(cH#~Ox{uFfVcox2@G59^-Z&(SIipRJvQvm+OD`2(5 zL*2uA8Nx5FWcA9|;ib|~qP?J-S~W-%~J;n3rIEtuG*+4-EIt1td%>oN*;>lhs=oSzR z_Odq`nlC=Ft*qXRZBN#;_TTCHicQy@NkkqUdEDK+0BUQg%es?z5(m`=Bst2i81}Kv zH@hR#OqH&tL0rq+&s^rEXb_}w7J?kbMRiWk6Fyz^t7If=P-IT+k3C$Q0Km`UwWaFJ z7cO9{temPE^X%&eQF&B0qL6qu2O!U1=mNj@>b@sAxVzGx7}+Wg&J;y8vivgOOxy^* zO`{}0PrsXWiteQLbwileg#q#~pfVn1dYTtVAxUp>gxeSWf+Gd)(_|C9%Yw2#4w0!Q zJxJXnANipvPh2e|=CGr>XEsQHE2&*g4vb>af2FnD*bm7u=pHOlJthtS!SkVQUd5`h_jXQda+A;R+SG;eM zdBI_@Wx}~pil4NyJdmrlPjP^uQ>%cM>O9~c29Z+b--?5}JBwPy^_C{eP?L_CyF9a< zV|r;fuf{$wJ6!Kw-wna0oVNd@T+K3BQZX2lC&g@ioj%Y0s)s>5y%9{}{}l#a1gq7E zK~pTLP59##{-eqzkygI8HtfsTgmkju*pn#L!@+&oUi>7n7cy#*ht@fZl1h-%Umo65 z@2lO-Ru+ywZ}X+&&Wj)=Gr6zYDY5PlxA8Ht+$OyahYjKrqg;f-Y2>c>twIB8BB&$i zFi)b}n+FjKEu0*z`jDlIdXquXCzx!Ypeh`c{v^YSM^?0jQ|!Sob|9~Br`t#J)B;9W z$>A%2;;F7^IDVZU&!l>-*sxEu%AbfRtPqc}Px0=QIBq zzOyCO5N93@?3Ee$f{!_`{lf{oBZOdM zleeJ=7eL9fmV^GFDUdi}`pn_2#K4Q&Pt-q#k|}>k4&Rc}%Grj@*n3hKKBuQa)OmJW zTQ?Di$EMPr+{~72_t3(nr}K#z$;SZaM<$B}ayf)Wq6khGQ3uD+Z5f_KckGZ)<-GUt zvvHX-#+-)wz% zp?W1cO#70XPX?X-Kvkr_D^}#!A`D^Ko8G&G^m1swg4EUP*}Ncdjpb zTzDMEL2`G-;$&~V=KjhPHC48JiyP>`Z`p?VUja}+Dt|0t?ON4t-^x(&eFk?M2rpnB0%1d|G|lDD65 zCFGR~sSsVOUZ9KMuKkgBK`AS?o|~O{AgLD^V#CF+`rNPFcM%BO?_a#={z33a+?G(h z-(l{tTK1NS{oze_O<%s5k$AKKc7DLznZ*uo_7AH$Vgfw-6x<>0cjFw$Wfo*+iI%$1 z4r7a7;K%y&Ogx=t)(|D^A=CBtxt-Rl zQ|7HD7W)^8O|0hUjmD!*%i=SpYy~TwaoSbWmI`nHZ3H`)M$chMJ2Ys%*$2LKgoOT^ zZB~PlO!e6E)`u6ZvqSU~p7rIss+8rUCzUi3oKA9jVKB8{guXr@ymL6~$7PdcC)w#r z(>H1va&Zj0p^;r-@=(A{6c-4OOL2Ikxg4=_X@*mR{9QW9cMwMn9+l%jcBkKmycLdZ z(cGgK&k{}>`#dDDrjv+n9NmInwCr-5mwN}@I*|H@yQ5P`8S4sc+wCmDJg!C~rgLPz(pp%`Aj1UMaZ}18Gna*rwLVXE?uq8=-N`ue}42fIQvzf(6R9uXL3MX(D{S{HMMO&k!# z^OLJ?#w8CJTtDacnj7nzt3oc^Zhc4-GXBiuzvBjsX8a$&xJiUhqdIR9Q3uI-Eev5} zrafHU^RyTuYO5nHd!*dzorZ)DS6E1Ag}fy;(m7XO;VdhCg>=ikSV8vGXEA) zB;feJ0|VyR5;RE5@A(P+*yzBD7h7?tjU!D;JcYN0dBcBn%!?4+*zbhzGO1uCq&y|G zLFg!_O6%{|#2sxM#qZE5gV-fZg(qFSJZG9jDLiqq^b_-}`yDyDamBTm{eaaB1TS z39J?)FAF)!anm}apA_WkbWXH?1msk|qa8Zzgutf107~a_TG~&^I4-ztaw3C!WxY zLI{YL2b(XZYxzdlZRgPrwblwVA0+)8lHqkbehz@C?w#mdC1Tyjk_SV_o9&gXcve%) zT@IIgiKB{{XBTgyKsB+1zrocZ?0T6;d_Rzvug}(x8~S`lC-w)Ke|0z$HXvaPCe;Ev z;6n|%vuzAuq}(2Xzyg*?uYagCOt8+6SA!n{^RfKf2M39O&}kvCiPJ6o(e@Jo7xiMi z;)0OkLNz@T%u6&pn7UV}EOsh!F|38CTdnDwA`W^T+&RM~vKi~x@w?SJ5OsN6uxV&& zFq!I5^Ge6ke>nt#cFPKelqCyIegffyZNB2+!|Rn7q}OJ1OA~& zx{xs7&If-n{e|6)__q&ygW_F>JE@!xj4j%n$f>DAZD3n;yTirW9{0K4HAyFy9+IaJ zUDtqd)59c!0#!#FaQWt*O524rvD?6t4;mw>c;2f1&PR;eiC!UGK4Ev_q%WdYlz_LBjP~Uhk}%(QrAGm=9&Ljkk9Hx?;HFwP(!>k<3eoLe4vruXfCjz2a;eORJyAbjNj2?b1%M z%O9^VUi2QxB}n*V6o;M%F>M9t+T!RvALveK8&D8DJL99EW~}O zx){3W(PpIB^@@cBMq7+Q)H?6l`yaU^oXfX=Co;@0Yru=O+-dEaBg7YU%08b$iqCR)4s&W%cHU!)IKkq=paawpG&w>}7(gxHVRy6eJ{teYi^0 zXC5yfg0%9Y{cJbP?9`hx1TliO%*t7NJW}*CB7$FvIE*L2bz@%ni1J&iTj{8+XX zm7)LqxTxvq=s1zGBv7;8F_q>P+E{y3;A0>PsRHfu>DV{eJw)wJvjkt#vo6e(TyUMi^BKM8qF8VtS&i` z)HUa!K=2mv)7vM2gn|G7q_Ss3h&pC@k_{p4SIVZ`e@e)hjAzz&r#l{8qo#TC5(DRE zFSwMq-=+S@Oqq;MXjwgS;KWc4eY3>TUL7oIX89`i6|!ns?1gV{>?t|NtE3)3yo#w~ zi{<6z9m%wZKPO^BX!g;rolz{l?M`d^U2aC|gJ+L8=n5SPJ^*gF?c)4Is6R4`9CBd) zaSe@L^=ruZxX-S02%-iq$wuBKr%ChvmH-Z+&?73{kK1V??G)tc8eN@W)R2baa1y?* zP%{qeH8mv5-1(nb=iLQ&^;$X78Ct%$CsMWoNrcih*mS}UlAyV0_KBxh*LMWqsA0|u zpuVosz|p8_?ff|o%D1G+;@%I(e{`$nlg?%+Tbt|4Dx;*yJnqm}hYN6bj|P1%?Ts4F z9}DhzUp9{t0*Uw{W($Rcm?Si^H+;g1{xDtifd~=5DGLt=gQ<%ps+KYnCbRcn9-;D% zb=`xXbR575{>M8C&&JV-6H{MJ;YZf5&14wLZBad=tTpOiAq zhU5{Uq=6H^4Z6O^8Sn5Odb6@yNnO-Tei2{}%S6_|N51Wv+Rena0MOTeS`9zvK`C@A zkWj0}ykf)^(O%=E-@_r$el8}*uNnD8dZsjbUk<^QERe{?@MoN;V~uMjKp|cSNc~;m z0C(*5EA6MZBH#h&jwdW1)BtAC!6i`p#tr?&cKDMV)B{#bb8=aGh{@t?9eNBufWL zptqA4-Bcs!0w`u%5rp3Z1kwAvC+*!S1;FhuE6=(R#uFgRdE#(cF4fgHs;|>J6G2A0 z!K9{2*u)`IlrIj<&qqU9&F53D$bWFxQ`g0wb=^sS^6ZfUvE7;0!8ka zy_DmG<{uw-$c_WU6Y>6G&5>1UtLJ>k$5OrOB<6b}zxxI(l~3f76SeKUQMY^9W~*^` zSZ>1}`fh((OYz{TKX`y;9zp`}-bDmkV1dcY?f_VRjjDEZ#TIkUXJ}$Z(eaN$H&$I) z#KP){